Files
edr-platform/apps/edr-freight-web/portal/src/components/customer-actions/ContractCustomerAction.tsx
Marshal 2429f6b629 implement contract cancellation feature and update contract statuses
- Added functionality to cancel contracts, allowing users to provide a reason for cancellation.
- Updated contract statuses to include SUSPENDED and changed CLOSED to COMPLETED.
- Enhanced the UI to reflect the new cancellation option and updated messaging for contract statuses.
- Refactored contract booking actions to accommodate changes in booking logic for ONE_TIME and GENERAL contracts.
- Removed clearance document management from the contract detail page, as it is now handled per booking.
- Introduced a SQL script to reset bookings and train schedules for development purposes.
2026-07-28 05:02:58 +00:00

250 lines
6.6 KiB
TypeScript

import {
Button,
Group,
Modal,
Text,
ThemeIcon,
type ButtonProps,
} from "@mantine/core";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import type { LucideIcon } from "lucide-react";
import { useState, 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 { deriveContractCustomerAction } from "./deriveContractCustomerAction";
interface ContractCustomerActionProps {
contract: Freight.IContract;
bookings: Freight.IBooking[];
size?: ButtonProps["size"];
/** Extra props for list-row button styling (ContractsList). */
listStyle?: boolean;
}
export function ContractCustomerAction({
contract,
bookings,
size = "xs",
listStyle = false,
}: ContractCustomerActionProps) {
const navigate = useNavigate();
const action = deriveContractCustomerAction(contract, bookings);
const buttonStyles = listStyle
? {
root: {
fontWeight: 600,
fontSize: 13,
paddingInline: 14,
whiteSpace: "nowrap" as const,
boxShadow: action.primary
? "0 1px 2px rgba(14,163,113,0.25)"
: "none",
},
}
: undefined;
if (action.type === "pay") {
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";
return (
<Button
size={size}
radius="md"
h={listStyle ? 34 : undefined}
variant={variant}
color="edr-green"
leftSection={<Icon size={15} />}
onClick={(e) => {
e.stopPropagation();
navigate(action.to);
}}
styles={buttonStyles}
fw={listStyle ? undefined : 700}
fz={listStyle ? undefined : 13}
>
{action.label}
</Button>
);
}
/**
* One-click bare booking instance under a self-clearance import/export contract
* — ONE_TIME or GENERAL. No form, no date, no window gate: the new instance
* lands in per-booking clearance (AWAITING_DOCUMENTS) with the customer on 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 [confirmOpen, setConfirmOpen] = useState(false);
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.",
);
setConfirmOpen(false);
navigate(`/bookings/${booking.id}`);
},
onError: (e: Error) =>
toast.error(e.message || "Could not initiate the booking"),
});
return (
<>
<Modal
opened={confirmOpen}
onClose={() => {
if (!mutation.isPending) setConfirmOpen(false);
}}
centered
radius="lg"
size="md"
closeOnClickOutside={!mutation.isPending}
closeOnEscape={!mutation.isPending}
withCloseButton={!mutation.isPending}
title={
<Group gap={10} wrap="nowrap">
<ThemeIcon variant="light" color="edr-green" radius="md" size={34}>
<Icon size={18} />
</ThemeIcon>
<Text fw={700}>Initiate a new booking?</Text>
</Group>
}
>
<Text size="sm" c="dimmed">
This creates a new shipment booking under contract{" "}
<Text span fw={700} c="#10202F">
{contract.reference}
</Text>
. You&apos;ll upload the clearance documents next, and the shipment
quantity is drawn down from your contract&apos;s reserved capacity.
</Text>
<Group justify="flex-end" gap="sm" mt="lg">
<Button
variant="default"
radius="md"
onClick={() => setConfirmOpen(false)}
disabled={mutation.isPending}
>
Cancel
</Button>
<Button
color="edr-green"
radius="md"
leftSection={<Icon size={16} />}
loading={mutation.isPending}
onClick={() => mutation.mutate()}
>
Yes, initiate booking
</Button>
</Group>
</Modal>
<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();
setConfirmOpen(true);
}}
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,
bookings,
docButton,
}: {
contract: Freight.IContract;
bookings: Freight.IBooking[];
docButton: ReactNode;
}) {
return (
<Group gap={8} wrap="nowrap" justify="flex-end">
{docButton}
<ContractCustomerAction
contract={contract}
bookings={bookings}
size="sm"
listStyle
/>
</Group>
);
}