feat(operations-reporting): plan station targets per cargo category

The OCC report plans a station lane per cargo type — Nagad–Mojo container
and Nagad–Mojo fertilizer are separate numbers — but a target's identity
was period + metric + dimension + dimensionKey, so the two collided on
one slot. `cargo_category` is now part of the row and of the uniqueness
check; it stays null for cargo_category and container_class targets,
whose dimensionKey already carries the category.

The config grid showed raw codes (VOLUME_TONS, cargo_category, a yard
code). The list read now sends readable twins alongside the stored codes,
which stay exactly as they are because the reports join on them — the
same shape YardDistancesService uses. Labels resolve per dimension rather
than from one merged map: CONTAINER_EXPORT exists in both vocabularies
and reads differently in each, and merging them gave every cargo-category
row the container-class wording.
This commit is contained in:
Nathnael
2026-08-20 11:36:32 +00:00
parent 2ec818e590
commit f28b6faf29
5 changed files with 204 additions and 20 deletions

View File

@@ -13,6 +13,30 @@ export type TargetMetric = (typeof TARGET_METRICS)[number];
export const TARGET_DIMENSIONS = ['cargo_category', 'station', 'container_class'] as const;
export type TargetDimension = (typeof TARGET_DIMENSIONS)[number];
/**
* How each stored code reads on screen. The columns are enums the reports match
* on, so the stored values must stay exactly as they are — these exist for the
* admin grid, which otherwise shows `VOLUME_TONS` and `cargo_category` verbatim.
*/
export const TARGET_METRIC_LABELS: Record<TargetMetric, string> = {
TEU: 'TEU',
TRAINSET: 'Trainsets',
VOLUME_TONS: 'Volume (tons)',
};
export const TARGET_DIMENSION_LABELS: Record<TargetDimension, string> = {
cargo_category: 'Cargo category',
station: 'Station',
container_class: 'Container class',
};
export const TARGET_PERIOD_LABELS: Record<TargetPeriodType, string> = {
week: 'Weekly',
month: 'Monthly',
quarter: 'Quarterly',
year: 'Yearly',
};
/**
* The planned side of every "Plan / Operated / Implement Rate" table in the
* operations reporting spec. One row is one planned number: a period, a metric,
@@ -57,6 +81,15 @@ export class OperationsTarget extends BaseEntity {
})
plannedValue!: number;
/**
* Only for `station` targets, where the plan is per station AND per cargo
* type — the OCC report plans NagadMojo container and NagadMojo fertilizer
* separately. Null on `cargo_category` and `container_class` targets, whose
* `dimensionKey` already carries the category.
*/
@Column({ name: 'cargo_category', type: 'varchar', length: 60, nullable: true })
cargoCategory?: string | null;
@Column({ name: 'note', type: 'text', nullable: true })
note?: string | null;
}