import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { AlertTriangle, ShieldCheck } from 'lucide-react'; import { useAuthServiceContext } from '../../services'; import type { AdminGrantOutcome } from '../../utils/settleAdminGrant'; const OWNERSHIP_OUTCOME: Record = { granted: 'You now own this practice and have administrator access.', reload: 'You now own this practice. Reload the page to use your administrator access.', pending: 'You now own this practice. Your administrator access is still being set up; reload the page in a minute to use it.', }; import { useOrganizationOwnerService, type OwnerState } from '../../services/OrganizationOwnerService.tsx'; import { ownerNotice, nomineeNotifiedMessage } from './organizationOwnerCopy'; import { relayServerFailure } from '../../utils/fetchFailureMessage'; import { referencesPractitioner } from './ownerReference'; import { useRefreshSessionOrganization } from '../../hooks/useRefreshSessionOrganization'; import { Checkbox } from '../ui/checkbox'; import { isSaveRetired, useSaveScope, type SaveTransport } from '../../utils/saveOperation'; import { holdSettingsWrite, nextSettingsAttempt, recordSettingsAttempt, unresolvedSettingsWrite, useUnresolvedSettingsWrite, watchSettingsWrite, } from '../../utils/unresolvedSettingsWrite'; import { OWNER_ACTION_SETTLING, OWNER_ACTION_SUPERSEDED, OWNER_SAVED_REFRESH_FAILED, OWNER_UNFINISHED, OWNER_UNFINISHED_BY_LINK, newOwnerOperation, operationOf, operationSettlement, ownerActionHolder, ownerRecoveryWords, ownerStateShows, readOwnerAction, resumeRouteOf, unconfirmedOwnerAction, unfinishedOwnerAction, type OwnerAction, } from './ownerActionHold'; // - the notice that says who this practice belongs to, and warns when nobody does. // // THE SUPPORT CASE IT PREVENTS BEGINS WITH NOBODY HAVING NOTICED. An agency installs the plugin, // becomes the founding admin, configures everything, pays, and vanishes - and the practice discovers it // has no authority over its own account at the worst possible moment. A notice staff see every day is // the cheapest fix there is. // // ZERO WordPress: Tailwind + lucide only. export interface OwnerCandidate { id: string; name: string; } interface Props { organizationId: string | undefined; /** Whether this viewer may nominate. The notice must never offer what the reader cannot do. */ canNominate: boolean; /** Who could be named. The practice's own staff - an owner is a person who works here. */ candidates: OwnerCandidate[]; /** * - reports whether this practice HAS an owner, so the onboarding step beside the BAA one * can be driven from the same read this notice already makes. * * LIFTED RATHER THAN FETCHED TWICE. The obvious alternative - having the page call `fetchOwner` * itself - puts two reads behind one screen and, worse, creates two answers to *"does this practice * have an owner?"* that can disagree while one is in flight. One fetch, one answer. * * `undefined` means NOT ANSWERED - including when the read FAILED, which this component treats as * "say nothing" rather than as "no owner". The step contract is identical, so a failed read omits the * step instead of accusing the practice. */ onOwnerAssignedChange?: (ownerAssigned: boolean | undefined) => void; // The whole ownership state, so a handover (owner changes, still assigned) is visible too. onOwnerStateChange?: (state: OwnerState | undefined) => void; /** * - is the READER the owner? Lifted for exactly the reason above: this component * already makes the one read that answers it, and a second answer that can disagree while one is * in flight is worse than none. * * `undefined` means NOT ANSWERED - initial, and on a failed read. A consumer gating a screen * on this must treat it as "not yet", never as "no": a transport failure is not a statement * about who owns the practice. */ onViewerIsOwnerChange?: (viewerIsOwner: boolean | undefined) => void; /** A confirmed claim made this reader the owner; a screen gated on their old access reads it again. */ onOwnershipTaken?: () => void; } const OrganizationOwnerNotice: React.FC = ({ organizationId, canNominate, candidates, onOwnerAssignedChange, onOwnerStateChange, onViewerIsOwnerChange, onOwnershipTaken, }) => { // The reports below fire when the owner facts change, not when a parent passes a new callback: // a parent that re-reads the owner on a report would otherwise re-read on every render. const reports = useRef({ onOwnerAssignedChange, onOwnerStateChange, onViewerIsOwnerChange, onOwnershipTaken }); reports.current = { onOwnerAssignedChange, onOwnerStateChange, onViewerIsOwnerChange, onOwnershipTaken }; const { fetchOwner, acceptOwner, nominateOwner, startOwnerTransfer, acceptOwnerTransfer, startOwnerClaim, confirmOwnerClaim, resumeOwnerOperation, } = useOrganizationOwnerService(); const { practitionerId } = usePractitionerIdentity(); const { takeUpAdminGrant } = useAuthServiceContext() as { takeUpAdminGrant?: (organizationId: string) => Promise }; // Owning the practice makes you its administrator at once: this session takes the access up in place, // and says so only once a renewed token carries it. const takeUpOwnership = useCallback(async (belongs: () => boolean) => { const outcome = organizationId && takeUpAdminGrant ? await takeUpAdminGrant(organizationId).catch((): AdminGrantOutcome => 'pending') : 'pending'; if (belongs()) setOutcome(OWNERSHIP_OUTCOME[outcome]); }, [organizationId, takeUpAdminGrant]); const [state, setState] = useState(); const [accepting, setAccepting] = useState(false); const [agreed, setAgreed] = useState(false); const [error, setError] = useState(null); const [expanded, setExpanded] = useState(false); const [chosen, setChosen] = useState(''); const [outcome, setOutcome] = useState(null); // Where the claim code went, masked, and therefore also whether one is outstanding. The server // tells us rather than the screen assuming: a claim can be open from an earlier visit. const [claimSentTo, setClaimSentTo] = useState(null); const [claimCode, setClaimCode] = useState(''); // Set when an ownership write landed but the practice could not be re-read afterwards. const [organizationStale, setOrganizationStale] = useState(false); const refreshOrganization = useRefreshSessionOrganization(); // A held owner action that a check of the owner record did not find yet. That is not proof it will not land, so it stays held. const [notFound, setNotFound] = useState(false); const [reads, setReads] = useState(0); // The read, every owner action and what it reports belong to the practice, the person reading (AuthService's // viewer key names the practice too), what they may do here, and this opening. A change of any starts from a // fresh statement, choice and code, and nothing started before is sent, shown or reported. An action sent with // no reply is held for its sender until this practice's owner record shows it. const scope = useSaveScope(`${organizationId ?? ''}|${practitionerId ?? ''}|${canNominate ? 'nominates' : 'reads'}`, () => { setState(undefined); setAccepting(false); setAgreed(false); setError(null); setExpanded(false); setChosen(''); setOutcome(null); setClaimSentTo(null); setClaimCode(''); setOrganizationStale(false); setNotFound(false); }); const holder = ownerActionHolder(); const heldWrite = useUnresolvedSettingsWrite(holder); const heldAction = heldWrite ? readOwnerAction(heldWrite.detail) : undefined; // Admitted with stages still owed: only the same operation finishes it. const heldSettling = heldWrite?.state === 'recovering'; const load = useCallback(async () => { if (!organizationId) return; const operation = scope.start(); const heldFor = holder; try { const next = await operation.step(() => fetchOwner(organizationId, operation.transport)); // The owner record settles a held action it shows; one it does not show may still be on its way. const held = unresolvedSettingsWrite(heldFor); const action = held ? readOwnerAction(held.detail) : undefined; const unfinished = unfinishedOwnerAction(next, practitionerId); if (held && action && ownerStateShows(action, next, practitionerId)) holdSettingsWrite(heldFor, undefined); else if (held && action && operationSettlement(next, operationOf(action)?.id) === 'settling') { holdSettingsWrite(heldFor, { ...held, state: 'recovering' }); } else if (action) setNotFound(true); // Reopened after an action was admitted and not finished: the same operation is recovered from the read. else if (unfinished) holdSettingsWrite(heldFor, { kind: 'practice-owner', state: 'recovering', detail: JSON.stringify(unfinished) }); setState(next); // - `!needsOwner`, never re-derived from `status`. The service's own note says the // server owns that judgement: `needsOwner` is TRUE for a PENDING nomination as well as for // none (a nomination nobody accepted is the state that looks handled and is not), and FALSE // throughout a pending transfer (the practice keeps its owner the whole time). A client-side // rule would disagree with both the first time either changed. } catch (readError) { if (isSaveRetired(readError) || !operation.belongs()) return; // SILENT ON FAILURE, deliberately. This is a governance notice, not a gate: if the read // fails we show nothing rather than a warning we cannot substantiate, because "this practice // has no owner" is a serious claim to make on the strength of a failed request. setState(undefined); // And the onboarding step is told NOTHING rather than told "no owner" - same reasoning, and // the step contract already treats `undefined` as "omit" rather than as "outstanding". // The reporting itself now lives in the effect below, keyed on the state it describes. } finally { operation.retire(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [organizationId, fetchOwner, practitionerId, scope]); // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() => { void load(); }, [load, reads, canNominate]); // Every ownership write moves the Organization version, and practice saves are built from the // session copy, so it is re-read here or the next save is refused as a conflict. Re-read for the same action, // and its failure is said without unsaying the change. const commit = useCallback((next: OwnerState, transport: SaveTransport, belongs: () => boolean) => { setState(next); void refreshOrganization(transport) .then((fresh) => { if (belongs()) setOrganizationStale(!fresh); }) .catch((refreshError: unknown) => { if (!isSaveRetired(refreshError)) throw refreshError; }); }, [refreshOrganization]); // One owner action for this practice and reader: confirmed (then `done`), refused (the server's words; nothing // recorded), unknown (held with what it asked) or retired (nothing shown). const act = useCallback(async ( action: OwnerAction, send: (transport: SaveTransport) => Promise, done: (next: OwnerState, transport: SaveTransport, belongs: () => boolean) => Promise | void, ) => { const operation = scope.start(); const heldFor = holder; const startedWith = unresolvedSettingsWrite(heldFor); const attempt = nextSettingsAttempt(startedWith); const write = watchSettingsWrite((onSend) => send({ ...operation.transport, onSend })); setAccepting(true); setError(null); setNotFound(false); try { let next: OwnerState; try { next = await operation.step(write.run, true); } catch (actionError) { recordSettingsAttempt(heldFor, attempt, startedWith, write.unconfirmed() ? { kind: 'practice-owner', state: 'unconfirmed', detail: JSON.stringify(action) } : undefined); if (isSaveRetired(actionError) || !operation.belongs() || write.unconfirmed()) return; setError(relayServerFailure(actionError, 'That could not be completed.')); return; } // Done only once its operation settled: designation alone is not done. One admitted with stages still // owed is held for the same operation to finish; one a later change superseded was done and is said so. // An answer with no transition comes from a server that predates them and is read as before. const op = operationOf(action); const settlement = op && next.transition ? operationSettlement(next, op.id) : 'settled'; if (settlement === 'settling') { holdSettingsWrite(heldFor, { kind: 'practice-owner', state: 'recovering', detail: JSON.stringify(action) }); return; } holdSettingsWrite(heldFor, undefined); if (settlement === 'superseded') { if (operation.belongs()) setOutcome(OWNER_ACTION_SUPERSEDED); void load(); return; } await done(next, operation.transport, operation.belongs); } finally { if (operation.belongs()) setAccepting(false); operation.retire(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [scope, holder, load]); /** * THE CHECKLIST IS TOLD BY THE STATE CHANGING, NOT BY EACH HANDLER REMEMBERING TO SAY SO. * * Confirming ownership updated THIS card to "You own this practice" and left the setup checklist * beside it still reading 3 of 13, with the step flagged red, until a reload. Two things on one screen * disagreed about whether the step was done - and the one that was right was the one nobody was * watching, which invites an admin to do it twice. * * THE CAUSE WAS THE SHAPE, NOT THE CALL. The report lived in `load()` alone, while three other * handlers - accept, accept transfer and nominate - each did `setState(next)` and told nobody. * Adding three more calls would have fixed this symptom and left the defect: a fourth handler would * forget in exactly the same way, because remembering is the thing that failed. * * Keyed on `state`, so the fact reports itself however it changed. `undefined` still means "not known" * and omits the step, which covers first mount and the failed-read path alike - and the parent already * starts at `undefined`, so the mount-time report changes nothing. */ useEffect(() => { reports.current.onOwnerAssignedChange?.(state === undefined ? undefined : !state.needsOwner); }, [state]); useEffect(() => { reports.current.onOwnerStateChange?.(state); }, [state]); const isViewer = (reference: string | undefined): boolean => referencesPractitioner(reference, practitionerId); const viewerIsNominee = isViewer(state?.nomineePractitionerId); const viewerIsOwner = isViewer(state?.ownerPractitionerId); const viewerIsPendingTransferee = isViewer(state?.pendingTransferToPractitionerId); // Reported up for a parent that gates on ownership without an owner read of its own. // // `undefined` while there is no state - which is both the initial and the FAILED read. A gate // that treated a failed read as "not the owner" would hide the signing screen from the one person // who may use it, on the strength of a dropped request. useEffect(() => { reports.current.onViewerIsOwnerChange?.(state ? viewerIsOwner : undefined); }, [state, viewerIsOwner]); const notice = useMemo( () => ownerNotice(state, { viewerIsNominee, viewerCanNominate: canNominate, viewerIsOwner, viewerIsPendingTransferee, }), [state, viewerIsNominee, canNominate, viewerIsOwner, viewerIsPendingTransferee], ); const accept = useCallback(async () => { if (!organizationId || !state?.attestation) return; const statement = { statementVersion: state.attestation.version }; const kind = viewerIsPendingTransferee ? 'accept-transfer' : 'accept'; const operation = newOwnerOperation(kind, organizationId, practitionerId); // WHICH ACCEPTANCE depends on what is actually pending, not on which button was drawn. // They are separate routes with separate refusals - accepting a transfer through the // nomination route returns "this organization already has an owner", which is true and // useless to somebody who was just asked to take it over. await act( { action: kind, operation }, (transport) => (viewerIsPendingTransferee ? acceptOwnerTransfer(organizationId, statement, transport, operation) : acceptOwner(organizationId, statement, transport, operation)), async (next, transport, belongs) => { commit(next, transport, belongs); setExpanded(false); if (next.status === 'accepted') await takeUpOwnership(belongs); }, ); }, [organizationId, state?.attestation, acceptOwner, acceptOwnerTransfer, viewerIsPendingTransferee, commit, takeUpOwnership, act, practitionerId]); /** * Ask for the code. Nothing has moved when this returns - the practice has no new owner until the * code comes back. */ const requestClaimCode = useCallback(async () => { if (!organizationId) return; const operation = scope.start(); const write = watchSettingsWrite((onSend) => startOwnerClaim(organizationId, { ...operation.transport, onSend })); setAccepting(true); setError(null); try { const started = await operation.step(write.run, true); setClaimSentTo(started.sentTo); } catch (claimError) { if (isSaveRetired(claimError) || !operation.belongs()) return; // Sent with no answer: a code may be on its way. Its box is offered, and another is sent only on request. if (write.unconfirmed()) { setClaimSentTo("this practice's recorded contact details"); setError('We could not confirm a code was sent. If none arrives, send another.'); return; } // The server's own words where it has them: "this practice already has an owner", "you do // not work here any more" and "there is nowhere to send a code" need three different // things from the person reading them, and a generic sentence sends them nowhere. setError(relayServerFailure(claimError, 'That could not be completed.')); } finally { if (operation.belongs()) setAccepting(false); operation.retire(); } }, [organizationId, startOwnerClaim, scope]); const confirmClaim = useCallback(async () => { if (!organizationId || !state?.attestation || !claimCode.trim()) return; // The second fence. The button is disabled without the tick, and a callback that could fire // without it would send a request the server then refuses. if (!agreed) return; const statement = { statementVersion: state.attestation.version }; const code = claimCode.trim(); const operation = newOwnerOperation('claim', organizationId, practitionerId); await act({ action: 'claim', operation }, (transport) => confirmOwnerClaim(organizationId, code, statement, transport, operation), async (next, transport, belongs) => { commit(next, transport, belongs); setClaimSentTo(null); setClaimCode(''); setAgreed(false); setExpanded(false); await takeUpOwnership(belongs); if (belongs()) reports.current.onOwnershipTaken?.(); }); }, [organizationId, state?.attestation, claimCode, agreed, confirmOwnerClaim, commit, takeUpOwnership, act, practitionerId]); // The founder's first signed-in setup: naming themselves with the statement is their acceptance, bound by the // server to this sign-in and practice. Nothing goes to the practice's contact details. const confirmOwnSetup = useCallback(async () => { if (!organizationId || !practitionerId || !(state?.canSelfNominate || state?.youAreTheNamedOwner) || !state.attestation || !agreed) return; const statement = { statementVersion: state.attestation.version }; const self = practitionerId; const operation = newOwnerOperation('nominate', organizationId, self); await act({ action: 'nominate', practitionerId: self, operation }, (transport) => nominateOwner(organizationId, self, statement, undefined, transport, operation), async (next, transport, belongs) => { commit(next, transport, belongs); setAgreed(false); if (next.status !== 'accepted') return; await takeUpOwnership(belongs); if (belongs()) reports.current.onOwnershipTaken?.(); }); }, [organizationId, practitionerId, state?.canSelfNominate, state?.youAreTheNamedOwner, state?.attestation, agreed, act, nominateOwner, commit, takeUpOwnership]); const transfer = useCallback(async () => { if (!organizationId || !chosen) return; const to = chosen; await act({ action: 'transfer', practitionerId: to }, (transport) => startOwnerTransfer(organizationId, to, transport), (next, transport, belongs) => { commit(next, transport, belongs); // Same rule as a nomination: an unreachable recipient becomes an INSTRUCTION rather than // silence. A hand-off nobody was told about is the state that looks handled and is not. setOutcome(nomineeNotifiedMessage(next.notified) ?? null); setExpanded(false); }); }, [organizationId, chosen, startOwnerTransfer, commit, act]); /** * - WHICH SENTENCE THIS PERSON HAS TO AGREE TO, or undefined if the server offered none. * * THE GATE FOLLOWS THE SENTENCE, NOT THE BRANCH, and that is the whole reason it is written this * way. The plugin and the api deploy separately, so a build of this screen can meet an api that does * not send the contractor wording yet. Requiring a tick that no checkbox can render would leave the * button permanently disabled with nothing on screen explaining why - a dead control, which is a * defect this product has paid for more than once. No sentence, no tick required; the server is the * fence either way, and refuses a nomination it wanted an attestation for. */ const attestationToAgree = chosen !== '' && chosen === practitionerId ? state?.attestation : state?.contractorAttestation; const nominate = useCallback(async () => { if (!organizationId || !chosen || !state?.attestation) return; // The second fence, not the first - the button below is disabled without the tick. A callback // that could fire unticked would send a nomination the server then refuses, and the person would // see a failure message instead of a checkbox. if (attestationToAgree && !agreed) return; const named = chosen; const statement = state.attestation; const contractorStatement = state.contractorAttestation; { // WHICH ATTESTATION TRAVELS DEPENDS ON WHO IS BEING NAMED, and they are two different // statements by two different people. // // CORRECTED THE SECOND HALF OF THIS. It used to read "naming somebody else // asserts nothing about the admin", and that was the gap: naming somebody else IS a claim // about you - that you are a contractor setting this practice up on its behalf - and it went // unrecorded, leaving the one person the accountability was designed for with no record. // Naming yourself with the attestation is an acceptance, so it carries its own operation. const operation = named === practitionerId ? newOwnerOperation('nominate', organizationId, practitionerId) : undefined; await act({ action: 'nominate', practitionerId: named, ...(operation ? { operation } : {}) }, (transport) => nominateOwner( organizationId, named, named === practitionerId ? { statementVersion: statement.version } : undefined, named === practitionerId || !contractorStatement ? undefined : { statementVersion: contractorStatement.version }, transport, operation, ), async (next, transport, belongs) => { commit(next, transport, belongs); setOutcome(nomineeNotifiedMessage(next.nomineeNotified, next.nomineeNotNotifiedReason) ?? null); setExpanded(false); setAgreed(false); if (operation && next.status === 'accepted') await takeUpOwnership(belongs); }); } }, [ organizationId, chosen, state?.attestation, state?.contractorAttestation, attestationToAgree, agreed, practitionerId, nominateOwner, commit, act, takeUpOwnership, ]); // Finishes the held operation; nothing is asked again. const finish = useCallback(async (held: OwnerAction) => { const route = resumeRouteOf(held); const op = operationOf(held); if (!organizationId || !route || !op) return; await act(held, (transport) => resumeOwnerOperation(organizationId, route, op, held.action === 'nominate' ? held.practitionerId : undefined, transport), async (next, transport, belongs) => { commit(next, transport, belongs); await takeUpOwnership(belongs); }); }, [organizationId, resumeOwnerOperation, commit, takeUpOwnership, act]); const staleNotice = organizationStale && (

{OWNER_SAVED_REFRESH_FAILED}

); const linkUnfinished = state?.transition?.state === 'settling' && state.transition.route === 'accept-link' && Boolean(state.transition.operationId); if (!heldAction && linkUnfinished) { return (

{OWNER_UNFINISHED_BY_LINK}

); } if (heldAction && heldSettling) { return (

{state?.transition?.operationId === operationOf(heldAction)?.id ? OWNER_UNFINISHED : OWNER_ACTION_SETTLING}

{error &&

{error}

}
); } if (heldAction) { return (

{unconfirmedOwnerAction(heldAction)}

{notFound && ( This practice's owner record does not show it yet. It may still be recorded, so it is not sent again. Check again in a minute. )}
); } // A settled owner without administrator standing: the lawful next step, and never a repeat acceptance or claim. const recovery = ownerRecoveryWords(state, viewerIsOwner); if (notice.tone === 'none' && recovery) { return (

{recovery}

{staleNotice}
); } // The server's flag decides; a named representative is offered it from the named-owner fact too, and a refusal is shown. if ((state?.canSelfNominate || (state?.youAreTheNamedOwner && state.status !== 'accepted')) && state.attestation && practitionerId) { // An agency's named representative signs for the client organization; they are never told they own it. const representative = state.foundingOrigin === 'agency' || state.youAreTheNamedOwner === true; return (

{representative ? 'Confirm ownership for your organization' : 'Confirm you own this practice'}

{representative ? 'This finishes setting up the practice. You confirm as the authorized representative of the organization that owns it, and you get administrator access.' : 'This finishes setting up your practice. Confirming makes you its owner and gives you administrator access.'} {error &&

{error}

} {outcome &&

{outcome}

} {staleNotice}
); } if (notice.tone === 'none') return staleNotice || null; const action = notice.tone === 'action'; // `info` is a healthy state, so it gets neither the alarm colour nor the warning icon. Dressing a // hand-off as a problem is how the real "this practice has no owner" warning stops being read. const info = notice.tone === 'info'; const nominatingSelf = chosen !== '' && chosen === practitionerId; // The owner is offered a HAND-OVER, never a nomination: a practice with an owner already has one, // and the server refuses a nomination over a sitting owner precisely so an agency cannot re-nominate. const offeringTransfer = info && viewerIsOwner; // The staff member with nobody to ask. Computed here rather than read off the notice's wording, // for the same reason `offeringTransfer` is: a string comparison against copy is a gate that // breaks silently the first time somebody improves a sentence. const offeringClaim = Boolean(state?.needsOwner) && !canNominate; const tint = action ? 'border-[var(--blue-line)] bg-[var(--blue-soft)]' : info ? 'border-border bg-card' : 'border-[var(--amber-line)] bg-[var(--amber-soft)]'; return (

{action || info ? : } {notice.title}

{notice.body}

{recovery &&

{recovery}

} {error &&

{error}

} {/* ⚠️ Whether the nominee actually HEARD. ⛔ Silence is not success: an unreachable nominee becomes an instruction, because a nomination nobody was told about is exactly the pending state that looks handled and is not. */} {outcome &&

{outcome}

} {staleNotice} {/* ── THE OWNER HANDS IT OVER ───────────────────────────────────────────────────────── ⛔ A SEPARATE CONTROL FROM THE NOMINATION PICKER BELOW, not a reuse of it. A practice with an owner cannot be re-nominated — the server refuses that outright, precisely so an agency cannot nominate over a sitting owner — so offering "choose an owner" here would draw a button whose only outcome is a refusal. ⛔ And the owner is not among the candidates: transferring to yourself is refused, and offering it invites the click that produces the error. */} {offeringTransfer && expanded && (
{/* 🚨 The sentence that stops an owner believing they are done. They remain the owner — and the only person who cannot be removed — until the other person acts. */}

You stay the owner until they accept it themselves. Nothing moves before that.

)} {!action && !info && expanded && canNominate && (
{/* ⭐ WHICH SENTENCE APPEARS DEPENDS ON WHO IS BEING NAMED — two attestations, by two different people, and each person only ever sees their own. 🔬 CARD 764, CONTROL ①: THE OWNER-OPERATOR PATH DOES NOT MOVE. Somebody setting up their own single-practitioner practice names themselves, ticks the box that is already true, and is the Owner. Same click, same sentence, same number of steps — they are never asked whether they are an agency. */} {nominatingSelf && state?.attestation && ( )} {chosen !== '' && !nominatingSelf && ( <> {/* 🖊️ THE OTHER HALF, AND IT IS THE ONE THE ACCOUNTABILITY WAS BUILT FOR. Naming somebody else means you are setting this practice up on its behalf, and that claim is now a record with a name and a date on it rather than an unticked box. ⛔ SHOWN, NEVER SUMMARISED — this exact sentence is what gets stored, so a paraphrase here would be evidence of something nobody read. */} {state?.contractorAttestation && ( )}

They will be asked to confirm it themselves. Until they do, this practice still has no owner.

)}
)} {/* ⭐ THE ATTESTATION IS SHOWN, NOT SUMMARISED. The nominee is agreeing to a specific sentence, and that exact sentence is what gets stored as the evidence — a paraphrase on screen would mean the record proves agreement to words they never read. */} {action && expanded && state?.attestation && (
)} {/* TAKING AN ABANDONED PRACTICE BACK - promotion from within, which the takeover design prefers over anything support-mediated. Two steps, and the first one changes nothing: the code goes to the PRACTICE'S own recorded contact details, and only reading it back moves anything. */} {offeringClaim && expanded && (
{!claimSentTo ? ( <> {/* It says where the code is going BEFORE it is sent, because somebody who cannot reach that inbox needs to know now rather than after waiting for a message they will never see. */} {/* SPANS, NOT PARAGRAPHS, for the reason index.css states about itself: it normalises colour on the root and on p / li / label / input / select / textarea / button at a specificity the utility layer cannot beat and later in the file, so a `text-...` class on a

here is inert. A span is not on that list, so the colour lands and it costs no override. */} We will send a code to this practice's recorded contact details. Enter it to confirm you can receive messages there.

) : ( <> We have sent a code to {claimSentTo}. It is good for fifteen minutes. setClaimCode(event.target.value)} /> {/* SHOWN, NEVER SUMMARISED - this exact sentence is what gets stored, so a paraphrase here would be evidence of something nobody read. Same rule and the same statement as the nomination path, which is the point: two ways of becoming the owner must not carry different evidence for one act. */} {state?.attestation && ( )} Taking ownership also gives you administrator access to manage this practice.
{/* A code that expired needs another one, and the only control that could send it would otherwise be behind a panel this person has already left. */}
)}
)} {notice.action && !expanded && ( )}
); }; /** * The signed-in practitioner's own id. * * Read from auth rather than passed in: whether the reader IS the nominee decides whether this shows * a warning or a task, and a caller that got it wrong would show the one person who can fix it a notice * saying somebody ought to. */ function usePractitionerIdentity(): { practitionerId: string | undefined } { const auth = useAuthServiceContext() as { currentUser?: { fhir?: { resourceType?: string; id?: string } } }; const fhir = auth?.currentUser?.fhir; // Only a Practitioner. A patient or related person reading this must never be mistaken for the // nominee, which would turn a warning into an accept button they cannot use. return { practitionerId: fhir?.resourceType === 'Practitioner' ? fhir.id : undefined }; } export default OrganizationOwnerNotice; export { OrganizationOwnerNotice };