mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 06:35:42 +00:00
feat(WIP): filtering, exporting and more reports
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
import { BaseEntity } from '@edr/api-common';
|
||||
import { Column, Entity } from 'typeorm';
|
||||
|
||||
/**
|
||||
* numeric comes back from pg as a string. Every value here is arithmetic in a
|
||||
* report expression, so convert on read rather than making each caller do it.
|
||||
*/
|
||||
const asNumber = {
|
||||
to: (value: number) => value,
|
||||
from: (value: string | null) => (value === null ? null : Number(value)),
|
||||
};
|
||||
|
||||
/**
|
||||
* Single-row table holding the railway's operating standards — the numbers the
|
||||
* operations reports measure actual performance against. Same single-row shape
|
||||
* as `logo_settings` and `exchange_settings`; the app never inserts a second row.
|
||||
*
|
||||
* These live in the database rather than in a constants file because the
|
||||
* business treats them as tunable (the corridor standard is explicitly
|
||||
* described as "flexible"), and a planner must be able to change one without a
|
||||
* deployment.
|
||||
*/
|
||||
@Entity({ schema: 'freight', name: 'operations_standards' })
|
||||
export class OperationsStandard extends BaseEntity {
|
||||
/** Standard time a train may stand at an Ethiopian station, in hours. */
|
||||
@Column({
|
||||
name: 'station_standard_hours_ethiopia',
|
||||
type: 'numeric',
|
||||
precision: 6,
|
||||
scale: 2,
|
||||
default: 10,
|
||||
transformer: asNumber,
|
||||
})
|
||||
stationStandardHoursEthiopia!: number;
|
||||
|
||||
/** Standard time a train may stand at a Djibouti station, in hours. */
|
||||
@Column({
|
||||
name: 'station_standard_hours_djibouti',
|
||||
type: 'numeric',
|
||||
precision: 6,
|
||||
scale: 2,
|
||||
default: 13,
|
||||
transformer: asNumber,
|
||||
})
|
||||
stationStandardHoursDjibouti!: number;
|
||||
|
||||
/** Container turn-around cycle: 10 + 21 + 13 + 21. */
|
||||
@Column({
|
||||
name: 'cycle_standard_hours_container',
|
||||
type: 'numeric',
|
||||
precision: 6,
|
||||
scale: 2,
|
||||
default: 65,
|
||||
transformer: asNumber,
|
||||
})
|
||||
cycleStandardHoursContainer!: number;
|
||||
|
||||
/** Bulk cycle via DMP: 13 + 21 + 33 + 21. */
|
||||
@Column({
|
||||
name: 'cycle_standard_hours_bulk_dmp',
|
||||
type: 'numeric',
|
||||
precision: 6,
|
||||
scale: 2,
|
||||
default: 88,
|
||||
transformer: asNumber,
|
||||
})
|
||||
cycleStandardHoursBulkDmp!: number;
|
||||
|
||||
/** Bulk cycle via Negad freight yard: 13 + 21 + 41 + 21. */
|
||||
@Column({
|
||||
name: 'cycle_standard_hours_bulk_nagad',
|
||||
type: 'numeric',
|
||||
precision: 6,
|
||||
scale: 2,
|
||||
default: 96,
|
||||
transformer: asNumber,
|
||||
})
|
||||
cycleStandardHoursBulkNagad!: number;
|
||||
|
||||
/** Bulk cycle via BCC: 13 + 21 + 41 + 21. */
|
||||
@Column({
|
||||
name: 'cycle_standard_hours_bulk_bcc',
|
||||
type: 'numeric',
|
||||
precision: 6,
|
||||
scale: 2,
|
||||
default: 96,
|
||||
transformer: asNumber,
|
||||
})
|
||||
cycleStandardHoursBulkBcc!: number;
|
||||
|
||||
/**
|
||||
* Standard running time for one corridor leg, used when the yard pair has no
|
||||
* `yard_distances.standard_hours` of its own.
|
||||
*/
|
||||
@Column({
|
||||
name: 'default_leg_standard_hours',
|
||||
type: 'numeric',
|
||||
precision: 6,
|
||||
scale: 2,
|
||||
default: 21,
|
||||
transformer: asNumber,
|
||||
})
|
||||
defaultLegStandardHours!: number;
|
||||
|
||||
/** Grace on top of the leg standard before a train counts as delayed. */
|
||||
@Column({ name: 'delay_tolerance_minutes', type: 'int', default: 30 })
|
||||
delayToleranceMinutes!: number;
|
||||
|
||||
/** Charged tonnage per laden 20ft container. */
|
||||
@Column({
|
||||
name: 'charged_tons_full_20ft',
|
||||
type: 'numeric',
|
||||
precision: 8,
|
||||
scale: 2,
|
||||
default: 20,
|
||||
transformer: asNumber,
|
||||
})
|
||||
chargedTonsFull20ft!: number;
|
||||
|
||||
/** Charged tonnage per laden 40ft container. */
|
||||
@Column({
|
||||
name: 'charged_tons_full_40ft',
|
||||
type: 'numeric',
|
||||
precision: 8,
|
||||
scale: 2,
|
||||
default: 40,
|
||||
transformer: asNumber,
|
||||
})
|
||||
chargedTonsFull40ft!: number;
|
||||
|
||||
/** Charged tonnage per empty 20ft container. */
|
||||
@Column({
|
||||
name: 'charged_tons_empty_20ft',
|
||||
type: 'numeric',
|
||||
precision: 8,
|
||||
scale: 2,
|
||||
default: 2.24,
|
||||
transformer: asNumber,
|
||||
})
|
||||
chargedTonsEmpty20ft!: number;
|
||||
|
||||
/** Charged tonnage per empty 40ft container. */
|
||||
@Column({
|
||||
name: 'charged_tons_empty_40ft',
|
||||
type: 'numeric',
|
||||
precision: 8,
|
||||
scale: 2,
|
||||
default: 3.88,
|
||||
transformer: asNumber,
|
||||
})
|
||||
chargedTonsEmpty40ft!: number;
|
||||
|
||||
/** Charged tonnage per wagon of steel, fertilizer, rice, sugar, livestock. */
|
||||
@Column({
|
||||
name: 'charged_tons_per_wagon_general',
|
||||
type: 'numeric',
|
||||
precision: 8,
|
||||
scale: 2,
|
||||
default: 70,
|
||||
transformer: asNumber,
|
||||
})
|
||||
chargedTonsPerWagonGeneral!: number;
|
||||
|
||||
/** Charged tonnage per wagon of vegetables, milk, meat and other perishables. */
|
||||
@Column({
|
||||
name: 'charged_tons_per_wagon_perishable',
|
||||
type: 'numeric',
|
||||
precision: 8,
|
||||
scale: 2,
|
||||
default: 38,
|
||||
transformer: asNumber,
|
||||
})
|
||||
chargedTonsPerWagonPerishable!: number;
|
||||
|
||||
/**
|
||||
* Wagons in a full trainset when the cargo type has no
|
||||
* `cargo_types.full_trainset_wagons` of its own.
|
||||
*/
|
||||
@Column({ name: 'default_full_trainset_wagons', type: 'int', default: 50 })
|
||||
defaultFullTrainsetWagons!: number;
|
||||
|
||||
/** IAM user id of the last operator to change a standard. */
|
||||
@Column({ name: 'updated_by_id', type: 'uuid', nullable: true })
|
||||
updatedById?: string | null;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { BaseEntity } from '@edr/api-common';
|
||||
import { Column, Entity, Index } from 'typeorm';
|
||||
|
||||
/** Planning buckets the reports offer. Mirrors the reports' period filter. */
|
||||
export const TARGET_PERIOD_TYPES = ['week', 'month', 'quarter', 'year'] as const;
|
||||
export type TargetPeriodType = (typeof TARGET_PERIOD_TYPES)[number];
|
||||
|
||||
/** What is being planned. */
|
||||
export const TARGET_METRICS = ['TEU', 'TRAINSET', 'VOLUME_TONS'] as const;
|
||||
export type TargetMetric = (typeof TARGET_METRICS)[number];
|
||||
|
||||
/** Which axis `dimensionKey` names. */
|
||||
export const TARGET_DIMENSIONS = ['cargo_category', 'station', 'container_class'] as const;
|
||||
export type TargetDimension = (typeof TARGET_DIMENSIONS)[number];
|
||||
|
||||
/**
|
||||
* 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,
|
||||
* and the dimension value it applies to.
|
||||
*
|
||||
* `dimensionKey` holds a category key (not a label) — the same keys
|
||||
* `operations-classification.ts` emits, so a report can join on it directly.
|
||||
*
|
||||
* Uniqueness on the five-column slot is a partial index in the database
|
||||
* (WHERE deleted_at IS NULL) rather than a @Unique decorator, so a soft-deleted
|
||||
* target can be re-created — the same choice `yard_distances` makes.
|
||||
*/
|
||||
@Entity({ schema: 'freight', name: 'operations_targets' })
|
||||
@Index(['metric', 'periodType', 'periodStart'])
|
||||
export class OperationsTarget extends BaseEntity {
|
||||
@Column({ name: 'period_type', type: 'varchar', length: 10 })
|
||||
periodType!: TargetPeriodType;
|
||||
|
||||
/** First day of the bucket, normalised on write (Monday, 1st, quarter start). */
|
||||
@Column({ name: 'period_start', type: 'date' })
|
||||
periodStart!: string;
|
||||
|
||||
@Column({ name: 'metric', type: 'varchar', length: 20 })
|
||||
metric!: TargetMetric;
|
||||
|
||||
@Column({ name: 'dimension', type: 'varchar', length: 20 })
|
||||
dimension!: TargetDimension;
|
||||
|
||||
/** Category key, container-class key, or yard code — never a display label. */
|
||||
@Column({ name: 'dimension_key', type: 'varchar', length: 60 })
|
||||
dimensionKey!: string;
|
||||
|
||||
@Column({
|
||||
name: 'planned_value',
|
||||
type: 'numeric',
|
||||
precision: 14,
|
||||
scale: 3,
|
||||
transformer: {
|
||||
to: (value: number) => value,
|
||||
from: (value: string | null) => (value === null ? null : Number(value)),
|
||||
},
|
||||
})
|
||||
plannedValue!: number;
|
||||
|
||||
@Column({ name: 'note', type: 'text', nullable: true })
|
||||
note?: string | null;
|
||||
}
|
||||
Reference in New Issue
Block a user