import type { CheckIn, CheckInEvent, CheckInNote, CheckInStatus } from "../api/types"; const byTime = (a: { at: string }, b: { at: string }) => new Date(a.at).getTime() - new Date(b.at).getTime(); function upsertNote(notes: CheckInNote[], note: CheckInNote): CheckInNote[] { const rest = notes.filter((n) => n.id !== note.id); return [...rest, note].sort(byTime); } /** * Fold one check-in event into the snapshot. Events at or below lastEventId are replays * (after a reconnect with Last-Event-ID, or a poll overlapping the stream) and change nothing. */ export function applyEvent(state: CheckIn, event: CheckInEvent): CheckIn { if (event.id <= state.lastEventId) return state; const next: CheckIn = { ...state, lastEventId: event.id }; switch (event.type) { case "status": { const last = state.history[state.history.length - 1]; const repeat = last && last.status === event.status && last.at === event.at; return { ...next, status: event.status, statusAt: event.at, later: event.status === "later" ? event.later : state.later, history: repeat ? state.history : [...state.history, { status: event.status, at: event.at, detail: event.detail }], }; } case "note": case "note_updated": return { ...next, notes: upsertNote(state.notes, event.note), urgent: state.urgent || event.note.urgent }; case "transcript": { const rest = state.transcript.filter((l) => l.id !== event.line.id); return { ...next, transcript: [...rest, event.line].sort((a, b) => a.offsetSeconds - b.offsetSeconds) }; } case "summary": return { ...next, summary: event.summary }; case "urgent": return { ...next, urgent: true, notes: state.notes.map((n) => (n.id === event.noteId ? { ...n, urgent: true } : n)), }; case "alert": return event.message ? { ...next, alerts: [...state.alerts, { id: event.id, at: event.at, kind: event.kind ?? "alert", message: event.message }] } : next; } } export const applyEvents = (state: CheckIn, events: CheckInEvent[]) => [...events].sort((a, b) => a.id - b.id).reduce(applyEvent, state); const ended: CheckInStatus[] = [ "completed", "declined", "not_reached", "identity_not_confirmed", "ended_early", "cancelled", ]; /** No more events are expected once a check-in has ended and its summary (if any) has arrived. */ export const isSettled = (c: Pick) => ended.includes(c.status) && (c.status !== "completed" || c.summary !== null); export const isEnded = (status: CheckInStatus) => ended.includes(status); export const isLive = (status: CheckInStatus) => status === "in_progress"; /** YCI-29: urgent notes still waiting for a clinician's acknowledgement. */ export const unacknowledged = (c: Pick) => c.notes.filter((n) => n.urgent && !n.acknowledgedAt); /** The notes a summary sentence rests on, in note order (YCI-25). */ export const sentenceNotes = (c: Pick, noteIds: string[]) => c.notes.filter((n) => noteIds.includes(n.id));