From dcba12993f17ae06d8ef37b1ca992b265526f613 Mon Sep 17 00:00:00 2001 From: Stephanos A Date: Tue, 2 Jun 2026 19:14:34 +0300 Subject: [PATCH] Build related issues resolution --- .../modules/bookings/guest-booking.service.ts | 13 +- .../backoffice/src/app/audit/page.tsx | 2 +- .../backoffice/src/app/coaches/page.tsx | 2 +- .../backoffice/src/app/dashboard/page.tsx | 4 +- .../backoffice/src/app/passengers/page.tsx | 2 +- .../backoffice/src/app/routes/page.tsx | 24 +- .../backoffice/src/app/seat-classes/page.tsx | 2 +- .../backoffice/src/lib/api/index.ts | 177 ++----- .../backoffice/src/types/edr.ts | 443 +++++++++++++++++- apps/edr-passenger-web/portal/README.md | 42 +- apps/edr-passenger-web/portal/index.html | 12 - apps/edr-passenger-web/portal/next.config.js | 4 + apps/edr-passenger-web/portal/src/App.tsx | 53 --- .../src/app/booking/confirmation/page.tsx | 7 +- .../portal/src/app/booking/layout.tsx | 2 + .../src/app/booking/passengers/page.tsx | 6 +- .../portal/src/app/booking/results/page.tsx | 14 +- .../portal/src/app/booking/review/page.tsx | 6 +- .../portal/src/app/booking/search/page.tsx | 18 +- .../portal/src/app/booking/seats/page.tsx | 23 +- .../portal/src/app/guide/page.tsx | 2 +- .../portal/src/app/login/page.tsx | 2 + .../portal/src/app/profile/page.tsx | 16 +- .../portal/src/components/AppHeader.tsx | 4 +- .../src/components/DualCalendarPicker.tsx | 2 +- .../components/schedules/ScheduleTable.tsx | 16 +- .../portal/src/lib/ethiopian-calendar.ts | 4 +- apps/edr-passenger-web/portal/src/main.tsx | 18 - .../src/pages/dashboard/DashboardPage.tsx | 13 - .../src/pages/passengers/PassengersPage.tsx | 10 - .../pages/schedules/ScheduleDetailPage.tsx | 36 -- .../src/pages/schedules/SchedulesPage.tsx | 20 - .../src/pages/stations/StationsPage.tsx | 32 -- .../src/pages/tickets/BookTicketPage.tsx | 32 -- .../src/pages/tickets/TicketDetailPage.tsx | 38 -- .../portal/src/pages/tickets/TicketsPage.tsx | 28 -- .../edr-passenger-web/portal/src/utils/api.ts | 2 +- .../portal/src/vite-env.d.ts | 9 - .../portal/tsconfig.app.json | 9 - .../portal/tsconfig.node.json | 14 - apps/edr-passenger-web/portal/vite.config.ts | 14 - pnpm-lock.yaml | 5 +- 42 files changed, 632 insertions(+), 550 deletions(-) delete mode 100644 apps/edr-passenger-web/portal/index.html delete mode 100644 apps/edr-passenger-web/portal/src/App.tsx delete mode 100644 apps/edr-passenger-web/portal/src/main.tsx delete mode 100644 apps/edr-passenger-web/portal/src/pages/dashboard/DashboardPage.tsx delete mode 100644 apps/edr-passenger-web/portal/src/pages/passengers/PassengersPage.tsx delete mode 100644 apps/edr-passenger-web/portal/src/pages/schedules/ScheduleDetailPage.tsx delete mode 100644 apps/edr-passenger-web/portal/src/pages/schedules/SchedulesPage.tsx delete mode 100644 apps/edr-passenger-web/portal/src/pages/stations/StationsPage.tsx delete mode 100644 apps/edr-passenger-web/portal/src/pages/tickets/BookTicketPage.tsx delete mode 100644 apps/edr-passenger-web/portal/src/pages/tickets/TicketDetailPage.tsx delete mode 100644 apps/edr-passenger-web/portal/src/pages/tickets/TicketsPage.tsx delete mode 100644 apps/edr-passenger-web/portal/src/vite-env.d.ts delete mode 100644 apps/edr-passenger-web/portal/tsconfig.app.json delete mode 100644 apps/edr-passenger-web/portal/tsconfig.node.json delete mode 100644 apps/edr-passenger-web/portal/vite.config.ts diff --git a/apps/edr-passenger-api/src/modules/bookings/guest-booking.service.ts b/apps/edr-passenger-api/src/modules/bookings/guest-booking.service.ts index 1214acebc..3af67730a 100644 --- a/apps/edr-passenger-api/src/modules/bookings/guest-booking.service.ts +++ b/apps/edr-passenger-api/src/modules/bookings/guest-booking.service.ts @@ -201,11 +201,22 @@ export class GuestBookingService { } } + // Generate unique phone number + let guestPhone = firstPassenger.phone || `+251${uniqueId.replace(/[^0-9]/g, '').slice(0, 9)}`; + + // Check if phone exists and generate unique one if it does + if (firstPassenger.phone) { + const existingUserByPhone = await this.prisma.user.findUnique({ where: { phone: firstPassenger.phone } }); + if (existingUserByPhone) { + guestPhone = `+251${uniqueId.replace(/[^0-9]/g, '').slice(0, 9)}`; + } + } + const tempUser = await this.prisma.user.create({ data: { fullName: firstPassenger.passengerName, email: guestEmail, - phone: firstPassenger.phone || `+251${uniqueId.replace(/[^0-9]/g, '').slice(0, 9)}`, + phone: guestPhone, passwordHash: await bcrypt.hash(Math.random().toString(36), 10), role: 'PASSENGER', }, diff --git a/apps/edr-passenger-web/backoffice/src/app/audit/page.tsx b/apps/edr-passenger-web/backoffice/src/app/audit/page.tsx index 95fc248e9..01c5aa8d1 100644 --- a/apps/edr-passenger-web/backoffice/src/app/audit/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/audit/page.tsx @@ -58,7 +58,7 @@ export default function AuditLogsPage() { const actions = [ { label: 'View Details', - onClick: (log: any) => window.location.href = `/audit/${log.id}`, + onClick: (log: any) => { window.location.href = `/audit/${log.id}`; }, variant: 'secondary' as const, icon: Eye, }, diff --git a/apps/edr-passenger-web/backoffice/src/app/coaches/page.tsx b/apps/edr-passenger-web/backoffice/src/app/coaches/page.tsx index 371fa0c96..8ae8dd642 100644 --- a/apps/edr-passenger-web/backoffice/src/app/coaches/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/coaches/page.tsx @@ -193,7 +193,7 @@ export default function CoachesPage() { columns={columns} data={coaches} actions={actions} - isLoading={isLoading} + loading={isLoading} /> diff --git a/apps/edr-passenger-web/backoffice/src/app/dashboard/page.tsx b/apps/edr-passenger-web/backoffice/src/app/dashboard/page.tsx index 6a91b5e12..581704933 100644 --- a/apps/edr-passenger-web/backoffice/src/app/dashboard/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/dashboard/page.tsx @@ -27,7 +27,7 @@ export default function DashboardPage() { const recentBookings = Array.isArray(recentBookingsData) ? recentBookingsData - : recentBookingsData?.items || recentBookingsData?.data || []; + : (recentBookingsData as any)?.items || (recentBookingsData as any)?.data || []; const columns = [ { key: 'reference', label: 'Reference', render: (item: any) => item.bookingRef || item.reference }, @@ -49,7 +49,7 @@ export default function DashboardPage() {

Dashboard

-

Welcome back! Here's what's happening today.

+

Welcome back! Here's what's happening today.

diff --git a/apps/edr-passenger-web/backoffice/src/app/passengers/page.tsx b/apps/edr-passenger-web/backoffice/src/app/passengers/page.tsx index ba7bfe33b..e80de0fe6 100644 --- a/apps/edr-passenger-web/backoffice/src/app/passengers/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/passengers/page.tsx @@ -65,7 +65,7 @@ export default function PassengersPage() { }, ]; - const actions = [ + const actions: any[] = [ // TODO: Create passenger detail page // { // label: 'View Details', diff --git a/apps/edr-passenger-web/backoffice/src/app/routes/page.tsx b/apps/edr-passenger-web/backoffice/src/app/routes/page.tsx index 1c67df1b2..4cc2f867c 100644 --- a/apps/edr-passenger-web/backoffice/src/app/routes/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/routes/page.tsx @@ -136,15 +136,17 @@ export default function RoutesPage() { const generateRouteCode = (originId: string, destId: string) => { if (!originId || !destId) return ''; - const origin = stations?.items?.find((s: any) => s.id === originId); - const dest = stations?.items?.find((s: any) => s.id === destId); + const stationsList = Array.isArray(stations) ? stations : (stations as any)?.items || []; + const origin = stationsList.find((s: any) => s.id === originId); + const dest = stationsList.find((s: any) => s.id === destId); return origin && dest ? `${origin.code}-${dest.code}` : ''; }; const generateRouteName = (originId: string, destId: string) => { if (!originId || !destId) return ''; - const origin = stations?.items?.find((s: any) => s.id === originId); - const dest = stations?.items?.find((s: any) => s.id === destId); + const stationsList = Array.isArray(stations) ? stations : (stations as any)?.items || []; + const origin = stationsList.find((s: any) => s.id === originId); + const dest = stationsList.find((s: any) => s.id === destId); return origin && dest ? `${origin.name} - ${dest.name}` : ''; }; @@ -272,7 +274,7 @@ export default function RoutesPage() { disabled={!!editingRoute} > - {stations?.items?.map((station: any) => ( + {(Array.isArray(stations) ? stations : (stations as any)?.items || []).map((station: any) => ( @@ -289,7 +291,7 @@ export default function RoutesPage() { disabled={!!editingRoute} > - {stations?.items?.map((station: any) => ( + {(Array.isArray(stations) ? stations : (stations as any)?.items || []).map((station: any) => ( @@ -373,8 +375,8 @@ export default function RoutesPage() {
{originStationId ? ( - {stations?.items?.find((s: any) => s.id === originStationId)?.name || 'Unknown'} - {' '}({stations?.items?.find((s: any) => s.id === originStationId)?.code || 'N/A'}) + {(Array.isArray(stations) ? stations : (stations as any)?.items || []).find((s: any) => s.id === originStationId)?.name || 'Unknown'} + {' '}({(Array.isArray(stations) ? stations : (stations as any)?.items || []).find((s: any) => s.id === originStationId)?.code || 'N/A'}) ) : ( Select origin station above @@ -399,7 +401,7 @@ export default function RoutesPage() { required > - {stations?.items?.filter((s: any) => + {(Array.isArray(stations) ? stations : (stations as any)?.items || []).filter((s: any) => s.id !== originStationId && s.id !== destinationStationId && !stops.some((st, idx) => idx !== index && st.stationId === s.id) @@ -455,8 +457,8 @@ export default function RoutesPage() {
{destinationStationId ? ( - {stations?.items?.find((s: any) => s.id === destinationStationId)?.name || 'Unknown'} - {' '}({stations?.items?.find((s: any) => s.id === destinationStationId)?.code || 'N/A'}) + {(Array.isArray(stations) ? stations : (stations as any)?.items || []).find((s: any) => s.id === destinationStationId)?.name || 'Unknown'} + {' '}({(Array.isArray(stations) ? stations : (stations as any)?.items || []).find((s: any) => s.id === destinationStationId)?.code || 'N/A'}) ) : ( Select destination station above diff --git a/apps/edr-passenger-web/backoffice/src/app/seat-classes/page.tsx b/apps/edr-passenger-web/backoffice/src/app/seat-classes/page.tsx index d4a06f1f9..9c09138b5 100644 --- a/apps/edr-passenger-web/backoffice/src/app/seat-classes/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/seat-classes/page.tsx @@ -125,7 +125,7 @@ export default function SeatClassesPage() {
{ + if (!params) return ''; + const cleanParams = Object.fromEntries( + Object.entries(params).filter(([_, value]) => value !== '' && value !== undefined && value !== null) + ); + return Object.keys(cleanParams).length ? `?${new URLSearchParams(cleanParams as Record).toString()}` : ''; +}; + // Bookings API export const bookingsApi = { getAll: async (params?: any) => { - const cleanParams = Object.fromEntries( - Object.entries(params || {}).filter(([_, value]) => value !== '' && value !== undefined && value !== null) - ); - const query = new URLSearchParams(cleanParams).toString(); - const response = await apiClient.get(`/bookings${query ? `?${query}` : ''}`); - if (response?.data) { - return Array.isArray(response.data) ? { items: response.data } : response; - } - return Array.isArray(response) ? { items: response } : response; + const response = await apiClient.get(`/bookings${buildQuery(params)}`); + return response?.data ? (Array.isArray(response.data) ? { items: response.data } : response) : (Array.isArray(response) ? { items: response } : response); }, getById: (id: string) => apiClient.get(`/bookings/${id}`), cancel: (id: string, data?: any) => apiClient.post(`/bookings/${id}/cancel`, data), @@ -22,32 +23,18 @@ export const bookingsApi = { // Passengers API export const passengersApi = { getAll: async (params?: any) => { - const cleanParams = Object.fromEntries( - Object.entries(params || {}).filter(([_, value]) => value !== '' && value !== undefined && value !== null) - ); - const query = new URLSearchParams(cleanParams).toString(); - const response = await apiClient.get(`/passengers${query ? `?${query}` : ''}`); - if (response?.data) { - return Array.isArray(response.data) ? { items: response.data } : response; - } - return Array.isArray(response) ? { items: response } : response; + const response = await apiClient.get(`/passengers${buildQuery(params)}`); + return response?.data ? (Array.isArray(response.data) ? { items: response.data } : response) : (Array.isArray(response) ? { items: response } : response); }, getById: (id: string) => apiClient.get(`/passengers/${id}`), verify: (nationalId: string) => apiClient.post('/passengers/verify-fayda', { nationalId }), }; + // Stations API export const stationsApi = { getAll: async (params?: any) => { - const cleanParams = Object.fromEntries( - Object.entries(params || {}).filter(([_, value]) => value !== '' && value !== undefined && value !== null) - ); - const query = new URLSearchParams(cleanParams).toString(); - const response = await apiClient.get(`/stations${query ? `?${query}` : ''}`); - // Handle wrapped response: { success, data: [...], timestamp } - if (response?.data) { - return Array.isArray(response.data) ? { items: response.data } : response; - } - return Array.isArray(response) ? { items: response } : response; + const response = await apiClient.get(`/stations${buildQuery(params)}`); + return response?.data ? (Array.isArray(response.data) ? { items: response.data } : response) : (Array.isArray(response) ? { items: response } : response); }, getById: (id: string) => apiClient.get(`/stations/${id}`), create: (data: any) => apiClient.post('/stations', data), @@ -58,26 +45,12 @@ export const stationsApi = { // Fleet API export const fleetApi = { getTrains: async (params?: any) => { - const cleanParams = Object.fromEntries( - Object.entries(params || {}).filter(([_, value]) => value !== '' && value !== undefined && value !== null) - ); - const query = new URLSearchParams(cleanParams).toString(); - const response = await apiClient.get(`/fleet/trains${query ? `?${query}` : ''}`); - if (response?.data) { - return Array.isArray(response.data) ? { items: response.data } : response; - } - return Array.isArray(response) ? { items: response } : response; + const response = await apiClient.get(`/fleet/trains${buildQuery(params)}`); + return response?.data ? (Array.isArray(response.data) ? { items: response.data } : response) : (Array.isArray(response) ? { items: response } : response); }, getCoaches: async (params?: any) => { - const cleanParams = Object.fromEntries( - Object.entries(params || {}).filter(([_, value]) => value !== '' && value !== undefined && value !== null) - ); - const query = new URLSearchParams(cleanParams).toString(); - const response = await apiClient.get(`/fleet/coaches${query ? `?${query}` : ''}`); - if (response?.data) { - return Array.isArray(response.data) ? { items: response.data } : response; - } - return Array.isArray(response) ? { items: response } : response; + const response = await apiClient.get(`/fleet/coaches${buildQuery(params)}`); + return response?.data ? (Array.isArray(response.data) ? { items: response.data } : response) : (Array.isArray(response) ? { items: response } : response); }, createTrain: (data: any) => apiClient.post('/fleet/trains', data), updateTrain: (id: string, data: any) => apiClient.patch(`/fleet/trains/${id}`, data), @@ -90,12 +63,8 @@ export const fleetApi = { // Schedules API export const schedulesApi = { getAll: async (params?: any) => { - const query = new URLSearchParams(params).toString(); - const response = await apiClient.get(`/schedules${query ? `?${query}` : ''}`); - if (response?.data) { - return Array.isArray(response.data) ? { items: response.data } : response; - } - return Array.isArray(response) ? { items: response } : response; + const response = await apiClient.get(`/schedules${buildQuery(params)}`); + return response?.data ? (Array.isArray(response.data) ? { items: response.data } : response) : (Array.isArray(response) ? { items: response } : response); }, getById: (id: string) => apiClient.get(`/schedules/${id}`), create: (data: any) => apiClient.post('/schedules', data), @@ -125,12 +94,8 @@ export const seatsApi = { // Payments API export const paymentsApi = { getAll: async (params?: any) => { - const query = new URLSearchParams(params).toString(); - const response = await apiClient.get(`/payments${query ? `?${query}` : ''}`); - if (response?.data) { - return Array.isArray(response.data) ? { items: response.data } : response; - } - return Array.isArray(response) ? { items: response } : response; + const response = await apiClient.get(`/payments${buildQuery(params)}`); + return response?.data ? (Array.isArray(response.data) ? { items: response.data } : response) : (Array.isArray(response) ? { items: response } : response); }, getById: (id: string) => apiClient.get(`/payments/${id}`), refund: (id: string, data: any) => apiClient.post(`/payments/${id}/refund`, data), @@ -140,12 +105,8 @@ export const paymentsApi = { // Tickets API export const ticketsApi = { getAll: async (params?: any) => { - const query = new URLSearchParams(params).toString(); - const response = await apiClient.get(`/tickets${query ? `?${query}` : ''}`); - if (response?.data) { - return Array.isArray(response.data) ? { items: response.data } : response; - } - return Array.isArray(response) ? { items: response } : response; + const response = await apiClient.get(`/tickets${buildQuery(params)}`); + return response?.data ? (Array.isArray(response.data) ? { items: response.data } : response) : (Array.isArray(response) ? { items: response } : response); }, getById: (id: string) => apiClient.get(`/tickets/${id}`), validate: (ticketId: string, data: any) => apiClient.post(`/tickets/${ticketId}/validate`, data), @@ -155,12 +116,8 @@ export const ticketsApi = { // Agents API export const agentsApi = { getAll: async (params?: any) => { - const query = new URLSearchParams(params).toString(); - const response = await apiClient.get(`/agents${query ? `?${query}` : ''}`); - if (response?.data) { - return Array.isArray(response.data) ? { items: response.data } : response; - } - return Array.isArray(response) ? { items: response } : response; + const response = await apiClient.get(`/agents${buildQuery(params)}`); + return response?.data ? (Array.isArray(response.data) ? { items: response.data } : response) : (Array.isArray(response) ? { items: response } : response); }, getById: (id: string) => apiClient.get(`/agents/${id}`), create: (data: any) => apiClient.post('/agents', data), @@ -174,12 +131,8 @@ export const agentsApi = { // Loyalty API export const loyaltyApi = { getAccounts: async (params?: any) => { - const query = new URLSearchParams(params).toString(); - const response = await apiClient.get(`/loyalty/accounts${query ? `?${query}` : ''}`); - if (response?.data) { - return Array.isArray(response.data) ? { items: response.data } : response; - } - return Array.isArray(response) ? { items: response } : response; + const response = await apiClient.get(`/loyalty/accounts${buildQuery(params)}`); + return response?.data ? (Array.isArray(response.data) ? { items: response.data } : response) : (Array.isArray(response) ? { items: response } : response); }, getAccount: (passengerId: string) => apiClient.get(`/loyalty/accounts/${passengerId}`), adjustPoints: (accountId: string, data: any) => apiClient.post(`/loyalty/accounts/${accountId}/adjust`, data), @@ -190,12 +143,8 @@ export const loyaltyApi = { // Wallet API export const walletApi = { getAccounts: async (params?: any) => { - const query = new URLSearchParams(params).toString(); - const response = await apiClient.get(`/wallet/accounts${query ? `?${query}` : ''}`); - if (response?.data) { - return Array.isArray(response.data) ? { items: response.data } : response; - } - return Array.isArray(response) ? { items: response } : response; + const response = await apiClient.get(`/wallet/accounts${buildQuery(params)}`); + return response?.data ? (Array.isArray(response.data) ? { items: response.data } : response) : (Array.isArray(response) ? { items: response } : response); }, getAccount: (passengerId: string) => apiClient.get(`/wallet/accounts/${passengerId}`), adjustBalance: (accountId: string, data: any) => apiClient.post(`/wallet/accounts/${accountId}/adjust`, data), @@ -205,12 +154,8 @@ export const walletApi = { // Promotions API export const promotionsApi = { getAll: async (params?: any) => { - const query = new URLSearchParams(params).toString(); - const response = await apiClient.get(`/promos${query ? `?${query}` : ''}`); - if (response?.data) { - return Array.isArray(response.data) ? { items: response.data } : response; - } - return Array.isArray(response) ? { items: response } : response; + const response = await apiClient.get(`/promos${buildQuery(params)}`); + return response?.data ? (Array.isArray(response.data) ? { items: response.data } : response) : (Array.isArray(response) ? { items: response } : response); }, getById: (id: string) => apiClient.get(`/promos/${id}`), create: (data: any) => apiClient.post('/promos', data), @@ -221,12 +166,8 @@ export const promotionsApi = { // Support API export const supportApi = { getConversations: async (params?: any) => { - const query = new URLSearchParams(params).toString(); - const response = await apiClient.get(`/support/conversations${query ? `?${query}` : ''}`); - if (response?.data) { - return Array.isArray(response.data) ? { items: response.data } : response; - } - return Array.isArray(response) ? { items: response } : response; + const response = await apiClient.get(`/support/conversations${buildQuery(params)}`); + return response?.data ? (Array.isArray(response.data) ? { items: response.data } : response) : (Array.isArray(response) ? { items: response } : response); }, getConversation: (id: string) => apiClient.get(`/support/conversations/${id}`), updateStatus: (id: string, status: string) => apiClient.patch(`/support/conversations/${id}/status`, { status }), @@ -242,24 +183,16 @@ export const notificationsApi = { updateTemplate: (id: string, data: any) => apiClient.patch(`/notifications/templates/${id}`, data), send: (data: any) => apiClient.post('/notifications/send', data), getHistory: async (params?: any) => { - const query = new URLSearchParams(params).toString(); - const response = await apiClient.get(`/notifications/history${query ? `?${query}` : ''}`); - if (response?.data) { - return Array.isArray(response.data) ? { items: response.data } : response; - } - return Array.isArray(response) ? { items: response } : response; + const response = await apiClient.get(`/notifications/history${buildQuery(params)}`); + return response?.data ? (Array.isArray(response.data) ? { items: response.data } : response) : (Array.isArray(response) ? { items: response } : response); }, }; // Fraud API export const fraudApi = { getAlerts: async (params?: any) => { - const query = new URLSearchParams(params).toString(); - const response = await apiClient.get(`/fraud/alerts${query ? `?${query}` : ''}`); - if (response?.data) { - return Array.isArray(response.data) ? { items: response.data } : response; - } - return Array.isArray(response) ? { items: response } : response; + const response = await apiClient.get(`/fraud/alerts${buildQuery(params)}`); + return response?.data ? (Array.isArray(response.data) ? { items: response.data } : response) : (Array.isArray(response) ? { items: response } : response); }, acknowledgeAlert: (id: string) => apiClient.patch(`/fraud/alerts/${id}/acknowledge`), getRules: () => apiClient.get('/fraud/rules'), @@ -270,12 +203,8 @@ export const fraudApi = { // Verifayda API export const verifaydaApi = { getVerifications: async (params?: any) => { - const query = new URLSearchParams(params).toString(); - const response = await apiClient.get(`/passengers/verifications${query ? `?${query}` : ''}`); - if (response?.data) { - return Array.isArray(response.data) ? { items: response.data } : response; - } - return Array.isArray(response) ? { items: response } : response; + const response = await apiClient.get(`/passengers/verifications${buildQuery(params)}`); + return response?.data ? (Array.isArray(response.data) ? { items: response.data } : response) : (Array.isArray(response) ? { items: response } : response); }, verify: (nationalId: string) => apiClient.post('/passengers/verify-fayda', { nationalId }), getStats: () => apiClient.get('/passengers/verification-stats'), @@ -284,12 +213,8 @@ export const verifaydaApi = { // Audit API export const auditApi = { getLogs: async (params?: any) => { - const query = new URLSearchParams(params).toString(); - const response = await apiClient.get(`/audit/logs${query ? `?${query}` : ''}`); - if (response?.data) { - return Array.isArray(response.data) ? { items: response.data } : response; - } - return Array.isArray(response) ? { items: response } : response; + const response = await apiClient.get(`/audit/logs${buildQuery(params)}`); + return response?.data ? (Array.isArray(response.data) ? { items: response.data } : response) : (Array.isArray(response) ? { items: response } : response); }, getLog: (id: string) => apiClient.get(`/audit/logs/${id}`), }; @@ -316,12 +241,8 @@ export const foodApi = { getCategories: () => apiClient.get('/food/categories'), getMenuItems: (scheduleId: string) => apiClient.get(`/food/menu/${scheduleId}`), getOrders: async (params?: any) => { - const query = new URLSearchParams(params).toString(); - const response = await apiClient.get(`/food/orders${query ? `?${query}` : ''}`); - if (response?.data) { - return Array.isArray(response.data) ? { items: response.data } : response; - } - return Array.isArray(response) ? { items: response } : response; + const response = await apiClient.get(`/food/orders${buildQuery(params)}`); + return response?.data ? (Array.isArray(response.data) ? { items: response.data } : response) : (Array.isArray(response) ? { items: response } : response); }, updateOrderStatus: (orderId: string, status: string) => apiClient.patch(`/food/orders/${orderId}/status`, { status }), createMenuItem: (data: any) => apiClient.post('/food/menu-items', data), @@ -330,12 +251,8 @@ export const foodApi = { // Reports API export const reportsApi = { getOperationalReports: async (params?: any) => { - const query = new URLSearchParams(params).toString(); - const response = await apiClient.get(`/reports/operational${query ? `?${query}` : ''}`); - if (response?.data) { - return Array.isArray(response.data) ? { items: response.data } : response; - } - return Array.isArray(response) ? { items: response } : response; + const response = await apiClient.get(`/reports/operational${buildQuery(params)}`); + return response?.data ? (Array.isArray(response.data) ? { items: response.data } : response) : (Array.isArray(response) ? { items: response } : response); }, getRevenue: (params?: any) => apiClient.get('/reports/revenue', { params }), getOccupancy: (params?: any) => apiClient.get('/reports/occupancy', { params }), diff --git a/apps/edr-passenger-web/backoffice/src/types/edr.ts b/apps/edr-passenger-web/backoffice/src/types/edr.ts index 91bdea160..e126090c2 100644 --- a/apps/edr-passenger-web/backoffice/src/types/edr.ts +++ b/apps/edr-passenger-web/backoffice/src/types/edr.ts @@ -1 +1,442 @@ -import { Passenger } from '@edr/types';\n\n// Enhanced Booking Types\nexport interface Booking {\n id: string;\n bookingRef: string;\n passengerId: string;\n passenger?: Passenger.IPassenger;\n scheduleId: string;\n status: 'DRAFT' | 'PENDING_PAYMENT' | 'CONFIRMED' | 'CANCELLED' | 'COMPLETED' | 'NO_SHOW' | 'REFUNDED';\n currency: string;\n totalMinor: number;\n adultCount: number;\n childCount: number;\n displayCurrency?: string;\n displayTotalMinor?: number;\n bookingType: string;\n contactEmail?: string;\n contactPhone?: string;\n source: string;\n promoCode?: string;\n paidAt?: string;\n createdAt: string;\n updatedAt: string;\n}\n\n// Station Types\nexport interface Station {\n id: string;\n code: string;\n name: string;\n city: string;\n countryCode?: string;\n isOperational: boolean;\n timezone: string;\n lat: number;\n lng: number;\n createdAt: string;\n}\n\n// Train Types\nexport interface Train {\n id: string;\n number: string;\n name: string;\n operatorId: string;\n operatorName?: string;\n description?: string;\n isActive: boolean;\n createdAt: string;\n updatedAt: string;\n}\n\n// Coach Types\nexport interface Coach {\n id: string;\n coachNumber: string;\n label: string;\n seatClassId: string;\n coachType?: string;\n mode: string;\n seatArrangement?: string;\n bedArrangement?: string;\n amenities?: any;\n totalUnits: number;\n isActive: boolean;\n createdAt: string;\n updatedAt: string;\n}\n\n// Schedule Types\nexport interface TrainSchedule {\n id: string;\n trainId: string;\n routeId?: string;\n originStationId: string;\n destinationStationId: string;\n departureAt: string;\n arrivalAt: string;\n durationMinutes: number;\n status: 'SCHEDULED' | 'BOARDING' | 'EN_ROUTE' | 'ARRIVED' | 'CANCELLED' | 'DELAYED';\n stopsCount: number;\n reservedCount: number;\n onTimePercent: number;\n carbonRating: string;\n notes?: string;\n train?: Train;\n originStation?: Station;\n destinationStation?: Station;\n}\n\n// Route Types\nexport interface Route {\n id: string;\n code: string;\n name: string;\n description?: string;\n effectiveFrom: string;\n effectiveUntil?: string;\n active: boolean;\n createdAt: string;\n stops?: RouteStop[];\n fareRules?: RouteFareRule[];\n}\n\nexport interface RouteStop {\n id: string;\n routeId: string;\n stationId: string;\n sequence: number;\n distanceKm?: number;\n createdAt: string;\n}\n\nexport interface RouteFareRule {\n id: string;\n routeId: string;\n seatClassId: string;\n passengerCategory: 'ADULT' | 'CHILD';\n baseFareMinor: number;\n discountPercent?: number;\n taxPercent?: number;\n surchargeMinor?: number;\n currency: string;\n validFrom: string;\n validUntil?: string;\n createdAt: string;\n}\n\n// Seat Types\nexport interface Seat {\n id: string;\n coachId: string;\n row: number;\n col: string;\n label: string;\n seatNumber?: string;\n kind: 'STANDARD' | 'PREMIUM' | 'ACCESSIBLE';\n status: 'AVAILABLE' | 'HELD' | 'BOOKED' | 'BLOCKED';\n heldUntil?: string;\n isWindow: boolean;\n isAisle: boolean;\n bedPosition?: string;\n premiumFeeMinor: number;\n eligibility?: string;\n}\n\nexport interface SeatClass {\n id: string;\n name: string;\n description?: string;\n basePrice: number;\n isActive: boolean;\n createdAt: string;\n updatedAt: string;\n}\n\n// Payment Types\nexport interface PaymentIntent {\n id: string;\n bookingId: string;\n amountMinor: number;\n currency: string;\n method: 'TELEBIRR' | 'CBE_BIRR' | 'EBIRR' | 'CARD' | 'WALLET' | 'WAAFI';\n provider?: string;\n status: 'REQUIRES_ACTION' | 'PROCESSING' | 'SUCCEEDED' | 'FAILED' | 'CANCELLED' | 'REFUNDED';\n providerRef?: string;\n merchantOrderId?: string;\n providerOrderId?: string;\n providerTxnId?: string;\n paidAt?: string;\n refundedAt?: string;\n failureCode?: string;\n failureMessage?: string;\n expiresAt?: string;\n createdAt: string;\n updatedAt: string;\n}\n\n// Agent Types\nexport interface Agent {\n id: string;\n userId: string;\n agentCode: string;\n stationId?: string;\n commissionRate: number;\n active: boolean;\n createdAt: string;\n user?: any;\n}\n\nexport interface AgentShift {\n id: string;\n agentId: string;\n openedAt: string;\n closedAt?: string;\n openingBalance: number;\n closingBalance?: number;\n reconciled: boolean;\n notes?: string;\n}\n\n// Loyalty Types\nexport interface LoyaltyAccount {\n id: string;\n passengerId: string;\n pointsBalance: number;\n lifetimePoints: number;\n tier: 'BRONZE' | 'SILVER' | 'GOLD' | 'PLATINUM';\n tierUpdatedAt?: string;\n updatedAt: string;\n}\n\nexport interface LoyaltyReward {\n id: string;\n accountId: string;\n title: string;\n costPoints: number;\n available: boolean;\n description?: string;\n}\n\n// Wallet Types\nexport interface WalletAccount {\n id: string;\n passengerId: string;\n balanceMinor: number;\n status: string;\n holdMinor: number;\n currency: string;\n updatedAt: string;\n}\n\n// Notification Types\nexport interface NotificationTemplate {\n id: string;\n code: string;\n channel: string;\n subject?: string;\n bodyTemplate: string;\n active: boolean;\n createdAt: string;\n}\n\n// Promotion Types\nexport interface Promotion {\n id: string;\n title: string;\n subtitle?: string;\n code: string;\n percentOff?: number;\n amountOffMinor?: number;\n validUntil: string;\n ctaLabel?: string;\n deepLink?: string;\n active: boolean;\n createdAt: string;\n}\n\n// Support Types\nexport interface SupportConversation {\n id: string;\n userId: string;\n assignedAgentId?: string;\n status: 'OPEN' | 'RESOLVED' | 'CLOSED';\n createdAt: string;\n messages?: SupportMessage[];\n}\n\nexport interface SupportMessage {\n id: string;\n conversationId: string;\n sender: 'USER' | 'BOT' | 'AGENT';\n text: string;\n attachments?: any;\n createdAt: string;\n}\n\n// Fraud Types\nexport interface FraudAlert {\n id: string;\n userId: string;\n eventType: string;\n triggeredRules: string[];\n context: any;\n severity: string;\n acknowledged: boolean;\n createdAt: string;\n}\n\nexport interface FraudRule {\n id: string;\n type: string;\n enabled: boolean;\n threshold: number;\n config?: any;\n createdAt: string;\n updatedAt: string;\n}\n\n// Audit Types\nexport interface AuditLog {\n id: string;\n userId?: string;\n action: string;\n entityType: string;\n entityId?: string;\n oldData?: any;\n newData?: any;\n ipAddress?: string;\n userAgent?: string;\n createdAt: string;\n}\n\n// Dashboard Types\nexport interface DashboardStats {\n totalBookings: number;\n totalRevenue: number;\n totalPassengers: number;\n activeTrips: number;\n bookingsToday: number;\n revenueToday: number;\n occupancyRate: number;\n averageTicketPrice: number;\n cancellationRate: number;\n onTimePerformance: number;\n}\n\nexport interface RevenueData {\n date: string;\n revenue: number;\n bookings: number;\n}\n\n// Filter Types\nexport interface BookingFilters {\n status?: string;\n dateFrom?: string;\n dateTo?: string;\n search?: string;\n source?: string;\n paymentStatus?: string;\n page?: number;\n pageSize?: number;\n}\n\nexport interface PassengerFilters {\n search?: string;\n verified?: boolean;\n nationality?: string;\n registrationDateFrom?: string;\n registrationDateTo?: string;\n page?: number;\n pageSize?: number;\n}\n\nexport interface PaymentFilters {\n status?: string;\n method?: string;\n dateFrom?: string;\n dateTo?: string;\n amountFrom?: number;\n amountTo?: number;\n page?: number;\n pageSize?: number;\n}\n\n// Live Tracking Types\nexport interface TripLiveStatus {\n id: string;\n scheduleId: string;\n state: string;\n currentLocationLabel?: string;\n progressPercent: number;\n delayMinutes: number;\n currentSpeedKph?: number;\n platformLabel?: string;\n updatedAt: string;\n}\n\nexport interface StationCrowdSignal {\n id: string;\n stationId: string;\n level: string;\n label: string;\n statusLabel: string;\n confidence?: number;\n observedAt?: string;\n updatedAt: string;\n}\n\n// Food & Dining Types\nexport interface MenuCategory {\n id: string;\n name: string;\n}\n\nexport interface MenuItem {\n id: string;\n scheduleId: string;\n categoryId: string;\n name: string;\n priceMinor: number;\n currency: string;\n available: boolean;\n availableUntil?: string;\n}\n\nexport interface FoodOrder {\n id: string;\n bookingId: string;\n status: 'PENDING' | 'PREPARING' | 'READY' | 'DELIVERED' | 'CANCELLED';\n totalMinor: number;\n currency: string;\n specialInstructions?: string;\n estimatedReadyAt?: string;\n createdAt: string;\n}\n\n// Verifayda Types\nexport interface VerifaydaVerification {\n id: string;\n bookingId?: string;\n nationalId: string;\n requestPayload: any;\n responsePayload?: any;\n verified: boolean;\n failureReason?: string;\n verifiedAt?: string;\n createdAt: string;\n}\n\n// Admin User Types\nexport interface AdminUser {\n id: string;\n email: string;\n fullName: string;\n role: 'ADMIN' | 'AGENT' | 'SUPERVISOR' | 'STAFF';\n active: boolean;\n lastLoginAt?: string;\n createdAt: string;\n}\n \ No newline at end of file +import { Passenger } from '@edr/types'; + +// Enhanced Booking Types +export interface Booking { + id: string; + bookingRef: string; + passengerId: string; + passenger?: Passenger.IPassenger; + scheduleId: string; + status: 'DRAFT' | 'PENDING_PAYMENT' | 'CONFIRMED' | 'CANCELLED' | 'COMPLETED' | 'NO_SHOW' | 'REFUNDED'; + currency: string; + totalMinor: number; + adultCount: number; + childCount: number; + displayCurrency?: string; + displayTotalMinor?: number; + bookingType: string; + contactEmail?: string; + contactPhone?: string; + source: string; + promoCode?: string; + paidAt?: string; + createdAt: string; + updatedAt: string; +} + +// Station Types +export interface Station { + id: string; + code: string; + name: string; + city: string; + countryCode?: string; + isOperational: boolean; + timezone: string; + lat: number; + lng: number; + createdAt: string; +} + +// Train Types +export interface Train { + id: string; + number: string; + name: string; + operatorId: string; + operatorName?: string; + description?: string; + isActive: boolean; + createdAt: string; + updatedAt: string; +} + +// Coach Types +export interface Coach { + id: string; + coachNumber: string; + label: string; + seatClassId: string; + coachType?: string; + mode: string; + seatArrangement?: string; + bedArrangement?: string; + amenities?: any; + totalUnits: number; + isActive: boolean; + createdAt: string; + updatedAt: string; +} + +// Schedule Types +export interface TrainSchedule { + id: string; + trainId: string; + routeId?: string; + originStationId: string; + destinationStationId: string; + departureAt: string; + arrivalAt: string; + durationMinutes: number; + status: 'SCHEDULED' | 'BOARDING' | 'EN_ROUTE' | 'ARRIVED' | 'CANCELLED' | 'DELAYED'; + stopsCount: number; + reservedCount: number; + onTimePercent: number; + carbonRating: string; + notes?: string; + train?: Train; + originStation?: Station; + destinationStation?: Station; +} + +// Route Types +export interface Route { + id: string; + code: string; + name: string; + description?: string; + effectiveFrom: string; + effectiveUntil?: string; + active: boolean; + createdAt: string; + stops?: RouteStop[]; + fareRules?: RouteFareRule[]; +} + +export interface RouteStop { + id: string; + routeId: string; + stationId: string; + sequence: number; + distanceKm?: number; + createdAt: string; +} + +export interface RouteFareRule { + id: string; + routeId: string; + seatClassId: string; + passengerCategory: 'ADULT' | 'CHILD'; + baseFareMinor: number; + discountPercent?: number; + taxPercent?: number; + surchargeMinor?: number; + currency: string; + validFrom: string; + validUntil?: string; + createdAt: string; +} + +// Seat Types +export interface Seat { + id: string; + coachId: string; + row: number; + col: string; + label: string; + seatNumber?: string; + kind: 'STANDARD' | 'PREMIUM' | 'ACCESSIBLE'; + status: 'AVAILABLE' | 'HELD' | 'BOOKED' | 'BLOCKED'; + heldUntil?: string; + isWindow: boolean; + isAisle: boolean; + bedPosition?: string; + premiumFeeMinor: number; + eligibility?: string; +} + +export interface SeatClass { + id: string; + name: string; + description?: string; + basePrice: number; + isActive: boolean; + createdAt: string; + updatedAt: string; +} + +// Payment Types +export interface PaymentIntent { + id: string; + bookingId: string; + amountMinor: number; + currency: string; + method: 'TELEBIRR' | 'CBE_BIRR' | 'EBIRR' | 'CARD' | 'WALLET' | 'WAAFI'; + provider?: string; + status: 'REQUIRES_ACTION' | 'PROCESSING' | 'SUCCEEDED' | 'FAILED' | 'CANCELLED' | 'REFUNDED'; + providerRef?: string; + merchantOrderId?: string; + providerOrderId?: string; + providerTxnId?: string; + paidAt?: string; + refundedAt?: string; + failureCode?: string; + failureMessage?: string; + expiresAt?: string; + createdAt: string; + updatedAt: string; +} + +// Agent Types +export interface Agent { + id: string; + userId: string; + agentCode: string; + stationId?: string; + commissionRate: number; + active: boolean; + createdAt: string; + user?: any; +} + +export interface AgentShift { + id: string; + agentId: string; + openedAt: string; + closedAt?: string; + openingBalance: number; + closingBalance?: number; + reconciled: boolean; + notes?: string; +} + +// Loyalty Types +export interface LoyaltyAccount { + id: string; + passengerId: string; + pointsBalance: number; + lifetimePoints: number; + tier: 'BRONZE' | 'SILVER' | 'GOLD' | 'PLATINUM'; + tierUpdatedAt?: string; + updatedAt: string; +} + +export interface LoyaltyReward { + id: string; + accountId: string; + title: string; + costPoints: number; + available: boolean; + description?: string; +} + +// Wallet Types +export interface WalletAccount { + id: string; + passengerId: string; + balanceMinor: number; + status: string; + holdMinor: number; + currency: string; + updatedAt: string; +} + +// Notification Types +export interface NotificationTemplate { + id: string; + code: string; + channel: string; + subject?: string; + bodyTemplate: string; + active: boolean; + createdAt: string; +} + +// Promotion Types +export interface Promotion { + id: string; + title: string; + subtitle?: string; + code: string; + percentOff?: number; + amountOffMinor?: number; + validUntil: string; + ctaLabel?: string; + deepLink?: string; + active: boolean; + createdAt: string; +} + +// Support Types +export interface SupportConversation { + id: string; + userId: string; + assignedAgentId?: string; + status: 'OPEN' | 'RESOLVED' | 'CLOSED'; + createdAt: string; + messages?: SupportMessage[]; +} + +export interface SupportMessage { + id: string; + conversationId: string; + sender: 'USER' | 'BOT' | 'AGENT'; + text: string; + attachments?: any; + createdAt: string; +} + +// Fraud Types +export interface FraudAlert { + id: string; + userId: string; + eventType: string; + triggeredRules: string[]; + context: any; + severity: string; + acknowledged: boolean; + createdAt: string; +} + +export interface FraudRule { + id: string; + type: string; + enabled: boolean; + threshold: number; + config?: any; + createdAt: string; + updatedAt: string; +} + +// Audit Types +export interface AuditLog { + id: string; + userId?: string; + action: string; + entityType: string; + entityId?: string; + oldData?: any; + newData?: any; + ipAddress?: string; + userAgent?: string; + createdAt: string; +} + +// Dashboard Types +export interface DashboardStats { + totalBookings: number; + totalRevenue: number; + totalPassengers: number; + activeTrips: number; + bookingsToday: number; + revenueToday: number; + occupancyRate: number; + averageTicketPrice: number; + cancellationRate: number; + onTimePerformance: number; +} + +export interface RevenueData { + date: string; + revenue: number; + bookings: number; +} + +// Filter Types +export interface BookingFilters { + status?: string; + dateFrom?: string; + dateTo?: string; + search?: string; + source?: string; + paymentStatus?: string; + page?: number; + pageSize?: number; +} + +export interface PassengerFilters { + search?: string; + verified?: boolean; + nationality?: string; + registrationDateFrom?: string; + registrationDateTo?: string; + page?: number; + pageSize?: number; +} + +export interface PaymentFilters { + status?: string; + method?: string; + dateFrom?: string; + dateTo?: string; + amountFrom?: number; + amountTo?: number; + page?: number; + pageSize?: number; +} + +// Live Tracking Types +export interface TripLiveStatus { + id: string; + scheduleId: string; + state: string; + currentLocationLabel?: string; + progressPercent: number; + delayMinutes: number; + currentSpeedKph?: number; + platformLabel?: string; + updatedAt: string; +} + +export interface StationCrowdSignal { + id: string; + stationId: string; + level: string; + label: string; + statusLabel: string; + confidence?: number; + observedAt?: string; + updatedAt: string; +} + +// Food & Dining Types +export interface MenuCategory { + id: string; + name: string; +} + +export interface MenuItem { + id: string; + scheduleId: string; + categoryId: string; + name: string; + priceMinor: number; + currency: string; + available: boolean; + availableUntil?: string; +} + +export interface FoodOrder { + id: string; + bookingId: string; + status: 'PENDING' | 'PREPARING' | 'READY' | 'DELIVERED' | 'CANCELLED'; + totalMinor: number; + currency: string; + specialInstructions?: string; + estimatedReadyAt?: string; + createdAt: string; +} + +// Verifayda Types +export interface VerifaydaVerification { + id: string; + bookingId?: string; + nationalId: string; + requestPayload: any; + responsePayload?: any; + verified: boolean; + failureReason?: string; + verifiedAt?: string; + createdAt: string; +} + +// Admin User Types +export interface AdminUser { + id: string; + email: string; + fullName: string; + role: 'ADMIN' | 'AGENT' | 'SUPERVISOR' | 'STAFF'; + active: boolean; + lastLoginAt?: string; + createdAt: string; +} diff --git a/apps/edr-passenger-web/portal/README.md b/apps/edr-passenger-web/portal/README.md index b1e79bbab..a88c6f46d 100644 --- a/apps/edr-passenger-web/portal/README.md +++ b/apps/edr-passenger-web/portal/README.md @@ -14,6 +14,13 @@ Modern Next.js 14 web application for the Ethio-Djibouti Railway passenger booki 7. **Payment** - Choose payment method and process payment 8. **Confirmation** - View PNR, tickets with QR codes +### Dashboard (Backoffice) +- **Dashboard** - Overview of passenger operations +- **Tickets** - Issue, view, and manage tickets +- **Schedules** - View train schedules and details +- **Stations** - Browse station directory +- **Passengers** - Passenger management + ### Key Capabilities - **Fayda 2.0 Integration** - Ethiopian national ID verification - **Age-Based Pricing** - First child travels free @@ -40,7 +47,7 @@ Modern Next.js 14 web application for the Ethio-Djibouti Railway passenger booki ### Prerequisites - Node.js >= 20.x - pnpm >= 9.x -- EDR Passenger API running on port 3002 +- EDR Passenger API running on port 4000 ### Installation @@ -52,7 +59,7 @@ pnpm install cp .env.example .env.local # Update .env.local with API URL -NEXT_PUBLIC_API_URL=http://localhost:3002 +NEXT_PUBLIC_API_URL=http://localhost:4000 ``` ### Development @@ -79,6 +86,13 @@ pnpm start ``` src/ ├── app/ # Next.js App Router pages +│ ├── (dashboard)/ # Dashboard route group +│ │ ├── dashboard/ # Overview page +│ │ ├── tickets/ # Ticket management +│ │ ├── schedules/ # Schedule views +│ │ ├── stations/ # Station directory +│ │ ├── passengers/ # Passenger management +│ │ └── layout.tsx # Dashboard layout wrapper │ ├── booking/ │ │ ├── search/ # Search trains │ │ ├── results/ # Search results @@ -89,19 +103,33 @@ src/ │ │ ├── payment/ # Payment processing │ │ └── confirmation/ # Booking confirmation │ ├── login/ # Login page +│ ├── profile/ # User profile +│ ├── guide/ # Travel guide │ ├── layout.tsx # Root layout -│ ├── page.tsx # Home (redirects to search) -│ ├── providers.tsx # React Query provider +│ ├── page.tsx # Home (redirects to dashboard) +│ ├── providers.tsx # React Query + Theme provider │ └── globals.css # Global styles ├── components/ # Reusable components +│ ├── schedules/ # Schedule components +│ ├── tickets/ # Ticket components +│ ├── seats/ # Seat selector +│ ├── AppHeader.tsx # Navigation header +│ └── ThemeProvider.tsx # Theme context ├── lib/ # Core utilities │ ├── api-client.ts # Axios client with interceptors │ ├── auth-store.ts # Auth state (Zustand) │ ├── booking-store.ts # Booking flow state (Zustand) │ └── payment-store.ts # Payment state (Zustand) -├── types/ # TypeScript types -│ └── index.ts -└── hooks/ # Custom React hooks +├── hooks/ # Custom React hooks +│ ├── useSchedules.ts # Schedule queries +│ ├── useStations.ts # Station queries +│ └── useTickets.ts # Ticket queries +├── services/ # API service layer +│ ├── schedules.service.ts +│ ├── stations.service.ts +│ └── tickets.service.ts +└── types/ # TypeScript types + └── index.ts ``` ## State Management diff --git a/apps/edr-passenger-web/portal/index.html b/apps/edr-passenger-web/portal/index.html deleted file mode 100644 index 38eb8605d..000000000 --- a/apps/edr-passenger-web/portal/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - EDR Passenger Portal - - -
- - - diff --git a/apps/edr-passenger-web/portal/next.config.js b/apps/edr-passenger-web/portal/next.config.js index 9d962888f..d0993180a 100644 --- a/apps/edr-passenger-web/portal/next.config.js +++ b/apps/edr-passenger-web/portal/next.config.js @@ -2,6 +2,10 @@ const nextConfig = { reactStrictMode: true, transpilePackages: ['@edr/types', '@edr/ui-common'], + output: 'standalone', + experimental: { + cpus: 1, + }, }; export default nextConfig; diff --git a/apps/edr-passenger-web/portal/src/App.tsx b/apps/edr-passenger-web/portal/src/App.tsx deleted file mode 100644 index 4e890f1c3..000000000 --- a/apps/edr-passenger-web/portal/src/App.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { - useNavigate, - useLocation, - Routes, - Route, - Navigate, -} from "react-router-dom"; -import { DashboardLayout, type SidebarItem } from "@edr/ui-common"; - -import TicketsPage from "./pages/tickets/TicketsPage"; -import TicketDetailPage from "./pages/tickets/TicketDetailPage"; -import BookTicketPage from "./pages/tickets/BookTicketPage"; -import SchedulesPage from "./pages/schedules/SchedulesPage"; -import ScheduleDetailPage from "./pages/schedules/ScheduleDetailPage"; -import StationsPage from "./pages/stations/StationsPage"; -import PassengersPage from "./pages/passengers/PassengersPage"; -import DashboardPage from "./pages/dashboard/DashboardPage"; - -const sidebarItems: SidebarItem[] = [ - { label: "Dashboard", href: "/" }, - { label: "Tickets", href: "/tickets" }, - { label: "Schedules", href: "/schedules" }, - { label: "Stations", href: "/stations" }, - { label: "Passengers", href: "/passengers" }, -]; - -const App = () => { - const navigate = useNavigate(); - const location = useLocation(); - - return ( - - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - - ); -}; - -export default App; diff --git a/apps/edr-passenger-web/portal/src/app/booking/confirmation/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/confirmation/page.tsx index 7ed28a907..12d6bd5a7 100644 --- a/apps/edr-passenger-web/portal/src/app/booking/confirmation/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/booking/confirmation/page.tsx @@ -9,6 +9,8 @@ import { CheckCircle, Download, Share2, Copy, Printer, Mail, Train } from 'lucid import { QRCodeSVG } from 'qrcode.react'; import { format } from 'date-fns'; +export const dynamic = 'force-dynamic'; + export default function ConfirmationPage() { const router = useRouter(); const { bookingId, pnr, selectedSchedule, passengers, clearBooking } = useBookingStore(); @@ -18,7 +20,7 @@ export default function ConfirmationPage() { mutationFn: () => apiClient.patch(`/bookings/${bookingId}/confirm`, { status: 'SUCCEEDED' }), }); - const { data: booking } = useQuery({ + useQuery({ queryKey: ['booking', bookingId], queryFn: async () => { try { @@ -41,7 +43,8 @@ export default function ConfirmationPage() { if (bookingId && !confirmMutation.isSuccess && !confirmMutation.isPending) { confirmMutation.mutate(); } - }, [bookingId]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [bookingId, confirmMutation]); const copyPNR = () => { if (pnr) { diff --git a/apps/edr-passenger-web/portal/src/app/booking/layout.tsx b/apps/edr-passenger-web/portal/src/app/booking/layout.tsx index 276a3af51..4438f9903 100644 --- a/apps/edr-passenger-web/portal/src/app/booking/layout.tsx +++ b/apps/edr-passenger-web/portal/src/app/booking/layout.tsx @@ -3,6 +3,8 @@ import { usePathname } from 'next/navigation'; import { ProgressIndicator } from '@/components/ProgressIndicator'; +export const dynamic = 'force-dynamic'; + export default function BookingLayout({ children, }: { diff --git a/apps/edr-passenger-web/portal/src/app/booking/passengers/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/passengers/page.tsx index 7ed3e1a83..078d3cc73 100644 --- a/apps/edr-passenger-web/portal/src/app/booking/passengers/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/booking/passengers/page.tsx @@ -10,6 +10,8 @@ import { apiClient } from '@/lib/api-client'; import { useState, useEffect } from 'react'; import { CheckCircle, ExternalLink, Loader2 } from 'lucide-react'; +export const dynamic = 'force-dynamic'; + const passengerSchema = z.object({ name: z.string().min(2, 'Name is required'), dateOfBirth: z.string().min(1, 'Date of birth is required'), @@ -48,7 +50,7 @@ type FormData = z.infer; export default function PassengersPage() { const router = useRouter(); const { searchCriteria, setPassengers, setCreateAccount } = useBookingStore(); - const { user, isAuthenticated, logout, updateUser } = useAuthStore(); + const { user, isAuthenticated, updateUser } = useAuthStore(); const [faydaEnabled, setFaydaEnabled] = useState(true); const [verificationStatus, setVerificationStatus] = useState>({}); const [updatingUser, setUpdatingUser] = useState(false); @@ -59,7 +61,7 @@ export default function PassengersPage() { const { register, control, handleSubmit, setValue, watch, formState: { errors } } = useForm({ resolver: zodResolver(formSchema), defaultValues: { - passengers: Array.from({ length: totalPassengers }, (_, i) => ({ + passengers: Array.from({ length: totalPassengers }, () => ({ name: '', dateOfBirth: '', gender: undefined, diff --git a/apps/edr-passenger-web/portal/src/app/booking/results/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/results/page.tsx index 200e378e6..536fe435b 100644 --- a/apps/edr-passenger-web/portal/src/app/booking/results/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/booking/results/page.tsx @@ -9,6 +9,8 @@ import { ArrowRight, Clock, Calendar, Users, ChevronLeft, Loader2, Check, Chevro import { format } from 'date-fns'; import { useState } from 'react'; +export const dynamic = 'force-dynamic'; + export default function ResultsPage() { const router = useRouter(); const searchParams = useSearchParams(); @@ -39,11 +41,11 @@ export default function ResultsPage() { const { data: results, isLoading, error } = useQuery({ queryKey: ['search', searchData], - queryFn: async () => { + queryFn: async (): Promise => { console.log('Searching with criteria:', searchData); - const response = await apiClient.post('/search', searchData); + const response = await apiClient.post('/search', searchData) as Schedule[]; console.log('Search results:', response); - console.log('Number of results:', response?.length || 0); + console.log('Number of results:', Array.isArray(response) ? response.length : 0); return response; }, enabled: !!searchData.originStationId && !!searchData.destinationStationId, @@ -90,8 +92,8 @@ export default function ResultsPage() { trainNumber: schedule.trainNumber, origin: schedule.origin?.name || 'Origin', destination: schedule.destination?.name || 'Destination', - departureTime: schedule.departureAt || schedule.departureTime, - arrivalTime: schedule.arrivalAt || schedule.arrivalTime, + departureTime: schedule.departureAt || schedule.departureTime || '', + arrivalTime: schedule.arrivalAt || schedule.arrivalTime || '', duration: durationStr, baseFareAdult: selectedClassFare.baseFareMinor, baseFareChild: selectedClassFare.baseFareMinor, @@ -140,7 +142,7 @@ export default function ResultsPage() {

No Trains Found

- We couldn't find any trains matching your search criteria. Try adjusting your dates or route. + We couldn't find any trains matching your search criteria. Try adjusting your dates or route.