import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import toast from "react-hot-toast"; import type { QueryClient } from "@tanstack/react-query"; import type { Freight } from "@edr/types"; import { QUERY_KEYS } from "@/constants/QUERY_KEYS"; import { contractsService, type ContractListFilter, type SignContractPayload, } from "@/services/contracts.service"; function invalidateContractDetail(qc: QueryClient, id: string): Promise { return Promise.all([ qc.invalidateQueries({ queryKey: QUERY_KEYS.CONTRACTS.byId(id) }), qc.invalidateQueries({ queryKey: QUERY_KEYS.CONTRACTS.ROOT }), ]).then(() => undefined); } export function useContractList(filter?: ContractListFilter, enabled = true) { return useQuery({ queryKey: QUERY_KEYS.CONTRACTS.list(filter), queryFn: () => contractsService.list(filter), enabled, }); } export function useContractListSummary( filter?: ContractListFilter, enabled = true, ) { return useQuery({ queryKey: QUERY_KEYS.CONTRACTS.listSummary(filter), queryFn: () => contractsService.getListSummary(filter), enabled, }); } export function useContractDetail(id: string | undefined) { return useQuery({ queryKey: QUERY_KEYS.CONTRACTS.byId(id ?? ""), queryFn: () => contractsService.getById(id!), enabled: Boolean(id), }); } export function useContractClearanceQueue(region = "ET", enabled = true) { return useQuery({ queryKey: QUERY_KEYS.CONTRACTS.clearanceQueue(region), queryFn: () => contractsService.getClearanceQueue(region), enabled, }); } export function useContractMilestones(id: string | undefined) { return useQuery({ queryKey: QUERY_KEYS.CONTRACTS.milestones(id ?? ""), queryFn: () => contractsService.listMilestonesForContract(id!), enabled: Boolean(id), }); } export function useBookingMilestones(bookingId: string | undefined) { return useQuery({ queryKey: QUERY_KEYS.CONTRACTS.bookingMilestones(bookingId ?? ""), queryFn: () => contractsService.listMilestonesForBooking(bookingId!), enabled: Boolean(bookingId), }); } export function useContractMutations(contractId: string) { const qc = useQueryClient(); const onSuccess = (data: { id: string }, message: string) => { toast.success(message); void invalidateContractDetail(qc, data.id); }; const staffAccept = useMutation({ mutationFn: (validityDays: number) => contractsService.staffAccept(contractId, validityDays), onSuccess: (data) => onSuccess(data, "Contract accepted for approval"), onError: () => toast.error("Failed to accept contract"), }); const requestChanges = useMutation({ mutationFn: (note: string) => contractsService.requestChanges(contractId, note), onSuccess: (data) => onSuccess(data, "Changes requested from customer"), onError: () => toast.error("Failed to request changes"), }); const reject = useMutation({ mutationFn: (reason: string) => contractsService.reject(contractId, reason), onSuccess: (data) => onSuccess(data, "Contract rejected"), onError: () => toast.error("Failed to reject contract"), }); // Statuses that mean every approval step is done and the contract is ready to // be generated. Once the final approval lands we generate the PDF // automatically — staff no longer click a separate "Generate" button. const READY_TO_GENERATE = ["APPROVED", "APPROVED_PENDING_SIGNATURE"]; const approveStep = useMutation({ mutationFn: ({ stepId, requiredRole, }: { stepId: string; requiredRole: string; }) => contractsService.approveStep({ id: contractId, stepId, requiredRole }), onSuccess: async (data) => { // If this was the LAST approval, auto-generate the contract so it goes // straight to CONTRACT_READY without a manual step. const alreadyGenerated = Boolean( (data as Freight.IContract).contractGeneratedAt, ); if (READY_TO_GENERATE.includes(data.status) && !alreadyGenerated) { toast.success("Final approval complete — generating contract…"); try { const generated = await contractsService.generateContract(data.id); onSuccess(generated, "Contract generated and ready to sign"); return; } catch { toast.error("Approved, but contract generation failed. Retry below."); void invalidateContractDetail(qc, data.id); return; } } onSuccess(data, "Approval step completed"); }, onError: () => toast.error("Failed to approve step"), }); // Manual fallback generate — used only if auto-generation failed. const generateContract = useMutation({ mutationFn: () => contractsService.generateContract(contractId), onSuccess: (data) => onSuccess(data, "Contract generated"), onError: () => toast.error("Failed to generate contract"), }); const signContract = useMutation({ mutationFn: (payload: SignContractPayload) => contractsService.signContract(contractId, payload), onSuccess: (data) => onSuccess(data, "Contract signed"), onError: () => toast.error("Failed to sign contract"), }); const createBooking = useMutation({ mutationFn: (payload: Freight.CreateBookingUnderContractDto) => contractsService.createBookingUnderContract(contractId, payload), onSuccess: () => { toast.success("Booking created under contract"); void invalidateContractDetail(qc, contractId); }, onError: () => toast.error("Failed to create booking"), }); const isPending = staffAccept.isPending || requestChanges.isPending || reject.isPending || approveStep.isPending || generateContract.isPending || signContract.isPending || createBooking.isPending; return { staffAccept, requestChanges, reject, approveStep, generateContract, signContract, createBooking, isPending, }; } /** Pre-booking clearance mutations (GL ET) keyed on a contract. */ export function useContractClearanceMutations(contractId: string) { const qc = useQueryClient(); const refresh = () => { void qc.invalidateQueries({ queryKey: QUERY_KEYS.CONTRACTS.clearance(contractId), }); void qc.invalidateQueries({ queryKey: ["contracts", "clearance-queue"], }); void invalidateContractDetail(qc, contractId); }; const reviewDocument = useMutation({ mutationFn: (p: { fileKey: string; status: "APPROVED" | "QUERIED"; note?: string; }) => contractsService.reviewClearanceDocument(contractId, p), onSuccess: (_d, p) => { toast.success( p.status === "APPROVED" ? "Document approved" : "Query sent to customer", ); refresh(); }, onError: () => toast.error("Could not update document"), }); const uploadOutputDocuments = useMutation({ mutationFn: (files: Record) => contractsService.uploadClearanceOutput(contractId, files), onSuccess: () => { toast.success("Output documents uploaded"); refresh(); }, onError: () => toast.error("Upload failed"), }); const finalizeClearance = useMutation({ mutationFn: () => contractsService.finalizeClearance(contractId), onSuccess: () => { toast.success("Clearance finalized — ready for booking"); refresh(); }, onError: (e) => toast.error( e instanceof Error ? e.message : "Could not finalize clearance", ), }); return { reviewDocument, uploadOutputDocuments, finalizeClearance }; } /** Complete a post-booking GL milestone. */ export function useCompleteMilestone(bookingId: string) { const qc = useQueryClient(); return useMutation({ mutationFn: ({ code, note }: { code: string; note?: string }) => contractsService.completeMilestone(bookingId, code, note), onSuccess: () => { toast.success("Milestone completed"); void qc.invalidateQueries({ queryKey: QUERY_KEYS.CONTRACTS.bookingMilestones(bookingId), }); }, onError: () => toast.error("Failed to complete milestone"), }); }