Files
ims/app/(protected)/capa-owner/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

130 lines
5.6 KiB
TypeScript

export const dynamic = 'force-dynamic'
import Link from 'next/link'
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
import { getSession } from '@/lib/auth/get-session'
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 supabase = await createClient()
const { data: capas } = await supabase
.from('capa_actions')
.select('id, description, due_date, priority, status, incident_id, owner_notes, incidents (reference_no)')
.eq('owner_user_id', session.sub)
.order('due_date', { ascending: true, nullsFirst: false })
const rows = capas ?? []
const openCount = rows.filter(c => ['open', 'in_progress', 'reopened'].includes(c.status)).length
const overdueCount = rows.filter(c => c.status === 'overdue').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 incRef = (capa.incidents as unknown as { reference_no: string | null } | null)?.reference_no
const isOverdue = capa.status === 'overdue'
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>
{incRef && (
<Link href={`/hse/incidents/${capa.incident_id}`} className="text-xs text-blue-600 hover:underline mt-0.5 inline-block">
{incRef}
</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.due_date ? `Due ${new Date(capa.due_date as string).toLocaleDateString('en-MY')}` : 'No due date'}
</p>
{capa.priority && (
<p className={`text-xs ${PRIORITY_COLORS[capa.priority as string] ?? ''}`}>
{(capa.priority as string).toUpperCase()} priority
</p>
)}
<CapaOwnerActions capaId={capa.id} currentStatus={capa.status} />
</div>
</div>
{capa.status !== 'pending_verification' && (
<CapaOwnerNotesForm capaId={capa.id} initialNotes={(capa as { owner_notes: string | null }).owner_notes} />
)}
</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>
)
}