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 6ab811622..5e23b92d6 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 @@ -24,6 +24,8 @@ export default function ReportsPage() { const [dateRange, setDateRange] = useState('30'); const [startDate, setStartDate] = useState(''); const [endDate, setEndDate] = useState(''); + const [routeOrigin, setRouteOrigin] = useState(''); + const [routeDestination, setRouteDestination] = useState(''); const [exportModalOpen, setExportModalOpen] = useState(false); const [exportFormat, setExportFormat] = useState<'csv' | 'excel' | 'pdf'>('csv'); @@ -78,6 +80,12 @@ export default function ReportsPage() { return r ? 1 / r.rate : null; }; + const getBookingTicketCount = (booking: any): number => { + if (Array.isArray(booking.tickets)) return booking.tickets.length; + if (typeof booking.ticketCount === 'number') return booking.ticketCount; + return 0; + }; + const calcGrand = (rows: { currency: string; totalMinor: number }[]) => rows.reduce((sum, { currency, totalMinor }) => { const rate = toEtbRate(currency); @@ -107,6 +115,14 @@ export default function ReportsPage() { const confirmedBookings = bookings.filter((b: any) => b.status !== 'CANCELLED' && b.status !== 'REFUNDED'); + const totalRevenueMinor = confirmedBookings.reduce((sum, b: any) => { + const rate = toEtbRate(b.currency); + return rate !== null ? sum + Math.round((b.totalMinor || 0) * rate) : sum; + }, 0); + + const totalBookingsCount = confirmedBookings.length; + const totalTicketsCount = confirmedBookings.reduce((sum, b: any) => sum + getBookingTicketCount(b), 0); + const byDate = confirmedBookings.reduce((acc: Record, b: any) => { const date = new Date(b.createdAt).toISOString().split('T')[0]; if (!acc[date]) acc[date] = { totalMinor: 0, count: 0 }; @@ -123,7 +139,50 @@ export default function ReportsPage() { bookings: d.count || 0, })); - const avgDailyRevenue = chartData.length > 0 ? Math.round(overallGrand / 100 / chartData.length) : 0; + const avgDailyRevenueMinor = chartData.length > 0 ? Math.round(totalRevenueMinor / chartData.length) : 0; + + const totalRegularBookingsCount = confirmedBookings.filter((b: any) => { + const bookingType = String(b.bookingType || '').toUpperCase(); + return bookingType !== 'PACKAGE' && !b.packageId; + }).length; + + const totalPackageBookingsCount = confirmedBookings.filter((b: any) => { + const bookingType = String(b.bookingType || '').toUpperCase(); + return bookingType === 'PACKAGE' || Boolean(b.packageId); + }).length; + + 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'; + if (routeOrigin && origin !== routeOrigin) return false; + if (routeDestination && destination !== routeDestination) return false; + return true; + }); + + const routeOriginOptions = [ + ...new Set(confirmedBookings.map((b: any) => b.schedule?.originStation?.name || b.originStationName || b.origin || 'Unknown').filter(Boolean)), + ].sort() as string[]; + const routeDestinationOptions = [ + ...new Set(confirmedBookings.map((b: any) => b.schedule?.destinationStation?.name || b.destinationStationName || b.destination || 'Unknown').filter(Boolean)), + ].sort() as string[]; + + const routeRevenueData = Object.entries( + filteredRouteBookings.reduce((acc: Record, b: any) => { + const origin = b.schedule?.originStation?.name || b.originStationName || b.origin || 'Unknown'; + const destination = b.schedule?.destinationStation?.name || b.destinationStationName || b.destination || 'Unknown'; + const route = `${origin} → ${destination}`; + const rate = toEtbRate(b.currency); + const etbMinor = rate !== null ? Math.round((b.totalMinor || 0) * rate) : 0; + if (!acc[route]) acc[route] = { totalEtbMinor: 0, bookings: 0 }; + acc[route].totalEtbMinor += etbMinor; + acc[route].bookings += 1; + return acc; + }, {}), + ).map(([route, value]) => ({ + route, + totalEtbMinor: value.totalEtbMinor, + bookings: value.bookings, + })).sort((a, b) => b.totalEtbMinor - a.totalEtbMinor); const REPORT_COLS = ['Date', 'Revenue (ETB)', 'Confirmed Bookings']; @@ -161,6 +220,20 @@ export default function ReportsPage() { setExportModalOpen(false); }; + const doExportRouteRevenue = () => { + if (!routeRevenueData.length) { alert('No route revenue to export'); return; } + const rows = routeRevenueData.map((row) => [row.route, String(row.bookings), formatCurrency(row.totalEtbMinor, 'ETB')]); + const headers = ['Route', 'Bookings', 'Revenue (ETB)']; + const csv = [headers.map((h) => `"${h}"`).join(','), ...rows.map((r) => r.map((v) => `"${v}"`).join(','))].join('\n'); + const blob = new Blob([csv], { type: 'text/csv' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `route-revenue-${dates.startDate}-${dates.endDate}.csv`; + a.click(); + URL.revokeObjectURL(url); + }; + const renderCurrencyRow = ({ currency, totalMinor }: { currency: string; totalMinor: number }) => { const rate = toEtbRate(currency); const etbMinor = rate !== null ? Math.round(totalMinor * rate) : null; @@ -231,7 +304,7 @@ export default function ReportsPage() {

- {statsLoading ? '—' : formatCurrency(overallGrand, 'ETB')} + {isLoading ? '—' : formatCurrency(totalRevenueMinor, 'ETB')}

@@ -254,16 +327,16 @@ export default function ReportsPage() {

- {statsLoading ? '—' : (stats?.totalBookings ?? 0).toLocaleString()} + {isLoading ? '—' : totalBookingsCount.toLocaleString()}

Regular - {statsLoading ? '—' : (stats?.totalNormalBookings ?? 0).toLocaleString()} + {isLoading ? '—' : totalRegularBookingsCount.toLocaleString()}
Package - {statsLoading ? '—' : (stats?.totalPackageBookings ?? 0).toLocaleString()} + {isLoading ? '—' : totalPackageBookingsCount.toLocaleString()}
@@ -277,16 +350,16 @@ export default function ReportsPage() {

- {statsLoading ? '—' : (stats?.totalTickets ?? 0).toLocaleString()} + {isLoading ? '—' : totalTicketsCount.toLocaleString()}

Regular - {statsLoading ? '—' : (stats?.totalNormalTickets ?? 0).toLocaleString()} + {isLoading ? '—' : totalRegularBookingsCount.toLocaleString()}
Package - {statsLoading ? '—' : (stats?.totalPackageTickets ?? 0).toLocaleString()} + {isLoading ? '—' : totalPackageBookingsCount.toLocaleString()}
@@ -300,7 +373,7 @@ export default function ReportsPage() {

- {isLoading ? '—' : formatCurrency(avgDailyRevenue * 100, 'ETB')} + {isLoading ? '—' : formatCurrency(avgDailyRevenueMinor, 'ETB')}

Over {chartData.length} active day{chartData.length !== 1 ? 's' : ''} in range @@ -368,6 +441,85 @@ export default function ReportsPage() { )} + {/* Route Revenue Breakdown */} +

+
+
+

+ Revenue by Route +

+

+ Confirmed booking revenue for the selected date range, grouped by route. +

+
+
+ {routeRevenueData.length} route{routeRevenueData.length !== 1 ? 's' : ''} + +
+
+ +
+
+ + +
+
+ + +
+
+ + Export route revenue + +
+
+ + {routeRevenueData.length === 0 ? ( +

No route revenue data available for this range.

+ ) : ( +
+ {routeRevenueData.slice(0, 10).map((route) => ( +
+
{route.route}
+
{route.bookings.toLocaleString()} booking{route.bookings !== 1 ? 's' : ''}
+
+ {formatCurrency(route.totalEtbMinor, 'ETB')} +
+
+ ))} +
+ )} +
+ {/* Charts */}
{/* Revenue Trend */} @@ -489,8 +641,8 @@ export default function ReportsPage() { { 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: stats?.totalNormalBookings ?? 0, fromStats: true }, - { label: 'Package Bookings', value: stats?.totalPackageBookings ?? 0, fromStats: true }, + { label: 'Regular Bookings', value: totalRegularBookingsCount, fromStats: false }, + { label: 'Package Bookings', value: totalPackageBookingsCount, fromStats: false }, ].map(({ label, value, fromStats }) => (

{label}