chore: initial commit — UdyamLinc UI, phases A-E
Vite/React 19 console and enterprise PWA. Officer console screens, offline ledger and sync, data feeds and trip-wire rules screens with freshness chip. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import type { AlertState, RiskAlert } from './api'
|
||||
import { alertsFor } from './demo/ops'
|
||||
|
||||
/*
|
||||
* Officer actions on the alert queue.
|
||||
*
|
||||
* The queue itself is derived from the portfolio; only the state changes an
|
||||
* officer makes (acknowledge / escalate / snooze / close) and their audit trail
|
||||
* are persisted, which is exactly the split the deck describes — the model
|
||||
* proposes, the officer disposes, and every disposal is auditable.
|
||||
*/
|
||||
|
||||
const KEY = 'ul-alert-states'
|
||||
const AUDIT_KEY = 'ul-alert-audit'
|
||||
|
||||
export interface AuditEntry {
|
||||
alertId: string
|
||||
from: AlertState
|
||||
to: AlertState
|
||||
actor: string
|
||||
note: string | null
|
||||
at: string
|
||||
}
|
||||
|
||||
function readStates(): Record<string, AlertState> {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(KEY) ?? '{}') as Record<string, AlertState>
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
export function readAudit(): AuditEntry[] {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(AUDIT_KEY) ?? '[]') as AuditEntry[]
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export function setAlertState(alert: RiskAlert, to: AlertState, actor: string, note?: string) {
|
||||
const states = readStates()
|
||||
const from = states[alert.alertId] ?? alert.state
|
||||
states[alert.alertId] = to
|
||||
localStorage.setItem(KEY, JSON.stringify(states))
|
||||
|
||||
const audit: AuditEntry = { alertId: alert.alertId, from, to, actor, note: note ?? null, at: new Date().toISOString() }
|
||||
localStorage.setItem(AUDIT_KEY, JSON.stringify([audit, ...readAudit()].slice(0, 200)))
|
||||
window.dispatchEvent(new Event('ul-alerts-changed'))
|
||||
}
|
||||
|
||||
/** The alert queue for a block, with any officer actions applied on top. */
|
||||
export function useAlerts(block: string) {
|
||||
const [version, setVersion] = useState(0)
|
||||
|
||||
const bump = useCallback(() => setVersion((v) => v + 1), [])
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('ul-alerts-changed', bump)
|
||||
return () => window.removeEventListener('ul-alerts-changed', bump)
|
||||
}, [bump])
|
||||
|
||||
const states = readStates()
|
||||
// `version` is read so the memo-free derivation re-runs after every action.
|
||||
void version
|
||||
const alerts = alertsFor(block).map((a) => ({ ...a, state: states[a.alertId] ?? a.state }))
|
||||
|
||||
return { alerts, audit: readAudit() }
|
||||
}
|
||||
Reference in New Issue
Block a user