mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 12:58:13 +00:00
feat(WIP): filtering, exporting and more reports
This commit is contained in:
84
apps/edr-freight-api/src/scripts/tmp-ops-plan.ts
Normal file
84
apps/edr-freight-api/src/scripts/tmp-ops-plan.ts
Normal 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();
|
||||
99
apps/edr-freight-api/src/scripts/tmp-ops-verify.ts
Normal file
99
apps/edr-freight-api/src/scripts/tmp-ops-verify.ts
Normal 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();
|
||||
Reference in New Issue
Block a user