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<T extends { name: string }>(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<PatientTab, string> = {
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<PatientSummary, "kind" | "sharing">): 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<string, { label: string }>)[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}`;
}