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<CheckIn, "status" | "summary">) =>
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<CheckIn, "notes">) => 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<CheckIn, "notes">, noteIds: string[]) =>
c.notes.filter((n) => noteIds.includes(n.id));