mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 12:41:04 +00:00
- 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.
250 lines
6.6 KiB
TypeScript
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'll upload the clearance documents next, and the shipment
|
|
quantity is drawn down from your contract'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>
|
|
);
|
|
}
|