82 lines
3.2 KiB
TypeScript
82 lines
3.2 KiB
TypeScript
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<PostMessageResult, unknown, PostAdminMessageVars, PostAdminMessageContext>({
|
|
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<AdminTicketDetail>(key);
|
|
if (previous) {
|
|
const pending: AdminTicketMessage = {
|
|
id: null,
|
|
clientMessageId,
|
|
ticketId,
|
|
body,
|
|
authorRole: role,
|
|
createdAt: new Date().toISOString(),
|
|
isMine: true,
|
|
isInternal,
|
|
sendStatus: 'sending',
|
|
};
|
|
queryClient.setQueryData<AdminTicketDetail>(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<AdminTicketDetail>(key);
|
|
if (current) {
|
|
queryClient.setQueryData<AdminTicketDetail>(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() });
|
|
},
|
|
});
|
|
}
|