import { useEffect, useRef, useState } from "react"; import { ClipboardCheck } from "lucide-react"; import { Card } from "../../../components/ui/card"; import { Button } from "../../../components/ui/button"; import { Checkbox } from "../../../components/ui/checkbox"; import { REFERRAL_AUTH_VERSION, REFERRAL_AUTH_SUMMARY, cameFromOutsideInvite, decisionKeyOf, shownIdentityOf } from "../../../services"; import { isSaveRetired, isSaveTimedOut, TIMED_OUT_MAY_HAVE_SAVED, useSaveScope } from "../../../utils/saveOperation"; import type { SaveTransport } from "../../../utils/saveOperation"; import { useRowOwnedSaves } from "../../../utils/rowOwnedSave"; import type { ShownIdentity, TeamMembership } from "../../../services"; import { MemberIdentity } from "../CareTeamsPage/MemberIdentity"; import { RegistryIdentity } from "../CareTeamsPage/PendingApprovalsSection"; import { describeServerRefusal } from "../../../utils/serverRefusalMessage"; interface AttestPendingApprovalsProps { teamId: string; members: TeamMembership[]; /** `Practitioner/{id}` of the signed-in practitioner, used to refuse self-attestation. */ currentPractitionerRef?: string; attestMemberAuthorization: ( teamId: string, memberRef: string, agreedVersion: number, shownIdentity?: ShownIdentity, transport?: SaveTransport, ) => Promise<{ status: string; state: string }>; onChanged: () => Promise; /** False while the list behind this card is being re-read or could not be: nothing is offered on it. */ decisionsOpen?: boolean; } // S1c-c-2 (client half) - the IN-OFFICE path for the patient's referral authorization. // // WHY IT EXISTS: S1c-c made a recorded patient authorization mandatory before any referral Consent // is minted. On its own that gate is only satisfiable in the patient portal - and seniors and // tech-averse patients never open it, which is exactly the beta cohort. This is how a practitioner // records, in person, that the patient agreed. // // ATTESTING IS AN AUTHORITY EXPANSION: it effects an approval that is otherwise the patient's // alone. So this surface deliberately offers the action ONLY where the server would allow it - // mirroring the server's guards rather than letting staff click into a 403: // * only a membership actually awaiting the patient (`pending-patient-approval`) // * never the caller's OWN membership (no self-dealing - a practitioner vouching for their own access) // * never a related person (no referral Consent is ever minted for one) // * never an outside invitation not yet bound to an account, and a bound one only as the identity shown // The server enforces all three regardless; this only avoids offering what it will refuse. export const AttestPendingApprovals = ({ teamId, members, currentPractitionerRef, attestMemberAuthorization, onChanged, decisionsOpen = true, }: AttestPendingApprovalsProps) => { // The witness and the assent belong to the row as shown; a changed, replaced or removed row retires both. const [targetKey, setTargetKey] = useState(null); const [affirmed, setAffirmed] = useState(false); const [busy, setBusy] = useState(false); const [error, setError] = useState<{ message: string; reread: boolean } | null>(null); const [decided, setDecided] = useState>(() => new Set()); // Rows the server cannot tie to the invitation that was sent: rereading will not change that, only the patient can. const [unresolved, setUnresolved] = useState>(() => new Set()); const scope = useSaveScope(`${teamId}|${currentPractitionerRef ?? ""}`, () => { setTargetKey(null); setAffirmed(false); setBusy(false); }); const awaiting = members.filter( (member) => member.state === "pending-patient-approval" && member.memberClass !== "related-person" && member.memberRef !== currentPractitionerRef, ); const attestable = awaiting.filter((member) => member.memberClass !== "external-practitioner"); // An unbound invitee cannot be attested; it is listed read-only so the inviter sees it waiting. const emailed = awaiting.filter((member) => member.memberClass === "external-practitioner"); const keyOf = (member: TeamMembership) => decisionKeyOf(teamId, member); const liveKeys = new Set(attestable.map(keyOf)); const liveKeysRef = useRef(liveKeys); liveKeysRef.current = liveKeys; // Each attestation is owned to its row as shown, and nothing is sent while the list is being read again. const rowSaves = useRowOwnedSaves(scope, liveKeys, decisionsOpen, (key) => { if (key === targetKey) setBusy(false); }); const target = targetKey ? attestable.find((member) => keyOf(member) === targetKey) ?? null : null; useEffect(() => { if (decisionsOpen) return; setTargetKey(null); setAffirmed(false); }, [decisionsOpen]); useEffect(() => { if (targetKey && !target) { setTargetKey(null); setAffirmed(false); } }, [targetKey, target]); const liveKeyList = [...liveKeys].join("\n"); useEffect(() => { setDecided((prev) => { const kept = [...prev].filter((key) => liveKeysRef.current.has(key)); return kept.length === prev.size ? prev : new Set(kept); }); }, [liveKeyList]); // An outside identity is witnessed as the server named it: without that name there is nothing to confirm. const unnamedOutside = (member: TeamMembership) => cameFromOutsideInvite(member) && !member.displayName; if (awaiting.length === 0) return null; const record = async () => { if (!target || !targetKey) return; const key = targetKey; const operation = rowSaves.start(key); if (!operation) return; const transport = operation.transport; const shown = cameFromOutsideInvite(target) ? shownIdentityOf(target, target.displayName) : undefined; setBusy(true); setError(null); try { await operation.step(() => attestMemberAuthorization(teamId, target.memberRef, REFERRAL_AUTH_VERSION, shown, transport), true); } catch (failure) { if (!operation.belongs()) return; setBusy(false); // Not sent: the list is being read again, and it decides what is offered next. if (isSaveRetired(failure)) return; const code = (failure as { body?: { code?: string } } | null)?.body?.code; if (code === "IDENTITY_UNRESOLVED") { setUnresolved((prev) => new Set(prev).add(key)); setTargetKey(null); setAffirmed(false); return; } const changed = isSaveTimedOut(failure) || code === "IDENTITY_CHANGED" || code === "MEMBERSHIP_CHANGED"; if (changed) { setTargetKey(null); setAffirmed(false); } // Never report success on a refusal: the whole value of an attestation is that the record // says the patient agreed, so a failed one must not look like it landed. setError({ message: isSaveTimedOut(failure) ? TIMED_OUT_MAY_HAVE_SAVED : describeServerRefusal(failure, "Could not record the authorization"), reread: changed, }); return; } if (!operation.belongs()) return; setDecided((prev) => new Set(prev).add(key)); setTargetKey(null); setAffirmed(false); setBusy(false); await onChanged(); }; return (

Waiting on the patient's authorization

{attestable.length > 0 && (

If the patient is with you and has agreed in person, you can record their authorization here.

)} {error && (

{error.message}

{error.reread && ( )}
)} {emailed.length > 0 && (
    {emailed.map((member) => (
  • Invited by email. The patient approves before they can reach the record.

  • ))}
)}
{attestable.map((member) => (
{!decisionsOpen || decided.has(keyOf(member)) ? (

{decisionsOpen ? "Recorded. Updating the list." : "Checking this list for changes."}

) : unresolved.has(keyOf(member)) ? (

We cannot confirm who this invitation was for, so it cannot be recorded here. The patient can remove them from the team and invite them again.

) : unnamedOutside(member) ? (

Their account name could not be read, so this cannot be recorded here yet.

) : targetKey !== keyOf(member) && ( )}
{decisionsOpen && targetKey === keyOf(member) && (

What the patient is agreeing to

    {REFERRAL_AUTH_SUMMARY.map((line) => (
  • {line}
  • ))}
{/* Worded as a statement of fact the practitioner is making, not a convenience toggle — this is recorded as their attestation, with them named as the witness. */}
)}
))}
); }; export default AttestPendingApprovals;