mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 06:28:12 +00:00
refactor(workflow): Revamp booking progress stages and status mapping
This commit is contained in:
@@ -488,7 +488,7 @@ export default function MyPortalPage() {
|
|||||||
icon={Clock3}
|
icon={Clock3}
|
||||||
label="Awaiting Payment"
|
label="Awaiting Payment"
|
||||||
value={outstandingInvoices.length.toString()}
|
value={outstandingInvoices.length.toString()}
|
||||||
delta={`${formatCurrency(totalOutstanding || 377500, "ETB")} due`}
|
delta={`${formatCurrency(totalOutstanding || 0, "ETB")} due`}
|
||||||
deltaColor="edr-amber-text"
|
deltaColor="edr-amber-text"
|
||||||
divider
|
divider
|
||||||
/>
|
/>
|
||||||
@@ -506,9 +506,9 @@ export default function MyPortalPage() {
|
|||||||
value={
|
value={
|
||||||
dashboard
|
dashboard
|
||||||
? formatCurrency(
|
? formatCurrency(
|
||||||
dashboard.spendYtd,
|
dashboard.spendYtd,
|
||||||
dashboard.spendCurrency as Currency,
|
dashboard.spendCurrency as Currency,
|
||||||
)
|
)
|
||||||
: "—"
|
: "—"
|
||||||
}
|
}
|
||||||
delta={
|
delta={
|
||||||
@@ -693,7 +693,9 @@ export default function MyPortalPage() {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Text fz={26} fw={800} c="edr-text">
|
<Text fz={26} fw={800} c="edr-text">
|
||||||
{(dashboard?.freightVolume.totalTonnes ?? 0).toLocaleString()}{" "}
|
{(
|
||||||
|
dashboard?.freightVolume.totalTonnes ?? 0
|
||||||
|
).toLocaleString()}{" "}
|
||||||
t
|
t
|
||||||
</Text>
|
</Text>
|
||||||
<Text fz={13} c="edr-muted">
|
<Text fz={13} c="edr-muted">
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export function StatusHero({
|
|||||||
booking: Freight.IBooking;
|
booking: Freight.IBooking;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const status = booking.status as string;
|
const status = booking.status;
|
||||||
const cfg = STATUS_MAP[status] ?? STATUS_MAP.DRAFT;
|
const cfg = STATUS_MAP[status] ?? STATUS_MAP.DRAFT;
|
||||||
const negative = isNegative(status);
|
const negative = isNegative(status);
|
||||||
const draft = isDraftLike(status);
|
const draft = isDraftLike(status);
|
||||||
@@ -45,7 +45,12 @@ export function StatusHero({
|
|||||||
<Group gap={16} align="center" wrap="nowrap">
|
<Group gap={16} align="center" wrap="nowrap">
|
||||||
<div
|
<div
|
||||||
className="flex items-center justify-center rounded-2xl shrink-0"
|
className="flex items-center justify-center rounded-2xl shrink-0"
|
||||||
style={{ width: 56, height: 56, backgroundColor: tileBg, color: tileFg }}
|
style={{
|
||||||
|
width: 56,
|
||||||
|
height: 56,
|
||||||
|
backgroundColor: tileBg,
|
||||||
|
color: tileFg,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<HeroIcon size={26} />
|
<HeroIcon size={26} />
|
||||||
</div>
|
</div>
|
||||||
@@ -77,7 +82,7 @@ export function StatusHero({
|
|||||||
>
|
>
|
||||||
{chipLabel}
|
{chipLabel}
|
||||||
</Text>
|
</Text>
|
||||||
<Text fz="13.5px" fw={700} c="#10202F">
|
<Text fz="sm" fw={700} c="#10202F">
|
||||||
{chipValue}
|
{chipValue}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -114,8 +119,13 @@ function ProgressTracker({
|
|||||||
return (
|
return (
|
||||||
/* Scrollable on mobile so 5 stages never overflow */
|
/* Scrollable on mobile so 5 stages never overflow */
|
||||||
<Box
|
<Box
|
||||||
className="overflow-x-auto"
|
className="overflow-x-auto pt-2"
|
||||||
style={{ scrollbarWidth: "none", WebkitOverflowScrolling: "touch" } as React.CSSProperties}
|
style={
|
||||||
|
{
|
||||||
|
scrollbarWidth: "none",
|
||||||
|
WebkitOverflowScrolling: "touch",
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div className="flex items-start" style={{ minWidth: 440 }}>
|
<div className="flex items-start" style={{ minWidth: 440 }}>
|
||||||
{PROGRESS_STAGES.map((stage, idx) => {
|
{PROGRESS_STAGES.map((stage, idx) => {
|
||||||
@@ -131,14 +141,15 @@ function ProgressTracker({
|
|||||||
: state === "active"
|
: state === "active"
|
||||||
? activeFill
|
? activeFill
|
||||||
: "#0EA371";
|
: "#0EA371";
|
||||||
const circleBorder = state === "idle" ? "1px solid #E1E7EE" : undefined;
|
const circleBorder =
|
||||||
|
state === "idle" ? "1px solid #E1E7EE" : undefined;
|
||||||
const circleShadow =
|
const circleShadow =
|
||||||
state === "active" ? `0 0 0 4px ${activeRing}` : undefined;
|
state === "active" ? `0 0 0 4px ${activeRing}` : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={stage.label}
|
key={stage.label}
|
||||||
className="flex flex-1 flex-col items-center gap-[10px]"
|
className="flex flex-1 flex-col items-center"
|
||||||
>
|
>
|
||||||
<div className="flex w-full items-center">
|
<div className="flex w-full items-center">
|
||||||
{/* left connector */}
|
{/* left connector */}
|
||||||
@@ -147,15 +158,19 @@ function ProgressTracker({
|
|||||||
style={{
|
style={{
|
||||||
height: 3,
|
height: 3,
|
||||||
background:
|
background:
|
||||||
idx === 0 ? "transparent" : reachedLeft ? "#0EA371" : "#E1E7EE",
|
idx === 0
|
||||||
|
? "transparent"
|
||||||
|
: reachedLeft
|
||||||
|
? "#0EA371"
|
||||||
|
: "#E1E7EE",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/* stage circle */}
|
{/* stage circle */}
|
||||||
<div
|
<div
|
||||||
className="flex items-center justify-center rounded-full shrink-0"
|
className="flex items-center justify-center mb-2 rounded-full shrink-0"
|
||||||
style={{
|
style={{
|
||||||
width: 40,
|
width: 32,
|
||||||
height: 40,
|
height: 32,
|
||||||
backgroundColor: circleBg,
|
backgroundColor: circleBg,
|
||||||
border: circleBorder,
|
border: circleBorder,
|
||||||
boxShadow: circleShadow,
|
boxShadow: circleShadow,
|
||||||
@@ -173,32 +188,22 @@ function ProgressTracker({
|
|||||||
style={{
|
style={{
|
||||||
height: 3,
|
height: 3,
|
||||||
background:
|
background:
|
||||||
idx === last ? "transparent" : reachedRight ? "#0EA371" : "#E1E7EE",
|
idx === last
|
||||||
|
? "transparent"
|
||||||
|
: reachedRight
|
||||||
|
? "#0EA371"
|
||||||
|
: "#E1E7EE",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Text
|
<Text
|
||||||
fz="13.5px"
|
fz="14px"
|
||||||
fw={state === "active" ? 800 : 700}
|
fw={700}
|
||||||
ta="center"
|
ta="center"
|
||||||
c={state === "idle" ? "#9AA8B5" : "#10202F"}
|
c={state === "idle" ? "#9AA8B5" : "#10202F"}
|
||||||
>
|
>
|
||||||
{stage.label}
|
{stage.label}
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
|
||||||
fz="11.5px"
|
|
||||||
fw={state === "active" ? 700 : 500}
|
|
||||||
ta="center"
|
|
||||||
c={state === "active" ? activeSub : "#9AA8B5"}
|
|
||||||
>
|
|
||||||
{state === "done"
|
|
||||||
? "Completed"
|
|
||||||
: state === "active"
|
|
||||||
? negative
|
|
||||||
? "Stopped"
|
|
||||||
: "In progress"
|
|
||||||
: "Pending"}
|
|
||||||
</Text>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Box, Button, Group, Stack, Text } from "@mantine/core";
|
import { Box, Group, Stack, Text } from "@mantine/core";
|
||||||
import { CheckCircle2, Clock, FileText } from "lucide-react";
|
import { CheckCircle2, Clock } from "lucide-react";
|
||||||
|
|
||||||
import type { Freight } from "@edr/types";
|
import type { Freight } from "@edr/types";
|
||||||
|
|
||||||
@@ -173,16 +173,16 @@ export function PaymentCard({
|
|||||||
</Group>
|
</Group>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Button
|
{/* <Button */}
|
||||||
fullWidth
|
{/* fullWidth */}
|
||||||
mt={16}
|
{/* mt={16} */}
|
||||||
variant="default"
|
{/* variant="default" */}
|
||||||
radius={10}
|
{/* radius={10} */}
|
||||||
leftSection={<FileText size={17} color="#475569" />}
|
{/* leftSection={<FileText size={17} color="#475569" />} */}
|
||||||
styles={{ root: { height: 46 }, label: { fontSize: 13.5, fontWeight: 700, color: "#10202F" } }}
|
{/* styles={{ root: { height: 46 }, label: { fontSize: 13.5, fontWeight: 700, color: "#10202F" } }} */}
|
||||||
>
|
{/* > */}
|
||||||
Download invoice
|
{/* Download invoice */}
|
||||||
</Button>
|
{/* </Button> */}
|
||||||
</SectionCard>
|
</SectionCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
FileText,
|
FileText,
|
||||||
PackageCheck,
|
PackageCheck,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
|
Ship,
|
||||||
Train,
|
Train,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
@@ -15,32 +16,41 @@ export const PROGRESS_STAGES = [
|
|||||||
{
|
{
|
||||||
label: "Submitted",
|
label: "Submitted",
|
||||||
icon: ClipboardCheck,
|
icon: ClipboardCheck,
|
||||||
statuses: ["SUBMITTED", "PENDING_APPROVAL"],
|
statuses: ["SUBMITTED"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Approved",
|
label: "Approval",
|
||||||
|
icon: ShieldCheck,
|
||||||
|
statuses: ["PENDING_APPROVAL", "APPROVED_PENDING_SIGNATURE", "APPROVED"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Contract",
|
||||||
|
icon: ShieldCheck,
|
||||||
|
statuses: ["CONTRACT_READY", "SIGNED_CUSTOMER"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Payment",
|
||||||
icon: ShieldCheck,
|
icon: ShieldCheck,
|
||||||
statuses: [
|
statuses: [
|
||||||
"APPROVED_PENDING_SIGNATURE",
|
|
||||||
"APPROVED",
|
|
||||||
"CONTRACT_READY",
|
|
||||||
"SIGNED_CUSTOMER",
|
|
||||||
"FULLY_EXECUTED",
|
"FULLY_EXECUTED",
|
||||||
|
"SELECTED_FOR_BATCH",
|
||||||
|
"PAYMENT_VERIFICATION_IN_PROGRESS",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Loading",
|
||||||
|
icon: Ship,
|
||||||
|
statuses: [
|
||||||
|
"PAID",
|
||||||
|
"PNR_GENERATED",
|
||||||
|
"PENDING_CONSOLIDATION",
|
||||||
|
"CONSOLIDATED",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "In Transit",
|
label: "In Transit",
|
||||||
icon: Train,
|
icon: Train,
|
||||||
statuses: [
|
statuses: ["EXPIRED", "IN_TRANSIT"],
|
||||||
"SELECTED_FOR_BATCH",
|
|
||||||
"EXPIRED",
|
|
||||||
"PNR_GENERATED",
|
|
||||||
"PAYMENT_VERIFICATION_IN_PROGRESS",
|
|
||||||
"PAID",
|
|
||||||
"IN_TRANSIT",
|
|
||||||
"PENDING_CONSOLIDATION",
|
|
||||||
"CONSOLIDATED",
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Complete",
|
label: "Complete",
|
||||||
@@ -72,7 +82,7 @@ export const STATUS_MAP: Record<
|
|||||||
PENDING_APPROVAL: {
|
PENDING_APPROVAL: {
|
||||||
title: "Pending approval",
|
title: "Pending approval",
|
||||||
description: "Your booking is moving through the approval process.",
|
description: "Your booking is moving through the approval process.",
|
||||||
stage: 1,
|
stage: 2,
|
||||||
},
|
},
|
||||||
APPROVED_PENDING_SIGNATURE: {
|
APPROVED_PENDING_SIGNATURE: {
|
||||||
title: "Approved — awaiting signature",
|
title: "Approved — awaiting signature",
|
||||||
@@ -88,71 +98,71 @@ export const STATUS_MAP: Record<
|
|||||||
title: "Contract ready to sign",
|
title: "Contract ready to sign",
|
||||||
description:
|
description:
|
||||||
"Your contract is ready. Review and apply your signature to proceed.",
|
"Your contract is ready. Review and apply your signature to proceed.",
|
||||||
stage: 2,
|
stage: 3,
|
||||||
},
|
},
|
||||||
SIGNED_CUSTOMER: {
|
SIGNED_CUSTOMER: {
|
||||||
title: "Signed — awaiting staff",
|
title: "Signed — awaiting staff",
|
||||||
description:
|
description:
|
||||||
"Your signature has been submitted. Awaiting the final staff signature.",
|
"Your signature has been submitted. Awaiting the final staff signature.",
|
||||||
stage: 2,
|
stage: 3,
|
||||||
},
|
},
|
||||||
FULLY_EXECUTED: {
|
FULLY_EXECUTED: {
|
||||||
title: "Contract fully executed",
|
title: "Contract fully executed",
|
||||||
description: "Signed by all parties. You can now proceed to payment.",
|
description: "Signed by all parties. You can now proceed to payment.",
|
||||||
stage: 2,
|
stage: 4,
|
||||||
},
|
},
|
||||||
SELECTED_FOR_BATCH: {
|
SELECTED_FOR_BATCH: {
|
||||||
title: "Selected for a train — payment due",
|
title: "Selected for a train — payment due",
|
||||||
description:
|
description:
|
||||||
"Your booking was selected for a scheduled train. Complete payment within the pay window to secure your slot.",
|
"Your booking was selected for a scheduled train. Complete payment within the pay window to secure your slot.",
|
||||||
stage: 3,
|
stage: 4,
|
||||||
},
|
},
|
||||||
EXPIRED: {
|
EXPIRED: {
|
||||||
title: "Pay window expired",
|
title: "Pay window expired",
|
||||||
description:
|
description:
|
||||||
"The payment window was missed. You can move this booking to another schedule or cancel it.",
|
"The payment window was missed. You can move this booking to another schedule or cancel it.",
|
||||||
stage: 3,
|
stage: 6,
|
||||||
},
|
},
|
||||||
PNR_GENERATED: {
|
PNR_GENERATED: {
|
||||||
title: "Payment reference generated",
|
title: "Payment reference generated",
|
||||||
description:
|
description:
|
||||||
"A payment reference number has been generated for this booking.",
|
"A payment reference number has been generated for this booking.",
|
||||||
stage: 3,
|
stage: 5,
|
||||||
},
|
},
|
||||||
PAYMENT_VERIFICATION_IN_PROGRESS: {
|
PAYMENT_VERIFICATION_IN_PROGRESS: {
|
||||||
title: "Verifying payment",
|
title: "Verifying payment",
|
||||||
description: "Your payment is being verified.",
|
description: "Your payment is being verified.",
|
||||||
stage: 3,
|
stage: 4,
|
||||||
},
|
},
|
||||||
PAID: {
|
PAID: {
|
||||||
title: "Payment confirmed",
|
title: "Payment confirmed",
|
||||||
description: "Payment has been confirmed for this booking.",
|
description: "Payment has been confirmed for this booking.",
|
||||||
stage: 3,
|
stage: 5,
|
||||||
},
|
},
|
||||||
IN_TRANSIT: {
|
IN_TRANSIT: {
|
||||||
title: "Cargo moving",
|
title: "Cargo moving",
|
||||||
description: "Your shipment is currently moving through the rail network.",
|
description: "Your shipment is currently moving through the rail network.",
|
||||||
stage: 3,
|
stage: 6,
|
||||||
},
|
},
|
||||||
PENDING_CONSOLIDATION: {
|
PENDING_CONSOLIDATION: {
|
||||||
title: "Pending consolidation",
|
title: "Pending consolidation",
|
||||||
description: "Awaiting a consolidation partner shipment.",
|
description: "Awaiting a consolidation partner shipment.",
|
||||||
stage: 3,
|
stage: 5,
|
||||||
},
|
},
|
||||||
CONSOLIDATED: {
|
CONSOLIDATED: {
|
||||||
title: "Consolidated",
|
title: "Consolidated",
|
||||||
description: "Cargo has been consolidated with a partner shipment.",
|
description: "Cargo has been consolidated with a partner shipment.",
|
||||||
stage: 3,
|
stage: 5,
|
||||||
},
|
},
|
||||||
COMPLETED: {
|
COMPLETED: {
|
||||||
title: "Service complete",
|
title: "Service complete",
|
||||||
description: "Cargo delivered and service successfully terminated.",
|
description: "Cargo delivered and service successfully terminated.",
|
||||||
stage: 4,
|
stage: 7,
|
||||||
},
|
},
|
||||||
DELIVERED: {
|
DELIVERED: {
|
||||||
title: "Service complete",
|
title: "Service complete",
|
||||||
description: "Cargo delivered and service successfully terminated.",
|
description: "Cargo delivered and service successfully terminated.",
|
||||||
stage: 4,
|
stage: 7,
|
||||||
},
|
},
|
||||||
REJECTED: {
|
REJECTED: {
|
||||||
title: "Booking rejected",
|
title: "Booking rejected",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { CreateBookingPayload } from "@/services/bookings.service";
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { Alert, Box, Button, Group, Text, Title } from "@mantine/core";
|
import { Alert, Box, Button, Group, Text, Title } from "@mantine/core";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { AlertCircle, Check, ChevronLeft, ChevronRight } from "lucide-react";
|
import { AlertCircle, Check, ChevronLeft, ChevronRight, Send } from "lucide-react";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
@@ -97,6 +97,31 @@ export default function NewBookingPage() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const submitMutation = useMutation({
|
||||||
|
mutationFn: async (payload: CreateBookingPayload) => {
|
||||||
|
const booking = await api.bookings.create.call(payload);
|
||||||
|
|
||||||
|
const documents = form.getValues("documents") ?? {};
|
||||||
|
const hasDocuments = Object.values(documents).some((value) =>
|
||||||
|
Array.isArray(value) ? value.length > 0 : Boolean(value),
|
||||||
|
);
|
||||||
|
if (hasDocuments) {
|
||||||
|
await api.bookings.uploadDocuments.call({
|
||||||
|
id: booking.id,
|
||||||
|
files: documents,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await api.bookings.submit.call({ id: booking.id });
|
||||||
|
|
||||||
|
return booking;
|
||||||
|
},
|
||||||
|
onSuccess: (booking) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: api.bookings.list.queryKey() });
|
||||||
|
navigate(`/bookings/${booking.id}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const form = useForm<BookingFormInputValues, any, BookingFormValues>({
|
const form = useForm<BookingFormInputValues, any, BookingFormValues>({
|
||||||
defaultValues: initialBookingFormValues,
|
defaultValues: initialBookingFormValues,
|
||||||
resolver: zodResolver(bookingFormSchema),
|
resolver: zodResolver(bookingFormSchema),
|
||||||
@@ -116,6 +141,15 @@ export default function NewBookingPage() {
|
|||||||
return route;
|
return route;
|
||||||
}, [originYard, destinationYard]);
|
}, [originYard, destinationYard]);
|
||||||
|
|
||||||
|
const docValues = form.watch("documents") ?? {};
|
||||||
|
const hasDocuments = useMemo(
|
||||||
|
() =>
|
||||||
|
Object.values(docValues).some((value) =>
|
||||||
|
Array.isArray(value) ? value.length > 0 : Boolean(value),
|
||||||
|
),
|
||||||
|
[docValues],
|
||||||
|
);
|
||||||
|
|
||||||
async function handleContinue() {
|
async function handleContinue() {
|
||||||
const valid = await form.trigger(stepFields[step], { shouldFocus: true });
|
const valid = await form.trigger(stepFields[step], { shouldFocus: true });
|
||||||
if (!valid) return;
|
if (!valid) return;
|
||||||
@@ -123,14 +157,14 @@ export default function NewBookingPage() {
|
|||||||
setStep((currentStep) => Math.min(STEPS.length, currentStep + 1));
|
setStep((currentStep) => Math.min(STEPS.length, currentStep + 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSubmit = form.handleSubmit((data) => {
|
function buildApiPayload(data: BookingFormValues): CreateBookingPayload {
|
||||||
if (data.contractType === "renewal" && !data.previousContractRef) {
|
if (data.contractType === "renewal" && !data.previousContractRef) {
|
||||||
form.setError("previousContractRef", {
|
form.setError("previousContractRef", {
|
||||||
type: "manual",
|
type: "manual",
|
||||||
message: "Select a previous contract reference.",
|
message: "Select a previous contract reference.",
|
||||||
});
|
});
|
||||||
setStep(1);
|
setStep(1);
|
||||||
return;
|
throw new Error("Validation failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalWeight =
|
const totalWeight =
|
||||||
@@ -141,7 +175,6 @@ export default function NewBookingPage() {
|
|||||||
)
|
)
|
||||||
: Number(data.cargoWeight || 0);
|
: Number(data.cargoWeight || 0);
|
||||||
|
|
||||||
// ── Reference data lookups ──────────────────────────────────────────
|
|
||||||
const shippingLines = referenceData?.shipping_line ?? [];
|
const shippingLines = referenceData?.shipping_line ?? [];
|
||||||
const cargoTree = referenceData?.cargo_type ?? [];
|
const cargoTree = referenceData?.cargo_type ?? [];
|
||||||
const containerGroups = referenceData?.containers ?? [];
|
const containerGroups = referenceData?.containers ?? [];
|
||||||
@@ -174,8 +207,7 @@ export default function NewBookingPage() {
|
|||||||
(s) => s.id === data.serviceTypeId,
|
(s) => s.id === data.serviceTypeId,
|
||||||
)!;
|
)!;
|
||||||
|
|
||||||
// ── Build API payload ───────────────────────────────────────────────
|
return {
|
||||||
const apiPayload: CreateBookingPayload = {
|
|
||||||
scheduledDate: new Date().toISOString(),
|
scheduledDate: new Date().toISOString(),
|
||||||
contractType:
|
contractType:
|
||||||
data.contractType.toUpperCase() as CreateBookingPayload["contractType"],
|
data.contractType.toUpperCase() as CreateBookingPayload["contractType"],
|
||||||
@@ -222,8 +254,24 @@ export default function NewBookingPage() {
|
|||||||
: {}),
|
: {}),
|
||||||
...(cargoFreeText ? { cargoFreeText } : {}),
|
...(cargoFreeText ? { cargoFreeText } : {}),
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
createMutation.mutate(apiPayload);
|
const handleDraftSubmit = form.handleSubmit((data) => {
|
||||||
|
try {
|
||||||
|
const apiPayload = buildApiPayload(data);
|
||||||
|
createMutation.mutate(apiPayload);
|
||||||
|
} catch {
|
||||||
|
// validation error already handled
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleFullSubmit = form.handleSubmit((data) => {
|
||||||
|
try {
|
||||||
|
const apiPayload = buildApiPayload(data);
|
||||||
|
submitMutation.mutate(apiPayload);
|
||||||
|
} catch {
|
||||||
|
// validation error already handled
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -270,7 +318,7 @@ export default function NewBookingPage() {
|
|||||||
id="new-booking-form"
|
id="new-booking-form"
|
||||||
className="flex flex-col"
|
className="flex flex-col"
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleDraftSubmit}
|
||||||
>
|
>
|
||||||
<Box flex={1} p="24px">
|
<Box flex={1} p="24px">
|
||||||
<Box mb="lg">
|
<Box mb="lg">
|
||||||
@@ -295,6 +343,24 @@ export default function NewBookingPage() {
|
|||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{submitMutation.isError && (
|
||||||
|
<Alert
|
||||||
|
color="red"
|
||||||
|
icon={<AlertCircle size={16} />}
|
||||||
|
radius="md"
|
||||||
|
mb="lg"
|
||||||
|
>
|
||||||
|
<Text size="sm" fw={600}>
|
||||||
|
Failed to submit
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" mt={4} c="red.7">
|
||||||
|
{submitMutation.error instanceof Error
|
||||||
|
? submitMutation.error.message
|
||||||
|
: "An unexpected error occurred. Please try again."}
|
||||||
|
</Text>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
{step === 1 && (
|
{step === 1 && (
|
||||||
<Step1ContractType form={form} referenceData={referenceData} />
|
<Step1ContractType form={form} referenceData={referenceData} />
|
||||||
)}
|
)}
|
||||||
@@ -367,18 +433,35 @@ export default function NewBookingPage() {
|
|||||||
Continue
|
Continue
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Group>
|
||||||
type="submit"
|
<Button
|
||||||
form="new-booking-form"
|
type="submit"
|
||||||
color="edr-green"
|
form="new-booking-form"
|
||||||
radius="md"
|
variant={hasDocuments ? "outline" : "filled"}
|
||||||
loading={createMutation.isPending}
|
color="edr-green"
|
||||||
leftSection={
|
radius="md"
|
||||||
createMutation.isPending ? undefined : <Check size={16} />
|
loading={createMutation.isPending}
|
||||||
}
|
leftSection={
|
||||||
>
|
createMutation.isPending ? undefined : <Check size={16} />
|
||||||
{createMutation.isPending ? "Saving Draft..." : "Save as Draft"}
|
}
|
||||||
</Button>
|
>
|
||||||
|
{createMutation.isPending ? "Saving Draft..." : "Save as Draft"}
|
||||||
|
</Button>
|
||||||
|
{hasDocuments && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
color="edr-green"
|
||||||
|
radius="md"
|
||||||
|
loading={submitMutation.isPending}
|
||||||
|
leftSection={
|
||||||
|
submitMutation.isPending ? undefined : <Send size={16} />
|
||||||
|
}
|
||||||
|
onClick={() => handleFullSubmit()}
|
||||||
|
>
|
||||||
|
{submitMutation.isPending ? "Submitting..." : "Submit"}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
)}
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
Reference in New Issue
Block a user