import { useState } from "react"; import { ArrowLeft, LifeBuoy } from "lucide-react"; import { useSupportTicketService } from "../../services/SupportTicketService.tsx"; import { relayServerFailure } from "../../utils/fetchFailureMessage"; import { Input } from "../ui/input.tsx"; /** * THE SUPPORT TICKET FORM - the assistant's escalation, in the assistant's own panel. * * Spencer, 2026-09-08: *"when you click 'or submit a support ticket' it just displays the support * ticket form in the ai assistant overlay."* * * SO IT IS A VIEW, NOT A DESTINATION. No dialog over a dialog, no new tab, no route: the panel swaps * what it is showing and swaps back. Somebody escalating is already stuck, and taking their screen * away from them at that moment is how a support ticket turns into a phone call. * * WHAT THEY DO NOT HAVE TO TYPE is the point of putting it here rather than on a support site. The * workspace, the route and the question the assistant just failed to answer are all known, and they * are shown so the reader can SEE what is attached rather than trusting that something is. * * The context is where they were and what they asked - never a capture of the screen. A ticket * that scraped the page would carry another patient's name out of a front desk's queue the first * time staff raised one. */ export type SupportTicketFormProps = { /** Back to the conversation. The panel stays open either way. */ onBack: () => void; /** The surface being helped with, in the reader's terms. */ workspace: string; /** The in-app route, so support can open the screen rather than ask which one. */ route: string; /** The last thing asked of the assistant, when there was one. */ lastQuestion?: string; /** * Prefills only the first draft with a page-supplied recovery reference. * The reader sends the request; this is not its resulting support reference. */ recoveryReference?: string; }; export const SupportTicketForm = ({ onBack, workspace, route, lastQuestion, recoveryReference, }: SupportTicketFormProps) => { /* The hook, not the context: this service is a thin wrapper over the HTTP client with no shared state, so requiring a provider high in the tree would be ceremony that can only fail. */ const { submitSupportTicket } = useSupportTicketService(); const [subject, setSubject] = useState(recoveryReference ? "Help with an invitation that is on hold" : ""); const [body, setBody] = useState(recoveryReference ? `Reference: ${recoveryReference}\n\n` : ""); const [sending, setSending] = useState(false); const [error, setError] = useState(null); const [reference, setReference] = useState(null); const submit = async (event: React.FormEvent) => { event.preventDefault(); if (sending || !subject.trim() || !body.trim()) return; setSending(true); setError(null); try { const receipt = await submitSupportTicket({ subject: subject.trim(), body: body.trim(), workspace, route, lastQuestion, }); setReference(receipt.reference); } catch (failure) { /* * THE TEXT IS NOT CLEARED ON A FAILURE, and that is the whole of the error handling * that matters. Somebody has just written out a problem they could not solve; losing it * to a failed request is a worse experience than the one that brought them here. The * fields keep what they hold, and the button says try again. */ setError( relayServerFailure(failure, "That could not be sent just now. Your message is still here — try again."), ); } finally { setSending(false); } }; if (reference) { return (

Your ticket is in.

Quote {reference} if you get in touch about it. We will reply by email.

{/* THE ONLY WAY BACK TO THIS TICKET FOR AS LONG AS IT STAYS OPEN. An email arrives only once somebody replies, so without this link an open ticket has no door back to it at all - the person who just raised it would have to know the URL by heart. A plain anchor rather than `useNavigate`: the widget mounts wherever it is embedded, and cannot assume it sits inside a router context of its own. */} Follow this ticket
); } return (

This goes to Akashic support. Ask your practice administrator about patients or practice procedures.

{/* 🚨 `htmlFor` AND AN `id`, NOT A LABEL WRAPPED ROUND THE FIELD. Wrapping works for a native control and does NOT work here: `Input` is a component, so what sits inside the label at author time is a tag the checker cannot see through, and the association it reports is the one a browser would actually make. ⚖️ The textarea below wraps and passes for exactly that reason - it is a real `