diff --git a/apps/edr-passenger-api/prisma/schema.prisma b/apps/edr-passenger-api/prisma/schema.prisma index cee8dfee2..80220f114 100644 --- a/apps/edr-passenger-api/prisma/schema.prisma +++ b/apps/edr-passenger-api/prisma/schema.prisma @@ -437,6 +437,7 @@ model Seat { coach Coach @relation(fields: [coachId], references: [id]) bookingSeats BookingSeat[] blocks SeatBlock[] + ticketSeats TicketSeat[] @@unique([coachId, row, col]) @@unique([coachId, seatNumber]) @@ -627,6 +628,20 @@ model Ticket { validatorId String? booking Booking @relation(fields: [bookingId], references: [id]) validationLogs GateValidationLog[] + seats TicketSeat[] + + @@schema("passenger") +} + +model TicketSeat { + id String @id @default(uuid()) + ticketId String + seatId String + seatIndex Int @default(0) + ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) + seat Seat @relation(fields: [seatId], references: [id]) + @@index([ticketId]) + @@index([seatId]) @@schema("passenger") } diff --git a/apps/edr-passenger-api/src/main.ts b/apps/edr-passenger-api/src/main.ts index d85b17dbe..fe853ef76 100644 --- a/apps/edr-passenger-api/src/main.ts +++ b/apps/edr-passenger-api/src/main.ts @@ -236,6 +236,7 @@ Payment providers send notifications to: .addTag("Support", "FAQ management and live chat support") .addTag("Tickets", "QR ticket generation, PDFs, and gate validation") .addTag("Wallet", "Wallet balance, top-ups, and transaction ledger") + .addTag("Config", "System configuration and settings") //.addServer('http://localhost:4000', 'Development') // .addServer("https://api.edr-platform.com", "Production") .build(); diff --git a/apps/edr-passenger-api/src/modules/bookings/bookings.service.ts b/apps/edr-passenger-api/src/modules/bookings/bookings.service.ts index 7a12efc8c..1fd74de9f 100644 --- a/apps/edr-passenger-api/src/modules/bookings/bookings.service.ts +++ b/apps/edr-passenger-api/src/modules/bookings/bookings.service.ts @@ -106,14 +106,27 @@ export class BookingsService { const { search, status, page = 1, pageSize = 20 } = filters; const skip = (page - 1) * pageSize; - const where: any = { passenger: { user: { devices: { some: { id: deviceId } } } } }; + // Find user with this device ID + const device = await this.prisma.device.findUnique({ + where: { id: deviceId }, + include: { user: { include: { passenger: true } } }, + }).catch(() => null); + + const searchConditions = search ? [ + { bookingRef: { contains: search, mode: 'insensitive' } }, + { schedule: { originStation: { name: { contains: search, mode: 'insensitive' } } } }, + { schedule: { destinationStation: { name: { contains: search, mode: 'insensitive' } } } }, + ] : []; + + const where: any = { + OR: [ + { userAgent: deviceId }, + ...(device?.user?.passenger ? [{ passengerId: device.user.passenger.id }] : []), + ], + }; if (search) { - where.OR = [ - { bookingRef: { contains: search, mode: 'insensitive' } }, - { schedule: { originStation: { name: { contains: search, mode: 'insensitive' } } } }, - { schedule: { destinationStation: { name: { contains: search, mode: 'insensitive' } } } }, - ]; + where.AND = [{ OR: searchConditions }]; } if (status) { @@ -135,13 +148,8 @@ export class BookingsService { this.prisma.booking.count({ where }), ]); - const savedPassengers = await this.prisma.savedPassengerProfile.findMany({ - where: { deviceId }, - orderBy: { createdAt: 'desc' }, - }); - return { - bookings: items.map(booking => ({ + items: items.map(booking => ({ id: booking.id, bookingRef: booking.bookingRef, status: booking.status, @@ -162,17 +170,6 @@ export class BookingsService { paymentIntent: booking.paymentIntent, seatCount: booking.seats.length, })), - savedPassengers: savedPassengers.map(p => ({ - id: p.id, - passengerName: p.passengerName, - dateOfBirth: p.dateOfBirth.toISOString().split('T')[0], - idDocumentType: p.idDocumentType, - passportNumber: p.passportNumber || undefined, - passportCountry: p.passportCountry || undefined, - nationality: p.nationality || undefined, - phone: p.phone || undefined, - email: p.email || undefined, - })), meta: { page, pageSize, @@ -298,7 +295,6 @@ export class BookingsService { passengersData.push({ ...passenger, passengerName, dateOfBirth, category, verifaydaVerified, verifaydaData, nationality }); } - // Use first passenger's nationality for fare lookup (or allow per-passenger pricing) const primaryNationality = passengersData[0]?.nationality; const baseFareMinor = await this.getBaseFare(dto.scheduleId, dto.seatClassId, segmentRoute, fullRoute, primaryNationality); const adultFareMinor = baseFareMinor * adultCount; diff --git a/apps/edr-passenger-api/src/modules/bookings/guest-booking.service.ts b/apps/edr-passenger-api/src/modules/bookings/guest-booking.service.ts index ef66c6be1..93cbda7b0 100644 --- a/apps/edr-passenger-api/src/modules/bookings/guest-booking.service.ts +++ b/apps/edr-passenger-api/src/modules/bookings/guest-booking.service.ts @@ -250,6 +250,7 @@ export class GuestBookingService { displayCurrency, displayTotalMinor, bookingType: 'ONE_WAY', + userAgent: dto.deviceId, // contactEmail: firstPassenger.email, // Temporarily disabled until migration // contactPhone: firstPassenger.phone, // Temporarily disabled until migration seats: { diff --git a/apps/edr-passenger-api/src/modules/payments/payments.service.ts b/apps/edr-passenger-api/src/modules/payments/payments.service.ts index 661c0819e..aee99ed4b 100644 --- a/apps/edr-passenger-api/src/modules/payments/payments.service.ts +++ b/apps/edr-passenger-api/src/modules/payments/payments.service.ts @@ -182,8 +182,6 @@ export class PaymentsService { return this.formatIntentResponse(intent); } - - private formatIntentResponse( intent: Prisma.PaymentIntentGetPayload>, ): InitiateResponseDto { @@ -349,9 +347,31 @@ export class PaymentsService { }); }); - await this.seatsService.confirmSeats(booking.seats.map((s) => s.seatId)); - await this.ticketsService.generate(booking.id); - await this.awardLoyaltyPoints(booking.passengerId, booking.totalMinor, booking.id); + try { + await this.seatsService.confirmSeats(booking.seats.map((s) => s.seatId)); + } catch (err) { + this.logger.error(`Error confirming seats: ${err instanceof Error ? err.message : String(err)}`); + } + + try { + await this.createJourneySegments(booking); + } catch (err) { + this.logger.error(`Error creating journey segments: ${err instanceof Error ? err.message : String(err)}`); + } + + try { + await this.ticketsService.generate(booking.id); + } catch (err) { + this.logger.error(`Error generating ticket: ${err instanceof Error ? err.message : String(err)}`); + throw err; + } + + try { + await this.awardLoyaltyPoints(booking.passengerId, booking.totalMinor, booking.id); + } catch (err) { + this.logger.warn(`Error awarding loyalty points: ${err instanceof Error ? err.message : String(err)}`); + } + this.eventEmitter.emit('payment.succeeded', { booking }); return { alreadyFinalized: false }; } @@ -390,4 +410,47 @@ export class PaymentsService { await this.prisma.loyaltyAccount.update({ where: { passengerId }, data: { pointsBalance: { increment: points }, tier: tier as any } }); await this.prisma.loyaltyLedgerEntry.create({ data: { accountId: account.id, delta: points, reason: 'TRIP_COMPLETED', bookingId, balanceAfter: newBalance } }); } + + private async createJourneySegments(booking: Prisma.BookingGetPayload<{ include: { seats: true } }>) { + const schedule = await this.prisma.trainSchedule.findUnique({ + where: { id: booking.scheduleId }, + include: { stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } } }, + }); + if (!schedule) return; + + const stopTimes = schedule.stopTimes; + if (stopTimes.length < 2) return; + + const originSequence = stopTimes.findIndex(st => st.stationId === schedule.originStationId); + const destSequence = stopTimes.findIndex(st => st.stationId === schedule.destinationStationId); + + if (originSequence < 0 || destSequence < 0 || originSequence >= destSequence) return; + + const journey = await this.prisma.journey.create({ + data: { + passengerId: booking.passengerId, + status: 'CONFIRMED', + totalMinor: booking.totalMinor, + currency: booking.currency, + }, + }); + + const journeySegments = []; + for (const bookingSeat of booking.seats) { + for (let i = originSequence; i < destSequence; i++) { + journeySegments.push({ + journeyId: journey.id, + scheduleId: booking.scheduleId, + segmentOrder: i, + seatId: bookingSeat.seatId, + departureStationId: stopTimes[i].stationId, + arrivalStationId: stopTimes[i + 1].stationId, + }); + } + } + + if (journeySegments.length > 0) { + await this.prisma.journeySegment.createMany({ data: journeySegments }); + } + } } diff --git a/apps/edr-passenger-api/src/modules/seats/seats.service.ts b/apps/edr-passenger-api/src/modules/seats/seats.service.ts index 6abd4e11a..c625fada6 100644 --- a/apps/edr-passenger-api/src/modules/seats/seats.service.ts +++ b/apps/edr-passenger-api/src/modules/seats/seats.service.ts @@ -353,9 +353,13 @@ export class SeatsService { // No-op for status — availability is segment-scoped via JourneySegment // seat.status = BLOCKED is the only hard gate; BOOKED is not used as a booking flag } + async releaseSeats(seatIds: string[]) { - // Only reset seats that are physically BLOCKED back to AVAILABLE if needed - // For segment-based bookings, releasing is handled by JourneySegment deletion + if (seatIds.length > 0) { + await this.prisma.journeySegment.deleteMany({ + where: { seatId: { in: seatIds } }, + }); + } } async autoAssignSeats(scheduleId: string, count: number, seatClassName: string, eligibility?: string): Promise { @@ -480,6 +484,9 @@ export class SeatsService { @Cron(CronExpression.EVERY_MINUTE) async expireHolds() { const expired = await this.prisma.seatHold.findMany({ where: { expiresAt: { lt: new Date() } } }); - for (const hold of expired) { await this.releaseSeats(hold.seatIds); await this.prisma.seatHold.delete({ where: { id: hold.id } }); } + for (const hold of expired) { + await this.releaseSeats(hold.seatIds); + await this.prisma.seatHold.delete({ where: { id: hold.id } }); + } } } diff --git a/apps/edr-passenger-api/src/modules/tickets/tickets.controller.ts b/apps/edr-passenger-api/src/modules/tickets/tickets.controller.ts index 881b95e0a..5711355e0 100644 --- a/apps/edr-passenger-api/src/modules/tickets/tickets.controller.ts +++ b/apps/edr-passenger-api/src/modules/tickets/tickets.controller.ts @@ -1,16 +1,34 @@ -import { Body, Controller, Get, Param, Post, Query, UseGuards, Delete } from '@nestjs/common'; +import { Body, Controller, Get, Param, Post, Query, UseGuards, Delete, Patch } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; import { TicketsService } from './tickets.service'; import { JwtGuard } from '../../common/jwt.guard'; @ApiTags('Tickets') @Controller('tickets') -@UseGuards(JwtGuard) -@ApiBearerAuth('JWT-auth') export class TicketsController { constructor(private service: TicketsService) {} + @Post('generate/:bookingId') + @ApiOperation({ + summary: 'Generate ticket for booking (confirmation page)', + description: 'Creates a ticket when confirmation page is reached and permanently holds all associated seats with SeatBlock records.' + }) + generateTicket(@Param('bookingId') bookingId: string) { + return this.service.generate(bookingId); + } + + @Patch('update-seats/:bookingId') + @ApiOperation({ + summary: 'Update ticket seats before final confirmation', + description: 'Allows users to change selected seats after ticket generation. Removes old seat blocks and creates new ones for updated seats.' + }) + updateSeats(@Param('bookingId') bookingId: string, @Body() body: { seatIds: string[] }) { + return this.service.updateSeats(bookingId, body.seatIds); + } + @Get() + @UseGuards(JwtGuard) + @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'List all tickets with optional filters' }) listTickets( @Query('search') search?: string, @@ -27,6 +45,8 @@ export class TicketsController { } @Get(':bookingRef') + @UseGuards(JwtGuard) + @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Get ticket with QR code and passenger details', description: `Returns ticket information including: @@ -42,6 +62,8 @@ export class TicketsController { } @Post(':bookingRef/validate') + @UseGuards(JwtGuard) + @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Validate ticket at gate with audit logging', description: 'Validates ticket QR/barcode at station gate. Records validation in audit log with timestamp, gate, and validator.' @@ -55,27 +77,35 @@ export class TicketsController { } @Get(':ticketId/validation-logs') + @UseGuards(JwtGuard) + @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Get validation logs for ticket' }) getValidationLogs(@Param('ticketId') ticketId: string) { return this.service.getValidationLogs(ticketId); } @Get('offline/export') + @UseGuards(JwtGuard) + @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Export tickets for offline validation' }) exportOfflineData(@Query('scheduleId') scheduleId: string) { return this.service.exportOfflineData(scheduleId); } @Post('validate/offline') + @UseGuards(JwtGuard) + @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Batch import offline validations' }) validateOfflineBatch(@Body() body: { validations: any[] }) { return this.service.validateOfflineBatch(body.validations); } @Delete(':id') + @UseGuards(JwtGuard) + @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Delete ticket (admin only)', - description: 'Permanently deletes a ticket record' + description: 'Permanently deletes a ticket record and removes associated seat blocks' }) delete(@Param('id') id: string) { return this.service.delete(id); diff --git a/apps/edr-passenger-api/src/modules/tickets/tickets.module.ts b/apps/edr-passenger-api/src/modules/tickets/tickets.module.ts index f1c01ab9d..cd02974d3 100644 --- a/apps/edr-passenger-api/src/modules/tickets/tickets.module.ts +++ b/apps/edr-passenger-api/src/modules/tickets/tickets.module.ts @@ -1,6 +1,13 @@ import { Module } from '@nestjs/common'; import { TicketsController } from './tickets.controller'; import { TicketsService } from './tickets.service'; +import { JwtGuard } from '../../common/jwt.guard'; -@Module({ controllers: [TicketsController], providers: [TicketsService], exports: [TicketsService] }) +@Module({ + controllers: [TicketsController], + providers: [TicketsService, JwtGuard], + exports: [TicketsService, JwtGuard], +}) export class TicketsModule {} + +export { TicketsController } from './tickets.controller'; diff --git a/apps/edr-passenger-api/src/modules/tickets/tickets.service.ts b/apps/edr-passenger-api/src/modules/tickets/tickets.service.ts index 54e994765..71b65899c 100644 --- a/apps/edr-passenger-api/src/modules/tickets/tickets.service.ts +++ b/apps/edr-passenger-api/src/modules/tickets/tickets.service.ts @@ -19,6 +19,7 @@ export class TicketsService { where.OR = [ { bookingRef: { contains: filters.search, mode: 'insensitive' } }, { barcodePayload: { contains: filters.search, mode: 'insensitive' } }, + { booking: { bookingRef: { contains: filters.search, mode: 'insensitive' } } }, ]; } if (filters.status) { @@ -44,7 +45,13 @@ export class TicketsService { items: tickets.map((t) => ({ id: t.id, ticketNumber: t.barcodePayload, - booking: t.booking, + bookingRef: t.bookingRef, + booking: { + bookingRef: t.booking.bookingRef, + status: t.booking.status, + passenger: t.booking.passenger?.user || { fullName: 'Guest', email: t.booking.contactEmail }, + contactEmail: t.booking.contactEmail, + }, schedule: t.booking.schedule, seat: t.booking.seats[0]?.seat, status: t.booking.status, @@ -58,18 +65,90 @@ export class TicketsService { } async generate(bookingId: string) { + if (!bookingId) { + throw new BadRequestException('Booking ID is required'); + } + const booking = await this.prisma.booking.findUnique({ where: { id: bookingId }, - include: { schedule: { include: { originStation: true, destinationStation: true, train: true } }, seats: { include: { seat: { include: { coach: true } } } } }, + include: { + schedule: { include: { originStation: true, destinationStation: true, train: true } }, + seats: { include: { seat: { include: { coach: true } } } } + }, }); - if (!booking) throw new NotFoundException('Booking not found'); + if (!booking) throw new NotFoundException(`Booking ${bookingId} not found`); + const qrPayload = await QRCode.toDataURL(`edr:tkt:${booking.id}:${booking.bookingRef}`); const barcodePayload = `EDR${booking.bookingRef}${booking.id.substring(0, 8).toUpperCase()}`; - return this.prisma.ticket.upsert({ - where: { bookingId }, - update: { qrPayload, barcodePayload }, - create: { bookingId, bookingRef: booking.bookingRef, qrPayload, barcodePayload } + + const ticket = await this.prisma.ticket.upsert({ + where: { bookingId }, + update: { qrPayload, barcodePayload }, + create: { bookingId, bookingRef: booking.bookingRef, qrPayload, barcodePayload }, }); + + // Create permanent seat blocks for all booked seats + const seatIds = booking.seats.map(bs => bs.seatId); + for (const seatId of seatIds) { + await this.prisma.seatBlock.create({ + data: { + seatId, + reason: `Permanently booked in ticket ${ticket.id}`, + blockedBy: 'SYSTEM', + approvedBy: 'SYSTEM', + } + }).catch(() => null); // Ignore if already exists + } + + return ticket; + } + + async updateSeats(bookingId: string, newSeatIds: string[]) { + const booking = await this.prisma.booking.findUnique({ + where: { id: bookingId }, + include: { seats: true, ticket: true }, + }); + if (!booking) throw new NotFoundException(`Booking ${bookingId} not found`); + if (!booking.ticket) throw new BadRequestException('No ticket found for this booking'); + + // Remove old seat blocks + const oldSeatIds = booking.seats.map(bs => bs.seatId); + for (const seatId of oldSeatIds) { + await this.prisma.seatBlock.deleteMany({ + where: { + seatId, + reason: { contains: booking.ticket.id } + } + }); + } + + // Remove old booking seats + await this.prisma.bookingSeat.deleteMany({ where: { bookingId } }); + + // Create new seat blocks + for (const seatId of newSeatIds) { + await this.prisma.seatBlock.create({ + data: { + seatId, + reason: `Permanently booked in ticket ${booking.ticket.id}`, + blockedBy: 'SYSTEM', + approvedBy: 'SYSTEM', + } + }).catch(() => null); + } + + // Create new booking seats (placeholder with minimal data) + for (let i = 0; i < newSeatIds.length; i++) { + await this.prisma.bookingSeat.create({ + data: { + bookingId, + seatId: newSeatIds[i], + passengerName: `Passenger ${i + 1}`, + } + }); + } + + return { success: true, updatedSeats: newSeatIds.length }; } async getByRef(bookingRef: string) { @@ -197,6 +276,14 @@ export class TicketsService { if (!ticket) throw new NotFoundException('Ticket not found'); await this.prisma.gateValidationLog.deleteMany({ where: { ticketId: id } }); + + // Remove seat blocks associated with this ticket + await this.prisma.seatBlock.deleteMany({ + where: { + reason: { contains: id } + } + }); + await this.prisma.ticket.delete({ where: { id } }); return { deleted: true, ticketId: id }; diff --git a/apps/edr-passenger-web/backoffice/src/app/tickets/page.tsx b/apps/edr-passenger-web/backoffice/src/app/tickets/page.tsx index b5d8096a4..5bb7fccb2 100644 --- a/apps/edr-passenger-web/backoffice/src/app/tickets/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/tickets/page.tsx @@ -19,13 +19,9 @@ export default function TicketsPage() { const { data, isLoading, error } = useQuery({ queryKey: ['tickets', filters], - queryFn: () => ticketsApi.getAll(filters), + queryFn: () => ticketsApi.getAll({ ...filters, skip: 0, take: 50 }), }); - if (error) { - console.error('Tickets API Error:', error); - } - const regenerateMutation = useMutation({ mutationFn: ticketsApi.regenerate, onSuccess: () => { diff --git a/apps/edr-passenger-web/backoffice/src/lib/api/index.ts b/apps/edr-passenger-web/backoffice/src/lib/api/index.ts index 4f1f0322b..614fb3e1f 100644 --- a/apps/edr-passenger-web/backoffice/src/lib/api/index.ts +++ b/apps/edr-passenger-web/backoffice/src/lib/api/index.ts @@ -167,7 +167,10 @@ export const paymentsApi = { // Tickets API export const ticketsApi = { getAll: async (params?: any) => { - const query = new URLSearchParams(params as Record).toString(); + const cleanParams = Object.fromEntries( + Object.entries(params || {}).filter(([_, value]) => value !== '' && value !== undefined && value !== null) + ) as Record; + const query = new URLSearchParams(cleanParams).toString(); const response = await apiClient.get(`/tickets${query ? `?${query}` : ''}`); if (response?.data) { return Array.isArray(response.data) ? { items: response.data } : response; diff --git a/apps/edr-passenger-web/portal/src/app/booking/auth-check/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/auth-check/page.tsx index 3f48ec2dd..c1787010c 100644 --- a/apps/edr-passenger-web/portal/src/app/booking/auth-check/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/booking/auth-check/page.tsx @@ -77,7 +77,7 @@ export default function AuthCheckPage() { - @@ -119,7 +119,7 @@ export default function AuthCheckPage() { - diff --git a/apps/edr-passenger-web/portal/src/app/booking/confirmation/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/confirmation/page.tsx index 8addae4de..cbaac5fb3 100644 --- a/apps/edr-passenger-web/portal/src/app/booking/confirmation/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/booking/confirmation/page.tsx @@ -43,6 +43,10 @@ export default function ConfirmationPage() { if (bookingId && !confirmAttempted.current) { confirmAttempted.current = true; confirmMutation.mutate(); + + apiClient.post(`/tickets/generate/${bookingId}`).catch((err) => { + console.error('Failed to generate ticket:', err); + }); } }, [bookingId, confirmMutation]); @@ -167,8 +171,9 @@ export default function ConfirmationPage() {

Your tickets

{passengers.map((passenger, index) => { - const ticketNumber = `TKT-${bookingId?.slice(0, 8).toUpperCase()}-${(index + 1).toString().padStart(2, '0')}`; - const qrData = JSON.stringify({ + const backendTicket = _booking?.ticket || null; + const ticketNumber = backendTicket?.barcodePayload || `TKT-${bookingId?.slice(0, 8).toUpperCase()}-${(index + 1).toString().padStart(2, '0')}`; + const qrData = backendTicket?.qrPayload || JSON.stringify({ pnr, ticketNumber, passengerName: passenger.name, @@ -204,7 +209,7 @@ export default function ConfirmationPage() {

Seat

-

{passenger.seatId ? 'Assigned' : 'Will be assigned'}

+

{passenger.seatNumber || 'Will be assigned'}

diff --git a/apps/edr-passenger-web/portal/src/app/booking/seats/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/seats/page.tsx index 4a40e893d..80b28180c 100644 --- a/apps/edr-passenger-web/portal/src/app/booking/seats/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/booking/seats/page.tsx @@ -113,10 +113,14 @@ export default function SeatsPage() { const handleContinue = async () => { if (selectedSeats.length > 0) { await holdMutation.mutateAsync(selectedSeats); - const updatedPassengers = passengers.map((p, i) => ({ - ...p, - seatId: selectedSeats[i], - })); + const updatedPassengers = passengers.map((p, i) => { + const seatData = seats?.find((s: any) => s.id === selectedSeats[i]); + return { + ...p, + seatId: selectedSeats[i], + seatNumber: seatData?.number || seatData?.label || seatData?.seatNumber || '', + }; + }); setPassengers(updatedPassengers); } router.push('/booking/review'); @@ -139,10 +143,14 @@ export default function SeatsPage() { try { await holdMutation.mutateAsync(autoSelectedSeats); - const updatedPassengers = passengers.map((p, i) => ({ - ...p, - seatId: autoSelectedSeats[i], - })); + const updatedPassengers = passengers.map((p, i) => { + const seatData = availableSeats[i]; + return { + ...p, + seatId: autoSelectedSeats[i], + seatNumber: seatData?.number || seatData?.label || seatData?.seatNumber || '', + }; + }); setPassengers(updatedPassengers); router.push('/booking/review'); } catch (error: any) { diff --git a/apps/edr-passenger-web/portal/src/app/globals.css b/apps/edr-passenger-web/portal/src/app/globals.css index 84d3484db..640db836b 100644 --- a/apps/edr-passenger-web/portal/src/app/globals.css +++ b/apps/edr-passenger-web/portal/src/app/globals.css @@ -10,7 +10,7 @@ @layer components { .btn-primary { - @apply inline-flex items-center gap-2 px-6 py-3 bg-[rgb(20_113_76)] text-white font-semibold rounded-xl transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed shadow-lg hover:shadow-xl transform hover:-translate-y-0.5; + @apply inline-flex items-center justify-center gap-2 px-6 py-3 bg-[rgb(20_113_76)] text-white font-semibold rounded-xl transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed shadow-lg hover:shadow-xl transform hover:-translate-y-0.5; } .btn-primary:hover { diff --git a/apps/edr-passenger-web/portal/src/lib/booking-store.ts b/apps/edr-passenger-web/portal/src/lib/booking-store.ts index d51429bfd..6fdaaa53c 100644 --- a/apps/edr-passenger-web/portal/src/lib/booking-store.ts +++ b/apps/edr-passenger-web/portal/src/lib/booking-store.ts @@ -25,6 +25,7 @@ export interface PassengerDetail { idDocumentType?: string; isPrimaryPassenger: boolean; seatId?: string; + seatNumber?: string; phone?: string; email?: string; gender?: string;