Files
edr-platform/apps/edr-freight-web/backoffice/src/hooks/contracts/useContracts.ts
Marshal 0a23ade118 enhance contract management features
- Update ContractActionsToolbar to streamline contract generation and signing processes.
- Introduce getStaffRowAction for improved action handling in contract list rows.
- Implement auto-generation of contracts upon final approval in useContracts hook.
- Refactor BookingDetailPage to remove unnecessary container type property.
- Add action buttons in ContractRequestsPage for better user interaction.
- Simplify container types configuration in resources.
- Remove unused properties from booking types.
- Introduce ContractViewPage for staff and customer contract signing.
- Enhance NewContractPage to validate document uploads before proceeding.
- Improve StepDocuments to ensure required documents are uploaded.
2026-06-27 17:38:26 +00:00

251 lines
8.0 KiB
TypeScript

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<void> {
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<string, File | null>) =>
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"),
});
}