Passenger apss UI and UX updates

This commit is contained in:
Stephanos A
2026-06-04 08:19:36 +03:00
parent 8a9cc8afff
commit d9ae1c7f76
34 changed files with 1071 additions and 113 deletions

View File

@@ -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')

View File

@@ -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);

View File

@@ -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' })

View File

@@ -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 },

View File

@@ -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);
}
}

View File

@@ -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,
};
}
}

View File

@@ -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);
}
}

View File

@@ -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 };
}
}