Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FFDuBhMKvoWjrWT3ZnGmmr
69 lines
2.2 KiB
TypeScript
69 lines
2.2 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|
|
|
// Pure helpers extracted (conceptually) from dashboard logic — tested here as standalone functions
|
|
|
|
function computeCapaOnTimeRate(
|
|
capas: Array<{ due_date: string; completed_at: string | null; verified_at: string | null }>
|
|
): number | null {
|
|
if (capas.length === 0) return null
|
|
const onTime = capas.filter(c => {
|
|
const due = new Date(c.due_date)
|
|
const done = c.verified_at
|
|
? new Date(c.verified_at)
|
|
: c.completed_at
|
|
? new Date(c.completed_at)
|
|
: null
|
|
return done !== null && done <= due
|
|
})
|
|
return Math.round((onTime.length / capas.length) * 100)
|
|
}
|
|
|
|
function classifyIndicators(incidents: Array<{ incident_type: string }>) {
|
|
const leading = incidents.filter(i => ['hazard', 'near_miss'].includes(i.incident_type)).length
|
|
const lagging = incidents.filter(i => i.incident_type === 'injury').length
|
|
return { leading, lagging }
|
|
}
|
|
|
|
describe('CAPA on-time rate', () => {
|
|
it('returns null for empty list', () => {
|
|
expect(computeCapaOnTimeRate([])).toBeNull()
|
|
})
|
|
|
|
it('returns 100% when all CAPAs completed on time', () => {
|
|
const capas = [
|
|
{ due_date: '2026-07-10', completed_at: '2026-07-09', verified_at: null },
|
|
{ due_date: '2026-07-10', completed_at: null, verified_at: '2026-07-08' },
|
|
]
|
|
expect(computeCapaOnTimeRate(capas)).toBe(100)
|
|
})
|
|
|
|
it('returns 50% when half are late', () => {
|
|
const capas = [
|
|
{ due_date: '2026-07-10', completed_at: '2026-07-09', verified_at: null },
|
|
{ due_date: '2026-07-10', completed_at: '2026-07-12', verified_at: null },
|
|
]
|
|
expect(computeCapaOnTimeRate(capas)).toBe(50)
|
|
})
|
|
|
|
it('returns 0% when all CAPAs are overdue', () => {
|
|
const capas = [
|
|
{ due_date: '2026-07-10', completed_at: '2026-07-15', verified_at: null },
|
|
]
|
|
expect(computeCapaOnTimeRate(capas)).toBe(0)
|
|
})
|
|
})
|
|
|
|
describe('leading vs lagging indicators', () => {
|
|
it('counts hazard and near_miss as leading', () => {
|
|
const incidents = [
|
|
{ incident_type: 'hazard' },
|
|
{ incident_type: 'near_miss' },
|
|
{ incident_type: 'injury' },
|
|
{ incident_type: 'fire' },
|
|
]
|
|
const { leading, lagging } = classifyIndicators(incidents)
|
|
expect(leading).toBe(2)
|
|
expect(lagging).toBe(1)
|
|
})
|
|
})
|