import { useCallback, useEffect, useRef, useState } from "react"; import { useLocation } from "react-router-dom"; import SetupHelpWidget from "./SetupHelpWidget.tsx"; import { AssistantPanel } from "./AssistantPanel.tsx"; import { assistantSubject, useAssistantConversation } from "./useAssistantConversation.ts"; import { useAuthServiceContext } from "../../services"; import { useSupportTicketRequests, type SupportTicketOutcome, type SupportTicketRequest } from "./supportTicketRequest.ts"; /** * Where the setup & help widget meets the app. * * THE SEAM IS DELIBERATE AND THIN: the container knows nothing about what it holds, and this joins it * to the assistant. That boundary is what kept the shell still when the onboarding checklist came out * of this surface - the piece that changed was this file and the contract, not the widget. * * IT USED TO ASSEMBLE THE ONBOARDING CHECKLIST HERE, through `useSetupGuidance`: locations, * practitioners, schedules, the public-form count and a resolved task list, so the widget could name an * admin's next step and count the rest. The steps live on the overview page, which is the one place * that survives - so the assembly went with the checklist, and with it three list fetches that ran * behind every screen an admin opened. * * ONE RENDER FOR EVERY VIEWER, and that is the fix a previous version of this file needed rather than a * simplification for its own sake. It used to branch on whether the viewer could reach the setup * surface, because the affordance ended in `navigate("/dashboard/organization")` - and a signed-in * PATIENT clicked "Setup and help" and landed on their practice's admin screen (observed in a browser * 2026-08-19). The server held and nothing was disclosed, but a patient who believes they have seen * somebody else's admin screen has been harmed whether or not data crossed. The assistant is a help * destination that belongs to everybody, so there is no navigate left to gate and no branch left to * take. */ // Held above the panel, which unmounts on close, so reopening on the same page keeps the conversation. export const SetupHelpWidgetMount = () => { const conversation = useAssistantConversation(assistantSubject(useLocation())); const { currentUser, currentOrganization } = useAuthServiceContext(); const viewer = currentUser?.fhir?.id; const organizationId = currentOrganization?.id; const [ticketRequest, setTicketRequest] = useState(); const [openRequests, setOpenRequests] = useState(0); // Who is told what became of the request being held: only the asker of that exact opening. const answer = useRef<{ opening: string; tell?: (outcome: SupportTicketOutcome) => void } | undefined>(); const settle = useCallback((outcome: SupportTicketOutcome, opening?: string) => { const waiting = answer.current; if (!waiting || (opening !== undefined && waiting.opening !== opening)) return; answer.current = undefined; setTicketRequest(undefined); waiting.tell?.(outcome); }, []); // A request names the viewer and practice it was made for: another's is refused, and a change of either drops it. useEffect(() => { answer.current = undefined; setTicketRequest(undefined); }, [viewer, organizationId]); const take = useCallback((request: SupportTicketRequest, tell?: (outcome: SupportTicketOutcome) => void) => { if (!viewer || !organizationId || request.viewer !== viewer || request.organizationId !== organizationId) return false; // One opening's reference is never replaced by another's before the panel has taken it. if (answer.current && answer.current.opening !== request.opening) return false; answer.current = { opening: request.opening, tell }; setTicketRequest((current) => current ?? request); setOpenRequests((count) => count + 1); return true; }, [viewer, organizationId]); useSupportTicketRequests(take); return ( settle("refused")}> settle(outcome, opening)} /> ); }; export default SetupHelpWidgetMount;