// THE STAFF-SIDE ENTRY POINT FOR INVITING AN OUTSIDE PRACTITIONER -, situation 4 of the // consent chain. // // SPENCER'S RULING (2026-08-23): there are TWO surfaces from which a practitioner invites another // practitioner - through the REFERRAL system, and through the COLLABORATION TEAM system. This is the // collaboration-team one. The referral half is its own chain of cards and is not this file. // // What was actually missing, measured before building any of it: nothing but the mount. The dialog, // the service calls and the server routes all existed and were exercised by the PATIENT path only - // `CareTeamsPage` (which hosts `InviteOutsidePractitioner`) is mounted inside `RequirePatient`, and // `StaffCareTeamsPage` handled invites RECEIVED and never sent. A practitioner could accept an // invitation and could not send one. // // IT LIVES HERE, NOT IN `StaffCareTeamsPage.tsx`, ON PURPOSE - the same reason // `StartCaseConferenceButton` gives for itself. That page is a shared file other sessions restyle, so // this card's diff there is ONE import and ONE element, and every concern of this feature is in here. // // `patientInitiated={false}` IS THE WHOLE DIFFERENCE, and the component was built for both callers: // its own prop doc reads "True when the OWNING PATIENT is inviting; false when a team practitioner is // referring." Passing false skips `buildReferralAuthorization()` - a practitioner cannot agree on the // patient's behalf - and hides the patient's own agreement checkbox. // // SO NOBODY GETS ACCESS WITHOUT THE PATIENT, and that invariant is the server's, not this button's: // `CollaborationTeam/members/post.ts` states that "a member-initiated invite (a practitioner inviting // someone onto the patient's team) always goes to 'pending-patient-approval' - nobody the patient // didn't invite becomes active without the patient approving first." // This control creates a PENDING invitation. It grants nothing. // // ONLY THE OUTSIDE-PRACTITIONER TAB IS MOUNTED, deliberately. The patient's `InviteMemberDialog` // also carries a "Related person" tab; a practitioner adding the patient's family to a collaboration // team is a different and consent-sensitive act that nobody has ruled on. Shipping the whole dialog // would have smuggled it in. import { useState } from "react"; import { UserPlus } from "lucide-react"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "../../../components/ui/dialog"; import { InviteOutsidePractitioner } from "../CareTeamsPage/InviteOutsidePractitioner"; // From the BARREL, not `services/CollaborationTeamService` directly - the same import the host page // uses. Reaching past it works in the app and breaks every existing test of that page, which mocks the // barrel: a sibling suite went red on the deep import before this line was written. import { useCollaborationTeamServiceContext } from "../../../services"; interface Props { teamId: string; /** The team's name, so the dialog says which patient's team is being added to. */ teamName: string; /** Refresh the caller's list once an invite lands, so the new row appears without a reload. */ onInvited: () => Promise; } export const InvitePractitionerButton: React.FC = ({ teamId, teamName, onInvited }) => { const { resolveInvitee, inviteMember, inviteExternalPractitioner } = useCollaborationTeamServiceContext(); const [open, setOpen] = useState(false); return ( <> {/* House ghost, not the shared ghost variant: that one resolves to no border and no fill, so this drew as bare text beside a filled primary. Full-size because it stands beside one. */} Invite a practitioner to “{teamName}” {/* 🔑 Says the outcome rather than the mechanism. A practitioner who expects the invitee to get access immediately would otherwise read the pending state as a failure and invite again — and the patient would receive two approval requests for one referral. */} Search by email or NPI. The patient approves before anyone you invite can reach their record. { setOpen(false); await onInvited(); }} /> ); };