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:
2026-07-11 16:38:57 +08:00
co-authored by Claude Sonnet 4.6
parent 10e71969dd
commit 3473cf8ab7
2 changed files with 200 additions and 5 deletions
@@ -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)
})
})