From 3e27933c712a76479234518e1fe7b9e1eaa1e582 Mon Sep 17 00:00:00 2001 From: Stephanos A Date: Tue, 2 Jun 2026 20:36:01 +0300 Subject: [PATCH 1/2] Removed old files --- .../backoffice/src/app/dashboard/page.tsx | 2 +- .../backoffice/src/types/edr.ts | 2 +- .../src/components/schedules/ScheduleForm.tsx | 86 ------------------- .../components/schedules/ScheduleTable.tsx | 45 ---------- .../src/components/seats/SeatSelector.tsx | 42 --------- .../src/components/tickets/TicketForm.tsx | 70 --------------- .../src/components/tickets/TicketTable.tsx | 33 ------- .../portal/src/hooks/useSchedules.ts | 16 ---- .../portal/src/hooks/useStations.ts | 9 -- .../portal/src/hooks/useTickets.ts | 16 ---- .../portal/src/services/schedules.service.ts | 14 --- .../portal/src/services/stations.service.ts | 14 --- .../portal/src/services/tickets.service.ts | 30 ------- 13 files changed, 2 insertions(+), 377 deletions(-) delete mode 100644 apps/edr-passenger-web/portal/src/components/schedules/ScheduleForm.tsx delete mode 100644 apps/edr-passenger-web/portal/src/components/schedules/ScheduleTable.tsx delete mode 100644 apps/edr-passenger-web/portal/src/components/seats/SeatSelector.tsx delete mode 100644 apps/edr-passenger-web/portal/src/components/tickets/TicketForm.tsx delete mode 100644 apps/edr-passenger-web/portal/src/components/tickets/TicketTable.tsx delete mode 100644 apps/edr-passenger-web/portal/src/hooks/useSchedules.ts delete mode 100644 apps/edr-passenger-web/portal/src/hooks/useStations.ts delete mode 100644 apps/edr-passenger-web/portal/src/hooks/useTickets.ts delete mode 100644 apps/edr-passenger-web/portal/src/services/schedules.service.ts delete mode 100644 apps/edr-passenger-web/portal/src/services/stations.service.ts delete mode 100644 apps/edr-passenger-web/portal/src/services/tickets.service.ts 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 581704933..98036e71f 100644 --- a/apps/edr-passenger-web/backoffice/src/app/dashboard/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/dashboard/page.tsx @@ -49,7 +49,7 @@ export default function DashboardPage() {

Dashboard

-

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

+

Hello, welcome back! Here's what's happening today.

diff --git a/apps/edr-passenger-web/backoffice/src/types/edr.ts b/apps/edr-passenger-web/backoffice/src/types/edr.ts index e126090c2..cc8f8639b 100644 --- a/apps/edr-passenger-web/backoffice/src/types/edr.ts +++ b/apps/edr-passenger-web/backoffice/src/types/edr.ts @@ -1,6 +1,6 @@ import { Passenger } from '@edr/types'; -// Enhanced Booking Types +// Enhanced Booking Types export interface Booking { id: string; bookingRef: string; diff --git a/apps/edr-passenger-web/portal/src/components/schedules/ScheduleForm.tsx b/apps/edr-passenger-web/portal/src/components/schedules/ScheduleForm.tsx deleted file mode 100644 index 7174c3197..000000000 --- a/apps/edr-passenger-web/portal/src/components/schedules/ScheduleForm.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { FormEvent, useState } from "react"; -import { Button, FormField } from "@edr/ui-common"; - -export interface ScheduleFormPayload { - trainCode: string; - originStationId: string; - destinationStationId: string; - departureTime: string; - arrivalTime: string; - basePrice: number; -} - -export interface ScheduleFormProps { - onSubmit: (payload: ScheduleFormPayload) => void; - isSubmitting?: boolean; -} - -const ScheduleForm = ({ onSubmit, isSubmitting }: ScheduleFormProps) => { - const [trainCode, setTrainCode] = useState(""); - const [originStationId, setOriginStationId] = useState(""); - const [destinationStationId, setDestinationStationId] = useState(""); - const [departureTime, setDepartureTime] = useState(""); - const [arrivalTime, setArrivalTime] = useState(""); - const [basePrice, setBasePrice] = useState("0"); - - const handleSubmit = (event: FormEvent) => { - event.preventDefault(); - onSubmit({ - trainCode, - originStationId, - destinationStationId, - departureTime, - arrivalTime, - basePrice: Number(basePrice), - }); - }; - - return ( -
- setTrainCode(e.target.value)} - required - /> - setOriginStationId(e.target.value)} - required - /> - setDestinationStationId(e.target.value)} - required - /> - setDepartureTime(e.target.value)} - required - /> - setArrivalTime(e.target.value)} - required - /> - setBasePrice(e.target.value)} - min="0" - /> - - - ); -}; - -export default ScheduleForm; diff --git a/apps/edr-passenger-web/portal/src/components/schedules/ScheduleTable.tsx b/apps/edr-passenger-web/portal/src/components/schedules/ScheduleTable.tsx deleted file mode 100644 index 3d8b49242..000000000 --- a/apps/edr-passenger-web/portal/src/components/schedules/ScheduleTable.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { Table, type TableColumn } from "@edr/ui-common"; - -interface ISchedule { - id: string; - trainCode: string; - status: string; - departureTime: string; - arrivalTime: string; - basePrice: number; -} - -export interface ScheduleTableProps { - schedules: ISchedule[]; -} - -const columns: TableColumn[] = [ - { key: "trainCode", header: "Train" }, - { key: "status", header: "Status" }, - { - key: "departureTime", - header: "Departure", - render: (row) => new Date(row.departureTime).toLocaleString(), - }, - { - key: "arrivalTime", - header: "Arrival", - render: (row) => new Date(row.arrivalTime).toLocaleString(), - }, - { - key: "basePrice", - header: "Base price", - render: (row) => row.basePrice.toFixed(2), - }, -]; - -const ScheduleTable = ({ schedules }: ScheduleTableProps) => ( - row.id} - emptyMessage="No schedules yet" - /> -); - -export default ScheduleTable; diff --git a/apps/edr-passenger-web/portal/src/components/seats/SeatSelector.tsx b/apps/edr-passenger-web/portal/src/components/seats/SeatSelector.tsx deleted file mode 100644 index 38af99fcd..000000000 --- a/apps/edr-passenger-web/portal/src/components/seats/SeatSelector.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import clsx from "clsx"; -import type { Passenger } from "@edr/types"; - -export interface SeatSelectorProps { - seats: Passenger.ISeat[]; - selectedSeatId?: string; - onSelect: (seatId: string) => void; -} - -const SeatSelector = ({ - seats, - selectedSeatId, - onSelect, -}: SeatSelectorProps) => ( -
- {seats.map((seat) => { - const isAvailable = seat.status === "AVAILABLE"; - const isSelected = seat.id === selectedSeatId; - return ( - - ); - })} -
-); - -export default SeatSelector; diff --git a/apps/edr-passenger-web/portal/src/components/tickets/TicketForm.tsx b/apps/edr-passenger-web/portal/src/components/tickets/TicketForm.tsx deleted file mode 100644 index c717030bb..000000000 --- a/apps/edr-passenger-web/portal/src/components/tickets/TicketForm.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { FormEvent, useState } from "react"; -import { Button, FormField } from "@edr/ui-common"; - -import type { CreateTicketPayload } from "../../services/tickets.service"; - -export interface TicketFormProps { - onSubmit: (payload: CreateTicketPayload) => void; - isSubmitting?: boolean; -} - -const TicketForm = ({ onSubmit, isSubmitting }: TicketFormProps) => { - const [reference, setReference] = useState(""); - const [passengerId, setPassengerId] = useState(""); - const [scheduleId, setScheduleId] = useState(""); - const [seatId, setSeatId] = useState(""); - const [pricePaid, setPricePaid] = useState("0"); - - const handleSubmit = (event: FormEvent) => { - event.preventDefault(); - onSubmit({ - reference, - passengerId, - scheduleId, - seatId, - pricePaid: Number(pricePaid), - issuedAt: new Date().toISOString(), - }); - }; - - return ( -
- setReference(e.target.value)} - required - /> - setPassengerId(e.target.value)} - required - /> - setScheduleId(e.target.value)} - required - /> - setSeatId(e.target.value)} - required - /> - setPricePaid(e.target.value)} - min="0" - /> - - - ); -}; - -export default TicketForm; diff --git a/apps/edr-passenger-web/portal/src/components/tickets/TicketTable.tsx b/apps/edr-passenger-web/portal/src/components/tickets/TicketTable.tsx deleted file mode 100644 index eddc72a8d..000000000 --- a/apps/edr-passenger-web/portal/src/components/tickets/TicketTable.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import type { Passenger } from "@edr/types"; -import { Table, type TableColumn } from "@edr/ui-common"; - -export interface TicketTableProps { - tickets: Passenger.ITicket[]; -} - -const columns: TableColumn[] = [ - { key: "reference", header: "Reference" }, - { key: "passengerId", header: "Passenger" }, - { key: "status", header: "Status" }, - { - key: "issuedAt", - header: "Issued", - render: (row) => new Date(row.issuedAt).toLocaleString(), - }, - { - key: "pricePaid", - header: "Price", - render: (row) => row.pricePaid.toFixed(2), - }, -]; - -const TicketTable = ({ tickets }: TicketTableProps) => ( -
row.id} - emptyMessage="No tickets yet" - /> -); - -export default TicketTable; diff --git a/apps/edr-passenger-web/portal/src/hooks/useSchedules.ts b/apps/edr-passenger-web/portal/src/hooks/useSchedules.ts deleted file mode 100644 index e4fc7c2f7..000000000 --- a/apps/edr-passenger-web/portal/src/hooks/useSchedules.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; - -import { schedulesService } from "../services/schedules.service"; - -export const useSchedules = () => - useQuery({ - queryKey: ["schedules"], - queryFn: schedulesService.list, - }); - -export const useSchedule = (id: string) => - useQuery({ - queryKey: ["schedules", id], - queryFn: () => schedulesService.get(id), - enabled: Boolean(id), - }); diff --git a/apps/edr-passenger-web/portal/src/hooks/useStations.ts b/apps/edr-passenger-web/portal/src/hooks/useStations.ts deleted file mode 100644 index 23b4040a0..000000000 --- a/apps/edr-passenger-web/portal/src/hooks/useStations.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; - -import { stationsService } from "../services/stations.service"; - -export const useStations = () => - useQuery({ - queryKey: ["stations"], - queryFn: stationsService.list, - }); diff --git a/apps/edr-passenger-web/portal/src/hooks/useTickets.ts b/apps/edr-passenger-web/portal/src/hooks/useTickets.ts deleted file mode 100644 index f5fc84884..000000000 --- a/apps/edr-passenger-web/portal/src/hooks/useTickets.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; - -import { ticketsService } from "../services/tickets.service"; - -export const useTickets = () => - useQuery({ - queryKey: ["tickets"], - queryFn: ticketsService.list, - }); - -export const useTicket = (id: string) => - useQuery({ - queryKey: ["tickets", id], - queryFn: () => ticketsService.get(id), - enabled: Boolean(id), - }); diff --git a/apps/edr-passenger-web/portal/src/services/schedules.service.ts b/apps/edr-passenger-web/portal/src/services/schedules.service.ts deleted file mode 100644 index 1b65bdc4b..000000000 --- a/apps/edr-passenger-web/portal/src/services/schedules.service.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { Passenger } from "@edr/types"; - -import { api } from "../utils/api"; - -export const schedulesService = { - list: async (): Promise => { - const { data } = await api.get("/schedules"); - return data.data; - }, - get: async (id: string): Promise => { - const { data } = await api.get(`/schedules/${id}`); - return data.data; - }, -}; diff --git a/apps/edr-passenger-web/portal/src/services/stations.service.ts b/apps/edr-passenger-web/portal/src/services/stations.service.ts deleted file mode 100644 index 20010954a..000000000 --- a/apps/edr-passenger-web/portal/src/services/stations.service.ts +++ /dev/null @@ -1,14 +0,0 @@ -// import type { IStation } from "../types"; -import { api } from "../utils/api"; -import type { Passenger } from "@edr/types"; - -export const stationsService = { - list: async (): Promise => { - const { data } = await api.get("/stations"); - return data.data; - }, - get: async (id: string): Promise => { - const { data } = await api.get(`/stations/${id}`); - return data.data; - }, -}; diff --git a/apps/edr-passenger-web/portal/src/services/tickets.service.ts b/apps/edr-passenger-web/portal/src/services/tickets.service.ts deleted file mode 100644 index 4a6c7ac85..000000000 --- a/apps/edr-passenger-web/portal/src/services/tickets.service.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { Passenger, PaginatedResponse } from "@edr/types"; - -import { api } from "../utils/api"; - -export interface CreateTicketPayload { - reference: string; - passengerId: string; - scheduleId: string; - seatId: string; - pricePaid: number; - issuedAt: string; -} - -export const ticketsService = { - list: async (): Promise> => { - const { data } = await api.get("/tickets"); - return data.data; - }, - get: async (id: string): Promise => { - const { data } = await api.get(`/tickets/${id}`); - return data.data; - }, - create: async (payload: CreateTicketPayload): Promise => { - const { data } = await api.post("/tickets", payload); - return data.data; - }, - cancel: async (id: string): Promise => { - await api.delete(`/tickets/${id}`); - }, -}; From 5c0a118f13902fb75f47e907f01713f893b5510e Mon Sep 17 00:00:00 2001 From: Stephanos A Date: Tue, 2 Jun 2026 20:48:14 +0300 Subject: [PATCH 2/2] Build and deployment issues resolutions --- .../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 | 2 +- .../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 +++++++++++++----- 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 +- .../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 | 2 +- 40 files changed, 541 insertions(+), 177 deletions(-) create mode 100644 apps/edr-passenger-web/portal/index.html create mode 100644 apps/edr-passenger-web/portal/src/App.tsx create mode 100644 apps/edr-passenger-web/portal/src/main.tsx create mode 100644 apps/edr-passenger-web/portal/src/pages/dashboard/DashboardPage.tsx create mode 100644 apps/edr-passenger-web/portal/src/pages/passengers/PassengersPage.tsx create mode 100644 apps/edr-passenger-web/portal/src/pages/schedules/ScheduleDetailPage.tsx create mode 100644 apps/edr-passenger-web/portal/src/pages/schedules/SchedulesPage.tsx create mode 100644 apps/edr-passenger-web/portal/src/pages/stations/StationsPage.tsx create mode 100644 apps/edr-passenger-web/portal/src/pages/tickets/BookTicketPage.tsx create mode 100644 apps/edr-passenger-web/portal/src/pages/tickets/TicketDetailPage.tsx create mode 100644 apps/edr-passenger-web/portal/src/pages/tickets/TicketsPage.tsx create mode 100644 apps/edr-passenger-web/portal/src/vite-env.d.ts create mode 100644 apps/edr-passenger-web/portal/tsconfig.app.json create mode 100644 apps/edr-passenger-web/portal/tsconfig.node.json create 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 3af67730a..1214acebc 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,22 +201,11 @@ 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: guestPhone, + phone: firstPassenger.phone || `+251${uniqueId.replace(/[^0-9]/g, '').slice(0, 9)}`, 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 01c5aa8d1..95fc248e9 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 8ae8dd642..371fa0c96 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} - loading={isLoading} + isLoading={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 98036e71f..82b7c8cf5 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 as any)?.items || (recentBookingsData as any)?.data || []; + : recentBookingsData?.items || recentBookingsData?.data || []; const columns = [ { key: 'reference', label: 'Reference', render: (item: any) => item.bookingRef || item.reference }, 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 e80de0fe6..ba7bfe33b 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: any[] = [ + const actions = [ // 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 4cc2f867c..1c67df1b2 100644 --- a/apps/edr-passenger-web/backoffice/src/app/routes/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/routes/page.tsx @@ -136,17 +136,15 @@ export default function RoutesPage() { const generateRouteCode = (originId: string, destId: string) => { if (!originId || !destId) return ''; - 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); + const origin = stations?.items?.find((s: any) => s.id === originId); + const dest = stations?.items?.find((s: any) => s.id === destId); return origin && dest ? `${origin.code}-${dest.code}` : ''; }; const generateRouteName = (originId: string, destId: string) => { if (!originId || !destId) return ''; - 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); + const origin = stations?.items?.find((s: any) => s.id === originId); + const dest = stations?.items?.find((s: any) => s.id === destId); return origin && dest ? `${origin.name} - ${dest.name}` : ''; }; @@ -274,7 +272,7 @@ export default function RoutesPage() { disabled={!!editingRoute} > - {(Array.isArray(stations) ? stations : (stations as any)?.items || []).map((station: any) => ( + {stations?.items?.map((station: any) => ( @@ -291,7 +289,7 @@ export default function RoutesPage() { disabled={!!editingRoute} > - {(Array.isArray(stations) ? stations : (stations as any)?.items || []).map((station: any) => ( + {stations?.items?.map((station: any) => ( @@ -375,8 +373,8 @@ export default function RoutesPage() {
{originStationId ? ( - {(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'}) + {stations?.items?.find((s: any) => s.id === originStationId)?.name || 'Unknown'} + {' '}({stations?.items?.find((s: any) => s.id === originStationId)?.code || 'N/A'}) ) : ( Select origin station above @@ -401,7 +399,7 @@ export default function RoutesPage() { required > - {(Array.isArray(stations) ? stations : (stations as any)?.items || []).filter((s: any) => + {stations?.items?.filter((s: any) => s.id !== originStationId && s.id !== destinationStationId && !stops.some((st, idx) => idx !== index && st.stationId === s.id) @@ -457,8 +455,8 @@ export default function RoutesPage() {
{destinationStationId ? ( - {(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'}) + {stations?.items?.find((s: any) => s.id === destinationStationId)?.name || 'Unknown'} + {' '}({stations?.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 9c09138b5..d4a06f1f9 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 response = await apiClient.get(`/bookings${buildQuery(params)}`); - return response?.data ? (Array.isArray(response.data) ? { items: response.data } : response) : (Array.isArray(response) ? { items: response } : response); + 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; }, getById: (id: string) => apiClient.get(`/bookings/${id}`), cancel: (id: string, data?: any) => apiClient.post(`/bookings/${id}/cancel`, data), @@ -23,18 +22,32 @@ export const bookingsApi = { // Passengers API export const passengersApi = { getAll: async (params?: any) => { - 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); + 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; }, 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 response = await apiClient.get(`/stations${buildQuery(params)}`); - return response?.data ? (Array.isArray(response.data) ? { items: response.data } : response) : (Array.isArray(response) ? { items: response } : response); + 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; }, getById: (id: string) => apiClient.get(`/stations/${id}`), create: (data: any) => apiClient.post('/stations', data), @@ -45,12 +58,26 @@ export const stationsApi = { // Fleet API export const fleetApi = { getTrains: async (params?: any) => { - 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); + 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; }, getCoaches: async (params?: any) => { - 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); + 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; }, createTrain: (data: any) => apiClient.post('/fleet/trains', data), updateTrain: (id: string, data: any) => apiClient.patch(`/fleet/trains/${id}`, data), @@ -63,8 +90,12 @@ export const fleetApi = { // Schedules API export const schedulesApi = { getAll: async (params?: any) => { - 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); + 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; }, getById: (id: string) => apiClient.get(`/schedules/${id}`), create: (data: any) => apiClient.post('/schedules', data), @@ -94,8 +125,12 @@ export const seatsApi = { // Payments API export const paymentsApi = { getAll: async (params?: any) => { - 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); + 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; }, getById: (id: string) => apiClient.get(`/payments/${id}`), refund: (id: string, data: any) => apiClient.post(`/payments/${id}/refund`, data), @@ -105,8 +140,12 @@ export const paymentsApi = { // Tickets API export const ticketsApi = { getAll: async (params?: any) => { - 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); + 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; }, getById: (id: string) => apiClient.get(`/tickets/${id}`), validate: (ticketId: string, data: any) => apiClient.post(`/tickets/${ticketId}/validate`, data), @@ -116,8 +155,12 @@ export const ticketsApi = { // Agents API export const agentsApi = { getAll: async (params?: any) => { - 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); + 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; }, getById: (id: string) => apiClient.get(`/agents/${id}`), create: (data: any) => apiClient.post('/agents', data), @@ -131,8 +174,12 @@ export const agentsApi = { // Loyalty API export const loyaltyApi = { getAccounts: async (params?: any) => { - 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); + 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; }, getAccount: (passengerId: string) => apiClient.get(`/loyalty/accounts/${passengerId}`), adjustPoints: (accountId: string, data: any) => apiClient.post(`/loyalty/accounts/${accountId}/adjust`, data), @@ -143,8 +190,12 @@ export const loyaltyApi = { // Wallet API export const walletApi = { getAccounts: async (params?: any) => { - 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); + 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; }, getAccount: (passengerId: string) => apiClient.get(`/wallet/accounts/${passengerId}`), adjustBalance: (accountId: string, data: any) => apiClient.post(`/wallet/accounts/${accountId}/adjust`, data), @@ -154,8 +205,12 @@ export const walletApi = { // Promotions API export const promotionsApi = { getAll: async (params?: any) => { - 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); + 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; }, getById: (id: string) => apiClient.get(`/promos/${id}`), create: (data: any) => apiClient.post('/promos', data), @@ -166,8 +221,12 @@ export const promotionsApi = { // Support API export const supportApi = { getConversations: async (params?: any) => { - 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); + 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; }, getConversation: (id: string) => apiClient.get(`/support/conversations/${id}`), updateStatus: (id: string, status: string) => apiClient.patch(`/support/conversations/${id}/status`, { status }), @@ -183,16 +242,24 @@ 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 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); + 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; }, }; // Fraud API export const fraudApi = { getAlerts: async (params?: any) => { - 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); + 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; }, acknowledgeAlert: (id: string) => apiClient.patch(`/fraud/alerts/${id}/acknowledge`), getRules: () => apiClient.get('/fraud/rules'), @@ -203,8 +270,12 @@ export const fraudApi = { // Verifayda API export const verifaydaApi = { getVerifications: async (params?: any) => { - 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); + 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; }, verify: (nationalId: string) => apiClient.post('/passengers/verify-fayda', { nationalId }), getStats: () => apiClient.get('/passengers/verification-stats'), @@ -213,8 +284,12 @@ export const verifaydaApi = { // Audit API export const auditApi = { getLogs: async (params?: any) => { - 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); + 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; }, getLog: (id: string) => apiClient.get(`/audit/logs/${id}`), }; @@ -241,8 +316,12 @@ export const foodApi = { getCategories: () => apiClient.get('/food/categories'), getMenuItems: (scheduleId: string) => apiClient.get(`/food/menu/${scheduleId}`), getOrders: async (params?: any) => { - 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); + 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; }, updateOrderStatus: (orderId: string, status: string) => apiClient.patch(`/food/orders/${orderId}/status`, { status }), createMenuItem: (data: any) => apiClient.post('/food/menu-items', data), @@ -251,8 +330,12 @@ export const foodApi = { // Reports API export const reportsApi = { getOperationalReports: async (params?: any) => { - 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); + 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; }, getRevenue: (params?: any) => apiClient.get('/reports/revenue', { params }), getOccupancy: (params?: any) => apiClient.get('/reports/occupancy', { params }), diff --git a/apps/edr-passenger-web/portal/README.md b/apps/edr-passenger-web/portal/README.md index a88c6f46d..b1e79bbab 100644 --- a/apps/edr-passenger-web/portal/README.md +++ b/apps/edr-passenger-web/portal/README.md @@ -14,13 +14,6 @@ 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 @@ -47,7 +40,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 4000 +- EDR Passenger API running on port 3002 ### Installation @@ -59,7 +52,7 @@ pnpm install cp .env.example .env.local # Update .env.local with API URL -NEXT_PUBLIC_API_URL=http://localhost:4000 +NEXT_PUBLIC_API_URL=http://localhost:3002 ``` ### Development @@ -86,13 +79,6 @@ 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 @@ -103,33 +89,19 @@ 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 dashboard) -│ ├── providers.tsx # React Query + Theme provider +│ ├── page.tsx # Home (redirects to search) +│ ├── providers.tsx # React Query 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) -├── 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 +├── types/ # TypeScript types +│ └── index.ts +└── hooks/ # Custom React hooks ``` ## State Management diff --git a/apps/edr-passenger-web/portal/index.html b/apps/edr-passenger-web/portal/index.html new file mode 100644 index 000000000..38eb8605d --- /dev/null +++ b/apps/edr-passenger-web/portal/index.html @@ -0,0 +1,12 @@ + + + + + + EDR Passenger Portal + + +
+ + + diff --git a/apps/edr-passenger-web/portal/next.config.js b/apps/edr-passenger-web/portal/next.config.js index d0993180a..9d962888f 100644 --- a/apps/edr-passenger-web/portal/next.config.js +++ b/apps/edr-passenger-web/portal/next.config.js @@ -2,10 +2,6 @@ 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 new file mode 100644 index 000000000..4e890f1c3 --- /dev/null +++ b/apps/edr-passenger-web/portal/src/App.tsx @@ -0,0 +1,53 @@ +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 12d6bd5a7..7ed28a907 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,8 +9,6 @@ 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(); @@ -20,7 +18,7 @@ export default function ConfirmationPage() { mutationFn: () => apiClient.patch(`/bookings/${bookingId}/confirm`, { status: 'SUCCEEDED' }), }); - useQuery({ + const { data: booking } = useQuery({ queryKey: ['booking', bookingId], queryFn: async () => { try { @@ -43,8 +41,7 @@ export default function ConfirmationPage() { if (bookingId && !confirmMutation.isSuccess && !confirmMutation.isPending) { confirmMutation.mutate(); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [bookingId, confirmMutation]); + }, [bookingId]); 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 4438f9903..276a3af51 100644 --- a/apps/edr-passenger-web/portal/src/app/booking/layout.tsx +++ b/apps/edr-passenger-web/portal/src/app/booking/layout.tsx @@ -3,8 +3,6 @@ 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 078d3cc73..7ed3e1a83 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,8 +10,6 @@ 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'), @@ -50,7 +48,7 @@ type FormData = z.infer; export default function PassengersPage() { const router = useRouter(); const { searchCriteria, setPassengers, setCreateAccount } = useBookingStore(); - const { user, isAuthenticated, updateUser } = useAuthStore(); + const { user, isAuthenticated, logout, updateUser } = useAuthStore(); const [faydaEnabled, setFaydaEnabled] = useState(true); const [verificationStatus, setVerificationStatus] = useState>({}); const [updatingUser, setUpdatingUser] = useState(false); @@ -61,7 +59,7 @@ export default function PassengersPage() { const { register, control, handleSubmit, setValue, watch, formState: { errors } } = useForm({ resolver: zodResolver(formSchema), defaultValues: { - passengers: Array.from({ length: totalPassengers }, () => ({ + passengers: Array.from({ length: totalPassengers }, (_, i) => ({ 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 536fe435b..200e378e6 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,8 +9,6 @@ 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(); @@ -41,11 +39,11 @@ export default function ResultsPage() { const { data: results, isLoading, error } = useQuery({ queryKey: ['search', searchData], - queryFn: async (): Promise => { + queryFn: async () => { console.log('Searching with criteria:', searchData); - const response = await apiClient.post('/search', searchData) as Schedule[]; + const response = await apiClient.post('/search', searchData); console.log('Search results:', response); - console.log('Number of results:', Array.isArray(response) ? response.length : 0); + console.log('Number of results:', response?.length || 0); return response; }, enabled: !!searchData.originStationId && !!searchData.destinationStationId, @@ -92,8 +90,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, @@ -142,7 +140,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.

row.id} + emptyMessage="No stations" + /> + )} + + ); +}; + +export default StationsPage; diff --git a/apps/edr-passenger-web/portal/src/pages/tickets/BookTicketPage.tsx b/apps/edr-passenger-web/portal/src/pages/tickets/BookTicketPage.tsx new file mode 100644 index 000000000..8f02a9264 --- /dev/null +++ b/apps/edr-passenger-web/portal/src/pages/tickets/BookTicketPage.tsx @@ -0,0 +1,32 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { useNavigate } from "react-router-dom"; + +import TicketForm from "../../components/tickets/TicketForm"; +import { ticketsService } from "../../services/tickets.service"; + +const BookTicketPage = () => { + const navigate = useNavigate(); + const queryClient = useQueryClient(); + + const mutation = useMutation({ + mutationFn: ticketsService.create, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["tickets"] }); + navigate("/tickets"); + }, + }); + + return ( +
+

+ Issue ticket +

+ +
+ ); +}; + +export default BookTicketPage; diff --git a/apps/edr-passenger-web/portal/src/pages/tickets/TicketDetailPage.tsx b/apps/edr-passenger-web/portal/src/pages/tickets/TicketDetailPage.tsx new file mode 100644 index 000000000..d53614726 --- /dev/null +++ b/apps/edr-passenger-web/portal/src/pages/tickets/TicketDetailPage.tsx @@ -0,0 +1,38 @@ +import { useParams } from "react-router-dom"; + +import { useTicket } from "../../hooks/useTickets"; + +const TicketDetailPage = () => { + const { id } = useParams<{ id: string }>(); + const { data, isLoading } = useTicket(id ?? ""); + + if (isLoading) return
Loading…
; + if (!data) + return
Ticket not found.
; + + return ( +
+

+ Ticket {data.reference} +

+
+
Passenger ID
+
{data.passengerId}
+
Schedule ID
+
{data.scheduleId}
+
Seat ID
+
{data.seatId}
+
Status
+
{data.status}
+
Issued
+
+ {new Date(data.issuedAt).toLocaleString()} +
+
Price paid
+
{data.pricePaid.toFixed(2)}
+
+
+ ); +}; + +export default TicketDetailPage; diff --git a/apps/edr-passenger-web/portal/src/pages/tickets/TicketsPage.tsx b/apps/edr-passenger-web/portal/src/pages/tickets/TicketsPage.tsx new file mode 100644 index 000000000..b33471fac --- /dev/null +++ b/apps/edr-passenger-web/portal/src/pages/tickets/TicketsPage.tsx @@ -0,0 +1,28 @@ +import { Link } from "react-router-dom"; +import { Button } from "@edr/ui-common"; + +import TicketTable from "../../components/tickets/TicketTable"; +import { useTickets } from "../../hooks/useTickets"; + +const TicketsPage = () => { + const { data, isLoading } = useTickets(); + const items = data?.items ?? []; + + return ( +
+
+

Tickets

+ + + +
+ {isLoading ? ( +
Loading…
+ ) : ( + + )} +
+ ); +}; + +export default TicketsPage; diff --git a/apps/edr-passenger-web/portal/src/utils/api.ts b/apps/edr-passenger-web/portal/src/utils/api.ts index d84d9a124..ca7c2031e 100644 --- a/apps/edr-passenger-web/portal/src/utils/api.ts +++ b/apps/edr-passenger-web/portal/src/utils/api.ts @@ -1,7 +1,7 @@ import axios from "axios"; export const api = axios.create({ - baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000', + baseURL: import.meta.env.VITE_API_URL, }); // TODO: integrate @edr/auth — add a request interceptor here that attaches diff --git a/apps/edr-passenger-web/portal/src/vite-env.d.ts b/apps/edr-passenger-web/portal/src/vite-env.d.ts new file mode 100644 index 000000000..29c29a188 --- /dev/null +++ b/apps/edr-passenger-web/portal/src/vite-env.d.ts @@ -0,0 +1,9 @@ +/// + +interface ImportMetaEnv { + readonly VITE_API_URL: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/apps/edr-passenger-web/portal/tsconfig.app.json b/apps/edr-passenger-web/portal/tsconfig.app.json new file mode 100644 index 000000000..73df43221 --- /dev/null +++ b/apps/edr-passenger-web/portal/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "@edr/tsconfig/react.json", + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "useDefineForClassFields": true, + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/apps/edr-passenger-web/portal/tsconfig.node.json b/apps/edr-passenger-web/portal/tsconfig.node.json new file mode 100644 index 000000000..181375c8f --- /dev/null +++ b/apps/edr-passenger-web/portal/tsconfig.node.json @@ -0,0 +1,14 @@ +{ + "extends": "@edr/tsconfig/base.json", + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "moduleResolution": "Bundler", + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "noEmit": true + }, + "include": ["vite.config.ts"] +} diff --git a/apps/edr-passenger-web/portal/vite.config.ts b/apps/edr-passenger-web/portal/vite.config.ts new file mode 100644 index 000000000..324a8dfa7 --- /dev/null +++ b/apps/edr-passenger-web/portal/vite.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], + server: { + port: 5174, + host: "0.0.0.0", + }, + // test: { + // environment: "jsdom", + // globals: true, + // }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 053d67038..8964fecde 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -222,7 +222,7 @@ importers: version: 11.1.0(chokidar@4.0.3)(prettier@3.8.3)(typescript@5.9.3) '@nestjs/testing': specifier: ^11.1.19 - version: 11.1.23(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(@nestjs/platform-express@11.1.23) + version: 11.1.23(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)(@nestjs/microservices@11.1.24)(@nestjs/platform-express@11.1.23) '@types/bcrypt': specifier: ^5.0.2 version: 5.0.2