Merge pull request #1256 from Tria-plc/alpha

Overall report and segment tariff override updates
This commit is contained in:
Stephanos A.
2026-08-12 13:23:00 +03:00
committed by GitHub
2 changed files with 88 additions and 45 deletions

View File

@@ -88,20 +88,48 @@ export class FareEngineService {
where: {
routeId: route.id,
seatClassId: dto.seatClassId,
originStopSequence: originStop.sequence,
destinationStopSequence: destStop.sequence,
AND: [
{
OR: [
{
originStopSequence: originStop.sequence,
destinationStopSequence: destStop.sequence,
},
{
originStopSequence: destStop.sequence,
destinationStopSequence: originStop.sequence,
},
],
},
{
OR: [{ validUntil: null }, { validUntil: { gte: now } }],
},
],
validFrom: { lte: now },
OR: [{ validUntil: null }, { validUntil: { gte: now } }],
nationality: dto.nationality ?? null,
},
}) ?? await this.prisma.segmentFareRule.findFirst({
where: {
routeId: route.id,
seatClassId: dto.seatClassId,
originStopSequence: originStop.sequence,
destinationStopSequence: destStop.sequence,
AND: [
{
OR: [
{
originStopSequence: originStop.sequence,
destinationStopSequence: destStop.sequence,
},
{
originStopSequence: destStop.sequence,
destinationStopSequence: originStop.sequence,
},
],
},
{
OR: [{ validUntil: null }, { validUntil: { gte: now } }],
},
],
validFrom: { lte: now },
OR: [{ validUntil: null }, { validUntil: { gte: now } }],
nationality: null,
},
});

View File

@@ -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>
))}