mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Adding seat blocking revenue loss dashboard
This commit is contained in:
@@ -2,5 +2,9 @@ export * from "./common/index";
|
||||
export * from "./freight/index";
|
||||
export * as Freight from "./freight/index";
|
||||
export * as Passenger from "./passenger/index";
|
||||
// Flat re-export: the Blocked Seat Revenue Loss shapes are shared verbatim between the
|
||||
// passenger API and the backoffice, and reading them through the `Passenger.` namespace
|
||||
// on every line buys nothing.
|
||||
export * from "./passenger/blocked-seat-revenue-loss";
|
||||
export type { PaymentEvent, PaymentEventType, PaymentFailedEvent, PaymentSucceededEvent, PaymentIntentSnapshot, InitiatePaymentRequest } from "./common/payments";
|
||||
export { PaymentReferenceType, PaymentService } from "./common/payments";
|
||||
|
||||
169
packages/types/src/passenger/blocked-seat-revenue-loss.ts
Normal file
169
packages/types/src/passenger/blocked-seat-revenue-loss.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* Blocked Seat Revenue Loss report — shared shapes for
|
||||
* `GET /reports/blocked-seats-revenue-loss`.
|
||||
*
|
||||
* Every monetary field is an **integer count of minor units** (ETB cents, DJF centimes …)
|
||||
* and is always paired with its `currency`. Never sum across currencies.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Why a seat was pulled out of sale. Mirrors the Prisma `SeatBlockReasonCategory` enum.
|
||||
*
|
||||
* Declared as a const object + union type rather than a TS `enum` so that Prisma's own
|
||||
* generated string-literal union assigns to it directly, with no cast at the boundary.
|
||||
*/
|
||||
export const SeatBlockReasonCategory = {
|
||||
Maintenance: "MAINTENANCE",
|
||||
VipReserved: "VIP_RESERVED",
|
||||
Safety: "SAFETY",
|
||||
Operational: "OPERATIONAL",
|
||||
Other: "OTHER",
|
||||
} as const;
|
||||
|
||||
export type SeatBlockReasonCategory =
|
||||
(typeof SeatBlockReasonCategory)[keyof typeof SeatBlockReasonCategory];
|
||||
|
||||
/** Every category value, in the order they should appear in a picker. */
|
||||
export const SEAT_BLOCK_REASON_CATEGORIES: readonly SeatBlockReasonCategory[] =
|
||||
Object.values(SeatBlockReasonCategory);
|
||||
|
||||
/** Human labels for {@link SeatBlockReasonCategory}, plus the legacy null bucket. */
|
||||
export const SEAT_BLOCK_REASON_CATEGORY_LABELS: Record<string, string> = {
|
||||
MAINTENANCE: "Maintenance",
|
||||
VIP_RESERVED: "VIP Reserved",
|
||||
SAFETY: "Safety",
|
||||
OPERATIONAL: "Operational",
|
||||
OTHER: "Other",
|
||||
UNCATEGORIZED: "Uncategorized",
|
||||
};
|
||||
|
||||
/** Bucket label used for blocks written before `reasonCategory` existed. */
|
||||
export const UNCATEGORIZED_REASON_CATEGORY = "UNCATEGORIZED";
|
||||
|
||||
/**
|
||||
* How the block reached this schedule.
|
||||
* - `SCHEDULE` — a `SeatBlock` row naming this `scheduleId` directly.
|
||||
* - `GLOBAL` — a `SeatBlock` row with no `scheduleId`, in effect at departure, whose
|
||||
* seat's coach was assigned to this schedule.
|
||||
*/
|
||||
export type BlockedSeatBlockType = "SCHEDULE" | "GLOBAL";
|
||||
|
||||
/** One blocked seat on one schedule — the drill-down row. */
|
||||
export interface BlockedSeatLossDetail {
|
||||
blockId: string;
|
||||
seatId: string;
|
||||
coachNumber: string | null;
|
||||
seatNumber: string | null;
|
||||
seatClassName: string | null;
|
||||
/** Operator's free-text detail, verbatim. */
|
||||
reason: string;
|
||||
/** `null` on rows written before the column existed — render as "Uncategorized". */
|
||||
reasonCategory: SeatBlockReasonCategory | null;
|
||||
blockType: BlockedSeatBlockType;
|
||||
/** IAM user id, or `SYSTEM` for system-initiated blocks. */
|
||||
blockedBy: string;
|
||||
/** `null` on legacy rows — render as "Unknown". */
|
||||
blockedByName: string | null;
|
||||
approvedBy: string | null;
|
||||
blockedAt: string;
|
||||
unblockAt: string | null;
|
||||
/** True when the block has no scheduled end. */
|
||||
stillBlocked: boolean;
|
||||
/** Whole days from `blockedAt` to `unblockAt`, or to now while still blocked. */
|
||||
daysBlocked: number;
|
||||
estimatedLossMinor: number;
|
||||
currency: string;
|
||||
}
|
||||
|
||||
/** One schedule with at least one blocked seat counted against it. */
|
||||
export interface BlockedSeatLossSchedule {
|
||||
scheduleId: string;
|
||||
trainNumber: string;
|
||||
routeName: string | null;
|
||||
originStation: string;
|
||||
destinationStation: string;
|
||||
departureAt: string;
|
||||
status: string;
|
||||
/** Non-dining, non-placeholder seats on the coaches assigned to this schedule. */
|
||||
sellableSeats: number;
|
||||
/** Seats with a CONFIRMED/BOARDED booking on this schedule. */
|
||||
soldSeats: number;
|
||||
/** `soldSeats / sellableSeats`, as a percentage rounded to one decimal. */
|
||||
loadFactorPercent: number;
|
||||
blockedSeatCount: number;
|
||||
/** Loss at full occupancy — the sum of the fares these seats would have sold for. */
|
||||
estimatedLossMinor: number;
|
||||
/** `estimatedLossMinor × loadFactor` — what the train's actual demand supports. */
|
||||
adjustedLossMinor: number;
|
||||
currency: string;
|
||||
blocks: BlockedSeatLossDetail[];
|
||||
}
|
||||
|
||||
/** Loss totals for one currency. */
|
||||
export interface BlockedSeatLossByCurrency {
|
||||
currency: string;
|
||||
estimatedLossMinor: number;
|
||||
adjustedLossMinor: number;
|
||||
}
|
||||
|
||||
/** Loss grouped by reason category, per currency. */
|
||||
export interface BlockedSeatLossByReasonCategory {
|
||||
/** A {@link SeatBlockReasonCategory} value, or {@link UNCATEGORIZED_REASON_CATEGORY}. */
|
||||
reasonCategory: string;
|
||||
count: number;
|
||||
estimatedLossMinor: number;
|
||||
currency: string;
|
||||
}
|
||||
|
||||
/** Loss grouped by the staff member who blocked the seat, per currency. */
|
||||
export interface BlockedSeatLossByBlocker {
|
||||
blockedBy: string;
|
||||
blockedByName: string;
|
||||
count: number;
|
||||
estimatedLossMinor: number;
|
||||
currency: string;
|
||||
}
|
||||
|
||||
export interface BlockedSeatLossSummary {
|
||||
schedulesAffected: number;
|
||||
blockedSeatCount: number;
|
||||
lossByCurrency: BlockedSeatLossByCurrency[];
|
||||
topReasonCategories: BlockedSeatLossByReasonCategory[];
|
||||
topBlockers: BlockedSeatLossByBlocker[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Provenance for the numbers above. `methodology` and `exclusions` are meant to be
|
||||
* rendered verbatim in the UI — this report is a counterfactual, and the assumptions
|
||||
* behind it have to travel with it.
|
||||
*/
|
||||
export interface BlockedSeatLossMeta {
|
||||
/** Total schedules matching the filters, before pagination. */
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
dateFrom: string;
|
||||
dateTo: string;
|
||||
/** Nationality the fares were priced at (drives tariff variant and currency). */
|
||||
nationalityAssumption: string;
|
||||
methodology: string;
|
||||
exclusions: string[];
|
||||
/** Schedules whose fare could not be computed — their seats count but carry no loss. */
|
||||
schedulesWithoutFare: number;
|
||||
}
|
||||
|
||||
export interface BlockedSeatRevenueLossReport {
|
||||
summary: BlockedSeatLossSummary;
|
||||
schedules: BlockedSeatLossSchedule[];
|
||||
meta: BlockedSeatLossMeta;
|
||||
}
|
||||
|
||||
/** Compact roll-up embedded in `GET /dashboard/backoffice-stats`. */
|
||||
export interface BlockedSeatRevenueLossStat {
|
||||
periodDays: number;
|
||||
lossByCurrency: BlockedSeatLossByCurrency[];
|
||||
schedulesAffected: number;
|
||||
blockedSeatCount: number;
|
||||
/** Largest category by estimated loss, or `null` when nothing is blocked. */
|
||||
topReasonCategory: string | null;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { BaseEntity } from "../common";
|
||||
|
||||
export * from "./support-chat";
|
||||
export * from "./blocked-seat-revenue-loss";
|
||||
|
||||
export enum TicketStatus {
|
||||
Reserved = "RESERVED",
|
||||
|
||||
Reference in New Issue
Block a user