import { useCallback, createContext, useContext, ReactNode } from 'react'; import { environment } from '../environments/environment.ts'; import { useHttpService } from './HttpService.tsx'; import { announceSupportTicketRaised } from '../utils/supportTicketRaised.ts'; /** * SUPPORT TICKETS - the escalation out of the in-product assistant. * * Spencer, 2026-09-08: *"we haven't built the support ticket form yet. Go ahead and build the * support ticket form and the link to switch to it within the ai assistant overlay and I'll have * toncy wire it up once it's built."* * * NOT FRESHDESK. There is no Freshdesk tenant, embed or URL anywhere in this client - the earlier * header tab that opened one was removed, and the only trace left is a comment explaining why. The * ticket system is Akashic's own, operated from `codemonkeys.akashic.health`, and this is the client * half of it. * * THE SERVER SIDE NOW EXISTS. The request below is the whole contract: one POST, a body with five * fields, and a ticket id and reference back - never the whole ticket, so a caller must not build one * out of this response. * * POST {fhirIdentityApi}/support/tickets * { subject, body, workspace, route, lastQuestion? } -> { ticketId, reference } * * WHY `fhirIdentityApi`: a ticket is raised BY a person and about their account, not about a * patient's clinical record, so it belongs with the identity-scoped routes (proofing, related-person, * payment) rather than on the clinical host. The server derives who is asking from the caller's own * auth context - the body never carries an identity, exactly as the proofing routes never do. * * NOTHING CLINICAL TRAVELS. The context attached is where the person was and what they last asked * the assistant - a route and their own words. It is deliberately NOT a page capture: a support * ticket that scrapes the screen would carry another patient's name off a receptionist's queue the * first time somebody at a front desk raised one. */ const API_URL = environment.fhirIdentityApi; export type SupportTicketDraft = { /** One line, the reader's own summary. Required - a ticket with no subject cannot be triaged. */ subject: string; /** What happened, in their words. */ body: string; /** * The surface they were on when they asked, in the product's terms rather than a URL - "the * patient portal", "the front desk". Support reads this before anything else. */ workspace: string; /** The in-app route, so support can open the same screen rather than asking which one it was. */ route: string; /** * The last thing they asked the assistant, when there was one. This is the single most useful * field on the form and the reader never types it: somebody escalates BECAUSE the assistant did * not answer, so the question that failed is the question support needs. */ lastQuestion?: string; }; export type SupportTicketReceipt = { ticketId: string; /** The human reference to quote back - "AKS-1042". */ reference: string; }; export const useSupportTicketService = () => { const { post } = useHttpService(); const submitSupportTicket = useCallback( async (draft: SupportTicketDraft): Promise => { const receipt = await post(`${API_URL}/support/tickets`, draft); announceSupportTicketRaised(); return receipt; }, [post], ); return { submitSupportTicket }; }; const SupportTicketServiceContext = createContext< ReturnType | undefined >(undefined); export const SupportTicketServiceProvider = ({ children }: { children: ReactNode }) => { const value = useSupportTicketService(); return ( {children} ); }; export const useSupportTicketServiceContext = () => { const context = useContext(SupportTicketServiceContext); if (!context) { throw new Error( 'useSupportTicketServiceContext must be used within a SupportTicketServiceProvider', ); } return context; };