mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-06 11:15:03 +00:00
feat: add wagon performance report and export functionality
- Implemented for Excel download of wagon performance report sections, including column width adjustment and timestamped filenames. - Created to compute derived wagon performance figures based on movements and status logs, ensuring consistency with API data. - Enhanced with new fields for tracking last movement and statistics window days for improved reporting capabilities.
This commit is contained in:
@@ -105,4 +105,18 @@ export class ListWagonsQueryDto {
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
maintenanceTo?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description:
|
||||
'Window (days) the per-row load/move counts are counted over. Does not filter rows.',
|
||||
default: 90,
|
||||
minimum: 1,
|
||||
maximum: 3650,
|
||||
})
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Max(3650)
|
||||
statsWindowDays?: number;
|
||||
}
|
||||
|
||||
@@ -177,6 +177,7 @@ export class WagonsService {
|
||||
async findAll(query: ListWagonsQueryDto = {}): Promise<PaginatedResponse<Wagon>> {
|
||||
const page = await paginateQuery(this.buildListQuery(query), query, { defaultPageSize: 10 });
|
||||
await this.attachStatusDates(page.items);
|
||||
await this.attachMovementStats(page.items, query.statsWindowDays ?? 90);
|
||||
return page;
|
||||
}
|
||||
|
||||
@@ -216,6 +217,56 @@ export class WagonsService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-wagon movement rollups for the wagon performance report: when the
|
||||
* wagon last arrived anywhere (the idle clock), and how many loaded / total
|
||||
* moves it made inside `windowDays`. One grouped query per page, in the same
|
||||
* shape as `attachStatusDates` above — never one request per row.
|
||||
*/
|
||||
private async attachMovementStats(wagons: Wagon[], windowDays: number): Promise<void> {
|
||||
if (!wagons.length) return;
|
||||
const since = new Date(Date.now() - windowDays * 24 * 60 * 60 * 1000);
|
||||
const rows: Array<{
|
||||
wagonId: string;
|
||||
lastMovedAt: Date | null;
|
||||
loadsInWindow: string;
|
||||
movesInWindow: string;
|
||||
emptyMovesInWindow: string;
|
||||
}> = await this.dataSource
|
||||
.getRepository(WagonMovement)
|
||||
.createQueryBuilder('m')
|
||||
.select('m.wagon_id', 'wagonId')
|
||||
.addSelect('MAX(m.occurred_at)', 'lastMovedAt')
|
||||
.addSelect(
|
||||
'COUNT(*) FILTER (WHERE m.occurred_at >= :since AND m.kind = :loaded)',
|
||||
'loadsInWindow',
|
||||
)
|
||||
.addSelect(
|
||||
'COUNT(*) FILTER (WHERE m.occurred_at >= :since AND m.kind = :empty)',
|
||||
'emptyMovesInWindow',
|
||||
)
|
||||
.addSelect('COUNT(*) FILTER (WHERE m.occurred_at >= :since)', 'movesInWindow')
|
||||
.where('m.wagon_id IN (:...ids)', { ids: wagons.map((w) => w.id) })
|
||||
.setParameters({
|
||||
since,
|
||||
loaded: WagonMovementKind.Loaded,
|
||||
empty: WagonMovementKind.EmptyReposition,
|
||||
})
|
||||
.groupBy('m.wagon_id')
|
||||
.getRawMany();
|
||||
|
||||
const byId = new Map(rows.map((r) => [r.wagonId, r]));
|
||||
for (const w of wagons) {
|
||||
const r = byId.get(w.id);
|
||||
Object.assign(w, {
|
||||
lastMovedAt: r?.lastMovedAt ?? null,
|
||||
loadsInWindow: Number(r?.loadsInWindow ?? 0),
|
||||
movesInWindow: Number(r?.movesInWindow ?? 0),
|
||||
emptyMovesInWindow: Number(r?.emptyMovesInWindow ?? 0),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<Wagon> {
|
||||
const wagon = await this.wagonRepo.findOne({
|
||||
where: { id },
|
||||
@@ -328,11 +379,35 @@ export class WagonsService {
|
||||
/** Movement ledger for one wagon, newest first (loaded legs, repositions, manual moves). */
|
||||
async listMovements(wagonId: string): Promise<WagonMovement[]> {
|
||||
await this.findById(wagonId); // 404 on unknown wagon
|
||||
return this.dataSource.getRepository(WagonMovement).find({
|
||||
const movements = await this.dataSource.getRepository(WagonMovement).find({
|
||||
where: { wagonId },
|
||||
relations: { fromYard: true, toYard: true },
|
||||
order: { occurredAt: 'DESC', createdAt: 'DESC' },
|
||||
});
|
||||
await this.attachBookingReferences(movements);
|
||||
return movements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve each loaded move's booking to its human reference, so the UI can
|
||||
* show (and link to) "BKG-11284" rather than a raw uuid. One query for the
|
||||
* whole ledger; `wagon_movements` deliberately has no FK to bookings, so
|
||||
* this is a read-time join on primary keys, exactly like the labels in
|
||||
* `wagon-history.service`.
|
||||
*/
|
||||
private async attachBookingReferences(movements: WagonMovement[]): Promise<void> {
|
||||
const ids = [...new Set(movements.map((m) => m.bookingId).filter((v): v is string => !!v))];
|
||||
if (!ids.length) return;
|
||||
const rows: Array<{ id: string; reference: string }> = await this.dataSource.query(
|
||||
`SELECT id, reference FROM freight.bookings WHERE id = ANY($1::uuid[])`,
|
||||
[ids],
|
||||
);
|
||||
const byId = new Map(rows.map((r) => [r.id, r.reference]));
|
||||
for (const m of movements) {
|
||||
Object.assign(m, {
|
||||
bookingReference: m.bookingId ? (byId.get(m.bookingId) ?? null) : null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async remove(id: string, userId?: string | null): Promise<void> {
|
||||
|
||||
Reference in New Issue
Block a user