feat: incident report form, API route, middleware shared-route + redirect
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { FileUpload } from '@/components/incidents/file-upload'
|
||||
import type { IncidentType, MedicalStatus } from '@/lib/incidents/validate'
|
||||
|
||||
const INCIDENT_TYPE_LABELS: Record<IncidentType, string> = {
|
||||
injury: 'Injury / Medical',
|
||||
near_miss: 'Near Miss',
|
||||
hazard: 'Hazard / Unsafe Condition',
|
||||
asset_damage: 'Asset / Equipment Damage',
|
||||
environmental: 'Environmental Incident',
|
||||
security: 'Security Incident',
|
||||
fire: 'Fire / Emergency',
|
||||
}
|
||||
|
||||
const MEDICAL_STATUS_LABELS: Record<MedicalStatus, string> = {
|
||||
none: 'No treatment needed',
|
||||
first_aid: 'First aid only',
|
||||
medical_treatment: 'Medical treatment (non-LTI)',
|
||||
lti: 'Lost Time Injury (LTI)',
|
||||
}
|
||||
|
||||
interface Props {
|
||||
zoneToken: string | null
|
||||
zoneName: string | null
|
||||
siteName: string | null
|
||||
}
|
||||
|
||||
export function ReportForm({ zoneToken }: Props) {
|
||||
const router = useRouter()
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [files, setFiles] = useState<File[]>([])
|
||||
|
||||
const [form, setForm] = useState({
|
||||
incident_type: '' as IncidentType | '',
|
||||
description: '',
|
||||
injury_involved: false,
|
||||
medical_status: '' as MedicalStatus | '',
|
||||
asset_involved: false,
|
||||
})
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
setError(null)
|
||||
setSubmitting(true)
|
||||
|
||||
try {
|
||||
const fd = new FormData()
|
||||
if (zoneToken) fd.append('zone_token', zoneToken)
|
||||
fd.append('incident_type', form.incident_type)
|
||||
fd.append('description', form.description)
|
||||
fd.append('injury_involved', String(form.injury_involved))
|
||||
fd.append('asset_involved', String(form.asset_involved))
|
||||
if (form.injury_involved && form.medical_status) {
|
||||
fd.append('medical_status', form.medical_status)
|
||||
}
|
||||
files.forEach(f => fd.append('files', f))
|
||||
|
||||
const res = await fetch('/api/incidents', { method: 'POST', body: fd })
|
||||
const data = await res.json()
|
||||
|
||||
if (!res.ok) {
|
||||
setError(data.details ? data.details.join('. ') : data.error)
|
||||
return
|
||||
}
|
||||
|
||||
router.push(`/report/success?ref=${data.reference_no}`)
|
||||
} catch {
|
||||
setError('Something went wrong. Please try again.')
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-5 bg-white rounded-xl shadow-sm p-5">
|
||||
{error && (
|
||||
<div className="bg-red-50 border border-red-200 rounded p-3 text-sm text-red-700">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Incident type <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<select
|
||||
required
|
||||
className="w-full border border-gray-300 rounded-lg px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
value={form.incident_type}
|
||||
onChange={e => setForm(f => ({ ...f, incident_type: e.target.value as IncidentType }))}
|
||||
>
|
||||
<option value="">Select type…</option>
|
||||
{Object.entries(INCIDENT_TYPE_LABELS).map(([v, l]) => (
|
||||
<option key={v} value={v}>{l}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
What happened? <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
required
|
||||
minLength={10}
|
||||
rows={4}
|
||||
placeholder="Describe what happened, where, and any immediate actions taken…"
|
||||
className="w-full border border-gray-300 rounded-lg px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none"
|
||||
value={form.description}
|
||||
onChange={e => setForm(f => ({ ...f, description: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="w-4 h-4 text-blue-600"
|
||||
checked={form.injury_involved}
|
||||
onChange={e => setForm(f => ({ ...f, injury_involved: e.target.checked, medical_status: '' }))}
|
||||
/>
|
||||
<span className="text-sm font-medium text-gray-700">Person was injured</span>
|
||||
</label>
|
||||
|
||||
{form.injury_involved && (
|
||||
<div className="ml-7">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Treatment level <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<select
|
||||
required={form.injury_involved}
|
||||
className="w-full border border-gray-300 rounded-lg px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
value={form.medical_status}
|
||||
onChange={e => setForm(f => ({ ...f, medical_status: e.target.value as MedicalStatus }))}
|
||||
>
|
||||
<option value="">Select treatment…</option>
|
||||
{Object.entries(MEDICAL_STATUS_LABELS).map(([v, l]) => (
|
||||
<option key={v} value={v}>{l}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="w-4 h-4 text-blue-600"
|
||||
checked={form.asset_involved}
|
||||
onChange={e => setForm(f => ({ ...f, asset_involved: e.target.checked }))}
|
||||
/>
|
||||
<span className="text-sm font-medium text-gray-700">Equipment / asset was damaged</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Photos / Videos / Documents
|
||||
</label>
|
||||
<FileUpload onFilesChange={setFiles} disabled={submitting} />
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={submitting || !form.incident_type}
|
||||
className="w-full bg-blue-600 text-white py-3 rounded-lg font-medium text-sm
|
||||
hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
{submitting ? 'Submitting…' : 'Submit Incident Report'}
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user