feat: incident report form, API route, middleware shared-route + redirect

This commit is contained in:
2026-07-10 13:19:45 +08:00
parent a40a0f7c59
commit 3f8da5bd91
9 changed files with 505 additions and 32 deletions
+109
View File
@@ -0,0 +1,109 @@
import { NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase/server'
import { validateIncidentInput } from '@/lib/incidents/validate'
import { uploadEvidenceFile, type EvidenceStage } from '@/lib/supabase/storage'
export const dynamic = 'force-dynamic'
export async function POST(request: Request) {
const supabase = await createClient()
const { data, error: authError } = await supabase.auth.getUser()
if (authError || !data?.user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
const user = data.user
let body: Record<string, unknown>
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<string, unknown>
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 any,
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 any || undefined,
}
const validation = validateIncidentInput(input)
if (!validation.ok) {
return NextResponse.json({ error: 'Validation failed', details: validation.errors }, { status: 422 })
}
const { data: zone, error: zoneError } = await supabase
.from('zones')
.select('id, site_id')
.eq('qr_code_token', input.zone_token)
.single()
if (zoneError || !zone) {
return NextResponse.json({ error: 'Zone not found' }, { status: 404 })
}
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: user.id,
description: input.description.trim(),
injury_involved: input.injury_involved,
asset_involved: input.asset_involved,
medical_status: input.injury_involved ? (input.medical_status ?? 'none') : null,
})
.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')
evidenceRows.push({
incident_id: incident.id,
stage: 'report',
file_url: publicUrl,
file_type: file.type,
file_hash: hash,
uploaded_by: user.id,
})
} 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: user.id },
})
return NextResponse.json({ id: incident.id, reference_no: incident.reference_no }, { status: 201 })
}