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
+19 -7
View File
@@ -17,21 +17,23 @@ const TYPE_OPTIONS = [
{ value: '', label: 'All Types' },
{ value: 'injury', label: 'Injury' },
{ value: 'near_miss', label: 'Near Miss' },
{ value: 'dangerous_occurrence', label: 'Dangerous Occurrence' },
{ value: 'occupational_disease', label: 'Occupational Disease' },
{ value: 'hazard', label: 'Hazard' },
{ value: 'asset_damage', label: 'Asset Damage' },
{ value: 'environmental', label: 'Environmental' },
{ value: 'mhe_asset', label: 'MHE / Asset' },
{ value: 'security', label: 'Security' },
{ value: 'fire', label: 'Fire' },
{ value: 'transport', label: 'Transport' },
]
export interface SiteOption { id: string; name: string }
export interface TruckOption { id: string; truck_no: string }
interface FiltersProps {
sites: SiteOption[]
trucks: TruckOption[]
}
function FiltersInner({ sites }: FiltersProps) {
function FiltersInner({ sites, trucks }: FiltersProps) {
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
@@ -58,7 +60,7 @@ function FiltersInner({ sites }: FiltersProps) {
}, [updateParam])
const hasFilters = searchParams.get('q') || searchParams.get('status') ||
searchParams.get('type') || searchParams.get('site_id')
searchParams.get('type') || searchParams.get('site_id') || searchParams.get('truck_id')
return (
<div className="flex flex-wrap gap-2 mb-4">
@@ -102,6 +104,16 @@ function FiltersInner({ sites }: FiltersProps) {
{sites.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
</select>
)}
{trucks.length > 0 && (
<select
value={searchParams.get('truck_id') ?? ''}
onChange={e => updateParam('truck_id', e.target.value)}
className="text-sm border border-gray-300 rounded-lg px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white"
>
<option value="">All Trucks</option>
{trucks.map(tk => <option key={tk.id} value={tk.id}>{tk.truck_no}</option>)}
</select>
)}
{hasFilters && (
<button
onClick={() => {
@@ -117,10 +129,10 @@ function FiltersInner({ sites }: FiltersProps) {
)
}
export function IncidentFilters({ sites }: FiltersProps) {
export function IncidentFilters({ sites, trucks }: FiltersProps) {
return (
<Suspense fallback={<div className="h-10 mb-4" />}>
<FiltersInner sites={sites} />
<FiltersInner sites={sites} trucks={trucks} />
</Suspense>
)
}