Major: Unified Activity System with Multi-VIP Support & Enhanced Search/Filtering
Some checks failed
CI/CD Pipeline / Backend Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Tests (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled

## Overview
Complete architectural overhaul merging dual event systems into a unified activity model
with multi-VIP support, enhanced search capabilities, and improved UX throughout.

## Database & Schema Changes

### Unified Activity Model (Breaking Change)
- Merged Event/EventTemplate/EventAttendance into single ScheduleEvent model
- Dropped duplicate tables: Event, EventAttendance, EventTemplate
- Single source of truth for all activities (transport, meals, meetings, events)
- Migration: 20260131180000_drop_duplicate_event_tables

### Multi-VIP Support (Breaking Change)
- Changed schema from single vipId to vipIds array (String[])
- Enables multiple VIPs per activity (ridesharing, group events)
- Migration: 20260131122613_multi_vip_support
- Updated all backend services to handle multi-VIP queries

### Seed Data Updates
- Rebuilt seed.ts with unified activity model
- Added multi-VIP rideshare examples (3 VIPs in SUV, 4 VIPs in van)
- Includes mix of transport + non-transport activities
- Balanced VIP test data (50% OFFICE_OF_DEVELOPMENT, 50% ADMIN)

## Backend Changes

### Services Cleanup
- Removed deprecated common-events endpoints
- Updated EventsService for multi-VIP support
- Enhanced VipsService with multi-VIP activity queries
- Updated DriversService, VehiclesService for unified model
- Added add-vips-to-event.dto for bulk VIP assignment

### Abilities & Permissions
- Updated ability.factory.ts: Event → ScheduleEvent subject
- Enhanced guards for unified activity permissions
- Maintained RBAC (Administrator, Coordinator, Driver roles)

### DTOs
- Updated create-event.dto: vipId → vipIds array
- Updated update-event.dto: vipId → vipIds array
- Added add-vips-to-event.dto for bulk operations
- Removed obsolete event-template DTOs

## Frontend Changes

### UI/UX Improvements

**Renamed "Schedule" → "Activities" Throughout**
- More intuitive terminology for coordinators
- Updated navigation, page titles, buttons
- Changed "Schedule Events" to "Activities" in Admin Tools

**Activities Page Enhancements**
- Added comprehensive search bar (searches: title, location, description, VIP names, driver, vehicle)
- Added sortable columns: Title, Type, VIPs, Start Time, Status
- Visual sort indicators (↑↓ arrows)
- Real-time result count when searching
- Empty state with helpful messaging

**Admin Tools Updates**
- Balanced VIP test data: 10 OFFICE_OF_DEVELOPMENT + 10 ADMIN
- More BSA-relevant organizations (Coca-Cola, AT&T, Walmart vs generic orgs)
- BSA leadership titles (National President, Chief Scout Executive, Regional Directors)
- Relabeled "Schedule Events" → "Activities"

### Component Updates

**EventList.tsx (Activities Page)**
- Added search state management with real-time filtering
- Implemented multi-field sorting with direction toggle
- Enhanced empty states for search + no data scenarios
- Filter tabs + search work together seamlessly

**VIPSchedule.tsx**
- Updated for multi-VIP schema (vipIds array)
- Shows complete itinerary timeline per VIP
- Displays all activities for selected VIP
- Groups by day with formatted dates

**EventForm.tsx**
- Updated to handle vipIds array instead of single vipId
- Multi-select VIP assignment
- Maintains backward compatibility

**AdminTools.tsx**
- New balanced VIP test data (10/10 split)
- BSA-context organizations
- Updated button labels ("Add Test Activities")

### Routing & Navigation
- Removed /common-events routes
- Updated navigation menu labels
- Maintained protected route structure
- Cleaner URL structure

## New Features

### Multi-VIP Activity Support
- Activities can have multiple VIPs (ridesharing, group events)
- Efficient seat utilization tracking (3/6 seats, 4/12 seats)
- Better coordination for shared transport

### Advanced Search & Filtering
- Full-text search across multiple fields
- Instant filtering as you type
- Search + type filters work together
- Clear visual feedback (result counts)

### Sortable Data Tables
- Click column headers to sort
- Toggle ascending/descending
- Visual indicators for active sort
- Sorts persist with search/filter

### Enhanced Admin Tools
- One-click test data generation
- Realistic BSA Jamboree scenario data
- Balanced department representation
- Complete 3-day itineraries per VIP

## Testing & Validation

### Playwright E2E Tests
- Added e2e/ directory structure
- playwright.config.ts configured
- PLAYWRIGHT_GUIDE.md documentation
- Ready for comprehensive E2E testing

### Manual Testing Performed
- Multi-VIP activity creation ✓
- Search across all fields ✓
- Column sorting (all fields) ✓
- Filter tabs + search combination ✓
- Admin Tools data generation ✓
- Database migrations ✓

## Breaking Changes & Migration

**Database Schema Changes**
1. Run migrations: `npx prisma migrate deploy`
2. Reseed database: `npx prisma db seed`
3. Existing data incompatible (dev environment - safe to nuke)

**API Changes**
- POST /events now requires vipIds array (not vipId string)
- GET /events returns vipIds array
- GET /vips/:id/schedule updated for multi-VIP
- Removed /common-events/* endpoints

**Frontend Type Changes**
- ScheduleEvent.vipIds: string[] (was vipId: string)
- EventFormData updated accordingly
- All pages handle array-based VIP assignment

## File Changes Summary

**Added:**
- backend/prisma/migrations/20260131180000_drop_duplicate_event_tables/
- backend/src/events/dto/add-vips-to-event.dto.ts
- frontend/src/components/InlineDriverSelector.tsx
- frontend/e2e/ (Playwright test structure)
- Documentation: NAVIGATION_UX_IMPROVEMENTS.md, PLAYWRIGHT_GUIDE.md

**Modified:**
- 30+ backend files (schema, services, DTOs, abilities)
- 20+ frontend files (pages, components, types)
- Admin tools, seed data, navigation

**Removed:**
- Event/EventAttendance/EventTemplate database tables
- Common events frontend pages
- Obsolete event template DTOs

## Next Steps

**Pending (Phase 3):**
- Activity Templates for bulk event creation
- Operations Dashboard (today's activities + conflicts)
- Complete workflow testing with real users
- Additional E2E test coverage

## Notes
- Development environment - no production data affected
- Database can be reset anytime: `npx prisma migrate reset`
- All servers tested and running successfully
- HMR working correctly for frontend changes

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-31 16:35:24 +01:00
parent 868f7efc23
commit d2754db377
63 changed files with 7345 additions and 667 deletions

View File

@@ -0,0 +1,74 @@
-- AlterTable
ALTER TABLE "schedule_events" ADD COLUMN "eventId" TEXT;
-- CreateTable
CREATE TABLE "event_templates" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"defaultDuration" INTEGER NOT NULL DEFAULT 60,
"location" TEXT,
"type" "EventType" NOT NULL DEFAULT 'EVENT',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3),
CONSTRAINT "event_templates_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "events" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"startTime" TIMESTAMP(3) NOT NULL,
"endTime" TIMESTAMP(3) NOT NULL,
"location" TEXT NOT NULL,
"type" "EventType" NOT NULL DEFAULT 'EVENT',
"templateId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3),
CONSTRAINT "events_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "event_attendance" (
"id" TEXT NOT NULL,
"eventId" TEXT NOT NULL,
"vipId" TEXT NOT NULL,
"addedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "event_attendance_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "events_startTime_endTime_idx" ON "events"("startTime", "endTime");
-- CreateIndex
CREATE INDEX "events_templateId_idx" ON "events"("templateId");
-- CreateIndex
CREATE INDEX "event_attendance_eventId_idx" ON "event_attendance"("eventId");
-- CreateIndex
CREATE INDEX "event_attendance_vipId_idx" ON "event_attendance"("vipId");
-- CreateIndex
CREATE UNIQUE INDEX "event_attendance_eventId_vipId_key" ON "event_attendance"("eventId", "vipId");
-- CreateIndex
CREATE INDEX "schedule_events_eventId_idx" ON "schedule_events"("eventId");
-- AddForeignKey
ALTER TABLE "schedule_events" ADD CONSTRAINT "schedule_events_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES "events"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "events" ADD CONSTRAINT "events_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "event_templates"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "event_attendance" ADD CONSTRAINT "event_attendance_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES "events"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "event_attendance" ADD CONSTRAINT "event_attendance_vipId_fkey" FOREIGN KEY ("vipId") REFERENCES "vips"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,15 @@
/*
Warnings:
- You are about to drop the column `vipId` on the `schedule_events` table. All the data in the column will be lost.
*/
-- DropForeignKey
ALTER TABLE "schedule_events" DROP CONSTRAINT "schedule_events_vipId_fkey";
-- DropIndex
DROP INDEX "schedule_events_vipId_idx";
-- AlterTable
ALTER TABLE "schedule_events" DROP COLUMN "vipId",
ADD COLUMN "vipIds" TEXT[];

View File

@@ -0,0 +1,11 @@
-- Drop the event_attendance join table first (has foreign keys)
DROP TABLE IF EXISTS "event_attendance" CASCADE;
-- Drop the events table (references event_templates)
DROP TABLE IF EXISTS "events" CASCADE;
-- Drop the event_templates table
DROP TABLE IF EXISTS "event_templates" CASCADE;
-- Drop the eventId column from schedule_events (referenced dropped events table)
ALTER TABLE "schedule_events" DROP COLUMN IF EXISTS "eventId";

View File

@@ -51,7 +51,6 @@ model VIP {
venueTransport Boolean @default(false)
notes String? @db.Text
flights Flight[]
events ScheduleEvent[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime? // Soft delete
@@ -171,8 +170,7 @@ enum VehicleStatus {
model ScheduleEvent {
id String @id @default(uuid())
vipId String
vip VIP @relation(fields: [vipId], references: [id], onDelete: Cascade)
vipIds String[] // Array of VIP IDs for multi-passenger trips
title String
// Location details
@@ -204,7 +202,6 @@ model ScheduleEvent {
deletedAt DateTime? // Soft delete
@@map("schedule_events")
@@index([vipId])
@@index([driverId])
@@index([vehicleId])
@@index([startTime, endTime])
@@ -225,3 +222,4 @@ enum EventStatus {
COMPLETED
CANCELLED
}

View File

@@ -1,4 +1,4 @@
import { PrismaClient, Role, Department, ArrivalMode, EventType, EventStatus } from '@prisma/client';
import { PrismaClient, Role, Department, ArrivalMode, EventType, EventStatus, VehicleType, VehicleStatus } from '@prisma/client';
const prisma = new PrismaClient();
@@ -8,6 +8,7 @@ async function main() {
// Clean up existing data (careful in production!)
await prisma.scheduleEvent.deleteMany({});
await prisma.flight.deleteMany({});
await prisma.vehicle.deleteMany({});
await prisma.driver.deleteMany({});
await prisma.vIP.deleteMany({});
await prisma.user.deleteMany({});
@@ -35,8 +36,57 @@ async function main() {
},
});
// Note: test@test.com user is auto-created and auto-approved on first login (see auth.service.ts)
console.log('✅ Created sample users');
// Create sample vehicles with capacity
const blackSUV = await prisma.vehicle.create({
data: {
name: 'Black Suburban',
type: VehicleType.SUV,
licensePlate: 'ABC-1234',
seatCapacity: 6,
status: VehicleStatus.AVAILABLE,
notes: 'Leather interior, tinted windows',
},
});
const whiteVan = await prisma.vehicle.create({
data: {
name: 'White Sprinter Van',
type: VehicleType.VAN,
licensePlate: 'XYZ-5678',
seatCapacity: 12,
status: VehicleStatus.AVAILABLE,
notes: 'High roof, wheelchair accessible',
},
});
const blueSedan = await prisma.vehicle.create({
data: {
name: 'Blue Camry',
type: VehicleType.SEDAN,
licensePlate: 'DEF-9012',
seatCapacity: 4,
status: VehicleStatus.AVAILABLE,
notes: 'Fuel efficient, good for short trips',
},
});
const grayBus = await prisma.vehicle.create({
data: {
name: 'Gray Charter Bus',
type: VehicleType.BUS,
licensePlate: 'BUS-0001',
seatCapacity: 40,
status: VehicleStatus.AVAILABLE,
notes: 'Full size charter bus, A/C, luggage compartment',
},
});
console.log('✅ Created sample vehicles with capacities');
// Create sample drivers
const driver1 = await prisma.driver.create({
data: {
@@ -54,6 +104,22 @@ async function main() {
},
});
const driver3 = await prisma.driver.create({
data: {
name: 'Amanda Washington',
phone: '+1 (555) 234-5678',
department: Department.OFFICE_OF_DEVELOPMENT,
},
});
const driver4 = await prisma.driver.create({
data: {
name: 'Michael Thompson',
phone: '+1 (555) 876-5432',
department: Department.ADMIN,
},
});
console.log('✅ Created sample drivers');
// Create sample VIPs
@@ -96,52 +162,161 @@ async function main() {
},
});
const vip3 = await prisma.vIP.create({
data: {
name: 'Emily Richardson (Harvard University)',
organization: 'Harvard University',
department: Department.OFFICE_OF_DEVELOPMENT,
arrivalMode: ArrivalMode.FLIGHT,
airportPickup: true,
venueTransport: true,
notes: 'Board member, requires accessible vehicle',
},
});
const vip4 = await prisma.vIP.create({
data: {
name: 'David Chen (Stanford)',
organization: 'Stanford University',
department: Department.OFFICE_OF_DEVELOPMENT,
arrivalMode: ArrivalMode.FLIGHT,
airportPickup: true,
venueTransport: true,
notes: 'Keynote speaker',
},
});
console.log('✅ Created sample VIPs');
// Create sample events
// Create sample schedule events (unified activities) - NOW WITH MULTIPLE VIPS!
// Multi-VIP rideshare to Campfire Night (3 VIPs in one SUV)
await prisma.scheduleEvent.create({
data: {
vipId: vip1.id,
title: 'Airport Pickup',
location: 'LAX Terminal 4',
vipIds: [vip3.id, vip4.id, vip1.id], // 3 VIPs sharing a ride
title: 'Transport to Campfire Night',
pickupLocation: 'Grand Hotel Lobby',
dropoffLocation: 'Camp Amphitheater',
startTime: new Date('2026-02-15T19:45:00'),
endTime: new Date('2026-02-15T20:00:00'),
description: 'Rideshare: Emily, David, and Dr. Johnson to campfire',
type: EventType.TRANSPORT,
status: EventStatus.SCHEDULED,
driverId: driver3.id,
vehicleId: blackSUV.id, // 3 VIPs in 6-seat SUV (3/6 seats used)
},
});
// Single VIP transport
await prisma.scheduleEvent.create({
data: {
vipIds: [vip1.id],
title: 'Airport Pickup - Dr. Johnson',
pickupLocation: 'LAX Terminal 4',
dropoffLocation: 'Grand Hotel',
startTime: new Date('2026-02-15T11:30:00'),
endTime: new Date('2026-02-15T12:30:00'),
description: 'Pick up Dr. Johnson from LAX',
type: EventType.TRANSPORT,
status: EventStatus.SCHEDULED,
driverId: driver1.id,
vehicleId: blueSedan.id, // 1 VIP in 4-seat sedan (1/4 seats used)
},
});
// Two VIPs sharing lunch transport
await prisma.scheduleEvent.create({
data: {
vipId: vip1.id,
title: 'Welcome Dinner',
location: 'Grand Hotel Restaurant',
startTime: new Date('2026-02-15T19:00:00'),
endTime: new Date('2026-02-15T21:00:00'),
description: 'Welcome dinner with board members',
type: EventType.MEAL,
status: EventStatus.SCHEDULED,
driverId: driver2.id,
},
});
await prisma.scheduleEvent.create({
data: {
vipId: vip2.id,
title: 'Conference Transport',
location: 'Convention Center',
startTime: new Date('2026-02-16T14:30:00'),
endTime: new Date('2026-02-16T15:00:00'),
description: 'Transport to conference venue',
vipIds: [vip1.id, vip2.id],
title: 'Transport to Lunch - Day 1',
pickupLocation: 'Grand Hotel Lobby',
dropoffLocation: 'Main Dining Hall',
startTime: new Date('2026-02-15T11:45:00'),
endTime: new Date('2026-02-15T12:00:00'),
description: 'Rideshare: Dr. Johnson and Ms. Williams to lunch',
type: EventType.TRANSPORT,
status: EventStatus.SCHEDULED,
driverId: driver1.id,
driverId: driver2.id,
vehicleId: blueSedan.id, // 2 VIPs in 4-seat sedan (2/4 seats used)
},
});
console.log('✅ Created sample events');
// Large group transport in van
await prisma.scheduleEvent.create({
data: {
vipIds: [vip1.id, vip2.id, vip3.id, vip4.id],
title: 'Morning Shuttle to Conference',
pickupLocation: 'Grand Hotel Lobby',
dropoffLocation: 'Conference Center',
startTime: new Date('2026-02-15T08:00:00'),
endTime: new Date('2026-02-15T08:30:00'),
description: 'All VIPs to morning conference session',
type: EventType.TRANSPORT,
status: EventStatus.SCHEDULED,
driverId: driver4.id,
vehicleId: whiteVan.id, // 4 VIPs in 12-seat van (4/12 seats used)
},
});
// Non-transport activities (unified system)
// Opening Ceremony - all VIPs attending
await prisma.scheduleEvent.create({
data: {
vipIds: [vip1.id, vip2.id, vip3.id, vip4.id],
title: 'Opening Ceremony',
location: 'Main Stage',
startTime: new Date('2026-02-15T10:00:00'),
endTime: new Date('2026-02-15T11:30:00'),
description: 'Welcome and opening remarks',
type: EventType.EVENT,
status: EventStatus.SCHEDULED,
},
});
// Lunch - Day 1 (all VIPs)
await prisma.scheduleEvent.create({
data: {
vipIds: [vip1.id, vip2.id, vip3.id, vip4.id],
title: 'Lunch - Day 1',
location: 'Main Dining Hall',
startTime: new Date('2026-02-15T12:00:00'),
endTime: new Date('2026-02-15T13:30:00'),
description: 'Day 1 lunch for all attendees',
type: EventType.MEAL,
status: EventStatus.SCHEDULED,
},
});
// Campfire Night (all VIPs)
await prisma.scheduleEvent.create({
data: {
vipIds: [vip1.id, vip2.id, vip3.id, vip4.id],
title: 'Campfire Night',
location: 'Camp Amphitheater',
startTime: new Date('2026-02-15T20:00:00'),
endTime: new Date('2026-02-15T22:00:00'),
description: 'Evening campfire and networking',
type: EventType.EVENT,
status: EventStatus.SCHEDULED,
},
});
// Private meeting - just Dr. Johnson and Ms. Williams
await prisma.scheduleEvent.create({
data: {
vipIds: [vip1.id, vip2.id],
title: 'Donor Meeting',
location: 'Conference Room A',
startTime: new Date('2026-02-15T14:00:00'),
endTime: new Date('2026-02-15T15:00:00'),
description: 'Private meeting with development team',
type: EventType.MEETING,
status: EventStatus.SCHEDULED,
},
});
console.log('✅ Created sample schedule events with multi-VIP rideshares and activities');
console.log('\n🎉 Database seeded successfully!');
console.log('\nSample Users:');
@@ -150,9 +325,23 @@ async function main() {
console.log('\nSample VIPs:');
console.log('- Dr. Robert Johnson (Flight arrival)');
console.log('- Ms. Sarah Williams (Self-driving)');
console.log('- Emily Richardson (Harvard University)');
console.log('- David Chen (Stanford)');
console.log('\nSample Drivers:');
console.log('- John Smith');
console.log('- Jane Doe');
console.log('- Amanda Washington');
console.log('- Michael Thompson');
console.log('\nSample Vehicles:');
console.log('- Black Suburban (SUV, 6 seats)');
console.log('- White Sprinter Van (Van, 12 seats)');
console.log('- Blue Camry (Sedan, 4 seats)');
console.log('- Gray Charter Bus (Bus, 40 seats)');
console.log('\nSchedule Tasks (Multi-VIP Examples):');
console.log('- 3 VIPs sharing SUV to Campfire (3/6 seats)');
console.log('- 2 VIPs sharing sedan to Lunch (2/4 seats)');
console.log('- 4 VIPs in van to Conference (4/12 seats)');
console.log('- 1 VIP solo in sedan from Airport (1/4 seats)');
}
main()