mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 18:20:57 +00:00
248 lines
8.1 KiB
TypeScript
248 lines
8.1 KiB
TypeScript
import { Inject, Injectable } from "@nestjs/common";
|
|
|
|
import { CargoType } from "../rule-engine/entities/cargo-type.entity";
|
|
import { ContainerType } from "../rule-engine/entities/container-type.entity";
|
|
import {
|
|
CARGO_TYPES_REPOSITORY,
|
|
ICargoTypesRepository,
|
|
} from "../rule-engine/interfaces/cargo-types.repository.interface";
|
|
import {
|
|
CONTAINER_TYPES_REPOSITORY,
|
|
IContainerTypesRepository,
|
|
} from "../rule-engine/interfaces/container-types.repository.interface";
|
|
import {
|
|
IServiceTypesRepository,
|
|
SERVICE_TYPES_REPOSITORY,
|
|
} from "../rule-engine/interfaces/service-types.repository.interface";
|
|
import {
|
|
IShippingLinesRepository,
|
|
SHIPPING_LINES_REPOSITORY,
|
|
} from "../rule-engine/interfaces/shipping-lines.repository.interface";
|
|
import {
|
|
IYardsRepository,
|
|
YARDS_REPOSITORY,
|
|
} from "../rule-engine/interfaces/yards.repository.interface";
|
|
import { YardFacilitiesService } from "../rule-engine/services/yard-facilities.service";
|
|
import {
|
|
BookingReferenceCargoTypeChildDto,
|
|
BookingReferenceCargoTypeGroupDto,
|
|
BookingReferenceContainerSizeGroupDto,
|
|
BookingReferenceContainerTypeDto,
|
|
BookingReferenceDataDto,
|
|
BookingReferenceServiceDto,
|
|
BookingReferenceShippingLineDto,
|
|
BookingReferenceYardDto,
|
|
} from "./dto/booking-reference-data.dto";
|
|
|
|
/**
|
|
* Reference cargo tree: top-level groups, each carrying its selectable
|
|
* commodities.
|
|
*
|
|
* `cargo_types` is an arbitrary-depth tree (Bulk → Steel Billet → S1 → …), but
|
|
* only a LEAF is a real commodity — an intermediate node is a container for
|
|
* finer types, and booking against it would be ambiguous. So each group's
|
|
* `children` are all of its leaf descendants, flattened, whatever the depth.
|
|
* Deep leaves carry their path below the group ("Steel Billet → S1") so a
|
|
* generically-named leaf still reads unambiguously in a dropdown.
|
|
*
|
|
* A group with no active descendants is its own leaf and is emitted as its
|
|
* single child — otherwise it is selectable as a group but offers no commodity,
|
|
* which dead-ends every form that requires one.
|
|
*/
|
|
export function buildCargoTypeTree(
|
|
rows: CargoType[],
|
|
): BookingReferenceCargoTypeGroupDto[] {
|
|
const active = rows.filter((r) => r.isActive);
|
|
|
|
const byOrder = (a: CargoType, b: CargoType) =>
|
|
a.displayOrder - b.displayOrder || a.code.localeCompare(b.code);
|
|
|
|
const childrenOf = new Map<string, CargoType[]>();
|
|
for (const row of active) {
|
|
if (!row.parentGroupId) continue;
|
|
const siblings = childrenOf.get(row.parentGroupId) ?? [];
|
|
siblings.push(row);
|
|
childrenOf.set(row.parentGroupId, siblings);
|
|
}
|
|
for (const siblings of childrenOf.values()) siblings.sort(byOrder);
|
|
|
|
const parents = active.filter((r) => !r.parentGroupId).sort(byOrder);
|
|
|
|
/** Depth-first leaf walk; `trail` is the path below the group. */
|
|
const collectLeaves = (
|
|
node: CargoType,
|
|
trail: string[],
|
|
seen: Set<string>,
|
|
): BookingReferenceCargoTypeChildDto[] => {
|
|
// Admin-entered parent pointers could in principle cycle — never loop.
|
|
if (seen.has(node.id)) return [];
|
|
seen.add(node.id);
|
|
|
|
const kids = childrenOf.get(node.id) ?? [];
|
|
if (kids.length === 0) {
|
|
return [
|
|
{
|
|
id: node.id,
|
|
name: [...trail, node.cargoTypeName].join(" → "),
|
|
code: node.code,
|
|
unit_of_measure: node.unitOfMeasure ?? null,
|
|
},
|
|
];
|
|
}
|
|
const nextTrail = [...trail, node.cargoTypeName];
|
|
return kids.flatMap((kid) => collectLeaves(kid, nextTrail, seen));
|
|
};
|
|
|
|
return parents.map((parent) => {
|
|
const kids = childrenOf.get(parent.id) ?? [];
|
|
const children =
|
|
kids.length === 0
|
|
? // The group itself is the commodity.
|
|
[
|
|
{
|
|
id: parent.id,
|
|
name: parent.cargoTypeName,
|
|
code: parent.code,
|
|
unit_of_measure: parent.unitOfMeasure ?? null,
|
|
},
|
|
]
|
|
: kids.flatMap((kid) => collectLeaves(kid, [], new Set<string>()));
|
|
|
|
return {
|
|
id: parent.id,
|
|
name: parent.cargoTypeName,
|
|
code: parent.code,
|
|
children,
|
|
};
|
|
});
|
|
}
|
|
|
|
export function groupContainersBySize(
|
|
rows: ContainerType[],
|
|
): BookingReferenceContainerSizeGroupDto[] {
|
|
const active = rows.filter((r) => r.isActive);
|
|
const bySize = new Map<string, ContainerType[]>();
|
|
|
|
for (const ct of active) {
|
|
const sizeKey =
|
|
ct.sizeFt != null && ct.sizeFt > 0 ? `${ct.sizeFt}ft` : "other";
|
|
const list = bySize.get(sizeKey) ?? [];
|
|
list.push(ct);
|
|
bySize.set(sizeKey, list);
|
|
}
|
|
|
|
const sortSizeKey = (key: string): number => {
|
|
if (key === "other") return Number.MAX_SAFE_INTEGER;
|
|
const n = parseInt(key, 10);
|
|
return Number.isNaN(n) ? Number.MAX_SAFE_INTEGER - 1 : n;
|
|
};
|
|
|
|
return [...bySize.entries()]
|
|
.sort(([a], [b]) => sortSizeKey(a) - sortSizeKey(b))
|
|
.map(([size, types]) => ({
|
|
size,
|
|
types: types
|
|
.sort(
|
|
(a, b) =>
|
|
(a.displayOrder ?? 0) - (b.displayOrder ?? 0) ||
|
|
a.code.localeCompare(b.code),
|
|
)
|
|
.map(
|
|
(ct): BookingReferenceContainerTypeDto => ({
|
|
id: ct.id,
|
|
name: ct.label?.trim() ? ct.label : ct.code,
|
|
code: ct.code,
|
|
is_reefer: ct.isReefer ?? false,
|
|
}),
|
|
),
|
|
}));
|
|
}
|
|
|
|
@Injectable()
|
|
export class BookingReferenceDataService {
|
|
constructor(
|
|
@Inject(YARDS_REPOSITORY)
|
|
private readonly yardsRepository: IYardsRepository,
|
|
@Inject(CONTAINER_TYPES_REPOSITORY)
|
|
private readonly containerTypesRepository: IContainerTypesRepository,
|
|
@Inject(SERVICE_TYPES_REPOSITORY)
|
|
private readonly serviceTypesRepository: IServiceTypesRepository,
|
|
@Inject(SHIPPING_LINES_REPOSITORY)
|
|
private readonly shippingLinesRepository: IShippingLinesRepository,
|
|
@Inject(CARGO_TYPES_REPOSITORY)
|
|
private readonly cargoTypesRepository: ICargoTypesRepository,
|
|
private readonly yardFacilitiesService: YardFacilitiesService,
|
|
) { }
|
|
|
|
async getReferenceData(): Promise<BookingReferenceDataDto> {
|
|
const [
|
|
yards,
|
|
containerTypes,
|
|
serviceTypes,
|
|
shippingLines,
|
|
cargoTypes,
|
|
facilityYards,
|
|
] = await Promise.all([
|
|
this.yardsRepository.findAll({
|
|
where: { isActive: true },
|
|
order: { displayOrder: "ASC", code: "ASC" },
|
|
}),
|
|
this.containerTypesRepository.findAll({
|
|
where: { isActive: true },
|
|
order: { displayOrder: "ASC", code: "ASC" },
|
|
}),
|
|
this.serviceTypesRepository.findAll({
|
|
where: { isActive: true },
|
|
order: { displayOrder: "ASC", code: "ASC" },
|
|
}),
|
|
this.shippingLinesRepository.findAll({
|
|
where: { isActive: true },
|
|
order: { label: "ASC", code: "ASC" },
|
|
}),
|
|
this.cargoTypesRepository.findAll({
|
|
where: { isActive: true },
|
|
order: { displayOrder: "ASC", code: "ASC" },
|
|
}),
|
|
this.yardFacilitiesService.listFacilityYards(),
|
|
]);
|
|
|
|
// Every active yard is still listed; a yard with no facility record simply
|
|
// reports no capability, so the forms drop it from the pickers themselves.
|
|
const facilityByYardId = new Map(facilityYards.map((f) => [f.yardId, f]));
|
|
|
|
return {
|
|
yard: yards.map((y): BookingReferenceYardDto => {
|
|
const facility = facilityByYardId.get(y.id);
|
|
return {
|
|
id: y.id,
|
|
name: y.label,
|
|
code: y.code,
|
|
country: y.country,
|
|
hasContainerFacilityOrigin:
|
|
facility?.hasContainerFacilityOrigin ?? false,
|
|
hasBulkFacilityOrigin: facility?.hasBulkFacilityOrigin ?? false,
|
|
hasContainerFacilityDestination:
|
|
facility?.hasContainerFacilityDestination ?? false,
|
|
hasBulkFacilityDestination:
|
|
facility?.hasBulkFacilityDestination ?? false,
|
|
};
|
|
}),
|
|
containers: groupContainersBySize(containerTypes),
|
|
service: serviceTypes.map(
|
|
(s): BookingReferenceServiceDto => ({
|
|
name: s.serviceName,
|
|
...s,
|
|
}),
|
|
),
|
|
shipping_line: shippingLines.map(
|
|
(sl): BookingReferenceShippingLineDto => ({
|
|
id: sl.id,
|
|
name: sl.label,
|
|
code: sl.code,
|
|
}),
|
|
),
|
|
cargo_type: buildCargoTypeTree(cargoTypes),
|
|
};
|
|
}
|
|
}
|