import type { PatientSummary, ShareScope, Sharing } from "../api/types"; import type { Pronoun } from "../api/types"; import { dayLabel, listNames, pronouns, scopeLabels, statusLabels } from "./format"; /** PAT-2: an urgent check-in flag, unread check-in notes, new information, then the rest. */ export function attentionRank(p: PatientSummary): number { if (p.attention.urgent) return 0; if (p.attention.unreadNotes > 0) return 1; if (p.attention.newSinceLastCheck) return 2; return 3; } /** PAT-2 order; within each group, the most recent activity first. */ export function sortPatients(list: PatientSummary[]): PatientSummary[] { return [...list].sort( (a, b) => attentionRank(a) - attentionRank(b) || new Date(b.lastActivityAt).getTime() - new Date(a.lastActivityAt).getTime() || a.name.localeCompare(b.name), ); } export const normalizeName = (s: string) => s .normalize("NFD") .replace(/\p{Diacritic}/gu, "") .toLowerCase() .replace(/\s+/g, " ") .trim(); /** Search by name: every word typed must start a word of the name, in any order. */ export function searchPatients(list: T[], query: string): T[] { const words = normalizeName(query).split(" ").filter(Boolean); if (!words.length) return list; return list.filter((p) => { const parts = normalizeName(p.name).split(" "); return words.every((w) => parts.some((part) => part.startsWith(w))); }); } export function attentionLabel(p: PatientSummary): string | null { if (p.attention.urgent) return "Urgent check-in note"; if (p.attention.unreadNotes) return `${p.attention.unreadNotes} unread check-in ${p.attention.unreadNotes === 1 ? "note" : "notes"}`; if (p.attention.newSinceLastCheck) return "New information since your last check"; return null; } export type PatientTab = "overview" | "check" | "check_ins" | "shared" | "records" | "activity"; export const tabNames: Record = { overview: "Overview", check: "Check", check_ins: "Check-ins", shared: "From MARMAR Health", records: "Records", activity: "Activity", }; const live = (s: Sharing | null) => !!s && s.status !== "stopped"; /** * PAT-3 tabs, driven by what the patient shares (INV-3). A connected patient who shares * neither check-ins nor calls shows no Check-ins tab; nothing shared hides the app view. * Activity is not a tab: it is in the page's rail, beside every tab. */ export function visibleTabs(p: Pick): PatientTab[] { const tabs: PatientTab[] = ["overview"]; if (p.kind === "connected" && live(p.sharing) && p.sharing!.scopes.length) tabs.push("shared"); tabs.push("check"); if ( p.kind === "clinic_added" || (live(p.sharing) && (p.sharing!.allowsCheckInCalls || p.sharing!.scopes.includes("check_ins"))) ) tabs.push("check_ins"); tabs.push("records"); return tabs; } /** The shared sections to show and to fetch. Stopped sharing shows none (INV-8). */ export function sharedSections(sharing: Sharing | null): ShareScope[] { if (!live(sharing)) return []; const order: ShareScope[] = ["medicines", "doses", "check_ins", "records"]; return order.filter((s) => sharing!.scopes.includes(s)); } /** "Shared by Mariam and Funke". */ export const sharedByLine = (s: Sharing) => (s.sharedBy.length ? `Shared by ${listNames(s.sharedBy)}` : "Shared from MARMAR Health"); /** "shares medicines, doses taken, check-ins and records" (INV-11). */ export function sharesLine(scopes: ShareScope[]): string { const order: ShareScope[] = ["medicines", "doses", "check_ins", "records"]; const named = order.filter((s) => scopes.includes(s)).map((s) => scopeLabels[s]); return named.length ? `shares ${listNames(named)}` : "shares nothing yet"; } export const sharingLabels = { connected: { label: "Connected", tone: "green" }, paused: { label: "Paused", tone: "amber" }, stopped: { label: "Sharing stopped", tone: "" }, } as const; export type TileCell = { value: string; note: string }; export type TileCells = { medicines: TileCell; doses: TileCell; lastCheckIn: TileCell }; const NOT_SHARED: TileCell = { value: "Not shared", note: "Not part of this share" }; const PAUSED: TileCell = { value: "Paused", note: "Hidden until sharing resumes" }; const UNKNOWN: TileCell = { value: "Shared", note: "Open the patient to see" }; /** "Completed", "Missed": a check-in's state in words. */ export function checkInStateLabel(status: string): string { const known = (statusLabels as Record)[status]; if (known) return known.label; const words = status.replace(/_/g, " "); return words.charAt(0).toUpperCase() + words.slice(1); } /** * SHR-1: the three tiles, each from what the patient shares. An area that is not shared says * "Not shared"; while sharing is paused, nothing new shows. Null when sharing has stopped. */ export function tileValues(p: PatientSummary, now = new Date()): TileCells | null { const sharing = p.sharing; if (!sharing || sharing.status === "stopped") return null; if (sharing.status === "paused") return { medicines: PAUSED, doses: PAUSED, lastCheckIn: PAUSED }; const scopes = sharing.scopes; const their = pronouns(p.pronoun).possessive; const medicines: TileCell = !scopes.includes("medicines") ? NOT_SHARED : p.medicines ? { value: String(p.medicines.count), note: p.medicines.count ? `From ${their} app` : `None listed in ${their} app` } : UNKNOWN; const d = p.doses; const doses: TileCell = !scopes.includes("doses") ? NOT_SHARED : !d ? UNKNOWN : !d.expected ? { value: "None due", note: "No doses were due this week" } : { value: `${d.taken} of ${d.expected}`, note: d.taken && d.onTime === d.taken ? "Taken on time" : `${d.onTime} on time` }; const last = p.lastCheckIn; const lastCheckIn: TileCell = last ? { value: dayLabel(last.at, now), note: [last.kind === "yasmar_call" ? "Yasmar call" : "Daily check-in", last.status ? checkInStateLabel(last.status) : null].filter(Boolean).join(" ยท ") } : scopes.includes("check_ins") || sharing.allowsCheckInCalls ? { value: "None yet", note: "No check-in yet" } : NOT_SHARED; return { medicines, doses, lastCheckIn }; } /** "with her daughter Funke" (INV-11); they/them when the patient's pronouns are not stated. */ export function withLine(person: { name: string; relation: string } | null, pronoun: Pronoun | null): string { if (!person) return ""; return `with ${pronouns(pronoun).possessive} ${person.relation} ${person.name}`; }