import { useCallback } from 'react'; import type { Bundle, Encounter, Patient, Practitioner } from 'fhir/r4'; import { useAllergyIntoleranceServiceContext } from '../../../services/AllergyIntoleranceService'; import { useConditionServiceContext } from '../../../services/ConditionService'; import { useImmunizationServiceContext } from '../../../services/ImmunizationService'; import { useMedicationRequestServiceContext } from '../../../services/MedicationRequestService'; import { useNutritionOrderServiceContext } from '../../../services/NutritionOrderService'; import { useObservationServiceContext } from '../../../services/ObservationService'; import { useProcedureServiceContext } from '../../../services/ProcedureService'; import { useRiskAssessmentServiceContext } from '../../../services/RiskAssessmentService'; import { useVisionPrescriptionServiceContext } from '../../../services/VisionPrescriptionService'; import type { CataloguedKind } from '../../../utils/recordFieldCatalogue.generated'; import type { SaveOperation, SaveTransport } from '../../../utils/saveOperation'; /** * Filing a confirmed item, on the ordinary route for its kind. * * IT REUSES EACH KIND'S OWN TRANSFORM AND CREATE, and that is the whole design. Those already build the * resource, stamp the patient, the encounter and the recorder, and post it to the right endpoint - so a * shortcut past them would be a second answer to what each resource looks like. * * WHAT THEY DO NOT DO IS REFRESH THE LIST THE CHART READS, and this file used to claim they did. A walk * on 2026-09-04 disproved it: the resource was stored correctly and the visit panel still read empty. * That is why filing ends with a forced refetch below rather than trusting the create. * * WHY A BRANCH PER KIND RATHER THAN A LOOP: the signatures are NOT uniform. Most transforms take * (item, patient, encounter, recorder); VisionPrescription takes (item, patient, PRACTITIONER, encounter). * A generic call would put the encounter in the practitioner's slot on that one kind and file a * prescription signed by a visit - silently, because both are objects and nothing type-checks a * positional mistake between two optionals. */ export interface FileRecordItemContext { patient: Patient; encounter?: Encounter; /** The signed-in practitioner. Stamped as the recorder, never typed by anybody. */ practitioner?: Practitioner; /** The filing this belongs to: retired, nothing more is sent and the chart is not asked again. */ operation?: SaveOperation; /** A reading is filed under this operation id, the same on every resend of that filing. */ operationId?: string; } const REFRESH_ATTEMPTS = 6; const REFRESH_DELAY_MS = 1500; /** Does a refetched bundle hold the resource with this id? */ const listHolds = (bundle: unknown, id: string): boolean => Boolean((bundle as Bundle | undefined)?.entry?.some((entry) => entry.resource?.id === id)); export const useFileRecordItem = () => { const allergy = useAllergyIntoleranceServiceContext(); const condition = useConditionServiceContext(); const diet = useNutritionOrderServiceContext(); const immunization = useImmunizationServiceContext(); const medication = useMedicationRequestServiceContext(); const observation = useObservationServiceContext(); const procedure = useProcedureServiceContext(); const risk = useRiskAssessmentServiceContext(); const vision = useVisionPrescriptionServiceContext(); /** * File one item. Rejects if the kind has no route here, rather than resolving as though it worked. * * THE CALLER MUST SEE A FAILURE. Unlike reading a note for suggestions - where silence is the right * answer because nothing was promised - this is the moment the clinician believes something is in * the chart. A swallowed error there is the worst outcome in the whole flow. */ return useCallback(async ( kind: CataloguedKind, values: Record, { patient, encounter, practitioner, operation, operationId }: FileRecordItemContext, ): Promise => { const send = (create: (transport?: SaveTransport) => Promise): Promise => (operation ? operation.step(() => create(operation.transport), true) : create(undefined)); let created: { id?: string } | undefined; switch (kind) { case 'allergy': created = await send((transport) => allergy.createAllergyIntolerance( allergy.transformToFhirAllergyIntolerance(values, patient, encounter, practitioner), transport, )); break; case 'condition': created = await send((transport) => condition.createCondition( condition.transformToFhirCondition(values, patient, encounter, practitioner), transport, )); break; case 'diet': created = await send((transport) => diet.createNutritionOrder( diet.transformToFhirNutritionOrder(values, patient, encounter), transport, )); break; case 'immunization': created = await send((transport) => immunization.createImmunization( immunization.transformToFhirImmunization(values, patient, encounter), transport, )); break; case 'medication': created = await send((transport) => medication.createMedicationRequest( medication.transformToFhirMedicationRequest(values, patient, encounter, practitioner), transport, )); break; case 'observation': { if (!operationId) throw new Error('A reading is filed only under its operation id.'); const filed = await send((transport) => observation.createObservationOnce( observation.transformToFhirObservation(values, patient, encounter), operationId, transport, )); // Saved but not shown to this clinician now: filed all the same, with no id to wait for. created = 'resourceType' in filed ? filed : undefined; break; } case 'procedure': created = await send((transport) => procedure.createProcedure( procedure.transformToFhirProcedure(values, patient, encounter, practitioner), transport, )); break; case 'risk': created = await send((transport) => risk.createRiskAssessment( risk.transformToFhirRiskAssessment(values, patient, encounter), transport, )); break; case 'vision': // THE ODD SIGNATURE, and the reason this is a switch. Practitioner THIRD, encounter // FOURTH - the reverse of every other kind here. created = await send((transport) => vision.createVisionPrescription( vision.transformToFhirVisionPrescription( values, patient, practitioner as Practitioner, encounter, ), transport, )); break; default: throw new Error(`No route to file a record item of kind '${kind as string}'.`); } /* WHAT WAS JUST FILED HAS TO BE ON SCREEN, and nothing else was going to put it there. WALKED AND FOUND, 2026-09-04: a diagnosis filed from a visit reached the datastore exactly right - real SNOMED code, right subject, linked to its own encounter - and the panel beside the note went on saying "No problems recorded yet". Every list on the chart is served from a per-patient cache that a create does not touch, so the clinician who has just recorded a diagnosis reads the one sentence that says it did not work. There is no way from the screen to tell a stale panel from a lost write, and the alarming reading is the reasonable one. FORCED, because the cache is fresh for five minutes and an ordinary call would be answered from it - the request that exists to reveal the new item would be the one guaranteed not to. */ const refresh: Record Promise> = { allergy: allergy.fetchAllergyIntolerancesWithPolling, condition: condition.fetchConditionsWithPolling, diet: diet.fetchNutritionOrdersWithPolling, immunization: immunization.fetchImmunizationsWithPolling, medication: medication.fetchMedicationRequestsWithPolling, observation: observation.fetchObservationsWithPolling, procedure: procedure.fetchProceduresWithPolling, risk: risk.fetchRiskAssessmentsWithPolling, vision: vision.fetchVisionPrescriptionsWithPolling, }; /* AND IT WAITS FOR THE ITEM, because the polling underneath does not. `fetch…WithPolling` polls only for `lastUpserted`, which the creates above never set, so it accepted the first answer - and on C02 (2026-09-30) that answer did not yet hold the diagnosis: chart Record showed it while the visit's Patient reference still read "No problems recorded yet" until a reload. Asked again, a bounded number of times, until the id just filed is in the list. */ try { for (let attempt = 1; ; attempt++) { if (operation && !operation.belongs()) break; const list = await refresh[kind](patient, true); if (!created?.id || listHolds(list, created.id) || attempt >= REFRESH_ATTEMPTS) break; await new Promise((resolve) => setTimeout(resolve, REFRESH_DELAY_MS)); } } catch (cause) { // THE ITEM IS FILED. Rethrowing here would tell the clinician it was not, which is false and // is the worse of the two wrongs - they would file it a second time. A stale panel is // recoverable by opening the chart; a duplicate diagnosis is not. console.error('Filed, but the chart could not be refreshed to show it', cause); } }, [allergy, condition, diet, immunization, medication, observation, procedure, risk, vision]); };