Files

132 lines
4.4 KiB
TypeScript

export const dynamic = 'force-dynamic'
import { notFound, redirect } from 'next/navigation'
import Link from 'next/link'
import { asAdmin } from '@/lib/db/with-user'
import { getSession } from '@/lib/auth/get-session'
import { capaActions, incidents, users } from '@/lib/db/schema'
import { eq } from 'drizzle-orm'
import { aliasedTable } from 'drizzle-orm'
import { VerifyForm } from '@/components/capa/verify-form'
import { CloseCapaButton } from '@/components/capa/close-capa-button'
import { CapaOwnerActions } from '@/components/capa/capa-owner-actions'
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 ownerAlias = aliasedTable(users, 'owner')
const [capa] = await asAdmin(db =>
db.select({
id: capaActions.id,
incidentId: capaActions.incidentId,
rootCauseRef: capaActions.rootCauseRef,
description: capaActions.description,
department: capaActions.department,
dueDate: capaActions.dueDate,
priority: capaActions.priority,
status: capaActions.status,
completedAt: capaActions.completedAt,
ownerNotes: capaActions.ownerNotes,
ownerUserId: capaActions.ownerUserId,
incidentRefNo: incidents.referenceNo,
ownerName: ownerAlias.name,
})
.from(capaActions)
.leftJoin(incidents, eq(capaActions.incidentId, incidents.id))
.leftJoin(ownerAlias, eq(capaActions.ownerUserId, ownerAlias.id))
.where(eq(capaActions.id, id))
.limit(1)
)
if (!capa) notFound()
const isHse = session && ['hse', 'admin'].includes(session.role)
const status = capa.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.incidentRefNo ?? '—'}
</p>
<p className="text-gray-900 font-medium">{capa.description}</p>
</div>
<span className={`px-2 py-1 rounded text-xs font-semibold ${PRIORITY_BADGE[capa.priority] ?? ''}`}>
{capa.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.ownerName ?? '—'}</p>
</div>
<div>
<p className="text-xs text-gray-500">Department</p>
<p className="text-gray-800">{capa.department}</p>
</div>
<div>
<p className="text-xs text-gray-500">Due Date</p>
<p className="text-gray-800">{capa.dueDate}</p>
</div>
<div>
<p className="text-xs text-gray-500">Status</p>
<p className="text-gray-800 capitalize">{status.replace(/_/g, ' ')}</p>
</div>
{capa.ownerNotes && (
<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.ownerNotes}</p>
</div>
)}
</div>
{capa.rootCauseRef && (
<div>
<p className="text-xs text-gray-500">Root Cause Reference</p>
<p className="text-sm text-gray-800">{capa.rootCauseRef}</p>
</div>
)}
{capa.completedAt && (
<p className="text-xs text-gray-500">
Completed: {new Date(capa.completedAt).toLocaleString('en-MY', { timeZone: 'Asia/Kuala_Lumpur' })}
</p>
)}
</div>
{['open', 'in_progress', 'overdue', 'reopened'].includes(status) &&
(isHse || session?.sub === capa.ownerUserId) && (
<CapaOwnerActions capaId={id} currentStatus={status} />
)}
{isHse && status === 'pending_verification' && (
<VerifyForm capaId={id} />
)}
{isHse && status === 'verified' && (
<CloseCapaButton capaId={id} />
)}
</main>
)
}