style: booking detail pages fix

This commit is contained in:
Nathnael
2026-06-11 08:27:38 +00:00
parent e01a7faa20
commit 3fa09ec644
6 changed files with 254 additions and 272 deletions

View File

@@ -18,6 +18,7 @@ import {
X,
XCircle,
} from "lucide-react";
import { useMemo, useRef, useState } from "react";
import { useNavigate } from "react-router-dom";
@@ -136,22 +137,17 @@ export function DraftBookingView({
<PageHeader
booking={booking}
actions={
<>
<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`)}
/>
</>
<HeaderButton
dark
icon={<Pencil size={16} />}
label="Continue editing"
onClick={() => navigate(`/bookings/${booking.id}/edit`)}
/>
}
menuActions={{
onCancel: () => setCancelDialogOpen(true),
onSupport: () => navigate("/support"),
}}
/>
<MutationErrors
@@ -249,7 +245,7 @@ export function DraftBookingView({
<NoticeBanner
tone="red"
icon={<AlertCircle size={16} />}
style={{ marginBottom: 12 }}
className="mb-3"
>
{docError}
</NoticeBanner>

View File

@@ -49,6 +49,11 @@ export function ReadonlyBookingView({ booking }: { booking: Freight.IBooking })
/>
)
}
menuActions={{
onViewContract: booking.signedByCeoAt ? () => {} : undefined,
onRebook: () => navigate("/bookings/new"),
onSupport: () => navigate("/support"),
}}
/>
{isNegative(status) ? (

View File

@@ -7,13 +7,13 @@ export function NoticeBanner({
icon,
title,
children,
style,
className,
}: {
tone: "amber" | "red";
icon: ReactNode;
title?: string;
children: ReactNode;
style?: React.CSSProperties;
className?: string;
}) {
const palette =
tone === "amber"
@@ -21,33 +21,26 @@ export function NoticeBanner({
: { border: "#F3C8C1", bg: "#FBEAE7", color: "#A93226" };
return (
<Group
align="flex-start"
gap={12}
wrap="nowrap"
p="md"
<div
className={`flex items-start gap-3 rounded-[14px] border p-4 ${className ?? ""}`}
style={{
borderRadius: 14,
border: `1px solid ${palette.border}`,
borderColor: palette.border,
backgroundColor: palette.bg,
color: palette.color,
...style,
}}
>
<Box component="span" mt={2} style={{ flexShrink: 0 }}>
{icon}
</Box>
<Box miw={0}>
<span className="mt-0.5 shrink-0">{icon}</span>
<div className="min-w-0">
{title && (
<Text fz="13.5px" fw={800} c={palette.color}>
{title}
</Text>
)}
<Text fz="13px" c={palette.color} style={{ lineHeight: 1.45 }}>
<Text fz="13px" c={palette.color} className="leading-[1.45]">
{children}
</Text>
</Box>
</Group>
</div>
</div>
);
}
@@ -61,54 +54,35 @@ export function ActionRequiredBanner({
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",
}}
<div
className="flex flex-wrap items-center justify-between gap-6 rounded-2xl border border-[#F4D9A8] px-[22px] py-5"
style={{ backgroundColor: "#FDF3E0" }}
>
<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",
}}
<div className="flex min-w-0 flex-1 items-center gap-4">
<div
className="flex shrink-0 items-center justify-center rounded-xl border border-[#F4D9A8]"
style={{ width: 44, height: 44, backgroundColor: "#fff", color: "#C77F12" }}
>
<AlertTriangle size={23} />
</Box>
<Box miw={0}>
</div>
<div className="min-w-0">
<Text
fz="10.5px"
fw={800}
c="#B07A2A"
tt="uppercase"
style={{ letterSpacing: 0.6 }}
className="tracking-[0.6px]"
>
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 }}>
<Text mt={2} fz="13px" c="#7A6A4E" className="leading-[1.45]">
{children}
</Text>
</Box>
</Group>
</div>
</div>
{onAction && (
<Button
onClick={onAction}
@@ -123,7 +97,7 @@ export function ActionRequiredBanner({
Review & Resubmit
</Button>
)}
</Group>
</div>
);
}
@@ -145,44 +119,24 @@ export function CancelledBanner({
radius={16}
p={20}
bg="#FCEEEA"
style={{ border: "1px solid #F3C6BB" }}
className="border border-[#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",
}}
<div
className="flex shrink-0 items-center justify-center rounded-[13px] border border-[#F3C6BB]"
style={{ width: 46, height: 46, backgroundColor: "#fff", color: "#D93C15" }}
>
<XCircle size={24} />
</Box>
</div>
<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",
}}
<span
className="inline-flex rounded-full px-[10px] py-1 text-[10.5px] font-extrabold uppercase tracking-[0.3px] text-white"
style={{ backgroundColor: "#D93C15" }}
>
{pillLabel}
</Box>
</span>
<Text mt={6} fz="15.5px" fw={700} c="#10202F">
{title}
</Text>
@@ -206,33 +160,26 @@ export function CancelledBanner({
)}
</Group>
{reason && (
<Group
gap={11}
align="flex-start"
wrap="nowrap"
p={14}
style={{
borderRadius: 12,
backgroundColor: "#fff",
border: "1px solid #F3C6BB",
}}
<div
className="flex items-start gap-[11px] rounded-xl border border-[#F3C6BB] p-[14px]"
style={{ backgroundColor: "#fff" }}
>
<StickyNote size={20} color="#B0341A" style={{ flexShrink: 0 }} />
<Box miw={0}>
<StickyNote size={20} color="#B0341A" className="shrink-0" />
<div className="min-w-0">
<Text
fz="10.5px"
fw={700}
c="#B0341A"
tt="uppercase"
style={{ letterSpacing: 0.5 }}
className="tracking-[0.5px]"
>
Reason for cancellation
</Text>
<Text mt={3} fz="13.5px" c="#3D2A26" style={{ lineHeight: 1.45 }}>
<Text mt={3} fz="13.5px" c="#3D2A26" className="leading-[1.45]">
{reason}
</Text>
</Box>
</Group>
</div>
</div>
)}
</Stack>
</Paper>

View File

@@ -1,17 +1,36 @@
import { Box, Button, Group, Stack, Text } from "@mantine/core";
import { ArrowDownLeft, ArrowUpRight } from "lucide-react";
import { ActionIcon, Button, Group, Menu, Stack, Text } from "@mantine/core";
import {
ArrowDownLeft,
ArrowUpRight,
Edit2,
FileText,
HelpCircle,
MoreHorizontal,
RefreshCw,
XCircle,
} from "lucide-react";
import type { ReactNode } from "react";
import type { Freight } from "@edr/types";
import { bookingSubtitle, isDraftLike, isNegative } from "../utils";
export interface PageHeaderMenuActions {
onViewContract?: () => void;
onCancel?: () => void;
onEdit?: () => void;
onSupport?: () => void;
onRebook?: () => void;
}
export function PageHeader({
booking,
actions,
menuActions,
}: {
booking: Freight.IBooking;
actions: ReactNode;
actions?: ReactNode;
menuActions?: PageHeaderMenuActions;
}) {
const status = booking.status as string;
const negative = isNegative(status);
@@ -23,6 +42,8 @@ export function PageHeader({
const pillText = negative ? "#A93226" : draft ? "#475569" : "#0A6F4D";
const isExport = booking.tradeDirection === "EXPORT";
const hasMenu = menuActions && Object.values(menuActions).some(Boolean);
return (
<Group justify="space-between" align="flex-start" wrap="wrap" gap="md">
<Stack gap={8} miw={0}>
@@ -30,58 +51,90 @@ export function PageHeader({
<Text fz="26px" fw={800} c="#10202F">
{booking.reference}
</Text>
<Group
component="span"
gap={7}
align="center"
wrap="nowrap"
style={{
display: "inline-flex",
borderRadius: 999,
padding: "6px 12px",
fontSize: 12,
fontWeight: 700,
backgroundColor: pillBg,
border: `1px solid ${pillBorder}`,
color: pillText,
}}
<span
className="inline-flex items-center gap-[7px] rounded-full px-3 py-1.5 text-xs font-bold"
style={{ backgroundColor: pillBg, border: `1px solid ${pillBorder}`, color: pillText }}
>
<Box
component="span"
style={{
width: 7,
height: 7,
borderRadius: 999,
backgroundColor: dotColor,
}}
<span
className="shrink-0 rounded-full"
style={{ width: 7, height: 7, backgroundColor: dotColor }}
/>
{status.replace(/_/g, " ").replace(/\b\w/g, (m) => m.toUpperCase())}
</Group>
<Group
component="span"
gap={6}
align="center"
wrap="nowrap"
style={{
display: "inline-flex",
borderRadius: 999,
backgroundColor: "#F1F4F7",
padding: "6px 11px",
fontSize: 12,
fontWeight: 700,
color: "#475569",
}}
>
</span>
<span className="inline-flex items-center gap-[6px] rounded-full bg-[#F1F4F7] px-[11px] py-1.5 text-xs font-bold text-[#475569]">
{isExport ? <ArrowUpRight size={14} /> : <ArrowDownLeft size={14} />}
{isExport ? "Export" : "Import"}
</Group>
</span>
</Group>
<Text fz="14px" c="#6B7C8E">
{bookingSubtitle(booking)}
</Text>
</Stack>
<Group gap={12} wrap="wrap">
<Group gap={8} wrap="nowrap" align="center">
{actions}
{hasMenu && (
<Menu shadow="md" radius={12} position="bottom-end">
<Menu.Target>
<ActionIcon
variant="default"
size={42}
radius={10}
aria-label="More options"
>
<MoreHorizontal size={18} />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown miw={210}>
{menuActions!.onViewContract && (
<Menu.Item
leftSection={<FileText size={15} />}
onClick={menuActions!.onViewContract}
>
View contract
</Menu.Item>
)}
{menuActions!.onEdit && (
<Menu.Item
leftSection={<Edit2 size={15} />}
onClick={menuActions!.onEdit}
>
Edit
</Menu.Item>
)}
{menuActions!.onSupport && (
<Menu.Item
leftSection={<HelpCircle size={15} />}
onClick={menuActions!.onSupport}
>
Contact customer support
</Menu.Item>
)}
{menuActions!.onRebook && (
<Menu.Item
leftSection={<RefreshCw size={15} />}
onClick={menuActions!.onRebook}
>
Rebook similar schedule
</Menu.Item>
)}
{menuActions!.onCancel && (
<>
<Menu.Divider />
<Menu.Item
color="red"
leftSection={<XCircle size={15} />}
onClick={menuActions!.onCancel}
>
Cancel booking
</Menu.Item>
</>
)}
</Menu.Dropdown>
</Menu>
)}
</Group>
</Group>
);

View File

@@ -43,20 +43,12 @@ export function StatusHero({
<SectionCard p={28}>
<Group justify="space-between" align="center" wrap="wrap" gap="md">
<Group gap={16} align="center" wrap="nowrap">
<Box
style={{
width: 56,
height: 56,
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: 16,
backgroundColor: tileBg,
color: tileFg,
}}
<div
className="flex items-center justify-center rounded-2xl shrink-0"
style={{ width: 56, height: 56, backgroundColor: tileBg, color: tileFg }}
>
<HeroIcon size={26} />
</Box>
</div>
<Box>
<Text fz="21px" fw={800} c="#10202F">
{cfg.title}
@@ -72,11 +64,7 @@ export function StatusHero({
wrap="nowrap"
px="md"
py="sm"
style={{
borderRadius: 14,
border: "1px solid #E6ECF2",
backgroundColor: "#F7FAFC",
}}
className="rounded-[14px] border border-[#E6ECF2] bg-[#F7FAFC]"
>
<History size={22} color="#64748B" />
<Box>
@@ -85,7 +73,7 @@ export function StatusHero({
fw={700}
c="#9AA8B5"
tt="uppercase"
style={{ letterSpacing: 0.6 }}
className="tracking-[0.6px]"
>
{chipLabel}
</Text>
@@ -124,104 +112,97 @@ function ProgressTracker({
const activeSub = tone === "ink" ? "#475569" : "#0A6F4D";
return (
<Group align="flex-start" gap={0} wrap="nowrap" w="100%">
{PROGRESS_STAGES.map((stage, idx) => {
const state =
idx < current ? "done" : idx === current ? "active" : "idle";
const Icon = stage.icon;
const reachedLeft = current >= idx && current >= 0;
const reachedRight = current > idx && current >= 0;
/* Scrollable on mobile so 5 stages never overflow */
<Box
className="overflow-x-auto"
style={{ scrollbarWidth: "none", WebkitOverflowScrolling: "touch" } as React.CSSProperties}
>
<div className="flex items-start" style={{ minWidth: 440 }}>
{PROGRESS_STAGES.map((stage, idx) => {
const state =
idx < current ? "done" : idx === current ? "active" : "idle";
const Icon = stage.icon;
const reachedLeft = current >= idx && current >= 0;
const reachedRight = current > idx && current >= 0;
const circleStyle: React.CSSProperties =
state === "idle"
? { backgroundColor: "#EEF2F6", border: "1px solid #E1E7EE" }
: {
backgroundColor: state === "active" ? activeFill : "#0EA371",
boxShadow:
state === "active" ? `0 0 0 4px ${activeRing}` : undefined,
};
const circleBg =
state === "idle"
? "#EEF2F6"
: state === "active"
? activeFill
: "#0EA371";
const circleBorder = state === "idle" ? "1px solid #E1E7EE" : undefined;
const circleShadow =
state === "active" ? `0 0 0 4px ${activeRing}` : undefined;
return (
<Box
key={stage.label}
flex={1}
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 10,
}}
>
<Box style={{ display: "flex", alignItems: "center", width: "100%" }}>
<Box
flex={1}
style={{
height: 3,
borderRadius: 999,
background:
idx === 0
? "transparent"
: reachedLeft
? "#0EA371"
: "#E1E7EE",
}}
/>
<Box
style={{
width: 40,
height: 40,
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: 999,
...circleStyle,
}}
return (
<div
key={stage.label}
className="flex flex-1 flex-col items-center gap-[10px]"
>
<div className="flex w-full items-center">
{/* left connector */}
<div
className="flex-1 rounded-full"
style={{
height: 3,
background:
idx === 0 ? "transparent" : reachedLeft ? "#0EA371" : "#E1E7EE",
}}
/>
{/* stage circle */}
<div
className="flex items-center justify-center rounded-full shrink-0"
style={{
width: 40,
height: 40,
backgroundColor: circleBg,
border: circleBorder,
boxShadow: circleShadow,
}}
>
{state === "done" ? (
<Check size={16} color="#fff" />
) : state === "active" ? (
<Icon size={16} color="#fff" />
) : null}
</div>
{/* right connector */}
<div
className="flex-1 rounded-full"
style={{
height: 3,
background:
idx === last ? "transparent" : reachedRight ? "#0EA371" : "#E1E7EE",
}}
/>
</div>
<Text
fz="13.5px"
fw={state === "active" ? 800 : 700}
ta="center"
c={state === "idle" ? "#9AA8B5" : "#10202F"}
>
{state === "done" ? (
<Check size={16} color="#fff" />
) : state === "active" ? (
<Icon size={16} color="#fff" />
) : null}
</Box>
<Box
flex={1}
style={{
height: 3,
borderRadius: 999,
background:
idx === last
? "transparent"
: reachedRight
? "#0EA371"
: "#E1E7EE",
}}
/>
</Box>
<Text
fz="13.5px"
fw={state === "active" ? 800 : 700}
ta="center"
c={state === "idle" ? "#9AA8B5" : "#10202F"}
>
{stage.label}
</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>
</Box>
);
})}
</Group>
{stage.label}
</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>
</Box>
);
}

View File

@@ -18,14 +18,14 @@ export function BodyGrid({ left, right }: { left: ReactNode; right: ReactNode })
align={{ base: "stretch", lg: "flex-start" }}
gap={24}
>
<Box flex={1} miw={0} display="flex" style={{ flexDirection: "column", gap: 24 }}>
{/* Left column: capped at 700px on tablet so it doesn't bleed edge-to-edge */}
<Box flex={1} miw={0} maw={700} className="flex flex-col gap-6 lg:max-w-none">
{left}
</Box>
<Box
w={360}
maw="100%"
display="flex"
style={{ flexDirection: "column", gap: 24, flexShrink: 0 }}
className="flex flex-col gap-6 shrink-0"
>
{right}
</Box>