// Admin domain — TanStack Query hooks + query keys (the only public surface). // State-transition mutations invalidate the proposals/lineItems domain keys // (rule 3 — cross-domain invalidation via the sibling key objects) and carry // the same toasts the AdminWorkspace page shows today. Page-specific side // effects (dialog close, dirty reset, window.open, navigate) stay at the // call site via the mutation's callbacks. import { useMutation, useQuery, useQueryClient, type QueryClient } from '@tanstack/react-query'; import { toast } from 'react-toastify'; import { adminApi } from './api'; import { lineItemsApi } from '../lineItems/api'; import { proposalsKeys } from '../proposals/use-cases'; import { lineItemsKeys } from '../lineItems/use-cases'; import type { UpdateLineItemEntry } from '../lineItems/types'; export const adminKeys = { all: ['admin'] as const, dashboard: () => [...adminKeys.all, 'dashboard'] as const, similar: (proposalId: string) => [...adminKeys.all, 'similar', proposalId] as const, }; /** * Every state transition must refresh every view of the proposal: the * workspace (detail + line items) AND the cached queue/list/stats/KPI * queries — lists sit under the global 5-minute staleTime, so without the * broad invalidation an admin returning to the queue after approving sees * stale statuses. */ function invalidateProposalViews(queryClient: QueryClient, proposalId: string) { queryClient.invalidateQueries({ queryKey: proposalsKeys.detail(proposalId) }); queryClient.invalidateQueries({ queryKey: lineItemsKeys.byProposal(proposalId) }); queryClient.invalidateQueries({ queryKey: proposalsKeys.lists() }); queryClient.invalidateQueries({ queryKey: proposalsKeys.stats() }); queryClient.invalidateQueries({ queryKey: adminKeys.dashboard() }); } /** Admin dashboard KPIs (AdminDashboard). */ export function useAdminDashboard() { return useQuery({ queryKey: adminKeys.dashboard(), queryFn: adminApi.getDashboard, }); } /** RAG similarity results (AdminWorkspace, SimilarProposalsPanel). */ export function useSimilarProposals(proposalId: string | undefined) { return useQuery({ queryKey: adminKeys.similar(proposalId ?? ''), queryFn: () => adminApi.getSimilar(proposalId!), enabled: !!proposalId, }); } export interface SaveWorkspaceVariables { refinedScope: string; lineItems: UpdateLineItemEntry[]; } /** * The workspace "Save" action: persist the refined scope, then replace the * proposal's line items (mirrors AdminWorkspace saveMutation exactly). */ export function useSaveProposalWorkspace(proposalId: string) { const queryClient = useQueryClient(); return useMutation({ mutationFn: async ({ refinedScope, lineItems }: SaveWorkspaceVariables) => { await adminApi.updateProposal(proposalId, { refinedScope }); await lineItemsApi.bulkUpdate(proposalId, lineItems); }, onSuccess: () => { invalidateProposalViews(queryClient, proposalId); toast.success('Changes saved'); }, onError: (error: Error) => { toast.error(`Save failed: ${error.message}`); }, }); } export function useApproveProposal(proposalId: string) { const queryClient = useQueryClient(); return useMutation({ mutationFn: () => adminApi.approveProposal(proposalId), onSuccess: () => { invalidateProposalViews(queryClient, proposalId); toast.success('Proposal approved'); }, onError: (error: Error) => { toast.error(`Approval failed: ${error.message}`); }, }); } export function useSendProposal(proposalId: string) { const queryClient = useQueryClient(); return useMutation({ mutationFn: () => adminApi.sendProposal(proposalId), onSuccess: () => { invalidateProposalViews(queryClient, proposalId); toast.success('Proposal marked as sent'); }, // Fix: WEB-H5 — mutation must surface failures to the user (relocated // from AdminWorkspace during the domain-layer refactor) onError: (error: Error) => { toast.error(`Send failed: ${error.message}`); }, }); } export function useReviseProposal(proposalId: string) { const queryClient = useQueryClient(); return useMutation({ mutationFn: () => adminApi.reviseProposal(proposalId), onSuccess: () => { invalidateProposalViews(queryClient, proposalId); toast.success('Revision created'); }, // Fix: WEB-H6 — mutation must surface failures to the user (relocated // from AdminWorkspace during the domain-layer refactor) onError: (error: Error) => { toast.error(`Revision failed: ${error.message}`); }, }); } export function useReturnToReview(proposalId: string) { const queryClient = useQueryClient(); return useMutation({ mutationFn: () => adminApi.returnToReview(proposalId), onSuccess: () => { invalidateProposalViews(queryClient, proposalId); toast.success('Proposal returned to review'); }, onError: (error: Error) => { toast.error(`Return to review failed: ${error.message}`); }, }); } export function useGenerateSuggestions(proposalId: string) { return useMutation({ mutationFn: () => adminApi.generateSuggestions(proposalId), onSuccess: () => { toast.info('AI suggestion generation started'); }, onError: (error: Error) => { toast.error(`Regeneration failed: ${error.message}`); }, }); } /** * Fetch (or queue generation of) the branded PDF. Resolves null while * generation is queued (202). Success handling — open the download and * toast — is page behavior and belongs at the call site. */ export function useProposalPdf(proposalId: string) { return useMutation({ mutationFn: (regenerate?: boolean) => adminApi.getPdf(proposalId, regenerate ?? false), onError: () => { toast.error('Failed to generate PDF'); }, }); }