Merge branch 'freight/feat/booking-schedule' into freight/develop

This commit is contained in:
ghost2023
2026-06-16 12:39:56 +03:00
35 changed files with 2648 additions and 963 deletions

View File

@@ -13,6 +13,8 @@ const statusColorMap: Record<string, string> = {
FULLY_EXECUTED: "indigo",
PNR_GENERATED: "violet",
PAYMENT_VERIFICATION_IN_PROGRESS: "yellow",
SELECTED_FOR_BATCH: "orange",
EXPIRED: "red",
PAID: "green",
IN_TRANSIT: "cyan",
COMPLETED: "indigo",

View File

@@ -0,0 +1,92 @@
import { useEffect, useState } from "react";
import { Group, Stack, Text } from "@mantine/core";
import { Timer } from "lucide-react";
import { SectionCard } from "./SectionCard";
export interface BookingPaymentCountdownCardProps {
/** ISO timestamp marking the end of the pay window. */
paymentDeadline: string;
}
interface Remaining {
days: number;
hours: number;
minutes: number;
seconds: number;
expired: boolean;
}
function getRemaining(deadlineMs: number): Remaining {
const diff = deadlineMs - Date.now();
if (diff <= 0) {
return { days: 0, hours: 0, minutes: 0, seconds: 0, expired: true };
}
const totalSeconds = Math.floor(diff / 1000);
return {
days: Math.floor(totalSeconds / 86400),
hours: Math.floor((totalSeconds % 86400) / 3600),
minutes: Math.floor((totalSeconds % 3600) / 60),
seconds: totalSeconds % 60,
expired: false,
};
}
function Segment({ value, label }: { value: number; label: string }) {
return (
<Stack gap={2} align="center" style={{ minWidth: 56 }}>
<Text fw={700} size="2rem" style={{ lineHeight: 1, fontVariantNumeric: "tabular-nums" }}>
{String(value).padStart(2, "0")}
</Text>
<Text size="xs" c="dimmed" tt="uppercase" lts="0.06em">
{label}
</Text>
</Stack>
);
}
/** Live countdown to the payment deadline. Ticks every second; shows an expired state past the deadline. */
export function BookingPaymentCountdownCard({ paymentDeadline }: BookingPaymentCountdownCardProps) {
const deadlineMs = new Date(paymentDeadline).getTime();
const [remaining, setRemaining] = useState<Remaining>(() => getRemaining(deadlineMs));
useEffect(() => {
setRemaining(getRemaining(deadlineMs));
const interval = setInterval(() => {
const next = getRemaining(deadlineMs);
setRemaining(next);
if (next.expired) {
clearInterval(interval);
}
}, 1000);
return () => clearInterval(interval);
}, [deadlineMs]);
const accent = remaining.expired ? "red" : "orange";
return (
<SectionCard
icon={Timer}
title="Payment Deadline"
subtitle={
remaining.expired
? "The pay window has closed"
: "Time remaining to complete payment"
}
accent={accent}
>
{remaining.expired ? (
<Text fw={600} c="red.7">
Expired
</Text>
) : (
<Group justify="center" gap="lg" wrap="nowrap">
<Segment value={remaining.days} label="Days" />
<Segment value={remaining.hours} label="Hours" />
<Segment value={remaining.minutes} label="Mins" />
<Segment value={remaining.seconds} label="Secs" />
</Group>
)}
</SectionCard>
);
}

View File

@@ -137,6 +137,8 @@ export interface BookingDetailView {
priorityScore: number;
cargoTotalWeightVgm: number;
pnrCode?: string | null;
/** End of the pay window once the booking is SELECTED_FOR_BATCH. */
paymentDeadline?: string | null;
createdAt: string;
updatedAt: string;
company?: BookingNamedRefView;

View File

@@ -9,6 +9,7 @@ export * from "./BookingContainersCard";
export * from "./BookingApprovalCard";
export * from "./BookingReviewNotesCard";
export * from "./BookingPaymentCard";
export * from "./BookingPaymentCountdownCard";
export * from "./BookingFactsCard";
export * from "./BookingDocumentsCard";
export * from "./BookingRequestHero";

View File

@@ -50,6 +50,14 @@ export const BOOKING_STATUS_STYLES: Record<string, StatusStyle> = {
label: "Payment Verification",
color: "bg-amber-50 text-amber-800 border-amber-200",
},
SELECTED_FOR_BATCH: {
label: "Selected for Batch",
color: "bg-orange-50 text-orange-700 border-orange-200",
},
EXPIRED: {
label: "Expired",
color: "bg-red-50 text-red-700 border-red-200",
},
PAID: {
label: "Paid",
color: "bg-[color:var(--freight-brand-muted)] text-[color:var(--freight-brand)] border-[color:var(--freight-brand-border)]",
@@ -241,6 +249,8 @@ export const BOOKING_LIST_TABS = [
"FULLY_EXECUTED",
"PNR_GENERATED",
"PAYMENT_VERIFICATION_IN_PROGRESS",
"SELECTED_FOR_BATCH",
"EXPIRED",
],
},
{
@@ -270,6 +280,8 @@ export const WORKFLOW_STAGES = [
"FULLY_EXECUTED",
"PNR_GENERATED",
"PAYMENT_VERIFICATION_IN_PROGRESS",
"SELECTED_FOR_BATCH",
"EXPIRED",
],
},
{

View File

@@ -1,21 +1,21 @@
import { useParams, useNavigate } from "react-router-dom";
import { Container, Stack, Grid } from "@mantine/core";
import { Container, Grid, Stack } from "@mantine/core";
import { useNavigate, useParams } from "react-router-dom";
import Breadcrumbs from "@/components/ui/Breadcrumbs";
import {
detailStyles,
type BookingDetailView,
BookingDetailToolbar,
BookingDetailHeader,
BookingLifecycleStepper,
BookingRouteCard,
BookingContainersCard,
BookingApprovalCard,
BookingReviewNotesCard,
BookingPaymentCard,
BookingFactsCard,
BookingContainersCard,
BookingDetailToolbar,
BookingDocumentsCard,
BookingFactsCard,
BookingLifecycleStepper,
BookingPaymentCard,
BookingPaymentCountdownCard,
BookingReviewNotesCard,
BookingRouteCard,
detailStyles,
type BookingDetailView
} from "@/components/bookings/detail";
import Breadcrumbs from "@/components/ui/Breadcrumbs";
const BookingDetailPage = () => {
const { id } = useParams<{ id: string }>();
@@ -25,8 +25,9 @@ const BookingDetailPage = () => {
const booking: BookingDetailView = {
id: id || "a61955b7-af21-4664-84d3-7e4e66293b6f",
reference: "BKG-2026-001456",
status: "IN_TRANSIT",
status: "SELECTED_FOR_BATCH",
scheduledDate: "2026-06-15",
paymentDeadline: "2026-06-18T17:00:00Z",
totalAmount: 15750.5,
paymentCurrency: "USD",
paymentStatus: "PAID",
@@ -138,6 +139,9 @@ const BookingDetailPage = () => {
{/* RIGHT — summary sidebar */}
<Grid.Col span={{ base: 12, lg: 4 }}>
<Stack gap="lg">
{booking.status === "SELECTED_FOR_BATCH" && booking.paymentDeadline && (
<BookingPaymentCountdownCard paymentDeadline={booking.paymentDeadline} />
)}
<BookingPaymentCard
totalAmount={booking.totalAmount}
currency={booking.paymentCurrency}