mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Passenger portal build issue resolution
This commit is contained in:
@@ -1,10 +1,9 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: 'export',
|
||||
reactStrictMode: true,
|
||||
transpilePackages: ['@edr/types', '@edr/ui-common'],
|
||||
images: {
|
||||
unoptimized: true, // Required for static export
|
||||
unoptimized: true,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useRouter } from 'next/navigation';
|
||||
import { useBookingStore } from '@/lib/booking-store';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { apiClient } from '@/lib/api-client';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import { CheckCircle, Download, Share2, Copy, Printer, Mail, Train } from 'lucide-react';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import { format } from 'date-fns';
|
||||
@@ -15,6 +15,7 @@ export default function ConfirmationPage() {
|
||||
const router = useRouter();
|
||||
const { bookingId, pnr, selectedSchedule, passengers, clearBooking } = useBookingStore();
|
||||
const [copied, setCopied] = useState(false);
|
||||
const confirmAttempted = useRef(false);
|
||||
|
||||
const confirmMutation = useMutation({
|
||||
mutationFn: () => apiClient.patch(`/bookings/${bookingId}/confirm`, { status: 'SUCCEEDED' }),
|
||||
@@ -27,7 +28,6 @@ export default function ConfirmationPage() {
|
||||
return await apiClient.get(`/bookings/${bookingId}`);
|
||||
} catch (error) {
|
||||
console.log('Booking API not available, using local data');
|
||||
// Return mock booking data
|
||||
return {
|
||||
id: bookingId,
|
||||
pnr,
|
||||
@@ -40,10 +40,11 @@ export default function ConfirmationPage() {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (bookingId && !confirmMutation.isSuccess && !confirmMutation.isPending) {
|
||||
if (bookingId && !confirmAttempted.current) {
|
||||
confirmAttempted.current = true;
|
||||
confirmMutation.mutate();
|
||||
}
|
||||
}, [bookingId]);
|
||||
}, [bookingId, confirmMutation]);
|
||||
|
||||
const copyPNR = () => {
|
||||
if (pnr) {
|
||||
@@ -54,7 +55,6 @@ export default function ConfirmationPage() {
|
||||
};
|
||||
|
||||
const handleDownloadTickets = () => {
|
||||
// Mock download - in production this would call the API
|
||||
alert('Ticket download will be available soon. Your tickets are displayed below.');
|
||||
};
|
||||
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
// This is the updated onSubmit function for passengers/page.tsx
|
||||
// Replace the existing onSubmit function with this one
|
||||
|
||||
const onSubmit = async (data: FormData) => {
|
||||
setSaving(true);
|
||||
try {
|
||||
let passengerId = '';
|
||||
|
||||
console.log('[Passengers] onSubmit called, isAuthenticated:', isAuthenticated, 'user:', user);
|
||||
|
||||
// For authenticated users, fetch the passenger profile to get the passengerId
|
||||
if (isAuthenticated && user?.id) {
|
||||
try {
|
||||
console.log('[Passengers] Fetching passenger profile from /passengers/me');
|
||||
const passengerProfile: any = await apiClient.get('/passengers/me');
|
||||
console.log('[Passengers] Passenger profile response:', passengerProfile);
|
||||
passengerId = passengerProfile?.id || '';
|
||||
console.log('[Passengers] Extracted passengerId:', passengerId);
|
||||
} catch (error) {
|
||||
console.error('[Passengers] Failed to fetch passenger profile:', error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[Passengers] passengerId before saving:', passengerId);
|
||||
|
||||
const passengerDetails = data.passengers.map((p, i) => ({
|
||||
name: p.name,
|
||||
dateOfBirth: p.dateOfBirth,
|
||||
gender: p.gender,
|
||||
nationality: p.nationality,
|
||||
nationalId: p.nationalId,
|
||||
passportNumber: p.passportNumber,
|
||||
passportCountry: p.passportCountry,
|
||||
phone: p.phone,
|
||||
email: p.email,
|
||||
isPrimaryPassenger: i === 0,
|
||||
passengerId: i === 0 && passengerId ? passengerId : undefined,
|
||||
}));
|
||||
|
||||
const deviceId = typeof window !== 'undefined'
|
||||
? (localStorage.getItem('deviceId') || crypto.randomUUID())
|
||||
: crypto.randomUUID();
|
||||
|
||||
await apiClient.post('/passengers/save-details', {
|
||||
passengers: passengerDetails,
|
||||
userId: user?.id,
|
||||
deviceId,
|
||||
});
|
||||
|
||||
setPassengers(passengerDetails);
|
||||
setCreateAccount(data.createAccount);
|
||||
|
||||
// Save passengerId to booking store for later use
|
||||
if (isAuthenticated && passengerId) {
|
||||
const { setPassengerId } = useBookingStore.getState();
|
||||
setPassengerId(passengerId);
|
||||
console.log('[Passengers] Saved passengerId to booking store:', passengerId);
|
||||
} else {
|
||||
console.warn('[Passengers] Not saving passengerId - isAuthenticated:', isAuthenticated, 'passengerId:', passengerId);
|
||||
}
|
||||
|
||||
// Store in localStorage as additional backup
|
||||
if (typeof window !== 'undefined' && passengerId) {
|
||||
localStorage.setItem('booking_passengerId', passengerId);
|
||||
console.log('[Passengers] Stored passengerId in localStorage:', passengerId);
|
||||
}
|
||||
|
||||
router.push('/booking/seats');
|
||||
} catch (error) {
|
||||
console.error('Failed to save passenger details:', error);
|
||||
alert('Failed to save passenger details. Please try again.');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
@@ -1,58 +0,0 @@
|
||||
const onSubmit = async (data: FormData) => {
|
||||
setSaving(true);
|
||||
try {
|
||||
let passengerId = '';
|
||||
|
||||
// For authenticated users, fetch the passenger profile to get the passengerId
|
||||
if (isAuthenticated && user?.id) {
|
||||
try {
|
||||
const passengerProfile: any = await apiClient.get('/passengers/me');
|
||||
passengerId = passengerProfile?.id || '';
|
||||
console.log('Fetched passengerId:', passengerId);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch passenger profile:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const passengerDetails = data.passengers.map((p, i) => ({
|
||||
name: p.name,
|
||||
dateOfBirth: p.dateOfBirth,
|
||||
gender: p.gender,
|
||||
nationality: p.nationality,
|
||||
nationalId: p.nationalId,
|
||||
passportNumber: p.passportNumber,
|
||||
passportCountry: p.passportCountry,
|
||||
phone: p.phone,
|
||||
email: p.email,
|
||||
isPrimaryPassenger: i === 0,
|
||||
passengerId: i === 0 && passengerId ? passengerId : undefined,
|
||||
}));
|
||||
|
||||
const deviceId = typeof window !== 'undefined'
|
||||
? (localStorage.getItem('deviceId') || crypto.randomUUID())
|
||||
: crypto.randomUUID();
|
||||
|
||||
await apiClient.post('/passengers/save-details', {
|
||||
passengers: passengerDetails,
|
||||
userId: user?.id,
|
||||
deviceId,
|
||||
});
|
||||
|
||||
setPassengers(passengerDetails);
|
||||
setCreateAccount(data.createAccount);
|
||||
|
||||
// Save passengerId to booking store for later use
|
||||
if (isAuthenticated && passengerId) {
|
||||
const { setPassengerId } = useBookingStore.getState();
|
||||
setPassengerId(passengerId);
|
||||
console.log('Saved passengerId to booking store:', passengerId);
|
||||
}
|
||||
|
||||
router.push('/booking/seats');
|
||||
} catch (error) {
|
||||
console.error('Failed to save passenger details:', error);
|
||||
alert('Failed to save passenger details. Please try again.');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
@@ -1,4 +0,0 @@
|
||||
const onSubmit = async (data: FormData) => {
|
||||
setSaving(true);
|
||||
try {
|
||||
const passengerDetails = data.passengers.map((p, i) => ({\n name: p.name,\n dateOfBirth: p.dateOfBirth,\n gender: p.gender,\n nationality: p.nationality,\n nationalId: p.nationalId,\n passportNumber: p.passportNumber,\n passportCountry: p.passportCountry,\n phone: p.phone,\n email: p.email,\n isPrimaryPassenger: i === 0,\n passengerId: i === 0 && isAuthenticated ? user?.passenger?.id : undefined,\n }));\n\n const deviceId = typeof window !== 'undefined'\n ? (localStorage.getItem('deviceId') || crypto.randomUUID())\n : crypto.randomUUID();\n\n await apiClient.post('/passengers/save-details', {\n passengers: passengerDetails,\n userId: user?.id,\n deviceId,\n });\n\n setPassengers(passengerDetails);\n setCreateAccount(data.createAccount);\n \n // Save passengerId from authenticated user to booking store\n if (isAuthenticated && user?.passenger?.id) {\n const { setPassengerId } = useBookingStore.getState();\n setPassengerId(user.passenger.id);\n }\n \n router.push('/booking/seats');\n } catch (error) {\n console.error('Failed to save passenger details:', error);\n alert('Failed to save passenger details. Please try again.');\n } finally {\n setSaving(false);\n }\n };
|
||||
@@ -1,6 +0,0 @@
|
||||
'use client';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
import { useForm, useFieldArray } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
@@ -52,7 +52,6 @@ export default function PassengersPage() {
|
||||
const { user, isAuthenticated, updateUser } = useAuthStore();
|
||||
const [faydaEnabled, setFaydaEnabled] = useState(true);
|
||||
const [verificationStatus, setVerificationStatus] = useState<Record<number, 'success' | 'error'>>({});
|
||||
const [updatingUser, setUpdatingUser] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [formInitialized, setFormInitialized] = useState(false);
|
||||
const [nationalityMismatch, setNationalityMismatch] = useState(false);
|
||||
@@ -309,7 +308,9 @@ export default function PassengersPage() {
|
||||
<button
|
||||
onClick={() => {
|
||||
clearBooking();
|
||||
if (typeof window !== 'undefined') {
|
||||
window.location.href = '/booking/search';
|
||||
}
|
||||
}}
|
||||
className="btn-primary w-full flex items-center justify-center gap-2"
|
||||
>
|
||||
@@ -378,14 +379,9 @@ export default function PassengersPage() {
|
||||
type="button"
|
||||
onClick={() => openFaydaVerification(index)}
|
||||
className="btn-primary flex items-center justify-center gap-2 mx-auto"
|
||||
disabled={updatingUser}
|
||||
>
|
||||
{updatingUser ? (
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
) : (
|
||||
<ExternalLink className="w-5 h-5" />
|
||||
)}
|
||||
{updatingUser ? 'Updating Profile...' : 'Verify with Fayda'}
|
||||
{isLoggedInNotVerified ? 'Verify with Fayda' : 'Verify with Fayda'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
// Save passengerId to booking store for later use
|
||||
if (isAuthenticated && passengerId) {
|
||||
const { setPassengerId } = useBookingStore.getState();
|
||||
setPassengerId(passengerId);
|
||||
console.log('Saved passengerId to booking store:', passengerId);
|
||||
|
||||
// Also save to localStorage as backup
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem('booking_passengerId', passengerId);
|
||||
console.log('Saved passengerId to localStorage:', passengerId);
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
// Key changes needed in passengers/page.tsx onSubmit function:
|
||||
|
||||
const onSubmit = async (data: FormData) => {
|
||||
setSaving(true);
|
||||
try {
|
||||
// Build passenger details - ALWAYS create new records, don't reuse
|
||||
const passengerDetails = data.passengers.map((p, i) => ({
|
||||
name: p.name,
|
||||
dateOfBirth: p.dateOfBirth,
|
||||
gender: p.gender,
|
||||
nationality: p.nationality,
|
||||
nationalId: p.nationalId,
|
||||
passportNumber: p.passportNumber,
|
||||
passportCountry: p.passportCountry,
|
||||
phone: p.phone,
|
||||
email: p.email,
|
||||
isPrimaryPassenger: i === 0,
|
||||
// IMPORTANT: Don't include passengerId here - it's only needed in booking creation
|
||||
}));
|
||||
|
||||
const deviceId = typeof window !== 'undefined'
|
||||
? (localStorage.getItem('deviceId') || crypto.randomUUID())
|
||||
: crypto.randomUUID();
|
||||
|
||||
// Save passenger details (this is for UI reference, not booking creation)
|
||||
await apiClient.post('/passengers/save-details', {
|
||||
passengers: passengerDetails,
|
||||
userId: user?.id,
|
||||
deviceId,
|
||||
});
|
||||
|
||||
// Store in booking store for the next step (seats selection)
|
||||
setPassengers(passengerDetails);
|
||||
setCreateAccount(data.createAccount);
|
||||
|
||||
router.push('/booking/seats');
|
||||
} catch (error) {
|
||||
console.error('Failed to save passenger details:', error);
|
||||
alert('Failed to save passenger details. Please try again.');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
@@ -1,114 +0,0 @@
|
||||
// Key changes needed in review/page.tsx handleConfirm function:
|
||||
|
||||
const handleConfirm = async () => {
|
||||
console.log('handleConfirm called');
|
||||
try {
|
||||
const { searchCriteria } = useBookingStore.getState();
|
||||
|
||||
// Validate required data
|
||||
if (!seatHold?.holdId) {
|
||||
alert('Please select seats before continuing.');
|
||||
router.push('/booking/seats');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!searchCriteria?.originStationId || !searchCriteria?.destinationStationId) {
|
||||
alert('Missing search criteria. Please start over.');
|
||||
router.push('/booking/search');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get seat class
|
||||
let seatClassId = 'default-seat-class-id';
|
||||
try {
|
||||
const seatClasses: any = await apiClient.get('/seat-classes');
|
||||
if (seatClasses && seatClasses.length > 0) {
|
||||
seatClassId = seatClasses[0].id;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch seat classes:', err);
|
||||
}
|
||||
|
||||
let bookingData: any;
|
||||
|
||||
if (isAuthenticated) {
|
||||
// For authenticated users: get passengerId from user profile
|
||||
let passengerId = '';
|
||||
|
||||
try {
|
||||
// Fetch user's passenger profile
|
||||
const passengerProfile: any = await apiClient.get('/passengers/me');
|
||||
passengerId = passengerProfile?.id;
|
||||
console.log('Got passengerId from profile:', passengerId);
|
||||
} catch (error) {
|
||||
console.error('Failed to get passenger profile:', error);
|
||||
throw new Error('Unable to retrieve your passenger profile. Please try again.');
|
||||
}
|
||||
|
||||
if (!passengerId) {
|
||||
throw new Error('Passenger profile not found. Please update your profile and try again.');
|
||||
}
|
||||
|
||||
bookingData = {
|
||||
scheduleId: selectedSchedule?.id || '',
|
||||
holdId: seatHold.holdId,
|
||||
originStationId: searchCriteria.originStationId,
|
||||
destinationStationId: searchCriteria.destinationStationId,
|
||||
seatClassId: seatClassId,
|
||||
displayCurrency: 'ETB',
|
||||
passengerId: passengerId,
|
||||
passengers: passengers.map((p) => {
|
||||
const isEthiopian = p.nationality === 'ETHIOPIAN' || p.nationality === 'Ethiopian';
|
||||
return {
|
||||
seatId: p.seatId || '',
|
||||
passengerName: p.name,
|
||||
dateOfBirth: p.dateOfBirth,
|
||||
idDocumentType: isEthiopian ? 'NATIONAL_ID' : 'PASSPORT',
|
||||
idDocumentNumber: isEthiopian ? (p.nationalId || '') : '',
|
||||
passportNumber: !isEthiopian ? (p.passportNumber || '') : '',
|
||||
passportCountry: !isEthiopian ? (p.passportCountry || '') : '',
|
||||
nationality: p.nationality,
|
||||
};
|
||||
}),
|
||||
};
|
||||
} else {
|
||||
// For guests: send full passenger details
|
||||
bookingData = {
|
||||
scheduleId: selectedSchedule?.id || '',
|
||||
holdId: seatHold.holdId,
|
||||
originStationId: searchCriteria.originStationId,
|
||||
destinationStationId: searchCriteria.destinationStationId,
|
||||
seatClassId: seatClassId,
|
||||
displayCurrency: 'ETB',
|
||||
passengers: passengers.map(p => {
|
||||
const isEthiopian = p.nationality === 'ETHIOPIAN' || p.nationality === 'Ethiopian';
|
||||
return {
|
||||
seatId: p.seatId || '',
|
||||
passengerName: p.name,
|
||||
dateOfBirth: p.dateOfBirth,
|
||||
idDocumentType: isEthiopian ? 'NATIONAL_ID' : 'PASSPORT',
|
||||
idDocumentNumber: isEthiopian ? (p.nationalId || '') : '',
|
||||
passportNumber: !isEthiopian ? (p.passportNumber || '') : '',
|
||||
passportCountry: !isEthiopian ? (p.passportCountry || '') : '',
|
||||
nationality: p.nationality,
|
||||
phone: p.phone || '',
|
||||
email: p.email || '',
|
||||
};
|
||||
}),
|
||||
createAccount: createAccount || false,
|
||||
savePassengerDetails: true,
|
||||
deviceId: typeof window !== 'undefined' ? (localStorage.getItem('deviceId') || `device-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined' && !isAuthenticated && bookingData.deviceId && !localStorage.getItem('deviceId')) {
|
||||
localStorage.setItem('deviceId', bookingData.deviceId);
|
||||
}
|
||||
|
||||
console.log('Creating booking with payload:', bookingData);
|
||||
await createBookingMutation.mutateAsync(bookingData);
|
||||
} catch (error) {
|
||||
console.error('Error in handleConfirm:', error);
|
||||
alert(error instanceof Error ? error.message : 'An unexpected error occurred. Please try again.');
|
||||
}
|
||||
};
|
||||
@@ -77,7 +77,7 @@ export default function SeatsPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const coaches = (seatMapData as any)?.coaches || [];
|
||||
const coaches = useMemo(() => (seatMapData as any)?.coaches || [], [seatMapData]);
|
||||
|
||||
const filteredCoaches = useMemo(() => {
|
||||
return selectedSchedule?.selectedSeatClass
|
||||
@@ -221,7 +221,7 @@ export default function SeatsPage() {
|
||||
) : seats.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
|
||||
<p>No seats available in this coach</p>
|
||||
<p className="text-sm mt-2">Please select a different coach</p>
|
||||
<p className="text-sm mt-2\">Please select a different coach</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -5,7 +5,6 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useAuthStore } from '@/lib/auth-store';
|
||||
import { useBookingStore } from '@/lib/booking-store';
|
||||
import { useState, Suspense } from 'react';
|
||||
import { Train } from 'lucide-react';
|
||||
|
||||
@@ -20,7 +19,6 @@ function LoginContent() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const login = useAuthStore((s) => s.login);
|
||||
const { searchCriteria } = useBookingStore();
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { format, toZonedTime } from 'date-fns-tz';
|
||||
|
||||
const ADDIS_TZ = 'Africa/Addis_Ababa';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
export const formatCurrency = (amount: number, currency: string = 'ETB'): string => {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
@@ -11,16 +9,13 @@ export const formatCurrency = (amount: number, currency: string = 'ETB'): string
|
||||
};
|
||||
|
||||
export const formatDate = (date: string | Date, formatStr: string = 'MMM dd, yyyy'): string => {
|
||||
const zonedDate = toZonedTime(new Date(date), ADDIS_TZ);
|
||||
return format(zonedDate, formatStr, { timeZone: ADDIS_TZ });
|
||||
return format(new Date(date), formatStr);
|
||||
};
|
||||
|
||||
export const formatDateTime = (date: string | Date): string => {
|
||||
const zonedDate = toZonedTime(new Date(date), ADDIS_TZ);
|
||||
return format(zonedDate, 'MMM dd, yyyy HH:mm', { timeZone: ADDIS_TZ });
|
||||
return format(new Date(date), 'MMM dd, yyyy HH:mm');
|
||||
};
|
||||
|
||||
export const formatTime = (date: string | Date): string => {
|
||||
const zonedDate = toZonedTime(new Date(date), ADDIS_TZ);
|
||||
return format(zonedDate, 'HH:mm', { timeZone: ADDIS_TZ });
|
||||
return format(new Date(date), 'HH:mm');
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user