mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 04:08:11 +00:00
Adding seat blocking revenue loss dashboard
This commit is contained in:
@@ -1,6 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { DashboardController } from './dashboard.controller';
|
||||
import { DashboardService } from './dashboard.service';
|
||||
import { ReportsModule } from '../reports/reports.module';
|
||||
|
||||
@Module({ controllers: [DashboardController], providers: [DashboardService] })
|
||||
@Module({
|
||||
// ReportsModule owns the blocked-seat revenue loss rule; the dashboard's roll-up
|
||||
// reads it from there instead of keeping a second copy of the definition.
|
||||
imports: [ReportsModule],
|
||||
controllers: [DashboardController],
|
||||
providers: [DashboardService],
|
||||
})
|
||||
export class DashboardModule {}
|
||||
|
||||
@@ -1,17 +1,34 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectDataSource } from '@nestjs/typeorm';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { BlockedSeatRevenueLossStat } from '@edr/types';
|
||||
import { PrismaService } from '../../common/prisma.service';
|
||||
import { ReportsService } from '../reports/reports.service';
|
||||
|
||||
/** Window the dashboard's blocked-seat loss roll-up covers. Matches the report's default. */
|
||||
const BLOCKED_SEAT_LOSS_PERIOD_DAYS = 30;
|
||||
|
||||
/** Shown when nothing is blocked, or when the loss roll-up could not be computed. */
|
||||
const EMPTY_BLOCKED_SEAT_LOSS: BlockedSeatRevenueLossStat = {
|
||||
periodDays: BLOCKED_SEAT_LOSS_PERIOD_DAYS,
|
||||
lossByCurrency: [],
|
||||
schedulesAffected: 0,
|
||||
blockedSeatCount: 0,
|
||||
topReasonCategory: null,
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class DashboardService {
|
||||
private readonly logger = new Logger(DashboardService.name);
|
||||
|
||||
constructor(
|
||||
private prisma: PrismaService,
|
||||
@InjectDataSource() private dataSource: DataSource,
|
||||
private reports: ReportsService,
|
||||
) {}
|
||||
|
||||
async getBackofficeStats() {
|
||||
const [totalBookings, totalPackageBookings, totalTickets, totalPassengers, blockedSeatsCount, revenueRows, packageRevenueRows] =
|
||||
const [totalBookings, totalPackageBookings, totalTickets, totalPassengers, blockedSeatsCount, revenueRows, packageRevenueRows, blockedSeatRevenueLoss] =
|
||||
await Promise.all([
|
||||
this.prisma.booking.count({ where: { status: { in: ['CONFIRMED', 'BOARDED'] } } }),
|
||||
this.prisma.booking.count({ where: { status: { in: ['CONFIRMED', 'BOARDED'] }, packageId: { not: null } } }),
|
||||
@@ -38,6 +55,9 @@ export class DashboardService {
|
||||
AND id IN (SELECT "bookingId" FROM passenger."PaymentIntent" WHERE status = 'SUCCEEDED')
|
||||
GROUP BY COALESCE("displayCurrency"::text, "currency"::text)
|
||||
`,
|
||||
// Joined into this same call on purpose: the dashboard's request count stays
|
||||
// exactly where it was, and the card renders from the payload it already fetches.
|
||||
this.getBlockedSeatRevenueLossStat(),
|
||||
]);
|
||||
|
||||
const totalPackageTickets = await this.prisma.ticket.count({
|
||||
@@ -58,11 +78,43 @@ export class DashboardService {
|
||||
totalNormalTickets: totalTickets - totalPackageTickets,
|
||||
totalPassengers,
|
||||
blockedSeatsCount,
|
||||
blockedSeatRevenueLoss,
|
||||
revenueByCurrency: toMap(revenueRows),
|
||||
packageRevenueByCurrency: toMap(packageRevenueRows),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Compact roll-up of the Blocked Seat Revenue Loss report over the last 30 days.
|
||||
*
|
||||
* Reuses the report service rather than re-deriving the rule — there is exactly one
|
||||
* definition of what a blocked seat costs. A failure here degrades to zeroes instead of
|
||||
* taking the whole dashboard down with it.
|
||||
*/
|
||||
private async getBlockedSeatRevenueLossStat(): Promise<BlockedSeatRevenueLossStat> {
|
||||
try {
|
||||
// pageSize 1: only the summary is read, and paging does not change what it covers.
|
||||
const report = await this.reports.getBlockedSeatsRevenueLoss({ page: 1, pageSize: 1 });
|
||||
const { summary } = report;
|
||||
|
||||
return {
|
||||
periodDays: BLOCKED_SEAT_LOSS_PERIOD_DAYS,
|
||||
lossByCurrency: summary.lossByCurrency,
|
||||
schedulesAffected: summary.schedulesAffected,
|
||||
blockedSeatCount: summary.blockedSeatCount,
|
||||
// topReasonCategories is already sorted by estimated loss, descending.
|
||||
topReasonCategory: summary.topReasonCategories[0]?.reasonCategory ?? null,
|
||||
};
|
||||
} catch (err) {
|
||||
this.logger.warn(
|
||||
`Blocked-seat revenue loss roll-up unavailable — ${
|
||||
err instanceof Error ? err.message : String(err)
|
||||
}`,
|
||||
);
|
||||
return EMPTY_BLOCKED_SEAT_LOSS;
|
||||
}
|
||||
}
|
||||
|
||||
async getHomeDashboard(passengerId: string) {
|
||||
const now = new Date();
|
||||
const [passenger, upcomingBooking, wallet, promos, weatherAlerts, stationSignals, savedRoutes] = await Promise.all([
|
||||
|
||||
Reference in New Issue
Block a user