import type { Appointment, Observation, Task } from "fhir/r4"; import { getCustomTaskDueDisplay, resolveTaskAppointment } from "../services/TaskService"; import { getTaskMeasurement } from "./measurementTask"; import { fhirCalendarDate } from "./practiceTimezone"; // A plan's steps are the Tasks based on it. No clock is read here: "today" comes from the practice calendar. /** A step that is finished, withdrawn or was never real is not something the patient still owes. */ const NOT_OUTSTANDING = new Set(["completed", "cancelled", "failed", "rejected", "entered-in-error", "draft"]); export const planOf = (task: Task): string | undefined => (task.basedOn ?? []) .map((reference) => reference.reference ?? "") .find((reference) => reference.startsWith("CarePlan/")) ?.split("/")[1]; /** The plan's steps the patient still owes, soonest due first; undated steps last. */ export const outstandingPlanTasks = (planId: string, tasks: readonly Task[]): Task[] => tasks .filter((task) => planOf(task) === planId && !NOT_OUTSTANDING.has(task.status ?? "")) .sort((left, right) => (left.restriction?.period?.end ?? "~").localeCompare(right.restriction?.period?.end ?? "~")); /** The instant or date a step is due by: its visit's start where it is due at one, else its own date. */ export const planTaskDueValue = (task: Task, appointments: readonly Appointment[]): string | undefined => resolveTaskAppointment(task, [...appointments])?.start || task.restriction?.period?.end; export interface PlanDue { dueToday: number; pastDue: number; } /** Owed steps due today and past due, on the practice calendar; an undated step is neither. */ export const planDueCounts = ( planId: string, tasks: readonly Task[], appointments: readonly Appointment[], today: string, zone: string, ): PlanDue => outstandingPlanTasks(planId, tasks).reduce((counts, task) => { const due = planTaskDueValue(task, appointments); if (!due) return counts; const day = fhirCalendarDate(due, zone); if (day === today) return { ...counts, dueToday: counts.dueToday + 1 }; if (day < today) return { ...counts, pastDue: counts.pastDue + 1 }; return counts; }, { dueToday: 0, pastDue: 0 }); /** "1 task past due · 2 due today", the deck's plan-card line. Nothing when nothing is due. */ export const planDueLine = ({ dueToday, pastDue }: PlanDue): string | undefined => { const parts = [ pastDue > 0 ? `${pastDue} ${pastDue === 1 ? "task" : "tasks"} past due` : "", dueToday > 0 ? `${dueToday} ${pastDue > 0 ? "" : dueToday === 1 ? "task " : "tasks "}due today` : "", ].filter(Boolean); return parts.length > 0 ? parts.join(" · ") : undefined; }; export interface PlanStep { id: string; title: string; /** The task list's own due line. */ due: string; /** The readings this step asked for, oldest first, when it asks for a measurement. */ readings: { id: string; value: number; when: string }[]; } const SETTLED = new Set(["final", "amended", "corrected"]); /** Settled readings based on the plan with the step's measurement code; never matched by name or day. */ export const stepReadings = (task: Task, planId: string, observations: readonly Observation[]) => { const measured = getTaskMeasurement(task)?.code.coding?.filter((coding) => coding.system && coding.code) ?? []; if (measured.length === 0) return []; return observations .filter((observation) => SETTLED.has(observation.status ?? "")) .filter((observation) => (observation.basedOn ?? []).some((reference) => reference.reference === `CarePlan/${planId}`)) .filter((observation) => (observation.code?.coding ?? []).some((coding) => measured.some((own) => own.system === coding.system && own.code === coding.code))) .filter((observation) => typeof observation.valueQuantity?.value === "number" && observation.effectiveDateTime) .map((observation) => ({ id: observation.id ?? "", value: observation.valueQuantity!.value!, when: observation.effectiveDateTime! })) .sort((left, right) => left.when.localeCompare(right.when)); }; export const planSteps = ( planId: string, tasks: readonly Task[], appointments: readonly Appointment[], observations: readonly Observation[], now: number = Date.now(), ): PlanStep[] => outstandingPlanTasks(planId, tasks).map((task) => ({ id: task.id ?? "", title: task.description?.trim() || task.code?.text?.trim() || "Something to do", due: getCustomTaskDueDisplay(task, resolveTaskAppointment(task, [...appointments], now), now), readings: stepReadings(task, planId, observations), }));