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
Complete rewrite from Express to NestJS with enterprise-grade features: ## Backend Improvements - Migrated from Express to NestJS 11.0.1 with TypeScript - Implemented Prisma ORM 7.3.0 for type-safe database access - Added CASL authorization system replacing role-based guards - Created global exception filters with structured logging - Implemented Auth0 JWT authentication with Passport.js - Added vehicle management with conflict detection - Enhanced event scheduling with driver/vehicle assignment - Comprehensive error handling and logging ## Frontend Improvements - Upgraded to React 19.2.0 with Vite 7.2.4 - Implemented CASL-based permission system - Added AbilityContext for declarative permissions - Created ErrorHandler utility for consistent error messages - Enhanced API client with request/response logging - Added War Room (Command Center) dashboard - Created VIP Schedule view with complete itineraries - Implemented Vehicle Management UI - Added mock data generators for testing (288 events across 20 VIPs) ## New Features - Vehicle fleet management (types, capacity, status tracking) - Complete 3-day Jamboree schedule generation - Individual VIP schedule pages with PDF export (planned) - Real-time War Room dashboard with auto-refresh - Permission-based navigation filtering - First user auto-approval as administrator ## Documentation - Created CASL_AUTHORIZATION.md (comprehensive guide) - Created ERROR_HANDLING.md (error handling patterns) - Updated CLAUDE.md with new architecture - Added migration guides and best practices ## Technical Debt Resolved - Removed custom authentication in favor of Auth0 - Replaced role checks with CASL abilities - Standardized error responses across API - Implemented proper TypeScript typing - Added comprehensive logging Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
460 lines
17 KiB
TypeScript
460 lines
17 KiB
TypeScript
import React, { useState } from 'react';
|
|
|
|
interface Flight {
|
|
flightNumber: string;
|
|
flightDate: string;
|
|
segment: number;
|
|
validated?: boolean;
|
|
validationData?: any;
|
|
}
|
|
|
|
interface VipFormData {
|
|
name: string;
|
|
organization: string;
|
|
department: 'Office of Development' | 'Admin';
|
|
transportMode: 'flight' | 'self-driving';
|
|
flights?: Flight[];
|
|
expectedArrival?: string;
|
|
needsAirportPickup?: boolean;
|
|
needsVenueTransport: boolean;
|
|
notes: string;
|
|
}
|
|
|
|
interface VipFormProps {
|
|
onSubmit: (vipData: VipFormData) => void;
|
|
onCancel: () => void;
|
|
}
|
|
|
|
const VipForm: React.FC<VipFormProps> = ({ onSubmit, onCancel }) => {
|
|
const [formData, setFormData] = useState<VipFormData>({
|
|
name: '',
|
|
organization: '',
|
|
department: 'Office of Development',
|
|
transportMode: 'flight',
|
|
flights: [{ flightNumber: '', flightDate: '', segment: 1 }],
|
|
expectedArrival: '',
|
|
needsAirportPickup: true,
|
|
needsVenueTransport: true,
|
|
notes: ''
|
|
});
|
|
|
|
const [flightValidating, setFlightValidating] = useState<{ [key: number]: boolean }>({});
|
|
const [flightErrors, setFlightErrors] = useState<{ [key: number]: string }>({});
|
|
|
|
const handleSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
|
|
// Only include flights with flight numbers
|
|
const validFlights = formData.flights?.filter(f => f.flightNumber) || [];
|
|
|
|
onSubmit({
|
|
...formData,
|
|
flights: validFlights.length > 0 ? validFlights : undefined
|
|
});
|
|
};
|
|
|
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
|
const { name, value, type } = e.target;
|
|
|
|
if (type === 'checkbox') {
|
|
const checked = (e.target as HTMLInputElement).checked;
|
|
setFormData(prev => ({
|
|
...prev,
|
|
[name]: checked
|
|
}));
|
|
} else {
|
|
setFormData(prev => ({
|
|
...prev,
|
|
[name]: value
|
|
}));
|
|
}
|
|
};
|
|
|
|
const handleTransportModeChange = (mode: 'flight' | 'self-driving') => {
|
|
setFormData(prev => ({
|
|
...prev,
|
|
transportMode: mode,
|
|
flights: mode === 'flight' ? [{ flightNumber: '', flightDate: '', segment: 1 }] : undefined,
|
|
expectedArrival: mode === 'self-driving' ? prev.expectedArrival : '',
|
|
needsAirportPickup: mode === 'flight' ? true : false
|
|
}));
|
|
|
|
// Clear flight errors when switching away from flight mode
|
|
if (mode !== 'flight') {
|
|
setFlightErrors({});
|
|
}
|
|
};
|
|
|
|
const handleFlightChange = (index: number, field: 'flightNumber' | 'flightDate', value: string) => {
|
|
setFormData(prev => ({
|
|
...prev,
|
|
flights: prev.flights?.map((flight, i) =>
|
|
i === index ? { ...flight, [field]: value, validated: false } : flight
|
|
) || []
|
|
}));
|
|
|
|
// Clear validation for this flight when it changes
|
|
setFlightErrors(prev => ({ ...prev, [index]: '' }));
|
|
};
|
|
|
|
const addConnectingFlight = () => {
|
|
const currentFlights = formData.flights || [];
|
|
if (currentFlights.length < 3) {
|
|
setFormData(prev => ({
|
|
...prev,
|
|
flights: [...currentFlights, {
|
|
flightNumber: '',
|
|
flightDate: currentFlights[currentFlights.length - 1]?.flightDate || '',
|
|
segment: currentFlights.length + 1
|
|
}]
|
|
}));
|
|
}
|
|
};
|
|
|
|
const removeConnectingFlight = (index: number) => {
|
|
setFormData(prev => ({
|
|
...prev,
|
|
flights: prev.flights?.filter((_, i) => i !== index).map((flight, i) => ({
|
|
...flight,
|
|
segment: i + 1
|
|
})) || []
|
|
}));
|
|
|
|
// Clear errors for removed flight
|
|
setFlightErrors(prev => {
|
|
const newErrors = { ...prev };
|
|
delete newErrors[index];
|
|
return newErrors;
|
|
});
|
|
};
|
|
|
|
const validateFlight = async (index: number) => {
|
|
const flight = formData.flights?.[index];
|
|
if (!flight || !flight.flightNumber || !flight.flightDate) {
|
|
setFlightErrors(prev => ({ ...prev, [index]: 'Please enter flight number and date' }));
|
|
return;
|
|
}
|
|
|
|
setFlightValidating(prev => ({ ...prev, [index]: true }));
|
|
setFlightErrors(prev => ({ ...prev, [index]: '' }));
|
|
|
|
try {
|
|
const url = `/api/flights/${flight.flightNumber}?date=${flight.flightDate}`;
|
|
const response = await fetch(url);
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
|
|
// Update flight with validation data
|
|
setFormData(prev => ({
|
|
...prev,
|
|
flights: prev.flights?.map((f, i) =>
|
|
i === index ? { ...f, validated: true, validationData: data } : f
|
|
) || []
|
|
}));
|
|
|
|
setFlightErrors(prev => ({ ...prev, [index]: '' }));
|
|
} else {
|
|
const errorData = await response.json();
|
|
setFlightErrors(prev => ({
|
|
...prev,
|
|
[index]: errorData.error || 'Invalid flight number'
|
|
}));
|
|
}
|
|
} catch (error) {
|
|
setFlightErrors(prev => ({
|
|
...prev,
|
|
[index]: 'Error validating flight'
|
|
}));
|
|
} finally {
|
|
setFlightValidating(prev => ({ ...prev, [index]: false }));
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="modal-overlay">
|
|
<div className="modal-content">
|
|
{/* Modal Header */}
|
|
<div className="modal-header">
|
|
<h2 className="text-2xl font-bold text-slate-800">
|
|
Add New VIP
|
|
</h2>
|
|
<p className="text-slate-600 mt-2">Enter VIP details and travel information</p>
|
|
</div>
|
|
|
|
{/* Modal Body */}
|
|
<div className="modal-body">
|
|
<form onSubmit={handleSubmit} className="space-y-8">
|
|
{/* Basic Information Section */}
|
|
<div className="form-section">
|
|
<div className="form-section-header">
|
|
<h3 className="form-section-title">Basic Information</h3>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div className="form-group">
|
|
<label htmlFor="name" className="form-label">Full Name *</label>
|
|
<input
|
|
type="text"
|
|
id="name"
|
|
name="name"
|
|
value={formData.name}
|
|
onChange={handleChange}
|
|
className="form-input"
|
|
placeholder="Enter full name"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div className="form-group">
|
|
<label htmlFor="organization" className="form-label">Organization *</label>
|
|
<input
|
|
type="text"
|
|
id="organization"
|
|
name="organization"
|
|
value={formData.organization}
|
|
onChange={handleChange}
|
|
className="form-input"
|
|
placeholder="Enter organization name"
|
|
required
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="form-group">
|
|
<label htmlFor="department" className="form-label">Department *</label>
|
|
<select
|
|
id="department"
|
|
name="department"
|
|
value={formData.department}
|
|
onChange={handleChange}
|
|
className="form-select"
|
|
required
|
|
>
|
|
<option value="Office of Development">Office of Development</option>
|
|
<option value="Admin">Admin</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Transportation Section */}
|
|
<div className="form-section">
|
|
<div className="form-section-header">
|
|
<h3 className="form-section-title">Transportation Details</h3>
|
|
</div>
|
|
|
|
<div className="form-group">
|
|
<label className="form-label">How are you arriving? *</label>
|
|
<div className="radio-group">
|
|
<div
|
|
className={`radio-option ${formData.transportMode === 'flight' ? 'selected' : ''}`}
|
|
onClick={() => handleTransportModeChange('flight')}
|
|
>
|
|
<input
|
|
type="radio"
|
|
name="transportMode"
|
|
value="flight"
|
|
checked={formData.transportMode === 'flight'}
|
|
onChange={() => handleTransportModeChange('flight')}
|
|
className="form-radio mr-3"
|
|
/>
|
|
<span className="font-medium">Arriving by Flight</span>
|
|
</div>
|
|
<div
|
|
className={`radio-option ${formData.transportMode === 'self-driving' ? 'selected' : ''}`}
|
|
onClick={() => handleTransportModeChange('self-driving')}
|
|
>
|
|
<input
|
|
type="radio"
|
|
name="transportMode"
|
|
value="self-driving"
|
|
checked={formData.transportMode === 'self-driving'}
|
|
onChange={() => handleTransportModeChange('self-driving')}
|
|
className="form-radio mr-3"
|
|
/>
|
|
<span className="font-medium">Self-Driving</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Flight Mode Fields */}
|
|
{formData.transportMode === 'flight' && formData.flights && (
|
|
<div className="space-y-6">
|
|
{formData.flights.map((flight, index) => (
|
|
<div key={index} className="bg-white border-2 border-blue-200 rounded-xl p-6 shadow-sm">
|
|
<div className="flex justify-between items-center mb-4">
|
|
<h4 className="text-lg font-bold text-slate-800">
|
|
{index === 0 ? 'Primary Flight' : `Connecting Flight ${index}`}
|
|
</h4>
|
|
{index > 0 && (
|
|
<button
|
|
type="button"
|
|
onClick={() => removeConnectingFlight(index)}
|
|
className="text-red-500 hover:text-red-700 font-medium text-sm bg-red-50 hover:bg-red-100 px-3 py-1 rounded-lg transition-colors duration-200"
|
|
>
|
|
Remove
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
|
<div className="form-group">
|
|
<label htmlFor={`flightNumber-${index}`} className="form-label">Flight Number *</label>
|
|
<input
|
|
type="text"
|
|
id={`flightNumber-${index}`}
|
|
value={flight.flightNumber}
|
|
onChange={(e) => handleFlightChange(index, 'flightNumber', e.target.value)}
|
|
className="form-input"
|
|
placeholder="e.g., AA123"
|
|
required={index === 0}
|
|
/>
|
|
</div>
|
|
|
|
<div className="form-group">
|
|
<label htmlFor={`flightDate-${index}`} className="form-label">Flight Date *</label>
|
|
<input
|
|
type="date"
|
|
id={`flightDate-${index}`}
|
|
value={flight.flightDate}
|
|
onChange={(e) => handleFlightChange(index, 'flightDate', e.target.value)}
|
|
className="form-input"
|
|
required={index === 0}
|
|
min={new Date().toISOString().split('T')[0]}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
type="button"
|
|
className="btn btn-secondary w-full"
|
|
onClick={() => validateFlight(index)}
|
|
disabled={flightValidating[index] || !flight.flightNumber || !flight.flightDate}
|
|
>
|
|
{flightValidating[index] ? (
|
|
<>
|
|
<span className="animate-spin inline-block w-4 h-4 border-2 border-white border-t-transparent rounded-full mr-2"></span>
|
|
Validating Flight...
|
|
</>
|
|
) : (
|
|
<>Validate Flight</>
|
|
)}
|
|
</button>
|
|
|
|
{/* Flight Validation Results */}
|
|
{flightErrors[index] && (
|
|
<div className="mt-4 bg-red-50 border border-red-200 rounded-lg p-4">
|
|
<div className="text-red-700 font-medium">{flightErrors[index]}</div>
|
|
</div>
|
|
)}
|
|
|
|
{flight.validated && flight.validationData && (
|
|
<div className="mt-4 bg-green-50 border border-green-200 rounded-lg p-4">
|
|
<div className="text-green-700 font-medium mb-2">
|
|
Valid: {flight.validationData.airline || 'Flight'} - {flight.validationData.departure?.airport} → {flight.validationData.arrival?.airport}
|
|
</div>
|
|
{flight.validationData.flightDate !== flight.flightDate && (
|
|
<div className="text-sm text-green-600">
|
|
Live tracking starts 4 hours before departure on {new Date(flight.flightDate).toLocaleDateString()}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
|
|
{formData.flights.length < 3 && (
|
|
<button
|
|
type="button"
|
|
className="btn btn-secondary w-full"
|
|
onClick={addConnectingFlight}
|
|
>
|
|
Add Connecting Flight
|
|
</button>
|
|
)}
|
|
|
|
<div className="checkbox-option checked">
|
|
<input
|
|
type="checkbox"
|
|
name="needsAirportPickup"
|
|
checked={formData.needsAirportPickup || false}
|
|
onChange={handleChange}
|
|
className="form-checkbox mr-3"
|
|
/>
|
|
<span className="font-medium">Needs Airport Pickup (from final destination)</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Self-Driving Mode Fields */}
|
|
{formData.transportMode === 'self-driving' && (
|
|
<div className="form-group">
|
|
<label htmlFor="expectedArrival" className="form-label">Expected Arrival *</label>
|
|
<input
|
|
type="datetime-local"
|
|
id="expectedArrival"
|
|
name="expectedArrival"
|
|
value={formData.expectedArrival}
|
|
onChange={handleChange}
|
|
className="form-input"
|
|
required
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Universal Transportation Option */}
|
|
<div className={`checkbox-option ${formData.needsVenueTransport ? 'checked' : ''}`}>
|
|
<input
|
|
type="checkbox"
|
|
name="needsVenueTransport"
|
|
checked={formData.needsVenueTransport}
|
|
onChange={handleChange}
|
|
className="form-checkbox mr-3"
|
|
/>
|
|
<div>
|
|
<span className="font-medium">Needs Transportation Between Venues</span>
|
|
<div className="text-sm text-slate-500 mt-1">
|
|
Check this if the VIP needs rides between different event locations
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Additional Information Section */}
|
|
<div className="form-section">
|
|
<div className="form-section-header">
|
|
<h3 className="form-section-title">Additional Information</h3>
|
|
</div>
|
|
|
|
<div className="form-group">
|
|
<label htmlFor="notes" className="form-label">Additional Notes</label>
|
|
<textarea
|
|
id="notes"
|
|
name="notes"
|
|
value={formData.notes}
|
|
onChange={handleChange}
|
|
rows={4}
|
|
className="form-textarea"
|
|
placeholder="Special requirements, dietary restrictions, accessibility needs, etc."
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Form Actions */}
|
|
<div className="form-actions">
|
|
<button type="button" className="btn btn-secondary" onClick={onCancel}>
|
|
Cancel
|
|
</button>
|
|
<button type="submit" className="btn btn-primary">
|
|
Add VIP
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default VipForm;
|