Files
ims/components/capa/verify-form.tsx
T
adminandClaude Fable 5 576557181a feat: Phase 5 & 6 — usability, compliance hardening, analytics
Phase 5 (usability + compliance):
- In-app notification bell/badge: migration 016 adds read state + per-user
  RLS + create_in_app_notification SECURITY DEFINER RPC; /api/notifications;
  wired into incident creation, CAPA assign/verify, escalation cron
- Incident closure: new POST /api/incidents/[id]/close (requires verification
  status + all CAPAs verified); migration 017 locks closed incidents at DB
  level (update/delete triggers) with append-only incident_addenda + UI panel
- Server-side pagination on HSE/supervisor incident inboxes (.range, 25/page)
- Investigation form: alcohol/urine test result + witness statement refs
  (existing schema columns, now editable)
- Type-specific intake fields: migration 018 adds incidents.type_details
  JSONB; whitelist validation; environmental/asset/security/fire field
  groups in report form; EN/MS/ZH labels; offline queue support
- JKKP 8 annual register CSV export (/api/reports/jkkp8) + dashboard button
  + January statutory deadline banner
- Admin page: user invite (service-role client), role/site/active management,
  site + zone CRUD with QR report links — replaces Phase 0 stub
- Evidence gallery thumbnails via Supabase render transform with fallback

Phase 6 (analytics):
- 12-month stacked trend chart (leading/lagging/other) + top root causes
  (lib/dashboard/trends.ts pure helpers)
- AI rising-risk zones: /api/dashboard/ai/risk-flags aggregates 90-day
  zone stats, claude-opus-4-8 forced tool_use, panel on HSE + management
  dashboards, suggestion audit-logged

Also fixes 9 pre-existing missing /ims basePath prefixes in client fetches
and download links.

132 tests passing, tsc clean, next build clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPf5Rc8QPx6V8KLEEgfKEQ
2026-07-12 10:25:08 +08:00

90 lines
3.0 KiB
TypeScript

'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
interface Props {
capaId: string
}
export function VerifyForm({ capaId }: Props) {
const router = useRouter()
const [verdict, setVerdict] = useState<'verified' | 'reopened' | null>(null)
const [reopenReason, setReopenReason] = useState('')
const [saving, setSaving] = useState(false)
const [error, setError] = useState<string | null>(null)
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
if (!verdict) return
if (verdict === 'reopened' && !reopenReason.trim()) {
setError('Reopen reason is required')
return
}
setSaving(true)
setError(null)
const res = await fetch(`/ims/api/capa/${capaId}/verify`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ verdict, reopen_reason: reopenReason || null }),
})
if (!res.ok) {
const data = await res.json()
setError(data.error ?? 'Verification failed')
setSaving(false)
return
}
router.push('/hse/capa')
router.refresh()
}
return (
<form onSubmit={handleSubmit} className="border border-gray-200 rounded-xl p-5 space-y-4 bg-purple-50">
<h3 className="text-sm font-semibold text-gray-800">HSE Verification</h3>
<div className="flex gap-3">
<button
type="button"
onClick={() => setVerdict('verified')}
className={`flex-1 py-2 rounded-lg text-sm font-medium border ${
verdict === 'verified'
? 'bg-green-600 text-white border-green-600'
: 'bg-white text-gray-700 border-gray-300 hover:border-green-500'
}`}
>
Verified Effective
</button>
<button
type="button"
onClick={() => setVerdict('reopened')}
className={`flex-1 py-2 rounded-lg text-sm font-medium border ${
verdict === 'reopened'
? 'bg-orange-600 text-white border-orange-600'
: 'bg-white text-gray-700 border-gray-300 hover:border-orange-500'
}`}
>
Reopen Ineffective
</button>
</div>
{verdict === 'reopened' && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Why is the action ineffective? *</label>
<textarea
value={reopenReason}
onChange={e => setReopenReason(e.target.value)}
rows={2} required
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm"
placeholder="Describe why the corrective action did not resolve the root cause…"
/>
</div>
)}
{error && <p className="text-sm text-red-600">{error}</p>}
{verdict && (
<button type="submit" disabled={saving}
className="w-full bg-gray-900 text-white rounded-lg py-2 text-sm font-semibold disabled:opacity-50">
{saving ? 'Saving…' : 'Submit Verification'}
</button>
)}
</form>
)
}