mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Overall report and segment tariff override updates
This commit is contained in:
@@ -88,20 +88,48 @@ export class FareEngineService {
|
||||
where: {
|
||||
routeId: route.id,
|
||||
seatClassId: dto.seatClassId,
|
||||
AND: [
|
||||
{
|
||||
OR: [
|
||||
{
|
||||
originStopSequence: originStop.sequence,
|
||||
destinationStopSequence: destStop.sequence,
|
||||
validFrom: { lte: now },
|
||||
},
|
||||
{
|
||||
originStopSequence: destStop.sequence,
|
||||
destinationStopSequence: originStop.sequence,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
OR: [{ validUntil: null }, { validUntil: { gte: now } }],
|
||||
},
|
||||
],
|
||||
validFrom: { lte: now },
|
||||
nationality: dto.nationality ?? null,
|
||||
},
|
||||
}) ?? await this.prisma.segmentFareRule.findFirst({
|
||||
where: {
|
||||
routeId: route.id,
|
||||
seatClassId: dto.seatClassId,
|
||||
AND: [
|
||||
{
|
||||
OR: [
|
||||
{
|
||||
originStopSequence: originStop.sequence,
|
||||
destinationStopSequence: destStop.sequence,
|
||||
validFrom: { lte: now },
|
||||
},
|
||||
{
|
||||
originStopSequence: destStop.sequence,
|
||||
destinationStopSequence: originStop.sequence,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
OR: [{ validUntil: null }, { validUntil: { gte: now } }],
|
||||
},
|
||||
],
|
||||
validFrom: { lte: now },
|
||||
nationality: null,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
Tooltip, Legend, ResponsiveContainer, PieChart, Pie, Cell,
|
||||
} from 'recharts';
|
||||
import { bookingsApi } from '@/lib/api';
|
||||
import { dashboardApi } from '@/lib/api/dashboard';
|
||||
import { apiClient } from '@/lib/api-client';
|
||||
import { formatCurrency } from '@/lib/utils';
|
||||
import ActionButton from '@/components/ui/ActionButton';
|
||||
@@ -62,12 +61,6 @@ export default function ReportsPage() {
|
||||
const dates = getDateRange();
|
||||
|
||||
// Confirmed-ticket revenue — same source as dashboard
|
||||
const { data: stats, isLoading: statsLoading } = useQuery({
|
||||
queryKey: ['backoffice-stats'],
|
||||
queryFn: dashboardApi.getBackofficeStats,
|
||||
staleTime: 60000,
|
||||
});
|
||||
|
||||
const { data: exchangeRates = [] } = useQuery<any[]>({
|
||||
queryKey: ['currencies'],
|
||||
queryFn: () => apiClient.get('/currencies'),
|
||||
@@ -92,19 +85,13 @@ export default function ReportsPage() {
|
||||
return rate !== null ? sum + Math.round(totalMinor * rate) : sum;
|
||||
}, 0);
|
||||
|
||||
const normalRows = stats?.revenueByCurrency ?? [];
|
||||
const packageRows = stats?.packageRevenueByCurrency ?? [];
|
||||
const normalGrand = calcGrand(normalRows);
|
||||
const packageGrand = calcGrand(packageRows);
|
||||
const overallGrand = normalGrand + packageGrand;
|
||||
|
||||
// Bookings for charts / status distribution
|
||||
const { data: bookingsData, isLoading: bookingsLoading } = useQuery({
|
||||
queryKey: ['all-bookings'],
|
||||
queryFn: () => bookingsApi.getAll({ pageSize: 1000 }),
|
||||
queryKey: ['all-bookings', dates.startDate, dates.endDate],
|
||||
queryFn: () => bookingsApi.getAll({ pageSize: 5000, dateFrom: dates.startDate, dateTo: dates.endDate }),
|
||||
});
|
||||
|
||||
const isLoading = statsLoading || bookingsLoading;
|
||||
const isLoading = bookingsLoading;
|
||||
|
||||
const allBookings: any[] = Array.isArray(bookingsData?.items) ? bookingsData.items : [];
|
||||
|
||||
@@ -113,7 +100,7 @@ export default function ReportsPage() {
|
||||
return d >= dates.startDate && d <= dates.endDate;
|
||||
});
|
||||
|
||||
const confirmedBookings = bookings.filter((b: any) => b.status !== 'CANCELLED' && b.status !== 'REFUNDED');
|
||||
const confirmedBookings = bookings.filter((b: any) => b.status === 'CONFIRMED' || b.status === 'BOARDED');
|
||||
|
||||
const totalRevenueMinor = confirmedBookings.reduce((sum, b: any) => {
|
||||
const rate = toEtbRate(b.currency);
|
||||
@@ -151,6 +138,34 @@ export default function ReportsPage() {
|
||||
return bookingType === 'PACKAGE' || Boolean(b.packageId);
|
||||
}).length;
|
||||
|
||||
const normalRows = confirmedBookings
|
||||
.filter((b: any) => {
|
||||
const bookingType = String(b.bookingType || '').toUpperCase();
|
||||
return bookingType !== 'PACKAGE' && !b.packageId;
|
||||
})
|
||||
.reduce((acc: Record<string, number>, b: any) => {
|
||||
const currency = b.currency || 'ETB';
|
||||
acc[currency] = (acc[currency] || 0) + (b.totalMinor || 0);
|
||||
return acc;
|
||||
}, {} as Record<string, number>);
|
||||
|
||||
const packageRows = confirmedBookings
|
||||
.filter((b: any) => {
|
||||
const bookingType = String(b.bookingType || '').toUpperCase();
|
||||
return bookingType === 'PACKAGE' || Boolean(b.packageId);
|
||||
})
|
||||
.reduce((acc: Record<string, number>, b: any) => {
|
||||
const currency = b.currency || 'ETB';
|
||||
acc[currency] = (acc[currency] || 0) + (b.totalMinor || 0);
|
||||
return acc;
|
||||
}, {} as Record<string, number>);
|
||||
|
||||
const normalRowData = Object.entries(normalRows).map(([currency, totalMinor]) => ({ currency, totalMinor }));
|
||||
const packageRowData = Object.entries(packageRows).map(([currency, totalMinor]) => ({ currency, totalMinor }));
|
||||
const normalGrand = calcGrand(normalRowData);
|
||||
const packageGrand = calcGrand(packageRowData);
|
||||
const overallGrand = normalGrand + packageGrand;
|
||||
|
||||
const filteredRouteBookings = confirmedBookings.filter((b: any) => {
|
||||
const origin = b.schedule?.originStation?.name || b.originStationName || b.origin || 'Unknown';
|
||||
const destination = b.schedule?.destinationStation?.name || b.destinationStationName || b.destination || 'Unknown';
|
||||
@@ -201,8 +216,8 @@ export default function ReportsPage() {
|
||||
`th,td{border:1px solid #ccc;padding:4px 8px}th{background:#10b981;color:#fff}</style></head><body>` +
|
||||
`<h2>Revenue Report — ${esc(dates.startDate)} to ${esc(dates.endDate)}</h2>` +
|
||||
`<p>Total Revenue: ${esc(formatCurrency(overallGrand, 'ETB'))} | ` +
|
||||
`Bookings: ${esc(String(stats?.totalBookings ?? 0))} | ` +
|
||||
`Tickets: ${esc(String(stats?.totalTickets ?? 0))}</p>` +
|
||||
`Bookings: ${esc(String(totalBookingsCount))} | ` +
|
||||
`Tickets: ${esc(String(totalTicketsCount))}</p>` +
|
||||
`<table><thead><tr>${thead}</tr></thead><tbody>${tbody}</tbody></table></body></html>`
|
||||
);
|
||||
w.document.close(); w.print();
|
||||
@@ -309,11 +324,11 @@ export default function ReportsPage() {
|
||||
<div className="flex flex-col gap-1 border-t border-border pt-2 mt-1">
|
||||
<div className="flex justify-between text-xs">
|
||||
<span className="text-muted-foreground">Regular</span>
|
||||
<span className="font-semibold tabular-nums">{statsLoading ? '—' : formatCurrency(normalGrand, 'ETB')}</span>
|
||||
<span className="font-semibold tabular-nums">{isLoading ? '—' : formatCurrency(normalGrand, 'ETB')}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-xs">
|
||||
<span className="text-muted-foreground">Package</span>
|
||||
<span className="font-semibold tabular-nums">{statsLoading ? '—' : formatCurrency(packageGrand, 'ETB')}</span>
|
||||
<span className="font-semibold tabular-nums">{isLoading ? '—' : formatCurrency(packageGrand, 'ETB')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -386,9 +401,9 @@ export default function ReportsPage() {
|
||||
<h2 className="text-sm font-semibold uppercase tracking-widest text-muted-foreground mb-4">
|
||||
Revenue Breakdown — Confirmed Tickets
|
||||
</h2>
|
||||
{statsLoading ? (
|
||||
{isLoading ? (
|
||||
<p className="text-sm text-muted-foreground">Loading…</p>
|
||||
) : !normalRows.length && !packageRows.length ? (
|
||||
) : !normalRowData.length && !packageRowData.length ? (
|
||||
<p className="text-sm text-muted-foreground">No revenue data yet.</p>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
@@ -397,12 +412,12 @@ export default function ReportsPage() {
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Regular</span>
|
||||
<span className="text-xs text-muted-foreground tabular-nums">
|
||||
{(stats?.totalNormalBookings ?? 0).toLocaleString()} bookings · {(stats?.totalNormalTickets ?? 0).toLocaleString()} tickets
|
||||
{totalRegularBookingsCount.toLocaleString()} bookings · {totalRegularTicketsCount.toLocaleString()} tickets
|
||||
</span>
|
||||
</div>
|
||||
{normalRows.length === 0
|
||||
{normalRowData.length === 0
|
||||
? <p className="text-xs text-muted-foreground py-1">No revenue yet</p>
|
||||
: normalRows.map(renderCurrencyRow)}
|
||||
: normalRowData.map(renderCurrencyRow)}
|
||||
{normalRows.length > 0 && (
|
||||
<div className="flex items-center justify-between rounded-md bg-muted/40 px-3 py-2 mt-1">
|
||||
<span className="text-xs font-semibold text-muted-foreground">Subtotal</span>
|
||||
@@ -416,13 +431,13 @@ export default function ReportsPage() {
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Package</span>
|
||||
<span className="text-xs text-muted-foreground tabular-nums">
|
||||
{(stats?.totalPackageBookings ?? 0).toLocaleString()} bookings · {(stats?.totalPackageTickets ?? 0).toLocaleString()} tickets
|
||||
{totalPackageBookingsCount.toLocaleString()} bookings · {totalPackageTicketsCount.toLocaleString()} tickets
|
||||
</span>
|
||||
</div>
|
||||
{packageRows.length === 0
|
||||
{packageRowData.length === 0
|
||||
? <p className="text-xs text-muted-foreground py-1">No revenue yet</p>
|
||||
: packageRows.map(renderCurrencyRow)}
|
||||
{packageRows.length > 0 && (
|
||||
: packageRowData.map(renderCurrencyRow)}
|
||||
{packageRowData.length > 0 && (
|
||||
<div className="flex items-center justify-between rounded-md bg-muted/40 px-3 py-2 mt-1">
|
||||
<span className="text-xs font-semibold text-muted-foreground">Subtotal</span>
|
||||
<span className="text-sm font-bold tabular-nums">{formatCurrency(packageGrand, 'ETB')}</span>
|
||||
@@ -431,7 +446,7 @@ export default function ReportsPage() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!statsLoading && (normalRows.length > 0 || packageRows.length > 0) && (
|
||||
{(normalRowData.length > 0 || packageRowData.length > 0) && (
|
||||
<div className="flex items-center justify-between rounded-lg border border-border bg-muted/30 px-4 py-3 mt-4">
|
||||
<span className="text-sm font-semibold text-muted-foreground">Grand Total (ETB equivalent)</span>
|
||||
<span className="text-lg font-bold text-emerald-600 dark:text-emerald-400 tabular-nums">
|
||||
@@ -637,17 +652,17 @@ export default function ReportsPage() {
|
||||
<h3 className="text-base font-semibold mb-4">Summary</h3>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-3">
|
||||
{[
|
||||
{ label: 'Active Days', value: chartData.length, fromStats: false },
|
||||
{ label: 'Confirmed', value: bookings.filter((b: any) => b.status === 'CONFIRMED').length, fromStats: false },
|
||||
{ label: 'Boarded', value: bookings.filter((b: any) => b.status === 'BOARDED').length, fromStats: false },
|
||||
{ label: 'Cancelled', value: bookings.filter((b: any) => b.status === 'CANCELLED').length, fromStats: false },
|
||||
{ label: 'Regular Bookings', value: totalRegularBookingsCount, fromStats: false },
|
||||
{ label: 'Package Bookings', value: totalPackageBookingsCount, fromStats: false },
|
||||
].map(({ label, value, fromStats }) => (
|
||||
{ label: 'Active Days', value: chartData.length },
|
||||
{ label: 'Confirmed', value: bookings.filter((b: any) => b.status === 'CONFIRMED').length },
|
||||
{ label: 'Boarded', value: bookings.filter((b: any) => b.status === 'BOARDED').length },
|
||||
{ label: 'Cancelled', value: bookings.filter((b: any) => b.status === 'CANCELLED').length },
|
||||
{ label: 'Regular Bookings', value: totalRegularBookingsCount },
|
||||
{ label: 'Package Bookings', value: totalPackageBookingsCount },
|
||||
].map(({ label, value }) => (
|
||||
<div key={label} className="border border-border rounded-lg p-3 text-center">
|
||||
<p className="text-xs text-muted-foreground">{label}</p>
|
||||
<p className="text-xl font-bold mt-1 tabular-nums">
|
||||
{fromStats && statsLoading ? '—' : value.toLocaleString()}
|
||||
{value.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user