Merge branch 'dev' of https://github.com/Tria-plc/edr-platform into feature/group-booking

This commit is contained in:
Roba Boru
2026-08-24 18:05:21 +03:00
324 changed files with 25363 additions and 5666 deletions

View File

@@ -8,6 +8,10 @@ export class LogExcessBaggageDto {
@IsOptional() @IsString() bookingReference?: string;
@ApiPropertyOptional({ example: 'agent-uuid', description: 'Injected from IAM token; optional override' })
@IsOptional() @IsString() agentId?: string;
@ApiPropertyOptional({ example: '+251911223344', description: 'Override the phone the payment link SMS should go to. Defaults to the booking contact phone.' })
@IsOptional() @IsString() contactPhone?: string;
@ApiPropertyOptional({ example: 'passenger@example.com', description: 'Override the email the payment link should also be sent to. Defaults to the booking contact email.' })
@IsOptional() @IsString() contactEmail?: string;
@ApiProperty({ example: 7, description: 'Excess weight in kg above the free allowance' })
@IsInt() @IsPositive() excessWeightKg: number;
@ApiPropertyOptional({ description: 'Collect cash now instead of sending a payment link' })

View File

@@ -115,8 +115,8 @@ export class ExcessBaggageService {
const totalMinor = feePerKgMinor * dto.excessWeightKg;
const expiresAt = new Date(Date.now() + CHARGE_TTL_MS);
const contactPhone = booking.contactPhone ?? booking.passenger?.user?.phone ?? null;
const contactEmail = booking.contactEmail ?? booking.passenger?.user?.email ?? null;
const contactPhone = dto.contactPhone?.trim() || (booking.contactPhone ?? booking.passenger?.user?.phone ?? null);
const contactEmail = dto.contactEmail?.trim() || (booking.contactEmail ?? booking.passenger?.user?.email ?? null);
const status = dto.collectCash ? 'CASH_COLLECTED' : 'PENDING';
const paidAt = dto.collectCash ? new Date() : null;

View File

@@ -229,10 +229,11 @@ export class FleetController {
}
@Get('coaches/utilization')
@ApiOperation({ summary: 'Coach utilization report — seats, bookings, and assignment history per coach' })
@ApiOperation({ summary: 'Coach utilization report — seats, bookings, and assignment history per coach for a selected schedule' })
@ApiQuery({ name: 'scheduleId', required: false, description: 'Optional schedule UUID to scope the utilization report to that schedule.' })
@ApiResponse({ status: 200, description: 'Coach utilization data' })
getCoachUtilization() {
return this.service.getCoachUtilization();
getCoachUtilization(@Query('scheduleId') scheduleId?: string) {
return this.service.getCoachUtilization(scheduleId);
}
@Get('coaches/:id')

View File

@@ -822,12 +822,35 @@ export class FleetService {
};
}
async getCoachUtilization() {
async getCoachUtilization(scheduleId?: string) {
const where = scheduleId ? { scheduleId } : {};
const coaches = await this.prisma.coach.findMany({
where: scheduleId
? {
assignments: {
some: { scheduleId },
},
}
: {},
include: {
coachType: true,
seats: { select: { id: true, status: true } },
seats: {
select: {
id: true,
status: true,
bookingSeats: {
where,
select: { id: true },
},
blocks: {
where,
select: { id: true, reasonCategory: true },
},
},
},
assignments: {
where,
include: {
schedule: {
select: { id: true, departureAt: true, status: true, _count: { select: { bookings: true } } },
@@ -842,10 +865,12 @@ export class FleetService {
return coaches.map((coach) => {
const totalSeats = coach.seats.length;
const bookedSeats = coach.seats.filter((s) => s.status === 'BOOKED').length;
const blockedSeats = coach.seats.filter((s) => s.status === 'BLOCKED').length;
const maintenanceSeats = coach.seats.filter((s) => (s.status as string) === 'UNDER_MAINTENANCE').length;
const availableSeats = coach.seats.filter((s) => s.status === 'AVAILABLE').length;
const bookedSeats = coach.seats.filter((s) => (s.bookingSeats?.length ?? 0) > 0).length;
const blockedSeats = coach.seats.filter((s) => (s.blocks?.length ?? 0) > 0).length;
const maintenanceSeats = coach.seats.filter((s) => (s.blocks ?? []).some((b) => b.reasonCategory === 'MAINTENANCE')).length;
const availableSeats = scheduleId
? Math.max(totalSeats - bookedSeats - blockedSeats - maintenanceSeats, 0)
: coach.seats.filter((s) => s.status === 'AVAILABLE').length;
const totalAssignments = coach.assignments.length;
const totalBookings = coach.assignments.reduce((sum, a) => sum + ((a.schedule as any)._count?.bookings ?? 0), 0);
const utilizationRate = totalSeats > 0 ? +((bookedSeats / totalSeats) * 100).toFixed(2) : 0;

View File

@@ -49,6 +49,8 @@ class CreateSupplementaryChargeDto {
@ApiProperty({ example: 'EDR-20240001', description: 'Booking reference number' }) @IsString() bookingRef: string;
@ApiProperty({ description: 'Amount owed in minor units (e.g. 5000 = 50 ETB)' }) @IsInt() @Min(1) amountMinor: number;
@ApiProperty({ example: 'UNDERPAYMENT' }) @IsString() reason: string;
@ApiPropertyOptional({ description: 'Override the phone the payment link SMS should go to. Falls back to the booking contact phone.', example: '+251911223344' }) @IsOptional() @IsString() contactPhone?: string;
@ApiPropertyOptional({ description: 'Override the email the payment link should also be sent to. Falls back to the booking contact email.', example: 'passenger@example.com' }) @IsOptional() @IsString() contactEmail?: string;
@ApiPropertyOptional() @IsOptional() @IsString() notes?: string;
}

View File

@@ -52,6 +52,8 @@ export class SupplementaryChargesService {
amountMinor: number;
reason: string;
notes?: string;
contactPhone?: string;
contactEmail?: string;
createdBy: string;
}) {
const booking = await this.prisma.booking.findUnique({
@@ -76,8 +78,8 @@ export class SupplementaryChargesService {
},
});
const phone = booking.contactPhone ?? booking.passenger?.user?.phone ?? null;
const email = booking.contactEmail ?? booking.passenger?.user?.email ?? null;
const phone = dto.contactPhone?.trim() || (booking.contactPhone ?? booking.passenger?.user?.phone ?? null);
const email = dto.contactEmail?.trim() || (booking.contactEmail ?? booking.passenger?.user?.email ?? null);
await this.sendLink(charge, booking.bookingRef, phone, email);
await this.auditService.log({

View File

@@ -200,7 +200,7 @@ export class SchedulesService {
liveStatus: { select: { delayMinutes: true } },
_count: { select: { coachAssignments: true, bookings: true } },
},
orderBy: { departureAt: 'asc' },
orderBy: { departureAt: 'desc' },
});
}