import type { CodeableConcept, Goal, Observation } from 'fhir/r4'; // A goal's progress, computed from the readings that exist. // // DERIVED, NEVER STORED. A stored progress value is a frozen snapshot that goes wrong the moment // anything else writes a reading - the same reason entitlements are derived rather than stored. Every // surface showing this goal (the plan, the goals page, a collaboration team holding it) computes it from // the same observations, which is why they all move together and none of them can disagree. export const AKASHIC_GOAL_DIRECTION_EXTENSION = 'https://akashichealthrecords.com/fhir/extensions/goal-direction'; export const AKASHIC_GOAL_START_VALUE_EXTENSION = 'https://akashichealthrecords.com/fhir/extensions/goal-start-value'; /** When the goal was set, stamped by the server; a reading taken before it is not this goal's progress. */ export const AKASHIC_GOAL_SET_AT_EXTENSION = 'https://akashichealthrecords.com/fhir/extensions/goal-set-at'; export type GoalDirection = 'up' | 'down'; export interface GoalProgress { startValue: number; targetValue: number; /** The latest reading, or `undefined` when none has been taken yet. */ currentValue: number | undefined; direction: GoalDirection; /** How far along, 0 to 1. Clamped at both ends. */ fraction: number; reached: boolean; } /** * Which way is progress, from the numbers the goal is set with: derived, never asked (ruling * 2026-09-02). 350 lb to 200 lb is downward, 4,000 steps to 8,000 upward. An equal pair is no goal. */ export const deriveGoalDirection = (startValue: number, targetValue: number): GoalDirection | undefined => { if (!Number.isFinite(startValue) || !Number.isFinite(targetValue) || startValue === targetValue) return undefined; return targetValue < startValue ? 'down' : 'up'; }; /** * Which way is progress, as stored on the goal when it was set. An unrecognised value is refused * rather than guessed. */ export const getGoalDirection = (goal: Goal | undefined): GoalDirection | undefined => { const value = goal?.extension?.find( (extension) => extension.url === AKASHIC_GOAL_DIRECTION_EXTENSION, )?.valueCode; return value === 'up' || value === 'down' ? value : undefined; }; /** Where the patient started, which is what makes a fraction meaningful. */ export const getGoalStartValue = (goal: Goal | undefined): number | undefined => goal?.extension?.find( (extension) => extension.url === AKASHIC_GOAL_START_VALUE_EXTENSION, )?.valueQuantity?.value; const sameMeasure = (a: CodeableConcept | undefined, b: CodeableConcept | undefined): boolean => { const codes = (concept: CodeableConcept | undefined) => (concept?.coding ?? []).map((coding) => `${coding.system ?? ''}|${coding.code ?? ''}`); return codes(a).some((code) => codes(b).includes(code)); }; /** * The latest reading of this goal's measure. * * LATEST, not first and not best. Showing the best reading would flatter the patient and never come * back down; showing the first would never move at all. And the measure has to match - every * observation a patient has is not this goal's business, or logging a blood sugar would move a weight * goal. */ /** * Is this reading in the goal's unit? The goal's unit is matched against the reading's display unit or * its UCUM code: the vitals form writes "kilogram" with code "kg", the patient's log writes "kg", and * both are the same unit. A reading with no unit, or a goal with none, is taken (see below). */ export const isInGoalUnit = (observation: Observation, goalUnit: string | undefined): boolean => { const quantity = observation.valueQuantity; if (!goalUnit || !quantity?.unit) return true; return quantity.unit === goalUnit || quantity.code === goalUnit; }; /** * When a reading was taken, for ordering. A form may save a reading with no effective time; it is then * placed by when it was issued, else recorded, rather than sorting as the oldest reading there is. */ export const readingTime = (observation: Observation): string => observation.effectiveDateTime ?? observation.effectivePeriod?.start ?? observation.effectivePeriod?.end ?? observation.effectiveInstant ?? observation.effectiveTiming?.event?.[0] ?? observation.issued ?? observation.meta?.lastUpdated ?? ''; /** Oldest first; a reading with no usable time sorts first. */ export const byReadingTime = (a: Observation, b: Observation): number => { const at = (observation: Observation) => { const t = Date.parse(readingTime(observation)); return Number.isNaN(t) ? 0 : t; }; return at(a) - at(b); }; /** A reading entered in error was withdrawn: it is never a goal's evidence. A corrected one still is. */ export const isValidReading = (observation: Observation): boolean => observation.status !== 'entered-in-error'; /** A goal set again is a new episode: a reading from before it was set is not one of its readings. */ export const isInGoalEpisode = (goal: Goal, observation: Observation): boolean => { const setAtValue = goal.extension?.find((extension) => extension.url === AKASHIC_GOAL_SET_AT_EXTENSION)?.valueDateTime; const setAt = setAtValue ? Date.parse(setAtValue) : Number.NaN; return Number.isNaN(setAt) || Date.parse(readingTime(observation)) >= setAt; }; /** * A goal reached is done and retired, so its episode ends at the first reading that reached it: a later * reading belongs to whatever goal is current. Expects the readings oldest first. A goal that cannot be * reached (no direction, target or start, or a zero range; see goalProgress) keeps every reading. */ export const untilReached = (goal: Goal, readings: Observation[]): Observation[] => { const direction = getGoalDirection(goal); const target = goal.target?.[0]?.detailQuantity?.value; const from = getGoalStartValue(goal); if (!direction || typeof target !== 'number' || typeof from !== 'number' || from === target) return readings; const reachedAt = readings.findIndex(({ valueQuantity }) => typeof valueQuantity?.value === 'number' && (direction === 'down' ? valueQuantity.value <= target : valueQuantity.value >= target)); return reachedAt === -1 ? readings : readings.slice(0, reachedAt + 1); }; const latestReading = (goal: Goal, observations: Observation[]): number | undefined => { const measure = goal.target?.[0]?.measure; const goalUnit = goal.target?.[0]?.detailQuantity?.unit; const matching = observations .filter((observation) => sameMeasure(observation.code, measure)) .filter(isValidReading) .filter((observation) => isInGoalEpisode(goal, observation)) .filter((observation) => typeof observation.valueQuantity?.value === 'number') // A reading in a different unit is not this goal's reading. 112 kg against a goal stated in // pounds would move the bar to 112 lb - a wrong number that looks entirely right, and the same // trap the measurement field's unit label exists to shut on the way in. Nothing is converted: // a silent conversion is a second place for the two to disagree. // // Only when BOTH are stated. An observation with no unit is far more likely to be one of ours // written before the unit was carried than a genuine mismatch, and blanking a working goal is // the worse failure of the two. .filter((observation) => isInGoalUnit(observation, goalUnit)) .sort(byReadingTime); const episode = untilReached(goal, matching); return episode[episode.length - 1]?.valueQuantity?.value; }; /** * How far along this goal is, or `undefined` when it cannot honestly be drawn. * * IT RETURNS NOTHING RATHER THAN GUESSING, in three cases: no direction (above), no target value, and a * start equal to the target. The last would divide by a zero range and produce `Infinity` or `NaN`, and * a bar showing either is worse than showing none - maintenance, holding a value rather than attaining * one, is a different intent and is deliberately not modelled as an attainment goal. */ export const goalProgress = ( goal: Goal | undefined, observations: Observation[], ): GoalProgress | undefined => { if (!goal) return undefined; const direction = getGoalDirection(goal); const targetValue = goal.target?.[0]?.detailQuantity?.value; const startValue = getGoalStartValue(goal); if (!direction || typeof targetValue !== 'number' || typeof startValue !== 'number') { return undefined; } const range = targetValue - startValue; if (range === 0) { return undefined; } const currentValue = latestReading(goal, observations); // No reading yet is nought, not "unknown drawn as something". `currentValue` stays undefined so a // surface can say "no readings yet" rather than claiming the patient is at their starting weight. const fraction = currentValue === undefined ? 0 : Math.min(1, Math.max(0, (currentValue - startValue) / range)); const reached = currentValue !== undefined && (direction === 'down' ? currentValue <= targetValue : currentValue >= targetValue); return { startValue, targetValue, currentValue, direction, fraction, reached }; };