Compare commits

...
3 Commits
Author SHA1 Message Date
admin acc1f58a29 feat(capa): add action buttons to CAPA detail page 2026-07-30 08:54:25 +08:00
admin 955c49a29d fix(test): remove unused beforeEach import in capa-owner-actions test 2026-07-30 08:43:57 +08:00
adminandClaude Sonnet 4.6 a4253a80f5 feat(capa): allow overdue CAPAs to be acted on, fix owner page stats
- 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
2026-07-30 08:41:41 +08:00
4 changed files with 57 additions and 4 deletions
+9 -3
View File
@@ -52,8 +52,14 @@ export default async function CapaOwnerPage() {
.orderBy(asc(capaActions.dueDate)) .orderBy(asc(capaActions.dueDate))
) )
const openCount = rows.filter(c => ['open', 'in_progress', 'reopened'].includes(c.status)).length const today = new Date().toISOString().slice(0, 10)
const overdueCount = rows.filter(c => c.status === 'overdue').length 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 doneCount = rows.filter(c => ['verified', 'closed'].includes(c.status)).length
const PRIORITY_COLORS: Record<string, string> = { const PRIORITY_COLORS: Record<string, string> = {
@@ -82,7 +88,7 @@ export default async function CapaOwnerPage() {
) : ( ) : (
<div className="bg-white rounded-xl shadow-sm divide-y divide-gray-50"> <div className="bg-white rounded-xl shadow-sm divide-y divide-gray-50">
{activeRows.map(capa => { {activeRows.map(capa => {
const isOverdue = capa.status === 'overdue' const isOverdue = isCapaOverdue(capa)
return ( return (
<div key={capa.id} className={`p-4 ${isOverdue ? 'bg-red-50' : ''}`}> <div key={capa.id} className={`p-4 ${isOverdue ? 'bg-red-50' : ''}`}>
<div className="flex items-start justify-between gap-4"> <div className="flex items-start justify-between gap-4">
+7
View File
@@ -9,6 +9,7 @@ import { eq } from 'drizzle-orm'
import { aliasedTable } from 'drizzle-orm' import { aliasedTable } from 'drizzle-orm'
import { VerifyForm } from '@/components/capa/verify-form' import { VerifyForm } from '@/components/capa/verify-form'
import { CloseCapaButton } from '@/components/capa/close-capa-button' import { CloseCapaButton } from '@/components/capa/close-capa-button'
import { CapaOwnerActions } from '@/components/capa/capa-owner-actions'
interface Props { interface Props {
params: Promise<{ id: string }> params: Promise<{ id: string }>
@@ -39,6 +40,7 @@ export default async function CapaDetailPage({ params }: Props) {
status: capaActions.status, status: capaActions.status,
completedAt: capaActions.completedAt, completedAt: capaActions.completedAt,
ownerNotes: capaActions.ownerNotes, ownerNotes: capaActions.ownerNotes,
ownerUserId: capaActions.ownerUserId,
incidentRefNo: incidents.referenceNo, incidentRefNo: incidents.referenceNo,
ownerName: ownerAlias.name, ownerName: ownerAlias.name,
}) })
@@ -112,6 +114,11 @@ export default async function CapaDetailPage({ params }: Props) {
)} )}
</div> </div>
{['open', 'in_progress', 'overdue', 'reopened'].includes(status) &&
(isHse || session?.sub === capa.ownerUserId) && (
<CapaOwnerActions capaId={id} currentStatus={status} />
)}
{isHse && status === 'pending_verification' && ( {isHse && status === 'pending_verification' && (
<VerifyForm capaId={id} /> <VerifyForm capaId={id} />
)} )}
+1 -1
View File
@@ -31,7 +31,7 @@ export function CapaOwnerActions({ capaId, currentStatus }: Props) {
} }
} }
if (currentStatus === 'open' || currentStatus === 'reopened') { if (currentStatus === 'open' || currentStatus === 'reopened' || currentStatus === 'overdue') {
return ( return (
<button <button
onClick={() => updateStatus('in_progress')} onClick={() => updateStatus('in_progress')}
@@ -0,0 +1,40 @@
import { describe, it, expect, vi } from 'vitest'
import { render, screen } from '@testing-library/react'
import { CapaOwnerActions } from '@/components/capa/capa-owner-actions'
// Mock Next.js router
vi.mock('next/navigation', () => ({
useRouter: () => ({ refresh: vi.fn() }),
}))
describe('CapaOwnerActions', () => {
it('shows Mark In Progress for open status', () => {
render(<CapaOwnerActions capaId="abc" currentStatus="open" />)
expect(screen.getByText('Mark In Progress')).toBeTruthy()
})
it('shows Mark In Progress for reopened status', () => {
render(<CapaOwnerActions capaId="abc" currentStatus="reopened" />)
expect(screen.getByText('Mark In Progress')).toBeTruthy()
})
it('shows Mark In Progress for overdue status', () => {
render(<CapaOwnerActions capaId="abc" currentStatus="overdue" />)
expect(screen.getByText('Mark In Progress')).toBeTruthy()
})
it('shows Submit for Verification for in_progress status', () => {
render(<CapaOwnerActions capaId="abc" currentStatus="in_progress" />)
expect(screen.getByText('Submit for Verification')).toBeTruthy()
})
it('renders nothing for pending_verification status', () => {
const { container } = render(<CapaOwnerActions capaId="abc" currentStatus="pending_verification" />)
expect(container.firstChild).toBeNull()
})
it('renders nothing for verified status', () => {
const { container } = render(<CapaOwnerActions capaId="abc" currentStatus="verified" />)
expect(container.firstChild).toBeNull()
})
})