general contrat

This commit is contained in:
Marshal
2026-07-10 17:57:27 +00:00
parent a58650bdfb
commit b50075dc83
23 changed files with 639 additions and 89 deletions

View File

@@ -1,10 +1,14 @@
import { Button, Group, type ButtonProps } from "@mantine/core";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import type { LucideIcon } from "lucide-react";
import type { ReactNode } from "react";
import toast from "react-hot-toast";
import { useNavigate } from "react-router-dom";
import type { Freight } from "@edr/types";
import { PayNowButton } from "@/pages/bookings/payments/PayNowButton";
import { api } from "@/services/api";
import { ContractClearanceAction } from "./ContractClearanceAction";
import { deriveContractCustomerAction } from "./deriveContractCustomerAction";
@@ -56,6 +60,18 @@ export function ContractCustomerAction({
return <PayNowButton booking={action.booking} label={action.label} size={size} />;
}
if (action.type === "initiate") {
return (
<InitiateBookingButton
contract={action.contract}
label={action.label}
icon={action.icon}
size={size}
listStyle={listStyle}
/>
);
}
const Icon = action.icon;
const variant = action.primary ? "filled" : "light";
@@ -80,6 +96,88 @@ export function ContractCustomerAction({
);
}
/**
* One-click bare booking instance under a GENERAL non-customs contract. No
* form, no date, no window gate — the new instance lands in per-booking
* clearance (AWAITING_DOCUMENTS) and the customer is taken straight to it.
*/
export function InitiateBookingButton({
contract,
label = "Initiate booking",
icon: Icon,
size = "xs",
listStyle = false,
fullWidth = false,
}: {
contract: Freight.IContract;
label?: string;
icon: LucideIcon;
size?: ButtonProps["size"];
listStyle?: boolean;
fullWidth?: boolean;
}) {
const navigate = useNavigate();
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: () =>
api.contracts.initiateBookingUnderContract.call({
id: contract.id,
// Multi-route contracts must name a route; single-route auto-selects.
contractRouteId:
(contract.routes?.length ?? 0) > 1
? contract.routes![0].id
: undefined,
}),
onSuccess: (booking) => {
queryClient.invalidateQueries({ queryKey: api.bookings.list.queryKey() });
queryClient.invalidateQueries({
queryKey: api.contracts.get.queryKey({ id: contract.id }),
});
toast.success(
"Booking initiated — upload your clearance documents to start the review.",
);
navigate(`/bookings/${booking.id}`);
},
onError: (e: Error) =>
toast.error(e.message || "Could not initiate the booking"),
});
return (
<Button
size={size}
radius="md"
h={listStyle ? 34 : undefined}
variant="filled"
color="edr-green"
fullWidth={fullWidth}
leftSection={<Icon size={15} />}
loading={mutation.isPending}
onClick={(e) => {
e.stopPropagation();
mutation.mutate();
}}
styles={
listStyle
? {
root: {
fontWeight: 600,
fontSize: 13,
paddingInline: 14,
whiteSpace: "nowrap" as const,
boxShadow: "0 1px 2px rgba(14,163,113,0.25)",
},
}
: undefined
}
fw={listStyle ? undefined : 700}
fz={listStyle ? undefined : 13}
>
{label}
</Button>
);
}
/** Action column cell: doc button + primary customer action. */
export function ContractCustomerActionCell({
contract,

View File

@@ -72,6 +72,14 @@ export type ContractCustomerAction =
label: string;
primary: boolean;
icon: LucideIcon;
}
| {
/** One-click bare booking instance (GENERAL non-customs) — mutation, not navigation. */
type: "initiate";
contract: Freight.IContract;
label: string;
primary: boolean;
icon: LucideIcon;
};
function findPayableBookingForContract(
@@ -210,6 +218,15 @@ export function deriveContractCustomerAction(
}
const bookingAction = getContractBookingAction(contract, bookings);
if (bookingAction.kind === "initiate") {
return {
type: "initiate",
contract,
label: "Initiate booking",
primary: true,
icon: PackagePlus,
};
}
if (bookingAction.kind === "book") {
return {
type: "navigate",