mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 18:48:11 +00:00
- 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.
251 lines
8.0 KiB
TypeScript
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"),
|
|
});
|
|
}
|