import type { Extension, Task } from 'fhir/r4'; // A checklist inside a custom task. The practice writes the steps on the ActivityDefinition; the server // copies them onto each Task by name; only the patient ticks them, and the server stamps each tick. // The same reader serves the template and the task, so the two cannot disagree about what a step is. export const AKASHIC_TASK_CHECKLIST_STEP_EXTENSION = 'https://akashichealthrecords.com/fhir/extensions/task-checklist-step'; export const AKASHIC_TASK_CHECKLIST_TICK_EXTENSION = 'https://akashichealthrecords.com/fhir/extensions/task-checklist-tick'; export interface ChecklistStep { id: string; instruction: string; /** When to do it, in the practice's words. Text only: it schedules nothing. */ timing?: string; } export interface ChecklistTick { step: string; at: string; } const sub = (extension: Extension, url: string) => extension.extension?.find((part) => part.url === url); /** The steps in authored order. A step with no id or no instruction is not a step. */ export const readChecklist = (resource: { extension?: Extension[] } | undefined): ChecklistStep[] => { const seen = new Set(); const steps: ChecklistStep[] = []; for (const extension of resource?.extension ?? []) { if (extension.url !== AKASHIC_TASK_CHECKLIST_STEP_EXTENSION) continue; const id = sub(extension, 'id')?.valueString?.trim(); const instruction = sub(extension, 'instruction')?.valueString?.trim(); const timing = sub(extension, 'timing')?.valueString?.trim(); if (!id || !instruction || seen.has(id)) continue; seen.add(id); steps.push({ id, instruction, ...(timing ? { timing } : {}) }); } return steps; }; export const readChecklistTicks = (task: { extension?: Extension[] } | undefined): ChecklistTick[] => (task?.extension ?? []).flatMap((extension) => { if (extension.url !== AKASHIC_TASK_CHECKLIST_TICK_EXTENSION) return []; const step = sub(extension, 'step')?.valueString?.trim(); const at = sub(extension, 'at')?.valueDateTime; return step && at ? [{ step, at }] : []; }); const stepExtension = (step: ChecklistStep): Extension => ({ url: AKASHIC_TASK_CHECKLIST_STEP_EXTENSION, extension: [ { url: 'id', valueString: step.id }, { url: 'instruction', valueString: step.instruction.trim() }, ...(step.timing?.trim() ? [{ url: 'timing', valueString: step.timing.trim() }] : []), ], }); /** * The extensions with exactly these steps, in this order. Every other extension - the file request, * the trigger, anything added later - is kept where it was. */ export const withChecklistSteps = (extensions: Extension[], steps: ChecklistStep[]): Extension[] => [ ...extensions.filter((extension) => extension.url !== AKASHIC_TASK_CHECKLIST_STEP_EXTENSION), ...steps.filter((step) => step.instruction.trim()).map(stepExtension), ]; /** A new step id. Stable once written, so a tick keeps pointing at its step when steps are reordered. */ export const newChecklistStepId = (): string => `step-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`; /** * The patient's tick, as the request the server accepts: stored ticks as they were plus this one. * The server stamps the time, so `at` here is a placeholder it replaces. */ export const withTickedStep = (task: Task, stepId: string, now: string): Task => { const ticks = readChecklistTicks(task); if (ticks.some((tick) => tick.step === stepId)) return task; const tick: Extension = { url: AKASHIC_TASK_CHECKLIST_TICK_EXTENSION, extension: [{ url: 'step', valueString: stepId }, { url: 'at', valueDateTime: now }], }; return { ...task, extension: [...(task.extension ?? []), tick] }; }; export const everyStepTicked = (task: Task): boolean => { const ticked = new Set(readChecklistTicks(task).map((tick) => tick.step)); return readChecklist(task).every((step) => ticked.has(step.id)); };