mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 13:28:11 +00:00
Merge branch 'freight_feature/contrat' of github.com:Tria-plc/edr-platform into freight_feature/contrat
This commit is contained in:
@@ -79,11 +79,11 @@ export default function ContractClearanceDetailPage() {
|
|||||||
<PageContainer>
|
<PageContainer>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="Clearance not found"
|
title="Clearance not found"
|
||||||
backTo="/dashboard/clearance"
|
backTo="/dashboard/contracts/clearance"
|
||||||
breadcrumbs={[
|
breadcrumbs={[
|
||||||
{
|
{
|
||||||
label: "Document Clearance",
|
label: "Document Clearance",
|
||||||
href: "/dashboard/clearance",
|
href: "/dashboard/contracts/clearance",
|
||||||
},
|
},
|
||||||
{ label: "Not found" },
|
{ label: "Not found" },
|
||||||
]}
|
]}
|
||||||
@@ -100,11 +100,11 @@ export default function ContractClearanceDetailPage() {
|
|||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={reference}
|
title={reference}
|
||||||
backTo="/dashboard/clearance"
|
backTo="/dashboard/contracts/clearance"
|
||||||
breadcrumbs={[
|
breadcrumbs={[
|
||||||
{
|
{
|
||||||
label: "Document Clearance",
|
label: "Document Clearance",
|
||||||
href: "/dashboard/clearance",
|
href: "/dashboard/contracts/clearance",
|
||||||
},
|
},
|
||||||
{ label: reference },
|
{ label: reference },
|
||||||
]}
|
]}
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ export default function ContractClearanceListPage() {
|
|||||||
}, [rows, pagination.pageIndex, pagination.pageSize]);
|
}, [rows, pagination.pageIndex, pagination.pageSize]);
|
||||||
|
|
||||||
const openDetail = useCallback(
|
const openDetail = useCallback(
|
||||||
(id: string) => navigate(`/dashboard/clearance/${id}`),
|
(id: string) => navigate(`/dashboard/contracts/clearance/${id}`),
|
||||||
[navigate],
|
[navigate],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -8,23 +8,27 @@ import {
|
|||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Center,
|
Center,
|
||||||
|
Divider,
|
||||||
Group,
|
Group,
|
||||||
Loader,
|
Loader,
|
||||||
|
Modal,
|
||||||
Paper,
|
Paper,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Textarea,
|
Textarea,
|
||||||
|
ThemeIcon,
|
||||||
Title,
|
Title,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import {
|
import {
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
CalendarDays,
|
CalendarDays,
|
||||||
|
CheckCircle2,
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
|
||||||
MapPin,
|
MapPin,
|
||||||
Package,
|
Package,
|
||||||
Send,
|
Receipt,
|
||||||
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
import type { Freight } from "@edr/types";
|
import type { Freight } from "@edr/types";
|
||||||
@@ -37,15 +41,12 @@ import {
|
|||||||
StepLabel,
|
StepLabel,
|
||||||
fieldStyles,
|
fieldStyles,
|
||||||
} from "./new-contract-form/shared";
|
} from "./new-contract-form/shared";
|
||||||
import { StepIndicator } from "./new-contract-form/StepIndicator";
|
|
||||||
import { formatRateUnit } from "./new-contract-form/unit-rates";
|
import { formatRateUnit } from "./new-contract-form/unit-rates";
|
||||||
import {
|
import {
|
||||||
SHIPMENT_STEPS,
|
|
||||||
ShipmentFormInputValues,
|
ShipmentFormInputValues,
|
||||||
ShipmentFormValues,
|
ShipmentFormValues,
|
||||||
initialShipmentFormValues,
|
initialShipmentFormValues,
|
||||||
shipmentFormSchema,
|
shipmentFormSchema,
|
||||||
shipmentStepFields,
|
|
||||||
} from "./new-shipment-form/schema";
|
} from "./new-shipment-form/schema";
|
||||||
import { computeShipmentTotal } from "./new-shipment-form/total";
|
import { computeShipmentTotal } from "./new-shipment-form/total";
|
||||||
import { ContractCapacityNotice } from "./new-shipment-form/ContractCapacityNotice";
|
import { ContractCapacityNotice } from "./new-shipment-form/ContractCapacityNotice";
|
||||||
@@ -58,7 +59,13 @@ export default function NewShipmentPage() {
|
|||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [step, setStep] = useState(0);
|
|
||||||
|
// Holds the validated values awaiting price confirmation. When set, the price
|
||||||
|
// modal is open. The customer confirms (books) or rejects (back to the form
|
||||||
|
// to edit and re-book).
|
||||||
|
const [pendingValues, setPendingValues] = useState<ShipmentFormValues | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
const { data: contract, isLoading } = useQuery(
|
const { data: contract, isLoading } = useQuery(
|
||||||
api.contracts.get.queryOptions({ input: { id: id! }, enabled: !!id }),
|
api.contracts.get.queryOptions({ input: { id: id! }, enabled: !!id }),
|
||||||
@@ -82,26 +89,6 @@ export default function NewShipmentPage() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const visibleStepIds = useMemo(() => SHIPMENT_STEPS.map((s) => s.id), []);
|
|
||||||
const currentStepIndex = visibleStepIds.indexOf(step);
|
|
||||||
const isLastStep = currentStepIndex === visibleStepIds.length - 1;
|
|
||||||
const isFirstStep = currentStepIndex <= 0;
|
|
||||||
const goToStep = (delta: number) => {
|
|
||||||
const idx = visibleStepIds.indexOf(step);
|
|
||||||
const nextIdx = Math.min(
|
|
||||||
visibleStepIds.length - 1,
|
|
||||||
Math.max(0, idx + delta),
|
|
||||||
);
|
|
||||||
setStep(visibleStepIds[nextIdx]);
|
|
||||||
};
|
|
||||||
|
|
||||||
async function handleContinue() {
|
|
||||||
const valid = await form.trigger(shipmentStepFields[step], {
|
|
||||||
shouldFocus: true,
|
|
||||||
});
|
|
||||||
if (valid) goToStep(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<Center mih={400} p="xl">
|
<Center mih={400} p="xl">
|
||||||
@@ -149,7 +136,9 @@ export default function NewShipmentPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildDto(values: ShipmentFormValues): Freight.CreateBookingUnderContractDto {
|
function buildDto(
|
||||||
|
values: ShipmentFormValues,
|
||||||
|
): Freight.CreateBookingUnderContractDto {
|
||||||
const isContainer = contract!.freightType === "CONTAINER";
|
const isContainer = contract!.freightType === "CONTAINER";
|
||||||
return {
|
return {
|
||||||
...(values.contractRouteId
|
...(values.contractRouteId
|
||||||
@@ -191,10 +180,22 @@ export default function NewShipmentPage() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSubmit = form.handleSubmit((values) => {
|
// Submit validates the whole form, then opens the price modal for confirmation.
|
||||||
submitMutation.mutate(buildDto(values));
|
const handleReview = form.handleSubmit((values) => {
|
||||||
|
setPendingValues(values);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
if (!pendingValues) return;
|
||||||
|
submitMutation.mutate(buildDto(pendingValues));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reject — close the modal and let the customer edit and re-book.
|
||||||
|
const handleReject = () => {
|
||||||
|
if (submitMutation.isPending) return;
|
||||||
|
setPendingValues(null);
|
||||||
|
};
|
||||||
|
|
||||||
const routes = contract.routes ?? [];
|
const routes = contract.routes ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -206,7 +207,14 @@ export default function NewShipmentPage() {
|
|||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Group justify="space-between" px="24px" align="flex-end" wrap="wrap" gap="md" mb="lg">
|
<Group
|
||||||
|
justify="space-between"
|
||||||
|
px="24px"
|
||||||
|
align="flex-end"
|
||||||
|
wrap="wrap"
|
||||||
|
gap="md"
|
||||||
|
mb="lg"
|
||||||
|
>
|
||||||
<Box>
|
<Box>
|
||||||
<Title order={1} fw={800} fz={26} style={{ letterSpacing: "-0.01em" }}>
|
<Title order={1} fw={800} fz={26} style={{ letterSpacing: "-0.01em" }}>
|
||||||
New Shipment Booking
|
New Shipment Booking
|
||||||
@@ -231,12 +239,13 @@ export default function NewShipmentPage() {
|
|||||||
onSubmit={(e) => e.preventDefault()}
|
onSubmit={(e) => e.preventDefault()}
|
||||||
>
|
>
|
||||||
<Box flex={1} p="24px">
|
<Box flex={1} p="24px">
|
||||||
<Box mb="lg">
|
|
||||||
<StepIndicator step={step} steps={SHIPMENT_STEPS} />
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{submitMutation.isError && (
|
{submitMutation.isError && (
|
||||||
<Alert color="red" icon={<AlertCircle size={16} />} radius="md" mb="lg">
|
<Alert
|
||||||
|
color="red"
|
||||||
|
icon={<AlertCircle size={16} />}
|
||||||
|
radius="md"
|
||||||
|
mb="lg"
|
||||||
|
>
|
||||||
<Text size="sm" fw={600}>
|
<Text size="sm" fw={600}>
|
||||||
Failed to create the shipment booking
|
Failed to create the shipment booking
|
||||||
</Text>
|
</Text>
|
||||||
@@ -248,14 +257,13 @@ export default function NewShipmentPage() {
|
|||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{step === 0 && (
|
{/* Single-step form — all sections on one page. */}
|
||||||
|
<Stack gap="lg" className="mx-auto max-w-4xl">
|
||||||
<RouteStep form={form} contract={contract} routes={routes} />
|
<RouteStep form={form} contract={contract} routes={routes} />
|
||||||
)}
|
|
||||||
{step === 1 && (
|
|
||||||
<ScheduleStep form={form} contract={contract} routes={routes} />
|
<ScheduleStep form={form} contract={contract} routes={routes} />
|
||||||
)}
|
<CargoStep form={form} contract={contract} />
|
||||||
{step === 2 && <CargoStep form={form} contract={contract} />}
|
<NotesSection form={form} />
|
||||||
{step === 3 && <ReviewStep form={form} contract={contract} />}
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
@@ -270,46 +278,153 @@ export default function NewShipmentPage() {
|
|||||||
marginTop: "auto",
|
marginTop: "auto",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Group justify="space-between" className="mx-auto max-w-4xl">
|
<Group justify="flex-end" className="mx-auto max-w-4xl">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="default"
|
color="edr-green"
|
||||||
radius="md"
|
radius="md"
|
||||||
leftSection={<ChevronLeft size={16} />}
|
leftSection={<Receipt size={16} />}
|
||||||
onClick={() => goToStep(-1)}
|
onClick={handleReview}
|
||||||
disabled={isFirstStep}
|
|
||||||
>
|
>
|
||||||
Back
|
Review price & book
|
||||||
</Button>
|
</Button>
|
||||||
{!isLastStep ? (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
color="edr-green"
|
|
||||||
radius="md"
|
|
||||||
rightSection={<ChevronRight size={16} />}
|
|
||||||
onClick={handleContinue}
|
|
||||||
>
|
|
||||||
Continue
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
color="edr-green"
|
|
||||||
radius="md"
|
|
||||||
leftSection={<Send size={16} />}
|
|
||||||
onClick={handleSubmit}
|
|
||||||
loading={submitMutation.isPending}
|
|
||||||
>
|
|
||||||
Submit booking
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Group>
|
</Group>
|
||||||
</Box>
|
</Box>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<PriceConfirmModal
|
||||||
|
contract={contract}
|
||||||
|
values={pendingValues}
|
||||||
|
loading={submitMutation.isPending}
|
||||||
|
onConfirm={handleConfirm}
|
||||||
|
onReject={handleReject}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function PriceConfirmModal({
|
||||||
|
contract,
|
||||||
|
values,
|
||||||
|
loading,
|
||||||
|
onConfirm,
|
||||||
|
onReject,
|
||||||
|
}: {
|
||||||
|
contract: Freight.IContract;
|
||||||
|
values: ShipmentFormValues | null;
|
||||||
|
loading: boolean;
|
||||||
|
onConfirm: () => void;
|
||||||
|
onReject: () => void;
|
||||||
|
}) {
|
||||||
|
const total = useMemo(
|
||||||
|
() => (values ? computeShipmentTotal(contract, values) : null),
|
||||||
|
[contract, values],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
opened={Boolean(values)}
|
||||||
|
onClose={onReject}
|
||||||
|
closeOnClickOutside={!loading}
|
||||||
|
closeOnEscape={!loading}
|
||||||
|
withCloseButton={!loading}
|
||||||
|
centered
|
||||||
|
radius="lg"
|
||||||
|
size="lg"
|
||||||
|
title={
|
||||||
|
<Group gap={10}>
|
||||||
|
<ThemeIcon variant="light" color="edr-green" radius="md" size={34}>
|
||||||
|
<Receipt size={18} />
|
||||||
|
</ThemeIcon>
|
||||||
|
<Box>
|
||||||
|
<Text fw={800} fz={16} c="#10202F">
|
||||||
|
Confirm shipment price
|
||||||
|
</Text>
|
||||||
|
<Text fz="xs" c="dimmed">
|
||||||
|
Review the total before booking this shipment.
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Group>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{total ? (
|
||||||
|
<Stack gap="md">
|
||||||
|
<Paper withBorder radius={16} p="lg" style={{ borderColor: "#E6ECF2" }}>
|
||||||
|
<Stack gap={10}>
|
||||||
|
{total.lines.map((line, i) => (
|
||||||
|
<Group key={i} justify="space-between" wrap="nowrap" gap="sm">
|
||||||
|
<Box style={{ minWidth: 0 }}>
|
||||||
|
<Text fz="sm" c="#10202F" fw={500}>
|
||||||
|
{line.label}
|
||||||
|
</Text>
|
||||||
|
<Text fz="xs" c="dimmed">
|
||||||
|
{line.quantity.toLocaleString()} ×{" "}
|
||||||
|
{line.unitPrice.toLocaleString()} {total.currency} ·{" "}
|
||||||
|
{formatRateUnit(line.unit)}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Text
|
||||||
|
fz="sm"
|
||||||
|
fw={600}
|
||||||
|
c="#10202F"
|
||||||
|
style={{ whiteSpace: "nowrap" }}
|
||||||
|
>
|
||||||
|
{line.amount.toLocaleString()} {total.currency}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
))}
|
||||||
|
{total.lines.length === 0 && (
|
||||||
|
<Text fz="sm" c="dimmed">
|
||||||
|
No priced lines — check the cargo details.
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
<Divider my="md" />
|
||||||
|
<Group justify="space-between" align="flex-end">
|
||||||
|
<Text
|
||||||
|
fz="xs"
|
||||||
|
fw={700}
|
||||||
|
tt="uppercase"
|
||||||
|
c="edr-green"
|
||||||
|
style={{ letterSpacing: "0.06em" }}
|
||||||
|
>
|
||||||
|
Total
|
||||||
|
</Text>
|
||||||
|
<Text fw={800} fz={28} c="#10202F">
|
||||||
|
{total.total.toLocaleString()}{" "}
|
||||||
|
<Text span fz={16} fw={700} c="edr-muted">
|
||||||
|
{total.currency}
|
||||||
|
</Text>
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
<Group justify="space-between" mt="xs">
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
radius="md"
|
||||||
|
leftSection={<X size={16} />}
|
||||||
|
onClick={onReject}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
Reject & edit
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="edr-green"
|
||||||
|
radius="md"
|
||||||
|
leftSection={<CheckCircle2 size={16} />}
|
||||||
|
onClick={onConfirm}
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
|
Confirm & book
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
) : null}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function RouteStep({
|
function RouteStep({
|
||||||
form,
|
form,
|
||||||
contract,
|
contract,
|
||||||
@@ -374,8 +489,7 @@ function ScheduleStep({
|
|||||||
routes: Freight.IContractRoute[];
|
routes: Freight.IContractRoute[];
|
||||||
}) {
|
}) {
|
||||||
const contractRouteId = form.watch("contractRouteId");
|
const contractRouteId = form.watch("contractRouteId");
|
||||||
const route =
|
const route = routes.find((r) => r.id === contractRouteId) ?? routes[0];
|
||||||
routes.find((r) => r.id === contractRouteId) ?? routes[0];
|
|
||||||
return (
|
return (
|
||||||
<StepCard>
|
<StepCard>
|
||||||
<StepHeader
|
<StepHeader
|
||||||
@@ -541,12 +655,31 @@ function CargoStep({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{bulkUnitIsItem && null}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</StepCard>
|
</StepCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function NotesSection({ form }: { form: ShipmentForm }) {
|
||||||
|
return (
|
||||||
|
<StepCard>
|
||||||
|
<Controller
|
||||||
|
name="notes"
|
||||||
|
control={form.control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Textarea
|
||||||
|
{...field}
|
||||||
|
label="Additional notes"
|
||||||
|
placeholder="Any special instructions for this shipment…"
|
||||||
|
rows={3}
|
||||||
|
radius="md"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</StepCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function ContainerLineEditor({
|
function ContainerLineEditor({
|
||||||
form,
|
form,
|
||||||
index,
|
index,
|
||||||
@@ -638,143 +771,55 @@ function ContainerLineEditor({
|
|||||||
|
|
||||||
<StepLabel>Per-container details</StepLabel>
|
<StepLabel>Per-container details</StepLabel>
|
||||||
<Stack gap={10} mt={8}>
|
<Stack gap={10} mt={8}>
|
||||||
{Array.from({ length: Math.max(quantity, units.length) }).map(
|
{Array.from({ length: Math.max(quantity, units.length) }).map((_, u) => (
|
||||||
(_, u) => (
|
<Group key={u} gap={10} grow align="flex-start">
|
||||||
<Group key={u} gap={10} grow align="flex-start">
|
<Controller
|
||||||
<Controller
|
name={`containers.${index}.units.${u}.containerNumber`}
|
||||||
name={`containers.${index}.units.${u}.containerNumber`}
|
control={form.control}
|
||||||
control={form.control}
|
render={({ field, fieldState }) => (
|
||||||
render={({ field, fieldState }) => (
|
<TextInput
|
||||||
<TextInput
|
{...field}
|
||||||
{...field}
|
label={u === 0 ? "Container number *" : undefined}
|
||||||
label={u === 0 ? "Container number *" : undefined}
|
placeholder="e.g. MSCU1234567"
|
||||||
placeholder="e.g. MSCU1234567"
|
error={fieldState.error?.message}
|
||||||
error={fieldState.error?.message}
|
radius={10}
|
||||||
radius={10}
|
styles={fieldStyles}
|
||||||
styles={fieldStyles}
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
/>
|
||||||
/>
|
<Controller
|
||||||
<Controller
|
name={`containers.${index}.units.${u}.sealNumber`}
|
||||||
name={`containers.${index}.units.${u}.sealNumber`}
|
control={form.control}
|
||||||
control={form.control}
|
render={({ field }) => (
|
||||||
render={({ field }) => (
|
<TextInput
|
||||||
<TextInput
|
{...field}
|
||||||
{...field}
|
label={u === 0 ? "Seal number" : undefined}
|
||||||
label={u === 0 ? "Seal number" : undefined}
|
placeholder="Optional"
|
||||||
placeholder="Optional"
|
radius={10}
|
||||||
radius={10}
|
styles={fieldStyles}
|
||||||
styles={fieldStyles}
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
/>
|
||||||
/>
|
<Controller
|
||||||
<Controller
|
name={`containers.${index}.units.${u}.vgmTons`}
|
||||||
name={`containers.${index}.units.${u}.vgmTons`}
|
control={form.control}
|
||||||
control={form.control}
|
render={({ field, fieldState }) => (
|
||||||
render={({ field, fieldState }) => (
|
<TextInput
|
||||||
<TextInput
|
{...field}
|
||||||
{...field}
|
type="number"
|
||||||
type="number"
|
label={u === 0 ? "VGM (tons) *" : undefined}
|
||||||
label={u === 0 ? "VGM (tons) *" : undefined}
|
placeholder="e.g. 24.5"
|
||||||
placeholder="e.g. 24.5"
|
min={0}
|
||||||
min={0}
|
step={0.01}
|
||||||
step={0.01}
|
error={fieldState.error?.message}
|
||||||
error={fieldState.error?.message}
|
radius={10}
|
||||||
radius={10}
|
styles={fieldStyles}
|
||||||
styles={fieldStyles}
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
/>
|
||||||
/>
|
</Group>
|
||||||
</Group>
|
))}
|
||||||
),
|
|
||||||
)}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ReviewStep({
|
|
||||||
form,
|
|
||||||
contract,
|
|
||||||
}: {
|
|
||||||
form: ShipmentForm;
|
|
||||||
contract: Freight.IContract;
|
|
||||||
}) {
|
|
||||||
const values = form.watch() as ShipmentFormValues;
|
|
||||||
const total = useMemo(
|
|
||||||
() => computeShipmentTotal(contract, values),
|
|
||||||
[contract, values],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack gap="lg">
|
|
||||||
<StepHeader
|
|
||||||
icon={<Package size={22} />}
|
|
||||||
title="Review & Submit"
|
|
||||||
description="Review your shipment and the computed total before submitting."
|
|
||||||
/>
|
|
||||||
<Paper withBorder radius={20} p="lg" style={{ borderColor: "#E6ECF2" }}>
|
|
||||||
<Text fz={14} fw={800} c="#10202F" mb="sm">
|
|
||||||
Estimated total
|
|
||||||
</Text>
|
|
||||||
<Stack gap={10}>
|
|
||||||
{total.lines.map((line, i) => (
|
|
||||||
<Group key={i} justify="space-between" wrap="nowrap" gap="sm">
|
|
||||||
<Box style={{ minWidth: 0 }}>
|
|
||||||
<Text fz="sm" c="#10202F" fw={500}>
|
|
||||||
{line.label}
|
|
||||||
</Text>
|
|
||||||
<Text fz="xs" c="dimmed">
|
|
||||||
{line.quantity.toLocaleString()} ×{" "}
|
|
||||||
{line.unitPrice.toLocaleString()} {total.currency} ·{" "}
|
|
||||||
{formatRateUnit(line.unit)}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
<Text fz="sm" fw={600} c="#10202F" style={{ whiteSpace: "nowrap" }}>
|
|
||||||
{line.amount.toLocaleString()} {total.currency}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
))}
|
|
||||||
{total.lines.length === 0 && (
|
|
||||||
<Text fz="sm" c="dimmed">
|
|
||||||
Enter cargo details to see the computed total.
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
<Box
|
|
||||||
mt="md"
|
|
||||||
p="md"
|
|
||||||
style={{
|
|
||||||
borderRadius: 14,
|
|
||||||
background: "linear-gradient(135deg, #F4FBF7 0%, #FFFFFF 70%)",
|
|
||||||
border: "1px solid #E6ECF2",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text fz="xs" fw={700} tt="uppercase" c="edr-green" style={{ letterSpacing: "0.06em" }}>
|
|
||||||
Total
|
|
||||||
</Text>
|
|
||||||
<Text fw={800} fz={28} c="#10202F" mt={4}>
|
|
||||||
{total.total.toLocaleString()}{" "}
|
|
||||||
<Text span fz={16} fw={700} c="edr-muted">
|
|
||||||
{total.currency}
|
|
||||||
</Text>
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
</Paper>
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
name="notes"
|
|
||||||
control={form.control}
|
|
||||||
render={({ field }) => (
|
|
||||||
<Textarea
|
|
||||||
{...field}
|
|
||||||
label="Additional notes"
|
|
||||||
placeholder="Any special instructions for this shipment…"
|
|
||||||
rows={3}
|
|
||||||
radius="md"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user