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

@@ -1,6 +1,7 @@
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
import {
ArrowLeft,
FileSignature,
FolderOpen,
Layers,
LayoutGrid,
@@ -254,6 +255,20 @@ export default function BookingRequestDetailPage() {
booking={booking}
mutations={mutations}
/>
{booking.isGovernment && booking.contractSummary && (
<Button
fullWidth
variant="default"
leftSection={<FileSignature size={16} />}
onClick={() =>
navigate(
`/dashboard/booking-requests/${booking.id}/contract`,
)
}
>
View / sign contract
</Button>
)}
{booking.customsClearingEnabled && (
<Button
fullWidth

View File

@@ -400,8 +400,13 @@ export default function NewBookingPage() {
}),
onSuccess: async (booking) => {
if (isGovernment) {
// The server already expedited + generated the contract at creation;
// this call is an idempotent no-op that doubles as a retry if that
// best-effort step failed.
await bookingsService.governmentExpedite(booking.id);
toast.success("Government booking created and expedited to scheduling");
toast.success(
"Government booking created — contract generated, priority scheduling queued",
);
} else {
toast.success("Booking created as draft");
}
@@ -891,7 +896,7 @@ export default function NewBookingPage() {
<Info size={14} color="var(--mantine-color-gray-5)" style={{ marginTop: 2, flexShrink: 0 }} />
<Text size="xs" c="dimmed">
{isGovernment
? "Government bookings skip the commercial 3-hour hold and enter the priority lane."
? "Government bookings skip every customer step: paid & eligible immediately, contract generated automatically (signable any time), priority seat on any open train of the route."
: "Overweight container lines are allowed here and flagged later at scheduling."}
</Text>
</Group>

View File

@@ -98,7 +98,8 @@ export default function ContractViewPage() {
const openSign = () => {
setSignerName(data?.savedSignature?.signerDisplayName ?? "");
setSignatureData(null);
setStampData(null);
// Prefill with the reusable stamp saved on the profile; still replaceable.
setStampData(data?.savedSignature?.stampImageUrl ?? null);
setDrawNew(false);
setSignOpen(true);
};

View File

@@ -56,6 +56,7 @@ import { RescheduleTrainDialog } from "@/components/trainScheduling/RescheduleTr
import BookingWindowSettingsModal from "@/components/trainScheduling/BookingWindowSettingsModal";
// import { ScheduleBatchPanel } from "@/components/trainScheduling/ScheduleBatchPanel";
import { ScheduleBookingsStep } from "@/components/trainScheduling/ScheduleBookingsStep";
import { SwitchGovernmentBookingModal } from "@/components/trainScheduling/SwitchGovernmentBookingModal";
import { ScheduleWorkspacePanel } from "@/components/trainScheduling/ScheduleWorkspacePanel";
import { FreightTypeBadge } from "@/components/trainScheduling/ScheduleStatusBadge";
import {
@@ -79,6 +80,7 @@ import { trainSchedulingService } from "@/services/trainScheduling.service";
import { useToast } from "@/hooks/use-toast";
import type {
ContainerPlacement,
EligibleContainerBooking,
FreightType,
TrainSchedulePreviewResponse,
} from "@/types/trainScheduling";
@@ -110,6 +112,7 @@ export default function TrainScheduleV2DetailPage() {
const [gatepassFileUrl, setGatepassFileUrl] = useState("");
const [gatepassNotes, setGatepassNotes] = useState("");
const [dispatchConfirmOpen, setDispatchConfirmOpen] = useState(false);
const [switchTarget, setSwitchTarget] = useState<EligibleContainerBooking | null>(null);
const autoPreviewedRef = useRef(false);
const detailQuery = useQuery(
@@ -192,6 +195,7 @@ export default function TrainScheduleV2DetailPage() {
const preview = useMutation(api.trainScheduling.preview.mutationOptions());
const assign = useMutation(api.trainScheduling.assignBookings.mutationOptions());
const unassign = useMutation(api.trainScheduling.unassignBooking.mutationOptions());
const switchGov = useMutation(api.trainScheduling.switchGovernmentBooking.mutationOptions());
const finalize = useMutation(api.trainScheduling.finalizeSchedule.mutationOptions());
const dispatch = useMutation(api.trainScheduling.dispatchSchedule.mutationOptions());
const downloadMarshalling = useMutation({
@@ -511,6 +515,17 @@ export default function TrainScheduleV2DetailPage() {
};
const handleUnassign = async (bookingId: string) => {
// Gov bookings never leave a train by removal — only by switching. The API
// enforces this too; the guard here just gives the warning without a call.
if (schedule?.bookings?.some((b) => b.id === bookingId && b.isGovernment)) {
toast({
title: "Government booking cannot be removed",
description:
"Government bookings cannot be removed from the train. They can only be switched onto another allocation.",
variant: "destructive",
});
return;
}
try {
await unassign.mutateAsync({ id: scheduleId, bookingId });
toast({ title: "Booking unassigned" });
@@ -631,6 +646,7 @@ export default function TrainScheduleV2DetailPage() {
id: b.id,
reference: b.reference ?? b.id.slice(0, 8),
weightTons: b.weightTons,
isGovernment: b.isGovernment,
}))}
eligibleItems={eligibleQuery.data?.items ?? []}
eligibleLoading={eligibleQuery.isLoading}
@@ -643,6 +659,7 @@ export default function TrainScheduleV2DetailPage() {
freightType={freightType}
canRemove={canModifyBookings}
onRemove={handleUnassign}
onSwitch={canModifyBookings ? setSwitchTarget : undefined}
/>
{canEditBookings ? (
@@ -1274,6 +1291,36 @@ export default function TrainScheduleV2DetailPage() {
onSaved={() => void detailQuery.refetch()}
/>
<SwitchGovernmentBookingModal
key={switchTarget?.id ?? "none"}
opened={Boolean(switchTarget)}
onClose={() => setSwitchTarget(null)}
govBooking={switchTarget}
assignedBookings={schedule.bookings ?? []}
loading={switchGov.isPending}
onConfirm={async (removeBookingIds) => {
if (!scheduleId || !switchTarget) return;
try {
await switchGov.mutateAsync({
id: scheduleId,
governmentBookingId: switchTarget.id,
removeBookingIds,
});
toast({ title: `Government booking ${switchTarget.reference} switched onto the train` });
setSwitchTarget(null);
setSelectedBookingIds([]);
setPreviewResult(null);
autoPreviewedRef.current = false;
} catch (err) {
toast({
title: "Switch failed",
description: parseError(err, "Could not switch the government booking"),
variant: "destructive",
});
}
}}
/>
<Modal
opened={dispatchConfirmOpen}
onClose={() => setDispatchConfirmOpen(false)}