mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Merge branch 'dev' of https://github.com/Tria-plc/edr-platform into feature/group-booking
This commit is contained in:
@@ -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' })
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -200,7 +200,7 @@ export class SchedulesService {
|
||||
liveStatus: { select: { delayMinutes: true } },
|
||||
_count: { select: { coachAssignments: true, bookings: true } },
|
||||
},
|
||||
orderBy: { departureAt: 'asc' },
|
||||
orderBy: { departureAt: 'desc' },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user