import { useCallback } from 'react'; import { environment } from '../environments/environment.ts'; import { useHttpService } from './HttpService.tsx'; // piece 4 - the client half of POST /R4/patient-readiness. // // WHAT THIS REPLACES. The receptionist appointment surface answered "has this patient finished // their intake?" by fetching AllergyIntolerance, MedicationStatement, List and RelatedPerson from the // browser. The receptionist workspace holds NO clinical scopes, so all four were refused and the panel // printed "Unable to verify the patient's profile and history." on every appointment - the feature had // never worked for the role it was built for. Spencer ruled 2026-08-04 that a receptionist has no // reason to see a clinical summary, so the ANSWER moved to the server and the evidence stays there. // // fhirBaseApi, NOT apiUrl. The route is declared in templates/fhir/02-fhir-base.yaml under // `RestApiId: !Ref Api` - the same gateway createPatient and matchCandidates use, and fhirBaseApi // already ends in /R4. A wrong base here is a SILENT 404 rather than an error: the caller would simply // see every patient go unavailable, which looks exactly like the bug this card is fixing. // const API_URL = environment.fhirBaseApi; /** * Mirrors MAX_PATIENTS_PER_REQUEST in handlers/PatientReadiness/post.ts. A deliberate twin: exceeding * it 400s the WHOLE request, so one over-long day view would put every patient on screen into * `unavailable` at once. Chunking here keeps that impossible rather than merely unlikely. */ export const MAX_PATIENTS_PER_READINESS_REQUEST = 50; export interface PatientReadinessSummary { completed: boolean; completedCount: number; totalCount: number; incompleteLabels: string[]; } /** * THREE STATES, AND COLLAPSING THEM RE-CREATES THE DEFECT THIS CARD EXISTS TO REMOVE. * 'ok' - we know the answer. * 'denied' - the caller may not ask about this patient. NOT "their intake is incomplete". * 'unavailable' - we could not find out. NOT "there is nothing to find". * Rendering a refusal or a failure as an unfinished intake is a false statement about a real person's * record. `readiness` is present if and only * if `status === 'ok'`, so an absent summary can never be mistaken for a zeroed one. */ export type PatientReadinessResult = | { status: 'ok'; readiness: PatientReadinessSummary } | { status: 'denied' } | { status: 'unavailable' }; interface ReadinessResponse { results?: Array<{ patientId?: string; status?: string; readiness?: PatientReadinessSummary }>; } const chunk = (items: T[], size: number): T[][] => { const out: T[][] = []; for (let i = 0; i < items.length; i += size) { out.push(items.slice(i, i + size)); } return out; }; export const usePatientReadinessService = () => { const { post } = useHttpService(); /** * Ask about many patients at once. Returns a result for EVERY id requested - a patient the server * said nothing about comes back `unavailable`, never absent, because an absence is what a caller * would render as "no tasks outstanding". * * Never throws. A transport failure is the same KIND of fact as the server's own `unavailable` * ("we could not answer"), and letting it reject would take out the appointment list along with it. */ const fetchPatientReadiness = useCallback( async (patientIds: string[]): Promise> => { const unique = [...new Set(patientIds.filter(Boolean))]; if (unique.length === 0) return {}; // Start every requested patient as unavailable, then upgrade on evidence. Fail-closed by // construction: any id the server omits, or answers with a status this client does not // recognise, keeps the honest "we don't know" rather than silently becoming an answer. const byPatient: Record = Object.fromEntries( unique.map((id) => [id, { status: 'unavailable' as const }]), ); await Promise.all( chunk(unique, MAX_PATIENTS_PER_READINESS_REQUEST).map(async (batch) => { try { const response = await post(`${API_URL}/patient-readiness`, { patientIds: batch, }); (response?.results ?? []).forEach((entry) => { if (!entry?.patientId || !(entry.patientId in byPatient)) return; if (entry.status === 'ok' && entry.readiness) { byPatient[entry.patientId] = { status: 'ok', readiness: entry.readiness }; } else if (entry.status === 'denied') { byPatient[entry.patientId] = { status: 'denied' }; } // Anything else — including an unrecognised status, or 'ok' with no // summary — leaves the pre-seeded `unavailable` in place. }); } catch (error) { console.error('Failed to load patient readiness for a batch:', error); // The pre-seeded `unavailable` already covers this batch. } }), ); return byPatient; }, [post], ); return { fetchPatientReadiness }; };