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