mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 10:45:44 +00:00
Build related issues resolution
This commit is contained in:
@@ -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({
|
const tempUser = await this.prisma.user.create({
|
||||||
data: {
|
data: {
|
||||||
fullName: firstPassenger.passengerName,
|
fullName: firstPassenger.passengerName,
|
||||||
email: guestEmail,
|
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),
|
passwordHash: await bcrypt.hash(Math.random().toString(36), 10),
|
||||||
role: 'PASSENGER',
|
role: 'PASSENGER',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export default function AuditLogsPage() {
|
|||||||
const actions = [
|
const actions = [
|
||||||
{
|
{
|
||||||
label: 'View Details',
|
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,
|
variant: 'secondary' as const,
|
||||||
icon: Eye,
|
icon: Eye,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ export default function CoachesPage() {
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
data={coaches}
|
data={coaches}
|
||||||
actions={actions}
|
actions={actions}
|
||||||
isLoading={isLoading}
|
loading={isLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export default function DashboardPage() {
|
|||||||
|
|
||||||
const recentBookings = Array.isArray(recentBookingsData)
|
const recentBookings = Array.isArray(recentBookingsData)
|
||||||
? recentBookingsData
|
? recentBookingsData
|
||||||
: recentBookingsData?.items || recentBookingsData?.data || [];
|
: (recentBookingsData as any)?.items || (recentBookingsData as any)?.data || [];
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ key: 'reference', label: 'Reference', render: (item: any) => item.bookingRef || item.reference },
|
{ key: 'reference', label: 'Reference', render: (item: any) => item.bookingRef || item.reference },
|
||||||
@@ -49,7 +49,7 @@ export default function DashboardPage() {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-foreground">Dashboard</h1>
|
<h1 className="text-3xl font-bold text-foreground">Dashboard</h1>
|
||||||
<p className="text-muted-foreground mt-1">Welcome back! Here's what's happening today.</p>
|
<p className="text-muted-foreground mt-1">Welcome back! Here's what's happening today.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
|
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export default function PassengersPage() {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const actions = [
|
const actions: any[] = [
|
||||||
// TODO: Create passenger detail page
|
// TODO: Create passenger detail page
|
||||||
// {
|
// {
|
||||||
// label: 'View Details',
|
// label: 'View Details',
|
||||||
|
|||||||
@@ -136,15 +136,17 @@ export default function RoutesPage() {
|
|||||||
|
|
||||||
const generateRouteCode = (originId: string, destId: string) => {
|
const generateRouteCode = (originId: string, destId: string) => {
|
||||||
if (!originId || !destId) return '';
|
if (!originId || !destId) return '';
|
||||||
const origin = stations?.items?.find((s: any) => s.id === originId);
|
const stationsList = Array.isArray(stations) ? stations : (stations as any)?.items || [];
|
||||||
const dest = stations?.items?.find((s: any) => s.id === destId);
|
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}` : '';
|
return origin && dest ? `${origin.code}-${dest.code}` : '';
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateRouteName = (originId: string, destId: string) => {
|
const generateRouteName = (originId: string, destId: string) => {
|
||||||
if (!originId || !destId) return '';
|
if (!originId || !destId) return '';
|
||||||
const origin = stations?.items?.find((s: any) => s.id === originId);
|
const stationsList = Array.isArray(stations) ? stations : (stations as any)?.items || [];
|
||||||
const dest = stations?.items?.find((s: any) => s.id === destId);
|
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}` : '';
|
return origin && dest ? `${origin.name} - ${dest.name}` : '';
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -272,7 +274,7 @@ export default function RoutesPage() {
|
|||||||
disabled={!!editingRoute}
|
disabled={!!editingRoute}
|
||||||
>
|
>
|
||||||
<option value="">Select Origin</option>
|
<option value="">Select Origin</option>
|
||||||
{stations?.items?.map((station: any) => (
|
{(Array.isArray(stations) ? stations : (stations as any)?.items || []).map((station: any) => (
|
||||||
<option key={station.id} value={station.id}>
|
<option key={station.id} value={station.id}>
|
||||||
{station.name} ({station.code})
|
{station.name} ({station.code})
|
||||||
</option>
|
</option>
|
||||||
@@ -289,7 +291,7 @@ export default function RoutesPage() {
|
|||||||
disabled={!!editingRoute}
|
disabled={!!editingRoute}
|
||||||
>
|
>
|
||||||
<option value="">Select Destination</option>
|
<option value="">Select Destination</option>
|
||||||
{stations?.items?.map((station: any) => (
|
{(Array.isArray(stations) ? stations : (stations as any)?.items || []).map((station: any) => (
|
||||||
<option key={station.id} value={station.id}>
|
<option key={station.id} value={station.id}>
|
||||||
{station.name} ({station.code})
|
{station.name} ({station.code})
|
||||||
</option>
|
</option>
|
||||||
@@ -373,8 +375,8 @@ export default function RoutesPage() {
|
|||||||
<div className="flex-1 font-medium">
|
<div className="flex-1 font-medium">
|
||||||
{originStationId ? (
|
{originStationId ? (
|
||||||
<span>
|
<span>
|
||||||
{stations?.items?.find((s: any) => s.id === originStationId)?.name || 'Unknown'}
|
{(Array.isArray(stations) ? stations : (stations as any)?.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)?.code || 'N/A'})
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-muted-foreground">Select origin station above</span>
|
<span className="text-muted-foreground">Select origin station above</span>
|
||||||
@@ -399,7 +401,7 @@ export default function RoutesPage() {
|
|||||||
required
|
required
|
||||||
>
|
>
|
||||||
<option value="">Select Station</option>
|
<option value="">Select Station</option>
|
||||||
{stations?.items?.filter((s: any) =>
|
{(Array.isArray(stations) ? stations : (stations as any)?.items || []).filter((s: any) =>
|
||||||
s.id !== originStationId &&
|
s.id !== originStationId &&
|
||||||
s.id !== destinationStationId &&
|
s.id !== destinationStationId &&
|
||||||
!stops.some((st, idx) => idx !== index && st.stationId === s.id)
|
!stops.some((st, idx) => idx !== index && st.stationId === s.id)
|
||||||
@@ -455,8 +457,8 @@ export default function RoutesPage() {
|
|||||||
<div className="flex-1 font-medium">
|
<div className="flex-1 font-medium">
|
||||||
{destinationStationId ? (
|
{destinationStationId ? (
|
||||||
<span>
|
<span>
|
||||||
{stations?.items?.find((s: any) => s.id === destinationStationId)?.name || 'Unknown'}
|
{(Array.isArray(stations) ? stations : (stations as any)?.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)?.code || 'N/A'})
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-muted-foreground">Select destination station above</span>
|
<span className="text-muted-foreground">Select destination station above</span>
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ export default function SeatClassesPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DataTable
|
<DataTable
|
||||||
data={data?.items || data || []}
|
data={(Array.isArray(data) ? data : (data as any)?.items) || []}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
actions={actions}
|
actions={actions}
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
import { apiClient } from '@/lib/api-client';
|
import { apiClient } from '@/lib/api-client';
|
||||||
import { PaginatedResponse } from '@edr/types';
|
import { PaginatedResponse } from '@edr/types';
|
||||||
|
|
||||||
|
const buildQuery = (params?: any) => {
|
||||||
|
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<string, string>).toString()}` : '';
|
||||||
|
};
|
||||||
|
|
||||||
// Bookings API
|
// Bookings API
|
||||||
export const bookingsApi = {
|
export const bookingsApi = {
|
||||||
getAll: async (params?: any) => {
|
getAll: async (params?: any) => {
|
||||||
const cleanParams = Object.fromEntries(
|
const response = await apiClient.get<any>(`/bookings${buildQuery(params)}`);
|
||||||
Object.entries(params || {}).filter(([_, value]) => value !== '' && value !== undefined && value !== null)
|
return response?.data ? (Array.isArray(response.data) ? { items: response.data } : response) : (Array.isArray(response) ? { items: response } : response);
|
||||||
);
|
|
||||||
const query = new URLSearchParams(cleanParams).toString();
|
|
||||||
const response = await apiClient.get<any>(`/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<any>(`/bookings/${id}`),
|
getById: (id: string) => apiClient.get<any>(`/bookings/${id}`),
|
||||||
cancel: (id: string, data?: any) => apiClient.post<any>(`/bookings/${id}/cancel`, data),
|
cancel: (id: string, data?: any) => apiClient.post<any>(`/bookings/${id}/cancel`, data),
|
||||||
@@ -22,32 +23,18 @@ export const bookingsApi = {
|
|||||||
// Passengers API
|
// Passengers API
|
||||||
export const passengersApi = {
|
export const passengersApi = {
|
||||||
getAll: async (params?: any) => {
|
getAll: async (params?: any) => {
|
||||||
const cleanParams = Object.fromEntries(
|
const response = await apiClient.get<any>(`/passengers${buildQuery(params)}`);
|
||||||
Object.entries(params || {}).filter(([_, value]) => value !== '' && value !== undefined && value !== null)
|
return response?.data ? (Array.isArray(response.data) ? { items: response.data } : response) : (Array.isArray(response) ? { items: response } : response);
|
||||||
);
|
|
||||||
const query = new URLSearchParams(cleanParams).toString();
|
|
||||||
const response = await apiClient.get<any>(`/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<any>(`/passengers/${id}`),
|
getById: (id: string) => apiClient.get<any>(`/passengers/${id}`),
|
||||||
verify: (nationalId: string) => apiClient.post<any>('/passengers/verify-fayda', { nationalId }),
|
verify: (nationalId: string) => apiClient.post<any>('/passengers/verify-fayda', { nationalId }),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Stations API
|
// Stations API
|
||||||
export const stationsApi = {
|
export const stationsApi = {
|
||||||
getAll: async (params?: any) => {
|
getAll: async (params?: any) => {
|
||||||
const cleanParams = Object.fromEntries(
|
const response = await apiClient.get<any>(`/stations${buildQuery(params)}`);
|
||||||
Object.entries(params || {}).filter(([_, value]) => value !== '' && value !== undefined && value !== null)
|
return response?.data ? (Array.isArray(response.data) ? { items: response.data } : response) : (Array.isArray(response) ? { items: response } : response);
|
||||||
);
|
|
||||||
const query = new URLSearchParams(cleanParams).toString();
|
|
||||||
const response = await apiClient.get<any>(`/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<any>(`/stations/${id}`),
|
getById: (id: string) => apiClient.get<any>(`/stations/${id}`),
|
||||||
create: (data: any) => apiClient.post<any>('/stations', data),
|
create: (data: any) => apiClient.post<any>('/stations', data),
|
||||||
@@ -58,26 +45,12 @@ export const stationsApi = {
|
|||||||
// Fleet API
|
// Fleet API
|
||||||
export const fleetApi = {
|
export const fleetApi = {
|
||||||
getTrains: async (params?: any) => {
|
getTrains: async (params?: any) => {
|
||||||
const cleanParams = Object.fromEntries(
|
const response = await apiClient.get<any>(`/fleet/trains${buildQuery(params)}`);
|
||||||
Object.entries(params || {}).filter(([_, value]) => value !== '' && value !== undefined && value !== null)
|
return response?.data ? (Array.isArray(response.data) ? { items: response.data } : response) : (Array.isArray(response) ? { items: response } : response);
|
||||||
);
|
|
||||||
const query = new URLSearchParams(cleanParams).toString();
|
|
||||||
const response = await apiClient.get<any>(`/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) => {
|
getCoaches: async (params?: any) => {
|
||||||
const cleanParams = Object.fromEntries(
|
const response = await apiClient.get<any>(`/fleet/coaches${buildQuery(params)}`);
|
||||||
Object.entries(params || {}).filter(([_, value]) => value !== '' && value !== undefined && value !== null)
|
return response?.data ? (Array.isArray(response.data) ? { items: response.data } : response) : (Array.isArray(response) ? { items: response } : response);
|
||||||
);
|
|
||||||
const query = new URLSearchParams(cleanParams).toString();
|
|
||||||
const response = await apiClient.get<any>(`/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<any>('/fleet/trains', data),
|
createTrain: (data: any) => apiClient.post<any>('/fleet/trains', data),
|
||||||
updateTrain: (id: string, data: any) => apiClient.patch<any>(`/fleet/trains/${id}`, data),
|
updateTrain: (id: string, data: any) => apiClient.patch<any>(`/fleet/trains/${id}`, data),
|
||||||
@@ -90,12 +63,8 @@ export const fleetApi = {
|
|||||||
// Schedules API
|
// Schedules API
|
||||||
export const schedulesApi = {
|
export const schedulesApi = {
|
||||||
getAll: async (params?: any) => {
|
getAll: async (params?: any) => {
|
||||||
const query = new URLSearchParams(params).toString();
|
const response = await apiClient.get<any>(`/schedules${buildQuery(params)}`);
|
||||||
const response = await apiClient.get<any>(`/schedules${query ? `?${query}` : ''}`);
|
return response?.data ? (Array.isArray(response.data) ? { items: response.data } : response) : (Array.isArray(response) ? { items: response } : response);
|
||||||
if (response?.data) {
|
|
||||||
return Array.isArray(response.data) ? { items: response.data } : response;
|
|
||||||
}
|
|
||||||
return Array.isArray(response) ? { items: response } : response;
|
|
||||||
},
|
},
|
||||||
getById: (id: string) => apiClient.get<any>(`/schedules/${id}`),
|
getById: (id: string) => apiClient.get<any>(`/schedules/${id}`),
|
||||||
create: (data: any) => apiClient.post<any>('/schedules', data),
|
create: (data: any) => apiClient.post<any>('/schedules', data),
|
||||||
@@ -125,12 +94,8 @@ export const seatsApi = {
|
|||||||
// Payments API
|
// Payments API
|
||||||
export const paymentsApi = {
|
export const paymentsApi = {
|
||||||
getAll: async (params?: any) => {
|
getAll: async (params?: any) => {
|
||||||
const query = new URLSearchParams(params).toString();
|
const response = await apiClient.get<any>(`/payments${buildQuery(params)}`);
|
||||||
const response = await apiClient.get<any>(`/payments${query ? `?${query}` : ''}`);
|
return response?.data ? (Array.isArray(response.data) ? { items: response.data } : response) : (Array.isArray(response) ? { items: response } : response);
|
||||||
if (response?.data) {
|
|
||||||
return Array.isArray(response.data) ? { items: response.data } : response;
|
|
||||||
}
|
|
||||||
return Array.isArray(response) ? { items: response } : response;
|
|
||||||
},
|
},
|
||||||
getById: (id: string) => apiClient.get<any>(`/payments/${id}`),
|
getById: (id: string) => apiClient.get<any>(`/payments/${id}`),
|
||||||
refund: (id: string, data: any) => apiClient.post<any>(`/payments/${id}/refund`, data),
|
refund: (id: string, data: any) => apiClient.post<any>(`/payments/${id}/refund`, data),
|
||||||
@@ -140,12 +105,8 @@ export const paymentsApi = {
|
|||||||
// Tickets API
|
// Tickets API
|
||||||
export const ticketsApi = {
|
export const ticketsApi = {
|
||||||
getAll: async (params?: any) => {
|
getAll: async (params?: any) => {
|
||||||
const query = new URLSearchParams(params).toString();
|
const response = await apiClient.get<any>(`/tickets${buildQuery(params)}`);
|
||||||
const response = await apiClient.get<any>(`/tickets${query ? `?${query}` : ''}`);
|
return response?.data ? (Array.isArray(response.data) ? { items: response.data } : response) : (Array.isArray(response) ? { items: response } : response);
|
||||||
if (response?.data) {
|
|
||||||
return Array.isArray(response.data) ? { items: response.data } : response;
|
|
||||||
}
|
|
||||||
return Array.isArray(response) ? { items: response } : response;
|
|
||||||
},
|
},
|
||||||
getById: (id: string) => apiClient.get<any>(`/tickets/${id}`),
|
getById: (id: string) => apiClient.get<any>(`/tickets/${id}`),
|
||||||
validate: (ticketId: string, data: any) => apiClient.post<any>(`/tickets/${ticketId}/validate`, data),
|
validate: (ticketId: string, data: any) => apiClient.post<any>(`/tickets/${ticketId}/validate`, data),
|
||||||
@@ -155,12 +116,8 @@ export const ticketsApi = {
|
|||||||
// Agents API
|
// Agents API
|
||||||
export const agentsApi = {
|
export const agentsApi = {
|
||||||
getAll: async (params?: any) => {
|
getAll: async (params?: any) => {
|
||||||
const query = new URLSearchParams(params).toString();
|
const response = await apiClient.get<any>(`/agents${buildQuery(params)}`);
|
||||||
const response = await apiClient.get<any>(`/agents${query ? `?${query}` : ''}`);
|
return response?.data ? (Array.isArray(response.data) ? { items: response.data } : response) : (Array.isArray(response) ? { items: response } : response);
|
||||||
if (response?.data) {
|
|
||||||
return Array.isArray(response.data) ? { items: response.data } : response;
|
|
||||||
}
|
|
||||||
return Array.isArray(response) ? { items: response } : response;
|
|
||||||
},
|
},
|
||||||
getById: (id: string) => apiClient.get<any>(`/agents/${id}`),
|
getById: (id: string) => apiClient.get<any>(`/agents/${id}`),
|
||||||
create: (data: any) => apiClient.post<any>('/agents', data),
|
create: (data: any) => apiClient.post<any>('/agents', data),
|
||||||
@@ -174,12 +131,8 @@ export const agentsApi = {
|
|||||||
// Loyalty API
|
// Loyalty API
|
||||||
export const loyaltyApi = {
|
export const loyaltyApi = {
|
||||||
getAccounts: async (params?: any) => {
|
getAccounts: async (params?: any) => {
|
||||||
const query = new URLSearchParams(params).toString();
|
const response = await apiClient.get<any>(`/loyalty/accounts${buildQuery(params)}`);
|
||||||
const response = await apiClient.get<any>(`/loyalty/accounts${query ? `?${query}` : ''}`);
|
return response?.data ? (Array.isArray(response.data) ? { items: response.data } : response) : (Array.isArray(response) ? { items: response } : response);
|
||||||
if (response?.data) {
|
|
||||||
return Array.isArray(response.data) ? { items: response.data } : response;
|
|
||||||
}
|
|
||||||
return Array.isArray(response) ? { items: response } : response;
|
|
||||||
},
|
},
|
||||||
getAccount: (passengerId: string) => apiClient.get<any>(`/loyalty/accounts/${passengerId}`),
|
getAccount: (passengerId: string) => apiClient.get<any>(`/loyalty/accounts/${passengerId}`),
|
||||||
adjustPoints: (accountId: string, data: any) => apiClient.post<any>(`/loyalty/accounts/${accountId}/adjust`, data),
|
adjustPoints: (accountId: string, data: any) => apiClient.post<any>(`/loyalty/accounts/${accountId}/adjust`, data),
|
||||||
@@ -190,12 +143,8 @@ export const loyaltyApi = {
|
|||||||
// Wallet API
|
// Wallet API
|
||||||
export const walletApi = {
|
export const walletApi = {
|
||||||
getAccounts: async (params?: any) => {
|
getAccounts: async (params?: any) => {
|
||||||
const query = new URLSearchParams(params).toString();
|
const response = await apiClient.get<any>(`/wallet/accounts${buildQuery(params)}`);
|
||||||
const response = await apiClient.get<any>(`/wallet/accounts${query ? `?${query}` : ''}`);
|
return response?.data ? (Array.isArray(response.data) ? { items: response.data } : response) : (Array.isArray(response) ? { items: response } : response);
|
||||||
if (response?.data) {
|
|
||||||
return Array.isArray(response.data) ? { items: response.data } : response;
|
|
||||||
}
|
|
||||||
return Array.isArray(response) ? { items: response } : response;
|
|
||||||
},
|
},
|
||||||
getAccount: (passengerId: string) => apiClient.get<any>(`/wallet/accounts/${passengerId}`),
|
getAccount: (passengerId: string) => apiClient.get<any>(`/wallet/accounts/${passengerId}`),
|
||||||
adjustBalance: (accountId: string, data: any) => apiClient.post<any>(`/wallet/accounts/${accountId}/adjust`, data),
|
adjustBalance: (accountId: string, data: any) => apiClient.post<any>(`/wallet/accounts/${accountId}/adjust`, data),
|
||||||
@@ -205,12 +154,8 @@ export const walletApi = {
|
|||||||
// Promotions API
|
// Promotions API
|
||||||
export const promotionsApi = {
|
export const promotionsApi = {
|
||||||
getAll: async (params?: any) => {
|
getAll: async (params?: any) => {
|
||||||
const query = new URLSearchParams(params).toString();
|
const response = await apiClient.get<any>(`/promos${buildQuery(params)}`);
|
||||||
const response = await apiClient.get<any>(`/promos${query ? `?${query}` : ''}`);
|
return response?.data ? (Array.isArray(response.data) ? { items: response.data } : response) : (Array.isArray(response) ? { items: response } : response);
|
||||||
if (response?.data) {
|
|
||||||
return Array.isArray(response.data) ? { items: response.data } : response;
|
|
||||||
}
|
|
||||||
return Array.isArray(response) ? { items: response } : response;
|
|
||||||
},
|
},
|
||||||
getById: (id: string) => apiClient.get<any>(`/promos/${id}`),
|
getById: (id: string) => apiClient.get<any>(`/promos/${id}`),
|
||||||
create: (data: any) => apiClient.post<any>('/promos', data),
|
create: (data: any) => apiClient.post<any>('/promos', data),
|
||||||
@@ -221,12 +166,8 @@ export const promotionsApi = {
|
|||||||
// Support API
|
// Support API
|
||||||
export const supportApi = {
|
export const supportApi = {
|
||||||
getConversations: async (params?: any) => {
|
getConversations: async (params?: any) => {
|
||||||
const query = new URLSearchParams(params).toString();
|
const response = await apiClient.get<any>(`/support/conversations${buildQuery(params)}`);
|
||||||
const response = await apiClient.get<any>(`/support/conversations${query ? `?${query}` : ''}`);
|
return response?.data ? (Array.isArray(response.data) ? { items: response.data } : response) : (Array.isArray(response) ? { items: response } : response);
|
||||||
if (response?.data) {
|
|
||||||
return Array.isArray(response.data) ? { items: response.data } : response;
|
|
||||||
}
|
|
||||||
return Array.isArray(response) ? { items: response } : response;
|
|
||||||
},
|
},
|
||||||
getConversation: (id: string) => apiClient.get<any>(`/support/conversations/${id}`),
|
getConversation: (id: string) => apiClient.get<any>(`/support/conversations/${id}`),
|
||||||
updateStatus: (id: string, status: string) => apiClient.patch<any>(`/support/conversations/${id}/status`, { status }),
|
updateStatus: (id: string, status: string) => apiClient.patch<any>(`/support/conversations/${id}/status`, { status }),
|
||||||
@@ -242,24 +183,16 @@ export const notificationsApi = {
|
|||||||
updateTemplate: (id: string, data: any) => apiClient.patch<any>(`/notifications/templates/${id}`, data),
|
updateTemplate: (id: string, data: any) => apiClient.patch<any>(`/notifications/templates/${id}`, data),
|
||||||
send: (data: any) => apiClient.post<any>('/notifications/send', data),
|
send: (data: any) => apiClient.post<any>('/notifications/send', data),
|
||||||
getHistory: async (params?: any) => {
|
getHistory: async (params?: any) => {
|
||||||
const query = new URLSearchParams(params).toString();
|
const response = await apiClient.get<any>(`/notifications/history${buildQuery(params)}`);
|
||||||
const response = await apiClient.get<any>(`/notifications/history${query ? `?${query}` : ''}`);
|
return response?.data ? (Array.isArray(response.data) ? { items: response.data } : response) : (Array.isArray(response) ? { items: response } : response);
|
||||||
if (response?.data) {
|
|
||||||
return Array.isArray(response.data) ? { items: response.data } : response;
|
|
||||||
}
|
|
||||||
return Array.isArray(response) ? { items: response } : response;
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fraud API
|
// Fraud API
|
||||||
export const fraudApi = {
|
export const fraudApi = {
|
||||||
getAlerts: async (params?: any) => {
|
getAlerts: async (params?: any) => {
|
||||||
const query = new URLSearchParams(params).toString();
|
const response = await apiClient.get<any>(`/fraud/alerts${buildQuery(params)}`);
|
||||||
const response = await apiClient.get<any>(`/fraud/alerts${query ? `?${query}` : ''}`);
|
return response?.data ? (Array.isArray(response.data) ? { items: response.data } : response) : (Array.isArray(response) ? { items: response } : response);
|
||||||
if (response?.data) {
|
|
||||||
return Array.isArray(response.data) ? { items: response.data } : response;
|
|
||||||
}
|
|
||||||
return Array.isArray(response) ? { items: response } : response;
|
|
||||||
},
|
},
|
||||||
acknowledgeAlert: (id: string) => apiClient.patch<any>(`/fraud/alerts/${id}/acknowledge`),
|
acknowledgeAlert: (id: string) => apiClient.patch<any>(`/fraud/alerts/${id}/acknowledge`),
|
||||||
getRules: () => apiClient.get<any[]>('/fraud/rules'),
|
getRules: () => apiClient.get<any[]>('/fraud/rules'),
|
||||||
@@ -270,12 +203,8 @@ export const fraudApi = {
|
|||||||
// Verifayda API
|
// Verifayda API
|
||||||
export const verifaydaApi = {
|
export const verifaydaApi = {
|
||||||
getVerifications: async (params?: any) => {
|
getVerifications: async (params?: any) => {
|
||||||
const query = new URLSearchParams(params).toString();
|
const response = await apiClient.get<any>(`/passengers/verifications${buildQuery(params)}`);
|
||||||
const response = await apiClient.get<any>(`/passengers/verifications${query ? `?${query}` : ''}`);
|
return response?.data ? (Array.isArray(response.data) ? { items: response.data } : response) : (Array.isArray(response) ? { items: response } : response);
|
||||||
if (response?.data) {
|
|
||||||
return Array.isArray(response.data) ? { items: response.data } : response;
|
|
||||||
}
|
|
||||||
return Array.isArray(response) ? { items: response } : response;
|
|
||||||
},
|
},
|
||||||
verify: (nationalId: string) => apiClient.post<any>('/passengers/verify-fayda', { nationalId }),
|
verify: (nationalId: string) => apiClient.post<any>('/passengers/verify-fayda', { nationalId }),
|
||||||
getStats: () => apiClient.get<any>('/passengers/verification-stats'),
|
getStats: () => apiClient.get<any>('/passengers/verification-stats'),
|
||||||
@@ -284,12 +213,8 @@ export const verifaydaApi = {
|
|||||||
// Audit API
|
// Audit API
|
||||||
export const auditApi = {
|
export const auditApi = {
|
||||||
getLogs: async (params?: any) => {
|
getLogs: async (params?: any) => {
|
||||||
const query = new URLSearchParams(params).toString();
|
const response = await apiClient.get<any>(`/audit/logs${buildQuery(params)}`);
|
||||||
const response = await apiClient.get<any>(`/audit/logs${query ? `?${query}` : ''}`);
|
return response?.data ? (Array.isArray(response.data) ? { items: response.data } : response) : (Array.isArray(response) ? { items: response } : response);
|
||||||
if (response?.data) {
|
|
||||||
return Array.isArray(response.data) ? { items: response.data } : response;
|
|
||||||
}
|
|
||||||
return Array.isArray(response) ? { items: response } : response;
|
|
||||||
},
|
},
|
||||||
getLog: (id: string) => apiClient.get<any>(`/audit/logs/${id}`),
|
getLog: (id: string) => apiClient.get<any>(`/audit/logs/${id}`),
|
||||||
};
|
};
|
||||||
@@ -316,12 +241,8 @@ export const foodApi = {
|
|||||||
getCategories: () => apiClient.get<any[]>('/food/categories'),
|
getCategories: () => apiClient.get<any[]>('/food/categories'),
|
||||||
getMenuItems: (scheduleId: string) => apiClient.get<any[]>(`/food/menu/${scheduleId}`),
|
getMenuItems: (scheduleId: string) => apiClient.get<any[]>(`/food/menu/${scheduleId}`),
|
||||||
getOrders: async (params?: any) => {
|
getOrders: async (params?: any) => {
|
||||||
const query = new URLSearchParams(params).toString();
|
const response = await apiClient.get<any>(`/food/orders${buildQuery(params)}`);
|
||||||
const response = await apiClient.get<any>(`/food/orders${query ? `?${query}` : ''}`);
|
return response?.data ? (Array.isArray(response.data) ? { items: response.data } : response) : (Array.isArray(response) ? { items: response } : response);
|
||||||
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<any>(`/food/orders/${orderId}/status`, { status }),
|
updateOrderStatus: (orderId: string, status: string) => apiClient.patch<any>(`/food/orders/${orderId}/status`, { status }),
|
||||||
createMenuItem: (data: any) => apiClient.post<any>('/food/menu-items', data),
|
createMenuItem: (data: any) => apiClient.post<any>('/food/menu-items', data),
|
||||||
@@ -330,12 +251,8 @@ export const foodApi = {
|
|||||||
// Reports API
|
// Reports API
|
||||||
export const reportsApi = {
|
export const reportsApi = {
|
||||||
getOperationalReports: async (params?: any) => {
|
getOperationalReports: async (params?: any) => {
|
||||||
const query = new URLSearchParams(params).toString();
|
const response = await apiClient.get<any>(`/reports/operational${buildQuery(params)}`);
|
||||||
const response = await apiClient.get<any>(`/reports/operational${query ? `?${query}` : ''}`);
|
return response?.data ? (Array.isArray(response.data) ? { items: response.data } : response) : (Array.isArray(response) ? { items: response } : response);
|
||||||
if (response?.data) {
|
|
||||||
return Array.isArray(response.data) ? { items: response.data } : response;
|
|
||||||
}
|
|
||||||
return Array.isArray(response) ? { items: response } : response;
|
|
||||||
},
|
},
|
||||||
getRevenue: (params?: any) => apiClient.get<any>('/reports/revenue', { params }),
|
getRevenue: (params?: any) => apiClient.get<any>('/reports/revenue', { params }),
|
||||||
getOccupancy: (params?: any) => apiClient.get<any>('/reports/occupancy', { params }),
|
getOccupancy: (params?: any) => apiClient.get<any>('/reports/occupancy', { params }),
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -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
|
7. **Payment** - Choose payment method and process payment
|
||||||
8. **Confirmation** - View PNR, tickets with QR codes
|
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
|
### Key Capabilities
|
||||||
- **Fayda 2.0 Integration** - Ethiopian national ID verification
|
- **Fayda 2.0 Integration** - Ethiopian national ID verification
|
||||||
- **Age-Based Pricing** - First child travels free
|
- **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
|
### Prerequisites
|
||||||
- Node.js >= 20.x
|
- Node.js >= 20.x
|
||||||
- pnpm >= 9.x
|
- pnpm >= 9.x
|
||||||
- EDR Passenger API running on port 3002
|
- EDR Passenger API running on port 4000
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
@@ -52,7 +59,7 @@ pnpm install
|
|||||||
cp .env.example .env.local
|
cp .env.example .env.local
|
||||||
|
|
||||||
# Update .env.local with API URL
|
# Update .env.local with API URL
|
||||||
NEXT_PUBLIC_API_URL=http://localhost:3002
|
NEXT_PUBLIC_API_URL=http://localhost:4000
|
||||||
```
|
```
|
||||||
|
|
||||||
### Development
|
### Development
|
||||||
@@ -79,6 +86,13 @@ pnpm start
|
|||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
├── app/ # Next.js App Router pages
|
├── 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/
|
│ ├── booking/
|
||||||
│ │ ├── search/ # Search trains
|
│ │ ├── search/ # Search trains
|
||||||
│ │ ├── results/ # Search results
|
│ │ ├── results/ # Search results
|
||||||
@@ -89,19 +103,33 @@ src/
|
|||||||
│ │ ├── payment/ # Payment processing
|
│ │ ├── payment/ # Payment processing
|
||||||
│ │ └── confirmation/ # Booking confirmation
|
│ │ └── confirmation/ # Booking confirmation
|
||||||
│ ├── login/ # Login page
|
│ ├── login/ # Login page
|
||||||
|
│ ├── profile/ # User profile
|
||||||
|
│ ├── guide/ # Travel guide
|
||||||
│ ├── layout.tsx # Root layout
|
│ ├── layout.tsx # Root layout
|
||||||
│ ├── page.tsx # Home (redirects to search)
|
│ ├── page.tsx # Home (redirects to dashboard)
|
||||||
│ ├── providers.tsx # React Query provider
|
│ ├── providers.tsx # React Query + Theme provider
|
||||||
│ └── globals.css # Global styles
|
│ └── globals.css # Global styles
|
||||||
├── components/ # Reusable components
|
├── components/ # Reusable components
|
||||||
|
│ ├── schedules/ # Schedule components
|
||||||
|
│ ├── tickets/ # Ticket components
|
||||||
|
│ ├── seats/ # Seat selector
|
||||||
|
│ ├── AppHeader.tsx # Navigation header
|
||||||
|
│ └── ThemeProvider.tsx # Theme context
|
||||||
├── lib/ # Core utilities
|
├── lib/ # Core utilities
|
||||||
│ ├── api-client.ts # Axios client with interceptors
|
│ ├── api-client.ts # Axios client with interceptors
|
||||||
│ ├── auth-store.ts # Auth state (Zustand)
|
│ ├── auth-store.ts # Auth state (Zustand)
|
||||||
│ ├── booking-store.ts # Booking flow state (Zustand)
|
│ ├── booking-store.ts # Booking flow state (Zustand)
|
||||||
│ └── payment-store.ts # Payment state (Zustand)
|
│ └── payment-store.ts # Payment state (Zustand)
|
||||||
├── types/ # TypeScript types
|
├── hooks/ # Custom React hooks
|
||||||
│ └── index.ts
|
│ ├── useSchedules.ts # Schedule queries
|
||||||
└── hooks/ # Custom React hooks
|
│ ├── 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
|
## State Management
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>EDR Passenger Portal</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -2,6 +2,10 @@
|
|||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
transpilePackages: ['@edr/types', '@edr/ui-common'],
|
transpilePackages: ['@edr/types', '@edr/ui-common'],
|
||||||
|
output: 'standalone',
|
||||||
|
experimental: {
|
||||||
|
cpus: 1,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
@@ -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 (
|
|
||||||
<DashboardLayout
|
|
||||||
title="EDR Passenger"
|
|
||||||
sidebarItems={sidebarItems}
|
|
||||||
activeHref={location.pathname}
|
|
||||||
onNavigate={navigate}
|
|
||||||
>
|
|
||||||
<Routes>
|
|
||||||
<Route path="/" element={<DashboardPage />} />
|
|
||||||
<Route path="/tickets" element={<TicketsPage />} />
|
|
||||||
<Route path="/tickets/new" element={<BookTicketPage />} />
|
|
||||||
<Route path="/tickets/:id" element={<TicketDetailPage />} />
|
|
||||||
<Route path="/schedules" element={<SchedulesPage />} />
|
|
||||||
<Route path="/schedules/:id" element={<ScheduleDetailPage />} />
|
|
||||||
<Route path="/stations" element={<StationsPage />} />
|
|
||||||
<Route path="/passengers" element={<PassengersPage />} />
|
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
|
||||||
</Routes>
|
|
||||||
</DashboardLayout>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default App;
|
|
||||||
@@ -9,6 +9,8 @@ import { CheckCircle, Download, Share2, Copy, Printer, Mail, Train } from 'lucid
|
|||||||
import { QRCodeSVG } from 'qrcode.react';
|
import { QRCodeSVG } from 'qrcode.react';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
export default function ConfirmationPage() {
|
export default function ConfirmationPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { bookingId, pnr, selectedSchedule, passengers, clearBooking } = useBookingStore();
|
const { bookingId, pnr, selectedSchedule, passengers, clearBooking } = useBookingStore();
|
||||||
@@ -18,7 +20,7 @@ export default function ConfirmationPage() {
|
|||||||
mutationFn: () => apiClient.patch(`/bookings/${bookingId}/confirm`, { status: 'SUCCEEDED' }),
|
mutationFn: () => apiClient.patch(`/bookings/${bookingId}/confirm`, { status: 'SUCCEEDED' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: booking } = useQuery({
|
useQuery({
|
||||||
queryKey: ['booking', bookingId],
|
queryKey: ['booking', bookingId],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
try {
|
try {
|
||||||
@@ -41,7 +43,8 @@ export default function ConfirmationPage() {
|
|||||||
if (bookingId && !confirmMutation.isSuccess && !confirmMutation.isPending) {
|
if (bookingId && !confirmMutation.isSuccess && !confirmMutation.isPending) {
|
||||||
confirmMutation.mutate();
|
confirmMutation.mutate();
|
||||||
}
|
}
|
||||||
}, [bookingId]);
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [bookingId, confirmMutation]);
|
||||||
|
|
||||||
const copyPNR = () => {
|
const copyPNR = () => {
|
||||||
if (pnr) {
|
if (pnr) {
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import { ProgressIndicator } from '@/components/ProgressIndicator';
|
import { ProgressIndicator } from '@/components/ProgressIndicator';
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
export default function BookingLayout({
|
export default function BookingLayout({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import { apiClient } from '@/lib/api-client';
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { CheckCircle, ExternalLink, Loader2 } from 'lucide-react';
|
import { CheckCircle, ExternalLink, Loader2 } from 'lucide-react';
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
const passengerSchema = z.object({
|
const passengerSchema = z.object({
|
||||||
name: z.string().min(2, 'Name is required'),
|
name: z.string().min(2, 'Name is required'),
|
||||||
dateOfBirth: z.string().min(1, 'Date of birth is required'),
|
dateOfBirth: z.string().min(1, 'Date of birth is required'),
|
||||||
@@ -48,7 +50,7 @@ type FormData = z.infer<typeof formSchema>;
|
|||||||
export default function PassengersPage() {
|
export default function PassengersPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { searchCriteria, setPassengers, setCreateAccount } = useBookingStore();
|
const { searchCriteria, setPassengers, setCreateAccount } = useBookingStore();
|
||||||
const { user, isAuthenticated, logout, updateUser } = useAuthStore();
|
const { user, isAuthenticated, updateUser } = useAuthStore();
|
||||||
const [faydaEnabled, setFaydaEnabled] = useState(true);
|
const [faydaEnabled, setFaydaEnabled] = useState(true);
|
||||||
const [verificationStatus, setVerificationStatus] = useState<Record<number, 'success' | 'error'>>({});
|
const [verificationStatus, setVerificationStatus] = useState<Record<number, 'success' | 'error'>>({});
|
||||||
const [updatingUser, setUpdatingUser] = useState(false);
|
const [updatingUser, setUpdatingUser] = useState(false);
|
||||||
@@ -59,7 +61,7 @@ export default function PassengersPage() {
|
|||||||
const { register, control, handleSubmit, setValue, watch, formState: { errors } } = useForm<FormData>({
|
const { register, control, handleSubmit, setValue, watch, formState: { errors } } = useForm<FormData>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
passengers: Array.from({ length: totalPassengers }, (_, i) => ({
|
passengers: Array.from({ length: totalPassengers }, () => ({
|
||||||
name: '',
|
name: '',
|
||||||
dateOfBirth: '',
|
dateOfBirth: '',
|
||||||
gender: undefined,
|
gender: undefined,
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import { ArrowRight, Clock, Calendar, Users, ChevronLeft, Loader2, Check, Chevro
|
|||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
export default function ResultsPage() {
|
export default function ResultsPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
@@ -39,11 +41,11 @@ export default function ResultsPage() {
|
|||||||
|
|
||||||
const { data: results, isLoading, error } = useQuery<Schedule[]>({
|
const { data: results, isLoading, error } = useQuery<Schedule[]>({
|
||||||
queryKey: ['search', searchData],
|
queryKey: ['search', searchData],
|
||||||
queryFn: async () => {
|
queryFn: async (): Promise<Schedule[]> => {
|
||||||
console.log('Searching with criteria:', searchData);
|
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('Search results:', response);
|
||||||
console.log('Number of results:', response?.length || 0);
|
console.log('Number of results:', Array.isArray(response) ? response.length : 0);
|
||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
enabled: !!searchData.originStationId && !!searchData.destinationStationId,
|
enabled: !!searchData.originStationId && !!searchData.destinationStationId,
|
||||||
@@ -90,8 +92,8 @@ export default function ResultsPage() {
|
|||||||
trainNumber: schedule.trainNumber,
|
trainNumber: schedule.trainNumber,
|
||||||
origin: schedule.origin?.name || 'Origin',
|
origin: schedule.origin?.name || 'Origin',
|
||||||
destination: schedule.destination?.name || 'Destination',
|
destination: schedule.destination?.name || 'Destination',
|
||||||
departureTime: schedule.departureAt || schedule.departureTime,
|
departureTime: schedule.departureAt || schedule.departureTime || '',
|
||||||
arrivalTime: schedule.arrivalAt || schedule.arrivalTime,
|
arrivalTime: schedule.arrivalAt || schedule.arrivalTime || '',
|
||||||
duration: durationStr,
|
duration: durationStr,
|
||||||
baseFareAdult: selectedClassFare.baseFareMinor,
|
baseFareAdult: selectedClassFare.baseFareMinor,
|
||||||
baseFareChild: selectedClassFare.baseFareMinor,
|
baseFareChild: selectedClassFare.baseFareMinor,
|
||||||
@@ -140,7 +142,7 @@ export default function ResultsPage() {
|
|||||||
</div>
|
</div>
|
||||||
<h2 className="text-2xl font-bold mb-3 text-gray-900 dark:text-gray-100">No Trains Found</h2>
|
<h2 className="text-2xl font-bold mb-3 text-gray-900 dark:text-gray-100">No Trains Found</h2>
|
||||||
<p className="text-gray-600 dark:text-gray-400 mb-8">
|
<p className="text-gray-600 dark:text-gray-400 mb-8">
|
||||||
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.
|
||||||
</p>
|
</p>
|
||||||
<button onClick={() => router.push(buildSearchUrl())} className="btn-primary">
|
<button onClick={() => router.push(buildSearchUrl())} className="btn-primary">
|
||||||
Modify Search
|
Modify Search
|
||||||
|
|||||||
@@ -209,9 +209,9 @@ export default function ReviewPage() {
|
|||||||
const baseFare = passengers.reduce((sum, p, i) => {
|
const baseFare = passengers.reduce((sum, p, i) => {
|
||||||
// Get the fare per passenger from the schedule
|
// Get the fare per passenger from the schedule
|
||||||
const farePerPassenger = selectedSchedule.baseFareAdult ||
|
const farePerPassenger = selectedSchedule.baseFareAdult ||
|
||||||
selectedSchedule.baseFare ||
|
(selectedSchedule as any).baseFare ||
|
||||||
selectedSchedule.fareAdult ||
|
(selectedSchedule as any).fareAdult ||
|
||||||
selectedSchedule.price ||
|
(selectedSchedule as any).price ||
|
||||||
0;
|
0;
|
||||||
|
|
||||||
console.log(`Passenger ${i}: ${p.name}, fare = ${farePerPassenger}`);
|
console.log(`Passenger ${i}: ${p.name}, fare = ${farePerPassenger}`);
|
||||||
|
|||||||
@@ -8,10 +8,12 @@ import { useQuery } from '@tanstack/react-query';
|
|||||||
import { apiClient } from '@/lib/api-client';
|
import { apiClient } from '@/lib/api-client';
|
||||||
import { useBookingStore } from '@/lib/booking-store';
|
import { useBookingStore } from '@/lib/booking-store';
|
||||||
import { Station } from '@/types';
|
import { Station } from '@/types';
|
||||||
import { Train, MapPin, Calendar, Users, ArrowRight, ArrowLeftRight, Plus, Minus, Search } from 'lucide-react';
|
import { Train, MapPin, Calendar, ArrowRight, ArrowLeftRight, Plus, Minus, Search } from 'lucide-react';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import ModernDatePicker from '@/components/ModernDatePicker';
|
import ModernDatePicker from '@/components/ModernDatePicker';
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
const searchSchema = z.object({
|
const searchSchema = z.object({
|
||||||
originStationId: z.string().min(1, 'Please select origin station'),
|
originStationId: z.string().min(1, 'Please select origin station'),
|
||||||
destinationStationId: z.string().min(1, 'Please select destination station'),
|
destinationStationId: z.string().min(1, 'Please select destination station'),
|
||||||
@@ -33,8 +35,8 @@ export default function SearchPage() {
|
|||||||
|
|
||||||
const { data: stations, isLoading, error } = useQuery<Station[]>({
|
const { data: stations, isLoading, error } = useQuery<Station[]>({
|
||||||
queryKey: ['stations'],
|
queryKey: ['stations'],
|
||||||
queryFn: async () => {
|
queryFn: async (): Promise<Station[]> => {
|
||||||
const response = await apiClient.get('/stations');
|
const response = await apiClient.get('/stations') as Station[];
|
||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -94,10 +96,10 @@ export default function SearchPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getStationByName = (name: string) => {
|
const getStationByName = (name: string) => {
|
||||||
if (!stations) return null;
|
if (!stations || !Array.isArray(stations)) return null;
|
||||||
const exactMatch = stations.find(s => s.name.toLowerCase() === name.toLowerCase());
|
const exactMatch = stations.find((s: Station) => s.name.toLowerCase() === name.toLowerCase());
|
||||||
if (exactMatch) return exactMatch;
|
if (exactMatch) return exactMatch;
|
||||||
return stations.find(s => s.name.toLowerCase().includes(name.toLowerCase()));
|
return stations.find((s: Station) => s.name.toLowerCase().includes(name.toLowerCase()));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePopularRoute = (fromName: string, toName: string) => {
|
const handlePopularRoute = (fromName: string, toName: string) => {
|
||||||
@@ -155,7 +157,7 @@ export default function SearchPage() {
|
|||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
<option value="">Select departure station</option>
|
<option value="">Select departure station</option>
|
||||||
{stations?.map((s) => (
|
{Array.isArray(stations) && stations.map((s: Station) => (
|
||||||
<option key={s.id} value={s.id}>{s.name}</option>
|
<option key={s.id} value={s.id}>{s.name}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
@@ -184,7 +186,7 @@ export default function SearchPage() {
|
|||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
<option value="">Select arrival station</option>
|
<option value="">Select arrival station</option>
|
||||||
{stations?.map((s) => (
|
{Array.isArray(stations) && stations.map((s: Station) => (
|
||||||
<option key={s.id} value={s.id} disabled={s.id === originId}>{s.name}</option>
|
<option key={s.id} value={s.id} disabled={s.id === originId}>{s.name}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@@ -4,16 +4,16 @@ import { useRouter } from 'next/navigation';
|
|||||||
import { useBookingStore } from '@/lib/booking-store';
|
import { useBookingStore } from '@/lib/booking-store';
|
||||||
import { useQuery, useMutation } from '@tanstack/react-query';
|
import { useQuery, useMutation } from '@tanstack/react-query';
|
||||||
import { apiClient } from '@/lib/api-client';
|
import { apiClient } from '@/lib/api-client';
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
import { Seat, Coach } from '@/types';
|
|
||||||
import CustomModal from '@/components/CustomModal';
|
import CustomModal from '@/components/CustomModal';
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
export default function SeatsPage() {
|
export default function SeatsPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { selectedSchedule, passengers, setSeatHold, setPassengers, searchCriteria } = useBookingStore();
|
const { selectedSchedule, passengers, setSeatHold, setPassengers, searchCriteria } = useBookingStore();
|
||||||
const [selectedSeats, setSelectedSeats] = useState<string[]>([]);
|
const [selectedSeats, setSelectedSeats] = useState<string[]>([]);
|
||||||
const [selectedCoach, setSelectedCoach] = useState<string | null>(null);
|
const [selectedCoach, setSelectedCoach] = useState<string | null>(null);
|
||||||
const [timeLeft, setTimeLeft] = useState<number | null>(null);
|
|
||||||
const [modalState, setModalState] = useState({
|
const [modalState, setModalState] = useState({
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
title: '',
|
title: '',
|
||||||
@@ -32,14 +32,14 @@ export default function SeatsPage() {
|
|||||||
if (seatMapData) {
|
if (seatMapData) {
|
||||||
console.log('Seat map data:', seatMapData);
|
console.log('Seat map data:', seatMapData);
|
||||||
console.log('Is array?', Array.isArray(seatMapData));
|
console.log('Is array?', Array.isArray(seatMapData));
|
||||||
console.log('Has coaches?', seatMapData?.coaches);
|
console.log('Has coaches?', (seatMapData as any)?.coaches);
|
||||||
}
|
}
|
||||||
}, [seatMapData]);
|
}, [seatMapData]);
|
||||||
|
|
||||||
const holdMutation = useMutation({
|
const holdMutation = useMutation({
|
||||||
mutationFn: async (seatIds: string[]) => {
|
mutationFn: async (seatIds: string[]) => {
|
||||||
// Create temporary passenger IDs for the hold
|
// Create temporary passenger IDs for the hold
|
||||||
const passengersForHold = passengers.slice(0, seatIds.length).map((p, i) => ({
|
const passengersForHold = passengers.slice(0, seatIds.length).map((_, i) => ({
|
||||||
passengerId: `temp-${Date.now()}-${i}`, // Temporary ID for guest booking
|
passengerId: `temp-${Date.now()}-${i}`, // Temporary ID for guest booking
|
||||||
seatId: seatIds[i],
|
seatId: seatIds[i],
|
||||||
}));
|
}));
|
||||||
@@ -60,7 +60,7 @@ export default function SeatsPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Extract coaches and seats from seat map data
|
// Extract coaches and seats from seat map data
|
||||||
const coaches = seatMapData?.coaches || [];
|
const coaches = useMemo(() => (seatMapData as any)?.coaches || [], [seatMapData]);
|
||||||
|
|
||||||
// Debug: Log coaches
|
// Debug: Log coaches
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -78,10 +78,11 @@ export default function SeatsPage() {
|
|||||||
? coaches.filter((c: any) => {
|
? coaches.filter((c: any) => {
|
||||||
// seatClass can be either a string or an object with a name property
|
// seatClass can be either a string or an object with a name property
|
||||||
const seatClassName = typeof c.seatClass === 'string' ? c.seatClass : (c.seatClass?.name || c.coachClass || '');
|
const seatClassName = typeof c.seatClass === 'string' ? c.seatClass : (c.seatClass?.name || c.coachClass || '');
|
||||||
console.log('Comparing:', seatClassName, 'with', selectedSchedule.selectedSeatClass);
|
const selectedClass = selectedSchedule.selectedSeatClass || '';
|
||||||
return seatClassName === selectedSchedule.selectedSeatClass ||
|
console.log('Comparing:', seatClassName, 'with', selectedClass);
|
||||||
seatClassName.replace(/_/g, ' ').toLowerCase() === selectedSchedule.selectedSeatClass.toLowerCase() ||
|
return seatClassName === selectedClass ||
|
||||||
seatClassName.toLowerCase() === selectedSchedule.selectedSeatClass.toLowerCase();
|
seatClassName.replace(/_/g, ' ').toLowerCase() === selectedClass.toLowerCase() ||
|
||||||
|
seatClassName.toLowerCase() === selectedClass.toLowerCase();
|
||||||
})
|
})
|
||||||
: coaches;
|
: coaches;
|
||||||
|
|
||||||
@@ -92,7 +93,7 @@ export default function SeatsPage() {
|
|||||||
}, [filteredCoaches]);
|
}, [filteredCoaches]);
|
||||||
|
|
||||||
const selectedCoachData = filteredCoaches.find((c: any) => c.id === selectedCoach);
|
const selectedCoachData = filteredCoaches.find((c: any) => c.id === selectedCoach);
|
||||||
const seats = selectedCoachData?.seats || [];
|
const seats = useMemo(() => selectedCoachData?.seats || [], [selectedCoachData]);
|
||||||
|
|
||||||
// Debug seats
|
// Debug seats
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export default function HowToGuidePage() {
|
|||||||
<div className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg space-y-3">
|
<div className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-semibold text-gray-900 dark:text-gray-100 mb-1">For Ethiopian Citizens:</p>
|
<p className="text-sm font-semibold text-gray-900 dark:text-gray-100 mb-1">For Ethiopian Citizens:</p>
|
||||||
<p className="text-sm text-gray-700 dark:text-gray-300">Click "Verify with Fayda" to auto-fill your details using your national ID.</p>
|
<p className="text-sm text-gray-700 dark:text-gray-300">Click "Verify with Fayda" to auto-fill your details using your national ID.</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-semibold text-gray-900 dark:text-gray-100 mb-1">For International Travelers:</p>
|
<p className="text-sm font-semibold text-gray-900 dark:text-gray-100 mb-1">For International Travelers:</p>
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import { useBookingStore } from '@/lib/booking-store';
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Train } from 'lucide-react';
|
import { Train } from 'lucide-react';
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
const loginSchema = z.object({
|
const loginSchema = z.object({
|
||||||
email: z.string().email('Invalid email address'),
|
email: z.string().email('Invalid email address'),
|
||||||
password: z.string().min(6, 'Password must be at least 6 characters'),
|
password: z.string().min(6, 'Password must be at least 6 characters'),
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import { useRouter } from 'next/navigation';
|
|||||||
import { useAuthStore } from '@/lib/auth-store';
|
import { useAuthStore } from '@/lib/auth-store';
|
||||||
import { useTheme } from '@/components/ThemeProvider';
|
import { useTheme } from '@/components/ThemeProvider';
|
||||||
import {
|
import {
|
||||||
User, Settings, Ticket, ChevronRight, Calendar, MapPin,
|
User, Settings, Ticket, Calendar, MapPin,
|
||||||
Download, Trash2, Lock, Bell, CreditCard, Globe,
|
Download, Trash2, Lock, Bell, CreditCard,
|
||||||
MapPinned, Palette, CheckCircle, XCircle, Clock,
|
MapPinned, Palette, CheckCircle,
|
||||||
Eye, Edit, LogOut, X
|
Eye, Edit, LogOut, X
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { apiClient } from '@/lib/api-client';
|
import { apiClient } from '@/lib/api-client';
|
||||||
@@ -84,11 +84,11 @@ export default function ProfilePage() {
|
|||||||
}
|
}
|
||||||
}, [isInitialized, isAuthenticated, user, router, fetchProfile]);
|
}, [isInitialized, isAuthenticated, user, router, fetchProfile]);
|
||||||
|
|
||||||
const { data: bookings, isLoading: loadingBookings } = useQuery({
|
const { data: bookings, isLoading: loadingBookings } = useQuery<Booking[]>({
|
||||||
queryKey: ['user-bookings'],
|
queryKey: ['user-bookings'],
|
||||||
queryFn: async () => {
|
queryFn: async (): Promise<Booking[]> => {
|
||||||
try {
|
try {
|
||||||
return await apiClient.get('/bookings/my-bookings');
|
return await apiClient.get('/bookings/my-bookings') as Booking[];
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -98,7 +98,7 @@ export default function ProfilePage() {
|
|||||||
|
|
||||||
const updateProfileMutation = useMutation({
|
const updateProfileMutation = useMutation({
|
||||||
mutationFn: (data: any) => apiClient.patch('/auth/profile', data),
|
mutationFn: (data: any) => apiClient.patch('/auth/profile', data),
|
||||||
onSuccess: (response) => {
|
onSuccess: (response: any) => {
|
||||||
const updatedData = response.data || response;
|
const updatedData = response.data || response;
|
||||||
updateUser(updatedData);
|
updateUser(updatedData);
|
||||||
setShowEditProfile(false);
|
setShowEditProfile(false);
|
||||||
@@ -143,7 +143,7 @@ export default function ProfilePage() {
|
|||||||
|
|
||||||
const downloadDataMutation = useMutation({
|
const downloadDataMutation = useMutation({
|
||||||
mutationFn: () => apiClient.get('/auth/download-data'),
|
mutationFn: () => apiClient.get('/auth/download-data'),
|
||||||
onSuccess: (data) => {
|
onSuccess: (data: any) => {
|
||||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||||
const url = window.URL.createObjectURL(blob);
|
const url = window.URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Train, LogOut, User, BookOpen, LogIn } from 'lucide-react';
|
import { Train, User, BookOpen, LogIn } from 'lucide-react';
|
||||||
import ThemeToggle from './ThemeToggle';
|
import ThemeToggle from './ThemeToggle';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useAuthStore } from '@/lib/auth-store';
|
import { useAuthStore } from '@/lib/auth-store';
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
export default function AppHeader() {
|
export default function AppHeader() {
|
||||||
const { user, isAuthenticated, initialize } = useAuthStore();
|
const { user, isAuthenticated, initialize } = useAuthStore();
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initialize();
|
initialize();
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export default function DualCalendarPicker({
|
|||||||
}: DualCalendarPickerProps) {
|
}: DualCalendarPickerProps) {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [calendarType, setCalendarType] = useState<'gregorian' | 'ethiopian'>('gregorian');
|
const [calendarType, setCalendarType] = useState<'gregorian' | 'ethiopian'>('gregorian');
|
||||||
const [currentDate, setCurrentDate] = useState(value || new Date());
|
const [, setCurrentDate] = useState(value || new Date());
|
||||||
const [viewMonth, setViewMonth] = useState(value?.getMonth() || new Date().getMonth());
|
const [viewMonth, setViewMonth] = useState(value?.getMonth() || new Date().getMonth());
|
||||||
const [viewYear, setViewYear] = useState(value?.getFullYear() || new Date().getFullYear());
|
const [viewYear, setViewYear] = useState(value?.getFullYear() || new Date().getFullYear());
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
import type { Passenger } from "@edr/types";
|
|
||||||
import { Table, type TableColumn } from "@edr/ui-common";
|
import { Table, type TableColumn } from "@edr/ui-common";
|
||||||
|
|
||||||
export interface ScheduleTableProps {
|
interface ISchedule {
|
||||||
schedules: Passenger.ISchedule[];
|
id: string;
|
||||||
|
trainCode: string;
|
||||||
|
status: string;
|
||||||
|
departureTime: string;
|
||||||
|
arrivalTime: string;
|
||||||
|
basePrice: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns: TableColumn<Passenger.ISchedule>[] = [
|
export interface ScheduleTableProps {
|
||||||
|
schedules: ISchedule[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns: TableColumn<ISchedule>[] = [
|
||||||
{ key: "trainCode", header: "Train" },
|
{ key: "trainCode", header: "Train" },
|
||||||
{ key: "status", header: "Status" },
|
{ key: "status", header: "Status" },
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ export function ethiopianToGregorian(ethDate: EthiopianDate): Date {
|
|||||||
/**
|
/**
|
||||||
* Get day of year from date (1-366)
|
* Get day of year from date (1-366)
|
||||||
*/
|
*/
|
||||||
function getDayOfYear(date: Date): number {
|
export function getDayOfYear(date: Date): number {
|
||||||
const start = new Date(date.getFullYear(), 0, 0);
|
const start = new Date(date.getFullYear(), 0, 0);
|
||||||
const diff = date.getTime() - start.getTime();
|
const diff = date.getTime() - start.getTime();
|
||||||
const oneDay = 1000 * 60 * 60 * 24;
|
const oneDay = 1000 * 60 * 60 * 24;
|
||||||
@@ -109,7 +109,7 @@ function getDayOfYear(date: Date): number {
|
|||||||
/**
|
/**
|
||||||
* Convert day of year to Date object
|
* Convert day of year to Date object
|
||||||
*/
|
*/
|
||||||
function dayOfYearToDate(year: number, dayOfYear: number): Date {
|
export function dayOfYearToDate(year: number, dayOfYear: number): Date {
|
||||||
const date = new Date(year, 0);
|
const date = new Date(year, 0);
|
||||||
date.setDate(dayOfYear);
|
date.setDate(dayOfYear);
|
||||||
return date;
|
return date;
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
import { StrictMode } from "react";
|
|
||||||
import { createRoot } from "react-dom/client";
|
|
||||||
import { BrowserRouter } from "react-router-dom";
|
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
||||||
|
|
||||||
import App from "./App";
|
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
|
||||||
|
|
||||||
createRoot(document.getElementById("root")!).render(
|
|
||||||
<StrictMode>
|
|
||||||
<QueryClientProvider client={queryClient}>
|
|
||||||
<BrowserRouter>
|
|
||||||
<App />
|
|
||||||
</BrowserRouter>
|
|
||||||
</QueryClientProvider>
|
|
||||||
</StrictMode>,
|
|
||||||
);
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
const DashboardPage = () => (
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<h1 className="text-2xl font-semibold text-gray-900">
|
|
||||||
Passenger Dashboard
|
|
||||||
</h1>
|
|
||||||
<p className="text-sm text-gray-600">
|
|
||||||
Daily ridership, ticket sales, occupancy by class, and schedule status go
|
|
||||||
here.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default DashboardPage;
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
const PassengersPage = () => (
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<h1 className="text-2xl font-semibold text-gray-900">Passengers</h1>
|
|
||||||
<p className="text-sm text-gray-600">
|
|
||||||
Passenger directory. Connect to <code>/passengers</code> when ready.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default PassengersPage;
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { useParams } from "react-router-dom";
|
|
||||||
|
|
||||||
import { useSchedule } from "../../hooks/useSchedules";
|
|
||||||
|
|
||||||
const ScheduleDetailPage = () => {
|
|
||||||
const { id } = useParams<{ id: string }>();
|
|
||||||
const { data, isLoading } = useSchedule(id ?? "");
|
|
||||||
|
|
||||||
if (isLoading) return <div className="text-sm text-gray-500">Loading…</div>;
|
|
||||||
if (!data)
|
|
||||||
return <div className="text-sm text-red-600">Schedule not found.</div>;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
<h1 className="text-2xl font-semibold text-gray-900">
|
|
||||||
Schedule {data.trainCode}
|
|
||||||
</h1>
|
|
||||||
<dl className="grid grid-cols-2 gap-2 text-sm">
|
|
||||||
<dt className="text-gray-500">Status</dt>
|
|
||||||
<dd className="text-gray-900">{data.status}</dd>
|
|
||||||
<dt className="text-gray-500">Departure</dt>
|
|
||||||
<dd className="text-gray-900">
|
|
||||||
{new Date(data.departureTime).toLocaleString()}
|
|
||||||
</dd>
|
|
||||||
<dt className="text-gray-500">Arrival</dt>
|
|
||||||
<dd className="text-gray-900">
|
|
||||||
{new Date(data.arrivalTime).toLocaleString()}
|
|
||||||
</dd>
|
|
||||||
<dt className="text-gray-500">Base price</dt>
|
|
||||||
<dd className="text-gray-900">{data.basePrice.toFixed(2)}</dd>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ScheduleDetailPage;
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import ScheduleTable from "../../components/schedules/ScheduleTable";
|
|
||||||
import { useSchedules } from "../../hooks/useSchedules";
|
|
||||||
|
|
||||||
const SchedulesPage = () => {
|
|
||||||
const { data, isLoading } = useSchedules();
|
|
||||||
const items = data ?? [];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<h1 className="text-2xl font-semibold text-gray-900">Schedules</h1>
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="text-sm text-gray-500">Loading…</div>
|
|
||||||
) : (
|
|
||||||
<ScheduleTable schedules={items} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SchedulesPage;
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { Table, type TableColumn } from "@edr/ui-common";
|
|
||||||
import type { Passenger } from "@edr/types";
|
|
||||||
|
|
||||||
import { useStations } from "../../hooks/useStations";
|
|
||||||
|
|
||||||
const columns: TableColumn<Passenger.IStation>[] = [
|
|
||||||
{ key: "code", header: "Code" },
|
|
||||||
{ key: "name", header: "Name" },
|
|
||||||
{ key: "city", header: "City" },
|
|
||||||
{ key: "country", header: "Country" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const StationsPage = () => {
|
|
||||||
const { data, isLoading } = useStations();
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<h1 className="text-2xl font-semibold text-gray-900">Stations</h1>
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="text-sm text-gray-500">Loading…</div>
|
|
||||||
) : (
|
|
||||||
<Table
|
|
||||||
columns={columns}
|
|
||||||
data={data ?? []}
|
|
||||||
rowKey={(row) => row.id}
|
|
||||||
emptyMessage="No stations"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default StationsPage;
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
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 (
|
|
||||||
<div className="max-w-lg">
|
|
||||||
<h1 className="mb-4 text-2xl font-semibold text-gray-900">
|
|
||||||
Issue ticket
|
|
||||||
</h1>
|
|
||||||
<TicketForm
|
|
||||||
onSubmit={mutation.mutate}
|
|
||||||
isSubmitting={mutation.isPending}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BookTicketPage;
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
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 <div className="text-sm text-gray-500">Loading…</div>;
|
|
||||||
if (!data)
|
|
||||||
return <div className="text-sm text-red-600">Ticket not found.</div>;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
<h1 className="text-2xl font-semibold text-gray-900">
|
|
||||||
Ticket {data.reference}
|
|
||||||
</h1>
|
|
||||||
<dl className="grid grid-cols-2 gap-2 text-sm">
|
|
||||||
<dt className="text-gray-500">Passenger ID</dt>
|
|
||||||
<dd className="text-gray-900">{data.passengerId}</dd>
|
|
||||||
<dt className="text-gray-500">Schedule ID</dt>
|
|
||||||
<dd className="text-gray-900">{data.scheduleId}</dd>
|
|
||||||
<dt className="text-gray-500">Seat ID</dt>
|
|
||||||
<dd className="text-gray-900">{data.seatId}</dd>
|
|
||||||
<dt className="text-gray-500">Status</dt>
|
|
||||||
<dd className="text-gray-900">{data.status}</dd>
|
|
||||||
<dt className="text-gray-500">Issued</dt>
|
|
||||||
<dd className="text-gray-900">
|
|
||||||
{new Date(data.issuedAt).toLocaleString()}
|
|
||||||
</dd>
|
|
||||||
<dt className="text-gray-500">Price paid</dt>
|
|
||||||
<dd className="text-gray-900">{data.pricePaid.toFixed(2)}</dd>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TicketDetailPage;
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
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 (
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h1 className="text-2xl font-semibold text-gray-900">Tickets</h1>
|
|
||||||
<Link to="/tickets/new">
|
|
||||||
<Button>Issue ticket</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="text-sm text-gray-500">Loading…</div>
|
|
||||||
) : (
|
|
||||||
<TicketTable tickets={items} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TicketsPage;
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
export const api = axios.create({
|
export const api = axios.create({
|
||||||
baseURL: import.meta.env.VITE_API_URL,
|
baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000',
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: integrate @edr/auth — add a request interceptor here that attaches
|
// TODO: integrate @edr/auth — add a request interceptor here that attaches
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
/// <reference types="vite/client" />
|
|
||||||
|
|
||||||
interface ImportMetaEnv {
|
|
||||||
readonly VITE_API_URL: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ImportMeta {
|
|
||||||
readonly env: ImportMetaEnv;
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "@edr/tsconfig/react.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
||||||
"useDefineForClassFields": true,
|
|
||||||
"skipLibCheck": true
|
|
||||||
},
|
|
||||||
"include": ["src"]
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"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"]
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
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,
|
|
||||||
// },
|
|
||||||
});
|
|
||||||
5
pnpm-lock.yaml
generated
5
pnpm-lock.yaml
generated
@@ -119,10 +119,7 @@ importers:
|
|||||||
version: 11.1.0(chokidar@4.0.3)(prettier@3.8.3)(typescript@5.9.3)
|
version: 11.1.0(chokidar@4.0.3)(prettier@3.8.3)(typescript@5.9.3)
|
||||||
'@nestjs/testing':
|
'@nestjs/testing':
|
||||||
specifier: ^11.1.19
|
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/microservices@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/platform-express@11.1.23)
|
||||||
'@prisma/client':
|
|
||||||
specifier: ^6.19.3
|
|
||||||
version: 6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3)
|
|
||||||
'@types/bcrypt':
|
'@types/bcrypt':
|
||||||
specifier: ^5.0.2
|
specifier: ^5.0.2
|
||||||
version: 5.0.2
|
version: 5.0.2
|
||||||
|
|||||||
Reference in New Issue
Block a user