import { useMutation, useQueryClient } from '@tanstack/react-query'; import { ticketKeys } from '../keys'; import { ticketsApi } from '../apis'; import type { AdminTicketDetail, AdminTicketMessage, PostMessageResult } from '../types'; import { useTicketViewer } from './useTicketViewer'; interface PostAdminMessageVars { body: string; /** Staff-only: a `true` posts an internal note (never reaches the user view). */ isInternal: boolean; /** Client-generated id; the reconcile key so the optimistic bubble is never double-rendered (§3.5). */ clientMessageId: string; } interface PostAdminMessageContext { previous?: AdminTicketDetail; } /** * The admin optimistic message send — the same pattern as `usePostMessage`, but over the `adminDetail(id)` * cache and carrying `isInternal` so a pending internal note shows its internal styling immediately. * * `onMutate` appends a **pending** `AdminTicketMessage` (with its `isInternal`) after `cancelQueries` + a * snapshot. `onError` rolls the thread back (composer keeps the draft + offers retry). `onSuccess` reconciles * the pending bubble **by `clientMessageId`** with the server message (never double-rendered). `onSettled` * invalidates the admin thread + the admin queues (a new note moves the queue's activity). */ export function usePostAdminMessage(ticketId: number) { const queryClient = useQueryClient(); const { role } = useTicketViewer(); return useMutation({ mutationFn: ({ body, isInternal, clientMessageId }) => ticketsApi.postAdminMessage(ticketId, { body, isInternal, clientMessageId }), onMutate: async ({ body, isInternal, clientMessageId }) => { const key = ticketKeys.adminDetail(ticketId); await queryClient.cancelQueries({ queryKey: key }); const previous = queryClient.getQueryData(key); if (previous) { const pending: AdminTicketMessage = { id: null, clientMessageId, ticketId, body, authorRole: role, createdAt: new Date().toISOString(), isMine: true, isInternal, sendStatus: 'sending', }; queryClient.setQueryData(key, { ...previous, messages: [...previous.messages, pending] }); } return { previous }; }, onError: (_err, _vars, context) => { if (context?.previous) queryClient.setQueryData(ticketKeys.adminDetail(ticketId), context.previous); }, onSuccess: (result, { clientMessageId }) => { const key = ticketKeys.adminDetail(ticketId); const current = queryClient.getQueryData(key); if (current) { queryClient.setQueryData(key, { ...current, messages: current.messages.map((m) => m.clientMessageId === clientMessageId ? { ...m, id: result.messageId, createdAt: result.sentAt, sendStatus: 'sent' } : m, ), }); } }, onSettled: () => { queryClient.invalidateQueries({ queryKey: ticketKeys.adminDetail(ticketId) }); queryClient.invalidateQueries({ queryKey: ticketKeys.adminLists() }); }, }); }