Major Enhancement: NestJS Migration + CASL Authorization + Error Handling
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
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>
This commit is contained in:
459
frontend-old-20260125/src/components/VipForm.tsx
Normal file
459
frontend-old-20260125/src/components/VipForm.tsx
Normal file
@@ -0,0 +1,459 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user