feat: delete + deactivate for sites, zones, and trucks; harden incident API error handling

- zones: add active column (migration 20260713000005)
- sites/zones API: PATCH (toggle active) + DELETE (blocked when incidents reference it)
- trucks API: PATCH + DELETE with same pattern
- admin page: select active for sites + zones
- site-zone-manager + truck-manager: deactivate toggle + delete button per row with busyId
- incidents API: reject reports on deactivated zone/site; wrap handler in top-level try-catch so unhandled errors return JSON (not HTML)
- report-form: parse JSON separately so HTTP status code surfaces instead of generic "Something went wrong"

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 16:05:36 +08:00
co-authored by Claude Sonnet 4.6
parent 370b985375
commit 690485f74e
8 changed files with 307 additions and 30 deletions
+14 -3
View File
@@ -177,15 +177,26 @@ export function ReportForm({ zoneToken, trucks, initialTruckId }: Props) {
files.forEach(f => fd.append('files', f))
const res = await fetch('/ims/api/incidents', { method: 'POST', body: fd })
const data = await res.json()
// Parse JSON separately so a non-JSON response (HTML error page) surfaces the status code
let data: Record<string, unknown> = {}
try {
data = await res.json()
} catch {
console.error('Non-JSON response from /api/incidents', res.status, res.statusText)
setError(`${t.errorGeneric} (HTTP ${res.status})`)
return
}
if (!res.ok) {
setError(data.details ? data.details.join('. ') : data.error)
const msg = Array.isArray(data.details) ? (data.details as string[]).join('. ') : (data.error as string)
setError(msg ?? t.errorGeneric)
return
}
router.push(`/report/success?ref=${data.reference_no}`)
} catch {
} catch (err) {
console.error('Submit error:', err)
setError(t.errorGeneric)
} finally {
setSubmitting(false)