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<PatientDetail, "lastCheckId">, 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<typeof tiles>) => 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<PatientDetail, "kind" | "sharing" | "quietHours" | "quietHoursKnown">): 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<PatientDetail, "identityOnFile" | "dateOfBirth" | "birthYear">): 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<CheckInListItem, "noteCount" | "unreadNotes">): 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<CheckInListItem, "status" | "summaryReady">): "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);