import { availableRecordEmpty, useChartRecordStanding } from '../../../../hooks/useRecordAccessStanding'; import { useEffect, useState } from 'react'; import { Item, ItemGroup } from "../../../../components/ui/item-group"; import { useNavigate } from 'react-router-dom'; import { Icon } from "../../../../components/ui/icon"; import { Card, CardHeader, CardBody } from '../../../../components/ui/wp-card'; import { Elevation } from '../../../../components/ui/elevation'; import { DismissibleAlert } from '../../../../components/ui/dismissible-alert'; import { Heading } from '../../../../components/ui/heading'; import { Flex, FlexItem, FlexBlock } from '../../../../components/ui/flex'; import { Text } from '../../../../components/ui/text'; import { Spacer } from '../../../../components/ui/spacer'; // `warning` and `info` are still needed by the WordPress icon component at the // criticality marker below - that is a separate sweep. Only the Button icons // moved to lucide. (Deliberately not naming the tag here: a literal tag name in a // comment makes greps and source-scanning guards read this file as still using it.) import { Pencil, Plus, TriangleAlert as warning, Info as info } from 'lucide-react'; import { AllergyIntolerance } from 'fhir/r4'; import { AppSpinner, FhirAllergyIntoleranceDisplay, ScrollableTab } from '../../../../components'; import { ResourceListStatus } from '../../../../components/ui/resource-list-status'; import { useFhirAllergyIntoleranceBroadcast, useFhirAllergyIntolerances, useFhirAllergyIntoleranceTargetedBroadcast, useFhirClinicalLists } from '../../../../hooks'; import { useAuthServiceContext, useAllergyIntoleranceServiceContext, useListServiceContext, usePatientServiceContext } from '../../../../services'; import { toast } from 'sonner'; import { describeWriteFailure } from '../../../../utils/fetchFailureMessage'; import { mayMissRecentWrite } from '../../../../utils/recentWritesUnresolved'; import { useMyProxies } from '../../../../hooks/useMyProxies'; import { describeProxyCannotAddToRecord, mayAddToRecordAsProxy, proxyStanding } from '../../../../utils/proxyFormAuthority'; import { SourcePractice } from '../../../../components/ui/source-practice'; import { RecordPracticeNote, recordPracticeId, useRecordPracticeView, useRetireOnPracticeChange } from '../../../../contexts/RecordPracticeContext'; const AllergyIntolerances = ({ isHomePage = false, elevated = false }: { isHomePage?: boolean; elevated?: boolean }) => { const { standing: recordStanding } = useChartRecordStanding(); const navigate = useNavigate(); const { userRole, USER_ROLES, actingAs } = useAuthServiceContext(); const { patientState } = usePatientServiceContext(); const { getAllergyIntoleranceDisplay } = useAllergyIntoleranceServiceContext(); const { confirmNoKnownAllergies } = useListServiceContext(); const { hasConfirmedNoKnownAllergies } = useFhirClinicalLists(); const { loading, error, clearError, allergyIntolerancesList, allergyIntolerancesBundle, listIsThisPatients, attempted, refused, incomplete, freshness: recordFreshness, } = useFhirAllergyIntolerances(); // The list may be missing an allergy recorded moments ago: it is shown, but never as complete, and never as "none". const mayBeIncomplete = !loading && !error && listIsThisPatients && mayMissRecentWrite(allergyIntolerancesBundle); const [selectedAllergyIntolerance, setSelectedAllergyIntolerance] = useState(null); const [showDisplayModal, setShowDisplayModal] = useState(false); // The record's practice context: its own allergies under a named practice; the empty state speaks for the whole record. const practiceView = useRecordPracticeView(allergyIntolerancesList, recordPracticeId); useRetireOnPracticeChange(() => { setShowDisplayModal(false); setSelectedAllergyIntolerance(null); }); // An allergy the list no longer holds (removed, or retired with a read that could not show it) is not kept open. const selectedGone = Boolean(selectedAllergyIntolerance) && !loading && !allergyIntolerancesList.some((allergy) => allergy.id === selectedAllergyIntolerance?.id); useEffect(() => { if (!selectedGone) return; setSelectedAllergyIntolerance(null); setShowDisplayModal(false); }, [selectedGone]); const [confirmingNoKnownAllergies, setConfirmingNoKnownAllergies] = useState(false); const spacerMargin = '24px'; // Below the forms tier a helper cannot add here or attest to none. The tier is known before the press, // so the page says so first, as the task page does. const { proxies, listAnswered } = useMyProxies(); const proxyTier = actingAs ? proxyStanding(proxies, actingAs.relatedPersonId, listAnswered) : undefined; const proxyCannotAdd = !!actingAs && !mayAddToRecordAsProxy(proxyTier); useFhirAllergyIntoleranceBroadcast(); useFhirAllergyIntoleranceTargetedBroadcast({ allergyIntolerance: selectedAllergyIntolerance || undefined, isOpen: showDisplayModal, onUpdate: (updatedAllergyIntolerance) => { setSelectedAllergyIntolerance(updatedAllergyIntolerance); }, onDelete: () => { setShowDisplayModal(false); setSelectedAllergyIntolerance(null); } }); const viewAllergyIntolerance = (allergyIntolerance: AllergyIntolerance) => { setSelectedAllergyIntolerance(allergyIntolerance); setShowDisplayModal(true); }; const editAllergyIntolerance = (event: React.MouseEvent, allergyIntolerance: AllergyIntolerance) => { event.stopPropagation(); const destination = isHomePage ? `../patient/clinical/allergy-intolerances/${allergyIntolerance.id}` : allergyIntolerance.id as string; navigate(destination, { relative: 'route' }); }; const addAllergyIntolerance = () => { const destination = isHomePage ? `../patient/clinical/allergy-intolerances/new` : 'new'; navigate(destination, { relative: 'route' }); }; const confirmNoAllergies = async () => { if (!patientState) { toast.error('Patient context is not available yet.'); return; } setConfirmingNoKnownAllergies(true); try { await confirmNoKnownAllergies(patientState); toast.success('Recorded that you have no known allergies.'); } catch (error) { console.error('Failed to record no known allergies:', error); /* * "PLEASE TRY AGAIN" IS THE WRONG ADVICE ON A REFUSAL, and this is the control that * proves it: measured on staging, the read this write depends on came back 403 and the * toast invited the clinician to press the button again, forever. * * WHAT MAKES IT WORSE THAN AN ORDINARY WRONG SENTENCE. "No known allergies" is a * clinical ASSERTION. An allergy list that is empty because nobody recorded anything * looks exactly like one that is empty because this failed - on a fact prescribing * decisions are made against - so the refusal has to name itself rather than promise * that another press might work. * * `describeWriteFailure` already holds both halves of this: it drops the "try again" * on a 401/403, and it replaces the gateway's own raw sentence - *"not authorized to * access this resource with an explicit deny in an identity-based policy"* - which is * exactly what this path was handed and would otherwise have relayed. */ toast.error(describeWriteFailure(error, 'Unable to record no known allergies')); } finally { setConfirmingNoKnownAllergies(false); } }; const isAuthorizedToEdit = () => { return !proxyCannotAdd && userRole && [USER_ROLES.PATIENT, USER_ROLES.RELATED_PERSON, USER_ROLES.PRACTITIONER].includes(userRole as string); }; const renderContent = () => { return ( <> {proxyCannotAdd && ( <> {describeProxyCannotAddToRecord(actingAs?.subjectName)} )} {mayBeIncomplete && ( <> This list may be missing an allergy recorded in the last few minutes. Reload to check again. )} {/* Card 150 — this empty state carries ACTIONS ("Add Allergy" / "No Known Allergies"), not just a message, so it stays inline and the primitive above owns the failure only. The `!error` gate is the load-bearing part: without it a refused load reads as "No known allergies reported", which on an allergy list is the most dangerous false negative in the whole chart. */} {/* Read as of a cutoff and not in full: what it left out is unknown, so it is never "none", and nothing here attests to none over it. */} {!error && !loading && incomplete && ( {allergyIntolerancesList.length === 0 ? 'Allergies could not be read in full, so what this record holds is not known.' : 'Allergies could not be read in full, so this list may be incomplete.'} )} {!error && !loading && !incomplete && allergyIntolerancesList.length === 0 && ( <> {!mayBeIncomplete && ( {hasConfirmedNoKnownAllergies ? 'No known allergies reported. You indicated that you have no known allergies or intolerances.' : availableRecordEmpty('No allergies or intolerances have been added yet.', recordStanding, recordFreshness)} )} {isAuthorizedToEdit() && ( <> {/* Not offered over a list that may be missing one: it would attest to none. */} {!hasConfirmedNoKnownAllergies && !mayBeIncomplete && ( )} )} )} {loading ? ( ) : ( {practiceView.shown.length !== 0 && ( {practiceView.shown.map((allergyIntolerance: AllergyIntolerance) => { const displayTitle = getAllergyIntoleranceDisplay(allergyIntolerance); const isHighCriticality = allergyIntolerance.criticality === 'high'; return ( viewAllergyIntolerance(allergyIntolerance)}> {displayTitle} {isAuthorizedToEdit() && ( )} ); })} )} )} {selectedAllergyIntolerance && ( { setShowDisplayModal(false); setSelectedAllergyIntolerance(null); }}/> )} ); }; const renderHeader = () => { return ( {isHomePage && ( Allergies & Intolerances )} {isAuthorizedToEdit() && ( )} ); }; if (isHomePage) { return ( {elevated && } {renderHeader()} {renderContent()} ); } return (
{renderHeader()} {renderContent()}
); }; export default AllergyIntolerances;