mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 03:40:56 +00:00
Package pricing issue resolution, passenger report, tickets filter updates
This commit is contained in:
@@ -320,8 +320,8 @@ export class BookingsService {
|
||||
id: b.id,
|
||||
bookingRef: b.bookingRef,
|
||||
status: b.status,
|
||||
totalMinor: b.totalMinor,
|
||||
currency: b.currency || null,
|
||||
totalMinor: b.displayTotalMinor ?? b.totalMinor,
|
||||
currency: b.displayCurrency ?? b.currency ?? null,
|
||||
displayCurrency: b.displayCurrency ?? null,
|
||||
displayTotalMinor: b.displayTotalMinor ?? null,
|
||||
adultCount: b.adultCount,
|
||||
@@ -578,7 +578,7 @@ export class BookingsService {
|
||||
|
||||
const mappedPkg = pkgItems.map((b: any) => ({
|
||||
id: b.id, bookingRef: b.bookingRef, status: b.status,
|
||||
totalMinor: b.totalMinor, currency: b.currency || b.displayCurrency,
|
||||
totalMinor: b.displayTotalMinor ?? b.totalMinor, currency: b.displayCurrency || b.currency,
|
||||
displayCurrency: b.displayCurrency, displayTotalMinor: b.displayTotalMinor,
|
||||
contactEmail: b.contactEmail, contactPhone: b.contactPhone,
|
||||
bookingType: 'PACKAGE', packageId: b.packageId, priceTierId: b.priceTierId,
|
||||
@@ -732,8 +732,8 @@ export class BookingsService {
|
||||
id: b.id,
|
||||
bookingRef: b.bookingRef,
|
||||
status: b.status,
|
||||
totalMinor: b.totalMinor,
|
||||
currency: b.currency || b.displayCurrency,
|
||||
totalMinor: b.displayTotalMinor ?? b.totalMinor,
|
||||
currency: b.displayCurrency || b.currency,
|
||||
displayCurrency: b.displayCurrency,
|
||||
displayTotalMinor: b.displayTotalMinor,
|
||||
contactEmail: b.contactEmail,
|
||||
@@ -1822,8 +1822,8 @@ export class BookingsService {
|
||||
id: pkgBooking.id,
|
||||
bookingRef: pkgBooking.bookingRef,
|
||||
status: pkgBooking.status,
|
||||
totalMinor: pkgBooking.totalMinor,
|
||||
currency: pkgBooking.currency || pkgBooking.displayCurrency,
|
||||
totalMinor: pkgBooking.displayTotalMinor ?? pkgBooking.totalMinor,
|
||||
currency: pkgBooking.displayCurrency || pkgBooking.currency,
|
||||
adultCount: pkgBooking.passengerCount,
|
||||
childCount: 0,
|
||||
displayCurrency: pkgBooking.displayCurrency,
|
||||
@@ -1852,7 +1852,7 @@ export class BookingsService {
|
||||
fullName: p.passengerName,
|
||||
category: 'ADULT',
|
||||
leg: 1,
|
||||
fareMinor: Math.round(pkgBooking.totalMinor / pkgBooking.passengerCount),
|
||||
fareMinor: Math.round((pkgBooking.displayTotalMinor ?? pkgBooking.totalMinor) / pkgBooking.passengerCount),
|
||||
verifaydaVerified: false,
|
||||
seat: null,
|
||||
})),
|
||||
|
||||
@@ -18,6 +18,12 @@ export class ReportsController {
|
||||
return this.service.generateReport(dto);
|
||||
}
|
||||
|
||||
@Get('occupancy')
|
||||
@ApiOperation({ summary: 'Occupancy report for a specific schedule' })
|
||||
getOccupancyReport(@Query('scheduleId') scheduleId: string) {
|
||||
return this.service.getOccupancyBySchedule(scheduleId);
|
||||
}
|
||||
|
||||
@Get(':reportId')
|
||||
@ApiOperation({ summary: 'Get report by ID' })
|
||||
getReport(@Param('reportId') reportId: string) {
|
||||
|
||||
@@ -191,6 +191,122 @@ export class ReportsService {
|
||||
};
|
||||
}
|
||||
|
||||
async getOccupancyBySchedule(scheduleId: string) {
|
||||
const schedule = await this.prisma.trainSchedule.findUnique({
|
||||
where: { id: scheduleId },
|
||||
include: {
|
||||
originStation: true,
|
||||
destinationStation: true,
|
||||
train: true,
|
||||
coachAssignments: {
|
||||
include: {
|
||||
coach: {
|
||||
include: {
|
||||
coachType: true,
|
||||
seats: { select: { id: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
bookings: {
|
||||
where: { status: { in: ['CONFIRMED', 'BOARDED'] } },
|
||||
include: {
|
||||
seats: {
|
||||
include: {
|
||||
seat: { include: { coach: { include: { coachType: true } } } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } },
|
||||
},
|
||||
});
|
||||
|
||||
if (!schedule) return null;
|
||||
|
||||
const totalSeats = (schedule as any).coachAssignments.reduce((s: number, a: any) => s + a.coach.seats.length, 0);
|
||||
const allBookingSeats = (schedule as any).bookings.flatMap((b: any) => b.seats);
|
||||
const totalPassengers = allBookingSeats.length;
|
||||
const occupancyRate = totalSeats > 0 ? +((totalPassengers / totalSeats) * 100).toFixed(1) : 0;
|
||||
|
||||
// Per-coach breakdown
|
||||
const coachMap = new Map<string, { coachNumber: string; coachType: string; totalSeats: number; booked: number }>();
|
||||
for (const assignment of (schedule as any).coachAssignments) {
|
||||
const c = assignment.coach;
|
||||
coachMap.set(c.id, {
|
||||
coachNumber: c.number,
|
||||
coachType: (c as any).coachType?.name ?? 'Unknown',
|
||||
totalSeats: c.seats.length,
|
||||
booked: 0,
|
||||
});
|
||||
}
|
||||
for (const bs of allBookingSeats) {
|
||||
const coachId = bs.seat?.coachId;
|
||||
if (coachId && coachMap.has(coachId)) coachMap.get(coachId)!.booked++;
|
||||
}
|
||||
const byCoach = [...coachMap.values()].map(c => ({
|
||||
...c,
|
||||
occupancyRate: c.totalSeats > 0 ? +((c.booked / c.totalSeats) * 100).toFixed(1) : 0,
|
||||
}));
|
||||
|
||||
// Per-origin station breakdown (using booking's originStationId)
|
||||
const originMap = new Map<string, { stationName: string; passengers: number }>();
|
||||
for (const booking of (schedule as any).bookings) {
|
||||
const stationId = booking.originStationId ?? schedule.originStationId;
|
||||
const stationName = (schedule as any).stopTimes.find((st: any) => st.stationId === stationId)?.station?.name
|
||||
?? (schedule as any).originStation?.name
|
||||
?? stationId;
|
||||
if (!originMap.has(stationId)) originMap.set(stationId, { stationName, passengers: 0 });
|
||||
originMap.get(stationId)!.passengers += booking.seats.length;
|
||||
}
|
||||
const byOrigin = [...originMap.values()].sort((a, b) => b.passengers - a.passengers);
|
||||
|
||||
// Per-destination station breakdown
|
||||
const destMap = new Map<string, { stationName: string; passengers: number }>();
|
||||
for (const booking of (schedule as any).bookings) {
|
||||
const stationId = booking.destinationStationId ?? schedule.destinationStationId;
|
||||
const stationName = (schedule as any).stopTimes.find((st: any) => st.stationId === stationId)?.station?.name
|
||||
?? (schedule as any).destinationStation?.name
|
||||
?? stationId;
|
||||
if (!destMap.has(stationId)) destMap.set(stationId, { stationName, passengers: 0 });
|
||||
destMap.get(stationId)!.passengers += booking.seats.length;
|
||||
}
|
||||
const byDestination = [...destMap.values()].sort((a, b) => b.passengers - a.passengers);
|
||||
|
||||
// Per-class breakdown
|
||||
const classMap = new Map<string, { className: string; totalSeats: number; booked: number }>();
|
||||
for (const assignment of (schedule as any).coachAssignments) {
|
||||
const typeName = (assignment.coach as any).coachType?.name ?? 'Unknown';
|
||||
if (!classMap.has(typeName)) classMap.set(typeName, { className: typeName, totalSeats: 0, booked: 0 });
|
||||
classMap.get(typeName)!.totalSeats += assignment.coach.seats.length;
|
||||
}
|
||||
for (const bs of allBookingSeats) {
|
||||
const typeName = bs.seat?.coach?.coachType?.name ?? 'Unknown';
|
||||
if (!classMap.has(typeName)) classMap.set(typeName, { className: typeName, totalSeats: 0, booked: 0 });
|
||||
classMap.get(typeName)!.booked++;
|
||||
}
|
||||
const byClass = [...classMap.values()].map(c => ({
|
||||
...c,
|
||||
occupancyRate: c.totalSeats > 0 ? +((c.booked / c.totalSeats) * 100).toFixed(1) : 0,
|
||||
}));
|
||||
|
||||
return {
|
||||
schedule: {
|
||||
id: schedule.id,
|
||||
trainName: (schedule as any).train?.name ?? (schedule as any).train?.number,
|
||||
origin: (schedule as any).originStation?.name,
|
||||
destination: (schedule as any).destinationStation?.name,
|
||||
departureAt: schedule.departureAt,
|
||||
arrivalAt: schedule.arrivalAt,
|
||||
},
|
||||
summary: { totalSeats, totalPassengers, occupancyRate },
|
||||
byCoach,
|
||||
byClass,
|
||||
byOrigin,
|
||||
byDestination,
|
||||
};
|
||||
}
|
||||
|
||||
async getReport(reportId: string) {
|
||||
return this.prisma.operationalReport.findUnique({ where: { id: reportId } });
|
||||
}
|
||||
|
||||
@@ -54,11 +54,16 @@ export class CreateScheduleDto {
|
||||
plannedTimes?: PlannedStopTimeDto[];
|
||||
}
|
||||
|
||||
export class CoachAssignmentDto {
|
||||
@ApiProperty({ example: 'coach-uuid' }) @IsString() coachId: string;
|
||||
@ApiProperty({ example: 1 }) @IsInt() @Min(1) positionNumber: number;
|
||||
}
|
||||
|
||||
export class UpdateScheduleDto {
|
||||
@ApiPropertyOptional({ example: '2026-06-15T08:00:00Z', description: 'Scheduled departure from the first stop (origin)' }) @IsOptional() @IsDateString() departureAt?: string;
|
||||
@ApiPropertyOptional({ example: '2026-06-15T20:00:00Z', description: 'Scheduled arrival at the last stop (destination)' }) @IsOptional() @IsDateString() arrivalAt?: string;
|
||||
@ApiPropertyOptional({ enum: TripStatus, example: TripStatus.SCHEDULED }) @IsOptional() @IsEnum(TripStatus) status?: TripStatus;
|
||||
@ApiPropertyOptional({ type: Array, description: 'List of coaches to assign' }) @IsOptional() @IsArray() coaches?: Array<{ coachId: string; positionNumber: number }>;
|
||||
@ApiPropertyOptional({ type: [CoachAssignmentDto], description: 'List of coaches to assign' }) @IsOptional() @IsArray() @ValidateNested({ each: true }) @Type(() => CoachAssignmentDto) coaches?: CoachAssignmentDto[];
|
||||
@ApiPropertyOptional({ example: false, description: 'Exclude from public search (reserved for packages)' }) @IsOptional() isPackageOnly?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ export class TicketsController {
|
||||
@ApiQuery({ name: 'originStationId', required: false })
|
||||
@ApiQuery({ name: 'destinationStationId', required: false })
|
||||
@ApiQuery({ name: 'arrivalDate', required: false })
|
||||
@ApiQuery({ name: 'departureDate', required: false })
|
||||
@ApiQuery({ name: 'dateFrom', required: false })
|
||||
@ApiQuery({ name: 'dateTo', required: false })
|
||||
@ApiQuery({ name: 'coachId', required: false })
|
||||
@@ -64,6 +65,7 @@ export class TicketsController {
|
||||
@Query('originStationId') originStationId?: string,
|
||||
@Query('destinationStationId') destinationStationId?: string,
|
||||
@Query('arrivalDate') arrivalDate?: string,
|
||||
@Query('departureDate') departureDate?: string,
|
||||
@Query('dateFrom') dateFrom?: string,
|
||||
@Query('dateTo') dateTo?: string,
|
||||
@Query('coachId') coachId?: string,
|
||||
@@ -76,6 +78,7 @@ export class TicketsController {
|
||||
originStationId,
|
||||
destinationStationId,
|
||||
arrivalDate,
|
||||
departureDate,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
coachId,
|
||||
|
||||
@@ -27,7 +27,7 @@ export class TicketsService {
|
||||
@InjectDataSource() private readonly dataSource: DataSource,
|
||||
) {}
|
||||
|
||||
async listTickets(filters: { search?: string; status?: string; originStationId?: string; destinationStationId?: string; arrivalDate?: string; dateFrom?: string; dateTo?: string; coachId?: string; skip: number; take: number }) {
|
||||
async listTickets(filters: { search?: string; status?: string; originStationId?: string; destinationStationId?: string; departureDate?: string; arrivalDate?: string; dateFrom?: string; dateTo?: string; coachId?: string; skip: number; take: number }) {
|
||||
const where: any = {};
|
||||
if (filters.search) {
|
||||
where.OR = [
|
||||
@@ -41,10 +41,16 @@ export class TicketsService {
|
||||
where.status = filters.status;
|
||||
}
|
||||
if (filters.originStationId) {
|
||||
where.booking = { ...where.booking, schedule: { ...where.booking?.schedule, originStationId: filters.originStationId } };
|
||||
where.booking = { ...where.booking, originStationId: filters.originStationId };
|
||||
}
|
||||
if (filters.destinationStationId) {
|
||||
where.booking = { ...where.booking, schedule: { ...where.booking?.schedule, destinationStationId: filters.destinationStationId } };
|
||||
where.booking = { ...where.booking, destinationStationId: filters.destinationStationId };
|
||||
}
|
||||
if (filters.departureDate) {
|
||||
const start = new Date(filters.departureDate);
|
||||
const end = new Date(filters.departureDate);
|
||||
end.setDate(end.getDate() + 1);
|
||||
where.booking = { ...where.booking, schedule: { ...where.booking?.schedule, departureAt: { gte: start, lt: end } } };
|
||||
}
|
||||
if (filters.arrivalDate) {
|
||||
const start = new Date(filters.arrivalDate);
|
||||
@@ -70,7 +76,7 @@ export class TicketsService {
|
||||
include: {
|
||||
booking: {
|
||||
include: {
|
||||
schedule: { include: { originStation: true, destinationStation: true, train: true } },
|
||||
schedule: { include: { originStation: true, destinationStation: true, train: true, stopTimes: { include: { station: true } } } },
|
||||
returnSchedule: { include: { originStation: true, destinationStation: true } },
|
||||
passenger: { include: { travelerProfiles: true } },
|
||||
seats: { include: { seat: { include: { coach: true } } } },
|
||||
@@ -146,6 +152,18 @@ export class TicketsService {
|
||||
contactPhone: t.booking?.contactPhone,
|
||||
returnSchedule: t.booking?.returnSchedule ?? null,
|
||||
seats: t.booking?.seats ?? [],
|
||||
originStation: (() => {
|
||||
const id = t.booking?.originStationId;
|
||||
if (!id) return t.booking?.schedule?.originStation ?? null;
|
||||
const stop = t.booking?.schedule?.stopTimes?.find((st: any) => st.stationId === id);
|
||||
return stop?.station ?? t.booking?.schedule?.originStation ?? null;
|
||||
})(),
|
||||
destinationStation: (() => {
|
||||
const id = t.booking?.destinationStationId;
|
||||
if (!id) return t.booking?.schedule?.destinationStation ?? null;
|
||||
const stop = t.booking?.schedule?.stopTimes?.find((st: any) => st.stationId === id);
|
||||
return stop?.station ?? t.booking?.schedule?.destinationStation ?? null;
|
||||
})(),
|
||||
},
|
||||
schedule: t.booking?.schedule,
|
||||
seat: t.seat ? {
|
||||
|
||||
Reference in New Issue
Block a user