mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
@@ -116,6 +116,28 @@ export class OverviewTonnagePointDto {
|
||||
@ApiProperty() tons!: number;
|
||||
}
|
||||
|
||||
/** One bucket × series cell of a two-dimensional breakdown. */
|
||||
export class OverviewMatrixCellDto {
|
||||
@ApiProperty({ example: 'Flat wagon' }) group!: string;
|
||||
@ApiProperty({ example: 'AVAILABLE' }) series!: string;
|
||||
@ApiProperty() count!: number;
|
||||
}
|
||||
|
||||
export class OverviewTrainLoadDto {
|
||||
@ApiProperty() scheduleId!: string;
|
||||
@ApiProperty({ example: '8001' }) trainNumber!: string;
|
||||
@ApiProperty({ example: '2026-08-13' }) date!: string;
|
||||
@ApiProperty({ example: 'EXPORT' }) direction!: string;
|
||||
@ApiProperty() wagonsAllocated!: number;
|
||||
@ApiProperty() wagonsTotal!: number;
|
||||
@ApiProperty() tons!: number;
|
||||
}
|
||||
|
||||
export class OverviewTurnaroundDto {
|
||||
@ApiProperty({ example: '8001' }) trainSet!: string;
|
||||
@ApiProperty({ example: 26.5 }) hours!: number;
|
||||
}
|
||||
|
||||
export class OverviewOperationsTabDto {
|
||||
@ApiProperty({ type: OverviewOperationsKpisDto })
|
||||
kpis!: OverviewOperationsKpisDto;
|
||||
@@ -150,6 +172,64 @@ export class OverviewOperationsTabDto {
|
||||
@ApiProperty({ type: [OverviewStatusCountDto] })
|
||||
cargoStatusBreakdown!: OverviewStatusCountDto[];
|
||||
|
||||
@ApiProperty({ type: [OverviewLabelCountDto] })
|
||||
bookingsByPort!: OverviewLabelCountDto[];
|
||||
|
||||
@ApiProperty({ type: [OverviewMatrixCellDto] })
|
||||
bookingStatusByPort!: OverviewMatrixCellDto[];
|
||||
|
||||
@ApiProperty({ type: [OverviewTrainLoadDto] })
|
||||
trainLoads!: OverviewTrainLoadDto[];
|
||||
|
||||
@ApiProperty()
|
||||
generatedAt!: string;
|
||||
}
|
||||
|
||||
export class OverviewFleetTabDto {
|
||||
@ApiProperty({ type: [OverviewStatusCountDto] })
|
||||
wagonStatusBreakdown!: OverviewStatusCountDto[];
|
||||
|
||||
@ApiProperty({ type: [OverviewMatrixCellDto] })
|
||||
wagonStatusByType!: OverviewMatrixCellDto[];
|
||||
|
||||
@ApiProperty({ type: [OverviewMatrixCellDto] })
|
||||
wagonStatusByYard!: OverviewMatrixCellDto[];
|
||||
|
||||
@ApiProperty({ type: [OverviewStatusCountDto] })
|
||||
locomotiveStatusBreakdown!: OverviewStatusCountDto[];
|
||||
|
||||
@ApiProperty({ type: [OverviewMatrixCellDto] })
|
||||
locomotivesByYard!: OverviewMatrixCellDto[];
|
||||
|
||||
@ApiProperty({ type: [OverviewLabelCountDto] })
|
||||
locomotivesByType!: OverviewLabelCountDto[];
|
||||
|
||||
@ApiProperty({ nullable: true, example: 26.5 })
|
||||
avgTurnaroundHours!: number | null;
|
||||
|
||||
@ApiProperty({ type: [OverviewTurnaroundDto] })
|
||||
turnaroundByTrain!: OverviewTurnaroundDto[];
|
||||
|
||||
@ApiProperty()
|
||||
generatedAt!: string;
|
||||
}
|
||||
|
||||
export class OverviewClearanceTabDto {
|
||||
@ApiProperty({ type: [OverviewStatusCountDto] })
|
||||
bookingDocumentsByStatus!: OverviewStatusCountDto[];
|
||||
|
||||
@ApiProperty({ type: [OverviewStatusCountDto] })
|
||||
contractDocumentsByStatus!: OverviewStatusCountDto[];
|
||||
|
||||
@ApiProperty({ type: [OverviewStatusCountDto] })
|
||||
invoicesByStatus!: OverviewStatusCountDto[];
|
||||
|
||||
@ApiProperty({ type: [OverviewLabelCountDto] })
|
||||
invoicesByType!: OverviewLabelCountDto[];
|
||||
|
||||
@ApiProperty({ type: [OverviewMatrixCellDto] })
|
||||
handoversByMile!: OverviewMatrixCellDto[];
|
||||
|
||||
@ApiProperty()
|
||||
generatedAt!: string;
|
||||
}
|
||||
@@ -167,6 +247,9 @@ export class OverviewCustomersTabDto {
|
||||
@ApiProperty({ type: [OverviewLabelCountDto] })
|
||||
topCustomersByBookings!: OverviewLabelCountDto[];
|
||||
|
||||
@ApiProperty({ type: [OverviewMatrixCellDto] })
|
||||
profilesByTypeStatus!: OverviewMatrixCellDto[];
|
||||
|
||||
@ApiProperty()
|
||||
generatedAt!: string;
|
||||
}
|
||||
|
||||
@@ -15,8 +15,10 @@ import { OverviewResponseDto } from './dto/overview-response.dto';
|
||||
import {
|
||||
OverviewBillingTabDto,
|
||||
OverviewBookingsTabDto,
|
||||
OverviewClearanceTabDto,
|
||||
OverviewContractsTabDto,
|
||||
OverviewCustomersTabDto,
|
||||
OverviewFleetTabDto,
|
||||
OverviewOperationsTabDto,
|
||||
OverviewStaffTabDto,
|
||||
} from './dto/overview-tab-response.dto';
|
||||
@@ -106,6 +108,22 @@ export class OverviewController {
|
||||
return this.overviewService.getOperationsTab(query.range ?? '30d');
|
||||
}
|
||||
|
||||
@Get('fleet')
|
||||
@BookingStaff(FREIGHT_PERMS.overview.view)
|
||||
@ApiOperation({ summary: 'Wagon and locomotive fleet detail, plus turnaround' })
|
||||
@ApiOkResponse({ type: OverviewFleetTabDto })
|
||||
getFleetTab(@Query() query: OverviewQueryDto): Promise<OverviewFleetTabDto> {
|
||||
return this.overviewService.getFleetTab(query.range ?? '30d');
|
||||
}
|
||||
|
||||
@Get('clearance')
|
||||
@BookingStaff(FREIGHT_PERMS.overview.view)
|
||||
@ApiOperation({ summary: 'Document review and invoice queues' })
|
||||
@ApiOkResponse({ type: OverviewClearanceTabDto })
|
||||
getClearanceTab(): Promise<OverviewClearanceTabDto> {
|
||||
return this.overviewService.getClearanceTab();
|
||||
}
|
||||
|
||||
@Get('customers')
|
||||
@BookingStaff(FREIGHT_PERMS.overview.view)
|
||||
@ApiOperation({ summary: 'Customers tab metrics and charts' })
|
||||
|
||||
@@ -58,6 +58,21 @@ export type OverviewRecentBookingRow = {
|
||||
createdAt: Date;
|
||||
};
|
||||
|
||||
/** One cell of a two-dimensional breakdown (bucket × stacked series). */
|
||||
export type MatrixCell = { group: string; series: string; count: number };
|
||||
|
||||
export type TurnaroundRow = { trainSet: string; hours: number };
|
||||
|
||||
export type TrainLoadRow = {
|
||||
scheduleId: string;
|
||||
trainNumber: string;
|
||||
date: string;
|
||||
direction: string;
|
||||
wagonsAllocated: number;
|
||||
wagonsTotal: number;
|
||||
tons: number;
|
||||
};
|
||||
|
||||
export type OverviewContractKpisRow = {
|
||||
total: number;
|
||||
totalActive: number;
|
||||
@@ -1261,4 +1276,334 @@ export class OverviewRepository {
|
||||
createdAt: row.createdAt,
|
||||
}));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fleet / operations detail. These read tables that have no repository
|
||||
// injected here (locomotives, train_set_wagons, document reviews, …), so they
|
||||
// go through the shared entity manager with plain SQL instead of adding six
|
||||
// more constructor arguments for one query each.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
private get sql() {
|
||||
return this.wagonRepository.manager;
|
||||
}
|
||||
|
||||
private async matrix(
|
||||
statement: string,
|
||||
params: unknown[] = [],
|
||||
): Promise<MatrixCell[]> {
|
||||
const rows = await this.sql.query<
|
||||
{ group: string; series: string; count: string }[]
|
||||
>(statement, params);
|
||||
return rows.map((row) => ({
|
||||
group: row.group,
|
||||
series: row.series,
|
||||
count: Number(row.count),
|
||||
}));
|
||||
}
|
||||
|
||||
private async labelCounts(
|
||||
statement: string,
|
||||
params: unknown[] = [],
|
||||
): Promise<{ label: string; count: number }[]> {
|
||||
const rows = await this.sql.query<{ label: string; count: string }[]>(
|
||||
statement,
|
||||
params,
|
||||
);
|
||||
return rows.map((row) => ({ label: row.label, count: Number(row.count) }));
|
||||
}
|
||||
|
||||
private async statusCounts(
|
||||
statement: string,
|
||||
params: unknown[] = [],
|
||||
): Promise<{ status: string; count: number }[]> {
|
||||
const rows = await this.sql.query<{ status: string; count: string }[]>(
|
||||
statement,
|
||||
params,
|
||||
);
|
||||
return rows.map((row) => ({ status: row.status, count: Number(row.count) }));
|
||||
}
|
||||
|
||||
/** Wagon lifecycle state crossed with wagon type — "how many flat wagons are detained". */
|
||||
getWagonStatusByType(): Promise<MatrixCell[]> {
|
||||
return this.matrix(`
|
||||
SELECT COALESCE(wt.name, 'Unknown') AS "group",
|
||||
w.status AS series,
|
||||
COUNT(*)::int AS count
|
||||
FROM freight.wagons w
|
||||
LEFT JOIN freight.wagon_types wt ON wt.id = w.wagon_type_id
|
||||
WHERE w.deleted_at IS NULL
|
||||
GROUP BY 1, 2
|
||||
ORDER BY 1, 2
|
||||
`);
|
||||
}
|
||||
|
||||
/** Same lifecycle state, per yard the wagon currently sits in. */
|
||||
getWagonStatusByYard(): Promise<MatrixCell[]> {
|
||||
return this.matrix(`
|
||||
SELECT y.label AS "group",
|
||||
w.status AS series,
|
||||
COUNT(*)::int AS count
|
||||
FROM freight.wagons w
|
||||
INNER JOIN freight.yards y ON y.id = w.current_yard_id
|
||||
WHERE w.deleted_at IS NULL
|
||||
GROUP BY 1, 2
|
||||
ORDER BY 1, 2
|
||||
`);
|
||||
}
|
||||
|
||||
getLocomotiveStatusBreakdown(): Promise<{ status: string; count: number }[]> {
|
||||
return this.statusCounts(`
|
||||
SELECT status, COUNT(*)::int AS count
|
||||
FROM freight.locomotives
|
||||
WHERE deleted_at IS NULL
|
||||
GROUP BY status
|
||||
ORDER BY count DESC
|
||||
`);
|
||||
}
|
||||
|
||||
/** Locomotives per station, split by status — the OCC's first question. */
|
||||
getLocomotivesByYard(): Promise<MatrixCell[]> {
|
||||
return this.matrix(`
|
||||
SELECT COALESCE(y.label, 'Unassigned') AS "group",
|
||||
l.status AS series,
|
||||
COUNT(*)::int AS count
|
||||
FROM freight.locomotives l
|
||||
LEFT JOIN freight.yards y ON y.id = l.current_yard_id
|
||||
WHERE l.deleted_at IS NULL
|
||||
GROUP BY 1, 2
|
||||
ORDER BY 1, 2
|
||||
`);
|
||||
}
|
||||
|
||||
getLocomotivesByType(): Promise<{ label: string; count: number }[]> {
|
||||
return this.labelCounts(`
|
||||
SELECT COALESCE(locomotive_type, 'Unknown') AS label,
|
||||
COUNT(*)::int AS count
|
||||
FROM freight.locomotives
|
||||
WHERE deleted_at IS NULL
|
||||
GROUP BY 1
|
||||
ORDER BY count DESC
|
||||
`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Departure-to-departure gap per train set over the range. Only consecutive
|
||||
* *actual* departures count — a schedule that never left says nothing about
|
||||
* how fast the set turned around.
|
||||
*/
|
||||
async getTrainTurnaround(
|
||||
days: number,
|
||||
limit: number,
|
||||
): Promise<{ avgHours: number | null; rows: TurnaroundRow[] }> {
|
||||
const rows = await this.sql.query<
|
||||
{ trainSet: string; hours: string }[]
|
||||
>(
|
||||
`
|
||||
WITH departures AS (
|
||||
SELECT s.train_set_id,
|
||||
s.actual_departure_at,
|
||||
LAG(s.actual_departure_at) OVER (
|
||||
PARTITION BY s.train_set_id ORDER BY s.actual_departure_at
|
||||
) AS previous_departure
|
||||
FROM freight.train_schedules s
|
||||
WHERE s.deleted_at IS NULL
|
||||
AND s.actual_departure_at IS NOT NULL
|
||||
AND s.actual_departure_at >= NOW() - make_interval(days => $1::int)
|
||||
)
|
||||
SELECT COALESCE(t.train_number, t.code, 'Train set') AS "trainSet",
|
||||
ROUND(
|
||||
AVG(
|
||||
EXTRACT(EPOCH FROM (d.actual_departure_at - d.previous_departure)) / 3600
|
||||
)::numeric,
|
||||
1
|
||||
) AS hours
|
||||
FROM departures d
|
||||
LEFT JOIN freight.train_sets ts ON ts.id = d.train_set_id
|
||||
LEFT JOIN freight.trains t ON t.id = ts.train_id
|
||||
WHERE d.previous_departure IS NOT NULL
|
||||
GROUP BY 1
|
||||
ORDER BY hours ASC
|
||||
LIMIT $2::int
|
||||
`,
|
||||
[days, limit],
|
||||
);
|
||||
|
||||
const mapped = rows.map((row) => ({
|
||||
trainSet: row.trainSet,
|
||||
hours: Number(row.hours),
|
||||
}));
|
||||
const avgHours = mapped.length
|
||||
? Number(
|
||||
(
|
||||
mapped.reduce((sum, row) => sum + row.hours, 0) / mapped.length
|
||||
).toFixed(1),
|
||||
)
|
||||
: null;
|
||||
|
||||
return { avgHours, rows: mapped };
|
||||
}
|
||||
|
||||
/**
|
||||
* Bookings per port yard. Import cargo enters at its origin yard, export
|
||||
* cargo leaves from its destination yard — anything else is counted at origin.
|
||||
*/
|
||||
getBookingsByPort(days: number): Promise<{ label: string; count: number }[]> {
|
||||
return this.labelCounts(
|
||||
`
|
||||
SELECT y.label AS label, COUNT(*)::int AS count
|
||||
FROM freight.bookings b
|
||||
INNER JOIN freight.yards y
|
||||
ON y.id = CASE WHEN b.trade_direction = 'EXPORT'
|
||||
THEN b.destination_yard_id ELSE b.origin_yard_id END
|
||||
WHERE b.deleted_at IS NULL
|
||||
AND (b.contract_kind IS NULL OR b.contract_kind <> 'GENERAL')
|
||||
AND b.created_at >= NOW() - make_interval(days => $1::int)
|
||||
GROUP BY 1
|
||||
ORDER BY count DESC
|
||||
`,
|
||||
[days],
|
||||
);
|
||||
}
|
||||
|
||||
getBookingStatusByPort(days: number): Promise<MatrixCell[]> {
|
||||
return this.matrix(
|
||||
`
|
||||
SELECT y.label AS "group", b.status AS series, COUNT(*)::int AS count
|
||||
FROM freight.bookings b
|
||||
INNER JOIN freight.yards y
|
||||
ON y.id = CASE WHEN b.trade_direction = 'EXPORT'
|
||||
THEN b.destination_yard_id ELSE b.origin_yard_id END
|
||||
WHERE b.deleted_at IS NULL
|
||||
AND (b.contract_kind IS NULL OR b.contract_kind <> 'GENERAL')
|
||||
AND b.created_at >= NOW() - make_interval(days => $1::int)
|
||||
GROUP BY 1, 2
|
||||
ORDER BY 1, 2
|
||||
`,
|
||||
[days],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wagon fill and tonnage per scheduled train. Slots come from the train set's
|
||||
* wagon list, the load from confirmed booking allocations against those slots.
|
||||
*/
|
||||
async getTrainLoads(limit: number): Promise<TrainLoadRow[]> {
|
||||
const rows = await this.sql.query<
|
||||
{
|
||||
scheduleId: string;
|
||||
trainNumber: string;
|
||||
date: string;
|
||||
direction: string;
|
||||
wagonsTotal: string;
|
||||
wagonsAllocated: string;
|
||||
tons: string;
|
||||
}[]
|
||||
>(
|
||||
`
|
||||
SELECT s.id AS "scheduleId",
|
||||
COALESCE(s.train_number, s.reference, '—') AS "trainNumber",
|
||||
to_char(s.scheduled_departure_date, 'YYYY-MM-DD') AS date,
|
||||
COALESCE(s.direction, 'DOMESTIC') AS direction,
|
||||
COALESCE(slots.total, 0)::int AS "wagonsTotal",
|
||||
COALESCE(load.wagons, 0)::int AS "wagonsAllocated",
|
||||
COALESCE(load.tons, 0)::float AS tons
|
||||
FROM freight.train_schedules s
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT COUNT(*)::int AS total
|
||||
FROM freight.train_set_wagons tsw
|
||||
WHERE tsw.train_set_id = s.train_set_id
|
||||
AND tsw.deleted_at IS NULL
|
||||
) slots ON TRUE
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT COUNT(DISTINCT wba.train_set_wagon_id)::int AS wagons,
|
||||
COALESCE(SUM(wba.allocated_weight_tons), 0) AS tons
|
||||
FROM freight.wagon_booking_allocations wba
|
||||
INNER JOIN freight.train_set_wagons tsw ON tsw.id = wba.train_set_wagon_id
|
||||
WHERE tsw.train_set_id = s.train_set_id
|
||||
AND wba.deleted_at IS NULL
|
||||
AND tsw.deleted_at IS NULL
|
||||
) load ON TRUE
|
||||
WHERE s.deleted_at IS NULL
|
||||
AND s.status <> 'DRAFT'
|
||||
ORDER BY s.scheduled_departure_date DESC
|
||||
LIMIT $1::int
|
||||
`,
|
||||
[limit],
|
||||
);
|
||||
|
||||
return rows.map((row) => ({
|
||||
scheduleId: row.scheduleId,
|
||||
trainNumber: row.trainNumber,
|
||||
date: row.date,
|
||||
direction: row.direction,
|
||||
wagonsTotal: Number(row.wagonsTotal),
|
||||
wagonsAllocated: Number(row.wagonsAllocated),
|
||||
tons: Number(row.tons),
|
||||
}));
|
||||
}
|
||||
|
||||
getBookingDocumentsByStatus(): Promise<{ status: string; count: number }[]> {
|
||||
return this.statusCounts(`
|
||||
SELECT status, COUNT(*)::int AS count
|
||||
FROM freight.booking_document_review
|
||||
WHERE deleted_at IS NULL
|
||||
GROUP BY status
|
||||
ORDER BY count DESC
|
||||
`);
|
||||
}
|
||||
|
||||
getContractDocumentsByStatus(): Promise<{ status: string; count: number }[]> {
|
||||
return this.statusCounts(`
|
||||
SELECT status, COUNT(*)::int AS count
|
||||
FROM freight.contract_document_review
|
||||
WHERE deleted_at IS NULL
|
||||
GROUP BY status
|
||||
ORDER BY count DESC
|
||||
`);
|
||||
}
|
||||
|
||||
getInvoicesByStatus(): Promise<{ status: string; count: number }[]> {
|
||||
return this.statusCounts(`
|
||||
SELECT status, COUNT(*)::int AS count
|
||||
FROM freight.invoices
|
||||
WHERE deleted_at IS NULL
|
||||
GROUP BY status
|
||||
ORDER BY count DESC
|
||||
`);
|
||||
}
|
||||
|
||||
getInvoicesByType(): Promise<{ label: string; count: number }[]> {
|
||||
return this.labelCounts(`
|
||||
SELECT COALESCE(type, 'Other') AS label, COUNT(*)::int AS count
|
||||
FROM freight.invoices
|
||||
WHERE deleted_at IS NULL
|
||||
GROUP BY 1
|
||||
ORDER BY count DESC
|
||||
`);
|
||||
}
|
||||
|
||||
/** Handover papers per mile, split into signed and awaiting signature. */
|
||||
getHandoversByMile(): Promise<MatrixCell[]> {
|
||||
return this.matrix(`
|
||||
SELECT COALESCE(mile_type, 'Unknown') AS "group",
|
||||
CASE WHEN signed_at IS NULL THEN 'PENDING' ELSE 'SIGNED' END AS series,
|
||||
COUNT(*)::int AS count
|
||||
FROM freight.booking_handovers
|
||||
WHERE deleted_at IS NULL
|
||||
GROUP BY 1, 2
|
||||
ORDER BY 1, 2
|
||||
`);
|
||||
}
|
||||
|
||||
/** Company profiles per type × status — active, pending and suspended per trade role. */
|
||||
getProfilesByTypeStatus(): Promise<MatrixCell[]> {
|
||||
return this.matrix(`
|
||||
SELECT type AS "group", status AS series, COUNT(*)::int AS count
|
||||
FROM freight.company_profiles
|
||||
WHERE deleted_at IS NULL
|
||||
GROUP BY 1, 2
|
||||
ORDER BY 1, 2
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,10 @@ import type { OverviewResponseDto } from './dto/overview-response.dto';
|
||||
import type {
|
||||
OverviewBillingTabDto,
|
||||
OverviewBookingsTabDto,
|
||||
OverviewClearanceTabDto,
|
||||
OverviewContractsTabDto,
|
||||
OverviewCustomersTabDto,
|
||||
OverviewFleetTabDto,
|
||||
OverviewOperationsTabDto,
|
||||
OverviewStaffTabDto,
|
||||
} from './dto/overview-tab-response.dto';
|
||||
@@ -237,6 +239,9 @@ export class OverviewService {
|
||||
wagonStatusBreakdown,
|
||||
containerStatusBreakdown,
|
||||
cargoStatusBreakdown,
|
||||
bookingsByPort,
|
||||
bookingStatusByPort,
|
||||
trainLoads,
|
||||
] = await Promise.all([
|
||||
this.overviewRepository.getOperationsKpis(),
|
||||
this.overviewRepository.getDepartureTrend(days),
|
||||
@@ -249,6 +254,9 @@ export class OverviewService {
|
||||
this.overviewRepository.getWagonStatusBreakdown(),
|
||||
this.overviewRepository.getContainerStatusBreakdown(),
|
||||
this.overviewRepository.getCargoStatusBreakdown(),
|
||||
this.overviewRepository.getBookingsByPort(days),
|
||||
this.overviewRepository.getBookingStatusByPort(days),
|
||||
this.overviewRepository.getTrainLoads(8),
|
||||
]);
|
||||
|
||||
return {
|
||||
@@ -263,6 +271,76 @@ export class OverviewService {
|
||||
wagonStatusBreakdown,
|
||||
containerStatusBreakdown,
|
||||
cargoStatusBreakdown,
|
||||
bookingsByPort,
|
||||
bookingStatusByPort,
|
||||
trainLoads,
|
||||
generatedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Rolling stock in depth: wagons and locomotives crossed with type and yard,
|
||||
* plus how fast each train set turns around. Feeds the operations and control
|
||||
* centre dashboards.
|
||||
*/
|
||||
async getFleetTab(
|
||||
range: OverviewRangeQuery = '30d',
|
||||
): Promise<OverviewFleetTabDto> {
|
||||
const days = OVERVIEW_RANGE_DAYS[range];
|
||||
|
||||
const [
|
||||
wagonStatusBreakdown,
|
||||
wagonStatusByType,
|
||||
wagonStatusByYard,
|
||||
locomotiveStatusBreakdown,
|
||||
locomotivesByYard,
|
||||
locomotivesByType,
|
||||
turnaround,
|
||||
] = await Promise.all([
|
||||
this.overviewRepository.getWagonStatusBreakdown(),
|
||||
this.overviewRepository.getWagonStatusByType(),
|
||||
this.overviewRepository.getWagonStatusByYard(),
|
||||
this.overviewRepository.getLocomotiveStatusBreakdown(),
|
||||
this.overviewRepository.getLocomotivesByYard(),
|
||||
this.overviewRepository.getLocomotivesByType(),
|
||||
this.overviewRepository.getTrainTurnaround(days, 8),
|
||||
]);
|
||||
|
||||
return {
|
||||
wagonStatusBreakdown,
|
||||
wagonStatusByType,
|
||||
wagonStatusByYard,
|
||||
locomotiveStatusBreakdown,
|
||||
locomotivesByYard,
|
||||
locomotivesByType,
|
||||
avgTurnaroundHours: turnaround.avgHours,
|
||||
turnaroundByTrain: turnaround.rows,
|
||||
generatedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
/** Document and invoice queues — the GL desks' and marketing's work in progress. */
|
||||
async getClearanceTab(): Promise<OverviewClearanceTabDto> {
|
||||
const [
|
||||
bookingDocumentsByStatus,
|
||||
contractDocumentsByStatus,
|
||||
invoicesByStatus,
|
||||
invoicesByType,
|
||||
handoversByMile,
|
||||
] = await Promise.all([
|
||||
this.overviewRepository.getBookingDocumentsByStatus(),
|
||||
this.overviewRepository.getContractDocumentsByStatus(),
|
||||
this.overviewRepository.getInvoicesByStatus(),
|
||||
this.overviewRepository.getInvoicesByType(),
|
||||
this.overviewRepository.getHandoversByMile(),
|
||||
]);
|
||||
|
||||
return {
|
||||
bookingDocumentsByStatus,
|
||||
contractDocumentsByStatus,
|
||||
invoicesByStatus,
|
||||
invoicesByType,
|
||||
handoversByMile,
|
||||
generatedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
@@ -273,19 +351,26 @@ export class OverviewService {
|
||||
): Promise<OverviewCustomersTabDto> {
|
||||
const days = OVERVIEW_RANGE_DAYS[range];
|
||||
|
||||
const [kpis, customerGrowthTrend, customersByType, topCustomersByBookings] =
|
||||
await Promise.all([
|
||||
this.overviewRepository.getCustomerKpis(),
|
||||
this.overviewRepository.getCustomerGrowthTrend(days),
|
||||
this.overviewRepository.getCustomersByType(),
|
||||
this.overviewRepository.getTopCustomersByBookings(8, dirs),
|
||||
]);
|
||||
const [
|
||||
kpis,
|
||||
customerGrowthTrend,
|
||||
customersByType,
|
||||
topCustomersByBookings,
|
||||
profilesByTypeStatus,
|
||||
] = await Promise.all([
|
||||
this.overviewRepository.getCustomerKpis(),
|
||||
this.overviewRepository.getCustomerGrowthTrend(days),
|
||||
this.overviewRepository.getCustomersByType(),
|
||||
this.overviewRepository.getTopCustomersByBookings(8, dirs),
|
||||
this.overviewRepository.getProfilesByTypeStatus(),
|
||||
]);
|
||||
|
||||
return {
|
||||
kpis,
|
||||
customerGrowthTrend,
|
||||
customersByType,
|
||||
topCustomersByBookings,
|
||||
profilesByTypeStatus,
|
||||
generatedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user