mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 13:10:56 +00:00
Merge pull request #466 from Tria-plc/alpha
Build issue resolution and production checklist updates
This commit is contained in:
@@ -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,
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -59,9 +59,7 @@ export default function PaymentPage() {
|
||||
queryKey: ['bookingAmount', bookingId, amountCurrency, selectedMethod],
|
||||
queryFn: async () => {
|
||||
const url = `/payments/booking-amount?bookingId=${bookingId}¤cy=${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);
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user