import type { CheckInStatus, NoteKind, Pronoun, Tier } from "../api/types";
import { checkInPills, dotTone } from "./pills";
const months = "Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" ");
const days = "Sun Mon Tue Wed Thu Fri Sat".split(" ");
const pad = (n: number) => String(n).padStart(2, "0");
/** "10:42" in the viewer's clock. */
export function clock(iso: string): string {
const d = new Date(iso);
return Number.isNaN(d.getTime()) ? "" : `${pad(d.getHours())}:${pad(d.getMinutes())}`;
}
const sameDay = (a: Date, b: Date) =>
a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
/** "Today", "Yesterday", "Tue 28 Oct" or "28 Oct 2025". */
export function dayLabel(iso: string, now = new Date()): string {
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return "";
if (sameDay(d, now)) return "Today";
const yesterday = new Date(now);
yesterday.setDate(now.getDate() - 1);
if (sameDay(d, yesterday)) return "Yesterday";
const tomorrow = new Date(now);
tomorrow.setDate(now.getDate() + 1);
if (sameDay(d, tomorrow)) return "Tomorrow";
const base = `${d.getDate()} ${months[d.getMonth()]}`;
return d.getFullYear() === now.getFullYear() ? `${days[d.getDay()]} ${base}` : `${base} ${d.getFullYear()}`;
}
/** "just now", "4 min ago", "Today 10:42", "Tue 28 Oct 10:42". */
export function whenLabel(iso: string, now = new Date()): string {
const d = new Date(iso);
const ms = now.getTime() - d.getTime();
if (Number.isNaN(ms)) return "";
if (ms >= 0 && ms < 60_000) return "just now";
if (ms >= 0 && ms < 3_600_000) return `${Math.floor(ms / 60_000)} min ago`;
return `${dayLabel(iso, now)} ${clock(iso)}`;
}
/** "as of 10:42", or with the day when it is not today (SHR-6). */
export function asOfLabel(iso: string, now = new Date()): string {
const day = dayLabel(iso, now);
return day === "Today" ? `as of ${clock(iso)}` : `as of ${day} ${clock(iso)}`;
}
/** SHR-6: changed after the clinician last looked. Nothing is new on a first look. */
export function isNewSince(changedAt: string, lastLookedAt: string | null): boolean {
if (!lastLookedAt) return false;
return new Date(changedAt).getTime() > new Date(lastLookedAt).getTime();
}
export const firstName = (name: string) => name.trim().split(/\s+/)[0] || name;
/** Pronouns for a person whose pronouns may not be stated: they/them unless known. */
export function pronouns(p: Pronoun | null | undefined) {
if (p === "he") return { subject: "he", object: "him", possessive: "his" };
if (p === "she") return { subject: "she", object: "her", possessive: "her" };
return { subject: "they", object: "them", possessive: "their" };
}
/** CHK-6: each tier with its time. None is worded as an instruction. */
export const tiers: Record<Tier, { label: string; when: string; tone: string; rank: number }> = {
get_help_now: { label: "Urgent review", when: "Before the next dose", tone: "red", rank: 0 },
today: { label: "Review today", when: "By the end of clinic today", tone: "amber", rank: 1 },
this_week: { label: "Review this week", when: "Within 7 days", tone: "amber", rank: 2 },
good_to_know: { label: "Good to know", when: "No deadline", tone: "", rank: 3 },
};
export const tierOf = (tier: string) => tiers[tier as Tier] ?? tiers.good_to_know;
/** YCI-28: each state's words, with the dot tone older chips use; the pills in pills.ts carry the colours. */
export const statusLabels = Object.fromEntries(
Object.entries(checkInPills).map(([status, pill]) => [status, { label: pill.label, tone: dotTone(pill.tone) }]),
) as Record<CheckInStatus, { label: string; tone: string }>;
/** YCI-24. */
export const noteKinds: Record<NoteKind, string> = {
symptom: "Symptom",
how_taken: "How a medicine is taken",
not_started: "Medicine not started",
side_effect: "Side effect",
allergy_history: "Allergy history",
other_medicines: "Other medicines",
measurement: "Measurement",
patient_question: "Patient's question",
red_flag: "Red flag",
not_understood: "Not understood",
};
/** "01:12" from seconds into the call. */
export function callOffset(seconds: number): string {
const s = Math.max(0, Math.round(seconds));
return `${pad(Math.floor(s / 60))}:${pad(s % 60)}`;
}
/** "Mariam and Funke", "Mariam, Funke and Bola". */
export function listNames(names: string[]): string {
if (names.length <= 1) return names[0] ?? "";
return `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`;
}
// Schedules in plain words (SHR-2): never a code such as twice_daily or 2/day. The backend's
// src/clinician/sharing/schedule-words.ts keeps the same table.
const SCHEDULE_WORDS: Record<string, string> = {
once_daily: "Once a day", daily: "Once a day", qd: "Once a day",
twice_daily: "Twice a day", bid: "Twice a day",
three_times_daily: "Three times a day", tid: "Three times a day",
four_times_daily: "Four times a day", qid: "Four times a day",
every_morning: "Every morning", every_evening: "Every evening", every_night: "Every night",
every_x_hours: "Every few hours", weekly: "Once a week", biweekly: "Every two weeks", monthly: "Once a month",
as_needed: "When needed", prn: "When needed", custom: "Own schedule",
};
const TIMES = ["", "Once", "Twice", "Three times", "Four times", "Five times", "Six times"];
/** "Twice a day" for twice_daily or 2/day; words already plain stay as they are. */
export function scheduleLabel(value: string | null | undefined): string | null {
const raw = value?.trim();
if (!raw) return null;
const key = raw.toLowerCase();
if (SCHEDULE_WORDS[key]) return SCHEDULE_WORDS[key];
const hours = /^every_(\d{1,2})_hours?$/.exec(key);
if (hours) return `Every ${Number(hours[1])} hours`;
const perDay = /^(\d+(?:\.\d+)?)\s*\/\s*day$/.exec(key);
if (perDay) {
const times = Number(perDay[1]);
return Number.isInteger(times) && times >= 1 && times < TIMES.length ? `${TIMES[times]} a day` : `${perDay[1]} times a day`;
}
if (/^[a-z0-9]+(?:_[a-z0-9]+)+$/.test(key)) {
const words = key.replace(/_/g, " ");
return words.charAt(0).toUpperCase() + words.slice(1);
}
return raw;
}
/** A fact's detail ("500 mg · 1 tablet · 2/day") with its schedule in words. */
export const factDetail = (detail: string) =>
detail
.split(" · ")
.map((part) => (/^\d+(?:\.\d+)?\s*\/\s*day$/.test(part.trim()) ? (scheduleLabel(part) ?? part) : part))
.join(" · ");
export const scopeLabels = {
medicines: "medicines",
doses: "doses taken",
check_ins: "check-ins",
records: "records",
} as const;