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

View File

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

View File

@@ -1,17 +1,36 @@
import { Box, Button, Group, Stack, Text } from "@mantine/core"; import { ActionIcon, Button, Group, Menu, Stack, Text } from "@mantine/core";
import { ArrowDownLeft, ArrowUpRight } from "lucide-react"; import {
ArrowDownLeft,
ArrowUpRight,
Edit2,
FileText,
HelpCircle,
MoreHorizontal,
RefreshCw,
XCircle,
} from "lucide-react";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import type { Freight } from "@edr/types"; import type { Freight } from "@edr/types";
import { bookingSubtitle, isDraftLike, isNegative } from "../utils"; import { bookingSubtitle, isDraftLike, isNegative } from "../utils";
export interface PageHeaderMenuActions {
onViewContract?: () => void;
onCancel?: () => void;
onEdit?: () => void;
onSupport?: () => void;
onRebook?: () => void;
}
export function PageHeader({ export function PageHeader({
booking, booking,
actions, actions,
menuActions,
}: { }: {
booking: Freight.IBooking; booking: Freight.IBooking;
actions: ReactNode; actions?: ReactNode;
menuActions?: PageHeaderMenuActions;
}) { }) {
const status = booking.status as string; const status = booking.status as string;
const negative = isNegative(status); const negative = isNegative(status);
@@ -23,6 +42,8 @@ export function PageHeader({
const pillText = negative ? "#A93226" : draft ? "#475569" : "#0A6F4D"; const pillText = negative ? "#A93226" : draft ? "#475569" : "#0A6F4D";
const isExport = booking.tradeDirection === "EXPORT"; const isExport = booking.tradeDirection === "EXPORT";
const hasMenu = menuActions && Object.values(menuActions).some(Boolean);
return ( return (
<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}>
@@ -30,58 +51,90 @@ export function PageHeader({
<Text fz="26px" fw={800} c="#10202F"> <Text fz="26px" fw={800} c="#10202F">
{booking.reference} {booking.reference}
</Text> </Text>
<Group
component="span" <span
gap={7} className="inline-flex items-center gap-[7px] rounded-full px-3 py-1.5 text-xs font-bold"
align="center" style={{ backgroundColor: pillBg, border: `1px solid ${pillBorder}`, color: pillText }}
wrap="nowrap"
style={{
display: "inline-flex",
borderRadius: 999,
padding: "6px 12px",
fontSize: 12,
fontWeight: 700,
backgroundColor: pillBg,
border: `1px solid ${pillBorder}`,
color: pillText,
}}
> >
<Box <span
component="span" className="shrink-0 rounded-full"
style={{ style={{ width: 7, height: 7, backgroundColor: dotColor }}
width: 7,
height: 7,
borderRadius: 999,
backgroundColor: dotColor,
}}
/> />
{status.replace(/_/g, " ").replace(/\b\w/g, (m) => m.toUpperCase())} {status.replace(/_/g, " ").replace(/\b\w/g, (m) => m.toUpperCase())}
</Group> </span>
<Group
component="span" <span className="inline-flex items-center gap-[6px] rounded-full bg-[#F1F4F7] px-[11px] py-1.5 text-xs font-bold text-[#475569]">
gap={6}
align="center"
wrap="nowrap"
style={{
display: "inline-flex",
borderRadius: 999,
backgroundColor: "#F1F4F7",
padding: "6px 11px",
fontSize: 12,
fontWeight: 700,
color: "#475569",
}}
>
{isExport ? <ArrowUpRight size={14} /> : <ArrowDownLeft size={14} />} {isExport ? <ArrowUpRight size={14} /> : <ArrowDownLeft size={14} />}
{isExport ? "Export" : "Import"} {isExport ? "Export" : "Import"}
</Group> </span>
</Group> </Group>
<Text fz="14px" c="#6B7C8E"> <Text fz="14px" c="#6B7C8E">
{bookingSubtitle(booking)} {bookingSubtitle(booking)}
</Text> </Text>
</Stack> </Stack>
<Group gap={12} wrap="wrap">
<Group gap={8} wrap="nowrap" align="center">
{actions} {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>
</Group> </Group>
); );

View File

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

View File

@@ -18,14 +18,14 @@ export function BodyGrid({ left, right }: { left: ReactNode; right: ReactNode })
align={{ base: "stretch", lg: "flex-start" }} align={{ base: "stretch", lg: "flex-start" }}
gap={24} 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} {left}
</Box> </Box>
<Box <Box
w={360} w={360}
maw="100%" maw="100%"
display="flex" className="flex flex-col gap-6 shrink-0"
style={{ flexDirection: "column", gap: 24, flexShrink: 0 }}
> >
{right} {right}
</Box> </Box>