// 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 } 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 { UpdateProposalRequest } from './types'; 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, history: (proposalId: string) => [...adminKeys.all, 'history', proposalId] as const, audit: (proposalId: string) => [...adminKeys.all, 'audit', proposalId] as const, }; /** 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 function useProposalHistory(proposalId: string | undefined) { return useQuery({ queryKey: adminKeys.history(proposalId ?? ''), queryFn: () => adminApi.getHistory(proposalId!), enabled: !!proposalId, }); } export function useProposalAudit(proposalId: string | undefined) { return useQuery({ queryKey: adminKeys.audit(proposalId ?? ''), queryFn: () => adminApi.getAudit(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: () => { queryClient.invalidateQueries({ queryKey: lineItemsKeys.byProposal(proposalId) }); queryClient.invalidateQueries({ queryKey: proposalsKeys.detail(proposalId) }); toast.success('Changes saved'); }, onError: (error: Error) => { toast.error(`Save failed: ${error.message}`); }, }); } export function useUpdateProposal(proposalId: string) { const queryClient = useQueryClient(); return useMutation({ mutationFn: (data: UpdateProposalRequest) => adminApi.updateProposal(proposalId, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: proposalsKeys.detail(proposalId) }); }, onError: (error: Error) => { toast.error(`Save failed: ${error.message}`); }, }); } export function useApproveProposal(proposalId: string) { const queryClient = useQueryClient(); return useMutation({ mutationFn: () => adminApi.approveProposal(proposalId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: proposalsKeys.detail(proposalId) }); queryClient.invalidateQueries({ queryKey: lineItemsKeys.byProposal(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: () => { queryClient.invalidateQueries({ queryKey: proposalsKeys.detail(proposalId) }); toast.success('Proposal marked as sent'); }, onError: (error: Error) => { toast.error(`Send failed: ${error.message}`); }, }); } export function useReviseProposal(proposalId: string) { const queryClient = useQueryClient(); return useMutation({ mutationFn: () => adminApi.reviseProposal(proposalId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: proposalsKeys.detail(proposalId) }); toast.success('Revision created'); }, onError: (error: Error) => { toast.error(`Revision failed: ${error.message}`); }, }); } export function useReturnToReview(proposalId: string) { const queryClient = useQueryClient(); return useMutation({ mutationFn: () => adminApi.returnToReview(proposalId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: proposalsKeys.detail(proposalId) }); queryClient.invalidateQueries({ queryKey: lineItemsKeys.byProposal(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'); }, }); }