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:
@@ -42,7 +42,13 @@ interface Event {
|
||||
vip: {
|
||||
id: string;
|
||||
name: string;
|
||||
partySize?: number;
|
||||
} | null;
|
||||
vips?: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
partySize: number;
|
||||
}>;
|
||||
driver: {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -371,29 +377,30 @@ export function CommandCenter() {
|
||||
if (minutesLate >= 5) {
|
||||
alerts.push({
|
||||
type: 'critical',
|
||||
message: `${event.vip?.name || 'Unknown VIP'}: Trip ${minutesLate}min overdue - awaiting driver confirmation`,
|
||||
message: `${formatVipNames(event)}: Trip ${minutesLate}min overdue - awaiting driver confirmation`,
|
||||
link: `/events`,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
unassignedEvents.forEach((event) => {
|
||||
const vipLabel = formatVipNames(event);
|
||||
if (!event.driver && !event.vehicle) {
|
||||
alerts.push({
|
||||
type: 'critical',
|
||||
message: `${event.vip?.name || 'Unknown VIP'}: No driver or vehicle assigned (${getTimeUntil(event.startTime)})`,
|
||||
message: `${vipLabel}: No driver or vehicle assigned (${getTimeUntil(event.startTime)})`,
|
||||
link: `/events`,
|
||||
});
|
||||
} else if (!event.driver) {
|
||||
alerts.push({
|
||||
type: 'critical',
|
||||
message: `${event.vip?.name || 'Unknown VIP'}: No driver assigned (${getTimeUntil(event.startTime)})`,
|
||||
message: `${vipLabel}: No driver assigned (${getTimeUntil(event.startTime)})`,
|
||||
link: `/events`,
|
||||
});
|
||||
} else if (!event.vehicle) {
|
||||
alerts.push({
|
||||
type: 'warning',
|
||||
message: `${event.vip?.name || 'Unknown VIP'}: No vehicle assigned (${getTimeUntil(event.startTime)})`,
|
||||
message: `${vipLabel}: No vehicle assigned (${getTimeUntil(event.startTime)})`,
|
||||
link: `/events`,
|
||||
});
|
||||
}
|
||||
@@ -442,6 +449,16 @@ export function CommandCenter() {
|
||||
return 'text-blue-600 dark:text-blue-400';
|
||||
}
|
||||
|
||||
// Format VIP names for trip cards (show all, with party size indicators)
|
||||
function formatVipNames(event: Event): string {
|
||||
if (event.vips && event.vips.length > 0) {
|
||||
return event.vips.map(v =>
|
||||
v.partySize > 1 ? `${v.name} (+${v.partySize - 1})` : v.name
|
||||
).join(', ');
|
||||
}
|
||||
return event.vip?.name || 'Unknown VIP';
|
||||
}
|
||||
|
||||
const secondsSinceRefresh = Math.floor((now.getTime() - lastRefresh.getTime()) / 1000);
|
||||
|
||||
// System status
|
||||
@@ -642,7 +659,7 @@ export function CommandCenter() {
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="font-semibold text-foreground truncate text-sm">
|
||||
{trip.vip?.name || 'Unknown VIP'}
|
||||
{formatVipNames(trip)}
|
||||
</span>
|
||||
<span className="text-muted-foreground text-xs truncate max-w-[150px]">
|
||||
{trip.title}
|
||||
@@ -739,7 +756,7 @@ export function CommandCenter() {
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
{isImminent && <AlertTriangle className="h-3 w-3 text-red-600 flex-shrink-0" />}
|
||||
<span className="font-semibold text-foreground truncate text-sm">
|
||||
{trip.vip?.name || 'Unknown VIP'}
|
||||
{formatVipNames(trip)}
|
||||
</span>
|
||||
<span className="text-muted-foreground text-xs truncate max-w-[150px]">
|
||||
{trip.title}
|
||||
|
||||
Reference in New Issue
Block a user