import type { CodeableConcept, Quantity } from 'fhir/r4'; // What kind of number a measurement or a goal is in. // // > Spencer, 2026-09-02: *"you cant pigeon hole only 6 items, this could be any number of things. Just // > make a select for generic measurement types like weight, temperature, inches, centimeters, etc"* // // AN EARLIER VERSION OF THIS LISTED SIX CLINICAL THINGS - body weight, steps, HbA1c - and that was the // wrong axis. A practitioner is not picking a disease, they are saying what KIND of number the patient // will write down. The description they type says what it is about ("measure your calf", "take your // temperature before bed"); this says how it is counted. That way the list covers anything anybody wants // to track without somebody having to add their case to it first. // // WHY IT IS A LIST AT ALL AND NOT A FREE-TEXT UNIT. A goal only moves when its readings carry the SAME // coding, so two visits free-typing "peak flow" would produce two codings and the bar would sit at // nought while the patient logged faithfully - a failure with no symptom. A fixed set of KINDS is broad // enough to say anything and still guarantees the goal and its readings match. // // EVERY ENTRY IS ONE NUMBER, which is the only real constraint. `Task.output` and // `Observation.valueQuantity` each hold a single quantity, so a blood pressure - two numbers - is // deliberately absent rather than recorded as half a reading. export interface TrackableMeasure { /** Stable key, used as a form value - never the display text, which is allowed to be reworded. */ key: string; /** What the clinician picks, in their words. */ label: string; /** The unit every number on that goal is stated in. Empty for a plain count. */ unit: string; code: CodeableConcept; } const MEASUREMENT_TYPE_SYSTEM = 'https://akashichealthrecords.com/fhir/CodeSystem/measurement-type'; /** * OUR code for the kind, plus a LOINC one where the quantity genuinely has a universal code. * * The local code is what makes matching exact - the goal and its readings are always built from the same * entry, so they always share it. The LOINC is added where it exists so a reading still means something * to a system that has never heard of us, which is the whole point of a record that travels. Matching * needs only ONE coding in common, so the extra one is free. * * A unit is NOT a measure. `Goal.target.measure` is the parameter being tracked, so the unit lives in * `detailQuantity.unit` where FHIR puts it, and the kind lives here. */ const measure = (code: string, display: string, loinc?: string): CodeableConcept => ({ coding: [ { system: MEASUREMENT_TYPE_SYSTEM, code, display }, ...(loinc ? [{ system: 'http://loinc.org', code: loinc, display }] : []), ], text: display, }); /** UCUM's pound. The shown unit stays "lb", which is what goals are stated in and matched on. */ export const UCUM_POUND = '[lb_av]'; /** A reading's quantity: pounds also carry their UCUM code; any other unit is written as it was. */ export const readingQuantity = (value: number, unit: string | undefined): Quantity => { if (unit === 'lb') return { value, unit, system: 'http://unitsofmeasure.org', code: UCUM_POUND }; return unit ? { value, unit } : { value }; }; export const TRACKABLE_MEASURES: TrackableMeasure[] = [ // Weight { key: 'weight-lb', label: 'Weight — pounds (lb)', unit: 'lb', code: measure('weight-lb', 'Weight in pounds', '29463-7') }, { key: 'weight-kg', label: 'Weight — kilograms (kg)', unit: 'kg', code: measure('weight-kg', 'Weight in kilograms', '29463-7') }, { key: 'weight-oz', label: 'Weight — ounces (oz)', unit: 'oz', code: measure('weight-oz', 'Weight in ounces') }, { key: 'weight-g', label: 'Weight — grams (g)', unit: 'g', code: measure('weight-g', 'Weight in grams') }, // Length and distance { key: 'length-in', label: 'Length — inches (in)', unit: 'in', code: measure('length-in', 'Length in inches') }, { key: 'length-cm', label: 'Length — centimetres (cm)', unit: 'cm', code: measure('length-cm', 'Length in centimetres') }, { key: 'length-ft', label: 'Length — feet (ft)', unit: 'ft', code: measure('length-ft', 'Length in feet') }, { key: 'distance-mi', label: 'Distance — miles (mi)', unit: 'mi', code: measure('distance-mi', 'Distance in miles') }, { key: 'distance-km', label: 'Distance — kilometres (km)', unit: 'km', code: measure('distance-km', 'Distance in kilometres') }, // Temperature { key: 'temperature-f', label: 'Temperature — Fahrenheit (°F)', unit: '°F', code: measure('temperature-f', 'Temperature in Fahrenheit', '8310-5') }, { key: 'temperature-c', label: 'Temperature — Celsius (°C)', unit: '°C', code: measure('temperature-c', 'Temperature in Celsius', '8310-5') }, // Time and frequency { key: 'minutes', label: 'Minutes', unit: 'min', code: measure('minutes', 'Minutes') }, { key: 'hours', label: 'Hours', unit: 'h', code: measure('hours', 'Hours') }, { key: 'times-per-day', label: 'Times a day', unit: '/d', code: measure('times-per-day', 'Times a day') }, { key: 'days', label: 'Days', unit: 'd', code: measure('days', 'Days') }, // Counts and proportions { key: 'steps', label: 'Steps', unit: 'steps', code: measure('steps', 'Steps', '41950-7') }, { key: 'percent', label: 'Percentage (%)', unit: '%', code: measure('percent', 'Percentage') }, { key: 'count', label: 'A plain number', unit: '', code: measure('count', 'Count') }, // Clinical readings that are a single number { key: 'heart-rate', label: 'Heart rate — beats per minute', unit: 'bpm', code: measure('heart-rate', 'Heart rate', '8867-4') }, { key: 'glucose-mgdl', label: 'Blood sugar — mg/dL', unit: 'mg/dL', code: measure('glucose-mgdl', 'Blood sugar in mg/dL', '2339-0') }, { key: 'glucose-mmoll', label: 'Blood sugar — mmol/L', unit: 'mmol/L', code: measure('glucose-mmoll', 'Blood sugar in mmol/L', '2339-0') }, { key: 'volume-ml', label: 'Volume — millilitres (mL)', unit: 'mL', code: measure('volume-ml', 'Volume in millilitres') }, { key: 'volume-oz', label: 'Volume — fluid ounces (fl oz)', unit: 'fl oz', code: measure('volume-oz', 'Volume in fluid ounces') }, ]; /** The default, so a form that has not been touched still writes something coherent. */ export const DEFAULT_TRACKABLE_MEASURE = TRACKABLE_MEASURES[0] as TrackableMeasure; export const findTrackableMeasure = (key: string | undefined): TrackableMeasure | undefined => TRACKABLE_MEASURES.find((measureType) => measureType.key === key); /** * The catalogue entry a stored goal was built from, so an edit form can open on what it says. * * MATCHED ON THE CODE AND THE UNIT TOGETHER, because entries deliberately share codings - pounds and * kilograms are both a weight, and Fahrenheit and Celsius are both a temperature. Keying on the code * alone would reopen a kilogram goal as a pound one and silently rewrite it on save. */ export const findMeasureForGoal = ( code: CodeableConcept | undefined, unit: string | undefined, ): TrackableMeasure | undefined => { const codes = (code?.coding ?? []).map((coding) => `${coding.system ?? ''}|${coding.code ?? ''}`); return TRACKABLE_MEASURES.find((measureType) => measureType.unit === (unit ?? '') && (measureType.code.coding ?? []) .some((coding) => codes.includes(`${coding.system ?? ''}|${coding.code ?? ''}`))); };