This commit is contained in:
Marshal
2026-07-15 13:29:01 +00:00
parent c71a0043d6
commit 19c9da28ae
59 changed files with 3008 additions and 284 deletions

View File

@@ -125,12 +125,27 @@ export function useContractMutations(contractId: string) {
};
const staffAccept = useMutation({
mutationFn: (validityDays: number) =>
contractsService.staffAccept(contractId, validityDays),
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),
@@ -144,11 +159,6 @@ export function useContractMutations(contractId: string) {
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,
@@ -158,25 +168,15 @@ export function useContractMutations(contractId: 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");
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"),
});
@@ -239,6 +239,7 @@ export function useContractMutations(contractId: string) {
const isPending =
staffAccept.isPending ||
updateDocument.isPending ||
requestChanges.isPending ||
reject.isPending ||
approveStep.isPending ||
@@ -249,6 +250,7 @@ export function useContractMutations(contractId: string) {
return {
staffAccept,
updateDocument,
requestChanges,
reject,
approveStep,

View File

@@ -3,7 +3,11 @@ import toast from "react-hot-toast";
import { QUERY_KEYS } from "@/constants/QUERY_KEYS";
import { api } from "@/services/api";
import { ruleEngineService, type RuleEngineListParams } from "@/services/ruleEngine/ruleEngine.service";
import {
ruleEngineService,
type RuleEngineListParams,
type SubmitPriorityRuleChangePayload,
} from "@/services/ruleEngine/ruleEngine.service";
import { RULE_ENGINE_SELECT_NONE } from "@/pages/ruleEngine/config/resources";
import type {
RuleEngineRecord,
@@ -239,6 +243,68 @@ export const useRuleEngineMutations = (resource: RuleEngineResourceSlug) => {
return { create, update, remove };
};
/**
* Priority-rule approval workflow. Every create/update/delete of a priority
* config is SUBMITTED as a change request; an approver applies or rejects it.
* Error toasts surface the backend message so range-collision rejections
* ("15 overlaps existing rule …") reach the user verbatim.
*/
export const usePriorityRuleWorkflow = (enabled: boolean) => {
const qc = useQueryClient();
const backendMessage = (err: unknown, fallback: string) => {
const msg = (err as { response?: { data?: { message?: string | string[] } } })
?.response?.data?.message;
if (Array.isArray(msg)) return msg.join(", ");
return msg || fallback;
};
const pending = useQuery({
queryKey: QUERY_KEYS.RULE_ENGINE.priorityRuleChanges,
queryFn: () => ruleEngineService.listPriorityRuleChanges("PENDING"),
enabled,
});
const invalidate = async () => {
await qc.invalidateQueries({
queryKey: QUERY_KEYS.RULE_ENGINE.priorityRuleChanges,
});
await invalidateRuleEngineList(qc, "priority-configs");
};
const submit = useMutation({
mutationFn: (payload: SubmitPriorityRuleChangePayload) =>
ruleEngineService.submitPriorityRuleChange(payload),
onSuccess: async () => {
toast.success("Change submitted for approval — the team has been notified");
await invalidate();
},
onError: (err) => toast.error(backendMessage(err, "Failed to submit change")),
});
const approve = useMutation({
mutationFn: ({ id, decisionNote }: { id: string; decisionNote?: string }) =>
ruleEngineService.approvePriorityRuleChange(id, decisionNote),
onSuccess: async () => {
toast.success("Change approved and applied");
await invalidate();
},
onError: (err) => toast.error(backendMessage(err, "Failed to approve change")),
});
const reject = useMutation({
mutationFn: ({ id, decisionNote }: { id: string; decisionNote?: string }) =>
ruleEngineService.rejectPriorityRuleChange(id, decisionNote),
onSuccess: async () => {
toast.success("Change rejected");
await invalidate();
},
onError: (err) => toast.error(backendMessage(err, "Failed to reject change")),
});
return { pending, submit, approve, reject };
};
export const useRateWorkflow = () => {
const qc = useQueryClient();