mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 12:18:11 +00:00
changes
This commit is contained in:
@@ -374,23 +374,18 @@ export class ContractsService {
|
|||||||
if (companyProfileId) {
|
if (companyProfileId) {
|
||||||
// Business-license files are FileRecords (resource "company_profiles");
|
// Business-license files are FileRecords (resource "company_profiles");
|
||||||
// carry the live ones by reference. Staged/pending uploads are excluded by
|
// carry the live ones by reference. Staged/pending uploads are excluded by
|
||||||
// code. Codes are slugged from each document name so they group under
|
// code. The `business_license` prefix is preserved so the portal groups
|
||||||
// "Profile documents" on the contract detail page.
|
// them under "Business license" instead of the clearance catch-all — the
|
||||||
|
// index suffix keeps multiple licences distinct.
|
||||||
const records = await this.filesService.findByResource(
|
const records = await this.filesService.findByResource(
|
||||||
companyProfileId,
|
companyProfileId,
|
||||||
'company_profiles',
|
'company_profiles',
|
||||||
);
|
);
|
||||||
const slug = (name: string) =>
|
|
||||||
name
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/\.[a-z0-9]+$/, '')
|
|
||||||
.replace(/[^a-z0-9]+/g, '_')
|
|
||||||
.replace(/^_+|_+$/g, '') || 'profile_document';
|
|
||||||
|
|
||||||
records
|
records
|
||||||
.filter((r) => r.code === 'business_license')
|
.filter((r) => r.code === 'business_license')
|
||||||
.forEach((r, i) => {
|
.forEach((r, i) => {
|
||||||
const code = `${slug(r.name)}_${i + 1}`;
|
const code = `business_license_${i + 1}`;
|
||||||
if (existingCodes.has(code)) return;
|
if (existingCodes.has(code)) return;
|
||||||
docs.push({
|
docs.push({
|
||||||
code,
|
code,
|
||||||
|
|||||||
@@ -3444,19 +3444,22 @@ export class BookingBatchService implements OnModuleInit {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Physical wagons marshalled in the schedule's built train, or null when the
|
* 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
|
* schedule has NO built train and the legacy locomotive-derived capacity must
|
||||||
* locomotive-derived capacity must apply. This count is what caps a built
|
* apply. This count is what caps a built train's bookings: 50 wagons coupled
|
||||||
* train's bookings: 50 wagons coupled → 50 wagon slots, no more.
|
* → 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(
|
private async builtTrainWagonCount(
|
||||||
schedule: TrainSchedule,
|
schedule: TrainSchedule,
|
||||||
): Promise<number | null> {
|
): Promise<number | null> {
|
||||||
const trainId = schedule.trainSet?.train?.id;
|
const trainId = schedule.trainSet?.train?.id;
|
||||||
if (!trainId) return null;
|
if (!trainId) return null;
|
||||||
const count = await this.dataSource
|
return this.dataSource.getRepository(Wagon).count({ where: { trainId } });
|
||||||
.getRepository(Wagon)
|
|
||||||
.count({ where: { trainId } });
|
|
||||||
return count > 0 ? count : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -7131,9 +7131,19 @@ export class TrainSchedulingService {
|
|||||||
|
|
||||||
await this.dataSource.transaction(async (manager) => {
|
await this.dataSource.transaction(async (manager) => {
|
||||||
await manager.getRepository(TrainSetWagon).delete(trainSetWagonId);
|
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, {
|
await manager.getRepository(TrainSet).update(schedule.trainSetId, {
|
||||||
wagonCount: Math.max(0, (schedule.trainSet?.wagonCount ?? 0) - 1),
|
wagonCount: remaining.length,
|
||||||
totalLengthMeters: Math.max(0, (schedule.trainSet?.totalLengthMeters ?? 0) - (wagon.lengthMeters ?? 0)),
|
totalLengthMeters: roundTons(
|
||||||
|
remaining.reduce((sum, w) => sum + (Number(w.lengthMeters) || 0), 0),
|
||||||
|
),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -400,7 +400,9 @@ export default function TrainScheduleV2ListPage() {
|
|||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<Group gap={6} wrap="nowrap">
|
<Group gap={6} wrap="nowrap">
|
||||||
<MetricChip value={row.original.bookingsCount} label="bkg" />
|
<MetricChip value={row.original.bookingsCount} label="bkg" />
|
||||||
<MetricChip value={row.original.wagonCount} label="wgn" />
|
{/* Wagon SLOTS this schedule's bookings occupy — not the coupled
|
||||||
|
consist. A built train shows 0 here until bookings are allocated. */}
|
||||||
|
<MetricChip value={row.original.wagonCount} label="wgn used" />
|
||||||
<MetricChip value={`${row.original.totalWeightTons}T`} label="" subtle />
|
<MetricChip value={`${row.original.totalWeightTons}T`} label="" subtle />
|
||||||
</Group>
|
</Group>
|
||||||
),
|
),
|
||||||
@@ -950,7 +952,7 @@ function ScheduleCard({
|
|||||||
</Group>
|
</Group>
|
||||||
<Group gap={6} wrap="nowrap">
|
<Group gap={6} wrap="nowrap">
|
||||||
<MetricChip value={schedule.bookingsCount} label="bkg" />
|
<MetricChip value={schedule.bookingsCount} label="bkg" />
|
||||||
<MetricChip value={schedule.wagonCount} label="wgn" />
|
<MetricChip value={schedule.wagonCount} label="wgn used" />
|
||||||
<MetricChip value={`${schedule.totalWeightTons}T`} label="" subtle />
|
<MetricChip value={`${schedule.totalWeightTons}T`} label="" subtle />
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
@@ -158,7 +158,14 @@ const BUSINESS_LICENSE_DOC_CODES = new Set([
|
|||||||
"business_license",
|
"business_license",
|
||||||
"commercial_license",
|
"commercial_license",
|
||||||
"investment_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([
|
const PROFILE_DOC_CODES = new Set([
|
||||||
"tin_certificate",
|
"tin_certificate",
|
||||||
"national_id",
|
"national_id",
|
||||||
@@ -183,6 +190,15 @@ const KNOWN_FILE_LABELS: Record<string, string> = {
|
|||||||
|
|
||||||
function fileLabel(f: AnyFile) {
|
function fileLabel(f: AnyFile) {
|
||||||
if (KNOWN_FILE_LABELS[f.code]) return KNOWN_FILE_LABELS[f.code];
|
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_<ts>_<i>) — use the filename.
|
// Ad-hoc uploads carry a generated code (custom_<ts>_<i>) — use the filename.
|
||||||
if (f.code.startsWith("custom_")) return f.name;
|
if (f.code.startsWith("custom_")) return f.name;
|
||||||
return f.code
|
return f.code
|
||||||
@@ -245,7 +261,7 @@ export function DocumentsTab({ booking }: { booking: Freight.IBooking }) {
|
|||||||
const contractFiles = (contract?.files ?? []) as AnyFile[];
|
const contractFiles = (contract?.files ?? []) as AnyFile[];
|
||||||
const contractPdf = contractFiles.find((f) => f.code === "contract");
|
const contractPdf = contractFiles.find((f) => f.code === "contract");
|
||||||
const licenseFiles = contractFiles.filter((f) =>
|
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));
|
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)}
|
title={fileLabel(f)}
|
||||||
meta={
|
meta={
|
||||||
PROFILE_DOC_CODES.has(f.code) ||
|
PROFILE_DOC_CODES.has(f.code) ||
|
||||||
BUSINESS_LICENSE_DOC_CODES.has(f.code)
|
isBusinessLicenseCode(f.code)
|
||||||
? "From your company profile"
|
? "From your company profile"
|
||||||
: f.name
|
: f.name
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,14 @@ const LABEL_BY_CODE = new Map<string, string>([
|
|||||||
export function labelForDocCode(code: string): string {
|
export function labelForDocCode(code: string): string {
|
||||||
const known = LABEL_BY_CODE.get(code);
|
const known = LABEL_BY_CODE.get(code);
|
||||||
if (known) return known;
|
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
|
return code
|
||||||
.replace(/^custom_\d+_\d+$/, "Additional document")
|
.replace(/^custom_\d+_\d+$/, "Additional document")
|
||||||
.replace(/[_-]+/g, " ")
|
.replace(/[_-]+/g, " ")
|
||||||
|
|||||||
@@ -134,8 +134,15 @@ const BUSINESS_LICENSE_DOC_CODES = new Set([
|
|||||||
"business_license",
|
"business_license",
|
||||||
"commercial_license",
|
"commercial_license",
|
||||||
"investment_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.
|
// Onboarding / company-profile document codes seeded in file-upload-settings.
|
||||||
// These get attached to the contract at creation and belong under "Profile
|
// These get attached to the contract at creation and belong under "Profile
|
||||||
// documents" rather than the clearance set.
|
// documents" rather than the clearance set.
|
||||||
@@ -196,7 +203,7 @@ function groupContractDocuments(
|
|||||||
// The generated contract PDF lives in the contract list / home rows, not
|
// The generated contract PDF lives in the contract list / home rows, not
|
||||||
// here. Signature images are baked into that PDF — skip both.
|
// here. Signature images are baked into that PDF — skip both.
|
||||||
if (f.code === "contract" || f.code.startsWith("signature_")) continue;
|
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 (PROFILE_DOC_CODES.has(f.code)) profile.push(f);
|
||||||
else if (includeClearance && isClearanceCode(f.code)) clearance.push(f);
|
else if (includeClearance && isClearanceCode(f.code)) clearance.push(f);
|
||||||
else other.push(f);
|
else other.push(f);
|
||||||
|
|||||||
Reference in New Issue
Block a user