Merge pull request #466 from Tria-plc/alpha

Build issue resolution and production checklist updates
This commit is contained in:
Stephanos A.
2026-07-05 15:13:21 +03:00
committed by GitHub
13 changed files with 20 additions and 578 deletions

View File

@@ -1,238 +0,0 @@
/**
* SEGMENT-BASED SEAT RESERVATION EXAMPLE
*
* Demonstrates the complete flow for booking Addis Ababa → Dire Dawa
* on the Addis Ababa → Djibouti route with segment-based seat management.
*
* Route: Addis Ababa (seq:1) → Adama (seq:2) → Awash (seq:3) → Dire Dawa (seq:4) → Aysha (seq:5) → Djibouti (seq:6)
* Booking: Addis Ababa → Dire Dawa (segments: 1→2, 2→3, 3→4)
*/
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function exampleBookingFlow() {
console.log('=== SEGMENT-BASED BOOKING FLOW ===\n');
const scheduleId = 'schedule_add_dji_001';
const passengerId = 'passenger_kelemu';
const seatIds = ['seat_coach_a_1a', 'seat_coach_a_1b'];
const originStationId = 'st_ADD';
const destinationStationId = 'st_DRE';
try {
console.log('1. Checking seat availability...');
const segments = await getJourneySegments(scheduleId, originStationId, destinationStationId);
console.log('Journey segments:', segments.map(s => `${s.fromName}${s.toName}`));
console.log('\n2. Holding seats...');
const holdResult = await holdSeatsTransaction(scheduleId, seatIds, passengerId, originStationId, destinationStationId);
console.log('Hold created:', holdResult);
console.log('\n3. Processing payment...');
await new Promise(resolve => setTimeout(resolve, 5000));
console.log('\n4. Confirming booking...');
const bookingId = 'booking_' + Date.now();
const confirmResult = await confirmBookingTransaction(holdResult.holdId, bookingId, segments);
console.log('Booking confirmed:', confirmResult);
console.log('\n5. Simulating trip progress...');
await simulateTripProgress(scheduleId, segments);
} catch (error) {
console.error('Booking flow error:', error);
}
}
async function getJourneySegments(scheduleId: string, originStationId: string, destinationStationId: string) {
const stopTimes = await prisma.tripStopTime.findMany({
where: { scheduleId },
include: { station: true },
orderBy: { sequence: 'asc' },
});
const originStop = stopTimes.find(st => st.stationId === originStationId);
const destinationStop = stopTimes.find(st => st.stationId === destinationStationId);
if (!originStop || !destinationStop || originStop.sequence >= destinationStop.sequence) {
throw new Error('Invalid origin/destination');
}
const segments = [];
for (let i = originStop.sequence; i < destinationStop.sequence; i++) {
const fromStop = stopTimes.find(st => st.sequence === i);
const toStop = stopTimes.find(st => st.sequence === i + 1);
if (fromStop && toStop) {
segments.push({
fromStationId: fromStop.stationId,
toStationId: toStop.stationId,
fromSequence: fromStop.sequence,
toSequence: toStop.sequence,
fromName: fromStop.station.name,
toName: toStop.station.name,
});
}
}
return segments;
}
async function holdSeatsTransaction(scheduleId: string, seatIds: string[], passengerId: string, originStationId: string, destinationStationId: string) {
return prisma.$transaction(async (tx) => {
console.log(' → Starting seat hold transaction...');
const seats = await tx.seat.findMany({ where: { id: { in: seatIds } }, include: { coach: true } });
if (seats.length !== seatIds.length) throw new Error('Some seats not found');
for (const seat of seats) {
if (seat.status !== 'AVAILABLE') {
throw new Error(`Seat ${seat.seatNumber} is not available (status: ${seat.status})`);
}
}
const expiresAt = new Date(Date.now() + 10 * 60 * 1000);
const seatHold = await tx.seatHold.create({
data: { scheduleId, seatIds, passengerId, expiresAt },
});
await tx.seat.updateMany({ where: { id: { in: seatIds } }, data: { status: 'HELD', heldUntil: expiresAt } });
console.log(' → Seats held successfully');
return { holdId: seatHold.id, expiresAt, seats: seatIds.length };
});
}
async function confirmBookingTransaction(holdId: string, bookingId: string, segments: any[]) {
return prisma.$transaction(async (tx) => {
console.log(' → Starting booking confirmation transaction...');
const hold = await tx.seatHold.findUnique({ where: { id: holdId } });
if (!hold || hold.expiresAt < new Date()) throw new Error('Hold expired or not found');
const booking = await tx.booking.create({
data: {
id: bookingId,
bookingRef: 'BK' + Date.now().toString().slice(-6),
passengerId: hold.passengerId,
scheduleId: hold.scheduleId,
status: 'CONFIRMED',
totalMinor: 45000,
currency: 'ETB',
},
});
const journey = await tx.journey.create({
data: { passengerId: hold.passengerId, status: 'CONFIRMED', totalMinor: 45000, currency: 'ETB' },
});
for (const seatId of hold.seatIds) {
for (let i = 0; i < segments.length; i++) {
await tx.journeySegment.create({
data: {
journeyId: journey.id,
scheduleId: hold.scheduleId,
segmentOrder: i + 1,
seatId,
departureStationId: segments[i].fromStationId,
arrivalStationId: segments[i].toStationId,
},
});
}
}
for (const seatId of hold.seatIds) {
await tx.bookingSeat.create({ data: { bookingId, seatId, passengerName: 'Kelemu Ketsela' } });
}
await tx.seat.updateMany({ where: { id: { in: hold.seatIds } }, data: { status: 'BOOKED', heldUntil: null } });
await tx.seatHold.delete({ where: { id: holdId } });
console.log(' → Booking confirmed successfully');
return { bookingId, bookingRef: booking.bookingRef, confirmedSeats: hold.seatIds.length, segments: segments.length };
});
}
async function simulateTripProgress(scheduleId: string, bookedSegments: any[]) {
console.log(' → Simulating trip progress...');
for (const segment of bookedSegments) {
console.log(` → Train approaching ${segment.toName}...`);
await prisma.tripLiveStatus.upsert({
where: { scheduleId },
update: { currentLocationLabel: segment.toName, progressPercent: Math.round((segment.toSequence / 4) * 100) },
create: {
scheduleId,
state: 'EN_ROUTE',
currentLocationLabel: segment.toName,
progressPercent: Math.round((segment.toSequence / 4) * 100),
delayMinutes: 0,
},
});
if (segment.toName === 'Dire Dawa') {
console.log(' → Passengers reached destination, releasing seats...');
await releaseSeatsAtStation(scheduleId, segment.toStationId);
}
await new Promise(resolve => setTimeout(resolve, 2000));
}
}
async function releaseSeatsAtStation(scheduleId: string, stationId: string) {
return prisma.$transaction(async (tx) => {
const completedSegments = await tx.journeySegment.findMany({
where: { scheduleId, arrivalStationId: stationId },
include: { journey: { include: { journeySegments: { where: { scheduleId } } } } },
});
const seatsToRelease: string[] = [];
for (const segment of completedSegments) {
const passengerSegments = segment.journey.journeySegments.filter((js: any) => js.seatId === segment.seatId);
const maxOrder = Math.max(...passengerSegments.map((js: any) => js.segmentOrder));
if (segment.segmentOrder === maxOrder) seatsToRelease.push(segment.seatId!);
}
if (seatsToRelease.length > 0) {
await tx.seat.updateMany({ where: { id: { in: seatsToRelease } }, data: { status: 'AVAILABLE' } });
console.log(` → Released ${seatsToRelease.length} seats at station`);
}
return seatsToRelease;
});
}
async function checkOverlappingReservations(tx: any, scheduleId: string, seatId: string, segments: any[]) {
const activeHolds = await tx.seatHold.findMany({
where: { scheduleId, seatIds: { has: seatId }, expiresAt: { gt: new Date() } },
});
const activeBookings = await tx.journeySegment.findMany({
where: {
scheduleId,
seatId,
journey: { status: { in: ['PENDING_PAYMENT', 'CONFIRMED'] } },
},
});
return [...activeHolds, ...activeBookings];
}
if (require.main === module) {
exampleBookingFlow()
.then(() => console.log('\n=== EXAMPLES COMPLETED ==='))
.catch(console.error)
.finally(() => prisma.$disconnect());
}
export {
exampleBookingFlow,
getJourneySegments,
holdSeatsTransaction,
confirmBookingTransaction,
simulateTripProgress,
releaseSeatsAtStation,
checkOverlappingReservations,
};

View File

@@ -165,7 +165,7 @@ export default function AppReleasesPage() {
<div className="flex justify-end gap-2 pt-2 border-t border-muted">
<ActionButton variant="secondary" onClick={() => setFormOpen(false)}>Cancel</ActionButton>
<ActionButton type="submit" isLoading={saveMutation.isPending}>
<ActionButton type="submit" loading={saveMutation.isPending}>
{editing ? 'Save Changes' : 'Create Release'}
</ActionButton>
</div>

View File

@@ -9,6 +9,7 @@ import Badge from '@/components/ui/Badge';
import ActionButton from '@/components/ui/ActionButton';
import ConfirmDialog from '@/components/ui/ConfirmDialog';
import Modal from '@/components/ui/Modal';
import Image from 'next/image';
import { ticketsApi, apiClient, stationsApi, excessBaggageApi } from '@/lib/api';
import { formatDateTime, formatCurrency, formatDateTimeShort } from '@/lib/utils';
import { useAuthStore } from '@/lib/auth-store';
@@ -844,10 +845,13 @@ export default function TicketsPage() {
<SectionHeader title="QR Code" />
<div className="flex justify-center">
<div className="bg-white p-4 rounded-xl border border-muted inline-block">
<img
<Image
src={t.qrCode.startsWith('data:') ? t.qrCode : `data:image/png;base64,${t.qrCode}`}
alt={`QR Code for ${t.ticketNumber}`}
className="w-48 h-48 object-contain"
width={192}
height={192}
className="object-contain"
unoptimized
/>
<p className="text-center text-xs text-muted-foreground mt-2 font-mono">{t.ticketNumber}</p>
</div>

View File

@@ -1,100 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Test Routes API</title>
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
button { padding: 10px 20px; margin: 10px 0; cursor: pointer; }
pre { background: #f4f4f4; padding: 15px; border-radius: 5px; overflow-x: auto; }
.route { border: 1px solid #ddd; padding: 10px; margin: 10px 0; border-radius: 5px; }
</style>
</head>
<body>
<h1>Routes API Test</h1>
<button onclick="fetchRoutes()">Fetch All Routes</button>
<button onclick="deleteRoute()">Delete ADD-DDW Route</button>
<div id="output"></div>
<script>
const API_URL = 'http://localhost:4000';
const TOKEN = localStorage.getItem('token') || 'YOUR_JWT_TOKEN_HERE';
async function fetchRoutes() {
try {
const response = await fetch(`${API_URL}/routes`, {
headers: {
'Authorization': `Bearer ${TOKEN}`
}
});
const data = await response.json();
console.log('Routes response:', data);
const output = document.getElementById('output');
output.innerHTML = '<h2>Routes Found:</h2>';
if (Array.isArray(data)) {
output.innerHTML += `<p>Total routes: ${data.length}</p>`;
data.forEach(route => {
output.innerHTML += `
<div class="route">
<strong>${route.code}</strong> - ${route.name}<br>
<small>ID: ${route.id}</small><br>
<small>Active: ${route.active}</small><br>
<small>Stops: ${route._count?.stops || route.stops?.length || 0}</small>
</div>
`;
});
} else {
output.innerHTML += '<pre>' + JSON.stringify(data, null, 2) + '</pre>';
}
} catch (error) {
document.getElementById('output').innerHTML =
'<p style="color: red;">Error: ' + error.message + '</p>';
console.error('Error:', error);
}
}
async function deleteRoute() {
const code = 'ADD-DDW';
try {
// First fetch to get the route ID
const listResponse = await fetch(`${API_URL}/routes`, {
headers: { 'Authorization': `Bearer ${TOKEN}` }
});
const routes = await listResponse.json();
const route = routes.find(r => r.code === code);
if (!route) {
alert('Route ADD-DDW not found');
return;
}
if (confirm(`Delete route ${route.code} - ${route.name}?`)) {
const response = await fetch(`${API_URL}/routes/${route.id}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${TOKEN}`
}
});
if (response.ok) {
alert('Route deleted successfully');
fetchRoutes();
} else {
const error = await response.json();
alert('Error: ' + JSON.stringify(error));
}
}
} catch (error) {
alert('Error: ' + error.message);
console.error('Error:', error);
}
}
// Auto-fetch on load
fetchRoutes();
</script>
</body>
</html>

View File

@@ -1,132 +0,0 @@
// Test script for Stations CRUD operations
// Run this in the browser console on the backoffice app
async function testStationsCRUD() {
const API_URL = 'http://localhost:4000';
const token = localStorage.getItem('auth_token');
const headers = {
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : ''
};
console.log('🧪 Testing Stations CRUD Operations...\n');
try {
// 1. CREATE - Add a new station
console.log('1⃣ Testing CREATE Station...');
const newStation = {
code: 'TEST',
name: 'Test Station',
city: 'Test City',
countryCode: 'ET',
lat: '9.0320',
lng: '38.7469',
timezone: 'Africa/Addis_Ababa',
isOperational: true
};
const createResponse = await fetch(`${API_URL}/stations`, {
method: 'POST',
headers,
body: JSON.stringify(newStation)
});
if (!createResponse.ok) {
throw new Error(`CREATE failed: ${createResponse.status} ${await createResponse.text()}`);
}
const createdStation = await createResponse.json();
console.log('✅ Station created:', createdStation);
const stationId = createdStation.id || createdStation.data?.id;
if (!stationId) {
throw new Error('No station ID returned from create');
}
// 2. READ - Get the created station
console.log('\n2⃣ Testing READ Station...');
const readResponse = await fetch(`${API_URL}/stations/${stationId}`, {
method: 'GET',
headers
});
if (!readResponse.ok) {
throw new Error(`READ failed: ${readResponse.status}`);
}
const readStation = await readResponse.json();
console.log('✅ Station retrieved:', readStation);
// 3. UPDATE - Modify the station
console.log('\n3⃣ Testing UPDATE Station...');
const updateData = {
name: 'Test Station Updated',
city: 'Test City Updated',
isOperational: false
};
const updateResponse = await fetch(`${API_URL}/stations/${stationId}`, {
method: 'PATCH',
headers,
body: JSON.stringify(updateData)
});
if (!updateResponse.ok) {
throw new Error(`UPDATE failed: ${updateResponse.status} ${await updateResponse.text()}`);
}
const updatedStation = await updateResponse.json();
console.log('✅ Station updated:', updatedStation);
// 4. LIST - Get all stations
console.log('\n4⃣ Testing LIST Stations...');
const listResponse = await fetch(`${API_URL}/stations`, {
method: 'GET',
headers
});
if (!listResponse.ok) {
throw new Error(`LIST failed: ${listResponse.status}`);
}
const stations = await listResponse.json();
console.log('✅ Stations list retrieved:', stations);
// 5. DELETE - Remove the test station
console.log('\n5⃣ Testing DELETE Station...');
const deleteResponse = await fetch(`${API_URL}/stations/${stationId}`, {
method: 'DELETE',
headers
});
if (!deleteResponse.ok) {
throw new Error(`DELETE failed: ${deleteResponse.status} ${await deleteResponse.text()}`);
}
console.log('✅ Station deleted successfully');
// 6. Verify deletion
console.log('\n6⃣ Verifying deletion...');
const verifyResponse = await fetch(`${API_URL}/stations/${stationId}`, {
method: 'GET',
headers
});
if (verifyResponse.status === 404) {
console.log('✅ Station deletion verified (404 Not Found)');
} else {
console.warn('⚠️ Station might still exist');
}
console.log('\n🎉 All tests passed!');
return { success: true, message: 'All CRUD operations working correctly' };
} catch (error) {
console.error('❌ Test failed:', error);
return { success: false, error: error.message };
}
}
// Run the test
testStationsCRUD();

View File

@@ -37,7 +37,6 @@ export default function ConfirmationPage() {
try {
return await apiClient.get(`/bookings/${bookingId}`);
} catch (error) {
console.log('Booking API not available, using local data');
return {
id: bookingId || '',
pnr: pnr || undefined,
@@ -57,13 +56,9 @@ export default function ConfirmationPage() {
// For other payment methods, ticket is generated by the payment webhook after payment completes
apiClient.get(`/bookings/${bookingId}`).then((data: any) => {
if (data?.status === 'CONFIRMED') {
apiClient.post(`/tickets/generate/${bookingId}`).catch((err) => {
console.error('Failed to generate ticket:', err);
});
apiClient.post(`/tickets/generate/${bookingId}`).catch(() => {});
}
}).catch((err) => {
console.error('Failed to fetch booking status:', err);
});
}).catch(() => {});
}
}, [bookingId]);
@@ -142,7 +137,6 @@ export default function ConfirmationPage() {
if (i < passengers.length - 1) await new Promise(r => setTimeout(r, 400));
}
} catch (error) {
console.error('❌ Failed to generate voucher:', error);
alert(`Failed to generate voucher: ${error instanceof Error ? error.message : 'Unknown error'}`);
} finally {
setIsGeneratingVoucher(false);

View File

@@ -34,9 +34,7 @@ function BookingDetailContent() {
queryKey: ['booking-detail', bookingRef],
queryFn: async () => {
if (!bookingRef) throw new Error('No booking reference provided');
console.log('🔍 Fetching Booking:', bookingRef);
const response = await apiClient.get(`/bookings/${bookingRef}`);
console.log('✅ Booking Response:', response);
// Handle wrapped response
return (response as any)?.data || response;
},
@@ -56,7 +54,6 @@ function BookingDetailContent() {
return response;
},
onSuccess: async (data: any) => {
console.log('Payment intent created:', data);
await apiClient.patch(`/bookings/${booking?.id}/confirm`, {
paymentIntentId: data.id,
paymentMethod: selectedPaymentMethod,
@@ -64,7 +61,6 @@ function BookingDetailContent() {
refetch();
},
onError: (error: any) => {
console.error('Payment failed:', error);
alert(error?.response?.data?.message || 'Payment failed. Please try again.');
},
});
@@ -99,11 +95,9 @@ function BookingDetailContent() {
setIsGeneratingVoucher(true);
try {
console.log('📄 Generating voucher for booking:', booking);
const { generateVoucherPDF } = await import('@/lib/generate-voucher');
await generateVoucherPDF(booking as any);
} catch (error) {
console.error('Failed to generate voucher:', error);
alert(`Failed to generate voucher: ${error instanceof Error ? error.message : 'Unknown error'}`);
} finally {
setIsGeneratingVoucher(false);
@@ -148,11 +142,6 @@ function BookingDetailContent() {
const isExpired = booking.status === 'EXPIRED';
const isCancelled = booking.status === 'CANCELLED';
console.log('📊 Booking Status:', booking.status);
console.log('📊 isPendingPayment:', isPendingPayment);
console.log('📊 isConfirmed:', isConfirmed);
console.log('📊 isExpired:', isExpired);
console.log('📊 isCancelled:', isCancelled);
const StatusBadge = () => {
const statusConfig = {

View File

@@ -736,7 +736,6 @@ export default function PassengersPage() {
// Remove code/state from the URL so a refresh doesn't re-trigger
router.replace('/booking/passengers');
} catch (error) {
console.error('Failed to complete Fayda verification:', error);
setVerificationStatus((prev) => ({ ...prev, [targetIndex]: 'error' }));
setFaydaErrors((prev) => ({ ...prev, [targetIndex]: 'Fayda verification failed. Please try again or enter details manually.' }));
} finally {
@@ -768,7 +767,6 @@ export default function PassengersPage() {
try {
// Fetch passenger profile from backend
const passengerData: any = await apiClient.get(`/passengers/me`);
console.log('Fetched passenger data:', passengerData);
if (!passengerData) {
setFormInitialized(true);
@@ -776,7 +774,6 @@ export default function PassengersPage() {
}
// Only populate first passenger
console.log('Setting passenger 0 values');
setValue('passengers.0.name', passengerData?.fullName || user.fullName || '');
setValue('passengers.0.dateOfBirth', passengerData?.dateOfBirth || user.dateOfBirth || '');
if (passengerData?.gender || user.gender) setValue('passengers.0.gender', (passengerData?.gender || user.gender) as any);
@@ -793,7 +790,6 @@ export default function PassengersPage() {
setFormInitialized(true);
} catch (error) {
console.error('Failed to fetch passenger data:', error);
setFormInitialized(true);
}
};
@@ -879,7 +875,6 @@ export default function PassengersPage() {
setFaydaErrors((prev) => ({ ...prev, [index]: 'Fayda verification was not completed. Please try again or enter details manually.' }));
}
} catch (error) {
console.error('Failed to get verification status:', error);
setVerificationStatus((prev) => ({ ...prev, [index]: 'error' }));
setFaydaErrors((prev) => ({ ...prev, [index]: 'Failed to confirm verification status. Please try again or enter details manually.' }));
} finally {
@@ -889,7 +884,6 @@ export default function PassengersPage() {
}
}, 1000);
} catch (error) {
console.error('Failed to start Fayda verification:', error);
setVerificationStatus((prev) => ({ ...prev, [index]: 'error' }));
setFaydaErrors((prev) => ({ ...prev, [index]: 'Failed to start verification. Please try again.' }));
setVerifyingIndex(null);
@@ -918,9 +912,7 @@ export default function PassengersPage() {
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);
}
}
@@ -958,12 +950,10 @@ export default function PassengersPage() {
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);

View File

@@ -59,9 +59,7 @@ export default function PaymentPage() {
queryKey: ['bookingAmount', bookingId, amountCurrency, selectedMethod],
queryFn: async () => {
const url = `/payments/booking-amount?bookingId=${bookingId}&currency=${amountCurrency}`;
console.log('[BookingAmount] Request:', { url, bookingId, currency: amountCurrency, selectedMethod });
const response: any = await apiClient.get(url);
console.log('[BookingAmount] Response:', response);
return response;
},
enabled: !!selectedMethod && !!bookingId,
@@ -115,7 +113,6 @@ export default function PaymentPage() {
router.push("/booking/confirmation");
},
onError: (error: any) => {
console.error("Payment failed:", error);
updateStatus("FAILED");
setPaymentError(
error?.response?.data?.message ||
@@ -160,10 +157,6 @@ export default function PaymentPage() {
// Add a small delay to allow state to be set from previous page
const timer = setTimeout(() => {
if (!bookingId || !pnr) {
console.log(
"Payment page: Missing booking data, redirecting to search",
);
console.log("bookingId:", bookingId, "pnr:", pnr);
router.push("/booking/search");
}
}, 500);

View File

@@ -107,11 +107,9 @@ export default function ResultsPage() {
payload.returnDate = searchData.returnDate;
}
console.log('🚂 Search Request:', JSON.stringify(payload, null, 2));
const response = await apiClient.post('/search', payload) as any;
console.log('✅ Search Response:', JSON.stringify(response, null, 2));
return response;
},

View File

@@ -15,13 +15,11 @@ import { isChild, isFirstChild, formatFare } from '@/utils/fare-utils';
function getPassengerIdFromToken(token: string): string | null {
try {
if (!token) {
console.warn('No token provided');
return null;
}
const parts = token.split('.');
if (parts.length !== 3) {
console.warn('Invalid token format - expected 3 parts, got', parts.length);
return null;
}
@@ -33,21 +31,16 @@ function getPassengerIdFromToken(token: string): string | null {
try {
decoded = JSON.parse(atob(padded));
} catch (e) {
console.error('Failed to parse base64:', e);
return null;
}
console.log('Decoded JWT payload keys:', Object.keys(decoded));
console.log('passengerId from JWT:', decoded.passengerId);
if (!decoded.passengerId) {
console.warn('No passengerId in JWT payload, available keys:', Object.keys(decoded));
return null;
}
return decoded.passengerId;
} catch (error) {
console.error('Error in getPassengerIdFromToken:', error);
return null;
}
}
@@ -149,7 +142,6 @@ export default function ReviewPage() {
setSeatDetails(details);
} catch (error) {
console.error('Failed to fetch seat details:', error);
}
};
@@ -159,9 +151,6 @@ export default function ReviewPage() {
const createBookingMutation = useMutation({
mutationFn: (data: any) => {
const endpoint = isAuthenticated ? '/bookings' : '/bookings/guest';
console.log('=== API REQUEST ===');
console.log('Endpoint:', endpoint);
console.log('Request Data:', JSON.stringify(data, null, 2));
return apiClient.post(endpoint, data);
},
onSuccess: (data: any) => {
@@ -201,7 +190,6 @@ export default function ReviewPage() {
}
if (!searchCriteria?.originStationId || !searchCriteria?.destinationStationId) {
console.error('Missing search criteria');
alert('Missing search criteria. Please start over.');
router.push('/booking/search');
return;
@@ -225,10 +213,8 @@ export default function ReviewPage() {
seatClassId = outboundClassName ? findByName(outboundClassName) : seatClasses[0].id;
returnSeatClassId = returnClassName ? findByName(returnClassName) : seatClasses[0].id;
console.log('Seat class lookup:', { outboundClassName, returnClassName, seatClassId, returnSeatClassId });
}
} catch (err) {
console.error('Failed to fetch seat classes:', err);
}
if (!seatClassId) {
@@ -242,11 +228,9 @@ export default function ReviewPage() {
const token = typeof window !== 'undefined' ? localStorage.getItem('auth_token') : null;
if (!token) {
console.error('No token in localStorage');
throw new Error('Authentication token not found. Please log in again.');
}
console.log('Token found, length:', token.length);
let passengerId = getPassengerIdFromToken(token);
@@ -274,7 +258,6 @@ export default function ReviewPage() {
const me: any = await apiClient.get('/passengers/me');
passengerId = me?.id || me?.passengerId || '';
} catch (err) {
console.error('Failed to resolve passengerId from /passengers/me:', err);
}
}
@@ -379,11 +362,8 @@ export default function ReviewPage() {
localStorage.setItem('deviceId', bookingData.deviceId);
}
console.log('Creating booking with payload:', JSON.stringify(bookingData, null, 2));
console.log('API endpoint:', isAuthenticated ? '/bookings' : '/bookings/guest');
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.');
}
};
@@ -392,29 +372,18 @@ export default function ReviewPage() {
if (isRoundTrip) {
if (!outboundSchedule || !inboundSchedule || !passengers.length) {
if (!createBookingMutation.isPending && !createBookingMutation.isSuccess) {
console.log('Redirecting to search - missing round trip data');
router.push('/booking/search');
}
}
} else {
if (!selectedSchedule || !passengers.length) {
if (!createBookingMutation.isPending && !createBookingMutation.isSuccess) {
console.log('Redirecting to search - missing data');
router.push('/booking/search');
}
}
}
}, [isRoundTrip, selectedSchedule, outboundSchedule, inboundSchedule, passengers.length, createBookingMutation.isPending, createBookingMutation.isSuccess, router]);
if (isRoundTrip && (!outboundSchedule || !inboundSchedule || !passengers.length)) {
return null;
}
if (!isRoundTrip && (!selectedSchedule || !passengers.length)) {
return null;
}
const fetchFareBreakdown = useCallback(async (scheduleId: string, originStationId: string, destinationStationId: string) => {
try {
const seatClasses: any[] = await apiClient.get('/seat-classes');
@@ -445,7 +414,6 @@ export default function ReviewPage() {
const result: any = await apiClient.get(`/search/fare-breakdown?${params}`);
setFareBreakdown(result);
} catch (err) {
console.error('Failed to fetch fare breakdown:', err);
}
}, [passengers, selectedSchedule, outboundSchedule, isRoundTrip, searchCriteria, displayCurrency]);
@@ -456,6 +424,14 @@ export default function ReviewPage() {
fetchFareBreakdown(scheduleId, searchCriteria.originStationId, searchCriteria.destinationStationId);
}, [fetchFareBreakdown, isRoundTrip, outboundSchedule?.id, selectedSchedule?.id, searchCriteria?.originStationId, searchCriteria?.destinationStationId]);
if (isRoundTrip && (!outboundSchedule || !inboundSchedule || !passengers.length)) {
return null;
}
if (!isRoundTrip && (!selectedSchedule || !passengers.length)) {
return null;
}
const total = packageTierPriceMinor ?? fareBreakdown?.totalMinor ?? 0;
// Shared fare sidebar — rendered in right column (desktop) and inline (mobile)

View File

@@ -173,21 +173,11 @@ export default function SeatsPage() {
queryKey: ["seatmap", currentSchedule?.id, coachTypeId, currentJourneyType],
queryFn: async () => {
const endpoint = `/seats/seatmap/${currentSchedule?.id}?coachTypeId=${coachTypeId}&journeyDirection=${journeyDirection}`;
console.log("🪑 Seatmap Request:", {
endpoint,
});
const response = await apiClient.get(endpoint);
console.log("✅ Seatmap Response:", {
endpoint,
fullResponse: response,
dataCoaches: (response as any)?.data?.coaches?.length || 0,
rootCoaches: (response as any)?.coaches?.length || 0,
});
const finalData = (response as any)?.data || response;
console.log("🎯 Final data structure:", finalData);
return finalData;
},
enabled: !!currentSchedule?.id && !!coachTypeId,
@@ -210,7 +200,6 @@ export default function SeatsPage() {
const originId = (scheduleForHold as any)?.originStationId || searchCriteria?.originStationId;
const destinationId = (scheduleForHold as any)?.destinationStationId || searchCriteria?.destinationStationId;
console.log('🎫 Hold request:', { scheduleId: currentSchedule?.id, originId, destinationId, isInbound });
return apiClient.post(`/seats/hold`, {
scheduleId: currentSchedule?.id,
originStationId: originId,
@@ -258,29 +247,10 @@ export default function SeatsPage() {
(seatMapData as any)?.coaches ||
(seatMapData as any)?.data?.coaches ||
[];
console.log("📦 Raw coaches data:", {
fromRoot: (seatMapData as any)?.coaches?.length || 0,
fromData: (seatMapData as any)?.data?.coaches?.length || 0,
using: rawCoaches.length,
hasRooms: rawCoaches.some((c: any) => c.rooms?.length > 0),
sampleRooms: rawCoaches[0]?.rooms?.length || 0,
});
return rawCoaches;
}, [seatMapData]);
const filteredCoaches = useMemo(() => {
console.log("🔍 Filtering coaches:", {
totalCoaches: coaches.length,
selectedSeatClass: currentSchedule?.selectedSeatClass,
coachesData: coaches.map((c: any) => ({
id: c.id,
name: c.name,
label: c.label,
seatClass: c.seatClass,
seatClasses: c.seatClasses,
seatsCount: c.seats?.length || 0,
})),
});
const coachesWithSeats = coaches.filter((c: any) => {
// Bed coaches store occupants in rooms.beds, not seats
@@ -288,12 +258,8 @@ export default function SeatsPage() {
return c.seats && c.seats.length > 0;
});
console.log(
"✅ Returning all coaches with seats/beds:",
coachesWithSeats.length,
);
return coachesWithSeats;
}, [coaches, currentSchedule?.selectedSeatClass]);
}, [coaches]);
const selectedCoachData = useMemo(

View File

@@ -28,6 +28,8 @@ export interface Schedule {
};
originStation?: Station; // For backward compatibility
destinationStation?: Station; // For backward compatibility
originStationId?: string; // For backward compatibility
destinationStationId?: string; // For backward compatibility
departureAt?: string; // API returns this
arrivalAt?: string; // API returns this
departureTime?: string; // For backward compatibility