feat(WIP): filtering, exporting and more reports

This commit is contained in:
Nathnael
2026-08-19 13:51:18 +00:00
parent e964a9b8f4
commit fb21ad1541
56 changed files with 3750 additions and 27 deletions

View File

@@ -0,0 +1,84 @@
/* Throwaway: proves the Plan / Implement rate path end to end. Delete after use. */
import { DataSource } from 'typeorm';
import { REPORTS } from '../modules/reports/report.registry';
import { normalisePeriodStart } from '../modules/operations-reporting/operations-targets.service';
async function main(): Promise<void> {
const ds = new DataSource({
type: 'postgres',
host: 'localhost',
port: 5433,
username: 'nati',
password: 'password',
database: 'nati_wt_opsreport',
entities: ['src/**/*.entity.ts'],
synchronize: false,
logging: false,
});
await ds.initialize();
const params = { period: 'month' };
const run = async (key: string) => {
const def = REPORTS.find((r) => r.key === key)!;
return def.query({ ds, params, directions: null }).getRawMany();
};
const before = await run('trainset-performance');
console.log('trainset rows before target:');
console.table(
before.map((r) => ({
period: r.period,
category: r.categoryKey,
wagons: r.wagons,
operated: r.operated,
plan: r.plan,
rate: r.implementRate,
})),
);
const target = before[0];
if (!target) {
console.log('no rows to plan against');
await ds.destroy();
return;
}
// period label is YYYY-MM for month; a target is stored on the bucket start.
const periodStart = normalisePeriodStart('month', `${target.period}-15`);
console.log(`\nnormalisePeriodStart('month', '${target.period}-15') = ${periodStart}`);
await ds.query(
`INSERT INTO freight.operations_targets
(period_type, period_start, metric, dimension, dimension_key, planned_value)
VALUES ('month', $1, 'TRAINSET', 'cargo_category', $2, $3)`,
[periodStart, target.categoryKey, Number(target.operated) * 2],
);
const after = await run('trainset-performance');
console.log('\ntrainset rows after inserting a target of 2x operated:');
console.table(
after.map((r) => ({
period: r.period,
category: r.categoryKey,
operated: r.operated,
plan: r.plan,
rate: r.implementRate,
})),
);
const row = after.find((r) => r.categoryKey === target.categoryKey && r.period === target.period);
const ok = Number(row?.plan) === Number(target.operated) * 2 && Math.abs(Number(row?.implementRate) - 50) < 0.05;
console.log(ok ? '\nPLAN OK — rate is 50% against a doubled plan' : '\nPLAN MISMATCH');
// Untouched categories must still read null, not zero.
const untouched = after.filter((r) => r.categoryKey !== target.categoryKey);
const nulls = untouched.every((r) => r.plan === null && r.implementRate === null);
console.log(nulls ? 'UNPLANNED OK — plan and rate are null, not zero' : 'UNPLANNED MISMATCH');
await ds.query(`DELETE FROM freight.operations_targets WHERE metric = 'TRAINSET'`);
await ds.destroy();
process.exit(ok && nulls ? 0 : 1);
}
void main();

View File

@@ -0,0 +1,99 @@
/* Throwaway harness: applies the operations migration to a scratch database and
* runs every operations report across several parameter sets. Delete after use. */
import { DataSource } from 'typeorm';
import { OperationsReporting3580000000000 } from '../migrations/3580000000000-OperationsReporting';
import { REPORTS } from '../modules/reports/report.registry';
import { ReportContext } from '../modules/reports/report.types';
const NEW_KEYS = [
'station-staying-time',
'turnaround-cycle',
'train-delays',
'trainset-performance',
'teu-performance',
'cargo-volume-performance',
'charged-vs-actual-volume',
'cargo-volume-by-station',
];
const PARAM_SETS: Record<string, unknown>[] = [
{},
{ period: 'month' },
{ period: 'quarter', direction: 'IMPORT' },
{ period: 'week', country: 'Djibouti' },
{ period: 'year', country: 'Ethiopia', categories: ['CONTAINER_IMPORT_MULTIMODAL', 'BULK'] },
{ dateFrom: '2020-01-01', dateTo: '2030-01-01', delayedOnly: 'true' },
{ trainNumber: 'X', origin: 'NAGAD', destination: 'KALITY' },
];
async function main(): Promise<void> {
const ds = new DataSource({
type: 'postgres',
host: 'localhost',
port: 5433,
username: 'nati',
password: 'password',
database: 'nati_wt_opsreport',
entities: ['src/**/*.entity.ts'],
synchronize: false,
logging: false,
});
await ds.initialize();
// Apply the migration's DDL by hand — this scratch database is not under
// migration control and only needs the two new tables and two columns.
const runner = ds.createQueryRunner();
await new OperationsReporting3580000000000().up(runner);
await runner.release();
console.log('migration applied\n');
let failures = 0;
for (const key of NEW_KEYS) {
const def = REPORTS.find((r) => r.key === key);
if (!def) {
console.log(`MISSING ${key}`);
failures++;
continue;
}
for (const [i, params] of PARAM_SETS.entries()) {
for (const directions of [null, ['IMPORT']] as (string[] | null)[]) {
const ctx: ReportContext = { ds, params, directions };
const label = `${key} [set ${i}${directions ? ' scoped' : ''}]`;
try {
const qb = def.query(ctx);
const rows = await qb.limit(5).getRawMany();
// The runner wraps every query for the total count — prove that works too.
const [sql, bound] = qb.getQueryAndParameters();
const counted = await ds.query(`SELECT COUNT(*)::int AS n FROM (${sql}) AS sub`, bound);
// Sorting: every sortable column must be a legal ORDER BY.
for (const col of def.columns.filter((c) => c.sortable)) {
await def
.query({ ds, params, directions })
.orderBy(col.sortExpr ?? `"${col.key}"`, 'DESC')
.limit(1)
.getRawMany();
}
const kpis = def.summary ? await def.summary(ctx) : [];
console.log(
`OK ${label.padEnd(50)} rows=${rows.length} total=${counted[0]?.n ?? '?'} kpis=${kpis
.map((k) => `${k.label}:${k.value}${k.unit ?? ''}`)
.join(' ')}`,
);
} catch (err) {
failures++;
console.log(`FAIL ${label}: ${(err as Error).message.split('\n')[0]}`);
}
}
}
}
console.log(failures ? `\n${failures} failures` : '\nall checks passed');
await ds.destroy();
process.exit(failures ? 1 : 0);
}
void main();