add booking request functionality for GENERAL customs contracts

- Create migration for booking_requests table with necessary fields and indexes.
- Implement BookingRequestRepository for database operations related to booking requests.
- Develop BookingRequestService to handle business logic for submitting, accepting, rejecting, and canceling booking requests.
- Create DTOs for creating booking requests and reviewing them.
- Define BookingRequest entity to map to the booking_requests table.
- Add UI components for managing shipment requests, including detail and list pages.
- Implement OperationDatePicker component for selecting available shipment days.
This commit is contained in:
Marshal
2026-06-29 09:30:44 +00:00
parent aeb5e0046e
commit 0f7cac2b68
46 changed files with 2665 additions and 992 deletions

View File

@@ -0,0 +1,248 @@
import { Box, Button, Group, Text } from "@mantine/core";
import {
Calendar as CalendarIcon,
Check,
ChevronLeft,
ChevronRight,
} from "lucide-react";
import { useMemo, useState } from "react";
export interface OperationDatePickerProps {
/** Selectable days as `yyyy-MM-dd` strings. */
availableDays: string[];
/** Show the loading state instead of the grid. */
isLoading?: boolean;
/** Currently selected day as `yyyy-MM-dd`, or "" when none. */
value: string;
/** Called with the picked `yyyy-MM-dd` day. */
onChange: (date: string) => void;
}
/** `yyyy-MM-dd` for a local date. */
function fmtDay(d: Date): string {
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
return `${y}-${m}-${day}`;
}
const MONTH_NAMES = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
/**
* Presentational month calendar for picking a binding shipment day. Only the
* `availableDays` (passed in by the caller, which owns the query) are
* selectable; every other day is disabled. Framework-light: no data fetching,
* no date library — both the portal and backoffice feed it their own
* availability results so the picker renders identically in each app.
*/
export function OperationDatePicker({
availableDays,
isLoading = false,
value,
onChange,
}: OperationDatePickerProps) {
// First-of-month for the visible month; defaults to the current month.
const [month, setMonth] = useState(() => {
const now = new Date();
return new Date(now.getFullYear(), now.getMonth(), 1);
});
const departureDays = useMemo(
() => new Set(availableDays ?? []),
[availableDays],
);
const cells = useMemo(() => {
const first = new Date(month.getFullYear(), month.getMonth(), 1);
// Monday-first grid: JS getDay() Sun=0..Sat=6 → shift so Mon=0.
const lead = (first.getDay() + 6) % 7;
const start = new Date(first);
start.setDate(first.getDate() - lead);
const today = new Date();
const todayStr = fmtDay(today);
return Array.from({ length: 42 }, (_, i) => {
const date = new Date(start);
date.setDate(start.getDate() + i);
const dateString = fmtDay(date);
return {
dateString,
day: date.getDate(),
inMonth: date.getMonth() === month.getMonth(),
today: dateString === todayStr,
selected: value === dateString,
hasDeparture: departureDays.has(dateString),
};
});
}, [month, departureDays, value]);
const shiftMonth = (delta: number) =>
setMonth((m) => new Date(m.getFullYear(), m.getMonth() + delta, 1));
return (
<Box
style={{
border: "1px solid #E6ECF2",
borderRadius: 12,
padding: 14,
maxWidth: 340,
}}
>
<Group justify="space-between" align="center" mb="sm">
<Button
variant="default"
size="xs"
px={6}
radius="xl"
onClick={() => shiftMonth(-1)}
>
<ChevronLeft size={15} />
</Button>
<Text fz="13px" fw={700} c="#10202F">
{MONTH_NAMES[month.getMonth()]} {month.getFullYear()}
</Text>
<Button
variant="default"
size="xs"
px={6}
radius="xl"
onClick={() => shiftMonth(1)}
>
<ChevronRight size={15} />
</Button>
</Group>
{isLoading ? (
<Group justify="center" py="md" gap={8}>
<CalendarIcon size={15} color="#9AA8B5" />
<Text fz="12px" c="dimmed">
Loading available days
</Text>
</Group>
) : (
<>
<Box
style={{
display: "grid",
gridTemplateColumns: "repeat(7, 1fr)",
gap: 4,
marginBottom: 6,
}}
>
{["M", "T", "W", "T", "F", "S", "S"].map((d, i) => (
<Text key={i} ta="center" fz="10px" fw={700} c="#9AA8B5">
{d}
</Text>
))}
</Box>
<Box
style={{
display: "grid",
gridTemplateColumns: "repeat(7, 1fr)",
gap: 4,
}}
>
{cells.map((c) => {
const clickable = c.hasDeparture && c.inMonth;
return (
<button
key={c.dateString}
type="button"
disabled={!clickable}
onClick={() => clickable && onChange(c.dateString)}
style={{
position: "relative",
height: 34,
borderRadius: 8,
fontSize: 12.5,
fontWeight: c.selected ? 800 : 600,
cursor: clickable ? "pointer" : "default",
border: c.selected
? "1.5px solid #12B981"
: clickable
? "1px solid #CDEBDD"
: "1px solid transparent",
background: c.selected
? "#12B981"
: clickable
? "#F4FBF7"
: "transparent",
color: c.selected
? "#fff"
: !c.inMonth
? "#CBD5E1"
: clickable
? "#0A6F4D"
: "#C4CDD6",
transition: "all 120ms ease",
}}
>
{c.day}
{c.hasDeparture && c.inMonth && !c.selected && (
<span
style={{
position: "absolute",
bottom: 4,
left: "50%",
transform: "translateX(-50%)",
width: 4,
height: 4,
borderRadius: "50%",
background: "#12B981",
}}
/>
)}
{c.selected && (
<Check
size={11}
color="#fff"
strokeWidth={3}
style={{
position: "absolute",
bottom: 3,
left: "50%",
transform: "translateX(-50%)",
}}
/>
)}
</button>
);
})}
</Box>
{value && (
<Text fz="12px" c="#0A6F4D" fw={600} mt="sm">
Selected:{" "}
{new Date(value + "T00:00:00").toLocaleDateString(undefined, {
weekday: "short",
month: "short",
day: "numeric",
year: "numeric",
})}
</Text>
)}
{departureDays.size === 0 && (
<Text fz="12px" c="orange.7" mt="sm">
No scheduled departures found for this route yet.
</Text>
)}
</>
)}
</Box>
);
}
export default OperationDatePicker;

View File

@@ -0,0 +1,5 @@
export {
OperationDatePicker,
default,
} from "./OperationDatePicker";
export type { OperationDatePickerProps } from "./OperationDatePicker";

View File

@@ -20,6 +20,9 @@ export type {
ViewableFile,
} from "./components/FileViewer";
export { OperationDatePicker } from "./components/OperationDatePicker";
export type { OperationDatePickerProps } from "./components/OperationDatePicker";
export { Badge } from "./components/badge";
// export type { BadgeProps } from "./components/badge";