add goverment booking

This commit is contained in:
Marshal
2026-07-31 00:41:51 +00:00
parent 34e4a7d13f
commit 65f18b4796
35 changed files with 913 additions and 88 deletions

View File

@@ -5,6 +5,7 @@ import toast from "react-hot-toast";
import { api } from "@/services/api";
import { ContractSignaturePad } from "@/components/bookings/ContractSignaturePad";
import { StampUpload } from "@/components/contracts/StampUpload";
import {
Card,
CardContent,
@@ -40,6 +41,7 @@ export function MySignatureCard() {
const [open, setOpen] = useState(false);
const [signerName, setSignerName] = useState("");
const [signatureData, setSignatureData] = useState<string | null>(null);
const [stampData, setStampData] = useState<string | null>(null);
const defaultName =
user?.name?.en || user?.username || user?.email || "";
@@ -47,6 +49,7 @@ export function MySignatureCard() {
const openDialog = () => {
setSignerName(saved?.signerDisplayName ?? defaultName);
setSignatureData(null);
setStampData(saved?.stampImageUrl ?? null);
setOpen(true);
};
@@ -56,6 +59,10 @@ export function MySignatureCard() {
{
signerDisplayName: signerName.trim(),
signatureImageBase64: signatureData,
// Only send the stamp when it changed — omitted keeps the saved one.
...(stampData && stampData !== saved?.stampImageUrl
? { stampImageBase64: stampData }
: {}),
},
{
onSuccess: () => {
@@ -101,6 +108,18 @@ export function MySignatureCard() {
You have not saved a signature yet.
</p>
)}
{saved?.stampImageUrl && (
<div className="space-y-2">
<div className="overflow-hidden rounded-lg border-2 border-dashed border-border bg-white">
<img
src={saved.stampImageUrl}
alt="My saved company stamp"
className="mx-auto h-24 w-full object-contain"
/>
</div>
<p className="text-xs text-muted-foreground">Company stamp</p>
</div>
)}
<Button variant="outline" size="sm" onClick={openDialog}>
{saved?.signatureImageUrl ? "Update signature" : "Add signature"}
</Button>
@@ -126,6 +145,11 @@ export function MySignatureCard() {
/>
</div>
<ContractSignaturePad onChange={setSignatureData} />
<StampUpload
value={stampData}
onChange={setStampData}
description="Stored on your profile and prefilled when you sign contracts."
/>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setOpen(false)}>

View File

@@ -1,5 +1,5 @@
import { useMemo } from "react";
import { ArrowRight, Package } from "lucide-react";
import { ArrowRight, Landmark, Package } from "lucide-react";
import {
Accordion,
Badge,
@@ -21,11 +21,13 @@ function EligibleBookingRow({
freightType,
selected,
onToggle,
onSwitch,
}: {
booking: EligibleContainerBooking;
freightType?: FreightType;
selected: boolean;
onToggle: () => void;
onSwitch?: (booking: EligibleContainerBooking) => void;
}) {
const resolvedFreightType = booking.freightType ?? freightType;
const isBulk = resolvedFreightType === "BULK";
@@ -61,6 +63,26 @@ function EligibleBookingRow({
{booking.schedulingStatus}
</Badge>
) : null}
{booking.isGovernment ? (
<Badge
variant="light"
size="xs"
color="yellow"
leftSection={<Landmark size={10} />}
>
Government
</Badge>
) : null}
{booking.isGovernment && onSwitch ? (
<Button
variant="light"
color="yellow"
size="compact-xs"
onClick={() => onSwitch(booking)}
>
Switch onto train
</Button>
) : null}
</Group>
<Text size="xs" c="dimmed">
{booking.customer}
@@ -102,6 +124,7 @@ export function EligibleBookingsPanel({
onSelectionChange,
assignedIds = [],
freightType,
onSwitch,
}: {
items: EligibleContainerBooking[];
isLoading?: boolean;
@@ -109,6 +132,7 @@ export function EligibleBookingsPanel({
onSelectionChange: (ids: string[]) => void;
assignedIds?: string[];
freightType?: FreightType;
onSwitch?: (booking: EligibleContainerBooking) => void;
}) {
const assignedSet = useMemo(() => new Set(assignedIds), [assignedIds]);
@@ -230,6 +254,7 @@ export function EligibleBookingsPanel({
freightType={freightType}
selected={selectedIds.includes(booking.id)}
onToggle={() => toggle(booking.id)}
onSwitch={onSwitch}
/>
))}
</Stack>

View File

@@ -7,7 +7,7 @@ import {
Tabs,
Text,
} from "@mantine/core";
import { ArrowRight, Package, Train } from "lucide-react";
import { ArrowRight, Landmark, Package, Train } from "lucide-react";
import type { EligibleContainerBooking, FreightType } from "@/types/trainScheduling";
@@ -17,6 +17,7 @@ export type AssignedBookingRow = {
id: string;
reference: string;
weightTons?: number;
isGovernment?: boolean;
};
export function ScheduleBookingsStep({
@@ -29,6 +30,7 @@ export function ScheduleBookingsStep({
freightType,
canRemove,
onRemove,
onSwitch,
}: {
assignedBookings: AssignedBookingRow[];
eligibleItems: EligibleContainerBooking[];
@@ -39,6 +41,7 @@ export function ScheduleBookingsStep({
freightType?: FreightType;
canRemove?: boolean;
onRemove?: (bookingId: string) => void;
onSwitch?: (booking: EligibleContainerBooking) => void;
}) {
return (
<Paper p="md" radius="lg" withBorder style={{ borderColor: "var(--mantine-color-gray-2)" }}>
@@ -91,6 +94,16 @@ export function ScheduleBookingsStep({
{booking.weightTons}T
</Badge>
) : null}
{booking.isGovernment ? (
<Badge
variant="light"
size="xs"
color="yellow"
leftSection={<Landmark size={10} />}
>
Government
</Badge>
) : null}
</Group>
<Group gap={6}>
<Text size="xs" c="dimmed">
@@ -130,6 +143,7 @@ export function ScheduleBookingsStep({
onSelectionChange={onSelectionChange}
assignedIds={assignedIds}
freightType={freightType}
onSwitch={onSwitch}
/>
</Tabs.Panel>
</Tabs>

View File

@@ -0,0 +1,143 @@
import { useMemo, useState } from "react";
import {
Alert,
Badge,
Button,
Checkbox,
Group,
Modal,
Stack,
Text,
} from "@mantine/core";
import { Landmark } from "lucide-react";
import type {
EligibleContainerBooking,
TrainScheduleDetail,
} from "@/types/trainScheduling";
/**
* Government-priority switch confirmation: pick the assigned commercial
* bookings to take off the train so the government booking can have their
* wagons. Mount with key={govBooking.id} so selection resets per booking.
*/
export function SwitchGovernmentBookingModal({
opened,
onClose,
govBooking,
assignedBookings,
loading,
onConfirm,
}: {
opened: boolean;
onClose: () => void;
govBooking: EligibleContainerBooking | null;
assignedBookings: TrainScheduleDetail["bookings"];
loading?: boolean;
onConfirm: (removeBookingIds: string[]) => void;
}) {
const [selected, setSelected] = useState<string[]>([]);
const candidates = useMemo(
() => assignedBookings.filter((b) => !b.isGovernment && b.wagonAssigned),
[assignedBookings],
);
const freedWagons = candidates
.filter((b) => selected.includes(b.id))
.reduce((sum, b) => sum + (b.wagonsRequired ?? 0), 0);
const toggle = (id: string) =>
setSelected((ids) =>
ids.includes(id) ? ids.filter((x) => x !== id) : [...ids, id],
);
return (
<Modal
opened={opened}
onClose={onClose}
title={
<Group gap="xs">
<Landmark size={18} />
<Text fw={600}>Switch in government booking</Text>
</Group>
}
radius="lg"
size="lg"
>
<Stack gap="md">
<Alert color="yellow" radius="md" variant="light">
Government bookings have priority. Select the booking(s) to switch out
together they must free at least as many wagons as{" "}
<Text span fw={600}>
{govBooking?.reference}
</Text>{" "}
needs. Switched-out customers are notified to rebook.
</Alert>
{candidates.length ? (
<Stack gap="xs">
{candidates.map((b) => (
<Group
key={b.id}
justify="space-between"
p="sm"
style={{
border: "1px solid var(--mantine-color-gray-2)",
borderRadius: 12,
}}
>
<Checkbox
color="edr-green"
checked={selected.includes(b.id)}
onChange={() => toggle(b.id)}
label={
<Group gap="xs">
<Text fw={600} size="sm">
{b.reference}
</Text>
<Text size="xs" c="dimmed">
{b.customer}
</Text>
</Group>
}
/>
<Group gap="xs">
<Badge variant="outline" size="xs" color="edr-green">
{b.weightTons}T
</Badge>
<Badge variant="light" size="xs">
{b.wagonsRequired ?? "?"} wagon{b.wagonsRequired === 1 ? "" : "s"}
</Badge>
</Group>
</Group>
))}
</Stack>
) : (
<Text size="sm" c="dimmed" ta="center" py="md">
No commercial bookings with wagons on this train to switch out.
</Text>
)}
<Group justify="space-between">
<Badge variant="light" color={selected.length ? "edr-green" : "gray"}>
Frees {freedWagons} wagon{freedWagons === 1 ? "" : "s"}
</Badge>
<Group gap="sm">
<Button variant="default" radius="md" onClick={onClose}>
Cancel
</Button>
<Button
color="edr-green"
radius="md"
disabled={!selected.length}
loading={loading}
onClick={() => onConfirm(selected)}
>
Confirm switch
</Button>
</Group>
</Group>
</Stack>
</Modal>
);
}