feat(bookings): enhance booking process with customs clearing and document handling

- Update bookings service to prioritize uploaded documents over profile snapshots.
- Refactor pricing data seeder to remove unused service types and streamline cargo type seeding.
- Add customs clearing information to BookingRouteServiceCard, displaying agent details if applicable.
- Extend BookingDetail type to include customs clearing options.
- Modify NewBookingPage to remove the scheduling step, integrating estimated shipment date into the route step.
- Update StepIndicator to reflect the new step structure.
- Revise document handling in StepDocuments to allow for user uploads while displaying onboarding documents.
- Adjust Step2ServiceType to manage customs clearing agent input based on service type.
- Implement shipment date input in Step4Route for one-time bookings.
- Revise Step8Review to reflect changes in document handling and scheduling.
This commit is contained in:
Marshal
2026-06-24 23:37:55 +00:00
parent d7688fce21
commit 15ab9f906e
11 changed files with 566 additions and 224 deletions

View File

@@ -480,7 +480,11 @@ export class BookingsService {
// Reuse the booking profile's onboarding documents instead of asking the
// customer to re-upload. Snapshot them onto the booking now (by reference),
// so a later active-profile switch never changes this booking's documents.
if (companyProfileId) {
//
// Skip this when the customer uploaded documents for this booking — those
// per-booking files take precedence, so auto-attaching the profile snapshots
// would create duplicates.
if (companyProfileId && files.length === 0) {
try {
const onboardingFiles =
await this.companiesService.getProfileOnboardingFiles(companyProfileId);

View File

@@ -32,7 +32,7 @@ export class PricingDataSeeder {
const prRepo = manager.getRepository(PriorityConfig);
const rRepo = manager.getRepository(Rate);
await this.upsertReferenceData(manager, ctRepo, stRepo, yRepo, slRepo);
await this.upsertReferenceData(manager, ctRepo, yRepo, slRepo);
await this.seedDomesticRoute(manager, yRepo);
await this.seedWeightLimits(wlRepo, ctRepo);
await this.seedPriorityConfigs(prRepo);
@@ -71,7 +71,6 @@ export class PricingDataSeeder {
private async upsertReferenceData(
manager: any,
ctRepo: any,
stRepo: any,
yRepo: any,
slRepo: any,
): Promise<void> {
@@ -155,47 +154,7 @@ export class PricingDataSeeder {
{ conflictPaths: { code: true } },
);
await stRepo.upsert(
[
{
code: "RAIL_CONTAINER",
serviceName: "Rail Container Service",
description: "Standard rail container transport",
canBeBookedAlone: true,
includesFirstMile: false,
includesLastMile: false,
includesCustoms: false,
priorityBonusPoints: 0,
isActive: true,
displayOrder: 1,
},
{
code: "RAIL_FORWARDING",
serviceName: "Rail Forwarding Service",
description: "Rail transport with first/last mile and customs",
canBeBookedAlone: true,
includesFirstMile: true,
includesLastMile: true,
includesCustoms: true,
priorityBonusPoints: 15,
isActive: true,
displayOrder: 2,
},
{
code: "RAIL_BULK",
serviceName: "Rail Bulk Transport",
description: "Bulk commodity rail transport",
canBeBookedAlone: true,
includesFirstMile: false,
includesLastMile: false,
includesCustoms: false,
priorityBonusPoints: 10,
isActive: true,
displayOrder: 3,
},
],
{ conflictPaths: { code: true } },
);
await slRepo.upsert(
[
@@ -238,53 +197,78 @@ export class PricingDataSeeder {
{ conflictPaths: { code: true } },
);
await manager.getRepository(CargoType).upsert(
await this.seedCargoTypes(manager);
}
/**
* Cargo types are a fixed two-level tree: two top-level groups — Bulk and
* Break Bulk — each with a set of commodity children. The groups are the
* stable parents the booking wizard renders; children carry the
* unit_of_measure used when reserving quantity (PER_TON for bulk commodities,
* PER_ITEM for break-bulk items like vehicles/machinery).
*
* Parents are upserted first, then re-read by code to resolve their ids so the
* children can be linked via parent_group_id (upsert doesn't return ids).
*/
private async seedCargoTypes(manager: any): Promise<void> {
const repo = manager.getRepository(CargoType);
const groups = [
{ code: "BULK", cargoTypeName: "Bulk", displayOrder: 1 },
{ code: "BREAK_BULK", cargoTypeName: "Break Bulk", displayOrder: 2 },
];
await repo.upsert(
groups.map((g) => ({ ...g, isActive: true })),
{ conflictPaths: { code: true } },
);
const bulk = await repo.findOneBy({ code: "BULK" });
const breakBulk = await repo.findOneBy({ code: "BREAK_BULK" });
if (!bulk || !breakBulk) return;
// Bulk commodities — measured by tonnage (PER_TON).
const bulkChildren = [
{ code: "SUGAR", cargoTypeName: "Sugar" },
{ code: "GRAIN", cargoTypeName: "Grain / Cereals" },
{ code: "WHEAT", cargoTypeName: "Wheat" },
{ code: "FERTILIZER", cargoTypeName: "Fertilizer" },
{ code: "CEMENT", cargoTypeName: "Cement / Clinker" },
{ code: "COAL", cargoTypeName: "Coal" },
];
// Break-bulk items — counted as whole units (PER_ITEM).
const breakBulkChildren = [
{ code: "CARS", cargoTypeName: "Cars / Vehicles" },
{ code: "MACHINERY", cargoTypeName: "Heavy Machinery" },
{ code: "STEEL", cargoTypeName: "Steel / Rebar" },
{ code: "PIPES", cargoTypeName: "Pipes" },
{ code: "TIMBER", cargoTypeName: "Timber" },
];
await repo.upsert(
[
{
code: "GRAIN",
cargoTypeName: "Grain / Cereals",
requiresDirectorApproval: false,
...bulkChildren.map((c, i) => ({
...c,
parentGroupId: bulk.id,
unitOfMeasure: "PER_TON",
isActive: true,
displayOrder: 1,
},
{
code: "FERTILIZER",
cargoTypeName: "Fertilizer",
requiresDirectorApproval: false,
displayOrder: i + 1,
})),
...breakBulkChildren.map((c, i) => ({
...c,
parentGroupId: breakBulk.id,
unitOfMeasure: "PER_ITEM",
isActive: true,
displayOrder: 2,
},
{
code: "CEMENT",
cargoTypeName: "Cement / Clinker",
requiresDirectorApproval: false,
isActive: true,
displayOrder: 3,
},
{
code: "STEEL",
cargoTypeName: "Steel / Rebar",
requiresDirectorApproval: true,
isActive: true,
displayOrder: 4,
},
{
code: "MACHINERY",
cargoTypeName: "Heavy Machinery",
requiresDirectorApproval: true,
isActive: true,
displayOrder: 5,
},
{
code: "OTHER_BULK",
cargoTypeName: "Other Bulk Cargo",
requiresDirectorApproval: false,
isActive: true,
displayOrder: 6,
},
displayOrder: i + 1,
})),
],
{ conflictPaths: { code: true } },
);
// Retire the old flat "Other Bulk Cargo" top-level type from earlier seeds so
// it no longer shows alongside the Bulk / Break Bulk groups. No-op on a fresh
// DB where it was never seeded.
await repo.update({ code: "OTHER_BULK" }, { isActive: false });
}
private async seedDomesticRoute(manager: any, yRepo: any): Promise<void> {