import type { FieldChange } from './diffResourceVersions'; // One sentence for a change to the care a patient takes part in: an appointment, something asked of // them, an agreement they gave, a care plan, a reading or result, or an entry in their own record. The vocabulary is the patient own: no resource type, no path // fragment, no code. An unmapped field degrades to a truthful phrase rather than to a path. const APPOINTMENT_WORDS: Readonly> = { status: 'the status of an appointment', start: 'the time of an appointment', end: 'the time of an appointment', minutesDuration: 'how long an appointment is', participant: 'who is part of an appointment', serviceType: 'what an appointment is for', reasonCode: 'the reason for an appointment', comment: 'the note on an appointment', patientInstruction: 'the instructions for an appointment', cancelationReason: 'why an appointment was cancelled', }; const TASK_WORDS: Readonly> = { status: 'the status of something you were asked to do', description: 'what you were asked to do', restriction: 'when something you were asked to do is due', executionPeriod: 'when something you were asked to do is due', owner: 'who is looking after something you were asked to do', priority: 'how urgent something you were asked to do is', note: 'the note on something you were asked to do', input: 'what you were asked to do', output: 'your answer to something you were asked to do', }; const CONSENT_WORDS: Readonly> = { status: 'the status of an agreement you gave', provision: 'what an agreement you gave covers', scope: 'what an agreement you gave covers', category: 'which agreement this is', dateTime: 'when an agreement you gave was recorded', performer: 'who recorded an agreement you gave', organization: 'which practice an agreement you gave is with', sourceAttachment: 'the document behind an agreement you gave', policy: 'the terms of an agreement you gave', }; const CARE_PLAN_WORDS: Readonly> = { status: 'the status of a care plan', intent: 'the status of a care plan', title: 'the name of a care plan', description: 'what a care plan says', activity: 'what a care plan asks you to do', goal: 'what a care plan is working towards', period: 'when a care plan runs', careTeam: 'who is working on a care plan', author: 'who wrote a care plan', contributor: 'who wrote a care plan', addresses: 'what a care plan is for', note: 'the note on a care plan', }; // A reading the patient logged and a result the practice recorded are the same resource, so the words // name neither: "a result" covers a lab value and a weight alike without claiming which one it was. const OBSERVATION_WORDS: Readonly> = { status: 'whether a result is final', code: 'what a result measures', valueQuantity: 'the value of a result', valueString: 'the value of a result', valueCodeableConcept: 'the value of a result', valueBoolean: 'the value of a result', valueInteger: 'the value of a result', component: 'the value of a result', interpretation: 'what a result means', referenceRange: 'the normal range for a result', effectiveDateTime: 'when a result was taken', effectivePeriod: 'when a result was taken', issued: 'when a result was issued', performer: 'who recorded a result', note: 'the note on a result', }; // The entries of the patient's own record share one shape of sentence: what the entry is, its status, the // dates, the note. `recordWords` builds that set for one noun so each type states only what differs. const recordWords = (noun: string, extra: Readonly> = {}): Readonly> => ({ code: `what ${noun} is`, clinicalStatus: `whether ${noun} is current`, verificationStatus: `whether ${noun} is confirmed`, status: `the status of ${noun}`, category: `what kind of entry ${noun} is`, note: `the note on ${noun}`, recordedDate: `when ${noun} was recorded`, recorder: `who recorded ${noun}`, asserter: `who reported ${noun}`, ...extra, }); const ALLERGY_WORDS = recordWords('an allergy', { criticality: 'how serious an allergy is', reaction: 'the reaction to an allergy', onsetDateTime: 'when an allergy started', lastOccurrence: 'when an allergy last happened', }); const CONDITION_WORDS = recordWords('a health condition', { severity: 'how serious a health condition is', bodySite: 'where a health condition is', onsetDateTime: 'when a health condition started', abatementDateTime: 'when a health condition ended', }); const MEDICATION_WORDS = recordWords('a medicine', { medicationCodeableConcept: 'which medicine it is', medicationReference: 'which medicine it is', dosage: 'how a medicine is taken', effectiveDateTime: 'when a medicine is taken', effectivePeriod: 'when a medicine is taken', reasonCode: 'why a medicine is taken', }); const IMMUNIZATION_WORDS = recordWords('a vaccination', { vaccineCode: 'which vaccine it was', occurrenceDateTime: 'when a vaccination was given', lotNumber: 'the batch of a vaccination', site: 'where a vaccination was given', doseQuantity: 'the dose of a vaccination', }); const PROCEDURE_WORDS = recordWords('a procedure', { performedDateTime: 'when a procedure was done', performedPeriod: 'when a procedure was done', bodySite: 'where a procedure was done', outcome: 'the outcome of a procedure', }); const FAMILY_HISTORY_WORDS = recordWords('a family history entry', { relationship: 'which relative a family history entry is about', condition: 'the condition in a family history entry', }); const DOCUMENT_WORDS = recordWords('a document', { description: 'the description of a document', type: 'what kind of document it is', content: 'the file of a document', date: 'when a document was added', author: 'who added a document', docStatus: 'the status of a document', }); const WORDS_BY_TYPE: Readonly>>> = { Appointment: APPOINTMENT_WORDS, Task: TASK_WORDS, Consent: CONSENT_WORDS, CarePlan: CARE_PLAN_WORDS, Observation: OBSERVATION_WORDS, AllergyIntolerance: ALLERGY_WORDS, Condition: CONDITION_WORDS, MedicationStatement: MEDICATION_WORDS, Immunization: IMMUNIZATION_WORDS, Procedure: PROCEDURE_WORDS, FamilyMemberHistory: FAMILY_HISTORY_WORDS, DocumentReference: DOCUMENT_WORDS, }; // What the sentence says when the field is one this module has no word for. Still names the thing // that changed, which is what separates it from saying nothing. const FALLBACKS: Readonly> = { Appointment: 'details of an appointment', Task: 'something you were asked to do', Consent: 'an agreement you gave', CarePlan: 'a care plan', Observation: 'a result', AllergyIntolerance: 'an allergy', Condition: 'a health condition', MedicationStatement: 'a medicine', Immunization: 'a vaccination', Procedure: 'a procedure', FamilyMemberHistory: 'a family history entry', DocumentReference: 'a document', }; const verb = (kind: FieldChange['kind']): string => kind === 'added' ? 'Added' : kind === 'removed' ? 'Removed' : 'Changed'; /** The first path segment, without its indexes: `participant[0].status` -> `participant`. */ const rootField = (path: string): string => path.split(/[.[]/)[0]; /** * A sentence describing one changed field on a resource the patient is the subject of. * * Never throws and never emits a path. A type this module does not cover returns undefined so the * caller can decide, rather than inventing a sentence about a resource it cannot read. */ export function describeCareChange( resourceType: string, changed: FieldChange, ): string | undefined { const words = WORDS_BY_TYPE[resourceType]; if (!words) return undefined; return `${verb(changed.kind)} ${words[rootField(changed.path)] ?? FALLBACKS[resourceType]}`; }