mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 21:50:57 +00:00
Merge branch 'dev' into freight/feat/fixes-v1
This commit is contained in:
@@ -241,11 +241,10 @@ export default function ContractDetailPage() {
|
||||
});
|
||||
// Intercity contracts are never window-gated: the shipment rides a passing
|
||||
// import/export train that staff assign later, so booking is always open.
|
||||
// GENERAL contracts are also not gated at creation — the booking enters the
|
||||
// per-booking clearance gate first and picks its shipment day at proceed time.
|
||||
// ONE_TIME and GENERAL contracts are both gated — booking is only possible
|
||||
// while a window on the contract's lane is open.
|
||||
const bookingWindowOpen =
|
||||
contract?.tradeDirection === "DOMESTIC" ||
|
||||
contract?.contractKind === "GENERAL" ||
|
||||
hasOpenWindow(bookingWindows);
|
||||
|
||||
// Draw-down capacity per cargo line (GENERAL contracts only). The backend
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
Modal,
|
||||
Paper,
|
||||
Stack,
|
||||
Switch,
|
||||
Text,
|
||||
TextInput,
|
||||
Textarea,
|
||||
@@ -32,6 +33,7 @@ import {
|
||||
MapPin,
|
||||
Package,
|
||||
Receipt,
|
||||
Repeat,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
|
||||
@@ -144,14 +146,12 @@ export default function NewShipmentPage() {
|
||||
|
||||
// Coarse gate: if the customer deep-links here while no booking window is
|
||||
// open, show the same closed-state notice as the contract page instead of the
|
||||
// form. Still allowed the moment any window isOpenNow. Intercity contracts
|
||||
// are never window-gated — the shipment rides a passing train that staff
|
||||
// pick at finalize time, so booking is always open. GENERAL contracts are not
|
||||
// gated at creation either: the booking enters per-booking clearance first
|
||||
// and picks its shipment day at proceed time.
|
||||
// form. Still allowed the moment any window isOpenNow. Applies to ONE_TIME
|
||||
// and GENERAL alike. Intercity contracts are never window-gated — the
|
||||
// shipment rides a passing train that staff pick at finalize time, so
|
||||
// booking is always open.
|
||||
if (
|
||||
contract.tradeDirection !== "DOMESTIC" &&
|
||||
contract.contractKind !== "GENERAL" &&
|
||||
!hasOpenWindow(bookingWindows)
|
||||
) {
|
||||
return (
|
||||
@@ -230,7 +230,12 @@ function NewShipmentBookingForm({
|
||||
);
|
||||
|
||||
const form = useForm<ShipmentFormInputValues, any, ShipmentFormValues>({
|
||||
defaultValues: initialShipmentFormValues,
|
||||
defaultValues: {
|
||||
...initialShipmentFormValues,
|
||||
// Seed the equipment-return toggle from the contract; the customer can
|
||||
// still flip it per shipment.
|
||||
withReturn: contract.equipmentReturn === "WITH_RETURN",
|
||||
},
|
||||
resolver: zodResolver(
|
||||
createShipmentFormSchema({
|
||||
isContainer: contract.freightType === "CONTAINER",
|
||||
@@ -276,8 +281,10 @@ function NewShipmentBookingForm({
|
||||
...(values.scheduledDate
|
||||
? { scheduledDate: new Date(values.scheduledDate).toISOString() }
|
||||
: {}),
|
||||
// Equipment return is a container concern — bulk keeps the contract default.
|
||||
...(isContainer
|
||||
? {
|
||||
equipmentReturn: values.withReturn ? "WITH_RETURN" : "WITHOUT_RETURN",
|
||||
containers: values.containers
|
||||
.filter((l) => Number(l.quantity) >= 1)
|
||||
.map((l) => ({
|
||||
@@ -405,6 +412,9 @@ function NewShipmentBookingForm({
|
||||
<Stack gap="lg" className="mx-auto max-w-4xl">
|
||||
<RouteStep form={form} contract={contract} routes={routes} />
|
||||
<CargoStep form={form} contract={contract} />
|
||||
{contract.freightType === "CONTAINER" && (
|
||||
<EquipmentReturnStep form={form} />
|
||||
)}
|
||||
<ScheduleStep form={form} contract={contract} routes={routes} />
|
||||
<NotesSection form={form} />
|
||||
</Stack>
|
||||
@@ -1187,6 +1197,78 @@ function CargoStep({
|
||||
);
|
||||
}
|
||||
|
||||
function EquipmentReturnStep({ form }: { form: ShipmentForm }) {
|
||||
return (
|
||||
<StepCard>
|
||||
<StepHeader
|
||||
icon={<Repeat size={22} />}
|
||||
title="Equipment Return"
|
||||
description="Choose whether the empty container(s) come back to EDR after unloading."
|
||||
/>
|
||||
<Controller
|
||||
name="withReturn"
|
||||
control={form.control}
|
||||
render={({ field }) => {
|
||||
const on = field.value ?? false;
|
||||
return (
|
||||
<Paper
|
||||
withBorder
|
||||
radius="md"
|
||||
p="md"
|
||||
style={{
|
||||
borderColor: on ? "#CDEBDD" : "#E6ECF2",
|
||||
background: on ? "#F6FBF8" : "white",
|
||||
cursor: "pointer",
|
||||
transition: "border-color 150ms ease, background 150ms ease",
|
||||
}}
|
||||
onClick={() => field.onChange(!on)}
|
||||
>
|
||||
<Group justify="space-between" wrap="nowrap" gap="md">
|
||||
<Group gap={13} wrap="nowrap" align="flex-start">
|
||||
<Box
|
||||
style={{
|
||||
width: 38,
|
||||
height: 38,
|
||||
flexShrink: 0,
|
||||
borderRadius: 11,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
background: on ? "#ECF6F1" : "#F1F4F7",
|
||||
color: on ? "#0A6F4D" : "#6B7C8E",
|
||||
}}
|
||||
>
|
||||
<Repeat size={18} />
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz={14} fw={700} c="#10202F">
|
||||
With return
|
||||
</Text>
|
||||
<Text fz={12} c="dimmed" style={{ lineHeight: 1.4 }}>
|
||||
{on
|
||||
? "Container(s) returned to EDR after unloading."
|
||||
: "Container(s) retained by you after delivery."}
|
||||
</Text>
|
||||
</Box>
|
||||
</Group>
|
||||
<Switch
|
||||
size="md"
|
||||
color="edr-green"
|
||||
aria-label="With return"
|
||||
checked={on}
|
||||
onChange={(e) => field.onChange(e.currentTarget.checked)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{ flexShrink: 0 }}
|
||||
/>
|
||||
</Group>
|
||||
</Paper>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</StepCard>
|
||||
);
|
||||
}
|
||||
|
||||
function NotesSection({ form }: { form: ShipmentForm }) {
|
||||
return (
|
||||
<StepCard>
|
||||
|
||||
@@ -61,11 +61,16 @@ export default function NewShipmentRequestPage() {
|
||||
|
||||
const isContainer = contract.freightType === "CONTAINER";
|
||||
const route = contract.routes?.[0];
|
||||
// GENERAL customs contracts: GL schedules the shipment during clearance —
|
||||
// the customer only states the quantity, never picks a date.
|
||||
const hasCustoms =
|
||||
contract.contractKind === "GENERAL" &&
|
||||
(contract.serviceType?.includesCustoms ?? contract.customsClearingEnabled);
|
||||
|
||||
const handleSubmit = () => {
|
||||
const dto: Freight.CreateBookingRequestDto = {
|
||||
contractRouteId: route?.id,
|
||||
scheduledDate: scheduledDate || undefined,
|
||||
scheduledDate: hasCustoms ? undefined : scheduledDate || undefined,
|
||||
notes: notes.trim() || undefined,
|
||||
};
|
||||
|
||||
@@ -104,19 +109,26 @@ export default function NewShipmentRequestPage() {
|
||||
|
||||
<Paper withBorder radius="lg" p="xl" style={{ borderColor: BORDER }}>
|
||||
<Stack gap="md">
|
||||
<DatePickerInput
|
||||
label="Preferred shipment date"
|
||||
placeholder="Pick a date"
|
||||
leftSection={<CalendarDays size={16} />}
|
||||
minDate={new Date().toISOString().slice(0, 10)}
|
||||
value={scheduledDate || null}
|
||||
onChange={(v) => setScheduledDate(v ?? "")}
|
||||
radius="md"
|
||||
popoverProps={{ withinPortal: true }}
|
||||
/>
|
||||
{!hasCustoms && (
|
||||
<DatePickerInput
|
||||
label="Preferred shipment date"
|
||||
placeholder="Pick a date"
|
||||
leftSection={<CalendarDays size={16} />}
|
||||
minDate={new Date().toISOString().slice(0, 10)}
|
||||
value={scheduledDate || null}
|
||||
onChange={(v) => setScheduledDate(v ?? "")}
|
||||
radius="md"
|
||||
popoverProps={{ withinPortal: true }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<NumberInput
|
||||
label={isContainer ? "Number of containers" : "Cargo weight (tons)"}
|
||||
description={
|
||||
hasCustoms
|
||||
? "Global Logistics schedules the shipment date during customs clearance — you only state the quantity."
|
||||
: undefined
|
||||
}
|
||||
value={quantity}
|
||||
onChange={setQuantity}
|
||||
min={1}
|
||||
|
||||
@@ -58,6 +58,9 @@ const containerLineSchema = z.object({
|
||||
const shipmentFormBase = z.object({
|
||||
contractRouteId: z.string().default(""),
|
||||
scheduledDate: z.string().default(""),
|
||||
// Container contracts only: return the empty container(s) to EDR after
|
||||
// unloading. Seeded from the contract's equipment return; bulk ignores it.
|
||||
withReturn: z.boolean().default(false),
|
||||
containers: z.array(containerLineSchema).default([]),
|
||||
cargoWeightTons: z.string().default(""),
|
||||
itemCount: z.string().default(""),
|
||||
@@ -210,6 +213,7 @@ export type ShipmentFormInputValues = z.input<typeof shipmentFormSchema>;
|
||||
export const initialShipmentFormValues: DeepPartial<ShipmentFormValues> = {
|
||||
contractRouteId: "",
|
||||
scheduledDate: "",
|
||||
withReturn: false,
|
||||
containers: [],
|
||||
cargoWeightTons: "",
|
||||
itemCount: "",
|
||||
@@ -229,6 +233,7 @@ export const shipmentStepFields: Record<
|
||||
"itemCount",
|
||||
"bulkHazardousQuantity",
|
||||
"bulkReeferQuantity",
|
||||
"withReturn",
|
||||
],
|
||||
2: ["scheduledDate"],
|
||||
3: ["notes"],
|
||||
|
||||
Reference in New Issue
Block a user