diff --git a/apps/edr-passenger-api/src/modules/fare-engine/fare-engine.service.ts b/apps/edr-passenger-api/src/modules/fare-engine/fare-engine.service.ts index 69b0266ed..348e9d6be 100644 --- a/apps/edr-passenger-api/src/modules/fare-engine/fare-engine.service.ts +++ b/apps/edr-passenger-api/src/modules/fare-engine/fare-engine.service.ts @@ -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, }, }); diff --git a/apps/edr-passenger-web/backoffice/src/app/reports/overall/page.tsx b/apps/edr-passenger-web/backoffice/src/app/reports/overall/page.tsx index 5e23b92d6..0ecc7a885 100644 --- a/apps/edr-passenger-web/backoffice/src/app/reports/overall/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/reports/overall/page.tsx @@ -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({ 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, b: any) => { + const currency = b.currency || 'ETB'; + acc[currency] = (acc[currency] || 0) + (b.totalMinor || 0); + return acc; + }, {} as Record); + + const packageRows = confirmedBookings + .filter((b: any) => { + const bookingType = String(b.bookingType || '').toUpperCase(); + return bookingType === 'PACKAGE' || Boolean(b.packageId); + }) + .reduce((acc: Record, b: any) => { + const currency = b.currency || 'ETB'; + acc[currency] = (acc[currency] || 0) + (b.totalMinor || 0); + return acc; + }, {} as Record); + + 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}` + `

Revenue Report — ${esc(dates.startDate)} to ${esc(dates.endDate)}

` + `

Total Revenue: ${esc(formatCurrency(overallGrand, 'ETB'))} | ` + - `Bookings: ${esc(String(stats?.totalBookings ?? 0))} | ` + - `Tickets: ${esc(String(stats?.totalTickets ?? 0))}

` + + `Bookings: ${esc(String(totalBookingsCount))} | ` + + `Tickets: ${esc(String(totalTicketsCount))}

` + `${thead}${tbody}
` ); w.document.close(); w.print(); @@ -309,11 +324,11 @@ export default function ReportsPage() {
Regular - {statsLoading ? '—' : formatCurrency(normalGrand, 'ETB')} + {isLoading ? '—' : formatCurrency(normalGrand, 'ETB')}
Package - {statsLoading ? '—' : formatCurrency(packageGrand, 'ETB')} + {isLoading ? '—' : formatCurrency(packageGrand, 'ETB')}
@@ -386,9 +401,9 @@ export default function ReportsPage() {

Revenue Breakdown — Confirmed Tickets

- {statsLoading ? ( + {isLoading ? (

Loading…

- ) : !normalRows.length && !packageRows.length ? ( + ) : !normalRowData.length && !packageRowData.length ? (

No revenue data yet.

) : (
@@ -397,12 +412,12 @@ export default function ReportsPage() {
Regular - {(stats?.totalNormalBookings ?? 0).toLocaleString()} bookings · {(stats?.totalNormalTickets ?? 0).toLocaleString()} tickets + {totalRegularBookingsCount.toLocaleString()} bookings · {totalRegularTicketsCount.toLocaleString()} tickets
- {normalRows.length === 0 + {normalRowData.length === 0 ?

No revenue yet

- : normalRows.map(renderCurrencyRow)} + : normalRowData.map(renderCurrencyRow)} {normalRows.length > 0 && (
Subtotal @@ -416,13 +431,13 @@ export default function ReportsPage() {
Package - {(stats?.totalPackageBookings ?? 0).toLocaleString()} bookings · {(stats?.totalPackageTickets ?? 0).toLocaleString()} tickets + {totalPackageBookingsCount.toLocaleString()} bookings · {totalPackageTicketsCount.toLocaleString()} tickets
- {packageRows.length === 0 + {packageRowData.length === 0 ?

No revenue yet

- : packageRows.map(renderCurrencyRow)} - {packageRows.length > 0 && ( + : packageRowData.map(renderCurrencyRow)} + {packageRowData.length > 0 && (
Subtotal {formatCurrency(packageGrand, 'ETB')} @@ -431,7 +446,7 @@ export default function ReportsPage() {
)} - {!statsLoading && (normalRows.length > 0 || packageRows.length > 0) && ( + {(normalRowData.length > 0 || packageRowData.length > 0) && (
Grand Total (ETB equivalent) @@ -637,17 +652,17 @@ export default function ReportsPage() {

Summary

{[ - { 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 }) => (

{label}

- {fromStats && statsLoading ? '—' : value.toLocaleString()} + {value.toLocaleString()}

))}