diff --git a/apps/app/src/components-v2/shared/statusSeverity.ts b/apps/app/src/components-v2/shared/statusSeverity.ts new file mode 100644 index 00000000..f497d5fa --- /dev/null +++ b/apps/app/src/components-v2/shared/statusSeverity.ts @@ -0,0 +1,59 @@ +// apps/app/src/components-v2/shared/statusSeverity.ts +// Single source of truth for status → PrimeVue Tag severity. +// Seeded VERBATIM from design spec §8 (commit ae0bd2da). Do NOT +// reinterpret here — a wrong severity is a spec-amendment process, +// not an edit to this file. Enforced bidirectionally by +// tests/unit/utils/statusSeverity.consistency.spec.ts (spec §8.X). + +export type TagSeverity = 'success' | 'warn' | 'info' | 'secondary' | 'danger' + +export const STATUS_SEVERITY: Readonly> = Object.freeze({ + // success — terminal-good / fully settled + approved: 'success', + completed: 'success', + confirmed: 'success', + contracted: 'success', + paid_in_full: 'success', + + // warn — organizer action required + pending_approval: 'warn', + pending: 'warn', + applied: 'warn', + option: 'warn', + offered: 'warn', + reverted: 'warn', + + // info — awaiting external party / in-progress, no viewer action + invited: 'info', + requested: 'info', + deposit_paid: 'info', + + // secondary — muted: absent / not-yet-live / archived + none: 'secondary', + draft: 'secondary', + dismissed: 'secondary', + + // danger — terminal-bad + rejected: 'danger', + cancelled: 'danger', + declined: 'danger', + no_show: 'danger', +}) + +const FALLBACK: TagSeverity = 'info' + +/** + * Resolve a status string to a Tag severity. The §8.X consistency + * test guarantees every live enum value is mapped, so the fallback + * is unreachable in a passing build; it exists only as defence in + * depth and emits a dev-only console warning so any gap is loud. + */ +export function statusSeverity(status: string): TagSeverity { + const hit = STATUS_SEVERITY[status] + if (hit) + return hit + if (import.meta.env.DEV) + console.warn(`[statusSeverity] unmapped status "${status}" — falling back to "${FALLBACK}". Add a §8 row + extend the §8.X test.`) + + return FALLBACK +} diff --git a/apps/app/tests/unit/utils/statusSeverity.consistency.spec.ts b/apps/app/tests/unit/utils/statusSeverity.consistency.spec.ts new file mode 100644 index 00000000..81002e6d --- /dev/null +++ b/apps/app/tests/unit/utils/statusSeverity.consistency.spec.ts @@ -0,0 +1,30 @@ +// apps/app/tests/unit/utils/statusSeverity.consistency.spec.ts +import { describe, expect, it } from 'vitest' +import { ShiftAssignmentStatus } from '@/types/shiftAssignment' +import { PersonStatus } from '@/types/person' +import { MatchStatus } from '@/types/identityMatch' +import { ArtistEngagementStatus, PaymentStatus } from '@/types/timetable' +import { STATUS_SEVERITY, statusSeverity } from '@/components-v2/shared/statusSeverity' + +const ENUMS = { ShiftAssignmentStatus, ArtistEngagementStatus, PaymentStatus, PersonStatus, MatchStatus } +const ALL_VALUES = Object.values(ENUMS).flatMap(e => Object.values(e)) as string[] + +describe('statusSeverity §8.X enforcement', () => { + it('completeness: every live enum value resolves to an explicit severity (no dev-fallback)', () => { + const missing = ALL_VALUES.filter(v => !(v in STATUS_SEVERITY)) + + expect(missing, `unmapped enum values: ${missing.join(', ')}`).toEqual([]) + }) + + it('no phantoms: every map key exists in at least one live enum', () => { + const orphans = Object.keys(STATUS_SEVERITY).filter(k => !ALL_VALUES.includes(k)) + + expect(orphans, `orphan keys: ${orphans.join(', ')}`).toEqual([]) + }) + + it('resolver returns the mapped severity and never throws on a known value', () => { + expect(statusSeverity('approved')).toBe('success') + expect(statusSeverity('no_show')).toBe('danger') + expect(statusSeverity('deposit_paid')).toBe('info') + }) +})