feat: dashboard — leading/lagging, zone heatmap, CAPA on-time rate, DOSH filing status
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FFDuBhMKvoWjrWT3ZnGmmr
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
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)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user