mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 14:08:11 +00:00
Passenger apss UI and UX updates
This commit is contained in:
@@ -138,6 +138,17 @@ export class BookingsController {
|
||||
return this.service.getByRef(ref);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@ApiOperation({
|
||||
summary: 'Update booking details',
|
||||
description: 'Updates booking information for admin/agent operations'
|
||||
})
|
||||
@ApiResponse({ status: 200, description: 'Booking updated successfully' })
|
||||
@ApiResponse({ status: 404, description: 'Booking not found' })
|
||||
update(@Param('id') id: string, @Body() dto: any) {
|
||||
return this.service.update(id, dto);
|
||||
}
|
||||
|
||||
@Patch(':bookingRef/modify')
|
||||
@UseGuards(JwtGuard)
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
@@ -151,6 +162,28 @@ export class BookingsController {
|
||||
return this.service.modify(dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@ApiOperation({
|
||||
summary: 'Delete booking (admin only)',
|
||||
description: 'Permanently deletes a booking record'
|
||||
})
|
||||
@ApiResponse({ status: 200, description: 'Booking deleted successfully' })
|
||||
@ApiResponse({ status: 404, description: 'Booking not found' })
|
||||
delete(@Param('id') id: string) {
|
||||
return this.service.delete(id);
|
||||
}
|
||||
|
||||
@Get(':id/usage')
|
||||
@ApiOperation({
|
||||
summary: 'Check if booking is in use',
|
||||
description: 'Returns list of modules/data that reference this booking'
|
||||
})
|
||||
@ApiResponse({ status: 200, description: 'Usage information retrieved' })
|
||||
@ApiResponse({ status: 404, description: 'Booking not found' })
|
||||
checkUsage(@Param('id') id: string) {
|
||||
return this.service.checkBookingUsage(id);
|
||||
}
|
||||
|
||||
@Delete(':bookingRef')
|
||||
@UseGuards(JwtGuard)
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
|
||||
@@ -366,6 +366,60 @@ export class BookingsService {
|
||||
return { cancelled: true, refundAmount: refundAmount / 100, currency: 'ETB' };
|
||||
}
|
||||
|
||||
async update(id: string, dto: any) {
|
||||
const booking = await this.prisma.booking.findUnique({ where: { id } });
|
||||
if (!booking) throw new NotFoundException('Booking not found');
|
||||
return this.prisma.booking.update({
|
||||
where: { id },
|
||||
data: {
|
||||
status: dto.status || booking.status,
|
||||
totalMinor: dto.totalMinor !== undefined ? dto.totalMinor : booking.totalMinor,
|
||||
displayCurrency: dto.displayCurrency || booking.displayCurrency,
|
||||
displayTotalMinor: dto.displayTotalMinor !== undefined ? dto.displayTotalMinor : booking.displayTotalMinor,
|
||||
},
|
||||
include: {
|
||||
schedule: { include: { originStation: true, destinationStation: true, train: true } },
|
||||
paymentIntent: true,
|
||||
seats: { include: { seat: true } },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async delete(id: string) {
|
||||
const booking = await this.prisma.booking.findUnique({ where: { id }, include: { seats: true } });
|
||||
if (!booking) throw new NotFoundException('Booking not found');
|
||||
|
||||
await this.seatsService.releaseSeats(booking.seats.map((s) => s.seatId));
|
||||
|
||||
await this.prisma.bookingSeat.deleteMany({ where: { bookingId: id } });
|
||||
await this.prisma.booking.delete({ where: { id } });
|
||||
|
||||
return { deleted: true, bookingRef: booking.bookingRef };
|
||||
}
|
||||
|
||||
async checkBookingUsage(id: string) {
|
||||
const booking = await this.prisma.booking.findUnique({ where: { id } });
|
||||
if (!booking) throw new NotFoundException('Booking not found');
|
||||
|
||||
const [ticketCount, paymentIntentCount, modificationsCount, cancellationCount] = await Promise.all([
|
||||
this.prisma.ticket.count({ where: { bookingId: id } }),
|
||||
this.prisma.paymentIntent.count({ where: { bookingId: id } }),
|
||||
this.prisma.bookingModification.count({ where: { bookingId: id } }),
|
||||
this.prisma.bookingCancellation.count({ where: { bookingId: id } }),
|
||||
]);
|
||||
|
||||
const usage = [];
|
||||
if (ticketCount > 0) usage.push('Ticket(s)');
|
||||
if (paymentIntentCount > 0) usage.push('Payment record(s)');
|
||||
if (modificationsCount > 0) usage.push('Modification history');
|
||||
if (cancellationCount > 0) usage.push('Cancellation record(s)');
|
||||
|
||||
return {
|
||||
isInUse: usage.length > 0,
|
||||
affectedModules: usage,
|
||||
};
|
||||
}
|
||||
|
||||
@Cron(CronExpression.EVERY_MINUTE)
|
||||
async expirePendingBookings() {
|
||||
const cutoff = new Date(Date.now() - 20 * 60 * 1000);
|
||||
|
||||
@@ -22,6 +22,14 @@ export class FleetController {
|
||||
@ApiResponse({ status: 201, description: 'Train created' })
|
||||
createTrain(@Body() dto: CreateTrainDto) { return this.service.createTrain(dto); }
|
||||
|
||||
@Patch('trains/:id')
|
||||
@ApiOperation({ summary: 'Update a train service' })
|
||||
@ApiParam({ name: 'id', description: 'Train UUID' })
|
||||
@ApiBody({ type: CreateTrainDto })
|
||||
@ApiResponse({ status: 200, description: 'Train updated' })
|
||||
@ApiResponse({ status: 404, description: 'Train not found' })
|
||||
updateTrain(@Param('id') id: string, @Body() dto: CreateTrainDto) { return this.service.updateTrain(id, dto); }
|
||||
|
||||
@Get('coaches')
|
||||
@ApiOperation({ summary: 'List coaches filtered by status, mode, seat class, or schedule assignment' })
|
||||
@ApiQuery({ name: 'isActive', required: false, type: Boolean, description: 'true = active only, false = inactive only, omit = all' })
|
||||
|
||||
@@ -112,6 +112,12 @@ export class FleetService {
|
||||
|
||||
createTrain(dto: CreateTrainDto) { return this.prisma.train.create({ data: dto }); }
|
||||
|
||||
async updateTrain(id: string, dto: CreateTrainDto) {
|
||||
const train = await this.prisma.train.findUnique({ where: { id } });
|
||||
if (!train) throw new NotFoundException('Train not found');
|
||||
return this.prisma.train.update({ where: { id }, data: dto });
|
||||
}
|
||||
|
||||
async getCoach(id: string) {
|
||||
const coach = await this.prisma.coach.findUnique({
|
||||
where: { id },
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Body, Controller, Get, Param, Post, UseGuards, Query, Request, UnauthorizedException } from '@nestjs/common';
|
||||
import { Body, Controller, Get, Param, Post, UseGuards, Query, Request, UnauthorizedException, Patch, Delete } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth, ApiResponse, ApiQuery } from '@nestjs/swagger';
|
||||
import { PassengersService } from './passengers.service';
|
||||
import { CreateTravelerProfileDto, CreateSavedRouteDto, VerifyFaydaDto, SavePassengersDto, RegisterPassengerDto } from './passengers.dto';
|
||||
@@ -352,4 +352,37 @@ Returns saved passenger details with generated IDs and confirmation.`,
|
||||
getSavedRoutes(@Param('id') id: string) {
|
||||
return this.service.getSavedRoutes(id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@ApiOperation({
|
||||
summary: 'Update passenger details',
|
||||
description: 'Updates passenger information for admin/agent operations'
|
||||
})
|
||||
@ApiResponse({ status: 200, description: 'Passenger updated successfully' })
|
||||
@ApiResponse({ status: 404, description: 'Passenger not found' })
|
||||
updatePassenger(@Param('id') id: string, @Body() dto: any) {
|
||||
return this.service.updatePassenger(id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@ApiOperation({
|
||||
summary: 'Delete passenger (admin only)',
|
||||
description: 'Permanently deletes a passenger record and associated data'
|
||||
})
|
||||
@ApiResponse({ status: 200, description: 'Passenger deleted successfully' })
|
||||
@ApiResponse({ status: 404, description: 'Passenger not found' })
|
||||
deletePassenger(@Param('id') id: string) {
|
||||
return this.service.deletePassenger(id);
|
||||
}
|
||||
|
||||
@Get(':id/usage')
|
||||
@ApiOperation({
|
||||
summary: 'Check if passenger is in use',
|
||||
description: 'Returns list of modules/data that reference this passenger'
|
||||
})
|
||||
@ApiResponse({ status: 200, description: 'Usage information retrieved' })
|
||||
@ApiResponse({ status: 404, description: 'Passenger not found' })
|
||||
checkUsage(@Param('id') id: string) {
|
||||
return this.service.checkPassengerUsage(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,6 +180,28 @@ export class PassengersService {
|
||||
|
||||
getSavedRoutes(passengerId: string) { return this.prisma.savedRoute.findMany({ where: { passengerId }, orderBy: { tripCount: 'desc' } }); }
|
||||
|
||||
async updatePassenger(id: string, dto: any) {
|
||||
const passenger = await this.prisma.passenger.findUnique({ where: { id } });
|
||||
if (!passenger) throw new NotFoundException('Passenger not found');
|
||||
return this.prisma.passenger.update({
|
||||
where: { id },
|
||||
data: {
|
||||
user: {
|
||||
update: {
|
||||
fullName: dto.fullName || undefined,
|
||||
email: dto.email || undefined,
|
||||
phone: dto.phone || undefined,
|
||||
nationality: dto.nationality || undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
user: { select: { fullName: true, email: true, phone: true, nationality: true } },
|
||||
loyalty: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async registerPassenger(dto: RegisterPassengerDto) {
|
||||
const isEthiopian = !!dto.nationalId;
|
||||
const isLoggedIn = !!dto.userId;
|
||||
@@ -270,4 +292,30 @@ export class PassengersService {
|
||||
message: 'Passenger details saved for guest booking',
|
||||
};
|
||||
}
|
||||
|
||||
async deletePassenger(id: string) {
|
||||
const passenger = await this.prisma.passenger.findUnique({ where: { id } });
|
||||
if (!passenger) throw new NotFoundException('Passenger not found');
|
||||
|
||||
await this.prisma.passenger.delete({ where: { id } });
|
||||
return { deleted: true, passengerId: id };
|
||||
}
|
||||
|
||||
async checkPassengerUsage(id: string) {
|
||||
const [bookingCount, loyaltyAccount, walletAccount] = await Promise.all([
|
||||
this.prisma.booking.count({ where: { passengerId: id } }),
|
||||
this.prisma.loyaltyAccount.findUnique({ where: { passengerId: id } }),
|
||||
this.prisma.walletAccount.findUnique({ where: { passengerId: id } }),
|
||||
]);
|
||||
|
||||
const usage = [];
|
||||
if (bookingCount > 0) usage.push(`${bookingCount} booking(s)`);
|
||||
if (loyaltyAccount) usage.push('Loyalty account');
|
||||
if (walletAccount) usage.push('Wallet account');
|
||||
|
||||
return {
|
||||
isInUse: usage.length > 0,
|
||||
affectedModules: usage,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Body, Controller, Get, Param, Post, Query, UseGuards } from '@nestjs/common';
|
||||
import { Body, Controller, Get, Param, Post, Query, UseGuards, Delete } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { TicketsService } from './tickets.service';
|
||||
import { JwtGuard } from '../../common/jwt.guard';
|
||||
@@ -10,6 +10,22 @@ import { JwtGuard } from '../../common/jwt.guard';
|
||||
export class TicketsController {
|
||||
constructor(private service: TicketsService) {}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'List all tickets with optional filters' })
|
||||
listTickets(
|
||||
@Query('search') search?: string,
|
||||
@Query('status') status?: string,
|
||||
@Query('skip') skip?: string,
|
||||
@Query('take') take?: string,
|
||||
) {
|
||||
return this.service.listTickets({
|
||||
search,
|
||||
status,
|
||||
skip: skip ? parseInt(skip) : 0,
|
||||
take: take ? parseInt(take) : 50,
|
||||
});
|
||||
}
|
||||
|
||||
@Get(':bookingRef')
|
||||
@ApiOperation({
|
||||
summary: 'Get ticket with QR code and passenger details',
|
||||
@@ -55,4 +71,13 @@ export class TicketsController {
|
||||
validateOfflineBatch(@Body() body: { validations: any[] }) {
|
||||
return this.service.validateOfflineBatch(body.validations);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@ApiOperation({
|
||||
summary: 'Delete ticket (admin only)',
|
||||
description: 'Permanently deletes a ticket record'
|
||||
})
|
||||
delete(@Param('id') id: string) {
|
||||
return this.service.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,50 @@ interface OfflineValidation {
|
||||
export class TicketsService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
async listTickets(filters: { search?: string; status?: string; skip: number; take: number }) {
|
||||
const where: any = {};
|
||||
if (filters.search) {
|
||||
where.OR = [
|
||||
{ bookingRef: { contains: filters.search, mode: 'insensitive' } },
|
||||
{ barcodePayload: { contains: filters.search, mode: 'insensitive' } },
|
||||
];
|
||||
}
|
||||
if (filters.status) {
|
||||
where.booking = { status: filters.status };
|
||||
}
|
||||
const tickets = await this.prisma.ticket.findMany({
|
||||
where,
|
||||
include: {
|
||||
booking: {
|
||||
include: {
|
||||
schedule: { include: { originStation: true, destinationStation: true, train: true } },
|
||||
seats: { include: { seat: { include: { coach: true } } } },
|
||||
passenger: { include: { user: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
skip: filters.skip,
|
||||
take: filters.take,
|
||||
orderBy: { issuedAt: 'desc' },
|
||||
});
|
||||
const total = await this.prisma.ticket.count({ where });
|
||||
return {
|
||||
items: tickets.map((t) => ({
|
||||
id: t.id,
|
||||
ticketNumber: t.barcodePayload,
|
||||
booking: t.booking,
|
||||
schedule: t.booking.schedule,
|
||||
seat: t.booking.seats[0]?.seat,
|
||||
status: t.booking.status,
|
||||
validatedAt: t.validatedAt,
|
||||
createdAt: t.issuedAt,
|
||||
})),
|
||||
total,
|
||||
skip: filters.skip,
|
||||
take: filters.take,
|
||||
};
|
||||
}
|
||||
|
||||
async generate(bookingId: string) {
|
||||
const booking = await this.prisma.booking.findUnique({
|
||||
where: { id: bookingId },
|
||||
@@ -147,4 +191,14 @@ export class TicketsService {
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
async delete(id: string) {
|
||||
const ticket = await this.prisma.ticket.findUnique({ where: { id } });
|
||||
if (!ticket) throw new NotFoundException('Ticket not found');
|
||||
|
||||
await this.prisma.gateValidationLog.deleteMany({ where: { ticketId: id } });
|
||||
await this.prisma.ticket.delete({ where: { id } });
|
||||
|
||||
return { deleted: true, ticketId: id };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user