feat: transport incidents with truck number support

- DB: trucks table + incidents.truck_id FK + transport enum value
- Validation: transport type requires truck_id
- Create API: validates truck exists/active, persists truck_id
- Report form: truck dropdown shown when type=transport (required)
- Admin: TruckManager CRUD + /api/admin/trucks route
- Detail: trucks join surfaced in incident-detail + detail page query
- Inbox (HSE + supervisor): truck filter, transport in TYPE_OPTIONS,
  fixed stale enum values (dropped dangerous_occurrence/mhe_asset/occupational_disease)
- List: transport label + truck number badge in rows
- i18n: transport + truckLabel/truckPlaceholder in en/ms/zh

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPf5Rc8QPx6V8KLEEgfKEQ
This commit is contained in:
2026-07-13 10:56:07 +08:00
co-authored by Claude Sonnet 4.6
parent 6c1ad71643
commit f90bf4ed03
16 changed files with 266 additions and 26 deletions
+29 -3
View File
@@ -10,9 +10,10 @@ interface Props {
zoneToken: string | null
zoneName: string | null
siteName: string | null
trucks: Array<{ id: string; truck_no: string; carrier: string | null }>
}
export function ReportForm({ zoneToken }: Props) {
export function ReportForm({ zoneToken, trucks }: Props) {
const router = useRouter()
const t = useTranslations('ReportForm')
const itLabels = useTranslations('IncidentType')
@@ -27,6 +28,7 @@ export function ReportForm({ zoneToken }: Props) {
['environmental', itLabels.environmental],
['security', itLabels.security],
['fire', itLabels.fire],
['transport', itLabels.transport],
]
const MEDICAL_STATUS_OPTIONS: [MedicalStatus, string][] = [
@@ -55,6 +57,7 @@ export function ReportForm({ zoneToken }: Props) {
injury_involved: false,
medical_status: '' as MedicalStatus | '',
asset_involved: false,
truck_id: '',
})
const [typeDetails, setTypeDetails] = useState<Record<string, string | boolean>>({})
@@ -116,6 +119,7 @@ export function ReportForm({ zoneToken }: Props) {
asset_involved: form.asset_involved,
medical_status: form.medical_status || undefined,
type_details: Object.keys(typeDetails).length > 0 ? typeDetails : undefined,
truck_id: form.truck_id || undefined,
created_at: new Date().toISOString(),
})
setSavedOffline(true)
@@ -168,6 +172,7 @@ export function ReportForm({ zoneToken }: Props) {
if (Object.keys(typeDetails).length > 0) {
fd.append('type_details', JSON.stringify(typeDetails))
}
if (form.truck_id) fd.append('truck_id', form.truck_id)
files.forEach(f => fd.append('files', f))
const res = await fetch('/ims/api/incidents', { method: 'POST', body: fd })
@@ -217,7 +222,7 @@ export function ReportForm({ zoneToken }: Props) {
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 }))
setForm(f => ({ ...f, incident_type: e.target.value as IncidentType, truck_id: '' }))
setTypeDetails({})
}}
>
@@ -254,6 +259,27 @@ export function ReportForm({ zoneToken }: Props) {
</div>
)}
{form.incident_type === 'transport' && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
{t.truckLabel} <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.truck_id}
onChange={e => setForm(f => ({ ...f, truck_id: e.target.value }))}
>
<option value="">{t.truckPlaceholder}</option>
{trucks.map(tk => (
<option key={tk.id} value={tk.id}>
{tk.truck_no}{tk.carrier ? `${tk.carrier}` : ''}
</option>
))}
</select>
</div>
)}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
{t.descriptionLabel} <span className="text-red-500">*</span>
@@ -344,7 +370,7 @@ export function ReportForm({ zoneToken }: Props) {
<button
type="submit"
disabled={submitting || !form.incident_type}
disabled={submitting || !form.incident_type || (form.incident_type === 'transport' && !form.truck_id)}
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"
>