- CapaOwnerActions: add 'overdue' to open/reopened branch so DB-overdue CAPAs show 'Mark In Progress' button (cron transitions open→overdue) - capa-owner page: compute overdue at runtime (dueDate < today) so stats and red highlight are correct even before cron runs - openCount now excludes overdue items to avoid double-counting - Add CapaOwnerActions test suite covering all status transitions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HEYxFQiCyxJvnBCoZeYzB9
145 lines
6.0 KiB
TypeScript
145 lines
6.0 KiB
TypeScript
export const dynamic = 'force-dynamic'
|
|
|
|
import Link from 'next/link'
|
|
import { asAdmin } from '@/lib/db/with-user'
|
|
import { redirect } from 'next/navigation'
|
|
import { getSession } from '@/lib/auth/get-session'
|
|
import { capaActions, incidents } from '@/lib/db/schema'
|
|
import { eq, asc } from 'drizzle-orm'
|
|
import { StatCard } from '@/components/dashboard/stat-card'
|
|
import { CapaOwnerActions } from '@/components/capa/capa-owner-actions'
|
|
import { CapaOwnerNotesForm } from '@/components/capa/capa-owner-notes-form'
|
|
|
|
const STATUS_LABELS: Record<string, string> = {
|
|
open: 'Open',
|
|
in_progress: 'In Progress',
|
|
overdue: 'Overdue',
|
|
pending_verification: 'Pending Verification',
|
|
verified: 'Verified',
|
|
reopened: 'Reopened',
|
|
closed: 'Closed',
|
|
}
|
|
|
|
const STATUS_COLORS: Record<string, string> = {
|
|
open: 'bg-gray-100 text-gray-700',
|
|
in_progress: 'bg-blue-100 text-blue-700',
|
|
overdue: 'bg-red-100 text-red-700',
|
|
pending_verification: 'bg-purple-100 text-purple-700',
|
|
verified: 'bg-green-100 text-green-700',
|
|
reopened: 'bg-orange-100 text-orange-700',
|
|
closed: 'bg-gray-100 text-gray-500',
|
|
}
|
|
|
|
export default async function CapaOwnerPage() {
|
|
const session = await getSession()
|
|
if (!session) redirect('/login')
|
|
if (!['capa_owner', 'admin', 'hse', 'supervisor'].includes(session.role)) redirect('/')
|
|
|
|
const rows = await asAdmin(db =>
|
|
db.select({
|
|
id: capaActions.id,
|
|
description: capaActions.description,
|
|
dueDate: capaActions.dueDate,
|
|
priority: capaActions.priority,
|
|
status: capaActions.status,
|
|
incidentId: capaActions.incidentId,
|
|
ownerNotes: capaActions.ownerNotes,
|
|
incidentRefNo: incidents.referenceNo,
|
|
})
|
|
.from(capaActions)
|
|
.leftJoin(incidents, eq(capaActions.incidentId, incidents.id))
|
|
.where(eq(capaActions.ownerUserId, session.sub))
|
|
.orderBy(asc(capaActions.dueDate))
|
|
)
|
|
|
|
const today = new Date().toISOString().slice(0, 10)
|
|
function isCapaOverdue(c: { status: string; dueDate: string | null }): boolean {
|
|
if (c.status === 'overdue') return true
|
|
return (c.status === 'open' || c.status === 'in_progress') && !!c.dueDate && c.dueDate < today
|
|
}
|
|
|
|
const openCount = rows.filter(c => ['open', 'in_progress', 'reopened'].includes(c.status) && !isCapaOverdue(c)).length
|
|
const overdueCount = rows.filter(c => isCapaOverdue(c)).length
|
|
const doneCount = rows.filter(c => ['verified', 'closed'].includes(c.status)).length
|
|
|
|
const PRIORITY_COLORS: Record<string, string> = {
|
|
high: 'text-red-600 font-semibold',
|
|
med: 'text-orange-500 font-medium',
|
|
low: 'text-gray-400',
|
|
}
|
|
|
|
const activeRows = rows.filter(c => !['verified', 'closed'].includes(c.status))
|
|
const doneRows = rows.filter(c => ['verified', 'closed'].includes(c.status))
|
|
|
|
return (
|
|
<main className="max-w-4xl mx-auto px-4 py-6">
|
|
<h1 className="text-2xl font-bold text-gray-900 mb-6">My CAPAs</h1>
|
|
|
|
<div className="grid grid-cols-3 gap-4 mb-6">
|
|
<StatCard label="Active" value={openCount} accent={openCount > 0 ? 'yellow' : 'green'} />
|
|
<StatCard label="Overdue" value={overdueCount} accent={overdueCount > 0 ? 'red' : 'green'} />
|
|
<StatCard label="Completed" value={doneCount} accent="green" />
|
|
</div>
|
|
|
|
{activeRows.length === 0 ? (
|
|
<div className="bg-white rounded-xl shadow-sm p-8 text-center">
|
|
<p className="text-gray-400 text-sm">No active CAPAs assigned to you.</p>
|
|
</div>
|
|
) : (
|
|
<div className="bg-white rounded-xl shadow-sm divide-y divide-gray-50">
|
|
{activeRows.map(capa => {
|
|
const isOverdue = isCapaOverdue(capa)
|
|
return (
|
|
<div key={capa.id} className={`p-4 ${isOverdue ? 'bg-red-50' : ''}`}>
|
|
<div className="flex items-start justify-between gap-4">
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-sm text-gray-800">{capa.description}</p>
|
|
{capa.incidentRefNo && (
|
|
<Link href={`/hse/incidents/${capa.incidentId}`} className="text-xs text-blue-600 hover:underline mt-0.5 inline-block">
|
|
{capa.incidentRefNo}
|
|
</Link>
|
|
)}
|
|
</div>
|
|
<div className="shrink-0 text-right space-y-1">
|
|
<span className={`text-xs rounded-full px-2 py-0.5 ${STATUS_COLORS[capa.status] ?? 'bg-gray-100 text-gray-600'}`}>
|
|
{STATUS_LABELS[capa.status] ?? capa.status}
|
|
</span>
|
|
<p className={`text-xs ${isOverdue ? 'text-red-600 font-semibold' : 'text-gray-400'}`}>
|
|
{capa.dueDate ? `Due ${new Date(capa.dueDate).toLocaleDateString('en-MY')}` : 'No due date'}
|
|
</p>
|
|
{capa.priority && (
|
|
<p className={`text-xs ${PRIORITY_COLORS[capa.priority] ?? ''}`}>
|
|
{capa.priority.toUpperCase()} priority
|
|
</p>
|
|
)}
|
|
<CapaOwnerActions capaId={capa.id} currentStatus={capa.status} />
|
|
</div>
|
|
</div>
|
|
{capa.status !== 'pending_verification' && (
|
|
<CapaOwnerNotesForm capaId={capa.id} initialNotes={capa.ownerNotes} />
|
|
)}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
)}
|
|
|
|
{doneRows.length > 0 && (
|
|
<div className="mt-6">
|
|
<h2 className="text-xs font-semibold text-gray-400 uppercase tracking-wide mb-2">Completed ({doneCount})</h2>
|
|
<div className="bg-white rounded-xl shadow-sm divide-y divide-gray-50 opacity-60">
|
|
{doneRows.map(capa => (
|
|
<div key={capa.id} className="p-4 flex items-center justify-between">
|
|
<p className="text-sm text-gray-500 line-clamp-1">{capa.description}</p>
|
|
<span className={`text-xs rounded-full px-2 py-0.5 ml-4 shrink-0 ${STATUS_COLORS[capa.status] ?? 'bg-gray-100'}`}>
|
|
{STATUS_LABELS[capa.status] ?? capa.status}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</main>
|
|
)
|
|
}
|