mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +00:00
feat(bookings): add estimated shipment date handling and validation for binding shipment day
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
/**
|
||||
* The booking wizard now captures a NON-BINDING estimated shipment date instead
|
||||
* of the binding scheduledDate. The binding scheduledDate (validated against
|
||||
* open train departures) is set later, at the operation-request step.
|
||||
*/
|
||||
export class AddEstimatedShipmentDate1820000000012
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'AddEstimatedShipmentDate1820000000012';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.bookings
|
||||
ADD COLUMN IF NOT EXISTS estimated_shipment_date timestamptz NULL;
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.bookings
|
||||
DROP COLUMN IF EXISTS estimated_shipment_date;
|
||||
`);
|
||||
}
|
||||
}
|
||||
@@ -795,6 +795,20 @@ export class BookingTransitionService {
|
||||
throw new BadRequestException('A valid schedule date is required');
|
||||
}
|
||||
|
||||
// The binding shipment day must have at least one OPEN departure on the
|
||||
// route — only schedule-backed days are selectable. The batch engine
|
||||
// assigns the specific train within that (route, day) pool later.
|
||||
const hasDeparture = await this.bookingsService.hasOpenDepartureOnDay(
|
||||
booking.originYardId,
|
||||
booking.destinationYardId,
|
||||
eatDay(date),
|
||||
);
|
||||
if (!hasDeparture) {
|
||||
throw new BadRequestException(
|
||||
'No departures available on the selected day for this route',
|
||||
);
|
||||
}
|
||||
|
||||
await this.bookingsRepository.update(bookingId, {
|
||||
status: 'OPERATION_REQUEST_PENDING',
|
||||
scheduledDate: date,
|
||||
|
||||
@@ -317,12 +317,14 @@ export class BookingsService {
|
||||
) {
|
||||
throw new BadRequestException('Selected schedule is not on the booking route');
|
||||
}
|
||||
} else if (!isGeneralContract) {
|
||||
// Day-level pool: the customer picked a DAY — require that the route has at
|
||||
// least one OPEN departure on that EAT day. The batch engine assigns the
|
||||
// train later. General contracts skip this — they have no shipment date at
|
||||
// creation; each drawdown order validates its own day.
|
||||
const day = eatDay(new Date(dto.scheduledDate!));
|
||||
} else if (dto.scheduledDate) {
|
||||
// A real (binding) scheduledDate was supplied (e.g. staff pinning a day
|
||||
// directly). Require that the route has at least one OPEN departure on
|
||||
// that EAT day. The booking wizard does NOT send scheduledDate at creation
|
||||
// — it captures a non-binding estimatedShipmentDate instead, and the
|
||||
// binding day is chosen later at the operation-request step. General
|
||||
// contracts also skip this (each drawdown order validates its own day).
|
||||
const day = eatDay(new Date(dto.scheduledDate));
|
||||
const hasDeparture =
|
||||
await this.trainSchedulingService.existsOpenScheduleOnRouteDay(
|
||||
dto.originYardId,
|
||||
@@ -428,6 +430,9 @@ export class BookingsService {
|
||||
financialTerms: dto.financialTerms,
|
||||
bookingType: isGeneralContract ? 'GENERAL_CONTRACT' : 'ONE_TIME',
|
||||
scheduledDate: dto.scheduledDate ? new Date(dto.scheduledDate) : null,
|
||||
estimatedShipmentDate: dto.estimatedShipmentDate
|
||||
? new Date(dto.estimatedShipmentDate)
|
||||
: null,
|
||||
startDate: dto.startDate ? new Date(dto.startDate) : undefined,
|
||||
endDate: dto.endDate ? new Date(dto.endDate) : undefined,
|
||||
status: 'DRAFT',
|
||||
@@ -621,6 +626,8 @@ export class BookingsService {
|
||||
);
|
||||
}
|
||||
if (dto.scheduledDate) updates.scheduledDate = new Date(dto.scheduledDate);
|
||||
if (dto.estimatedShipmentDate)
|
||||
updates.estimatedShipmentDate = new Date(dto.estimatedShipmentDate);
|
||||
if (dto.startDate) updates.startDate = new Date(dto.startDate);
|
||||
if (dto.endDate) updates.endDate = new Date(dto.endDate);
|
||||
delete updates.containers;
|
||||
@@ -696,6 +703,23 @@ export class BookingsService {
|
||||
}
|
||||
|
||||
/** Return a paginated list of bookings matching the filter. */
|
||||
/**
|
||||
* Whether a route has at least one OPEN train departure on the given EAT day.
|
||||
* Used to validate the binding shipment day chosen at the operation-request
|
||||
* step (only days with a schedule are selectable).
|
||||
*/
|
||||
async hasOpenDepartureOnDay(
|
||||
originYardId: string,
|
||||
destinationYardId: string,
|
||||
day: string,
|
||||
): Promise<boolean> {
|
||||
return this.trainSchedulingService.existsOpenScheduleOnRouteDay(
|
||||
originYardId,
|
||||
destinationYardId,
|
||||
day,
|
||||
);
|
||||
}
|
||||
|
||||
async findAll(
|
||||
filter: FilterBookingDto,
|
||||
forceCompanyId?: string,
|
||||
|
||||
@@ -155,14 +155,24 @@ export class CreateBookingDto {
|
||||
bookingType?: string;
|
||||
|
||||
/**
|
||||
* The day the customer wants to ship (the pool day key). Required for one-time
|
||||
* bookings; omitted for general contracts, which pick the date per order.
|
||||
* The BINDING shipment day (the pool day key), validated against open train
|
||||
* departures. Set later at the operation-request step — NOT at booking
|
||||
* creation. Optional here; staff may still pin it directly.
|
||||
*/
|
||||
@ApiPropertyOptional({ example: '2026-06-15T00:00:00.000Z' })
|
||||
@ValidateIf((o) => o.bookingType !== 'GENERAL_CONTRACT')
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
scheduledDate?: string;
|
||||
|
||||
/**
|
||||
* Non-binding shipment-date estimate captured in the booking wizard. Purely
|
||||
* informational — NOT validated against train departures.
|
||||
*/
|
||||
@ApiPropertyOptional({ example: '2026-06-15T00:00:00.000Z' })
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
estimatedShipmentDate?: string;
|
||||
|
||||
@ApiProperty({ enum: CONTRACT_TYPES })
|
||||
@IsIn([...CONTRACT_TYPES])
|
||||
contractType!: string;
|
||||
|
||||
@@ -155,10 +155,23 @@ export class Booking extends BaseEntity {
|
||||
/**
|
||||
* Nullable: general contracts have no shipment date at creation — the date is
|
||||
* chosen per drawdown order. One-time bookings always set this (the pool day key).
|
||||
*
|
||||
* NOTE: this is the BINDING shipment day, validated against actual open train
|
||||
* departures. It is set later, when the customer requests the operation — NOT
|
||||
* at booking creation. See estimatedShipmentDate for the non-binding estimate
|
||||
* captured in the booking wizard.
|
||||
*/
|
||||
@Column({ name: 'scheduled_date', type: 'timestamptz', nullable: true })
|
||||
scheduledDate?: Date | null;
|
||||
|
||||
/**
|
||||
* Non-binding shipment-date estimate captured in the booking wizard. Purely
|
||||
* informational — NOT validated against train departures. The binding
|
||||
* scheduledDate is chosen later at the operation-request step.
|
||||
*/
|
||||
@Column({ name: 'estimated_shipment_date', type: 'timestamptz', nullable: true })
|
||||
estimatedShipmentDate?: Date | null;
|
||||
|
||||
/**
|
||||
* General contracts only: when the ordering window closes, computed from the
|
||||
* global CONTRACT_PERIOD_MONTHS setting at activation. Null for one-time
|
||||
|
||||
@@ -9,9 +9,24 @@ import {
|
||||
TextInput,
|
||||
} from "@mantine/core";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
addMonths,
|
||||
eachDayOfInterval,
|
||||
endOfMonth,
|
||||
endOfWeek,
|
||||
format,
|
||||
isSameMonth,
|
||||
isToday,
|
||||
startOfMonth,
|
||||
startOfWeek,
|
||||
} from "date-fns";
|
||||
import {
|
||||
AlertCircle,
|
||||
Calendar as CalendarIcon,
|
||||
Check,
|
||||
CheckCircle2,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Clock,
|
||||
Download,
|
||||
FileText,
|
||||
@@ -86,6 +101,8 @@ export function ClearanceCard({ booking }: { booking: Freight.IBooking }) {
|
||||
const [adHoc, setAdHoc] = useState<Array<{ name: string; file: File | null }>>(
|
||||
[],
|
||||
);
|
||||
// Binding shipment day chosen for the operation request (yyyy-MM-dd).
|
||||
const [scheduledDate, setScheduledDate] = useState<string>("");
|
||||
|
||||
const refresh = () => {
|
||||
queryClient.invalidateQueries({
|
||||
@@ -339,6 +356,32 @@ export function ClearanceCard({ booking }: { booking: Freight.IBooking }) {
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{isReady && (
|
||||
<Box mt="lg">
|
||||
<Text fz="13px" fw={700} c="#10202F" mb={6}>
|
||||
Choose your shipment day
|
||||
</Text>
|
||||
<Text fz="12px" c="dimmed" mb="sm">
|
||||
Only days with a scheduled departure on your route can be selected.
|
||||
The operations team assigns the specific train for that day.
|
||||
</Text>
|
||||
<OperationDatePicker
|
||||
originYardId={booking.originYard?.id}
|
||||
destinationYardId={booking.destinationYard?.id}
|
||||
value={scheduledDate}
|
||||
onChange={setScheduledDate}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{proceedMutation.isError && (
|
||||
<Alert color="red" radius="md" icon={<AlertCircle size={16} />} mt="md">
|
||||
{proceedMutation.error instanceof Error
|
||||
? proceedMutation.error.message
|
||||
: "Could not request the operation. Please try again."}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Group justify="flex-end" mt="lg" gap="sm">
|
||||
{canUpload && (
|
||||
<Button
|
||||
@@ -362,11 +405,12 @@ export function ClearanceCard({ booking }: { booking: Freight.IBooking }) {
|
||||
leftSection={<CheckCircle2 size={16} />}
|
||||
onClick={() =>
|
||||
proceedMutation.mutate(
|
||||
{ id: booking.id },
|
||||
{ id: booking.id, scheduledDate },
|
||||
{ onSuccess: () => navigate(`/bookings/${booking.id}`) },
|
||||
)
|
||||
}
|
||||
loading={proceedMutation.isPending}
|
||||
disabled={!scheduledDate}
|
||||
>
|
||||
Proceed to operation
|
||||
</Button>
|
||||
@@ -375,3 +419,202 @@ export function ClearanceCard({ booking }: { booking: Freight.IBooking }) {
|
||||
</SectionCard>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compact month calendar for picking the binding shipment day at the
|
||||
* operation-request step. Only days that have an OPEN scheduled departure on the
|
||||
* booking route are selectable; all other days are disabled.
|
||||
*/
|
||||
function OperationDatePicker({
|
||||
originYardId,
|
||||
destinationYardId,
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
originYardId?: string;
|
||||
destinationYardId?: string;
|
||||
value: string;
|
||||
onChange: (date: string) => void;
|
||||
}) {
|
||||
const [month, setMonth] = useState(() => startOfMonth(new Date()));
|
||||
|
||||
const { data: availableDays, isLoading } = useQuery(
|
||||
api.bookings.getAvailableDays.queryOptions({
|
||||
input: { originYardId, destinationYardId },
|
||||
enabled: !!originYardId && !!destinationYardId,
|
||||
}),
|
||||
);
|
||||
|
||||
const departureDays = useMemo(
|
||||
() => new Set(availableDays ?? []),
|
||||
[availableDays],
|
||||
);
|
||||
|
||||
const cells = useMemo(() => {
|
||||
const start = startOfWeek(startOfMonth(month), { weekStartsOn: 1 });
|
||||
const end = endOfWeek(endOfMonth(month), { weekStartsOn: 1 });
|
||||
return eachDayOfInterval({ start, end }).map((date) => {
|
||||
const dateString = format(date, "yyyy-MM-dd");
|
||||
return {
|
||||
date,
|
||||
dateString,
|
||||
day: date.getDate(),
|
||||
inMonth: isSameMonth(date, month),
|
||||
today: isToday(date),
|
||||
selected: value === dateString,
|
||||
hasDeparture: departureDays.has(dateString),
|
||||
};
|
||||
});
|
||||
}, [month, departureDays, value]);
|
||||
|
||||
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={() => setMonth((m) => addMonths(m, -1))}
|
||||
>
|
||||
<ChevronLeft size={15} />
|
||||
</Button>
|
||||
<Text fz="13px" fw={700} c="#10202F">
|
||||
{format(month, "MMMM yyyy")}
|
||||
</Text>
|
||||
<Button
|
||||
variant="default"
|
||||
size="xs"
|
||||
px={6}
|
||||
radius="xl"
|
||||
onClick={() => setMonth((m) => addMonths(m, 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: {format(new Date(value + "T00:00:00"), "EEE, MMM d yyyy")}
|
||||
</Text>
|
||||
)}
|
||||
{!isLoading && departureDays.size === 0 && (
|
||||
<Text fz="12px" c="orange.7" mt="sm">
|
||||
No scheduled departures found for this route yet.
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -422,13 +422,14 @@ export default function NewBookingPage() {
|
||||
bookingType: isContract
|
||||
? Freight.BookingType.GeneralContract
|
||||
: Freight.BookingType.OneTime,
|
||||
// General contracts omit the shipment date — chosen per order later.
|
||||
...(isContract
|
||||
// The wizard captures a NON-BINDING estimate only — never the binding
|
||||
// scheduledDate (that is chosen later at the operation-request step and
|
||||
// validated against open departures). General contracts omit even the
|
||||
// estimate; the date is chosen per order later.
|
||||
...(isContract || !data.scheduledDate
|
||||
? {}
|
||||
: {
|
||||
scheduledDate: data.scheduledDate
|
||||
? new Date(data.scheduledDate).toISOString()
|
||||
: new Date().toISOString(),
|
||||
estimatedShipmentDate: new Date(data.scheduledDate).toISOString(),
|
||||
}),
|
||||
contractType:
|
||||
data.contractType.toUpperCase() as CreateBookingPayload["contractType"],
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { Box, Group, Stack, Switch, Text, TextInput } from "@mantine/core";
|
||||
import type { ReactNode } from "react";
|
||||
import { FileText, Info, Layers, Train, Truck } from "lucide-react";
|
||||
import { Check, FileText, Info, Layers, Train, Truck } from "lucide-react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { Controller, type UseFormReturn } from "react-hook-form";
|
||||
import { BookingFormInputValues, type BookingFormValues } from "./schema";
|
||||
import {
|
||||
fieldStyles,
|
||||
OptionCard,
|
||||
OptionFieldError,
|
||||
StepCard,
|
||||
StepHeader,
|
||||
@@ -88,17 +87,14 @@ export function Step2ServiceType({
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => (
|
||||
<div>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
{referenceData?.service
|
||||
.filter((s) => s.canBeBookedAlone)
|
||||
.map((s) => (
|
||||
<OptionCard
|
||||
<ServiceTypeCard
|
||||
key={s.id}
|
||||
selected={field.value === s.id}
|
||||
onClick={() => field.onChange(s.id)}
|
||||
icon={<Train className="h-5 w-5" />}
|
||||
iconBg="#EEF0FB"
|
||||
iconColor="#4F46E5"
|
||||
title={s.serviceName}
|
||||
description={s.description}
|
||||
/>
|
||||
@@ -365,6 +361,92 @@ export function Step2ServiceType({
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compact service-type selection card. A single horizontal row (icon · text ·
|
||||
* radio) — deliberately smaller than the shared OptionCard so the service list
|
||||
* stays scannable.
|
||||
*/
|
||||
function ServiceTypeCard({
|
||||
selected,
|
||||
onClick,
|
||||
title,
|
||||
description,
|
||||
}: {
|
||||
selected: boolean;
|
||||
onClick: () => void;
|
||||
title?: ReactNode;
|
||||
description?: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
style={{
|
||||
width: "100%",
|
||||
textAlign: "left",
|
||||
cursor: "pointer",
|
||||
borderRadius: 12,
|
||||
padding: "12px 14px",
|
||||
transition: "all 140ms ease",
|
||||
border: `1.5px solid ${selected ? "#12B981" : "#E6ECF2"}`,
|
||||
background: selected ? "#F4FBF7" : "#fff",
|
||||
boxShadow: selected
|
||||
? "0 0 0 1px #12B981, 0 4px 12px rgba(14,163,113,0.10)"
|
||||
: "0 1px 2px rgba(16,24,40,0.04)",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!selected) e.currentTarget.style.borderColor = "#BFE3D2";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!selected) e.currentTarget.style.borderColor = "#E6ECF2";
|
||||
}}
|
||||
>
|
||||
<Group gap={11} align="center" wrap="nowrap">
|
||||
<Box
|
||||
style={{
|
||||
width: 34,
|
||||
height: 34,
|
||||
flexShrink: 0,
|
||||
borderRadius: 9,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
background: selected ? "#E3F4EC" : "#EEF0FB",
|
||||
color: selected ? "#0A6F4D" : "#4F46E5",
|
||||
}}
|
||||
>
|
||||
<Train size={17} />
|
||||
</Box>
|
||||
<Box style={{ minWidth: 0, flex: 1 }}>
|
||||
<Text fz={13.5} fw={700} c="#10202F" truncate>
|
||||
{title}
|
||||
</Text>
|
||||
{description && (
|
||||
<Text fz={11.5} c="#6B7C8E" truncate style={{ lineHeight: 1.35 }}>
|
||||
{description}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
<Box
|
||||
style={{
|
||||
width: 18,
|
||||
height: 18,
|
||||
flexShrink: 0,
|
||||
borderRadius: "50%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
border: selected ? "none" : "1.5px solid #CBD5E1",
|
||||
background: selected ? "#12B981" : "transparent",
|
||||
}}
|
||||
>
|
||||
{selected && <Check size={11} color="#fff" strokeWidth={3} />}
|
||||
</Box>
|
||||
</Group>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function ServiceToggle({
|
||||
icon,
|
||||
title,
|
||||
|
||||
@@ -269,10 +269,14 @@ export const api = {
|
||||
bookingsService.submitClearanceDocuments(id, files),
|
||||
),
|
||||
|
||||
proceedToOperation: endpoint<{ id: string }, Freight.IBooking>(
|
||||
proceedToOperation: endpoint<
|
||||
{ id: string; scheduledDate: string },
|
||||
Freight.IBooking
|
||||
>(
|
||||
"bookings",
|
||||
"proceedToOperation",
|
||||
({ id }) => bookingsService.proceedToOperation(id),
|
||||
({ id, scheduledDate }) =>
|
||||
bookingsService.proceedToOperation(id, scheduledDate),
|
||||
),
|
||||
|
||||
checkPayment: endpoint<{ orderId: string }, { status: string }>(
|
||||
|
||||
@@ -206,8 +206,14 @@ export const bookingsService = {
|
||||
return data.data;
|
||||
},
|
||||
|
||||
proceedToOperation: async (id: string): Promise<Freight.IBooking> => {
|
||||
const { data } = await client.post(`/api/bookings/${id}/clearance/proceed`);
|
||||
proceedToOperation: async (
|
||||
id: string,
|
||||
scheduledDate: string,
|
||||
): Promise<Freight.IBooking> => {
|
||||
const { data } = await client.post(
|
||||
`/api/bookings/${id}/clearance/proceed`,
|
||||
{ scheduledDate },
|
||||
);
|
||||
return data.data;
|
||||
},
|
||||
|
||||
|
||||
@@ -646,8 +646,10 @@ export interface CreateBookingDto {
|
||||
companyId?: string | undefined;
|
||||
trainId?: string | undefined;
|
||||
trainScheduleId?: string | undefined;
|
||||
/** Optional for general contracts — they pick the date per order, not at creation. */
|
||||
/** Binding shipment day — set at the operation-request step, not at creation. */
|
||||
scheduledDate?: string | undefined;
|
||||
/** Non-binding shipment-date estimate captured in the booking wizard. */
|
||||
estimatedShipmentDate?: string | undefined;
|
||||
/** Defaults to ONE_TIME. GENERAL_CONTRACT creates an umbrella contract. */
|
||||
bookingType?: BookingType | undefined;
|
||||
contractType: string;
|
||||
|
||||
Reference in New Issue
Block a user