Files
ims/app/(protected)/hse/capa/[id]/page.tsx
T
adminandClaude Sonnet 4.6 d18d29168a feat(auth): phase 3 — replace Supabase GoTrue with bcryptjs+jose
Custom auth stack: bcryptjs password hashing (cost 10, GoTrue-compatible),
jose JWT session cookies (edge-safe, 8hr TTL), new API routes for
login/logout/reset/change-password, middleware rewritten to JWT-only
verification with no DB access. All 38 protected pages and API routes
migrated from supabase.auth.getUser() to getSession(). Supabase .from()
queries retained for Phase 4. lib/db/index.ts refactored to lazy Proxy
singleton to avoid module-level throw during Next.js build.

tsc: clean, build: clean, tests: 4/4 passed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-23 16:20:04 +08:00

114 lines
4.1 KiB
TypeScript

export const dynamic = 'force-dynamic'
import { notFound, redirect } from 'next/navigation'
import Link from 'next/link'
import { createClient } from '@/lib/supabase/server'
import { getSession } from '@/lib/auth/get-session'
import { VerifyForm } from '@/components/capa/verify-form'
import { CloseCapaButton } from '@/components/capa/close-capa-button'
interface Props {
params: Promise<{ id: string }>
}
const PRIORITY_BADGE: Record<string, string> = {
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 session = await getSession()
if (!session) redirect(`/login?redirect=/hse/capa/${id}`)
const supabase = await createClient()
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,
owner_notes,
incidents (reference_no, incident_type),
owner:users!owner_user_id (name, email)
`)
.eq('id', id)
.single()
if (!capa) notFound()
const isHse = session && ['hse', 'admin'].includes(session.role)
const status = (capa as { status: string }).status
return (
<main className="max-w-2xl mx-auto px-4 py-6">
<Link href="/hse/capa" className="text-sm text-blue-600 hover:underline mb-4 inline-block">
CAPA Board
</Link>
<div className="bg-white rounded-xl shadow-sm p-5 mb-4 space-y-4">
<div className="flex items-start justify-between">
<div>
<p className="text-xs text-gray-500 mb-1">
Incident: {(capa.incidents as unknown as { reference_no: string | null } | null)?.reference_no ?? '—'}
</p>
<p className="text-gray-900 font-medium">{(capa as { description: string }).description}</p>
</div>
<span className={`px-2 py-1 rounded text-xs font-semibold ${PRIORITY_BADGE[(capa as { priority: string }).priority] ?? ''}`}>
{(capa as { priority: string }).priority}
</span>
</div>
<div className="grid grid-cols-2 gap-3 text-sm">
<div>
<p className="text-xs text-gray-500">Owner</p>
<p className="text-gray-800">{(capa.owner as unknown as { name: string } | null)?.name ?? '—'}</p>
</div>
<div>
<p className="text-xs text-gray-500">Department</p>
<p className="text-gray-800">{(capa as { department: string }).department}</p>
</div>
<div>
<p className="text-xs text-gray-500">Due Date</p>
<p className="text-gray-800">{(capa as { due_date: string }).due_date}</p>
</div>
<div>
<p className="text-xs text-gray-500">Status</p>
<p className="text-gray-800 capitalize">{status.replace(/_/g, ' ')}</p>
</div>
{(capa as { owner_notes: string | null }).owner_notes && (
<div className="col-span-2">
<p className="text-xs text-gray-500">Owner Notes</p>
<p className="text-sm text-gray-800 whitespace-pre-wrap">
{(capa as { owner_notes: string }).owner_notes}
</p>
</div>
)}
</div>
{(capa as { root_cause_ref: string | null }).root_cause_ref && (
<div>
<p className="text-xs text-gray-500">Root Cause Reference</p>
<p className="text-sm text-gray-800">{(capa as { root_cause_ref: string }).root_cause_ref}</p>
</div>
)}
{(capa as { completed_at: string | null }).completed_at && (
<p className="text-xs text-gray-500">
Completed: {new Date((capa as { completed_at: string }).completed_at).toLocaleString('en-MY', { timeZone: 'Asia/Kuala_Lumpur' })}
</p>
)}
</div>
{isHse && status === 'pending_verification' && (
<VerifyForm capaId={id} />
)}
{isHse && status === 'verified' && (
<CloseCapaButton capaId={id} />
)}
</main>
)
}