import { Body, Controller, Delete, Get, Param, Post, Patch, Query, UseGuards } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiBearerAuth, ApiParam, ApiQuery, ApiResponse } from '@nestjs/swagger'; import { IsPublic } from '@tria-plc/api-common/modules/auth/decorators/public.decorator'; import { SeatsService } from './seats.service'; import { HoldSeatsDto } from './seats.dto'; import { JwtGuard } from '../../common/jwt.guard'; import { IamGuard } from '../../common/iam-adapter'; @ApiTags('Seats') @Controller('seats') export class SeatsController { constructor(private service: SeatsService) {} // ── Seat Map ────────────────────────────────────────────────────────────── @Get('seatmap/:scheduleId') @IsPublic() @ApiOperation({ summary: 'Get seat map with real-time availability by class', description: `Returns seat map for a schedule with availability by seat class: - Economy Regular - Economy Bed - VIP Bed Shows seat status: AVAILABLE, BOOKED, HELD, BLOCKED` }) @ApiParam({ name: 'scheduleId', description: 'TrainSchedule UUID' }) @ApiQuery({ name: 'coachId', required: false, description: 'Filter by coach UUID' }) @ApiResponse({ status: 200, description: 'Returns coaches with seats and seat class info' }) getSeatMap(@Param('scheduleId') scheduleId: string, @Query('coachId') coachId?: string) { return this.service.getSeatMap(scheduleId, coachId); } // ── Hold / Release ──────────────────────────────────────────────────────── @Get('holds') @UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'List active seat holds with full leg context', description: `Returns all non-expired holds enriched with: - **schedule**: train number, departure/arrival, full route origin→destination - **leg**: the specific origin→destination this hold covers (station name, code, stop sequence) - **seats**: seat label, coach, seat class, row, col - **ttlSeconds**: seconds remaining before the hold expires This makes it clear which segment of the route each seat is held for, enabling segment-based reuse of the same seat on non-overlapping legs.`, }) @ApiQuery({ name: 'scheduleId', required: false, description: 'Filter by TrainSchedule UUID' }) @ApiQuery({ name: 'passengerId', required: false, description: 'Filter by Passenger UUID' }) @ApiResponse({ status: 200, description: 'Active holds with schedule, leg, and seat details' }) getHolds( @Query('scheduleId') scheduleId?: string, @Query('passengerId') passengerId?: string, ) { return this.service.getHolds(scheduleId, passengerId); } @Get('holds/:holdId') @UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Get a single hold with full leg context' }) @ApiParam({ name: 'holdId', description: 'SeatHold UUID' }) @ApiResponse({ status: 200, description: 'Hold with schedule, leg, and seat details' }) @ApiResponse({ status: 404, description: 'Hold not found' }) getHold(@Param('holdId') holdId: string) { return this.service.getHold(holdId); } @Post('hold') @ApiOperation({ summary: 'Hold seats for 15 minutes before booking (Public - Guest booking supported)', description: `Temporarily reserves seats for a passenger to complete booking. **Features:** - 15-minute hold duration - Auto-release after expiry - Prevents double booking - Required before creating booking - **Public endpoint** - No authentication required (supports guest booking)` }) @ApiResponse({ status: 201, description: 'Seats held successfully with holdId' }) @ApiResponse({ status: 409, description: 'One or more seats unavailable' }) holdSeats(@Body() dto: HoldSeatsDto) { return this.service.holdSeats(dto); } @Delete('hold/:holdId') @UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Release a seat hold' }) @ApiParam({ name: 'holdId', description: 'Hold UUID' }) @ApiResponse({ status: 200, description: 'Hold released' }) @ApiResponse({ status: 404, description: 'Hold not found' }) releaseHold(@Param('holdId') holdId: string) { return this.service.releaseHold(holdId); } // ── Seat Block / Unblock ─────────────────────────────────────────────────── @Post(':seatId/block') @UseGuards(IamGuard) @ApiBearerAuth('IAM-auth') @ApiOperation({ summary: 'Block a seat (e.g., maintenance, damage)' }) @ApiParam({ name: 'seatId', description: 'Seat UUID' }) @ApiResponse({ status: 200, description: 'Seat blocked' }) blockSeat(@Param('seatId') seatId: string, @Body() body: { reason: string }) { return this.service.blockSeat(seatId, body.reason); } @Delete(':seatId/block') @UseGuards(IamGuard) @ApiBearerAuth('IAM-auth') @ApiOperation({ summary: 'Unblock a seat' }) @ApiParam({ name: 'seatId', description: 'Seat UUID' }) @ApiResponse({ status: 200, description: 'Seat unblocked' }) unblockSeat(@Param('seatId') seatId: string) { return this.service.unblockSeat(seatId); } // ── Remove Seat ──────────────────────────────────────────────────────────── @Patch(':seatId/remove') @UseGuards(IamGuard) @ApiBearerAuth('IAM-auth') @ApiOperation({ summary: 'Remove a seat by marking with negative seatNumber' }) @ApiParam({ name: 'seatId', description: 'Seat UUID' }) @ApiResponse({ status: 200, description: 'Seat removed (seatNumber negated), shows as empty space' }) @ApiResponse({ status: 404, description: 'Seat not found' }) removeSeat(@Param('seatId') seatId: string) { return this.service.removeSeat(seatId); } @Patch(':seatId/undo-remove') @UseGuards(IamGuard) @ApiBearerAuth('IAM-auth') @ApiOperation({ summary: 'Undo seat removal by restoring original seatNumber' }) @ApiParam({ name: 'seatId', description: 'Seat UUID' }) @ApiResponse({ status: 200, description: 'Seat restored (negative seatNumber removed)' }) @ApiResponse({ status: 404, description: 'Seat not found' }) @ApiResponse({ status: 400, description: 'Seat is not removed' }) undoRemoveSeat(@Param('seatId') seatId: string) { return this.service.undoRemoveSeat(seatId); } @Get('export/csv/:scheduleId') @UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Export seats as CSV' }) async exportCSV(@Param('scheduleId') scheduleId: string) { const csv = await this.service.exportSeatsCSV(scheduleId); return { csv, filename: `seats-${scheduleId}.csv` }; } @Post('import/preview') @UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Preview CSV import' }) previewCSV(@Body() body: { csv: string }) { return this.service.previewSeatsCSV(body.csv); } @Post('import/commit') @UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Commit CSV import' }) importCSV(@Body() body: { scheduleId: string; csv: string; commit: boolean }) { return this.service.importSeatsCSV(body.scheduleId, body.csv, body.commit); } }