From 540f4a1ff666badc5393bd4868d2a6792faa65ee Mon Sep 17 00:00:00 2001 From: Marshal Date: Mon, 20 Jul 2026 10:52:20 +0000 Subject: [PATCH] changes --- .../modules/contracts/contracts.service.ts | 13 ++++-------- .../train-scheduling/booking-batch.service.ts | 17 +++++++++------- .../train-scheduling.service.ts | 14 +++++++++++-- .../TrainScheduleV2ListPage.tsx | 6 ++++-- .../components/DocumentsTab.tsx | 20 +++++++++++++++++-- .../pages/bookings/resubmit/resubmitDocs.ts | 8 ++++++++ .../pages/contracts/ContractDetailPage.tsx | 9 ++++++++- 7 files changed, 64 insertions(+), 23 deletions(-) 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 224745cc4..c98e24b19 100644 --- a/apps/edr-freight-api/src/modules/contracts/contracts.service.ts +++ b/apps/edr-freight-api/src/modules/contracts/contracts.service.ts @@ -374,23 +374,18 @@ export class ContractsService { if (companyProfileId) { // Business-license files are FileRecords (resource "company_profiles"); // carry the live ones by reference. Staged/pending uploads are excluded by - // code. Codes are slugged from each document name so they group under - // "Profile documents" on the contract detail page. + // code. The `business_license` prefix is preserved so the portal groups + // them under "Business license" instead of the clearance catch-all — the + // index suffix keeps multiple licences distinct. const records = await this.filesService.findByResource( companyProfileId, 'company_profiles', ); - const slug = (name: string) => - name - .toLowerCase() - .replace(/\.[a-z0-9]+$/, '') - .replace(/[^a-z0-9]+/g, '_') - .replace(/^_+|_+$/g, '') || 'profile_document'; records .filter((r) => r.code === 'business_license') .forEach((r, i) => { - const code = `${slug(r.name)}_${i + 1}`; + const code = `business_license_${i + 1}`; if (existingCodes.has(code)) return; docs.push({ code, diff --git a/apps/edr-freight-api/src/modules/train-scheduling/booking-batch.service.ts b/apps/edr-freight-api/src/modules/train-scheduling/booking-batch.service.ts index 4a836060e..415b5df97 100644 --- a/apps/edr-freight-api/src/modules/train-scheduling/booking-batch.service.ts +++ b/apps/edr-freight-api/src/modules/train-scheduling/booking-batch.service.ts @@ -3444,19 +3444,22 @@ export class BookingBatchService implements OnModuleInit { /** * Physical wagons marshalled in the schedule's built train, or null when the - * schedule has no built train (or the consist is still empty) and the legacy - * locomotive-derived capacity must apply. This count is what caps a built - * train's bookings: 50 wagons coupled → 50 wagon slots, no more. + * schedule has NO built train and the legacy locomotive-derived capacity must + * apply. This count is what caps a built train's bookings: 50 wagons coupled + * → 50 wagon slots, no more. + * + * A built train with an EMPTY consist returns 0, NOT null: zero coupled + * wagons means zero capacity. Folding that case into null used to hand an + * un-consisted train the abstract locomotive budget, so an empty train + * advertised its full maxWagons as free space and accepted bookings the + * allocator could never place. */ private async builtTrainWagonCount( schedule: TrainSchedule, ): Promise { const trainId = schedule.trainSet?.train?.id; if (!trainId) return null; - const count = await this.dataSource - .getRepository(Wagon) - .count({ where: { trainId } }); - return count > 0 ? count : null; + return this.dataSource.getRepository(Wagon).count({ where: { trainId } }); } /** 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 9d426077e..2f7ca0776 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 @@ -7131,9 +7131,19 @@ export class TrainSchedulingService { await this.dataSource.transaction(async (manager) => { await manager.getRepository(TrainSetWagon).delete(trainSetWagonId); + // Recount from the slot rows rather than decrementing the cached counter. + // A blind `wagonCount - 1` desyncs the moment two removals race or the + // in-memory schedule graph is stale, and the counter is what the schedule + // capacity math reads. + const remaining = await manager.getRepository(TrainSetWagon).find({ + where: { trainSetId: schedule.trainSetId }, + select: { id: true, lengthMeters: true }, + }); await manager.getRepository(TrainSet).update(schedule.trainSetId, { - wagonCount: Math.max(0, (schedule.trainSet?.wagonCount ?? 0) - 1), - totalLengthMeters: Math.max(0, (schedule.trainSet?.totalLengthMeters ?? 0) - (wagon.lengthMeters ?? 0)), + wagonCount: remaining.length, + totalLengthMeters: roundTons( + remaining.reduce((sum, w) => sum + (Number(w.lengthMeters) || 0), 0), + ), }); }); diff --git a/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleV2ListPage.tsx b/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleV2ListPage.tsx index f40bd9526..aae182103 100644 --- a/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleV2ListPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleV2ListPage.tsx @@ -400,7 +400,9 @@ export default function TrainScheduleV2ListPage() { cell: ({ row }) => ( - + {/* Wagon SLOTS this schedule's bookings occupy — not the coupled + consist. A built train shows 0 here until bookings are allocated. */} + ), @@ -950,7 +952,7 @@ function ScheduleCard({ - + diff --git a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/DocumentsTab.tsx b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/DocumentsTab.tsx index 983411e89..8934a0a60 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/DocumentsTab.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/DocumentsTab.tsx @@ -158,7 +158,14 @@ const BUSINESS_LICENSE_DOC_CODES = new Set([ "business_license", "commercial_license", "investment_license", + "trade_license", ]); + +// Licences carried from the company profile are suffixed per file +// (`business_license_1`), so match on the stripped base code too. +const isBusinessLicenseCode = (code: string): boolean => + BUSINESS_LICENSE_DOC_CODES.has(code) || + BUSINESS_LICENSE_DOC_CODES.has(code.replace(/_\d+$/, "")); const PROFILE_DOC_CODES = new Set([ "tin_certificate", "national_id", @@ -183,6 +190,15 @@ const KNOWN_FILE_LABELS: Record = { function fileLabel(f: AnyFile) { if (KNOWN_FILE_LABELS[f.code]) return KNOWN_FILE_LABELS[f.code]; + // Profile documents carried onto the contract are suffixed per file + // (`business_license_2`) — label them from the base code, numbered. + const base = f.code.replace(/_\d+$/, ""); + if (KNOWN_FILE_LABELS[base]) { + const n = f.code.slice(base.length + 1); + return n && n !== "1" + ? `${KNOWN_FILE_LABELS[base]} ${n}` + : KNOWN_FILE_LABELS[base]; + } // Ad-hoc uploads carry a generated code (custom__) — use the filename. if (f.code.startsWith("custom_")) return f.name; return f.code @@ -245,7 +261,7 @@ export function DocumentsTab({ booking }: { booking: Freight.IBooking }) { const contractFiles = (contract?.files ?? []) as AnyFile[]; const contractPdf = contractFiles.find((f) => f.code === "contract"); const licenseFiles = contractFiles.filter((f) => - BUSINESS_LICENSE_DOC_CODES.has(f.code), + isBusinessLicenseCode(f.code), ); const profileFiles = contractFiles.filter((f) => PROFILE_DOC_CODES.has(f.code)); @@ -428,7 +444,7 @@ export function DocumentsTab({ booking }: { booking: Freight.IBooking }) { title={fileLabel(f)} meta={ PROFILE_DOC_CODES.has(f.code) || - BUSINESS_LICENSE_DOC_CODES.has(f.code) + isBusinessLicenseCode(f.code) ? "From your company profile" : f.name } diff --git a/apps/edr-freight-web/portal/src/pages/bookings/resubmit/resubmitDocs.ts b/apps/edr-freight-web/portal/src/pages/bookings/resubmit/resubmitDocs.ts index 0a2f1a30f..fb36c3fd2 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/resubmit/resubmitDocs.ts +++ b/apps/edr-freight-web/portal/src/pages/bookings/resubmit/resubmitDocs.ts @@ -26,6 +26,14 @@ const LABEL_BY_CODE = new Map([ export function labelForDocCode(code: string): string { const known = LABEL_BY_CODE.get(code); if (known) return known; + // Profile documents carried onto a contract are suffixed per file + // ("business_license_2") — label from the base code, numbered past the first. + const base = code.replace(/_\d+$/, ""); + const baseLabel = LABEL_BY_CODE.get(base); + if (baseLabel) { + const n = code.slice(base.length + 1); + return n && n !== "1" ? `${baseLabel} ${n}` : baseLabel; + } return code .replace(/^custom_\d+_\d+$/, "Additional document") .replace(/[_-]+/g, " ") diff --git a/apps/edr-freight-web/portal/src/pages/contracts/ContractDetailPage.tsx b/apps/edr-freight-web/portal/src/pages/contracts/ContractDetailPage.tsx index f03f99b01..89870ee93 100644 --- a/apps/edr-freight-web/portal/src/pages/contracts/ContractDetailPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/contracts/ContractDetailPage.tsx @@ -134,8 +134,15 @@ const BUSINESS_LICENSE_DOC_CODES = new Set([ "business_license", "commercial_license", "investment_license", + "trade_license", ]); +// Licences carried from the company profile are suffixed per file +// (`business_license_1`), so match on the stripped base code too. +const isBusinessLicenseCode = (code: string): boolean => + BUSINESS_LICENSE_DOC_CODES.has(code) || + BUSINESS_LICENSE_DOC_CODES.has(code.replace(/_\d+$/, "")); + // Onboarding / company-profile document codes seeded in file-upload-settings. // These get attached to the contract at creation and belong under "Profile // documents" rather than the clearance set. @@ -196,7 +203,7 @@ function groupContractDocuments( // The generated contract PDF lives in the contract list / home rows, not // here. Signature images are baked into that PDF — skip both. if (f.code === "contract" || f.code.startsWith("signature_")) continue; - else if (BUSINESS_LICENSE_DOC_CODES.has(f.code)) businessLicense.push(f); + else if (isBusinessLicenseCode(f.code)) businessLicense.push(f); else if (PROFILE_DOC_CODES.has(f.code)) profile.push(f); else if (includeClearance && isClearanceCode(f.code)) clearance.push(f); else other.push(f);