import type { CheckInListItem, CheckRun, PatientDetail, ShareScope } from "../api/types"; import { formatDate } from "../flow"; import { dayLabel, listNames, whenLabel } from "./format"; import { tileValues, type TileCell } from "./patients"; /** The quiet line under the name: what identity is on file, the clinic, and where the patient came from. */ export function headerLine(p: PatientDetail, clinicName: string): string[] { const born = p.dateOfBirth ? `Born ${formatDate(p.dateOfBirth)}` : p.birthYear ? `Born ${p.birthYear}` : p.age !== null ? "No date of birth on file" : null; return [born, clinicName || null, p.sharing ? "Shared from MARMAR Health" : "Clinic patient"].filter((part): part is string => !!part); } export type StatCell = TileCell & { label: string }; const CLINIC: TileCell = { value: "Not shared", note: "No app" }; /** "today" and "yesterday" mid-sentence; a date stays as it is. */ const lowerDay = (day: string) => (day === "Today" || day === "Yesterday" || day === "Tomorrow" ? day.toLowerCase() : day); /** Findings on the last check, from the check the page already loaded. Zero is never called safe. */ export function findingsCell(p: Pick, check: CheckRun | null, loading: boolean, now = new Date()): TileCell { if (!p.lastCheckId) return { value: "No check yet", note: "Run one on the Check tab" }; if (!check) return { value: loading ? "…" : "Not loaded", note: loading ? "Opening the last check" : "Open the Check tab" }; const findings = check.assessment.contextual.findings; const urgent = findings.filter((f) => f.tier === "get_help_now").length; const when = lowerDay(dayLabel(check.ranAt, now)); return { value: String(findings.length), note: urgent ? `${urgent} urgent review · checked ${when}` : `Checked ${when}` }; } /** * The stat row: medicines, doses this week (taken of due), last check-in and findings. Each area the * patient keeps back says "Not shared"; paused sharing shows nothing new (SHR-1, INV-3). */ export function statCells(p: PatientDetail, check: CheckRun | null, checkLoading: boolean, now = new Date()): StatCell[] { const tiles = tileValues(p, now); const stopped: TileCell | null = p.sharing?.status === "stopped" ? { value: "Stopped", note: `Since ${lowerDay(dayLabel(p.sharing.changedAt, now))}` } : null; const cell = (pick: (t: NonNullable) => TileCell): TileCell => (tiles ? pick(tiles) : stopped ?? CLINIC); return [ { label: "Medicines", ...cell((t) => t.medicines) }, { label: "Doses this week", ...cell((t) => t.doses) }, { label: "Last check-in", ...cell((t) => t.lastCheckIn) }, { label: "Findings", ...findingsCell(p, check, checkLoading, now) }, ]; } export type ScopeRow = { scope: ShareScope; label: string; shared: boolean }; const SCOPE_ORDER: [ShareScope, string][] = [ ["medicines", "Medicines"], ["doses", "Doses taken"], ["check_ins", "Check-ins"], ["records", "Records"], ]; /** The rail's Details: who shares and since when, what, calls, quiet hours and the identity on file. */ export type Details = { sharedBy: { label: string; value: string; note: string }; scopes: ScopeRow[] | null; scopesNote: string | null; calls: string; quietHours: string; identity: string; withheldSome: boolean; }; export function quietHoursLabel(p: Pick): string { if (!p.sharing) return "Not known: no app"; if (!(p.quietHoursKnown ?? (p.kind === "connected" && p.sharing.status === "connected"))) return "Not known"; const { start, end } = p.quietHours; return start === end ? "Off" : `${start} to ${end}`; } export function identityLabel(p: Pick): string { if (p.identityOnFile === "date_of_birth" && p.dateOfBirth) return `Date of birth · ${formatDate(p.dateOfBirth)}`; if (p.identityOnFile === "date_of_birth") return "Date of birth"; if (p.identityOnFile === "birth_year") return p.birthYear ? `Birth year only · ${p.birthYear}` : "Birth year only"; return "None on file"; } export function details(p: PatientDetail, now = new Date()): Details { const s = p.sharing; const sharedBy = s ? { label: "Shared by", value: s.sharedBy.length ? listNames(s.sharedBy) : "MARMAR Health", note: `${s.status === "stopped" ? "Stopped" : "Last changed"} ${whenLabel(s.changedAt, now)}` } : { label: "Added by", value: "Your clinic", note: "A clinic patient: the records are added in Context" }; const scopes = s && s.status !== "stopped" ? SCOPE_ORDER.map(([scope, label]) => ({ scope, label, shared: s.scopes.includes(scope) })) : null; const scopesNote = !s ? "Invite them to MARMAR Health to see medicines, doses and check-ins." : s.status === "paused" ? "Paused: new data is hidden until sharing resumes." : s.status === "stopped" ? "Live access has ended. You keep the checks and check-ins you saved." : null; const calls = !s ? "Needs MARMAR Health" : s.status !== "connected" ? "Not now" : s.allowsCheckInCalls ? "Yasmar may call" : "Not allowed"; return { sharedBy, scopes, scopesNote, calls, quietHours: quietHoursLabel(p), identity: identityLabel(p), withheldSome: !!s?.withheldSome }; } /** A check-in row's notes: "6 notes · 2 new". */ export function notesLabel(c: Pick): string { if (!c.noteCount) return "None"; return `${c.noteCount} ${c.noteCount === 1 ? "note" : "notes"}${c.unreadNotes ? ` · ${c.unreadNotes} new` : ""}`; } /** "Ready", "Not yet" while a summary can still come, or "None" for a call that ended without one. */ export function summaryLabel(c: Pick): "Ready" | "Not yet" | "None" { if (c.summaryReady) return "Ready"; return ["declined", "not_reached", "cancelled", "identity_not_confirmed", "expired"].includes(c.status) ? "None" : "Not yet"; } /** Only an open check-in that has not started can be cancelled from Context (as the call view offers). */ export const cancellable = (status: string) => ["sent", "ringing", "later", "missed"].includes(status);