feat: add party size tracking and master event linking
Add partySize field to VIP model (default 1) to track total people traveling with each VIP including entourage/handlers/spouses. Vehicle capacity checks now sum party sizes instead of just counting VIPs. Add masterEventId self-reference to ScheduleEvent for linking transport legs to shared itinerary items (events, meetings, meals). When creating a transport event, users can link it to a shared activity and VIPs auto-populate from the linked event. Changes: - Schema: partySize on VIP, masterEventId on ScheduleEvent - Backend: party-size-aware capacity checks, master/child event includes - VIP Form: party size input with helper text - Event Form: party-size capacity display, master event selector - Event List: party size in capacity and VIP names, master event badges - Command Center: all VIP names shown with party size indicators Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -55,4 +55,8 @@ export class CreateEventDto {
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
vehicleId?: string;
|
||||
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
masterEventId?: string;
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ export class EventsService {
|
||||
if (createEventDto.vehicleId && createEventDto.vipIds) {
|
||||
await this.checkVehicleCapacity(
|
||||
createEventDto.vehicleId,
|
||||
createEventDto.vipIds.length,
|
||||
createEventDto.vipIds,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -71,6 +71,13 @@ export class EventsService {
|
||||
include: {
|
||||
driver: true,
|
||||
vehicle: true,
|
||||
masterEvent: {
|
||||
select: { id: true, title: true, type: true, startTime: true, endTime: true },
|
||||
},
|
||||
childEvents: {
|
||||
where: { deletedAt: null },
|
||||
select: { id: true, title: true, type: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -83,6 +90,13 @@ export class EventsService {
|
||||
include: {
|
||||
driver: true,
|
||||
vehicle: true,
|
||||
masterEvent: {
|
||||
select: { id: true, title: true, type: true, startTime: true, endTime: true },
|
||||
},
|
||||
childEvents: {
|
||||
where: { deletedAt: null },
|
||||
select: { id: true, title: true, type: true },
|
||||
},
|
||||
},
|
||||
orderBy: { startTime: 'asc' },
|
||||
});
|
||||
@@ -96,6 +110,13 @@ export class EventsService {
|
||||
include: {
|
||||
driver: true,
|
||||
vehicle: true,
|
||||
masterEvent: {
|
||||
select: { id: true, title: true, type: true, startTime: true, endTime: true },
|
||||
},
|
||||
childEvents: {
|
||||
where: { deletedAt: null },
|
||||
select: { id: true, title: true, type: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -125,12 +146,10 @@ export class EventsService {
|
||||
|
||||
// Check vehicle capacity if vehicle or VIPs are being updated
|
||||
const vehicleId = updateEventDto.vehicleId || event.vehicleId;
|
||||
const vipCount = updateEventDto.vipIds
|
||||
? updateEventDto.vipIds.length
|
||||
: event.vipIds.length;
|
||||
const vipIds = updateEventDto.vipIds || event.vipIds;
|
||||
|
||||
if (vehicleId && vipCount > 0 && !updateEventDto.forceAssign) {
|
||||
await this.checkVehicleCapacity(vehicleId, vipCount);
|
||||
if (vehicleId && vipIds.length > 0 && !updateEventDto.forceAssign) {
|
||||
await this.checkVehicleCapacity(vehicleId, vipIds);
|
||||
}
|
||||
|
||||
// Check for conflicts if driver or times are being updated (unless forceAssign is true)
|
||||
@@ -190,6 +209,13 @@ export class EventsService {
|
||||
include: {
|
||||
driver: true,
|
||||
vehicle: true,
|
||||
masterEvent: {
|
||||
select: { id: true, title: true, type: true, startTime: true, endTime: true },
|
||||
},
|
||||
childEvents: {
|
||||
where: { deletedAt: null },
|
||||
select: { id: true, title: true, type: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -209,6 +235,13 @@ export class EventsService {
|
||||
include: {
|
||||
driver: true,
|
||||
vehicle: true,
|
||||
masterEvent: {
|
||||
select: { id: true, title: true, type: true, startTime: true, endTime: true },
|
||||
},
|
||||
childEvents: {
|
||||
where: { deletedAt: null },
|
||||
select: { id: true, title: true, type: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -233,9 +266,9 @@ export class EventsService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check vehicle capacity
|
||||
* Check vehicle capacity using sum of VIP party sizes
|
||||
*/
|
||||
private async checkVehicleCapacity(vehicleId: string, vipCount: number) {
|
||||
private async checkVehicleCapacity(vehicleId: string, vipIds: string[]) {
|
||||
const vehicle = await this.prisma.vehicle.findFirst({
|
||||
where: { id: vehicleId, deletedAt: null },
|
||||
});
|
||||
@@ -244,14 +277,20 @@ export class EventsService {
|
||||
throw new NotFoundException('Vehicle not found');
|
||||
}
|
||||
|
||||
if (vipCount > vehicle.seatCapacity) {
|
||||
const vips = await this.prisma.vIP.findMany({
|
||||
where: { id: { in: vipIds }, deletedAt: null },
|
||||
select: { partySize: true },
|
||||
});
|
||||
const totalPeople = vips.reduce((sum, v) => sum + v.partySize, 0);
|
||||
|
||||
if (totalPeople > vehicle.seatCapacity) {
|
||||
this.logger.warn(
|
||||
`Vehicle capacity exceeded: ${vipCount} VIPs > ${vehicle.seatCapacity} seats`,
|
||||
`Vehicle capacity exceeded: ${totalPeople} people > ${vehicle.seatCapacity} seats`,
|
||||
);
|
||||
throw new BadRequestException({
|
||||
message: `Vehicle capacity exceeded: ${vipCount} VIPs require more than ${vehicle.seatCapacity} available seats`,
|
||||
message: `Vehicle capacity exceeded: ${totalPeople} people require more than ${vehicle.seatCapacity} available seats`,
|
||||
capacity: vehicle.seatCapacity,
|
||||
requested: vipCount,
|
||||
requested: totalPeople,
|
||||
exceeded: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import {
|
||||
IsOptional,
|
||||
IsBoolean,
|
||||
IsDateString,
|
||||
IsInt,
|
||||
Min,
|
||||
} from 'class-validator';
|
||||
import { Department, ArrivalMode } from '@prisma/client';
|
||||
|
||||
@@ -33,6 +35,11 @@ export class CreateVipDto {
|
||||
@IsOptional()
|
||||
venueTransport?: boolean;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
@Min(1)
|
||||
partySize?: number;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
notes?: string;
|
||||
|
||||
Reference in New Issue
Block a user