marmar-context / web / context / src / logic / pills.ts
pills.ts
Raw
import type { CheckInStatus, ContextRecord, PatientSummary } from "../api/types";

/** Tinted pills: each state has its own colour and icon, and always its words (never colour alone). */
export type PillTone = "grey" | "indigo" | "green" | "amber" | "purple" | "blue" | "pink" | "red" | "red-outline";
export type PillIcon =
  | "sent" | "ringing" | "live" | "completed" | "missed" | "later" | "not_reached" | "expired" | "declined"
  | "identity" | "ended_early" | "cancelled" | "urgent"
  | "queued" | "reading" | "read" | "failed" | "held_back"
  | "connected" | "paused" | "stopped" | "clinic" | "draft";
export type Pill = { label: string; tone: PillTone; icon: PillIcon; meaning: string };

/** YCI-28: every check-in state the backend sends (docs/api-contract.md), in the order a call moves. */
export const checkInPills: Record<CheckInStatus, Pill> = {
  sent: { label: "Sent", tone: "grey", icon: "sent", meaning: "Asked. It waits in MARMAR Health until it may ring." },
  ringing: { label: "Ringing", tone: "indigo", icon: "ringing", meaning: "Yasmar is ringing the patient's phone." },
  in_progress: { label: "Live", tone: "green", icon: "live", meaning: "On the call now. Notes arrive as they are made." },
  completed: { label: "Completed", tone: "green", icon: "completed", meaning: "The call finished, with notes and a summary." },
  missed: { label: "Missed", tone: "amber", icon: "missed", meaning: "Nobody answered. It can ring again inside the window." },
  later: { label: "Later", tone: "blue", icon: "later", meaning: "The patient chose a later time inside the window." },
  not_reached: { label: "Not reached", tone: "amber", icon: "not_reached", meaning: "The window closed without a call." },
  declined: { label: "Declined", tone: "purple", icon: "declined", meaning: "The patient said no. Nothing was asked." },
  identity_not_confirmed: {
    label: "Identity not confirmed",
    tone: "pink",
    icon: "identity",
    meaning: "Whoever answered could not confirm who they were, so no questions were asked.",
  },
  ended_early: { label: "Ended early", tone: "amber", icon: "ended_early", meaning: "The call stopped before the end. It can carry on while the window is open." },
  cancelled: { label: "Cancelled", tone: "red-outline", icon: "cancelled", meaning: "Cancelled by you, or because sharing or calls were turned off." },
};

/** Urgent is a flag beside the state, never a state of its own. */
export const urgentPill: Pill = { label: "Urgent", tone: "red", icon: "urgent", meaning: "A red-flag note came up on the call. Shown beside the state." };

/** CHK-5: a finding's clinician wording the clinical team has not signed off (wording "derived" or "engine"). Quiet, beside the tier. */
export const draftWordingPill: Pill = {
  label: "Not yet signed off for clinicians",
  tone: "grey",
  icon: "draft",
  meaning: "MARMAR's clinical team has not signed off this wording for clinicians yet. The rule itself is in Why this finding.",
};

// The backend's own type also names `expired`; the contract says it sends not_reached instead.
const extra: Record<string, Pill> = {
  expired: { label: "Expired", tone: "amber", icon: "expired", meaning: "The window closed without a call." },
};

/** The pill for a state, or a grey one in plain words for a state this screen does not know yet. */
export function checkInPill(status: string): Pill {
  const known = (checkInPills as Record<string, Pill>)[status] ?? extra[status];
  if (known) return known;
  const words = status.replace(/_/g, " ");
  return { label: words.charAt(0).toUpperCase() + words.slice(1), tone: "grey", icon: "sent", meaning: "" };
}

/** The dot tone the older status chips use (green, amber, red or grey), from a pill's colour. */
export function dotTone(tone: PillTone): string {
  if (tone === "green" || tone === "amber") return tone;
  if (tone === "red" || tone === "red-outline") return "red";
  return "";
}

/** REC-4, REC-7: a record's reading state. Held back comes first: it is not used until the clinician decides. */
export function recordPill(r: Pick<ContextRecord, "reading" | "heldBack" | "lineCount">): Pill {
  if (r.heldBack) return { label: "Held back", tone: "pink", icon: "held_back", meaning: "It may name another patient. It is not used until you decide." };
  switch (r.reading) {
    case "read":
      return { label: `Read · ${r.lineCount} ${r.lineCount === 1 ? "line" : "lines"}`, tone: "green", icon: "read", meaning: "Read. Its lines can be reviewed on the Check tab." };
    case "failed":
      return { label: "Not read", tone: "red-outline", icon: "failed", meaning: "It could not be read." };
    case "reading":
      return { label: "Reading", tone: "indigo", icon: "reading", meaning: "Being read now." };
    default:
      return { label: "Waiting to be read", tone: "grey", icon: "queued", meaning: "In line to be read." };
  }
}

/** The patient's state on the right of the header: how the clinician has them. */
export function patientStatePill(p: Pick<PatientSummary, "sharing">): Pill {
  const s = p.sharing;
  if (!s) return { label: "Clinic-added", tone: "grey", icon: "clinic", meaning: "You added this patient and the records." };
  if (s.status === "paused") return { label: "Paused", tone: "amber", icon: "paused", meaning: "New data is hidden until sharing resumes." };
  if (s.status === "stopped") return { label: "Sharing stopped", tone: "grey", icon: "stopped", meaning: "Live access has ended." };
  return { label: "Connected", tone: "green", icon: "connected", meaning: "Shared from MARMAR Health." };
}