import type { Task } from 'fhir/r4'; import { getTaskMeasurement } from './measurementTask'; // What a plan activity IS, in the words the drawing uses. // // Check your blood sugar and log the reading Daily Measurement - mmol/L // Weigh yourself and log it Weekly Measurement - lb Stops when the goal is reached // Take metformin 1000mg, twice daily Standing instruction - not something to tick off // // THE THIRD IS THE ONE THAT MATTERS. A standing instruction must never be drawn or built as something to // tick off: it is how the patient lives, not a job with an end. Ticking it would ask them to declare // their medication finished, and an unanswerable row sitting in an attention list is how the list stops // being read. // // `tickable` LIVES HERE RATHER THAN IN EACH SURFACE, because the plan editor, the patient's task list // and the task modal all draw activities, and three places deciding this separately is how one of them // ends up with a checkbox on a medication. const DUE_MODE_EXTENSION = 'https://akashichealthrecords.com/fhir/extensions/task-due-mode'; const RECURRENCE_FREQUENCY_EXTENSION = 'https://akashichealthrecords.com/fhir/extensions/task-recurrence-frequency'; export type PlanActivityKind = 'measurement' | 'instruction'; export interface PlanActivityDescription { kind: PlanActivityKind; /** The lead word - "Daily", "Weekly", "Standing", "One-off". */ label: string; /** What follows it. */ detail: string; /** Whether the patient can answer it at all. False for a standing instruction. */ tickable: boolean; stopsWhenGoalReached: boolean; } const FREQUENCY_LABELS: Record = { daily: 'Daily', weekly: 'Weekly', monthly: 'Monthly', annually: 'Yearly', }; const readCode = (task: Task, url: string): string | undefined => task.extension?.find((extension) => extension.url === url)?.valueCode; /** * A standing instruction is one with no due date and no recurrence: nothing says when it is answered, * because it is not answered. An instruction WITH a due date is an ordinary job - "book your * three-month review" - and must stay tickable, or a real task would have no way to be completed. */ /** * Standing when it SAYS so, and otherwise when nothing about it says it is due. * * The explicit mode is checked first and on its own, because it is the only one of these that cannot * be produced by accident. The inferred form stays for everything written before the mode existed and * for anything that arrives without one - but it is the weaker test: a single stamped * `restriction.period.end` is enough to turn a standing instruction into a job with a deadline, which * is how one became tickable on every surface in the first place. */ const isStandingInstruction = (task: Task): boolean => { if (readCode(task, DUE_MODE_EXTENSION) === 'standing') return true; return !task.restriction?.period?.end && !readCode(task, RECURRENCE_FREQUENCY_EXTENSION) && readCode(task, DUE_MODE_EXTENSION) !== 'date'; }; /** * A plan's standing instruction: on a care plan and not answerable. The patient's list, its counts * and Home ask this, so it is never drawn or counted as a task to complete. A standing task that is * not on a plan (a redraw asking for a booking) is still a job and stays one. */ export const isStandingPlanInstruction = (task: Task): boolean => (task.basedOn ?? []).some((reference) => reference.reference?.startsWith('CarePlan/')) && !getTaskMeasurement(task) && isStandingInstruction(task); export interface DescribeActivityOptions { /** Whether this measurement's series stops once the goal it serves is met. */ stopsWhenGoalReached?: boolean; } export const describeActivity = ( task: Task, options: DescribeActivityOptions = {}, ): PlanActivityDescription => { const measurement = getTaskMeasurement(task); const stopsWhenGoalReached = options.stopsWhenGoalReached === true; if (measurement) { const frequency = readCode(task, RECURRENCE_FREQUENCY_EXTENSION); return { kind: 'measurement', label: (frequency && FREQUENCY_LABELS[frequency]) || 'One-off', detail: `Measurement · ${measurement.unit}`, // Always answerable: a measurement is answered with a number, which is the whole point. tickable: true, stopsWhenGoalReached, }; } if (isStandingInstruction(task)) { return { kind: 'instruction', label: 'Standing', detail: 'instruction · not something to tick off', tickable: false, stopsWhenGoalReached: false, }; } return { kind: 'instruction', label: 'One-off', detail: 'instruction', tickable: true, stopsWhenGoalReached: false, }; };