+
diff --git a/frontend/src/contexts/AuthContext.tsx b/frontend/src/contexts/AuthContext.tsx
index 47336d6..250ac0b 100644
--- a/frontend/src/contexts/AuthContext.tsx
+++ b/frontend/src/contexts/AuthContext.tsx
@@ -18,6 +18,8 @@ interface AuthContextType {
user: any;
backendUser: BackendUser | null;
isApproved: boolean;
+ isFetchingUser: boolean;
+ authError: string | null;
loginWithRedirect: () => void;
logout: () => void;
}
@@ -36,32 +38,65 @@ export function AuthProvider({ children }: { children: ReactNode }) {
const [backendUser, setBackendUser] = useState(null);
const [fetchingUser, setFetchingUser] = useState(false);
+ const [authError, setAuthError] = useState(null);
// Set up token and fetch backend user profile
useEffect(() => {
- if (isAuthenticated && !fetchingUser) {
+ // Wait for Auth0 to finish loading before fetching token
+ if (isAuthenticated && !isLoading && !fetchingUser && !backendUser) {
setFetchingUser(true);
+ setAuthError(null);
+
+ // Add timeout to prevent infinite loading
+ const timeoutId = setTimeout(() => {
+ setAuthError('Authentication timeout - please try logging in again');
+ setFetchingUser(false);
+ }, 10000); // 10 second timeout
+
getAccessTokenSilently()
.then(async (token) => {
+ clearTimeout(timeoutId);
+ console.log('[AUTH] Got access token, fetching user profile');
localStorage.setItem('auth0_token', token);
// Fetch backend user profile
try {
const response = await api.get('/auth/profile');
+ console.log('[AUTH] User profile fetched successfully:', response.data.email);
setBackendUser(response.data);
- } catch (error) {
+ setAuthError(null);
+ } catch (error: any) {
console.error('[AUTH] Failed to fetch user profile:', error);
setBackendUser(null);
+
+ // Set specific error message
+ if (error.response?.status === 401) {
+ setAuthError('Your account is pending approval or your session has expired');
+ } else {
+ setAuthError('Failed to load user profile - please try logging in again');
+ }
}
})
.catch((error) => {
+ clearTimeout(timeoutId);
console.error('[AUTH] Failed to get token:', error);
+ setBackendUser(null);
+
+ // Handle specific Auth0 errors
+ if (error.error === 'missing_refresh_token' || error.message?.includes('Missing Refresh Token')) {
+ setAuthError('Session expired - please log in again');
+ } else if (error.error === 'login_required') {
+ setAuthError('Login required');
+ } else {
+ setAuthError('Authentication failed - please try logging in again');
+ }
})
.finally(() => {
setFetchingUser(false);
});
}
- }, [isAuthenticated, getAccessTokenSilently]);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [isAuthenticated, isLoading]);
const handleLogout = () => {
localStorage.removeItem('auth0_token');
@@ -76,6 +111,8 @@ export function AuthProvider({ children }: { children: ReactNode }) {
user,
backendUser,
isApproved: backendUser?.isApproved ?? false,
+ isFetchingUser: fetchingUser,
+ authError,
loginWithRedirect,
logout: handleLogout,
}}
diff --git a/frontend/src/hooks/useDebounce.ts b/frontend/src/hooks/useDebounce.ts
new file mode 100644
index 0000000..7f1b966
--- /dev/null
+++ b/frontend/src/hooks/useDebounce.ts
@@ -0,0 +1,25 @@
+import { useEffect, useState } from 'react';
+
+/**
+ * Debounces a value by the specified delay.
+ * Useful for search inputs to avoid excessive filtering/API calls.
+ *
+ * @param value - The value to debounce
+ * @param delay - Delay in milliseconds (default: 300ms)
+ * @returns The debounced value
+ */
+export function useDebounce(value: T, delay: number = 300): T {
+ const [debouncedValue, setDebouncedValue] = useState(value);
+
+ useEffect(() => {
+ const handler = setTimeout(() => {
+ setDebouncedValue(value);
+ }, delay);
+
+ return () => {
+ clearTimeout(handler);
+ };
+ }, [value, delay]);
+
+ return debouncedValue;
+}
diff --git a/frontend/src/lib/abilities.ts b/frontend/src/lib/abilities.ts
index a51e352..e79e971 100644
--- a/frontend/src/lib/abilities.ts
+++ b/frontend/src/lib/abilities.ts
@@ -23,6 +23,8 @@ export type Subjects =
| 'ScheduleEvent'
| 'Flight'
| 'Vehicle'
+ | 'Event'
+ | 'EventTemplate'
| 'all';
/**
@@ -66,10 +68,10 @@ export function defineAbilitiesFor(user: User | null): AppAbility {
can(Action.Manage, 'all');
} else if (user.role === 'COORDINATOR') {
// Coordinators have full access except user management
- can(Action.Read, 'all');
- can(Action.Create, ['VIP', 'Driver', 'ScheduleEvent', 'Flight', 'Vehicle']);
- can(Action.Update, ['VIP', 'Driver', 'ScheduleEvent', 'Flight', 'Vehicle']);
- can(Action.Delete, ['VIP', 'Driver', 'ScheduleEvent', 'Flight', 'Vehicle']);
+ can(Action.Read, ['VIP', 'Driver', 'ScheduleEvent', 'Flight', 'Vehicle', 'Event', 'EventTemplate']);
+ can(Action.Create, ['VIP', 'Driver', 'ScheduleEvent', 'Flight', 'Vehicle', 'Event', 'EventTemplate']);
+ can(Action.Update, ['VIP', 'Driver', 'ScheduleEvent', 'Flight', 'Vehicle', 'Event', 'EventTemplate']);
+ can(Action.Delete, ['VIP', 'Driver', 'ScheduleEvent', 'Flight', 'Vehicle', 'Event', 'EventTemplate']);
// Cannot manage users
cannot(Action.Create, 'User');
diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts
new file mode 100644
index 0000000..e01ffb0
--- /dev/null
+++ b/frontend/src/lib/types.ts
@@ -0,0 +1,126 @@
+/**
+ * TypeScript interfaces for VIP Coordinator
+ */
+
+export interface VIP {
+ id: string;
+ name: string;
+ organization?: string;
+ department: 'OFFICE_OF_DEVELOPMENT' | 'ADMIN';
+ arrivalMode: 'FLIGHT' | 'SELF_DRIVING';
+ expectedArrival?: string;
+ airportPickup: boolean;
+ venueTransport: boolean;
+ notes?: string;
+ createdAt: string;
+ updatedAt: string;
+ deletedAt?: string;
+}
+
+export interface Driver {
+ id: string;
+ name: string;
+ phone: string;
+ department?: 'OFFICE_OF_DEVELOPMENT' | 'ADMIN';
+ userId?: string;
+ shiftStartTime?: string;
+ shiftEndTime?: string;
+ isAvailable: boolean;
+ createdAt: string;
+ updatedAt: string;
+ deletedAt?: string;
+}
+
+export interface ScheduleEvent {
+ id: string;
+ vipId: string;
+ vip?: VIP;
+ title: string;
+ pickupLocation?: string;
+ dropoffLocation?: string;
+ location?: string;
+ startTime: string;
+ endTime: string;
+ actualStartTime?: string;
+ actualEndTime?: string;
+ description?: string;
+ type: 'TRANSPORT' | 'MEETING' | 'EVENT' | 'MEAL' | 'ACCOMMODATION';
+ status: 'SCHEDULED' | 'IN_PROGRESS' | 'COMPLETED' | 'CANCELLED';
+ driverId?: string;
+ driver?: Driver;
+ vehicleId?: string;
+ eventId?: string;
+ event?: Event;
+ notes?: string;
+ createdAt: string;
+ updatedAt: string;
+ deletedAt?: string;
+}
+
+export interface EventTemplate {
+ id: string;
+ name: string;
+ description?: string;
+ defaultDuration: number; // in minutes
+ location?: string;
+ type: 'TRANSPORT' | 'MEETING' | 'EVENT' | 'MEAL' | 'ACCOMMODATION';
+ createdAt: string;
+ updatedAt: string;
+ deletedAt?: string;
+ _count?: {
+ events: number;
+ };
+}
+
+export interface Event {
+ id: string;
+ name: string;
+ description?: string;
+ startTime: string;
+ endTime: string;
+ location: string;
+ type: 'TRANSPORT' | 'MEETING' | 'EVENT' | 'MEAL' | 'ACCOMMODATION';
+ templateId?: string;
+ template?: EventTemplate;
+ attendees: EventAttendance[];
+ scheduleTasks?: ScheduleEvent[];
+ createdAt: string;
+ updatedAt: string;
+ deletedAt?: string;
+ _count?: {
+ attendees: number;
+ scheduleTasks: number;
+ };
+}
+
+export interface EventAttendance {
+ id: string;
+ eventId: string;
+ vipId: string;
+ vip: VIP;
+ addedAt: string;
+}
+
+export interface CreateEventTemplateDto {
+ name: string;
+ description?: string;
+ defaultDuration: number;
+ location?: string;
+ type: 'TRANSPORT' | 'MEETING' | 'EVENT' | 'MEAL' | 'ACCOMMODATION';
+}
+
+export interface CreateEventDto {
+ name: string;
+ description?: string;
+ startTime: string;
+ endTime: string;
+ location: string;
+ type: 'TRANSPORT' | 'MEETING' | 'EVENT' | 'MEAL' | 'ACCOMMODATION';
+ templateId?: string;
+}
+
+export interface AddVipsToEventDto {
+ vipIds: string[];
+ pickupMinutesBeforeEvent?: number;
+ pickupLocationOverride?: string;
+}
diff --git a/frontend/src/pages/AdminTools.tsx b/frontend/src/pages/AdminTools.tsx
index 5aec7f1..f4ead47 100644
--- a/frontend/src/pages/AdminTools.tsx
+++ b/frontend/src/pages/AdminTools.tsx
@@ -49,6 +49,7 @@ export function AdminTools() {
setIsLoading(true);
try {
const testVIPs = [
+ // OFFICE_OF_DEVELOPMENT (10 VIPs) - Corporate sponsors, foundations, major donors
{
name: 'Sarah Chen',
organization: 'Microsoft Corporation',
@@ -59,23 +60,7 @@ export function AdminTools() {
},
{
name: 'Marcus Johnson',
- organization: 'Goldman Sachs',
- department: 'OFFICE_OF_DEVELOPMENT',
- arrivalMode: 'FLIGHT',
- airportPickup: true,
- venueTransport: false,
- },
- {
- name: 'Dr. Aisha Patel',
- organization: 'Johns Hopkins Medicine',
- department: 'OFFICE_OF_DEVELOPMENT',
- arrivalMode: 'SELF_DRIVING',
- airportPickup: false,
- venueTransport: true,
- },
- {
- name: 'Roberto Gonzalez',
- organization: 'Tesla Inc',
+ organization: 'The Coca-Cola Company',
department: 'OFFICE_OF_DEVELOPMENT',
arrivalMode: 'FLIGHT',
airportPickup: true,
@@ -83,55 +68,15 @@ export function AdminTools() {
},
{
name: 'Jennifer Wu',
- organization: 'JPMorgan Chase',
+ organization: 'JPMorgan Chase Foundation',
department: 'OFFICE_OF_DEVELOPMENT',
arrivalMode: 'FLIGHT',
airportPickup: true,
venueTransport: true,
},
{
- name: 'David Okonkwo',
- organization: 'McKinsey & Company',
- department: 'OFFICE_OF_DEVELOPMENT',
- arrivalMode: 'FLIGHT',
- airportPickup: false,
- venueTransport: true,
- },
- {
- name: 'Emily Richardson',
- organization: 'Harvard University',
- department: 'ADMIN',
- arrivalMode: 'SELF_DRIVING',
- airportPickup: false,
- venueTransport: false,
- },
- {
- name: 'Yuki Tanaka',
- organization: 'Sony Corporation',
- department: 'OFFICE_OF_DEVELOPMENT',
- arrivalMode: 'FLIGHT',
- airportPickup: true,
- venueTransport: true,
- },
- {
- name: 'Alexander Volkov',
- organization: 'Deloitte Consulting',
- department: 'OFFICE_OF_DEVELOPMENT',
- arrivalMode: 'FLIGHT',
- airportPickup: true,
- venueTransport: false,
- },
- {
- name: 'Maria Rodriguez',
- organization: 'Bank of America',
- department: 'OFFICE_OF_DEVELOPMENT',
- arrivalMode: 'SELF_DRIVING',
- airportPickup: false,
- venueTransport: true,
- },
- {
- name: 'James O\'Brien',
- organization: 'Apple Inc',
+ name: 'Roberto Gonzalez',
+ organization: 'AT&T Inc',
department: 'OFFICE_OF_DEVELOPMENT',
arrivalMode: 'FLIGHT',
airportPickup: true,
@@ -146,64 +91,121 @@ export function AdminTools() {
venueTransport: true,
},
{
- name: 'Thomas Anderson',
- organization: 'Meta Platforms',
+ name: 'David Okonkwo',
+ organization: 'Bank of America',
department: 'OFFICE_OF_DEVELOPMENT',
arrivalMode: 'FLIGHT',
- airportPickup: true,
- venueTransport: false,
- },
- {
- name: 'Fatima Al-Rahman',
- organization: 'Morgan Stanley',
- department: 'OFFICE_OF_DEVELOPMENT',
- arrivalMode: 'FLIGHT',
- airportPickup: true,
+ airportPickup: false,
venueTransport: true,
},
{
- name: 'Henrik Larsson',
- organization: 'Spotify',
+ name: 'Maria Rodriguez',
+ organization: 'Walmart Foundation',
department: 'OFFICE_OF_DEVELOPMENT',
arrivalMode: 'SELF_DRIVING',
airportPickup: false,
venueTransport: true,
},
{
- name: 'Dr. Maya Krishnan',
- organization: 'Stanford University',
+ name: 'Yuki Tanaka',
+ organization: 'Honda Motor Company',
+ department: 'OFFICE_OF_DEVELOPMENT',
+ arrivalMode: 'FLIGHT',
+ airportPickup: true,
+ venueTransport: true,
+ },
+ {
+ name: 'Thomas Anderson',
+ organization: 'Verizon Communications',
+ department: 'OFFICE_OF_DEVELOPMENT',
+ arrivalMode: 'FLIGHT',
+ airportPickup: true,
+ venueTransport: false,
+ },
+ {
+ name: 'Isabella Costa',
+ organization: 'Target Corporation',
+ department: 'OFFICE_OF_DEVELOPMENT',
+ arrivalMode: 'FLIGHT',
+ airportPickup: false,
+ venueTransport: true,
+ },
+ // ADMIN (10 VIPs) - BSA Leadership and Staff
+ {
+ name: 'Roger A. Krone',
+ organization: 'BSA National President',
+ department: 'ADMIN',
+ arrivalMode: 'FLIGHT',
+ airportPickup: true,
+ venueTransport: true,
+ },
+ {
+ name: 'Emily Richardson',
+ organization: 'BSA Chief Scout Executive',
department: 'ADMIN',
arrivalMode: 'SELF_DRIVING',
airportPickup: false,
venueTransport: false,
},
{
- name: 'William Zhang',
- organization: 'Amazon Web Services',
- department: 'OFFICE_OF_DEVELOPMENT',
+ name: 'Dr. Maya Krishnan',
+ organization: 'BSA National Director of Program',
+ department: 'ADMIN',
+ arrivalMode: 'SELF_DRIVING',
+ airportPickup: false,
+ venueTransport: false,
+ },
+ {
+ name: 'James O\'Brien',
+ organization: 'BSA Northeast Regional Director',
+ department: 'ADMIN',
arrivalMode: 'FLIGHT',
airportPickup: true,
venueTransport: true,
},
{
- name: 'Isabella Costa',
- organization: 'Citigroup',
- department: 'OFFICE_OF_DEVELOPMENT',
+ name: 'Fatima Al-Rahman',
+ organization: 'BSA Western Region Executive',
+ department: 'ADMIN',
arrivalMode: 'FLIGHT',
- airportPickup: false,
+ airportPickup: true,
venueTransport: true,
},
{
- name: 'Mohammed Hassan',
- organization: 'Intel Corporation',
- department: 'OFFICE_OF_DEVELOPMENT',
+ name: 'William Zhang',
+ organization: 'BSA Southern Region Council',
+ department: 'ADMIN',
arrivalMode: 'FLIGHT',
airportPickup: true,
venueTransport: true,
},
{
name: 'Sophie Laurent',
- organization: 'Yale University',
+ organization: 'BSA National Volunteer Training',
+ department: 'ADMIN',
+ arrivalMode: 'SELF_DRIVING',
+ airportPickup: false,
+ venueTransport: true,
+ },
+ {
+ name: 'Alexander Volkov',
+ organization: 'BSA High Adventure Director',
+ department: 'ADMIN',
+ arrivalMode: 'FLIGHT',
+ airportPickup: true,
+ venueTransport: false,
+ },
+ {
+ name: 'Dr. Aisha Patel',
+ organization: 'BSA STEM & Innovation Programs',
+ department: 'ADMIN',
+ arrivalMode: 'SELF_DRIVING',
+ airportPickup: false,
+ venueTransport: true,
+ },
+ {
+ name: 'Henrik Larsson',
+ organization: 'BSA International Commissioner',
department: 'ADMIN',
arrivalMode: 'SELF_DRIVING',
airportPickup: false,
@@ -460,7 +462,7 @@ export function AdminTools() {
try {
await api.post('/events', {
- vipId: vip.id,
+ vipIds: [vip.id],
// driverId and vehicleId left unassigned to avoid conflicts
title: `Airport Pickup - ${vip.name}`,
type: 'TRANSPORT',
@@ -484,7 +486,7 @@ export function AdminTools() {
try {
await api.post('/events', {
- vipId: vip.id,
+ vipIds: [vip.id],
title: 'Registration & VIP Badge Collection',
type: 'EVENT',
status: 'SCHEDULED',
@@ -503,7 +505,7 @@ export function AdminTools() {
try {
await api.post('/events', {
- vipId: vip.id,
+ vipIds: [vip.id],
// driverId and vehicleId left unassigned to avoid conflicts
title: 'Transport to VIP Lodge',
type: 'TRANSPORT',
@@ -525,7 +527,7 @@ export function AdminTools() {
try {
await api.post('/events', {
- vipId: vip.id,
+ vipIds: [vip.id],
title: 'VIP Lodge Check-in',
type: 'ACCOMMODATION',
status: 'SCHEDULED',
@@ -545,7 +547,7 @@ export function AdminTools() {
try {
await api.post('/events', {
- vipId: vip.id,
+ vipIds: [vip.id],
title: 'VIP Welcome Dinner',
type: 'MEAL',
status: 'SCHEDULED',
@@ -569,7 +571,7 @@ export function AdminTools() {
try {
await api.post('/events', {
- vipId: vip.id,
+ vipIds: [vip.id],
title: 'VIP Breakfast',
type: 'MEAL',
status: 'SCHEDULED',
@@ -589,7 +591,7 @@ export function AdminTools() {
try {
await api.post('/events', {
- vipId: vip.id,
+ vipIds: [vip.id],
// driverId and vehicleId left unassigned to avoid conflicts
title: 'Transport to Opening Ceremony',
type: 'TRANSPORT',
@@ -612,7 +614,7 @@ export function AdminTools() {
try {
await api.post('/events', {
- vipId: vip.id,
+ vipIds: [vip.id],
title: 'Jamboree Opening Ceremony',
type: 'EVENT',
status: 'SCHEDULED',
@@ -632,7 +634,7 @@ export function AdminTools() {
try {
await api.post('/events', {
- vipId: vip.id,
+ vipIds: [vip.id],
title: 'Donor Recognition & Campaign Update',
type: 'MEETING',
status: 'SCHEDULED',
@@ -652,7 +654,7 @@ export function AdminTools() {
try {
await api.post('/events', {
- vipId: vip.id,
+ vipIds: [vip.id],
title: 'VIP Luncheon',
type: 'MEAL',
status: 'SCHEDULED',
@@ -672,7 +674,7 @@ export function AdminTools() {
try {
await api.post('/events', {
- vipId: vip.id,
+ vipIds: [vip.id],
// driverId and vehicleId left unassigned for guided tour
title: 'Jamboree Site Tour',
type: 'EVENT',
@@ -694,7 +696,7 @@ export function AdminTools() {
try {
await api.post('/events', {
- vipId: vip.id,
+ vipIds: [vip.id],
title: 'Gala Dinner & Awards Ceremony',
type: 'MEAL',
status: 'SCHEDULED',
@@ -718,7 +720,7 @@ export function AdminTools() {
try {
await api.post('/events', {
- vipId: vip.id,
+ vipIds: [vip.id],
title: 'Farewell Breakfast',
type: 'MEAL',
status: 'SCHEDULED',
@@ -738,7 +740,7 @@ export function AdminTools() {
try {
await api.post('/events', {
- vipId: vip.id,
+ vipIds: [vip.id],
title: 'VIP Lodge Checkout',
type: 'ACCOMMODATION',
status: 'SCHEDULED',
@@ -759,7 +761,7 @@ export function AdminTools() {
try {
await api.post('/events', {
- vipId: vip.id,
+ vipIds: [vip.id],
// driverId and vehicleId left unassigned to avoid conflicts
title: `Airport Departure - ${vip.name}`,
type: 'TRANSPORT',
@@ -1088,11 +1090,11 @@ export function AdminTools() {