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(enabled = true) { return useQuery({ queryKey: QUERY_KEYS.CONTRACTS.clearanceQueue("GL"), queryFn: () => contractsService.getClearanceQueue(), enabled, }); } export function useEtClearanceQueue(enabled = true) { return useQuery({ queryKey: QUERY_KEYS.CONTRACTS.clearanceQueue("ET"), queryFn: () => contractsService.getEtClearanceQueue(), enabled, }); } export function useDjClearanceQueue(enabled = true) { return useQuery({ queryKey: QUERY_KEYS.CONTRACTS.clearanceQueue("DJ"), queryFn: () => contractsService.getDjClearanceQueue(), enabled, }); } /** Path A self-clearance queue (Operations reviews non-customs contracts). */ export function useOpsClearanceQueue(enabled = true) { return useQuery({ queryKey: QUERY_KEYS.CONTRACTS.clearanceQueue("OPS"), queryFn: () => contractsService.getOpsClearanceQueue(), enabled, }); } export function useContractClearanceHistory(enabled = true) { return useQuery({ queryKey: QUERY_KEYS.CONTRACTS.clearanceHistory("GL"), queryFn: () => contractsService.getClearanceHistory(), enabled, }); } export function useOpsClearanceHistory(enabled = true) { return useQuery({ queryKey: QUERY_KEYS.CONTRACTS.clearanceHistory("OPS"), queryFn: () => contractsService.getOpsClearanceHistory(), enabled, }); } export function useContractMilestones(id: string | undefined) { return useQuery({ queryKey: QUERY_KEYS.CONTRACTS.milestones(id ?? ""), queryFn: () => contractsService.listMilestonesForContract(id!), enabled: Boolean(id), }); } export function useContractCapacity(id: string | undefined) { return useQuery({ queryKey: QUERY_KEYS.CONTRACTS.capacity(id ?? ""), queryFn: () => contractsService.getCapacity(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: (payload: { validityDays: number; documentSnapshot?: Freight.IContractDocumentSnapshot; }) => contractsService.staffAccept( contractId, payload.validityDays, payload.documentSnapshot, ), onSuccess: (data) => onSuccess(data, "Contract accepted for approval"), onError: () => toast.error("Failed to accept contract"), }); // Edit THIS contract's document articles (per-contract; never the templates). const updateDocument = useMutation({ mutationFn: (snapshot: Freight.IContractDocumentSnapshot) => contractsService.updateContractDocument(contractId, snapshot), onSuccess: (data) => onSuccess(data, "Contract document updated"), onError: () => toast.error("Failed to update contract document"), }); 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"), }); const approveStep = useMutation({ mutationFn: ({ stepId, requiredRole, }: { stepId: string; requiredRole: string; }) => contractsService.approveStep({ id: contractId, stepId, requiredRole }), onSuccess: (data) => { // The document is generated at the accept stage and reviewed during // approval, so the final approval moves the contract straight to // CONTRACT_READY on the server — no client-side generate call here. const message = data.status === "CONTRACT_READY" ? "Final approval complete — contract ready to sign" : "Approval step completed"; onSuccess(data, message); }, onError: () => toast.error("Failed to approve step"), }); // Per-step rejection by an approver (line staff / director / CEO). Terminal: // the contract goes to REJECTED and the customer must create a new one. const rejectStep = useMutation({ mutationFn: ({ stepId, reason }: { stepId: string; reason: string }) => contractsService.rejectStep({ id: contractId, stepId, reason }), onSuccess: (data) => onSuccess(data, "Contract rejected"), onError: () => toast.error("Failed to reject 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); }, // Surface the server's reason (e.g. a container already booked on the same // train) instead of a generic failure. onError: (e: Error) => toast.error(e.message || "Failed to create booking"), }); const completeBooking = useMutation({ mutationFn: ({ bookingId, payload, }: { bookingId: string; payload: Freight.CreateBookingUnderContractDto; }) => contractsService.completeBookingUnderContract( contractId, bookingId, payload, ), onSuccess: () => { toast.success("Booking completed"); void invalidateContractDetail(qc, contractId); }, onError: (e: Error) => toast.error(e.message || "Failed to complete booking"), }); const isPending = staffAccept.isPending || updateDocument.isPending || requestChanges.isPending || reject.isPending || approveStep.isPending || rejectStep.isPending || generateContract.isPending || signContract.isPending || createBooking.isPending; return { staffAccept, updateDocument, requestChanges, reject, approveStep, rejectStep, generateContract, signContract, createBooking, completeBooking, isPending, }; } /** * Pre-booking clearance mutations keyed on a contract. Pass `selfClear = true` * for Path A (non-customs) contracts so review/finalize hit the Operations * endpoints instead of the GL ET ones. Path A has no GL output upload step. */ export function useContractClearanceMutations( contractId: string, selfClear = false, ) { 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; }) => selfClear ? contractsService.opsReviewClearanceDocument(contractId, p) : contractsService.reviewClearanceDocument(contractId, p), onSuccess: (_d, p) => { toast.success( p.status === "APPROVED" ? "Document approved" : "Query sent to customer", ); refresh(); }, onError: (e) => toast.error( e instanceof Error ? e.message : "Could not update document", ), }); // Approve every still-pending customer document in one click. There is no // server-side bulk endpoint, so fan out the single-document review calls and // refresh once after they all settle. const approveAll = useMutation({ mutationFn: async (fileKeys: string[]) => { const review = selfClear ? contractsService.opsReviewClearanceDocument : contractsService.reviewClearanceDocument; await Promise.all( fileKeys.map((fileKey) => review(contractId, { fileKey, status: "APPROVED" }), ), ); }, onSuccess: (_d, fileKeys) => { toast.success( `${fileKeys.length} document${fileKeys.length === 1 ? "" : "s"} approved`, ); refresh(); }, onError: () => toast.error("Could not approve all documents"), }); 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: () => selfClear ? contractsService.opsFinalizeClearance(contractId) : contractsService.finalizeClearance(contractId), onSuccess: () => { toast.success( selfClear ? "Clearance approved — customer can now book" : "Clearance finalized — ready for booking", ); refresh(); }, onError: (e) => toast.error( e instanceof Error ? e.message : "Could not finalize clearance", ), }); return { reviewDocument, approveAll, 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"), }); } function invalidateMilestones(qc: QueryClient, bookingId: string) { void qc.invalidateQueries({ queryKey: QUERY_KEYS.CONTRACTS.bookingMilestones(bookingId), }); } /** Assign a customs risk level (completes RISK_ASSIGNED). */ export function useAssignRisk(bookingId: string) { const qc = useQueryClient(); return useMutation({ mutationFn: (payload: { riskLevel: Freight.CustomsRiskLevel; note?: string; }) => contractsService.assignRisk(bookingId, payload), onSuccess: () => { toast.success("Customs risk assigned"); invalidateMilestones(qc, bookingId); }, onError: () => toast.error("Failed to assign risk"), }); } /** Advise duty & tax (completes DUTY_TAXES_ADVISED). */ export function useAdviseDuty(bookingId: string) { const qc = useQueryClient(); return useMutation({ mutationFn: (payload: { amount: number; currency: string; declarationSerial?: string; note?: string; }) => contractsService.adviseDuty(bookingId, payload), onSuccess: () => { toast.success("Duty & tax advised to customer"); invalidateMilestones(qc, bookingId); }, onError: () => toast.error("Failed to advise duty & tax"), }); } /** Route the shipment to a station + bind GL staff. */ export function useAssignStation(bookingId: string) { const qc = useQueryClient(); return useMutation({ mutationFn: (payload: { stationYardId: string; staffId?: string }) => contractsService.assignStation(bookingId, payload), onSuccess: () => { toast.success("Shipment routed to station"); void qc.invalidateQueries({ queryKey: QUERY_KEYS.BOOKINGS.byId(bookingId), }); }, onError: () => toast.error("Failed to assign station"), }); } /** Upload GL post-booking documents (DO/RO/T1/…); auto-completes milestones. */ export function useUploadGlDocuments(bookingId: string) { const qc = useQueryClient(); return useMutation({ mutationFn: (files: Record) => contractsService.uploadGlDocuments(bookingId, files), onSuccess: (res) => { const n = res.completedMilestones.length; toast.success( n > 0 ? `Uploaded — ${n} milestone${n === 1 ? "" : "s"} advanced` : "Documents uploaded", ); invalidateMilestones(qc, bookingId); }, onError: () => toast.error("Failed to upload documents"), }); } /** Incidents for a shipment (damage / exceptions). */ export function useBookingIncidents(bookingId: string | undefined) { return useQuery({ queryKey: QUERY_KEYS.CONTRACTS.bookingIncidents(bookingId ?? ""), queryFn: () => contractsService.listIncidents(bookingId!), enabled: !!bookingId, }); } /** Report a cargo exception with photos. */ export function useReportIncident(bookingId: string) { const qc = useQueryClient(); return useMutation({ mutationFn: (payload: { incidentType: Freight.IncidentType; description: string; photos: File[]; }) => contractsService.reportIncident(bookingId, payload), onSuccess: () => { toast.success("Incident reported"); void qc.invalidateQueries({ queryKey: QUERY_KEYS.CONTRACTS.bookingIncidents(bookingId), }); }, onError: () => toast.error("Failed to report incident"), }); }