mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 17:38:12 +00:00
feat: implement shipping line booking completion functionality
- Added ShippingLineBookingCompletionController and associated service to handle the completion of shipping line bookings. - Introduced a new module for booking completion to maintain module separation and avoid cyclic dependencies. - Updated the train scheduling global rules to set default desk hours to 24 hours. - Modified existing services and entities to accommodate the new booking completion logic. - Enhanced the front-end components to support the new booking completion flow, including updates to the booking detail and bookings pages. - Implemented validation and error handling for booking completion, ensuring that only approved bookings can be completed. - Added migration to set default desk hours in the database.
This commit is contained in:
@@ -227,6 +227,12 @@ const RuleEngineFormDialog = ({
|
||||
current[name] ? { ...current, [name]: "" } : current,
|
||||
);
|
||||
setValues((current) => {
|
||||
// Mantine fires onChange even when the same option is re-picked, and the
|
||||
// cascades below clear dependent answers (yards, unit, scope). Re-picking
|
||||
// an unchanged value must be a no-op, or an untouched direction silently
|
||||
// wipes the yard pair and the submit fails with "origin/destination
|
||||
// missing" data the admin did fill in.
|
||||
if (current[name] === value) return current;
|
||||
const next = { ...current, [name]: value };
|
||||
// Changing what a rate applies to (or its surcharge trigger) can invalidate
|
||||
// the previously-chosen unit — reset it so the admin re-picks from the new
|
||||
|
||||
@@ -27,7 +27,8 @@ import type { UpdateScheduleWindowRulePayload } from "@/types/trainScheduling";
|
||||
/** Fallbacks matching the API's global-rules defaults (used when a field is null). */
|
||||
const DEFAULTS = {
|
||||
windowOpenHour: 8,
|
||||
windowCloseHour: 17,
|
||||
// Equal to open ⇒ 24-hour desk (the default).
|
||||
windowCloseHour: 8,
|
||||
windowDurationHours: 3,
|
||||
docReviewMinutes: 30,
|
||||
paymentWindowMinutes: 60,
|
||||
|
||||
@@ -22,7 +22,8 @@ import type { CreateScheduleWindowRulePayload } from "@/types/trainScheduling";
|
||||
/** Fallbacks matching the API's global-rules defaults (used if the fetch fails). */
|
||||
const DEFAULTS = {
|
||||
windowOpenHour: 8,
|
||||
windowCloseHour: 17,
|
||||
// Equal to open ⇒ 24-hour desk (the default).
|
||||
windowCloseHour: 8,
|
||||
windowDurationHours: 3,
|
||||
docReviewMinutes: 30,
|
||||
paymentWindowMinutes: 60,
|
||||
|
||||
@@ -682,7 +682,11 @@ const RuleEngineResourcePage = () => {
|
||||
// ton·km, container = per km + distance band) and the currency stays as
|
||||
// chosen (birr or dollar). Everything else remains USD-only.
|
||||
const isLastMile = values.appliesTo === "LAST_MILE";
|
||||
const { lastMileMode, ...rest } = values;
|
||||
// The shipping-line toggle is form-only — the API's whitelist rejects the
|
||||
// whole payload if it leaks through ("property isShippingLineRate should
|
||||
// not exist").
|
||||
const { lastMileMode, isShippingLineRate: _toggle, ...rest } = values;
|
||||
void _toggle;
|
||||
payload = {
|
||||
...rest,
|
||||
currency: isLastMile ? (values.currency ?? "ETB") : "USD",
|
||||
|
||||
@@ -220,7 +220,7 @@ export default function ShippingLineBookingDetailPage() {
|
||||
>
|
||||
{status === "OPERATION_CHANGES_REQUESTED"
|
||||
? "Resubmit booking"
|
||||
: "Complete booking"}
|
||||
: "Book"}
|
||||
</Button>
|
||||
)}
|
||||
</Group>
|
||||
@@ -317,7 +317,7 @@ export default function ShippingLineBookingDetailPage() {
|
||||
>
|
||||
{status === "OPERATION_CHANGES_REQUESTED"
|
||||
? "Resubmit booking"
|
||||
: "Complete booking"}
|
||||
: "Book"}
|
||||
</Button>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
@@ -41,9 +41,16 @@ import {
|
||||
DOC_STATE_COLOR,
|
||||
DOC_STATE_LABEL,
|
||||
} from "./booking-doc-state";
|
||||
import ShippingLineCompleteModal from "./ShippingLineCompleteModal";
|
||||
import ShippingLineDocumentsModal from "./ShippingLineDocumentsModal";
|
||||
import ShippingLineInitiateModal from "./ShippingLineInitiateModal";
|
||||
|
||||
/** Statuses where the booking is approved and waiting to be booked. */
|
||||
const BOOKABLE_STATUSES = new Set([
|
||||
"CLEARANCE_READY",
|
||||
"OPERATION_CHANGES_REQUESTED",
|
||||
]);
|
||||
|
||||
function ColHeader({ label }: { label: string }) {
|
||||
return (
|
||||
<Text fz={12} fw={700} c="edr-muted" tt="uppercase" lts="0.04em">
|
||||
@@ -67,6 +74,9 @@ export default function ShippingLineBookingsPage() {
|
||||
const [docsBooking, setDocsBooking] = useState<ShippingLineBooking | null>(
|
||||
null,
|
||||
);
|
||||
const [bookBooking, setBookBooking] = useState<ShippingLineBooking | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const bookingsQuery = useQuery({
|
||||
queryKey: ["shipping-line-bookings"],
|
||||
@@ -154,6 +164,7 @@ export default function ShippingLineBookingsPage() {
|
||||
const state = bookingDocState(booking);
|
||||
const showDocs = hasDocuments(state);
|
||||
const wantsUpload = needsUpload(state);
|
||||
const bookable = BOOKABLE_STATUSES.has(booking.status as string);
|
||||
|
||||
return (
|
||||
<Group
|
||||
@@ -162,7 +173,24 @@ export default function ShippingLineBookingsPage() {
|
||||
wrap="nowrap"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{showDocs && (
|
||||
{/* Approved documents make booking the primary move — the docs
|
||||
button steps back into the menu so one action owns the row. */}
|
||||
{bookable && (
|
||||
<Button
|
||||
size="xs"
|
||||
radius="md"
|
||||
color="edr-green"
|
||||
fw={700}
|
||||
fz={13}
|
||||
leftSection={<PackageCheck size={14} />}
|
||||
onClick={() => setBookBooking(booking)}
|
||||
>
|
||||
{booking.status === "OPERATION_CHANGES_REQUESTED"
|
||||
? "Resubmit"
|
||||
: "Book"}
|
||||
</Button>
|
||||
)}
|
||||
{showDocs && !bookable && (
|
||||
<Button
|
||||
size="xs"
|
||||
radius="md"
|
||||
@@ -214,19 +242,14 @@ export default function ShippingLineBookingsPage() {
|
||||
{DOC_STATE_ACTION_LABEL[state]}
|
||||
</Menu.Item>
|
||||
)}
|
||||
{/* Approved documents (or an operations return) unlock the
|
||||
completion step — it lives on the detail page. */}
|
||||
{(booking.status === "CLEARANCE_READY" ||
|
||||
booking.status === "OPERATION_CHANGES_REQUESTED") && (
|
||||
{bookable && (
|
||||
<Menu.Item
|
||||
leftSection={<PackageCheck size={15} />}
|
||||
onClick={() =>
|
||||
navigate(`/shipping-line/bookings/${booking.id}`)
|
||||
}
|
||||
onClick={() => setBookBooking(booking)}
|
||||
>
|
||||
{booking.status === "OPERATION_CHANGES_REQUESTED"
|
||||
? "Resubmit booking"
|
||||
: "Complete booking"}
|
||||
: "Book"}
|
||||
</Menu.Item>
|
||||
)}
|
||||
</Menu.Dropdown>
|
||||
@@ -325,6 +348,16 @@ export default function ShippingLineBookingsPage() {
|
||||
booking={docsBooking}
|
||||
onClose={() => setDocsBooking(null)}
|
||||
/>
|
||||
|
||||
<ShippingLineCompleteModal
|
||||
booking={bookBooking}
|
||||
onClose={() => setBookBooking(null)}
|
||||
onCompleted={(booking) => {
|
||||
setBookBooking(null);
|
||||
// Straight into the booking — its status and price just changed.
|
||||
navigate(`/shipping-line/bookings/${booking.id}`);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user