// The patient-facing agreements gate, wired: the notice, the three documents behind it, and the decline. // // THIS SUPERSEDES `OrgAuthorizationPrompt` FOR THE PATIENT'S OWN VIEW, and the difference is the point // of the card rather than a redesign. That prompt asked about ONE document, opened itself as a dialog on // arrival, and could be dismissed to a card on one page. Consent had become a task - something in a list, // on a page nobody has a reason to open - while an appointment the practice cannot lawfully honour sat in // the diary. Three documents, answered separately, in a notice that is frame furniture and does not // ambush anybody. // // DO NOT MOUNT BOTH. Two consent surfaces asking the same patient about overlapping documents is how // somebody answers one and is asked again by the other. import { useCallback, useEffect, useRef, useState } from 'react'; import type { Appointment, Patient } from 'fhir/r4'; import { useAuthServiceContext } from '../../services/AuthService.tsx'; import { useAppointmentServiceContext } from '../../services/AppointmentService.tsx'; import { useOrgAuthorizationServiceContext, type OrgAuthorizationAgreement, } from '../../services/OrgAuthorizationService.tsx'; import { useFirstSignInGateStatus } from '../FirstSignInGate/firstSignInGateStatus.tsx'; import { HttpError } from '../../services/HttpService.tsx'; import { isPermanentDenial, relayServerFailure } from '../../utils/fetchFailureMessage'; import { formatDate } from '../../utils/dateUtils'; import { cancelledForRefusedConsent, patientMayCancel } from '../../utils/appointmentCancellation'; import type { ConsentInstrumentId } from '../../config/consentInstruments'; import { ORG_AGREEMENT_ORDER, answerFromStatus, declineReceipt, type OrgAgreementState, type VisitsRead } from './orgAgreements'; import { useAgreementRecorded } from './agreementRecorded'; import { agreementOwner, agreementPracticeScope, renderedViewer, useOwnerGeneration } from './agreementOwner'; import { isPatientExperience } from '../../utils/mountContext'; import OutstandingAgreementsNotice from './OutstandingAgreementsNotice'; import AgreementDocumentDialog from './AgreementDocumentDialog'; import { agreementPatientId, agreementSubject } from './agreementSubject'; import { answerEligibilityOf } from '../../utils/proxyFormAuthority'; import { isAgreementAuthorityLost, useProxyMayAnswerAgreements } from '../../hooks/useProxyMayAnswerAgreements'; import { isSaveRetired } from '../../utils/saveOperation'; import { practiceDecisionFollowUp, PRACTICE_TEAM_PENDING_COPY } from '../../utils/practiceDecisionFollowUp'; import DeclineAgreementConfirmation, { type CancellableAppointment } from './DeclineAgreementConfirmation'; import { DismissibleAlert } from '../ui/dismissible-alert'; import { appointmentVersion } from '../../utils/appointmentVersion'; import { writeOutcomeUnknown } from '../../utils/appointmentWriteOutcome'; import { isSearchTruncated } from '../../utils/fetchAllPagesAsBundle'; import { isAsOfIncomplete } from '../../utils/searchTruncation'; import { mayMissRecentWrite } from '../../utils/recentWritesUnresolved'; /** * ONE DEFINITION OF WHAT IS AT RISK, AND IT IS THE ONE THAT DOES THE CANCELLING. * * The confirmation names what a decline will take away, so the list it shows and the list it acts on must * be the same list. `patientMayCancel` is the shared rule the patient's own Cancel button already uses - * a visit already over, already called off, or one the patient is physically at cannot be dropped by a * self-service action - so reading it here means the confirmation cannot promise a cancellation the next * step will refuse, or stay silent about one it will perform. * * A patient who has ARRIVED and then refuses is deliberately not in this list. That is a conversation * at the desk, and it is the front-desk half of this card rather than something to cancel out from under * somebody standing in reception. */ const NO_LONGER_MAY_ANSWER = 'Your access could not be confirmed to answer this for them, so nothing was recorded. Contact the practice for help.'; const OutstandingAgreements = () => { const { getConsentStatuses, getAgreement, agree, decline } = useOrgAuthorizationServiceContext(); const { wpOrganization, organizationCheckComplete, logout, actingAs, setActingAs, currentUser } = useAuthServiceContext(); // These agreements are a practice's. The neutral patients host has none to ask about, and an unresolved // practice is not yet known, so neither sends the practice-only read nor shows its outcome. const practiceScope = agreementPracticeScope(wpOrganization.current?.id, organizationCheckComplete, isPatientExperience()); const asksAPractice = practiceScope === 'practice'; const subject = agreementSubject(actingAs, currentUser); // The visits a decline reads and refreshes are this Patient alone, whatever record is loaded on the page. const subjectPatientId = agreementPatientId(actingAs, currentUser); const subjectPatient = useCallback((): Patient | undefined => (subjectPatientId ? { resourceType: 'Patient', id: subjectPatientId } : undefined), [subjectPatientId]); const { fetchAppointment, fetchAppointments, fetchAppointmentsWithPolling, updateAppointment } = useAppointmentServiceContext(); /** * THE MANDATORY AKASHIC GATE GOES FIRST, AND THIS RETURNS BEFORE THE FETCH RATHER THAN HIDING ITS * RESULT. Until a patient has accepted Akashic's own Terms there is nothing to ask them about a * practice - and firing an org-scoped authenticated request behind a gate they have not answered is * exactly the ordering the older prompt had to be corrected for. `unknown` is treated like unanswered. */ const mandatoryGateIsClear = useFirstSignInGateStatus() === 'clear'; const [agreements, setAgreements] = useState(null); const [openInstrument, setOpenInstrument] = useState(null); const [document, setDocument] = useState(null); const [documentError, setDocumentError] = useState(null); // The answer that made the dialog busy: only it, finishing, makes it idle again. const [busyBy, setBusyBy] = useState(null); const busy = busyBy !== null; const idleAfter = useCallback((operation: object) => setBusyBy((current) => (current === operation ? null : current)), []); const [confirmingDecline, setConfirmingDecline] = useState(null); // Opening or closing a document renews, so a read or answer started for one opening is never used in another. const endScope = useRef(() => {}); // Whether this viewer may answer is published by each status read below, and read again before every answer. const { limitation, begin, beginFollowUp, renew, readEligibility } = useProxyMayAnswerAgreements(subject?.subjectName ?? '', '', { scope: asksAPractice ? wpOrganization.current?.id ?? '' : `#${practiceScope}`, read: (transport) => getConsentStatuses(undefined, undefined, transport), onScopeEnded: () => endScope.current(), }); // The read the document on screen came from; an answer is sent only while it still belongs. const documentRead = useRef<{ belongs: () => boolean } | null>(null); const [atRisk, setAtRisk] = useState([]); // Whether the visits above were read: an unread list is never presented as no visits. const [atRiskRead, setAtRiskRead] = useState('loading'); /** Said only when a cancellation did not take. Silence is the success case. */ const [declineNotice, setDeclineNotice] = useState(null); // A decline used to end in a silent sign-out, which reads exactly like a crash. The session still // ends - that is ruled - but the person says when, after being told what happened. const [declineDone, setDeclineDone] = useState(false); // The visits this decline did cancel, named in its notice so the patient can see where they went. const [cancelledByDecline, setCancelledByDecline] = useState([]); // The decline whose outcome the receipt reports; the receipt shows only while that subject and viewer stand. const receiptOwner = useRef<{ belongs: () => boolean } | null>(null); // Whose answers these are. A change of viewer, practice or person withholds the statuses and holds read before it. const owner = agreementOwner(renderedViewer(currentUser), wpOrganization.current?.id, actingAs); const owned = useOwnerGeneration(owner); const agreementsFor = useRef(owned.generation); // Answers given this session, kept over a lagging status search that still says outstanding; for their owner only. const heldAnswers = useRef({ generation: owned.generation, answers: new Map() }); const heldFor = useCallback((generation: number) => { if (heldAnswers.current.generation !== generation) heldAnswers.current = { generation, answers: new Map() }; return heldAnswers.current.answers; }, []); const organizationName = wpOrganization.current?.name || 'This practice'; const readStatuses = useCallback(async () => { if (!mandatoryGateIsClear || !asksAPractice) return; const read = readEligibility(); const forGeneration = owned.current(); try { const statuses = await getConsentStatuses(undefined, undefined, read.transport); // Only the newest read in scope is used at all; an overtaken one changes nothing on screen. if (!read.publish(answerEligibilityOf(statuses))) return; const { instruments } = statuses; const held = heldFor(forGeneration); agreementsFor.current = forGeneration; setAgreements(ORG_AGREEMENT_ORDER.map((instrument) => { const answer = answerFromStatus(instruments[instrument] ?? 'none'); return { instrument, answer: answer === 'outstanding' ? held.get(instrument) ?? answer : answer, }; })); } catch (error) { if (!read.publish({ status: 'unavailable' })) return; /** * THE TWO FAILURES POINT IN OPPOSITE DIRECTIONS, and collapsing them is how the older * prompt stranded an unproofed proxy in front of an unsatisfiable dialog. * * A 401/403 is not a transient failure - it is a definitive "this gate is not yours to * answer". The server admits only the patient or a full-proxy related person, so this caller * could not satisfy the notice even in principle. Render NOTHING; whether this patient has * agreed is simply not information we were given. * * Anything else is a failed read, and there the safe direction is the other way: a gate that * should be up must not disappear because one request did not come back. */ agreementsFor.current = forGeneration; if (error instanceof HttpError && (error.status === 401 || error.status === 403)) { setAgreements([]); return; } console.error('Failed to read the consent statuses:', error); setAgreements(ORG_AGREEMENT_ORDER.map((instrument) => ({ instrument, answer: 'outstanding' }))); } // A different viewer, practice or person is a different answer: read again. }, [getConsentStatuses, mandatoryGateIsClear, asksAPractice, readEligibility, heldFor, owned.current, owner]); useEffect(() => { void readStatuses(); }, [readStatuses]); // The decline confirmation on screen and the visits it was read for, owned by the opening that started it. // `doomed` holds whole resources because cancelling replaces the whole appointment. const declinePreview = useRef<{ operation: ReturnType; doomed: Appointment[]; atRisk: CancellableAppointment[]; read: VisitsRead; } | null>(null); const dropDecline = useCallback(() => { declinePreview.current?.operation.retire(); declinePreview.current = null; setConfirmingDecline(null); setAtRisk([]); setAtRiskRead('loading'); }, []); // A document and decline confirmation opened for the last subject or viewer go with it. endScope.current = () => { dropDecline(); documentRead.current = null; setOpenInstrument(null); setDocument(null); setDocumentError(null); receiptOwner.current = null; setDeclineNotice(null); setDeclineDone(false); setCancelledByDecline([]); }; const openAgreement = useCallback(async (instrument: ConsentInstrumentId) => { renew(); dropDecline(); const reading = begin(); documentRead.current = reading; setOpenInstrument(instrument); setDocument(null); setDocumentError(null); try { const read = await getAgreement(undefined, instrument, reading.transport); if (!reading.belongs()) return; setDocument(read); } catch (error) { if (isSaveRetired(error) || !reading.belongs()) return; console.error('Failed to read the agreement text:', error); // Named rather than blank. The affirmation is disabled while there is no text, so this is // what stands between a patient and a document they cannot see - it has to say so. setDocumentError('This document could not be loaded. Close this and try again in a moment.'); } }, [begin, dropDecline, getAgreement, renew]); const closeDocument = useCallback(() => { renew(); dropDecline(); setOpenInstrument(null); setDocument(null); setDocumentError(null); }, [dropDecline, renew]); /** * HOLD THE ANSWER WE JUST GAVE, BECAUSE THE SEARCH THAT READS IT BACK LAGS BY MINUTES. * * MEASURED IN THE PRODUCT 2026-09-02: a patient pressed "I Have Read", the dialog closed, and the * document was **still in the list with a live link**. It only greyed out and gained its check after * roughly two minutes and a reload. **The write was correct and the screen said nothing had * happened**, which is a patient pressing it a second time. * * THIS IS THE SAME DEFECT FAMILY THE PRODUCT ALREADY NAMES - *"a screen says done while pointing * at a list that cannot see it yet"*. Writes and reads-by-id are immediate; SEARCHES lag, and * `getConsentStatuses` is a search. * * We do not have to wait, because we already know the answer: we just recorded it. The local list * is updated from what the write returned, and `readStatuses()` still runs afterwards for eventual * consistency - so this is a HOLD, never a substitute for the truth. Do not "simplify" this into * a bare refetch; that is exactly the version that was measured wrong. */ // Held only for the owner the answer was given under, and only while that owner still stands. const holdAnswer = useCallback((instrument: ConsentInstrumentId, answer: OrgAgreementState['answer'], generation: number) => { if (generation !== owned.current()) return; heldFor(generation).set(instrument, answer); if (agreementsFor.current !== generation) return; setAgreements((current) => current && current.map((entry) => ( entry.instrument === instrument ? { ...entry, answer } : entry ))); }, []); // Agreed at the booking review step: shown here straight away. const renderedGeneration = owned.generation; useAgreementRecorded((instrument, recordedFor) => { if (recordedFor === owner) holdAnswer(instrument, 'agreed', renderedGeneration); }); const affirm = useCallback(async () => { if (!openInstrument || !document || !documentRead.current?.belongs()) return; const operation = begin(); const forGeneration = owned.current(); setBusyBy(operation); try { await operation.admitSend(); await agree({ captureMethod: 'portal-pending', // PINNED AT THE MOMENT OF ANSWERING, and it is the version this screen actually // rendered - never a version read again afterwards. An audit asking what this patient // saw resolves THIS number. agreedVersion: document.version, instrument: openInstrument, }, undefined, operation.transport); if (!operation.belongs()) return; // The answer we just gave, held locally before the lagging search is asked again. holdAnswer(openInstrument, 'agreed', forGeneration); closeDocument(); await readStatuses(); } catch (error) { if (isSaveRetired(error) || !operation.belongs()) return; if (isAgreementAuthorityLost(error)) { setDocumentError(NO_LONGER_MAY_ANSWER); return; } console.error('Failed to record the agreement:', error); /** * A PERMANENT DENIAL MUST NOT SAY "PLEASE TRY AGAIN", and that sentence is a real defect * this surface already paid for once. * * Observed on staging 2026-08-29 on the older prompt: a clinician opened the patient portal, * the dialog read its own gate happily - GET agreement admits practitioners - and then the * affirmation was refused, because the write admits only patients and full-proxy related * persons. It offered a retry for something that could never succeed, so the only way past * the first screen was to decline. Whatever the reason, a gate that cannot be satisfied has * to say so and point somewhere, because the next press of the same button fails identically. */ const permanent = error instanceof HttpError && isPermanentDenial(error.status); setDocumentError(permanent ? 'This account cannot answer this on its own behalf. Your practice can record it for you.' : relayServerFailure(error, 'That could not be recorded. Please try again.')); } finally { idleAfter(operation); } }, [agree, begin, closeDocument, document, holdAnswer, idleAfter, openInstrument, readStatuses]); /** * WHAT THE DECLINE WOULD COST, READ BEFORE IT IS OFFERED. * * A refusal is about the RELATIONSHIP rather than a date, so every appointment with this practice is * at risk, not only the next one - and the confirmation names them. If the read fails the * confirmation still opens: it then warns without a list rather than not warning at all, because the * alternative is a patient declining with no idea anything is scheduled. */ const beginDecline = useCallback(async (instrument: ConsentInstrumentId) => { if (!documentRead.current?.belongs()) return; dropDecline(); const preview = { operation: begin(), doomed: [] as Appointment[], atRisk: [] as CancellableAppointment[], read: 'loading' as VisitsRead }; declinePreview.current = preview; const stands = () => declinePreview.current === preview && preview.operation.belongs(); setConfirmingDecline(instrument); const patient = subjectPatient(); if (!patient) { preview.read = 'unavailable'; setAtRiskRead('unavailable'); return; } try { // Every page is admitted to this preview and bounded by its deadline: once it retires, nothing more is sent. const operation = preview.operation; const bundle = await operation.step(() => fetchAppointments(patient, new Date().toISOString(), undefined, undefined, operation.transport)); if (!stands()) return; // A list cut short, or one that says it may be missing visits, is not every visit the decline cancels. const complete = !isSearchTruncated(bundle) && !isAsOfIncomplete(bundle) && !mayMissRecentWrite(bundle); const cancellable = (bundle.entry ?? []) .map((entry) => entry.resource as Appointment | undefined) .filter((appointment): appointment is Appointment => patientMayCancel(appointment)); // The RESOURCES are kept, not just their ids: a PUT replaces the whole appointment, so // cancelling from an id would mean re-reading each one, and re-reading is where an org tag or // a participant list gets dropped by whatever shape the second read happens to return. preview.doomed = cancellable; preview.atRisk = cancellable.map((appointment) => ({ id: appointment.id as string, description: appointment.description || appointment.serviceType?.[0]?.coding?.[0]?.display || 'Your appointment', whenLabel: appointment.start ? formatDate(appointment.start) : 'a date not yet set', })); preview.read = complete ? 'read' : 'partial'; setAtRisk(preview.atRisk); setAtRiskRead(preview.read); } catch (error) { if (!stands()) return; preview.read = 'unavailable'; setAtRiskRead('unavailable'); console.error('Failed to read the appointments a decline would cancel:', error); } }, [begin, dropDecline, fetchAppointments, subjectPatient]); const confirmDecline = useCallback(async () => { const preview = declinePreview.current; if (!confirmingDecline || !preview?.operation.belongs() || !documentRead.current?.belongs()) return; const { doomed } = preview; const operation = begin(); const forGeneration = owned.current(); setBusyBy(operation); try { /** * THE ANSWER IS RECORDED BEFORE THE APPOINTMENTS GO, AND THAT ORDER IS DELIBERATE. * * Cancelling first and failing to record would take somebody's visits away with no refusal * on file to explain why - the practice would see a patient who dropped three appointments * and no reason anywhere. This way round, the worst case is an answered refusal with a visit * still standing, which the desk can see and act on, and which the patient is told about. * * NAMED, never the bare call. Without the instrument the server answers for the legacy * welded agreement, so refusing one document would be recorded as refusing all three - the * exact conflation this card exists to undo. */ await operation.admitSend(); const answered = await operation.step(() => decline(undefined, confirmingDecline, operation.transport), true); if (!operation.belongs()) return; // The server cancels the visits too, and its count and any unfinished cleanup are what is reported. const serverCancelled = answered?.appointmentsCancelled ?? 0; const serverFollowUp = practiceDecisionFollowUp(answered?.consequencesIncomplete); const serverIncomplete = serverFollowUp.any; /** * THE ORDINARY CANCELLATION PATH, ONE APPOINTMENT AT A TIME, AND THE FAILURES ARE COUNTED. * * `updateAppointment` is the same PUT the patient's own Cancel button uses, which is where * slot release and the change notice live - a bulk write of our own would have to reproduce * both, and would be a second answer to "what happens when a visit is called off". * * `allSettled`, never `all`: one refusal must not abandon the rest. What is left standing * is COUNTED and said out loud, because the honest failure here is "your refusal is recorded * and one visit is still in the diary" - silence would leave a patient believing a visit was * cancelled when the practice still expects them. */ // The cancellations belong to the patient and practice the refusal was for, whatever is open by then; // what they report is shown only while this answer's opening still stands. // Each as it was read when the refusal was offered; one changed since is refused and counted below. const followUp = beginFollowUp(); const unfinished: string[][] = doomed.map(() => []); const outcomes = await Promise.allSettled(doomed.map((appointment, index) => followUp.step( () => updateAppointment(cancelledForRefusedConsent(appointment), { basedOn: appointmentVersion(appointment), onFollowUp: (reported) => { unfinished[index] = reported; }, transport: followUp.transport, }), true, ))); if (!operation.belongs()) return; // A cancellation refused or never answered is read again before it is counted: the server may already // have cancelled that visit, which a refusal of our own write does not show. const settled = await Promise.all(outcomes.map(async (outcome, index) => { if (outcome.status === 'fulfilled') return 'cancelled' as const; const refused = !writeOutcomeUnknown(outcome.reason); const recovery = beginFollowUp(); // Only a readable visit still standing after a definite refusal is counted as not cancelled. const read = await recovery.step(() => fetchAppointment(doomed[index].id as string, true, recovery.transport)) .then((stored) => (stored?.status === 'cancelled' ? 'cancelled' : stored?.id ? 'standing' : 'unreadable'), () => 'unreadable'); if (read === 'cancelled') return 'cancelled' as const; return refused && read === 'standing' ? 'not-cancelled' as const : 'unknown' as const; })); if (!operation.belongs()) return; const stranded = settled.filter((outcome) => outcome === 'not-cancelled').length; const unconfirmed = settled.filter((outcome) => outcome === 'unknown').length; const scheduleLeftBehind = settled.some((outcome, index) => outcome === 'cancelled' && unfinished[index].length > 0); const cancelledIds = new Set(doomed .filter((_, index) => settled[index] === 'cancelled') .map((appointment) => appointment.id)); receiptOwner.current = followUp; setCancelledByDecline(preview.atRisk.filter((visit) => cancelledIds.has(visit.id))); // Every count on the page reads the shared list, and none of them re-reads it on its own here. const refreshFor = subjectPatient(); if (refreshFor && doomed.length > 0) { // Owned like the cancellations: a refresh still unsent when the subject or viewer changes is never sent. const refresh = beginFollowUp(); refresh.step(() => fetchAppointmentsWithPolling(refreshFor, true, refresh.transport)).catch((error) => { console.error('Failed to re-read the appointments after a decline:', error); }); } holdAnswer(confirmingDecline, 'declined', forGeneration); setConfirmingDecline(null); closeDocument(); const unconfirmedLine = unconfirmed === 0 ? '' : ` We could not confirm whether ${unconfirmed === 1 ? 'one appointment was' : `${unconfirmed} appointments were`} cancelled - please check your appointments or contact the practice.`; const scheduleLine = scheduleLeftBehind ? ' The practice\'s schedule may still show a cancelled time as booked; the practice can free it.' : ''; const incompleteLine = serverFollowUp.appointments ? ' Some appointments with this practice may still be booked - please check your appointments or contact the practice.' : ''; const settledCleanly = stranded === 0 && unconfirmed === 0 && !serverIncomplete; setDeclineNotice(settledCleanly ? null : ['Your answer is recorded.', stranded === 0 ? '' : `${stranded === 1 ? 'One appointment could not be cancelled' : `${stranded} appointments could not be cancelled`} - please contact the practice.`, unconfirmedLine.trim(), incompleteLine.trim(), serverFollowUp.teams ? PRACTICE_TEAM_PENDING_COPY : '', serverFollowUp.other ? 'Some follow-up could not yet be confirmed.' : '', scheduleLine.trim(), ].filter(Boolean).join(' ')); /** * THE REFUSAL ENDS THE SESSION, AND THE ORDER ABOVE IS WHAT MAKES THAT SAFE. * * The agreement gate is a hard block, not a filter: there is no reduced portal for somebody * who has refused, so leaving them signed in hands them the whole product with the answer * on file saying they may not have it. The refusal is already recorded and the visits are * already called off by this point, so nothing is lost by ending the session here. * * ONE EXCEPTION, AND IT IS NOT A SOFTENING OF THE RULE. When a cancellation did not take, * the notice above is the only place a patient is told a visit is still standing, and it * lives on the signed-in page by design. Signing out would delete the one sentence that * stops them missing an appointment they believe is gone. So a stranded decline stays * signed in until somebody rules on where that sentence should live instead. */ if (settledCleanly) { // A helper who declines for somebody they act for keeps their own session. The refusal // closes the subject's portal, and the helper's account is not the one that answered. // Leaving the subject announces itself, so they are told what ended and where they are. if (actingAs) { setActingAs(null); return; } setDeclineNotice(declineReceipt({ named: cancelledIds.size, serverCancelled, visitsRead: preview.read, organizationName, scheduleLine, })); setDeclineDone(true); return; } await readStatuses(); } catch (error) { if (isSaveRetired(error) || !operation.belongs()) return; if (isAgreementAuthorityLost(error)) { setDocumentError(NO_LONGER_MAY_ANSWER); setConfirmingDecline(null); return; } console.error('Failed to record the decline:', error); setDocumentError(relayServerFailure(error, 'That could not be recorded. Please try again.')); setConfirmingDecline(null); } finally { idleAfter(operation); } }, [closeDocument, confirmingDecline, decline, fetchAppointment, fetchAppointmentsWithPolling, holdAnswer, logout, organizationName, subjectPatient, readStatuses, begin, beginFollowUp, idleAfter, updateAppointment]); const strandedNotice = declineNotice && receiptOwner.current?.belongs() ? (
{declineNotice} {declineDone && cancelledByDecline.length > 0 && ( <>
    {cancelledByDecline.map((visit) => (
  • {visit.description}, {visit.whenLabel}
  • ))}

You can agree at any time - ask the practice, or open the documents again here.

)} {declineDone && ( )}
) : null; // Nothing at all until the statuses are known. A notice that appears and then vanishes has told a // patient they owe something they do not, which is worse than a moment of nothing. // The stranded-appointment notice is the ONE thing that outlives that: it is said after the // agreements notice has gone, which is exactly when it is the only thing left to say. const shown = agreementsFor.current === owned.generation ? agreements : null; if (!mandatoryGateIsClear || !asksAPractice || shown === null) return strandedNotice; // Refused to this helper: why, from the access tier already known, never what the refused read would have said. const refusedToHelper = shown.length === 0 && actingAs && limitation; return ( <> {strandedNotice} {refusedToHelper && (
{limitation}
)} void openAgreement(instrument)} /> {openInstrument && documentRead.current?.belongs() && ( void affirm()} onDecline={() => void beginDecline(openInstrument)} onClose={closeDocument} subject={subject} limitation={limitation} /> )} {/* An answer this helper may no longer give is not offered, even from a confirmation already open. */} {confirmingDecline && !limitation && declinePreview.current?.operation.belongs() && ( void confirmDecline()} onGoBack={dropDecline} subject={subject} /> )} ); }; export default OutstandingAgreements;