mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +00:00
style: ui fixes to booking detail page
This commit is contained in:
@@ -29,7 +29,11 @@ import { CardTitle, PageShell, SectionCard } from "./components/layout";
|
||||
import { CountChip, DocRow, IconSquare } from "./components/Documents";
|
||||
import { EstimateCard } from "./components/pricing";
|
||||
import { HeaderButton, PageHeader } from "./components/PageHeader";
|
||||
import { MutationErrors, NoticeBanner } from "./components/Notices";
|
||||
import {
|
||||
ActionRequiredBanner,
|
||||
MutationErrors,
|
||||
NoticeBanner,
|
||||
} from "./components/Notices";
|
||||
import { ScheduleCard } from "./components/ScheduleCard";
|
||||
import { ShipmentDetailsCard } from "./components/ShipmentDetailsCard";
|
||||
import { StatusHero } from "./components/StatusHero";
|
||||
@@ -132,30 +136,39 @@ export function DraftBookingView({
|
||||
<PageHeader
|
||||
booking={booking}
|
||||
actions={
|
||||
<HeaderButton
|
||||
dark
|
||||
icon={<Pencil size={16} />}
|
||||
label="Continue editing"
|
||||
onClick={() => navigate(`/bookings/${booking.id}/edit`)}
|
||||
/>
|
||||
<>
|
||||
<HeaderButton
|
||||
icon={<Download size={16} />}
|
||||
label="Documents"
|
||||
onClick={() =>
|
||||
documentsRef.current?.scrollIntoView({ behavior: "smooth" })
|
||||
}
|
||||
/>
|
||||
<HeaderButton
|
||||
dark
|
||||
icon={<Pencil size={16} />}
|
||||
label="Continue editing"
|
||||
onClick={() => navigate(`/bookings/${booking.id}/edit`)}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
{booking.status === "CHANGES_REQUESTED" &&
|
||||
booking.latestChangeRequestNote && (
|
||||
<NoticeBanner
|
||||
tone="amber"
|
||||
icon={<AlertCircle size={18} />}
|
||||
title="Changes requested by staff"
|
||||
>
|
||||
{booking.latestChangeRequestNote}
|
||||
</NoticeBanner>
|
||||
)}
|
||||
<MutationErrors
|
||||
mutations={[uploadMutation, submitMutation, cancelMutation]}
|
||||
/>
|
||||
|
||||
<StatusHero booking={booking} />
|
||||
<StatusHero booking={booking}>
|
||||
{booking.status === "CHANGES_REQUESTED" &&
|
||||
booking.latestChangeRequestNote ? (
|
||||
<ActionRequiredBanner
|
||||
title="Review the requested changes, then resubmit."
|
||||
onAction={() => navigate(`/bookings/${booking.id}/edit`)}
|
||||
>
|
||||
{booking.latestChangeRequestNote}
|
||||
</ActionRequiredBanner>
|
||||
) : undefined}
|
||||
</StatusHero>
|
||||
|
||||
<BodyGrid
|
||||
left={
|
||||
@@ -169,48 +182,34 @@ export function DraftBookingView({
|
||||
</Text>
|
||||
</Group>
|
||||
<Stack gap={4}>
|
||||
<StepLine
|
||||
index={1}
|
||||
done
|
||||
title="Booking details"
|
||||
desc="Route, cargo and service are set."
|
||||
action={
|
||||
<StepGhostButton
|
||||
onClick={() => navigate(`/bookings/${booking.id}/edit`)}
|
||||
>
|
||||
Edit
|
||||
</StepGhostButton>
|
||||
}
|
||||
/>
|
||||
<StepLine index={1} done title="Cargo & route details" />
|
||||
<StepLine
|
||||
index={2}
|
||||
active={!allDocsUploaded}
|
||||
done={allDocsUploaded}
|
||||
highlight
|
||||
title="Upload documents"
|
||||
desc={
|
||||
title={
|
||||
allDocsUploaded
|
||||
? "All required documents uploaded."
|
||||
: `${uploadedCount} of ${REQUIRED_DOC_FIELDS.length} uploaded.`
|
||||
? "Required documents"
|
||||
: "Upload required documents"
|
||||
}
|
||||
desc={`${uploadedCount} of ${REQUIRED_DOC_FIELDS.length} uploaded.`}
|
||||
action={
|
||||
!allDocsUploaded && (
|
||||
<StepGhostButton
|
||||
onClick={() =>
|
||||
documentsRef.current?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
})
|
||||
}
|
||||
>
|
||||
Upload
|
||||
</StepGhostButton>
|
||||
)
|
||||
<StepGhostButton
|
||||
icon={<Upload size={16} color="#334155" />}
|
||||
onClick={() =>
|
||||
documentsRef.current?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
})
|
||||
}
|
||||
>
|
||||
Add documents
|
||||
</StepGhostButton>
|
||||
}
|
||||
/>
|
||||
<StepLine
|
||||
index={3}
|
||||
done={false}
|
||||
title="Submit for review"
|
||||
active={allDocsUploaded}
|
||||
title="Review & submit"
|
||||
desc="Send your booking to EDR staff."
|
||||
/>
|
||||
</Stack>
|
||||
@@ -223,7 +222,10 @@ export function DraftBookingView({
|
||||
onClick={handleSubmitRequest}
|
||||
disabled={submitMutation.isPending}
|
||||
loading={submitMutation.isPending}
|
||||
styles={{ root: { height: 46 }, label: { fontWeight: 700 } }}
|
||||
styles={{
|
||||
root: { height: 46 },
|
||||
label: { fontSize: 14, fontWeight: 800 },
|
||||
}}
|
||||
>
|
||||
{submitMutation.isPending ? "Submitting…" : "Submit for review"}
|
||||
</Button>
|
||||
@@ -268,7 +270,7 @@ export function DraftBookingView({
|
||||
? (file?.name ?? "Uploaded")
|
||||
: selected
|
||||
? selected.name
|
||||
: "Required · not uploaded"
|
||||
: "Not added yet"
|
||||
}
|
||||
status={
|
||||
isUploaded ? "verified" : selected ? "ready" : "missing"
|
||||
@@ -297,22 +299,26 @@ export function DraftBookingView({
|
||||
/>
|
||||
<Group gap={6} wrap="nowrap">
|
||||
<Button
|
||||
variant="default"
|
||||
variant="white"
|
||||
radius={9}
|
||||
leftSection={<Upload size={14} />}
|
||||
leftSection={<Upload size={16} color="#334155" />}
|
||||
onClick={() =>
|
||||
fileInputRefs.current[doc.key]?.click()
|
||||
}
|
||||
styles={{
|
||||
root: { height: 34, paddingInline: 12 },
|
||||
root: {
|
||||
height: 34,
|
||||
paddingInline: 13,
|
||||
border: "1.5px solid #CBD5E1",
|
||||
},
|
||||
label: {
|
||||
fontSize: 12.5,
|
||||
fontWeight: 700,
|
||||
color: "#475569",
|
||||
color: "#334155",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{selected ? "Change" : "Choose"}
|
||||
{selected ? "Change" : "Add"}
|
||||
</Button>
|
||||
{selected && (
|
||||
<ActionIcon
|
||||
@@ -361,7 +367,7 @@ export function DraftBookingView({
|
||||
<EstimateCard
|
||||
pricing={pricing}
|
||||
title="Estimated Cost"
|
||||
chip="Pending review"
|
||||
chip="Not invoiced"
|
||||
/>
|
||||
<ScheduleCard booking={booking} title="Schedule & Service" />
|
||||
<SupportCard onCancel={() => setCancelDialogOpen(true)} />
|
||||
|
||||
@@ -10,12 +10,14 @@ import { ActivityCard } from "./components/ActivityCard";
|
||||
import { ContractCard } from "./components/ContractCard";
|
||||
import { DocRow, IconSquare } from "./components/Documents";
|
||||
import { BodyGrid, CardTitle, PageShell, SectionCard } from "./components/layout";
|
||||
import { CancelledBanner } from "./components/Notices";
|
||||
import { HeaderButton, PageHeader } from "./components/PageHeader";
|
||||
import { PaymentCard } from "./components/pricing";
|
||||
import { ScheduleCard } from "./components/ScheduleCard";
|
||||
import { ShipmentDetailsCard } from "./components/ShipmentDetailsCard";
|
||||
import { StatusHero } from "./components/StatusHero";
|
||||
import { SupportCard } from "./components/SupportCard";
|
||||
import { fmtDate, isNegative } from "./utils";
|
||||
|
||||
export function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) {
|
||||
const navigate = useNavigate();
|
||||
@@ -49,7 +51,23 @@ export function ReadonlyBookingView({ booking }: { booking: Freight.IBooking })
|
||||
}
|
||||
/>
|
||||
|
||||
<StatusHero booking={booking} />
|
||||
{isNegative(status) ? (
|
||||
<CancelledBanner
|
||||
pillLabel={status === "REJECTED" ? "Rejected" : "Cancelled"}
|
||||
title={`This booking was ${
|
||||
status === "REJECTED" ? "rejected" : "cancelled"
|
||||
} on ${fmtDate(booking.updatedAt)}.`}
|
||||
subtitle={
|
||||
status === "REJECTED"
|
||||
? "This booking request has been rejected."
|
||||
: "This booking process has been terminated."
|
||||
}
|
||||
reason={booking.latestChangeRequestNote}
|
||||
onRebook={() => navigate("/bookings/new")}
|
||||
/>
|
||||
) : (
|
||||
<StatusHero booking={booking} />
|
||||
)}
|
||||
|
||||
<ContractCard booking={booking} navigate={navigate} />
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Box, Group, Text } from "@mantine/core";
|
||||
import { Train } from "lucide-react";
|
||||
|
||||
import type { Freight } from "@edr/types";
|
||||
|
||||
@@ -59,16 +58,11 @@ export function ActivityCard({ booking }: { booking: Freight.IBooking }) {
|
||||
style={{
|
||||
width: 18,
|
||||
height: 18,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRadius: 999,
|
||||
backgroundColor: first ? "#0EA371" : "#D4E9DF",
|
||||
boxShadow: first ? "0 0 0 4px #BFE8D4" : undefined,
|
||||
}}
|
||||
>
|
||||
{first && <Train size={10} color="#fff" />}
|
||||
</Box>
|
||||
/>
|
||||
{!lastItem && (
|
||||
<Box
|
||||
flex={1}
|
||||
@@ -91,13 +85,15 @@ export function ActivityCard({ booking }: { booking: Freight.IBooking }) {
|
||||
style={{
|
||||
borderRadius: 999,
|
||||
backgroundColor: "#ECF6F1",
|
||||
border: "1px solid #CDEBDD",
|
||||
padding: "3px 9px",
|
||||
fontSize: 10.5,
|
||||
fontWeight: 800,
|
||||
letterSpacing: 0.3,
|
||||
color: "#0A6F4D",
|
||||
}}
|
||||
>
|
||||
Latest
|
||||
Current
|
||||
</Box>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
@@ -10,29 +10,27 @@ const CONTRACT_CONFIG: Record<
|
||||
title: string;
|
||||
description: string;
|
||||
buttonLabel?: string;
|
||||
urgent?: boolean;
|
||||
}
|
||||
> = {
|
||||
APPROVED_PENDING_SIGNATURE: {
|
||||
title: "Contract being prepared",
|
||||
title: "Your contract is being prepared.",
|
||||
description:
|
||||
"Your booking has been approved. The contract will be available shortly.",
|
||||
},
|
||||
CONTRACT_READY: {
|
||||
title: "Action required — sign your contract",
|
||||
title: "Review and sign your contract to proceed.",
|
||||
description:
|
||||
"Your contract is ready. Review the agreement and apply your digital signature to proceed.",
|
||||
buttonLabel: "View & sign contract",
|
||||
urgent: true,
|
||||
"Once all parties sign, we'll issue your payment reference (PNR) and schedule the cargo.",
|
||||
buttonLabel: "Review & Sign",
|
||||
},
|
||||
SIGNED_CUSTOMER: {
|
||||
title: "You have signed the contract",
|
||||
title: "You have signed the contract.",
|
||||
description:
|
||||
"Your signature has been submitted. Awaiting the final staff signature.",
|
||||
buttonLabel: "View contract",
|
||||
},
|
||||
FULLY_EXECUTED: {
|
||||
title: "Contract fully executed",
|
||||
title: "Contract fully executed — proceed to payment.",
|
||||
description:
|
||||
"The contract has been signed by all parties. You can now proceed to payment.",
|
||||
buttonLabel: "View contract",
|
||||
@@ -48,37 +46,54 @@ export function ContractCard({
|
||||
}) {
|
||||
const c = CONTRACT_CONFIG[booking.status as string];
|
||||
if (!c) return null;
|
||||
const urgent = c.urgent;
|
||||
|
||||
return (
|
||||
<Paper
|
||||
radius={20}
|
||||
p="lg"
|
||||
withBorder={!urgent}
|
||||
bg={urgent ? "#0EA371" : "#ECF6F1"}
|
||||
style={{ borderColor: urgent ? "transparent" : "#CDEBDD" }}
|
||||
radius={16}
|
||||
px={22}
|
||||
py={20}
|
||||
bg="#F1FAF6"
|
||||
style={{ border: "1px solid #CFEBDD" }}
|
||||
>
|
||||
<Group justify="space-between" align="center" wrap="wrap" gap="md">
|
||||
<Group justify="space-between" align="center" wrap="wrap" gap={20}>
|
||||
<Group gap={16} align="center" wrap="nowrap">
|
||||
<Box
|
||||
style={{
|
||||
width: 44,
|
||||
height: 44,
|
||||
flexShrink: 0,
|
||||
width: 46,
|
||||
height: 46,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRadius: 12,
|
||||
borderRadius: 13,
|
||||
backgroundColor: "#fff",
|
||||
color: urgent ? "#0EA371" : "#0A6F4D",
|
||||
border: "1px solid #CDEBDD",
|
||||
color: "#0A6F4D",
|
||||
}}
|
||||
>
|
||||
<FileSignature size={22} />
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="15px" fw={800} c={urgent ? "#fff" : "#10202F"}>
|
||||
<Box miw={0}>
|
||||
<Box
|
||||
component="span"
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
borderRadius: 999,
|
||||
backgroundColor: "#0A6F4D",
|
||||
padding: "4px 10px",
|
||||
fontSize: 10.5,
|
||||
fontWeight: 800,
|
||||
letterSpacing: 0.3,
|
||||
color: "#fff",
|
||||
textTransform: "uppercase",
|
||||
}}
|
||||
>
|
||||
What's next
|
||||
</Box>
|
||||
<Text mt={6} fz="15.5px" fw={700} c="#10202F">
|
||||
{c.title}
|
||||
</Text>
|
||||
<Text mt={2} fz="13px" c={urgent ? "rgba(255,255,255,0.8)" : "#6B7C8E"}>
|
||||
<Text mt={2} fz="13px" c="#5B6B7A">
|
||||
{c.description}
|
||||
</Text>
|
||||
</Box>
|
||||
@@ -87,10 +102,10 @@ export function ContractCard({
|
||||
<Button
|
||||
onClick={() => navigate(`/bookings/${booking.id}/contract`)}
|
||||
radius={10}
|
||||
color={urgent ? "#0C1A2B" : "edr-green"}
|
||||
leftSection={<FileSignature size={15} />}
|
||||
color="edr-green"
|
||||
leftSection={<FileSignature size={18} />}
|
||||
styles={{
|
||||
root: { height: 42, paddingInline: 16 },
|
||||
root: { height: 42, paddingInline: 18 },
|
||||
label: { fontSize: 13, fontWeight: 700 },
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,7 +1,33 @@
|
||||
import { Box, Group, Text } from "@mantine/core";
|
||||
import { CheckCircle2, FileText } from "lucide-react";
|
||||
import { CheckCircle2, FileText, MinusCircle } from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
type DocStatus = "verified" | "ready" | "missing";
|
||||
|
||||
const STATUS_PILL: Record<
|
||||
DocStatus,
|
||||
{ label: string; bg: string; color: string; border?: string; icon?: ReactNode }
|
||||
> = {
|
||||
verified: {
|
||||
label: "Uploaded",
|
||||
bg: "#ECF6F1",
|
||||
color: "#0A6F4D",
|
||||
border: "#CDEBDD",
|
||||
icon: <CheckCircle2 size={13} />,
|
||||
},
|
||||
ready: {
|
||||
label: "Ready",
|
||||
bg: "#EAF1FB",
|
||||
color: "#2E5B96",
|
||||
},
|
||||
missing: {
|
||||
label: "Not added",
|
||||
bg: "#F1F4F7",
|
||||
color: "#9AA8B5",
|
||||
icon: <MinusCircle size={13} />,
|
||||
},
|
||||
};
|
||||
|
||||
export function DocRow({
|
||||
title,
|
||||
meta,
|
||||
@@ -11,12 +37,15 @@ export function DocRow({
|
||||
}: {
|
||||
title: string;
|
||||
meta: string;
|
||||
status: "verified" | "ready" | "missing";
|
||||
status: DocStatus;
|
||||
action: ReactNode;
|
||||
last?: boolean;
|
||||
}) {
|
||||
const missing = status === "missing";
|
||||
const tileBg = status === "ready" ? "#EAF1FB" : "#F1F4F7";
|
||||
const tileFg = status === "ready" ? "#2E5B96" : "#475569";
|
||||
const tileFg =
|
||||
status === "ready" ? "#2E5B96" : missing ? "#9AA8B5" : "#475569";
|
||||
const pill = STATUS_PILL[status];
|
||||
|
||||
return (
|
||||
<Group
|
||||
@@ -41,52 +70,36 @@ export function DocRow({
|
||||
color: tileFg,
|
||||
}}
|
||||
>
|
||||
<FileText size={18} />
|
||||
<FileText size={20} />
|
||||
</Box>
|
||||
<Box miw={0} flex={1}>
|
||||
<Text truncate fz="13.5px" fw={700} c="#10202F">
|
||||
<Text truncate fz="13.5px" fw={700} c={missing ? "#6B7C8E" : "#10202F"}>
|
||||
{title}
|
||||
</Text>
|
||||
<Text truncate fz="12px" c="#9AA8B5">
|
||||
{meta}
|
||||
</Text>
|
||||
</Box>
|
||||
{status === "verified" && (
|
||||
<Group
|
||||
component="span"
|
||||
gap={6}
|
||||
align="center"
|
||||
wrap="nowrap"
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
borderRadius: 999,
|
||||
backgroundColor: "#ECF6F1",
|
||||
padding: "5px 11px",
|
||||
fontSize: 11.5,
|
||||
fontWeight: 700,
|
||||
color: "#0A6F4D",
|
||||
}}
|
||||
>
|
||||
<CheckCircle2 size={13} />
|
||||
Verified
|
||||
</Group>
|
||||
)}
|
||||
{status === "ready" && (
|
||||
<Box
|
||||
component="span"
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
borderRadius: 999,
|
||||
backgroundColor: "#EAF1FB",
|
||||
padding: "5px 11px",
|
||||
fontSize: 11.5,
|
||||
fontWeight: 700,
|
||||
color: "#2E5B96",
|
||||
}}
|
||||
>
|
||||
Ready
|
||||
</Box>
|
||||
)}
|
||||
<Group
|
||||
component="span"
|
||||
gap={6}
|
||||
align="center"
|
||||
wrap="nowrap"
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
flexShrink: 0,
|
||||
borderRadius: 999,
|
||||
backgroundColor: pill.bg,
|
||||
border: pill.border ? `1px solid ${pill.border}` : undefined,
|
||||
padding: "5px 11px",
|
||||
fontSize: 11.5,
|
||||
fontWeight: 700,
|
||||
color: pill.color,
|
||||
}}
|
||||
>
|
||||
{pill.icon}
|
||||
{pill.label}
|
||||
</Group>
|
||||
{action}
|
||||
</Group>
|
||||
);
|
||||
@@ -147,15 +160,15 @@ export function CountChip({
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
borderRadius: 999,
|
||||
padding: "5px 11px",
|
||||
padding: "5px 10px",
|
||||
fontSize: 11.5,
|
||||
fontWeight: 700,
|
||||
backgroundColor: done ? "#ECF6F1" : "#FDF3E0",
|
||||
color: done ? "#0A6F4D" : "#9A5B00",
|
||||
backgroundColor: done ? "#ECF6F1" : "#F1F4F7",
|
||||
color: done ? "#0A6F4D" : "#6B7C8E",
|
||||
}}
|
||||
>
|
||||
{done && <CheckCircle2 size={13} />}
|
||||
{uploaded}/{total} uploaded
|
||||
{uploaded} of {total} added
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Box, Group, Text } from "@mantine/core";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import { Box, Button, Group, Paper, Stack, Text } from "@mantine/core";
|
||||
import { AlertCircle, AlertTriangle, PencilLine, StickyNote, XCircle } from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export function NoticeBanner({
|
||||
@@ -51,6 +51,194 @@ export function NoticeBanner({
|
||||
);
|
||||
}
|
||||
|
||||
export function ActionRequiredBanner({
|
||||
title,
|
||||
children,
|
||||
onAction,
|
||||
}: {
|
||||
title: string;
|
||||
children: ReactNode;
|
||||
onAction?: () => void;
|
||||
}) {
|
||||
return (
|
||||
<Group
|
||||
justify="space-between"
|
||||
align="center"
|
||||
wrap="wrap"
|
||||
gap={24}
|
||||
px={22}
|
||||
py={20}
|
||||
style={{
|
||||
borderRadius: 16,
|
||||
backgroundColor: "#FDF3E0",
|
||||
border: "1px solid #F4D9A8",
|
||||
}}
|
||||
>
|
||||
<Group gap={16} align="center" wrap="nowrap" miw={0} flex={1}>
|
||||
<Box
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
width: 44,
|
||||
height: 44,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRadius: 12,
|
||||
backgroundColor: "#fff",
|
||||
border: "1px solid #F4D9A8",
|
||||
color: "#C77F12",
|
||||
}}
|
||||
>
|
||||
<AlertTriangle size={23} />
|
||||
</Box>
|
||||
<Box miw={0}>
|
||||
<Text
|
||||
fz="10.5px"
|
||||
fw={800}
|
||||
c="#B07A2A"
|
||||
tt="uppercase"
|
||||
style={{ letterSpacing: 0.6 }}
|
||||
>
|
||||
Action required · You
|
||||
</Text>
|
||||
<Text mt={5} fz="15.5px" fw={700} c="#10202F">
|
||||
{title}
|
||||
</Text>
|
||||
<Text mt={2} fz="13px" c="#7A6A4E" style={{ lineHeight: 1.45 }}>
|
||||
{children}
|
||||
</Text>
|
||||
</Box>
|
||||
</Group>
|
||||
{onAction && (
|
||||
<Button
|
||||
onClick={onAction}
|
||||
radius={10}
|
||||
color="#F2A516"
|
||||
leftSection={<PencilLine size={18} color="#5A3D08" />}
|
||||
styles={{
|
||||
root: { height: 42, paddingInline: 18, flexShrink: 0 },
|
||||
label: { fontSize: 13, fontWeight: 800, color: "#3D2A05" },
|
||||
}}
|
||||
>
|
||||
Review & Resubmit
|
||||
</Button>
|
||||
)}
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
export function CancelledBanner({
|
||||
pillLabel = "Cancelled",
|
||||
title,
|
||||
subtitle,
|
||||
reason,
|
||||
onRebook,
|
||||
}: {
|
||||
pillLabel?: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
reason?: string | null;
|
||||
onRebook?: () => void;
|
||||
}) {
|
||||
return (
|
||||
<Paper
|
||||
radius={16}
|
||||
p={20}
|
||||
bg="#FCEEEA"
|
||||
style={{ border: "1px solid #F3C6BB" }}
|
||||
>
|
||||
<Stack gap={14}>
|
||||
<Group justify="space-between" align="center" wrap="wrap" gap={20}>
|
||||
<Group gap={16} align="center" wrap="nowrap" miw={0}>
|
||||
<Box
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
width: 46,
|
||||
height: 46,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRadius: 13,
|
||||
backgroundColor: "#fff",
|
||||
border: "1px solid #F3C6BB",
|
||||
color: "#D93C15",
|
||||
}}
|
||||
>
|
||||
<XCircle size={24} />
|
||||
</Box>
|
||||
<Box miw={0}>
|
||||
<Box
|
||||
component="span"
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
borderRadius: 999,
|
||||
backgroundColor: "#D93C15",
|
||||
padding: "4px 10px",
|
||||
fontSize: 10.5,
|
||||
fontWeight: 800,
|
||||
letterSpacing: 0.3,
|
||||
color: "#fff",
|
||||
textTransform: "uppercase",
|
||||
}}
|
||||
>
|
||||
{pillLabel}
|
||||
</Box>
|
||||
<Text mt={6} fz="15.5px" fw={700} c="#10202F">
|
||||
{title}
|
||||
</Text>
|
||||
<Text mt={2} fz="13px" c="#6B7C8E">
|
||||
{subtitle}
|
||||
</Text>
|
||||
</Box>
|
||||
</Group>
|
||||
{onRebook && (
|
||||
<Button
|
||||
onClick={onRebook}
|
||||
radius={10}
|
||||
color="edr-green"
|
||||
styles={{
|
||||
root: { height: 42, paddingInline: 18 },
|
||||
label: { fontSize: 13, fontWeight: 700 },
|
||||
}}
|
||||
>
|
||||
Rebook shipment
|
||||
</Button>
|
||||
)}
|
||||
</Group>
|
||||
{reason && (
|
||||
<Group
|
||||
gap={11}
|
||||
align="flex-start"
|
||||
wrap="nowrap"
|
||||
p={14}
|
||||
style={{
|
||||
borderRadius: 12,
|
||||
backgroundColor: "#fff",
|
||||
border: "1px solid #F3C6BB",
|
||||
}}
|
||||
>
|
||||
<StickyNote size={20} color="#B0341A" style={{ flexShrink: 0 }} />
|
||||
<Box miw={0}>
|
||||
<Text
|
||||
fz="10.5px"
|
||||
fw={700}
|
||||
c="#B0341A"
|
||||
tt="uppercase"
|
||||
style={{ letterSpacing: 0.5 }}
|
||||
>
|
||||
Reason for cancellation
|
||||
</Text>
|
||||
<Text mt={3} fz="13.5px" c="#3D2A26" style={{ lineHeight: 1.45 }}>
|
||||
{reason}
|
||||
</Text>
|
||||
</Box>
|
||||
</Group>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
export function MutationErrors({
|
||||
mutations,
|
||||
}: {
|
||||
|
||||
@@ -27,7 +27,7 @@ export function PageHeader({
|
||||
<Group justify="space-between" align="flex-start" wrap="wrap" gap="md">
|
||||
<Stack gap={8} miw={0}>
|
||||
<Group gap={12} align="center" wrap="wrap">
|
||||
<Text fz="18px" fw={800}>
|
||||
<Text fz="26px" fw={800} c="#10202F">
|
||||
{booking.reference}
|
||||
</Text>
|
||||
<Group
|
||||
|
||||
@@ -3,11 +3,49 @@ import type { ReactNode } from "react";
|
||||
|
||||
import type { Freight } from "@edr/types";
|
||||
|
||||
import { fmtDate } from "../utils";
|
||||
import { fmtDate, isDraftLike, isNegative } from "../utils";
|
||||
import { CardTitle, SectionCard } from "./layout";
|
||||
|
||||
type Row = { label: string; value: ReactNode; muted?: boolean };
|
||||
|
||||
function StatusPill({ status }: { status: string }) {
|
||||
const negative = isNegative(status);
|
||||
const draft = isDraftLike(status);
|
||||
const dot = negative ? "#C0392B" : draft ? "#94A3B8" : "#0EA371";
|
||||
const color = negative ? "#A93226" : draft ? "#475569" : "#0A6F4D";
|
||||
const bg = negative ? "#FBEAE7" : draft ? "#F1F4F7" : "#ECF6F1";
|
||||
const border = negative ? "#F3C8C1" : draft ? "#E1E7EE" : "#CDEBDD";
|
||||
const label = status
|
||||
.replace(/_/g, " ")
|
||||
.toLowerCase()
|
||||
.replace(/\b\w/g, (m) => m.toUpperCase());
|
||||
|
||||
return (
|
||||
<Group
|
||||
component="span"
|
||||
gap={6}
|
||||
align="center"
|
||||
wrap="nowrap"
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
borderRadius: 999,
|
||||
backgroundColor: bg,
|
||||
border: `1px solid ${border}`,
|
||||
padding: "4px 10px",
|
||||
fontSize: 11.5,
|
||||
fontWeight: 700,
|
||||
color,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="span"
|
||||
style={{ width: 6, height: 6, borderRadius: 999, backgroundColor: dot }}
|
||||
/>
|
||||
{label}
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
export function ScheduleCard({
|
||||
booking,
|
||||
title,
|
||||
@@ -30,6 +68,11 @@ export function ScheduleCard({
|
||||
muted: !booking.trainId,
|
||||
};
|
||||
|
||||
const statusRow: Row = {
|
||||
label: "Status",
|
||||
value: <StatusPill status={booking.status as string} />,
|
||||
};
|
||||
|
||||
const rows: Row[] = consignment
|
||||
? [
|
||||
{ label: "Consignment ID", value: booking.reference },
|
||||
@@ -40,6 +83,7 @@ export function ScheduleCard({
|
||||
{ label: "Consolidation", value: consolidation },
|
||||
]
|
||||
: [
|
||||
statusRow,
|
||||
{ label: "Service", value: service },
|
||||
{ label: "Equipment return", value: equipmentReturn },
|
||||
{ label: "Proposed date", value: fmtDate(booking.scheduledDate) },
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Box, Group, Text } from "@mantine/core";
|
||||
import { AlertTriangle, Check, Clock, FileText, History } from "lucide-react";
|
||||
import { AlertTriangle, Check, FileText, History } from "lucide-react";
|
||||
|
||||
import type { Freight } from "@edr/types";
|
||||
|
||||
@@ -7,7 +7,13 @@ import { PROGRESS_STAGES, STATUS_MAP } from "../constants";
|
||||
import { fmtDate, isDraftLike, isNegative } from "../utils";
|
||||
import { SectionCard } from "./layout";
|
||||
|
||||
export function StatusHero({ booking }: { booking: Freight.IBooking }) {
|
||||
export function StatusHero({
|
||||
booking,
|
||||
children,
|
||||
}: {
|
||||
booking: Freight.IBooking;
|
||||
children?: React.ReactNode;
|
||||
}) {
|
||||
const status = booking.status as string;
|
||||
const cfg = STATUS_MAP[status] ?? STATUS_MAP.DRAFT;
|
||||
const negative = isNegative(status);
|
||||
@@ -34,7 +40,7 @@ export function StatusHero({ booking }: { booking: Freight.IBooking }) {
|
||||
);
|
||||
|
||||
return (
|
||||
<SectionCard>
|
||||
<SectionCard p={28}>
|
||||
<Group justify="space-between" align="center" wrap="wrap" gap="md">
|
||||
<Group gap={16} align="center" wrap="nowrap">
|
||||
<Box
|
||||
@@ -52,10 +58,10 @@ export function StatusHero({ booking }: { booking: Freight.IBooking }) {
|
||||
<HeroIcon size={26} />
|
||||
</Box>
|
||||
<Box>
|
||||
<Text size="lg" fw={700}>
|
||||
<Text fz="21px" fw={800} c="#10202F">
|
||||
{cfg.title}
|
||||
</Text>
|
||||
<Text mt={4} fz="14px" c="#6B7C8E">
|
||||
<Text mt={5} fz="14px" c="#6B7C8E">
|
||||
{cfg.description}
|
||||
</Text>
|
||||
</Box>
|
||||
@@ -72,25 +78,33 @@ export function StatusHero({ booking }: { booking: Freight.IBooking }) {
|
||||
backgroundColor: "#F7FAFC",
|
||||
}}
|
||||
>
|
||||
<Clock size={20} color="#0A6F4D" />
|
||||
<History size={22} color="#64748B" />
|
||||
<Box>
|
||||
<Text fz="12px" fw={600}>
|
||||
<Text
|
||||
fz="10.5px"
|
||||
fw={700}
|
||||
c="#9AA8B5"
|
||||
tt="uppercase"
|
||||
style={{ letterSpacing: 0.6 }}
|
||||
>
|
||||
{chipLabel}
|
||||
</Text>
|
||||
<Text fz="13px" fw={700}>
|
||||
<Text fz="13.5px" fw={700} c="#10202F">
|
||||
{chipValue}
|
||||
</Text>
|
||||
</Box>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
<Box my={24} h={1} w="100%" bg="#EEF2F6" />
|
||||
<Box my={26} h={1} w="100%" bg="#EEF2F6" />
|
||||
|
||||
<ProgressTracker
|
||||
current={cfg.stage}
|
||||
tone={draft ? "ink" : "green"}
|
||||
negative={negative}
|
||||
/>
|
||||
{children ?? (
|
||||
<ProgressTracker
|
||||
current={cfg.stage}
|
||||
tone={draft ? "ink" : "green"}
|
||||
negative={negative}
|
||||
/>
|
||||
)}
|
||||
</SectionCard>
|
||||
);
|
||||
}
|
||||
@@ -185,7 +199,7 @@ function ProgressTracker({
|
||||
</Box>
|
||||
<Text
|
||||
fz="13.5px"
|
||||
fw={700}
|
||||
fw={state === "active" ? 800 : 700}
|
||||
ta="center"
|
||||
c={state === "idle" ? "#9AA8B5" : "#10202F"}
|
||||
>
|
||||
@@ -193,7 +207,7 @@ function ProgressTracker({
|
||||
</Text>
|
||||
<Text
|
||||
fz="11.5px"
|
||||
fw={500}
|
||||
fw={state === "active" ? 700 : 500}
|
||||
ta="center"
|
||||
c={state === "active" ? activeSub : "#9AA8B5"}
|
||||
>
|
||||
|
||||
@@ -2,6 +2,41 @@ import { Box, Button, Group, Text } from "@mantine/core";
|
||||
import { Check } from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
function StepCircle({
|
||||
index,
|
||||
done,
|
||||
active,
|
||||
}: {
|
||||
index: number;
|
||||
done?: boolean;
|
||||
active?: boolean;
|
||||
}) {
|
||||
const style: React.CSSProperties = done
|
||||
? { backgroundColor: "#0EA371", color: "#fff" }
|
||||
: active
|
||||
? { backgroundColor: "#0C1A2B", color: "#fff" }
|
||||
: { backgroundColor: "#EEF2F6", color: "#9AA8B5" };
|
||||
|
||||
return (
|
||||
<Box
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
width: 26,
|
||||
height: 26,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRadius: 999,
|
||||
fontSize: 12,
|
||||
fontWeight: 700,
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
{done ? <Check size={16} strokeWidth={3} /> : index}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export function StepLine({
|
||||
index,
|
||||
title,
|
||||
@@ -9,63 +44,49 @@ export function StepLine({
|
||||
action,
|
||||
done,
|
||||
active,
|
||||
highlight,
|
||||
}: {
|
||||
index: number;
|
||||
title: string;
|
||||
desc: string;
|
||||
desc?: string;
|
||||
action?: ReactNode;
|
||||
done?: boolean;
|
||||
active?: boolean;
|
||||
highlight?: boolean;
|
||||
}) {
|
||||
const badgeStyle: React.CSSProperties = done
|
||||
? { backgroundColor: "#0EA371", color: "#fff" }
|
||||
: active
|
||||
? { backgroundColor: "#0C1A2B", color: "#fff" }
|
||||
: {
|
||||
backgroundColor: "#EEF2F6",
|
||||
color: "#9AA8B5",
|
||||
border: "1px solid #E1E7EE",
|
||||
};
|
||||
if (active) {
|
||||
return (
|
||||
<Group
|
||||
gap={14}
|
||||
align="center"
|
||||
justify="space-between"
|
||||
wrap="nowrap"
|
||||
px={14}
|
||||
py={12}
|
||||
style={{ borderRadius: 12, backgroundColor: "#F4F7FA" }}
|
||||
>
|
||||
<Group gap={14} align="center" wrap="nowrap" miw={0}>
|
||||
<StepCircle index={index} active />
|
||||
<Box miw={0}>
|
||||
<Text fz="14px" fw={800} c="#10202F">
|
||||
{title}
|
||||
</Text>
|
||||
{desc && (
|
||||
<Text mt={2} fz="12.5px" c="#6B7C8E">
|
||||
{desc}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Group>
|
||||
{action}
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Group
|
||||
gap={14}
|
||||
align="center"
|
||||
wrap="nowrap"
|
||||
px="sm"
|
||||
py={12}
|
||||
style={{
|
||||
borderRadius: 12,
|
||||
backgroundColor: highlight ? "#F4F7FA" : undefined,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
width: 28,
|
||||
height: 28,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRadius: 999,
|
||||
fontSize: 12,
|
||||
fontWeight: 700,
|
||||
...badgeStyle,
|
||||
}}
|
||||
>
|
||||
{done ? <Check size={14} /> : index}
|
||||
</Box>
|
||||
<Box miw={0} flex={1}>
|
||||
<Text fz="13.5px" fw={700} c="#10202F">
|
||||
{title}
|
||||
</Text>
|
||||
<Text fz="12.5px" c="#9AA8B5">
|
||||
{desc}
|
||||
</Text>
|
||||
</Box>
|
||||
{action}
|
||||
<Group gap={14} align="center" wrap="nowrap" px={12} py={10}>
|
||||
<StepCircle index={index} done={done} />
|
||||
<Text fz="14px" fw={700} c={done ? "#6B7C8E" : "#9AA8B5"}>
|
||||
{title}
|
||||
</Text>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
@@ -73,18 +94,26 @@ export function StepLine({
|
||||
export function StepGhostButton({
|
||||
children,
|
||||
onClick,
|
||||
icon,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
onClick?: () => void;
|
||||
icon?: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<Button
|
||||
variant="default"
|
||||
variant="white"
|
||||
radius={9}
|
||||
onClick={onClick}
|
||||
leftSection={icon}
|
||||
styles={{
|
||||
root: { height: 32, paddingInline: 12 },
|
||||
label: { fontSize: 12.5, fontWeight: 700, color: "#475569" },
|
||||
root: {
|
||||
height: 34,
|
||||
paddingInline: 14,
|
||||
border: "1.5px solid #CBD5E1",
|
||||
flexShrink: 0,
|
||||
},
|
||||
label: { fontSize: 12.5, fontWeight: 700, color: "#334155" },
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { ReactNode, Ref } from "react";
|
||||
export function PageShell({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<Box mih="100vh">
|
||||
<Box mx="auto" w="100%" px="lg" py={28}>
|
||||
<Box mx="auto" w="100%" px={32} pt={28} pb={32}>
|
||||
<Stack gap="lg">{children}</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -48,7 +48,7 @@ export function SectionCard({ children, ref, ...props }: SectionCardProps) {
|
||||
|
||||
export function CardTitle({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<Text size="lg" fw={700}>
|
||||
<Text fz="16px" fw={800} c="#10202F">
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Box, Button, Group, Stack, Text } from "@mantine/core";
|
||||
import { FileText } from "lucide-react";
|
||||
import { CheckCircle2, Clock, FileText } from "lucide-react";
|
||||
|
||||
import type { Freight } from "@edr/types";
|
||||
|
||||
@@ -41,24 +41,45 @@ export function EstimateCard({
|
||||
<SectionCard p={22}>
|
||||
<Group justify="space-between" align="center">
|
||||
<CardTitle>{title}</CardTitle>
|
||||
<Box
|
||||
<Group
|
||||
component="span"
|
||||
gap={6}
|
||||
align="center"
|
||||
wrap="nowrap"
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
borderRadius: 999,
|
||||
backgroundColor: "#F1F4F7",
|
||||
padding: "5px 11px",
|
||||
fontSize: 11.5,
|
||||
fontWeight: 700,
|
||||
color: "#475569",
|
||||
color: "#6B7C8E",
|
||||
}}
|
||||
>
|
||||
<Clock size={13} />
|
||||
{chip}
|
||||
</Box>
|
||||
</Group>
|
||||
</Group>
|
||||
<Box mt={12}>
|
||||
<Text fz="26px" fw={800} c="#10202F">
|
||||
{priceTotal(pricing)}
|
||||
</Text>
|
||||
<Group gap={8} align="flex-end" wrap="nowrap">
|
||||
<Text fz="26px" fw={800} c="#10202F" lh={1.1}>
|
||||
{priceTotal(pricing)}
|
||||
</Text>
|
||||
<Box
|
||||
component="span"
|
||||
mb={4}
|
||||
style={{
|
||||
borderRadius: 6,
|
||||
backgroundColor: "#F1F4F7",
|
||||
padding: "3px 7px",
|
||||
fontSize: 11,
|
||||
fontWeight: 700,
|
||||
color: "#6B7C8E",
|
||||
}}
|
||||
>
|
||||
est.
|
||||
</Box>
|
||||
</Group>
|
||||
<Text mt={4} fz="12.5px" c="#9AA8B5">
|
||||
A firm price is confirmed after EDR reviews your booking.
|
||||
</Text>
|
||||
@@ -101,9 +122,13 @@ export function PaymentCard({
|
||||
<SectionCard p={22}>
|
||||
<Group justify="space-between" align="center">
|
||||
<CardTitle>Payment</CardTitle>
|
||||
<Box
|
||||
<Group
|
||||
component="span"
|
||||
gap={6}
|
||||
align="center"
|
||||
wrap="nowrap"
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
borderRadius: 999,
|
||||
padding: "5px 11px",
|
||||
fontSize: 11.5,
|
||||
@@ -113,10 +138,11 @@ export function PaymentCard({
|
||||
border: paid ? "1px solid #CDEBDD" : undefined,
|
||||
}}
|
||||
>
|
||||
{paid && <CheckCircle2 size={13} />}
|
||||
{paid
|
||||
? "Paid"
|
||||
: (booking.paymentStatus?.replace(/_/g, " ") ?? "Pending")}
|
||||
</Box>
|
||||
</Group>
|
||||
</Group>
|
||||
<Box mt={12}>
|
||||
<Text fz="26px" fw={800} c="#10202F">
|
||||
|
||||
Reference in New Issue
Block a user