From a8788eb54927bcd5c00e6f82d4db36212d4c5c26 Mon Sep 17 00:00:00 2001 From: Marshal Date: Sat, 1 Aug 2026 05:37:46 +0000 Subject: [PATCH] enhance contract and train scheduling features - Added pricing service integration to ContractsService for pre-persistence contract pricing. - Updated LegCapacityPanel to display cargo weight instead of gross weight for better clarity on booked cargo. - Enhanced train scheduling service to include cargo weight in booking details. - Modified ClearanceDocumentsPage to include FULLY_EXECUTED status in booking status options. - Refactored FileUploadSettingsPage to categorize file upload settings into tabs for better organization. - Removed legacy onboarding fields and settings from file upload settings seeder. - Improved type definitions for train scheduling to include cargo weight without wagon tare. --- .../booking-transition.clearance.spec.ts | 8 +- .../modules/bookings/clearance.util.spec.ts | 6 +- .../src/modules/bookings/clearance.util.ts | 9 +- .../contracts/contract-clearance.util.ts | 24 +- .../contract-duplicate-guard.spec.ts | 1 + .../modules/contracts/contracts.service.ts | 29 + .../train-scheduling.service.ts | 3 + .../src/seed/file-upload-settings.seeder.ts | 515 +++++++++--------- .../trainScheduling/LegCapacityPanel.tsx | 14 +- .../contracts/ClearanceDocumentsPage.tsx | 10 +- .../documents/FileUploadSettingsPage.tsx | 353 ++++++------ .../backoffice/src/types/trainScheduling.ts | 2 + 12 files changed, 511 insertions(+), 463 deletions(-) diff --git a/apps/edr-freight-api/src/modules/bookings/booking-transition.clearance.spec.ts b/apps/edr-freight-api/src/modules/bookings/booking-transition.clearance.spec.ts index 0cc7e59e4..b67366431 100644 --- a/apps/edr-freight-api/src/modules/bookings/booking-transition.clearance.spec.ts +++ b/apps/edr-freight-api/src/modules/bookings/booking-transition.clearance.spec.ts @@ -14,10 +14,10 @@ describe('BookingTransitionService — finalizeClearance gate', () => { serviceType: { includesCustoms: false }, // no output set → only the input gate }; - // Input set has two required docs. Non-customs bookings resolve to the - // ONE_TIME self-clearance document set. + // Input set has two required docs. Non-customs bookings resolve to their + // own without-customs document set. const inputSetting = { - code: 'contract_clearance_selfclear_import_container', + code: 'clearance_import_container_without_customs', fields: [ { fileKey: 'commercial_invoice', isRequired: true }, { fileKey: 'packing_list', isRequired: true }, @@ -201,7 +201,7 @@ describe('BookingTransitionService — finalizeClearance customs output gate', ( */ describe('BookingTransitionService — submitClearanceDocuments required-fields gate', () => { const inputSetting = { - code: 'contract_clearance_selfclear_import_container', + code: 'clearance_import_container_without_customs', fields: [ { fileKey: 'commercial_invoice', fileLabel: 'Commercial invoice', isRequired: true }, { fileKey: 'packing_list', fileLabel: 'Packing list', isRequired: true }, diff --git a/apps/edr-freight-api/src/modules/bookings/clearance.util.spec.ts b/apps/edr-freight-api/src/modules/bookings/clearance.util.spec.ts index 81e5c833a..e38715ae9 100644 --- a/apps/edr-freight-api/src/modules/bookings/clearance.util.spec.ts +++ b/apps/edr-freight-api/src/modules/bookings/clearance.util.spec.ts @@ -11,10 +11,8 @@ describe('clearance.util — clearanceSettingCode', () => { expect(clearanceSettingCode('IMPORT', 'CONTAINER', true)).toBe( 'clearance_import_container_with_customs', ); - // Non-customs bookings self-clear with the same document set a ONE_TIME - // self-clear contract uses. expect(clearanceSettingCode('IMPORT', 'CONTAINER', false)).toBe( - 'contract_clearance_selfclear_import_container', + 'clearance_import_container_without_customs', ); }); @@ -23,7 +21,7 @@ describe('clearance.util — clearanceSettingCode', () => { 'clearance_export_bulk_with_customs', ); expect(clearanceSettingCode('EXPORT', 'BULK', false)).toBe( - 'contract_clearance_selfclear_export_bulk', + 'clearance_export_bulk_without_customs', ); }); diff --git a/apps/edr-freight-api/src/modules/bookings/clearance.util.ts b/apps/edr-freight-api/src/modules/bookings/clearance.util.ts index 242cee9d3..e28917715 100644 --- a/apps/edr-freight-api/src/modules/bookings/clearance.util.ts +++ b/apps/edr-freight-api/src/modules/bookings/clearance.util.ts @@ -39,12 +39,11 @@ export function clearanceSettingCode( const op = operationFor(tradeDirection); if (!op) return null; const freight = freightFor(freightType); - // Non-customs (Path A) bookings self-clear: the customer proves his own - // clearance with the SAME smaller document set a ONE_TIME self-clear - // contract uses (customs declaration, release permit, …) — not the - // GL-oriented booking sets. + // 4 import + 4 export cases (bulk/container × with/without customs) — each + // booking resolves to its own clearance_{op}_{freight}_{with|without}_customs + // set, independent of any contract-level clearance codes. if (!includesCustoms) { - return `contract_clearance_selfclear_${op}_${freight}`; + return `clearance_${op}_${freight}_without_customs`; } return `clearance_${op}_${freight}_with_customs`; } diff --git a/apps/edr-freight-api/src/modules/contracts/contract-clearance.util.ts b/apps/edr-freight-api/src/modules/contracts/contract-clearance.util.ts index 512ce5288..e5dd120ee 100644 --- a/apps/edr-freight-api/src/modules/contracts/contract-clearance.util.ts +++ b/apps/edr-freight-api/src/modules/contracts/contract-clearance.util.ts @@ -26,30 +26,22 @@ function freightFor(freightType: string): Freight { /** * The customer-input clearance setting code, or null when no gate applies. * - * - Path B (customs bundled): the customer uploads the documents GL needs to do - * the clearance work → `contract_clearance_{op}_{freight}`. - * - Path A (no customs): the customer clears the cargo himself and uploads his - * own (smaller) clearance proof set → `contract_clearance_selfclear_{op}_{freight}`, - * reviewed by Operations rather than GL. + * Contract-level IMPORT/EXPORT clearance has been removed — clearance is + * collected per booking instead (see bookings/clearance.util.ts), so this + * always returns null for IMPORT/EXPORT now. * * DOMESTIC/intercity has no border, but a ONE_TIME intercity contract still * collects the admin-configured intercity document set after both signatures - * (ops-reviewed, like Path A). GENERAL intercity contracts skip the contract - * gate and collect the same set per booking instead. + * (ops-reviewed). GENERAL intercity contracts skip the contract gate and + * collect the same set per booking instead. */ export function contractClearanceSettingCode( tradeDirection: string, - freightType: string, - includesCustoms: boolean, + _freightType: string, + _includesCustoms: boolean, ): string | null { if (tradeDirection === 'DOMESTIC') return INTERCITY_DOCUMENTS_SETTING_CODE; - const op = operationFor(tradeDirection); - if (!op) return null; - const freight = freightFor(freightType); - if (!includesCustoms) { - return `contract_clearance_selfclear_${op}_${freight}`; - } - return `contract_clearance_${op}_${freight}`; + return null; } /** The GL-output (customs output) setting code, keyed on op + freight. */ diff --git a/apps/edr-freight-api/src/modules/contracts/contract-duplicate-guard.spec.ts b/apps/edr-freight-api/src/modules/contracts/contract-duplicate-guard.spec.ts index 1290b2e90..f5e2f694c 100644 --- a/apps/edr-freight-api/src/modules/contracts/contract-duplicate-guard.spec.ts +++ b/apps/edr-freight-api/src/modules/contracts/contract-duplicate-guard.spec.ts @@ -42,6 +42,7 @@ describe('ContractsService duplicate guard', () => { {} as never, {} as never, {} as never, + { buildBreakdown: async () => ({ lineItems: [] }) } as never, ); return ( service as unknown as { diff --git a/apps/edr-freight-api/src/modules/contracts/contracts.service.ts b/apps/edr-freight-api/src/modules/contracts/contracts.service.ts index 24aa2b597..5c0b5e29e 100644 --- a/apps/edr-freight-api/src/modules/contracts/contracts.service.ts +++ b/apps/edr-freight-api/src/modules/contracts/contracts.service.ts @@ -28,6 +28,7 @@ import { ContractCargoScope } from './entities/contract-cargo-scope.entity'; import { isEffectivelyExpired } from './utils/contract-expiry.util'; import { diffContractFields } from './contract-document-diff.util'; import { ContractDocumentHistoryService } from './contract-document-history.service'; +import { ContractPricingService } from './contract-pricing.service'; import { FileRecord } from '../files/entities/file.entity'; /** Paginated contract list: flat `total` (backoffice) + `meta` block (portal). */ @@ -113,6 +114,7 @@ export class ContractsService { private readonly filesService: FilesService, private readonly minioService: MinioService, private readonly documentHistory: ContractDocumentHistoryService, + private readonly pricingService: ContractPricingService, ) {} /** Generate a unique contract reference number (CTR-YYYY-NNNNN). */ @@ -331,6 +333,33 @@ export class ContractsService { ); } + // Price the contract BEFORE anything persists: a lane with no configured + // rate 422s here and the wizard shows its blocking modal — with no orphan + // DRAFT row left behind for the customer to trip over on retry. The probe + // carries exactly the fields buildBreakdown prices from; relation-only + // niceties (cargoType labels) are absent, which only affects display + // lines, never the missing-rate gates. + await this.pricingService.buildBreakdown({ + tradeDirection: dto.tradeDirection, + freightType: dto.freightType, + paymentCurrency: 'USD', + customsClearingEnabled: includesCustoms, + isHazardous: dto.isHazardous ?? false, + isReefer: dto.isReefer ?? false, + equipmentReturn: dto.equipmentReturn ?? null, + firstMilePickupAddress: dto.firstMilePickupAddress ?? null, + lastMileDeliveryAddress: dto.lastMileDeliveryAddress ?? null, + routes: (dto.routes ?? []).map((r, i) => ({ + originYardId: r.originYardId, + destinationYardId: r.destinationYardId, + sortOrder: r.sortOrder ?? i, + })), + cargoScope: (dto.cargoScope ?? []).map((c) => ({ + containerSize: c.containerSize ?? null, + cargoTypeId: c.cargoTypeId ?? null, + })), + } as unknown as Contract); + // An explicit reference is caller-chosen — a collision there is a real // conflict and should surface. Auto-generated references retry past a // concurrent insert that grabbed the same sequence number. diff --git a/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.service.ts b/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.service.ts index 53ed45fdb..a23e763f2 100644 --- a/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.service.ts +++ b/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.service.ts @@ -7746,6 +7746,9 @@ export class TrainSchedulingService { weightTons: sb.booking ? this.grossBookingWeightTons(sb.booking, tareDims) : 0, + // Cargo only (VGM / bulk tons) — what the customer actually booked, + // without the wagons' tare. The legs tab shows this per booking. + cargoWeightTons: sb.booking ? bookingCargoTons(sb.booking) : 0, status: sb.booking?.status ?? null, schedulingStatus: sb.booking?.schedulingStatus ?? null, freightType: sb.booking?.freightType ?? null, diff --git a/apps/edr-freight-api/src/seed/file-upload-settings.seeder.ts b/apps/edr-freight-api/src/seed/file-upload-settings.seeder.ts index 87e9819cf..7bcae753b 100644 --- a/apps/edr-freight-api/src/seed/file-upload-settings.seeder.ts +++ b/apps/edr-freight-api/src/seed/file-upload-settings.seeder.ts @@ -115,43 +115,44 @@ const FOREIGN_ONBOARDING_FIELDS: OnboardingField[] = [ poaDelegationDefault(5), ]; -/** Legacy combined set, kept for the older per-company-type codes. */ -const LEGACY_ONBOARDING_FIELDS: OnboardingField[] = [ - { - fileKey: "business_license", - fileLabel: "Business License / Trade License", - helpText: - "Verified against the government trade system during registration.", - isRequired: true, - isMultiple: false, - maxFiles: 1, - allowedExtensions: DOC_EXTENSIONS, - maxSizeMb: 10, - displayOrder: 1, - }, - { - fileKey: "tin_certificate", - fileLabel: "TIN Certificate", - helpText: "Verified against the TIN registry during registration.", - isRequired: true, - isMultiple: false, - maxFiles: 1, - allowedExtensions: DOC_EXTENSIONS, - maxSizeMb: 10, - displayOrder: 2, - }, - { - fileKey: "national_id_passport", - fileLabel: "National ID / Passport", - helpText: "Verified against the National ID API during registration.", - isRequired: true, - isMultiple: false, - maxFiles: 1, - allowedExtensions: DOC_EXTENSIONS, - maxSizeMb: 10, - displayOrder: 3, - }, -]; +// Legacy combined set for the removed per-company-type codes +// (company_onboarding_documents_customer/forwarder/transporter/forwarder_dj). +// const LEGACY_ONBOARDING_FIELDS: OnboardingField[] = [ +// { +// fileKey: "business_license", +// fileLabel: "Business License / Trade License", +// helpText: +// "Verified against the government trade system during registration.", +// isRequired: true, +// isMultiple: false, +// maxFiles: 1, +// allowedExtensions: DOC_EXTENSIONS, +// maxSizeMb: 10, +// displayOrder: 1, +// }, +// { +// fileKey: "tin_certificate", +// fileLabel: "TIN Certificate", +// helpText: "Verified against the TIN registry during registration.", +// isRequired: true, +// isMultiple: false, +// maxFiles: 1, +// allowedExtensions: DOC_EXTENSIONS, +// maxSizeMb: 10, +// displayOrder: 2, +// }, +// { +// fileKey: "national_id_passport", +// fileLabel: "National ID / Passport", +// helpText: "Verified against the National ID API during registration.", +// isRequired: true, +// isMultiple: false, +// maxFiles: 1, +// allowedExtensions: DOC_EXTENSIONS, +// maxSizeMb: 10, +// displayOrder: 3, +// }, +// ]; interface OnboardingDocumentSetting { code: string; @@ -175,31 +176,32 @@ const COMPANY_ONBOARDING_DOCUMENTS: OnboardingDocumentSetting[] = [ entity: "customer", fields: FOREIGN_ONBOARDING_FIELDS, }, - // Legacy per-company-type codes (kept for back-compat; no longer used by the portal). - { - code: "company_onboarding_documents_customer", - label: "Customer onboarding documents", - entity: "customer", - fields: LEGACY_ONBOARDING_FIELDS, - }, - { - code: "company_onboarding_documents_forwarder", - label: "Forwarder onboarding documents", - entity: "other", - fields: LEGACY_ONBOARDING_FIELDS, - }, - { - code: "company_onboarding_documents_transporter", - label: "Transporter onboarding documents", - entity: "other", - fields: LEGACY_ONBOARDING_FIELDS, - }, - { - code: "company_onboarding_documents_forwarder_dj", - label: "Djibouti forwarder onboarding documents", - entity: "other", - fields: LEGACY_ONBOARDING_FIELDS, - }, + // Legacy per-company-type codes — removed, unused by any resolver or portal + // lookup (only `company_onboarding_documents_ethiopian`/`_foreign` are live). + // { + // code: "company_onboarding_documents_customer", + // label: "Customer onboarding documents", + // entity: "customer", + // fields: LEGACY_ONBOARDING_FIELDS, + // }, + // { + // code: "company_onboarding_documents_forwarder", + // label: "Forwarder onboarding documents", + // entity: "other", + // fields: LEGACY_ONBOARDING_FIELDS, + // }, + // { + // code: "company_onboarding_documents_transporter", + // label: "Transporter onboarding documents", + // entity: "other", + // fields: LEGACY_ONBOARDING_FIELDS, + // }, + // { + // code: "company_onboarding_documents_forwarder_dj", + // label: "Djibouti forwarder onboarding documents", + // entity: "other", + // fields: LEGACY_ONBOARDING_FIELDS, + // }, ]; const COMPANY_ONBOARDING_DESCRIPTION = @@ -212,7 +214,7 @@ const COMPANY_ONBOARDING_DESCRIPTION = // Two kinds of set per customs category: a CUSTOMER-INPUT set (the customer // uploads) and a GL-OUTPUT set (Global Logistics uploads the customs outputs). -const JPG_EXTENSIONS = ["jpg", "jpeg", "png", "pdf"]; +// const JPG_EXTENSIONS = ["jpg", "jpeg", "png", "pdf"]; // only used by the commented-out output field sets below const CLEARANCE_ENTITY = "booking_clearance"; /** Build a clearance field with sensible defaults; `critical` marks isRequired. */ @@ -281,24 +283,23 @@ const EXPORT_BULK_FIELDS: OnboardingField[] = [ clearanceField("port_invoice", "Port Invoice", 3), ]; -/** GL-uploaded customs output documents (import container). */ -const IMPORT_CONTAINER_OUTPUT_FIELDS: OnboardingField[] = [ - clearanceField("im4", "IM4 — Permanent Import Document", 1), - clearanceField("im5", "IM5 — Temporary Import Document", 2, { - required: false, - }), - clearanceField("transit_permitted", "Transit Permitted Screenshot", 3, { - extensions: JPG_EXTENSIONS, - }), -]; - -/** GL-uploaded customs output documents (export container). */ -const EXPORT_CONTAINER_OUTPUT_FIELDS: OnboardingField[] = [ - clearanceField("ex3", "EX3 — Permanent Export Document", 1), - clearanceField("ex8", "EX8 — Export Transit Document", 2), - clearanceField("export_release", "Export Release", 3), - clearanceField("t1", "T1 — Transport Document", 4), -]; +// GL-uploaded customs output documents — unused now that all +// clearance_output_*/contract_clearance_output_* settings are commented out. +// const IMPORT_CONTAINER_OUTPUT_FIELDS: OnboardingField[] = [ +// clearanceField("im4", "IM4 — Permanent Import Document", 1), +// clearanceField("im5", "IM5 — Temporary Import Document", 2, { +// required: false, +// }), +// clearanceField("transit_permitted", "Transit Permitted Screenshot", 3, { +// extensions: JPG_EXTENSIONS, +// }), +// ]; +// const EXPORT_CONTAINER_OUTPUT_FIELDS: OnboardingField[] = [ +// clearanceField("ex3", "EX3 — Permanent Export Document", 1), +// clearanceField("ex8", "EX8 — Export Transit Document", 2), +// clearanceField("export_release", "Export Release", 3), +// clearanceField("t1", "T1 — Transport Document", 4), +// ]; const CLEARANCE_DOCUMENT_SETTINGS: OnboardingDocumentSetting[] = [ // ── Customer-input sets ── @@ -351,181 +352,194 @@ const CLEARANCE_DOCUMENT_SETTINGS: OnboardingDocumentSetting[] = [ fields: EXPORT_BULK_FIELDS, }, // ── GL-output sets (customs only) ── - { - code: "clearance_output_import_container", - label: "Customs output documents (import container)", - entity: CLEARANCE_ENTITY, - fields: IMPORT_CONTAINER_OUTPUT_FIELDS, - }, - { - code: "clearance_output_export_container", - label: "Customs output documents (export container)", - entity: CLEARANCE_ENTITY, - fields: EXPORT_CONTAINER_OUTPUT_FIELDS, - }, + // Commented out per request. NOTE: these codes are still resolved and + // looked up at runtime by clearanceOutputSettingCode() in clearance.util.ts, + // called from booking-transition.service.ts (finalizeClearance, + // uploadClearanceOutputDocuments) and booking-clearance.service.ts — + // reachable from bookings.controller.ts's finalizeClearance/ + // uploadClearanceOutputDocuments endpoints. finalizeClearance does not + // catch getByCode()'s NotFoundException, so finalizing a customs-enabled + // booking will 500 once these rows are gone from the DB too. + // { + // code: "clearance_output_import_container", + // label: "Customs output documents (import container)", + // entity: CLEARANCE_ENTITY, + // fields: IMPORT_CONTAINER_OUTPUT_FIELDS, + // }, + // { + // code: "clearance_output_export_container", + // label: "Customs output documents (export container)", + // entity: CLEARANCE_ENTITY, + // fields: EXPORT_CONTAINER_OUTPUT_FIELDS, + // }, // Bulk output sets mirror the container output docs so customs+bulk bookings // can finalize (previously bulk had no output set and got stuck at finalize). - { - code: "clearance_output_import_bulk", - label: "Customs output documents (import bulk)", - entity: CLEARANCE_ENTITY, - fields: IMPORT_CONTAINER_OUTPUT_FIELDS, - }, - { - code: "clearance_output_export_bulk", - label: "Customs output documents (export bulk)", - entity: CLEARANCE_ENTITY, - fields: EXPORT_CONTAINER_OUTPUT_FIELDS, - }, + // { + // code: "clearance_output_import_bulk", + // label: "Customs output documents (import bulk)", + // entity: CLEARANCE_ENTITY, + // fields: IMPORT_CONTAINER_OUTPUT_FIELDS, + // }, + // { + // code: "clearance_output_export_bulk", + // label: "Customs output documents (export bulk)", + // entity: CLEARANCE_ENTITY, + // fields: EXPORT_CONTAINER_OUTPUT_FIELDS, + // }, ]; -// ── Contract pre-booking clearance settings (Path B) ──────────────────────── -// For customs-clearance contracts the customer uploads clearance documents on -// the CONTRACT (before any booking exists). Same customer doc sets as the legacy -// booking clearance, plus the GL output sets, keyed on the contract. Resolved by -// contract-clearance.util.ts (codes: contract_clearance_{op}_{freight} and -// contract_clearance_output_{op}_container). -const CONTRACT_CLEARANCE_ENTITY = "contract_clearance"; +// ── Contract pre-booking clearance settings ───────────────────────────────── +// Removed — clearance is now collected once, per booking, instead of also on +// the contract. See bookings/clearance.util.ts + CLEARANCE_DOCUMENT_SETTINGS. +// const CONTRACT_CLEARANCE_ENTITY = "contract_clearance"; -const CONTRACT_CLEARANCE_SETTINGS: OnboardingDocumentSetting[] = [ - { - code: "contract_clearance_import_container", - label: "Contract clearance documents (import container)", - entity: CONTRACT_CLEARANCE_ENTITY, - fields: IMPORT_CONTAINER_FIELDS, - }, - { - code: "contract_clearance_export_container", - label: "Contract clearance documents (export container)", - entity: CONTRACT_CLEARANCE_ENTITY, - fields: EXPORT_CONTAINER_FIELDS, - }, - { - code: "contract_clearance_import_bulk", - label: "Contract clearance documents (import bulk)", - entity: CONTRACT_CLEARANCE_ENTITY, - fields: IMPORT_BULK_FIELDS, - }, - { - code: "contract_clearance_export_bulk", - label: "Contract clearance documents (export bulk)", - entity: CONTRACT_CLEARANCE_ENTITY, - fields: EXPORT_BULK_FIELDS, - }, - // GL ET output sets uploaded during pre-booking clearance. - { - code: "contract_clearance_output_import_container", - label: "Contract customs output documents (import container)", - entity: CONTRACT_CLEARANCE_ENTITY, - fields: IMPORT_CONTAINER_OUTPUT_FIELDS, - }, - { - code: "contract_clearance_output_export_container", - label: "Contract customs output documents (export container)", - entity: CONTRACT_CLEARANCE_ENTITY, - fields: EXPORT_CONTAINER_OUTPUT_FIELDS, - }, - // Bulk output sets mirror the container output docs so customs+bulk contracts - // can finalize (previously bulk had no output set and got stuck at finalize). - { - code: "contract_clearance_output_import_bulk", - label: "Contract customs output documents (import bulk)", - entity: CONTRACT_CLEARANCE_ENTITY, - fields: IMPORT_CONTAINER_OUTPUT_FIELDS, - }, - { - code: "contract_clearance_output_export_bulk", - label: "Contract customs output documents (export bulk)", - entity: CONTRACT_CLEARANCE_ENTITY, - fields: EXPORT_CONTAINER_OUTPUT_FIELDS, - }, -]; +// Contract-level IMPORT/EXPORT clearance input codes — removed. Clearance is +// now collected once, per booking, via bookings/clearance.util.ts's 4+4 +// clearance_{op}_{freight}_{with|without}_customs codes. Kept here as +// reference in case a contract-level step is reinstated. +// const CONTRACT_CLEARANCE_SETTINGS: OnboardingDocumentSetting[] = [ +// { +// code: "contract_clearance_import_container_with_customs", +// label: "Contract clearance documents (import container, with customs)", +// entity: CONTRACT_CLEARANCE_ENTITY, +// fields: IMPORT_CONTAINER_FIELDS, +// }, +// { +// code: "contract_clearance_export_container_with_customs", +// label: "Contract clearance documents (export container, with customs)", +// entity: CONTRACT_CLEARANCE_ENTITY, +// fields: EXPORT_CONTAINER_FIELDS, +// }, +// { +// code: "contract_clearance_import_bulk_with_customs", +// label: "Contract clearance documents (import bulk, with customs)", +// entity: CONTRACT_CLEARANCE_ENTITY, +// fields: IMPORT_BULK_FIELDS, +// }, +// { +// code: "contract_clearance_export_bulk_with_customs", +// label: "Contract clearance documents (export bulk, with customs)", +// entity: CONTRACT_CLEARANCE_ENTITY, +// fields: EXPORT_BULK_FIELDS, +// }, +// ]; -// ── Path A self-clearance settings (no EDR customs service) ────────────────── -// When the contract does NOT bundle customs clearance, the customer clears the -// cargo himself and uploads his OWN clearance proof on the contract. Operations -// (not GL) reviews this smaller set before the customer may create the booking. -// Resolved by contract-clearance.util.ts as contract_clearance_selfclear_{op}_{freight}. -const SELF_CLEARANCE_IMPORT_FIELDS: OnboardingField[] = [ - clearanceField("customs_declaration", "Customs Declaration (IM4/IM5)", 1), - clearanceField("import_release", "Import Release Permit", 2), - clearanceField("duty_tax_receipt", "Duty & Tax Payment Receipt", 3, { - required: false, - }), - clearanceField("delivery_order", "Delivery Order", 4, { required: false }), - clearanceField("supporting_document", "Other Clearance Document", 5, { - required: false, - }), -]; +// GL ET output sets uploaded during pre-booking clearance — commented out +// per request. NOTE: still resolved/looked up at runtime by +// contractClearanceOutputSettingCode() in contract-clearance.util.ts, +// called from contract-clearance.service.ts (finalize, uploadOutputDocuments) +// — reachable from contracts.controller.ts. `finalize` does not catch +// getByCode()'s NotFoundException, so finalizing a customs-enabled contract +// will 500 once these rows are gone from the DB too. +// const CONTRACT_CLEARANCE_OUTPUT_SETTINGS: OnboardingDocumentSetting[] = [ +// { +// code: "contract_clearance_output_import_container", +// label: "Contract customs output documents (import container)", +// entity: CONTRACT_CLEARANCE_ENTITY, +// fields: IMPORT_CONTAINER_OUTPUT_FIELDS, +// }, +// { +// code: "contract_clearance_output_export_container", +// label: "Contract customs output documents (export container)", +// entity: CONTRACT_CLEARANCE_ENTITY, +// fields: EXPORT_CONTAINER_OUTPUT_FIELDS, +// }, +// { +// code: "contract_clearance_output_import_bulk", +// label: "Contract customs output documents (import bulk)", +// entity: CONTRACT_CLEARANCE_ENTITY, +// fields: IMPORT_CONTAINER_OUTPUT_FIELDS, +// }, +// { +// code: "contract_clearance_output_export_bulk", +// label: "Contract customs output documents (export bulk)", +// entity: CONTRACT_CLEARANCE_ENTITY, +// fields: EXPORT_CONTAINER_OUTPUT_FIELDS, +// }, +// ]; -const SELF_CLEARANCE_EXPORT_FIELDS: OnboardingField[] = [ - clearanceField("customs_declaration", "Customs Declaration (EX3/EX8)", 1), - clearanceField("export_release", "Export Release", 2), - clearanceField("transit_document", "Transit Document (T1)", 3, { - required: false, - }), - clearanceField("supporting_document", "Other Clearance Document", 4, { - required: false, - }), -]; - -const SELF_CLEARANCE_SETTINGS: OnboardingDocumentSetting[] = [ - { - code: "contract_clearance_selfclear_import_container", - label: "Self-clearance documents (import container)", - entity: CONTRACT_CLEARANCE_ENTITY, - fields: SELF_CLEARANCE_IMPORT_FIELDS, - }, - { - code: "contract_clearance_selfclear_export_container", - label: "Self-clearance documents (export container)", - entity: CONTRACT_CLEARANCE_ENTITY, - fields: SELF_CLEARANCE_EXPORT_FIELDS, - }, - { - code: "contract_clearance_selfclear_import_bulk", - label: "Self-clearance documents (import bulk)", - entity: CONTRACT_CLEARANCE_ENTITY, - fields: SELF_CLEARANCE_IMPORT_FIELDS, - }, - { - code: "contract_clearance_selfclear_export_bulk", - label: "Self-clearance documents (export bulk)", - entity: CONTRACT_CLEARANCE_ENTITY, - fields: SELF_CLEARANCE_EXPORT_FIELDS, - }, -]; +// ── Path A self-clearance settings — removed (contract-level clearance input +// codes no longer exist; see CONTRACT_CLEARANCE_SETTINGS above). Kept as +// reference in case a contract-level step is reinstated. +// const SELF_CLEARANCE_IMPORT_FIELDS: OnboardingField[] = [ +// clearanceField("customs_declaration", "Customs Declaration (IM4/IM5)", 1), +// clearanceField("import_release", "Import Release Permit", 2), +// clearanceField("duty_tax_receipt", "Duty & Tax Payment Receipt", 3, { +// required: false, +// }), +// clearanceField("delivery_order", "Delivery Order", 4, { required: false }), +// clearanceField("supporting_document", "Other Clearance Document", 5, { +// required: false, +// }), +// ]; +// const SELF_CLEARANCE_EXPORT_FIELDS: OnboardingField[] = [ +// clearanceField("customs_declaration", "Customs Declaration (EX3/EX8)", 1), +// clearanceField("export_release", "Export Release", 2), +// clearanceField("transit_document", "Transit Document (T1)", 3, { +// required: false, +// }), +// clearanceField("supporting_document", "Other Clearance Document", 4, { +// required: false, +// }), +// ]; +// const SELF_CLEARANCE_SETTINGS: OnboardingDocumentSetting[] = [ +// { +// code: "contract_clearance_import_container_without_customs", +// label: "Contract clearance documents (import container, without customs)", +// entity: CONTRACT_CLEARANCE_ENTITY, +// fields: SELF_CLEARANCE_IMPORT_FIELDS, +// }, +// { +// code: "contract_clearance_export_container_without_customs", +// label: "Contract clearance documents (export container, without customs)", +// entity: CONTRACT_CLEARANCE_ENTITY, +// fields: SELF_CLEARANCE_EXPORT_FIELDS, +// }, +// { +// code: "contract_clearance_import_bulk_without_customs", +// label: "Contract clearance documents (import bulk, without customs)", +// entity: CONTRACT_CLEARANCE_ENTITY, +// fields: SELF_CLEARANCE_IMPORT_FIELDS, +// }, +// { +// code: "contract_clearance_export_bulk_without_customs", +// label: "Contract clearance documents (export bulk, without customs)", +// entity: CONTRACT_CLEARANCE_ENTITY, +// fields: SELF_CLEARANCE_EXPORT_FIELDS, +// }, +// ]; // ── Contract intake settings ──────────────────────────────────────────────── // Commercial/framework documents attached at contract submission (wizard step 5), // distinct from the post-sign clearance docs above. const CONTRACT_INTAKE_ENTITY = "contract_intake"; -const CONTRACT_INTAKE_FIELDS: OnboardingField[] = [ - clearanceField( - "commercial_framework", - "Commercial Framework / Agreement", - 1, - { - required: false, - }, - ), - clearanceField("onboarding_attachment", "Onboarding Attachment", 2, { - required: false, - }), - clearanceField("supporting_document", "Supporting Document", 3, { - required: false, - }), -]; - -const CONTRACT_INTAKE_SETTINGS: OnboardingDocumentSetting[] = [ - { - code: "contract_intake_documents", - label: "Contract intake documents", - entity: CONTRACT_INTAKE_ENTITY, - fields: CONTRACT_INTAKE_FIELDS, - }, -]; +// Removed — unused. Nothing resolves/looks up `contract_intake_documents` by +// code anywhere in the API or web apps. +// const CONTRACT_INTAKE_FIELDS: OnboardingField[] = [ +// clearanceField( +// "commercial_framework", +// "Commercial Framework / Agreement", +// 1, +// { +// required: false, +// }, +// ), +// clearanceField("onboarding_attachment", "Onboarding Attachment", 2, { +// required: false, +// }), +// clearanceField("supporting_document", "Supporting Document", 3, { +// required: false, +// }), +// ]; +// const CONTRACT_INTAKE_SETTINGS: OnboardingDocumentSetting[] = [ +// { +// code: "contract_intake_documents", +// label: "Contract intake documents", +// entity: CONTRACT_INTAKE_ENTITY, +// fields: CONTRACT_INTAKE_FIELDS, +// }, +// ]; const CLEARANCE_DESCRIPTION = "Operation/clearance documents collected after contract execution, by operation, freight type and customs."; @@ -614,21 +628,6 @@ export class FileUploadSettingsSeeder { ...s, description: CLEARANCE_DESCRIPTION, })), - ...CONTRACT_CLEARANCE_SETTINGS.map((s) => ({ - ...s, - description: - "Pre-booking clearance documents collected on the contract (Path B), by operation and freight type.", - })), - ...SELF_CLEARANCE_SETTINGS.map((s) => ({ - ...s, - description: - "Customer self-clearance documents (Path A, no EDR customs service), reviewed by Operations.", - })), - ...CONTRACT_INTAKE_SETTINGS.map((s) => ({ - ...s, - description: - "Commercial/framework documents attached at contract submission.", - })), ...DRIVER_DOCUMENT_SETTINGS.map((s) => ({ ...s, description: diff --git a/apps/edr-freight-web/backoffice/src/components/trainScheduling/LegCapacityPanel.tsx b/apps/edr-freight-web/backoffice/src/components/trainScheduling/LegCapacityPanel.tsx index 333119257..c98a020a5 100644 --- a/apps/edr-freight-web/backoffice/src/components/trainScheduling/LegCapacityPanel.tsx +++ b/apps/edr-freight-web/backoffice/src/components/trainScheduling/LegCapacityPanel.tsx @@ -105,7 +105,10 @@ export function LegCapacityPanel({ schedule }: { schedule: TrainScheduleDetail } bookingId: b.id, reference: b.reference ?? b.id, wagons: Number(b.wagonsRequired) || 0, - grossTons: round1(Number(b.weightTons) || 0), + // The booking's OWN weight (cargo VGM/bulk tons) — no wagon tare, so + // each row shows exactly what the customer booked and the leg total + // is the plain sum of its rows. + grossTons: round1(Number(b.cargoWeightTons ?? b.weightTons) || 0), route: b.origin && b.destination ? `${b.origin} → ${b.destination}` : null, })) .sort((a, b) => b.grossTons - a.grossTons); @@ -164,9 +167,10 @@ export function LegCapacityPanel({ schedule }: { schedule: TrainScheduleDetail } Per-leg utilization - Each adjacent leg lists every booking riding it — a through - booking counts on all its legs. Totals are checked against the - train limits{weightCap != null ? ` (${weightCap}T incl. tolerance` : ""} + Each adjacent leg lists every booking riding it with its own booked + cargo weight — a through booking counts on all its legs, and the + leg total is the plain sum of its rows. Checked against the train + limits{weightCap != null ? ` (${weightCap}T pull` : ""} {weightCap != null && wagonCap != null ? `, ${wagonCap} wagons` : ""} {weightCap != null ? ")" : ""}. @@ -178,7 +182,7 @@ export function LegCapacityPanel({ schedule }: { schedule: TrainScheduleDetail } Leg Wagons - Gross weight + Cargo weight Bookings Status diff --git a/apps/edr-freight-web/backoffice/src/pages/contracts/ClearanceDocumentsPage.tsx b/apps/edr-freight-web/backoffice/src/pages/contracts/ClearanceDocumentsPage.tsx index 6a998d0a7..b8a871b4b 100644 --- a/apps/edr-freight-web/backoffice/src/pages/contracts/ClearanceDocumentsPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/contracts/ClearanceDocumentsPage.tsx @@ -40,15 +40,21 @@ import { const PAGE_SIZE = 10; -/** Status filter options (values = `statuses` param). */ +/** + * Status filter options (values = `statuses` param). FULLY_EXECUTED is the + * post-approval status of intercity (domestic) bookings — kept in the list as + * history, otherwise an approved intercity row vanishes from the hub. + */ const BOOKING_STATUS_OPTIONS = [ { - value: "AWAITING_DOCUMENTS,DOCUMENTS_UNDER_REVIEW,CLEARANCE_READY", + value: + "AWAITING_DOCUMENTS,DOCUMENTS_UNDER_REVIEW,CLEARANCE_READY,FULLY_EXECUTED", label: "All statuses", }, { value: "AWAITING_DOCUMENTS", label: "Awaiting documents" }, { value: "DOCUMENTS_UNDER_REVIEW", label: "Under review" }, { value: "CLEARANCE_READY", label: "Clearance ready" }, + { value: "FULLY_EXECUTED", label: "Approved (history)" }, ]; const TRADE_DIRECTION_OPTIONS = [ diff --git a/apps/edr-freight-web/backoffice/src/pages/documents/FileUploadSettingsPage.tsx b/apps/edr-freight-web/backoffice/src/pages/documents/FileUploadSettingsPage.tsx index 9283fc070..753136270 100644 --- a/apps/edr-freight-web/backoffice/src/pages/documents/FileUploadSettingsPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/documents/FileUploadSettingsPage.tsx @@ -7,31 +7,46 @@ import { Card, Code, Group, + SimpleGrid, Stack, Table, + Tabs, Text, TextInput, ThemeIcon, } from "@mantine/core"; import { + ArrowDownToLine, + ArrowUpFromLine, FileUp, + FolderOpen, HardDrive, + Inbox, Layers, Paperclip, Search, Settings, + TrainFront, X, } from "lucide-react"; import { useQuery } from "@tanstack/react-query"; import { KpiStrip, PageContainer, PageHeader } from "@/components/page"; -import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles"; import { api } from "@/services/api"; import { getMinFiles, type FileUploadSetting } from "@/types/fileUploadSettings"; -import { DataTable, type ColumnDef } from "@edr/ui-common"; import ManageFileUploadFieldsDialog from "./ManageFileUploadFieldsDialog"; +type DocCategory = "import" | "export" | "intercity" | "other"; + +function categorize(setting: FileUploadSetting): DocCategory { + const code = setting.code.toLowerCase(); + if (code.includes("intercity")) return "intercity"; + if (code.includes("export")) return "export"; + if (code.includes("import")) return "import"; + return "other"; +} + export default function FileUploadSettingsPage() { const [query, setQuery] = useState(""); @@ -60,6 +75,17 @@ export default function FileUploadSettingsPage() { ); }, [fileUploadSettings, query]); + const byCategory = useMemo(() => { + const groups: Record = { + import: [], + export: [], + intercity: [], + other: [], + }; + for (const s of filtered) groups[categorize(s)].push(s); + return groups; + }, [filtered]); + const totalFields = fileUploadSettings.reduce( (sum, s) => sum + s.fields.length, 0, @@ -73,128 +99,7 @@ export default function FileUploadSettingsPage() { 0, ); - const columns = useMemo[]>(() => { - const headerClassName = ruleEngineTable.headerCell; - const cellClassName = ruleEngineTable.bodyCell; - return [ - { - id: "setting", - header: "Setting", - meta: { headerClassName, cellClassName }, - cell: ({ row }) => { - const s = row.original; - return ( - - - - - - - {s.label} - - - {s.description ?? "No description"} - - - - ); - }, - }, - { - id: "code", - header: "Code", - meta: { headerClassName, cellClassName }, - cell: ({ row }) => {row.original.code}, - }, - { - id: "entity", - header: "Entity", - meta: { headerClassName, cellClassName }, - cell: ({ row }) => ( - - {row.original.entity ?? "—"} - - ), - }, - { - id: "fields", - header: "Fields", - meta: { headerClassName, cellClassName }, - cell: ({ row }) => ( - - - - {row.original.fields.length} - - - ), - }, - { - id: "requiredMulti", - header: "Required / Multi", - meta: { headerClassName, cellClassName }, - cell: ({ row }) => { - const required = row.original.fields.filter((f) => f.isRequired).length; - const multi = row.original.fields.filter((f) => f.isMultiple).length; - return ( - - - {required} required - - - {multi} multi - - - ); - }, - }, - { - id: "maxSize", - header: "Max size", - meta: { headerClassName, cellClassName }, - cell: ({ row }) => { - const maxSize = Math.max( - 0, - ...row.original.fields.map((f) => f.maxSizeMb), - ); - return ( - - - {maxSize ? `${maxSize} MB` : "—"} - - ); - }, - }, - { - id: "actions", - header: "Actions", - meta: { - headerClassName, - cellClassName: `${cellClassName} whitespace-nowrap`, - }, - // Settings are seeded/fixed — staff may only update a setting's fields, - // not create, edit, or delete the settings themselves. - cell: ({ row }) => { - const setting = row.original; - return ( - - - - - - ); - }, - }, - ]; - }, []); - - const tableStatus = isLoading ? "loading" : isError ? "error" : "success"; + const gridStatus = isLoading ? "loading" : isError ? "error" : "success"; return ( @@ -214,54 +119,98 @@ export default function FileUploadSettingsPage() { /> - - - } - value={query} - onChange={(e) => setQuery(e.currentTarget.value)} - rightSection={ - query ? ( - setQuery("")} - aria-label="Clear search" - > - - - ) : null - } - style={{ maxWidth: 420 }} - /> + + + + }> + Import + + }> + Export + + }> + Intercity + + }> + Other + + - - void refetch(), + {(["import", "export", "intercity", "other"] as const).map((tab) => ( + + + + } + value={query} + onChange={(e) => setQuery(e.currentTarget.value)} + rightSection={ + query ? ( + setQuery("")} + aria-label="Clear search" + > + + + ) : null } - : undefined - } - emptyMessage={ - query.trim() - ? "No file upload settings match your search." - : "No file upload settings configured." - } - containerClassName="border-0 shadow-none bg-transparent min-w-[920px]" - /> - - + style={{ maxWidth: 420 }} + /> + + + + {gridStatus === "error" ? ( + + + + Failed to load settings. + + + {error instanceof Error ? error.message : "Unknown error."} + + + + + ) : gridStatus === "loading" ? ( + + {Array.from({ length: 6 }).map((_, i) => ( + + ))} + + ) : byCategory[tab].length === 0 ? ( + + + + + {query.trim() + ? "No file upload settings match your search." + : "No file upload settings in this category."} + + + + ) : ( + + {byCategory[tab].map((setting) => ( + + ))} + + )} + + + + ))} + @@ -269,6 +218,72 @@ export default function FileUploadSettingsPage() { ); } +function SettingCard({ setting }: { setting: FileUploadSetting }) { + const required = setting.fields.filter((f) => f.isRequired).length; + const multi = setting.fields.filter((f) => f.isMultiple).length; + const maxSize = Math.max(0, ...setting.fields.map((f) => f.maxSizeMb)); + + return ( + + + + + + + + + {setting.label} + + + {setting.description ?? "No description"} + + + + + + + {setting.entity ?? "—"} + + + {setting.fields.length} field{setting.fields.length === 1 ? "" : "s"} + + {required > 0 && ( + + {required} required + + )} + {multi > 0 && ( + + {multi} multi + + )} + + + + + + {setting.code} + + + + + + + {maxSize ? `Max ${maxSize} MB per file` : "No size limit set"} + + + + + + + + + + ); +} + /** * Static reference: how `min_files` / `max_files` are derived from the * Required × Multiple toggles. Documentation aid for whoever wires uploaders. diff --git a/apps/edr-freight-web/backoffice/src/types/trainScheduling.ts b/apps/edr-freight-web/backoffice/src/types/trainScheduling.ts index 7185ef377..0ff8d1db6 100644 --- a/apps/edr-freight-web/backoffice/src/types/trainScheduling.ts +++ b/apps/edr-freight-web/backoffice/src/types/trainScheduling.ts @@ -676,6 +676,8 @@ export interface TrainScheduleDetail { reference: string | null; customer: string | null; weightTons: number; + /** Cargo only (VGM/bulk tons) — the booked weight without wagon tare. */ + cargoWeightTons?: number; status: string | null; schedulingStatus?: SchedulingStatus | null; freightType?: FreightType | string | null;