marmar-context / web / context / src / logic / urgent.ts
urgent.ts
Raw
import type { CheckInStatus, Tier } from "../api/types";
import { firstName } from "./format";

type TieredFinding = { id: string; tier: string };

/** Findings that cannot wait for a check-in: Urgent review (CHK-6). */
export const urgentFindings = <T extends TieredFinding>(findings: T[]) =>
  findings.filter((f) => f.tier === "get_help_now");

/** The clinician's "I will speak to Mariam today", kept for the day it was given. */
export type CallConfirmation = { patientId: string; confirmedAt: string };

const localDate = (d: Date, offsetMinutes: number) =>
  new Date(d.getTime() + offsetMinutes * 60_000).toISOString().slice(0, 10);

/** A confirmation counts only on the clinic's day it was given. */
export function confirmationValid(
  confirmation: CallConfirmation | null,
  patientId: string,
  now: Date,
  clinicOffset: number,
): boolean {
  if (!confirmation || confirmation.patientId !== patientId) return false;
  return localDate(new Date(confirmation.confirmedAt), clinicOffset) === localDate(now, clinicOffset);
}

export type UrgentGate =
  | { mode: "none"; canAskYasmar: true }
  | { mode: "call_first"; canAskYasmar: boolean; confirmLine: string; callLine: string };

/**
 * YCI-1a, urgent first: with an Urgent review finding, Context leads with "Call Mariam";
 * Ask Yasmar to check in opens only after "I will speak to Mariam today".
 */
export function urgentGate(input: {
  findings: TieredFinding[];
  patientId: string;
  patientName: string;
  confirmation: CallConfirmation | null;
  now: Date;
  clinicOffset: number;
}): UrgentGate {
  if (!urgentFindings(input.findings).length) return { mode: "none", canAskYasmar: true };
  const first = firstName(input.patientName);
  return {
    mode: "call_first",
    canAskYasmar: confirmationValid(input.confirmation, input.patientId, input.now, input.clinicOffset),
    confirmLine: `I will speak to ${first} today`,
    callLine: `Call ${first}`,
  };
}

const finished: CheckInStatus[] = ["completed", "declined", "identity_not_confirmed", "cancelled"];

/**
 * YCI-1a: a check-in linked to an Urgent review or Review today finding that has not
 * finished by the end of clinic hours raises "Mariam not reached. Call today."
 */
export function notReachedAlert(input: {
  status: CheckInStatus;
  linkedTiers: (Tier | string)[];
  patientName: string;
  clinicCloseAt: Date | null;
  now: Date;
}): string | null {
  const pressing = input.linkedTiers.some((t) => t === "get_help_now" || t === "today");
  if (!pressing || finished.includes(input.status)) return null;
  const late = input.status === "not_reached" || (input.clinicCloseAt !== null && input.now >= input.clinicCloseAt);
  return late ? `${firstName(input.patientName)} not reached. Call today.` : null;
}