feat: setted up the schedule in the booking

This commit is contained in:
Nathnael
2026-06-16 07:18:59 +00:00
parent d1347892db
commit adba481f23
10 changed files with 655 additions and 2944 deletions

File diff suppressed because one or more lines are too long

View File

@@ -75,11 +75,11 @@ function RequireAuth() {
* Only redirects on a confirmed "no company" response — never on a
* transient query error.
*/
function RequireCompany() {
function RequireCompany({path}: {path: string}) {
const { customerQuery } = useAuth();
if (customerQuery.isPending) return <FullScreenSpinner />;
if (customerQuery.isSuccess && !customerQuery.data)
if (customerQuery.isSuccess && !customerQuery.data && path !== "/portal")
return <Navigate to="/onboarding" replace />;
return <Outlet />;
}
@@ -175,7 +175,7 @@ const App = () => {
<Route path="/onboarding" element={<OnboardingPage />} />
</Route>
<Route element={<RequireCompany />}>
<Route element={<RequireCompany path={location.pathname} />}>
<Route
element={
<AppLayout

View File

@@ -1,15 +1,14 @@
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { api } from "@/services/api";
import type {
LoginPayload,
LoginResponse,
OtpResponse,
SignupPayload,
SignupResponse,
OtpResponse,
} from "@/types/auth";
import type { Result } from "@/utils/result";
import { extractApiError } from "@/utils/result";
import { useEffect } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
function setCookie(name: string, value: string, days: number) {
const expires = new Date();
@@ -32,6 +31,8 @@ const useAuth = () => {
api.auth.getMyInfo.queryOptions({
enabled: hasToken,
retry: false,
refetchOnMount: false,
refetchOnReconnect: false,
staleTime: 10 * 60 * 1000,
refetchOnWindowFocus: false,
}),
@@ -46,13 +47,6 @@ const useAuth = () => {
}),
);
useEffect(() => {
if (authQuery.isError) {
queryClient.clear();
localStorage.clear();
}
}, [authQuery.isError, queryClient]);
const isPending = authQuery.isPending && hasToken;
const isAuthenticated = hasToken && !!authQuery.data && !authQuery.isError;

View File

@@ -1,4 +1,5 @@
import {
Alert,
Box,
Grid,
Group,
@@ -255,6 +256,17 @@ export default function MyPortalPage() {
</Group>
</Link>
</Group>
<Alert>
<Text fz={13} c="edr-muted">
Setup your Company Profile to start booking shipments and managing invoices.
<Link to="/onboarding" className="font-medium text-edr-green.7">
Get started
</Link>
.
</Text>
</Alert>
{/* ── Stats Strip ───────────────────────────────────────────────────── */}
<Box className="rounded-2xl border border-edr-border bg-gradient-to-br from-white to-edr-soft px-7 py-5">

View File

@@ -7,7 +7,6 @@ import { AlertCircle, Check, ChevronLeft, ChevronRight } from "lucide-react";
import { useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom";
import { DevTool } from "@hookform/devtools";
import {
BookingFormInputValues,
STEPS,
@@ -23,9 +22,9 @@ import {
Step2ServiceType,
Step4Route,
Step5CargoDetails,
StepScheduling,
StepDocuments,
Step8Review,
StepDocuments,
StepScheduling,
} from "./new-booking-form/steps";
export default function NewBookingPage() {
@@ -72,11 +71,15 @@ export default function NewBookingPage() {
const destinationYard = form.watch("destinationYard");
const direction = useMemo(
() =>
getRouteDirection(
referenceData?.yard.find((y) => y.id === originYard),
referenceData?.yard.find((y) => y.id === destinationYard),
),
() =>{
const origin = referenceData?.yard.find((y) => y.id === originYard);
const destination = referenceData?.yard.find((y) => y.id === destinationYard);
const route = getRouteDirection(
origin,destination
)
return route
},
[originYard, destinationYard],
);

View File

@@ -289,13 +289,13 @@ export function getRouteDirection(
dest: Freight.BookingReferenceYard | null | undefined,
): Freight.ScheduleTradeDirection | null {
if (!origin || !dest) return null;
if (origin.country === "ethiopia" && dest.country === "ethiopia") {
if (origin.country === "Ethiopia" && dest.country === "Ethiopia") {
return "DOMESTIC";
}
if (origin.country === "ethiopia" && dest.country === "djibouti") {
if (origin.country === "Ethiopia" && dest.country === "Djibouti") {
return "IMPORT";
}
if (origin.country === "djibouti" && dest.country === "ethiopia") {
if (origin.country === "Djibouti" && dest.country === "Ethiopia") {
return "EXPORT";
}

View File

@@ -2,24 +2,24 @@ import {
Box,
Button,
Card,
Grid,
Group,
Stack,
Text,
Title,
useMantineTheme,
Divider,
} from "@mantine/core";
import { UseFormReturn } from "react-hook-form";
import { BookingFormInputValues, BookingFormValues } from "./schema";
import {
ChevronLeft,
ChevronRight,
Info,
Check,
Train,
Route,
Package,
Calendar as CalendarIcon,
} from "lucide-react";
import type { Freight } from "@edr/types";
import { useMemo, useState } from "react";
import React, { useMemo, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { api } from "@/services/api";
import {
@@ -39,25 +39,37 @@ interface StepSchedulingProps {
referenceData?: Freight.BookingReferenceData;
}
interface DayData {
day: number;
dateString: string;
isToday: boolean;
isCurrentMonth: boolean;
isSelectedDate: boolean;
schedules: Freight.BookableScheduleItem[];
hasSchedule: boolean;
}
export function StepScheduling({ form, referenceData }: StepSchedulingProps) {
const theme = useMantineTheme();
const [currentDate, setCurrentDate] = useState(new Date());
const selectedDate = form.watch("scheduledDate");
const selectedScheduleId = form.watch("trainScheduleId");
const originYardId = form.watch("originYard");
const destinationYardId = form.watch("destinationYard");
const cargoType = form.watch("cargoType");
const containers = form.watch("containers");
const cargoTypePath = form.watch("cargoTypePath");
const cargoWeight = form.watch("cargoWeight");
const originName = useMemo(
() =>
referenceData?.yard.find((y) => y.id === originYardId)?.name ??
"Not selected",
() => referenceData?.yard.find((y) => y.id === originYardId)?.name ?? "—",
[referenceData, originYardId],
);
const destinationName = useMemo(
() =>
referenceData?.yard.find((y) => y.id === destinationYardId)?.name ??
"Not selected",
referenceData?.yard.find((y) => y.id === destinationYardId)?.name ?? "—",
[referenceData, destinationYardId],
);
@@ -68,19 +80,35 @@ export function StepScheduling({ form, referenceData }: StepSchedulingProps) {
}),
);
const scheduleMap = useMemo(() => {
const map = new Map<string, Freight.BookableScheduleItem>();
// Group all schedules per date — multiple departures per day are allowed.
// scheduleDate comes back as a full ISO timestamp; slice to "yyyy-MM-dd" to
// match the format used by the calendar day keys.
const schedulesByDate = useMemo(() => {
const map = new Map<string, Freight.BookableScheduleItem[]>();
if (bookableSchedules) {
for (const s of bookableSchedules) {
if (!map.has(s.scheduleDate)) {
map.set(s.scheduleDate, s);
}
const dateKey = s.scheduleDate.slice(0, 10);
const existing = map.get(dateKey) ?? [];
map.set(dateKey, [...existing, s]);
}
}
return map;
}, [bookableSchedules]);
const days = useMemo(() => {
const selectedSchedule = useMemo(
() => bookableSchedules?.find((s) => s.id === selectedScheduleId),
[bookableSchedules, selectedScheduleId],
);
const availableCount = useMemo(() => {
let count = 0;
schedulesByDate.forEach((schedules) => {
if (schedules.some((s) => s.remainingWagons > 0)) count++;
});
return count;
}, [schedulesByDate]);
const days = useMemo((): DayData[] => {
const monthStart = startOfMonth(currentDate);
const monthEnd = endOfMonth(currentDate);
const calStart = startOfWeek(monthStart, { weekStartsOn: 1 });
@@ -88,302 +116,449 @@ export function StepScheduling({ form, referenceData }: StepSchedulingProps) {
return eachDayOfInterval({ start: calStart, end: calEnd }).map((date) => {
const dateString = format(date, "yyyy-MM-dd");
const schedule = scheduleMap.get(dateString);
const schedules = schedulesByDate.get(dateString) ?? [];
return {
day: date.getDate(),
dateString,
isToday: isToday(date),
isSelected: selectedDate === dateString,
isCurrentMonth: isSameMonth(date, currentDate),
isFull: schedule ? schedule.remainingWagons <= 0 : false,
hasSchedule: !!schedule,
remainingWagons: schedule?.remainingWagons ?? 0,
scheduleId: schedule?.id ?? "",
isSelectedDate: selectedDate === dateString,
schedules,
hasSchedule: schedules.length > 0,
};
});
}, [currentDate, scheduleMap, selectedDate]);
}, [currentDate, schedulesByDate, selectedDate]);
const legendItems = [
{ label: "Available", color: theme.colors.gray[1] },
{ label: "Full", color: theme.colors["edr-red-soft"][0] },
{ label: "No Service", color: "transparent" },
{ label: "Selected", color: theme.colors["edr-green"][5] },
];
const cargoSummary = useMemo(() => {
if (!cargoType) return "Not selected";
if (cargoType === "container") {
const parts = (containers ?? [])
.filter((c) => Number(c.qty) > 0)
.map((c) => `${c.qty} × ${c.type}`);
return parts.length > 0 ? `Container · ${parts.join(", ")}` : "Container";
}
const childId = cargoTypePath?.[1];
const commodity = referenceData?.cargo_type
.flatMap((g) => g.children ?? [])
.find((c) => c.id === childId);
const weight = cargoWeight ? ` · ${cargoWeight} t` : "";
return commodity ? `${commodity.name}${weight}` : "Bulk freight";
}, [cargoType, containers, cargoTypePath, cargoWeight, referenceData]);
const weeksCount = Math.ceil(days.length / 7);
return (
<Stack gap="xl">
<Grid>
<Grid.Col span={{ base: 12, md: 7.5 }}>
<Card>
<Box>
<Title order={3} fw={700}>
Shipment Date
</Title>
<Text c="edr-muted" size="sm" mt={4}>
Pick a shipment date for your booking from the available
schedule.
<Group gap={24} align="flex-start" wrap="nowrap">
{/* ── Calendar Card ───────────────────────────────────── */}
<Box style={{ flex: 1, minWidth: 0 }}>
<Card p={0} style={{ overflow: "hidden" }}>
{/* Card header */}
<Group
justify="space-between"
align="center"
px={24}
py={20}
style={{
borderBottom: `1px solid ${theme.colors["edr-border"][0]}`,
}}
>
<Stack gap={3}>
<Text fw={800} fz={18} c="edr-text.0">
Select a shipment date
</Text>
<Text fz={13} c="edr-muted">
Confirmed train departures · {originName} {destinationName}
</Text>
</Stack>
<Group gap={10}>
<Button
variant="default"
w={34}
h={34}
p={0}
radius="xl"
onClick={() => setCurrentDate((d) => addMonths(d, -1))}
>
<ChevronLeft size={16} />
</Button>
<Text fw={700} fz={14} c="edr-text.0" style={{ minWidth: 90, textAlign: "center" }}>
{format(currentDate, "MMMM yyyy")}
</Text>
<Button
variant="default"
w={34}
h={34}
p={0}
radius="xl"
onClick={() => setCurrentDate((d) => addMonths(d, 1))}
>
<ChevronRight size={16} />
</Button>
</Group>
</Group>
{/* Card body */}
<Stack gap={14} px={24} py={18}>
<Text fz={13} fw={600} c="edr-text.0">
{originYardId && destinationYardId
? `${availableCount} available departure${availableCount !== 1 ? "s" : ""} in ${format(currentDate, "MMMM")} — pick one to continue`
: "Select origin and destination to see available departures"}
</Text>
{/* Weekday headers */}
<Box
style={{
display: "grid",
gridTemplateColumns: "repeat(7, 1fr)",
gap: 6,
paddingBottom: 8,
}}
>
{["MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"].map((d) => (
<Text
key={d}
ta="center"
fz={10.5}
fw={700}
c="edr-muted"
style={{ letterSpacing: "0.06em" }}
>
{d}
</Text>
))}
</Box>
<Stack gap="md">
<Group justify="space-between" align="center">
<Stack gap={0}>
<Text fw={700} size="xl">
{format(currentDate, "MMMM yyyy")}
</Text>
</Stack>
<Group gap="xs">
<Button
variant="default"
size="sm"
p={6}
radius="md"
onClick={() => setCurrentDate((d) => addMonths(d, -1))}
>
<ChevronLeft size={18} />
</Button>
<Button
variant="default"
size="sm"
p={6}
radius="md"
onClick={() => setCurrentDate((d) => addMonths(d, 1))}
>
<ChevronRight size={18} />
</Button>
</Group>
</Group>
<Box
style={{
display: "grid",
gap: "4px",
gridTemplateColumns: "repeat(7, 1fr)",
}}
>
{["MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"].map((d) => (
<Text
key={d}
ta="center"
size="xs"
fw={700}
c="edr-muted"
mb={8}
>
{d}
</Text>
))}
{days.map((d, i) => {
const canSelect =
d.isCurrentMonth && d.hasSchedule && !d.isFull;
return (
<Button
key={i}
variant="unstyled"
disabled={!canSelect}
onClick={() => {
form.setValue("scheduledDate", d.dateString, {
{/* Date grid */}
<Stack gap={2}>
{Array.from({ length: weeksCount }, (_, wi) => (
<Box
key={wi}
style={{
display: "grid",
gridTemplateColumns: "repeat(7, 1fr)",
gap: 2,
}}
>
{days.slice(wi * 7, wi * 7 + 7).map((d, di) => (
<DayCell
key={di}
day={d}
selectedScheduleId={selectedScheduleId}
onSelectSchedule={(scheduleId, dateString) => {
form.setValue("scheduledDate", dateString, {
shouldValidate: true,
});
form.setValue("trainScheduleId", d.scheduleId, {
form.setValue("trainScheduleId", scheduleId, {
shouldValidate: true,
});
}}
style={{
height: "80px",
display: "flex",
flexDirection: "column",
alignItems: "start",
justifyContent: "start",
gap: "4px",
borderRadius: theme.radius.md,
cursor: canSelect ? "pointer" : "default",
backgroundColor: d.isSelected
? theme.colors["edr-green"][5]
: d.isFull
? theme.colors["edr-red-soft"][0]
: d.hasSchedule
? theme.colors.gray[0]
: "transparent",
border:
d.isToday && !d.isSelected
? `2px solid ${theme.colors["edr-green"][5]}`
: "none",
opacity: d.isCurrentMonth ? 1 : 0,
}}
>
<Text
size="sm"
fw={700}
c={d.isSelected ? "white" : "edr-text.0"}
>
{d.day}
</Text>
{d.isCurrentMonth && !d.isSelected && (
<>
{d.isFull && (
<Text
size="9px"
fw={800}
c="edr-red.0"
style={{ letterSpacing: "0.05em" }}
>
FULL
</Text>
)}
{d.hasSchedule && !d.isFull && (
<Text
size="9px"
fw={800}
c="edr-green.6"
style={{ letterSpacing: "0.05em" }}
>
{d.remainingWagons} WGN
</Text>
)}
</>
)}
</Button>
);
})}
</Box>
<Group gap="xl" mt="xs">
{legendItems.map((item) => (
<Group key={item.label} gap={8}>
<Box
style={{
width: 14,
height: 14,
borderRadius: 4,
backgroundColor: item.color,
border:
item.label === "No Service"
? `2px dashed ${theme.colors.gray[3]}`
: "none",
}}
/>
<Text size="xs" c="edr-muted" fw={600}>
{item.label}
))}
</Box>
))}
</Stack>
</Stack>
</Card>
</Box>
{/* ── Side Panel ──────────────────────────────────────── */}
<Stack gap={20} w={340} style={{ flexShrink: 0 }}>
{/* Booking summary card */}
<Card p={0} style={{ overflow: "hidden" }}>
<Group
justify="space-between"
align="center"
px={20}
py={18}
style={{
borderBottom: `1px solid ${theme.colors["edr-border"][0]}`,
}}
>
<Text fw={800} fz={15} c="edr-text.0">
Booking summary
</Text>
</Group>
<Stack gap={14} px={20} py={18}>
<SummaryRow
icon={<Route size={17} color={theme.colors["edr-muted"][0]} />}
label="ROUTE"
value={`${originName}${destinationName}`}
/>
<SummaryRow
icon={<Package size={17} color={theme.colors["edr-muted"][0]} />}
label="CARGO"
value={cargoSummary}
/>
{selectedSchedule && selectedDate && (
<Box
p={14}
style={{
borderRadius: theme.radius.lg,
backgroundColor: theme.colors["edr-soft"][0],
}}
>
<Stack gap={10}>
<Group gap={7}>
<Train size={16} color={theme.colors["edr-green"][7]} />
<Text
fz={10}
fw={700}
c="edr-green.7"
style={{ letterSpacing: "0.08em" }}
>
SELECTED DEPARTURE
</Text>
</Group>
))}
</Group>
</Stack>
</Card>
</Grid.Col>
<Grid.Col span={{ base: 12, md: 4.5 }}>
<Card>
<Stack gap="xl">
<Title order={4} fw={800} style={{ letterSpacing: "-0.02em" }}>
Booking Summary
</Title>
<Stack gap="md">
<SummaryRow label="Origin Yard" value={originName} />
<SummaryRow label="Destination Yard" value={destinationName} />
<SummaryRow
label="Cargo Type"
value={
cargoType
? cargoType === "container"
? "Container Freight"
: "Bulk Freight"
: "Not selected"
}
/>
<Divider my="sm" color="gray.2" />
<Group justify="space-between" align="center">
<Stack gap={2}>
<Text
size="xs"
c="edr-muted"
fw={600}
style={{ letterSpacing: "0.05em" }}
>
Shipment Date
<Text fw={800} fz={16} c="edr-text.0">
{format(new Date(selectedDate + "T00:00:00"), "EEE, MMM d yyyy")}
</Text>
<Group justify="space-between">
<Text fz={12.5} c="edr-muted">
Train
</Text>
<Text
fw={800}
size="lg"
c={selectedDate ? "edr-green.6" : "edr-muted"}
>
{selectedDate || "Not selected"}
<Text fz={12.5} fw={700} c="edr-text.0">
{selectedSchedule.trainNumber ?? selectedSchedule.id.slice(0, 8)}
</Text>
</Stack>
<Box
p={8}
style={{
borderRadius: theme.radius.md,
backgroundColor: selectedDate
? theme.colors["edr-green"][0]
: theme.colors.gray[1],
}}
>
<CalendarIcon
size={20}
color={
selectedDate
? theme.colors["edr-green"][6]
: theme.colors["edr-muted"][0]
}
/>
</Box>
</Group>
</Stack>
</Group>
<Group justify="space-between">
<Text fz={12.5} c="edr-muted">
Wagons available
</Text>
<Text fz={12.5} fw={700} c="edr-text.0">
{selectedSchedule.remainingWagons} / {selectedSchedule.maxWagons}
</Text>
</Group>
</Stack>
</Box>
)}
</Stack>
</Card>
{/* Help card */}
<Box
p={18}
style={{
borderRadius: theme.radius.xl,
backgroundColor: theme.colors["edr-ink"][0],
}}
>
<Stack gap={8}>
<Group gap={8} align="center">
<CalendarIcon size={18} color={theme.colors["edr-green"][5]} />
<Text fw={700} fz={14} c="white">
Need a different date?
</Text>
</Group>
<Text fz={12.5} style={{ color: "#A7B6C2", lineHeight: 1.5 }}>
Our freight desk can arrange charter departures for full-train loads.
</Text>
<Text fz={13} fw={700} c="edr-green.5" style={{ cursor: "pointer" }}>
Contact freight desk
</Text>
</Stack>
</Box>
</Stack>
</Group>
);
}
interface DayCellProps {
day: DayData;
selectedScheduleId: string;
onSelectSchedule: (scheduleId: string, dateString: string) => void;
}
function DayCell({ day: d, selectedScheduleId, onSelectSchedule }: DayCellProps) {
const theme = useMantineTheme();
if (!d.isCurrentMonth) {
return (
<Box
style={{
height: 92,
borderRadius: theme.radius.md,
opacity: 0.4,
overflow: "hidden",
padding: "4px 6px",
backgroundColor: "transparent",
}}
>
<Text fz={14} fw={600} c="edr-muted">
{d.day}
</Text>
</Box>
);
}
const cellBg = d.isSelectedDate
? theme.colors["edr-soft"][0]
: d.hasSchedule
? "#FFFFFF"
: "transparent";
const cellBorder = d.isSelectedDate
? `1.5px solid ${theme.colors["edr-green"][5]}`
: d.hasSchedule
? "1px solid #E7E8E5"
: "none";
return (
<Box
style={{
height: 92,
borderRadius: theme.radius.md,
backgroundColor: cellBg,
border: cellBorder,
overflow: "hidden",
padding: "4px 6px 6px",
display: "flex",
flexDirection: "column",
gap: 4,
}}
>
{/* Day number + check icon */}
<Group justify="space-between" align="center" style={{ flexShrink: 0 }}>
<Text
fz={14}
fw={d.hasSchedule ? 800 : 600}
c={
d.isToday && !d.isSelectedDate
? "edr-green.6"
: d.hasSchedule
? "edr-text.0"
: "edr-muted"
}
>
{d.day}
</Text>
{d.isSelectedDate && (
<Check
size={15}
color={theme.colors["edr-green"][5]}
strokeWidth={2.5}
/>
)}
</Group>
{/* Departure chips */}
{d.hasSchedule && (
<Stack gap={3} style={{ flex: 1, overflow: "hidden" }}>
{d.schedules.slice(0, 2).map((s) => {
const isChipSelected = s.id === selectedScheduleId;
const isFull = s.remainingWagons <= 0;
const canSelect = !isFull;
return (
<Box
p="md"
key={s.id}
onClick={() =>
canSelect && onSelectSchedule(s.id, d.dateString)
}
style={{
borderRadius: theme.radius.md,
border: `1px dashed ${theme.colors.gray[4]}`,
backgroundColor: "white",
display: "flex",
alignItems: "center",
gap: 4,
borderRadius: 7,
padding: "4px 6px",
cursor: canSelect ? "pointer" : "default",
backgroundColor: isChipSelected
? theme.colors["edr-green"][5]
: isFull
? theme.colors["edr-red-soft"][0]
: theme.colors["edr-soft"][0],
border: `1px solid ${
isChipSelected
? theme.colors["edr-green"][5]
: isFull
? "#EFCFCA"
: "#BFE3D4"
}`,
}}
>
<Group gap="xs" align="flex-start" wrap="nowrap">
<Info
size={16}
color={theme.colors["edr-muted"][0]}
style={{ flexShrink: 0, marginTop: 2 }}
/>
<Text
size="xs"
c="edr-muted"
fw={500}
style={{ lineHeight: 1.5 }}
>
Final confirmation of your selected date will be provided
after review of your booking details.
</Text>
</Group>
<Box
style={{
width: 6,
height: 6,
borderRadius: "50%",
backgroundColor: isChipSelected
? "white"
: isFull
? theme.colors["edr-red"][0]
: theme.colors["edr-green"][5],
flexShrink: 0,
}}
/>
<Text
fz={11}
fw={700}
style={{ flex: 1, minWidth: 0, overflow: "hidden" }}
truncate
c={
isChipSelected ? "white" : isFull ? "edr-red.0" : "edr-green.7"
}
>
{isFull
? "Full"
: s.trainNumber
? s.trainNumber
: `${s.remainingWagons} wgn`}
</Text>
<ChevronRight
size={12}
color={
isChipSelected
? "white"
: isFull
? theme.colors["edr-muted"][0]
: theme.colors["edr-green"][7]
}
/>
</Box>
</Stack>
</Card>
</Grid.Col>
</Grid>
</Stack>
);
})}
</Stack>
)}
</Box>
);
}
function SummaryRow({ label, value }: { label: string; value: string }) {
function SummaryRow({
icon,
label,
value,
}: {
icon: React.ReactNode;
label: string;
value: string;
}) {
const theme = useMantineTheme();
return (
<Stack gap={4}>
<Text
size="xs"
c="edr-muted"
fw={600}
textTransform="uppercase"
style={{ letterSpacing: "0.05em" }}
<Group gap={12} align="flex-start" wrap="nowrap">
<Box
w={34}
h={34}
style={{
borderRadius: theme.radius.md,
backgroundColor: theme.colors.gray[0],
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
}}
>
{label}
</Text>
<Text size="sm" fw={700} c="edr-text.0">
{value}
</Text>
</Stack>
{icon}
</Box>
<Stack gap={1} style={{ flex: 1, minWidth: 0 }}>
<Text fz={10} fw={700} style={{ color: "#9AA8B5", letterSpacing: "0.08em" }}>
{label}
</Text>
<Text fz={13} fw={600} c="edr-text.0">
{value}
</Text>
</Stack>
</Group>
);
}

View File

@@ -1,7 +1,7 @@
import { Switch, TextInput } from "@mantine/core";
import { FileText, Train, Truck } from "lucide-react";
import { useEffect, useRef } from "react";
import { Controller, type UseFormReturn } from "react-hook-form";
import { FileText, Train, Truck } from "lucide-react";
import { Switch, TextInput } from "@mantine/core";
import { BookingFormInputValues, type BookingFormValues } from "./schema";
import { OptionCard, OptionFieldError, StepHeader } from "./shared";

View File

@@ -1,7 +1,7 @@
import type { BaseEntity } from "../common";
export * from "./file_upload_settings";
export * from "./dropdown_settings";
export * from "./file_upload_settings";
export * from "./overview";
export enum TradeDirection {

2804
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff