diff --git a/app/(protected)/hse/capa/[id]/page.tsx b/app/(protected)/hse/capa/[id]/page.tsx new file mode 100644 index 0000000..a689504 --- /dev/null +++ b/app/(protected)/hse/capa/[id]/page.tsx @@ -0,0 +1,100 @@ +export const dynamic = 'force-dynamic' + +import { notFound, redirect } from 'next/navigation' +import Link from 'next/link' +import { createClient } from '@/lib/supabase/server' +import { VerifyForm } from '@/components/capa/verify-form' + +interface Props { + params: Promise<{ id: string }> +} + +const PRIORITY_BADGE: Record = { + low: 'bg-gray-100 text-gray-600', + med: 'bg-yellow-100 text-yellow-700', + high: 'bg-red-100 text-red-700', +} + +export default async function CapaDetailPage({ params }: Props) { + const { id } = await params + const supabase = await createClient() + const { data, error: authError } = await supabase.auth.getUser() + if (authError || !data?.user) redirect(`/login?redirect=/hse/capa/${id}`) + + const { data: profile } = await supabase + .from('users').select('role').eq('id', data.user.id).single() + + const { data: capa } = await supabase + .from('capa_actions') + .select(` + id, incident_id, root_cause_ref, description, department, + due_date, priority, status, completed_at, verified_by, verified_at, created_at, + incidents (reference_no, incident_type), + owner:users!owner_user_id (name, email) + `) + .eq('id', id) + .single() + + if (!capa) notFound() + + const isHse = profile && ['hse', 'admin'].includes(profile.role) + const status = (capa as { status: string }).status + + return ( +
+ + ← CAPA Board + + +
+
+
+

+ Incident: {(capa.incidents as unknown as { reference_no: string | null } | null)?.reference_no ?? '—'} +

+

{(capa as { description: string }).description}

+
+ + {(capa as { priority: string }).priority} + +
+ +
+
+

Owner

+

{(capa.owner as unknown as { name: string } | null)?.name ?? '—'}

+
+
+

Department

+

{(capa as { department: string }).department}

+
+
+

Due Date

+

{(capa as { due_date: string }).due_date}

+
+
+

Status

+

{status.replace(/_/g, ' ')}

+
+
+ + {(capa as { root_cause_ref: string | null }).root_cause_ref && ( +
+

Root Cause Reference

+

{(capa as { root_cause_ref: string }).root_cause_ref}

+
+ )} + + {(capa as { completed_at: string | null }).completed_at && ( +

+ Completed: {new Date((capa as { completed_at: string }).completed_at).toLocaleString('en-MY', { timeZone: 'Asia/Kuala_Lumpur' })} +

+ )} +
+ + {isHse && status === 'pending_verification' && ( + + )} +
+ ) +} diff --git a/app/api/capa/[id]/verify/route.ts b/app/api/capa/[id]/verify/route.ts new file mode 100644 index 0000000..be32bd6 --- /dev/null +++ b/app/api/capa/[id]/verify/route.ts @@ -0,0 +1,64 @@ +export const dynamic = 'force-dynamic' + +import { NextRequest, NextResponse } from 'next/server' +import { createClient } from '@/lib/supabase/server' + +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + const { id } = await params + const supabase = await createClient() + + const { data: { user }, error: authError } = await supabase.auth.getUser() + if (authError || !user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + + const { data: profile } = await supabase + .from('users').select('role').eq('id', user.id).single() + if (!profile || !['hse', 'admin'].includes(profile.role)) + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + + const { data: capa } = await supabase + .from('capa_actions').select('status, incident_id').eq('id', id).single() + if (!capa) return NextResponse.json({ error: 'Not found' }, { status: 404 }) + if (capa.status !== 'pending_verification') + return NextResponse.json({ error: 'CAPA must be pending_verification' }, { status: 409 }) + + const body: { verdict: 'verified' | 'reopened'; reopen_reason?: string } = await request.json() + if (body.verdict !== 'verified' && body.verdict !== 'reopened') + return NextResponse.json({ error: 'verdict must be verified or reopened' }, { status: 422 }) + + const update: Record = { + status: body.verdict, + verified_by: user.id, + verified_at: new Date().toISOString(), + } + + const { error } = await supabase + .from('capa_actions').update(update).eq('id', id) + + if (error) return NextResponse.json({ error: 'Update failed' }, { status: 500 }) + + await supabase.rpc('write_audit_log', { + p_table_name: 'capa_actions', + p_record_id: id, + p_action: body.verdict === 'verified' ? 'verified' : 'reopened', + p_new_value: { ...update, reopen_reason: body.reopen_reason ?? null }, + }) + + // Check if all CAPAs for this incident are verified — if so, transition incident to verification + const { data: openCapas } = await supabase + .from('capa_actions') + .select('id') + .eq('incident_id', capa.incident_id) + .not('status', 'in', '(verified,closed)') + + if (!openCapas || openCapas.length === 0) { + await supabase + .from('incidents') + .update({ status: 'verification' }) + .eq('id', capa.incident_id) + } + + return NextResponse.json({ ok: true }) +} diff --git a/components/capa/verify-form.tsx b/components/capa/verify-form.tsx new file mode 100644 index 0000000..5e58a2d --- /dev/null +++ b/components/capa/verify-form.tsx @@ -0,0 +1,89 @@ +'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(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(`/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 ( +
+

HSE Verification

+
+ + +
+ {verdict === 'reopened' && ( +
+ +