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 = { 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; /** YCI-24. */ export const noteKinds: Record = { 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 = { 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;