import { NextResponse } from 'next/server' import { createClient } from '@/lib/supabase/server' import { getSession } from '@/lib/auth/get-session' import { validateIncidentInput, validateTypeDetails, type IncidentType, type MedicalStatus } from '@/lib/incidents/validate' import { uploadEvidenceFile, type EvidenceStage } from '@/lib/supabase/storage' import { sendNewIncidentEmail } from '@/lib/notifications/email' import { sendWhatsAppMessage } from '@/lib/notifications/whatsapp' import { createInAppNotifications } from '@/lib/notifications/in-app' import { getApiKey } from '@/lib/settings' export const dynamic = 'force-dynamic' export async function POST(request: Request) { try { return await handlePost(request) } catch (err) { console.error('Unhandled error in POST /api/incidents:', err) return NextResponse.json({ error: 'Internal server error', details: [String(err)] }, { status: 500 }) } } async function handlePost(request: Request) { const session = await getSession() if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) const supabase = await createClient() const since = new Date(Date.now() - 60_000).toISOString() const { count: recentIncidents } = await supabase .from('audit_log') .select('id', { count: 'exact', head: true }) .eq('changed_by', session.sub) .eq('table_name', 'incidents') .eq('action', 'INSERT') .gte('changed_at', since) if ((recentIncidents ?? 0) > 0) return NextResponse.json({ error: 'Rate limited — please wait 60 seconds before submitting another incident' }, { status: 429 }) let body: Record let files: File[] = [] const contentType = request.headers.get('content-type') ?? '' if (contentType.includes('multipart/form-data')) { const form = await request.formData() body = Object.fromEntries( [...form.entries()].filter(([, v]) => typeof v === 'string') ) as Record files = form.getAll('files').filter((v): v is File => v instanceof File) } else { body = await request.json() } const input = { zone_token: body.zone_token as string, incident_type: body.incident_type as IncidentType, description: body.description as string, injury_involved: body.injury_involved === 'true' || body.injury_involved === true, asset_involved: body.asset_involved === 'true' || body.asset_involved === true, medical_status: body.medical_status as MedicalStatus | undefined || undefined, truck_id: (body.truck_id as string) || null, } const validation = validateIncidentInput(input) if (!validation.ok) { return NextResponse.json({ error: 'Validation failed', details: validation.errors }, { status: 422 }) } let truckId: string | null = null if (input.incident_type === 'transport') { const { data: truck } = await supabase .from('trucks').select('id').eq('id', input.truck_id).eq('active', true).single() if (!truck) return NextResponse.json({ error: 'Validation failed', details: ['truck not found or inactive'] }, { status: 422 }) truckId = truck.id } let rawDetails: Record | undefined if (typeof body.type_details === 'string' && body.type_details) { try { rawDetails = JSON.parse(body.type_details) } catch { return NextResponse.json({ error: 'Validation failed', details: ['type_details must be valid JSON'] }, { status: 422 }) } } else if (body.type_details && typeof body.type_details === 'object') { rawDetails = body.type_details as Record } const detailsCheck = validateTypeDetails(input.incident_type, rawDetails) if (!detailsCheck.ok) { return NextResponse.json({ error: 'Validation failed', details: detailsCheck.errors }, { status: 422 }) } const { data: zone, error: zoneError } = await supabase .from('zones') .select('id, site_id, active, sites(active)') .eq('qr_code_token', input.zone_token) .single() if (zoneError || !zone) { return NextResponse.json({ error: 'Zone not found' }, { status: 404 }) } if (zone.active === false || (zone.sites as { active?: boolean } | null)?.active === false) { return NextResponse.json({ error: 'This location is no longer active for reporting.' }, { status: 409 }) } const { data: incident, error: incidentError } = await supabase .from('incidents') .insert({ incident_type: input.incident_type, site_id: zone.site_id, zone_id: zone.id, reported_by: session.sub, description: input.description.trim(), injury_involved: input.injury_involved, asset_involved: input.asset_involved, medical_status: input.injury_involved ? (input.medical_status ?? 'none') : null, type_details: detailsCheck.sanitized, truck_id: truckId, }) .select('id, reference_no') .single() if (incidentError || !incident) { console.error('incident insert error:', incidentError) return NextResponse.json({ error: 'Failed to create incident' }, { status: 500 }) } const evidenceRows: Array<{ incident_id: string stage: EvidenceStage file_url: string file_type: string file_hash: string uploaded_by: string }> = [] for (const file of files) { try { const { publicUrl, hash } = await uploadEvidenceFile(supabase, file, incident.id, 'report', session.sub) evidenceRows.push({ incident_id: incident.id, stage: 'report', file_url: publicUrl, file_type: file.type, file_hash: hash, uploaded_by: session.sub, }) } catch (err) { console.error('file upload error:', err) } } if (evidenceRows.length > 0) { await supabase.from('evidence_files').insert(evidenceRows) } await supabase.rpc('write_audit_log', { p_table_name: 'incidents', p_record_id: incident.id, p_action: 'INSERT', p_new_value: { incident_type: input.incident_type, reported_by: session.sub }, }) sendNewIncidentEmail(incident.id, zone.site_id, incident.reference_no ?? '', input.incident_type) .catch(err => console.error('email notification failed:', err)) // WhatsApp alert — fire-and-forget alongside email ;(async () => { try { const phoneNumberId = await getApiKey('META_WHATSAPP_PHONE_NUMBER_ID') const accessToken = await getApiKey('META_WHATSAPP_ACCESS_TOKEN') const { data: siteData } = await supabase .from('sites').select('name').eq('id', zone.site_id).single() const siteName = (siteData as { name: string } | null)?.name ?? 'Unknown' const { data: recipients } = await supabase .from('users') .select('id, phone') .in('role', ['supervisor', 'hse']) .eq('site_id', zone.site_id) await createInAppNotifications( (recipients ?? []).map((r: { id: string }) => ({ userId: r.id, title: `New ${input.incident_type.replace(/_/g, ' ')} incident ${incident.reference_no ?? ''} at ${siteName}`, link: `/hse/incidents/${incident.id}`, incidentId: incident.id, })), ) for (const r of recipients ?? []) { const phone = (r as { phone: string | null }).phone ?? '' if (!phone) continue await sendWhatsAppMessage( phone, 'ims_incident_alert', [incident.reference_no ?? '', input.incident_type, siteName], phoneNumberId, accessToken, ) } } catch (err) { console.error('WhatsApp incident alert error:', err) } })() // Embed description asynchronously for future similarity search const supabaseForEmbed = supabase import('@/lib/settings').then(({ getApiKey }) => getApiKey('GOOGLE_AI_API_KEY').then(googleAiKey => import('@/lib/claude/embed').then(({ embedText }) => embedText(input.description.trim(), googleAiKey).then(embedding => supabase.from('incidents').update({ embedding: `[${embedding.join(',')}]` as unknown as string, }).eq('id', incident.id) ) ) ) ).catch(err => console.error('embed error:', err)) return NextResponse.json({ id: incident.id, reference_no: incident.reference_no }, { status: 201 }) }