mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 23:28:11 +00:00
Merge branch 'dev' of https://github.com/Tria-plc/edr-platform into alpha
This commit is contained in:
@@ -1,36 +1,64 @@
|
||||
import { Body, Controller, Delete, Get, Param, Post, Patch, Query, UseGuards } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth, ApiParam, ApiQuery, ApiResponse } from '@nestjs/swagger';
|
||||
import { SeatsService } from './seats.service';
|
||||
import { HoldSeatsDto } from './seats.dto';
|
||||
import { JwtGuard } from '../../common/jwt.guard';
|
||||
import { IamGuard } from '../../common/iam-adapter';
|
||||
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')
|
||||
@ApiTags("Seats")
|
||||
@Controller("seats")
|
||||
export class SeatsController {
|
||||
constructor(private service: SeatsService) {}
|
||||
|
||||
// ── Seat Map ──────────────────────────────────────────────────────────────
|
||||
@Get('seatmap/:scheduleId')
|
||||
@Get("seatmap/:scheduleId")
|
||||
@ApiOperation({
|
||||
summary: 'Get seat map filtered by coach type',
|
||||
description: `Returns all coaches of the given coachTypeId assigned to the schedule, each with their full seat list and real-time availability. Origin and destination are derived from the schedule. Omit coachTypeId to get all coaches.`
|
||||
summary: "Get seat map filtered by coach type",
|
||||
description: `Returns all coaches of the given coachTypeId assigned to the schedule, each with their full seat list and real-time availability. Origin and destination are derived from the schedule. Omit coachTypeId to get all coaches.`,
|
||||
})
|
||||
@ApiParam({ name: "scheduleId", description: "TrainSchedule UUID" })
|
||||
@ApiQuery({
|
||||
name: "coachTypeId",
|
||||
required: false,
|
||||
description:
|
||||
"Filter by CoachType UUID — returns all coaches of that type (e.g. all Economy coaches)",
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description:
|
||||
"List of coaches of the given type with their seats and availability",
|
||||
})
|
||||
@ApiParam({ name: 'scheduleId', description: 'TrainSchedule UUID' })
|
||||
@ApiQuery({ name: 'coachTypeId', required: false, description: 'Filter by CoachType UUID — returns all coaches of that type (e.g. all Economy coaches)' })
|
||||
@ApiResponse({ status: 200, description: 'List of coaches of the given type with their seats and availability' })
|
||||
getSeatMap(
|
||||
@Param('scheduleId') scheduleId: string,
|
||||
@Query('coachTypeId') coachTypeId?: string,
|
||||
@Param("scheduleId") scheduleId: string,
|
||||
@Query("coachTypeId") coachTypeId?: string,
|
||||
) {
|
||||
return this.service.getSeatMap(scheduleId, coachTypeId);
|
||||
}
|
||||
|
||||
// ── Hold / Release ────────────────────────────────────────────────────────
|
||||
@Get('holds')
|
||||
@UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth')
|
||||
@Get("holds")
|
||||
@UseGuards(JwtGuard)
|
||||
@ApiBearerAuth("JWT-auth")
|
||||
@ApiOperation({
|
||||
summary: 'List active seat holds with full leg context',
|
||||
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)
|
||||
@@ -39,25 +67,45 @@ export class SeatsController {
|
||||
|
||||
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' })
|
||||
@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); }
|
||||
@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); }
|
||||
@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)',
|
||||
@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:**
|
||||
@@ -65,74 +113,107 @@ This makes it clear which segment of the route each seat is held for, enabling s
|
||||
- Auto-release after expiry
|
||||
- Prevents double booking
|
||||
- Required before creating booking
|
||||
- **Public endpoint** - No authentication required (supports guest 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); }
|
||||
@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); }
|
||||
@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 }) {
|
||||
@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) {
|
||||
@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) {
|
||||
@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) {
|
||||
@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) {
|
||||
@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' })
|
||||
@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 }) {
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user