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:
312
frontend/src/components/EventForm.tsx
Normal file
312
frontend/src/components/EventForm.tsx
Normal file
@@ -0,0 +1,312 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { X } from 'lucide-react';
|
||||
import { api } from '@/lib/api';
|
||||
|
||||
interface EventFormProps {
|
||||
event?: ScheduleEvent | null;
|
||||
onSubmit: (data: EventFormData) => void;
|
||||
onCancel: () => void;
|
||||
isSubmitting: boolean;
|
||||
}
|
||||
|
||||
interface ScheduleEvent {
|
||||
id: string;
|
||||
vipId: string;
|
||||
title: string;
|
||||
location: string | null;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
description: string | null;
|
||||
type: string;
|
||||
status: string;
|
||||
driverId: string | null;
|
||||
}
|
||||
|
||||
interface VIP {
|
||||
id: string;
|
||||
name: string;
|
||||
organization: string | null;
|
||||
}
|
||||
|
||||
interface Driver {
|
||||
id: string;
|
||||
name: string;
|
||||
phone: string;
|
||||
}
|
||||
|
||||
export interface EventFormData {
|
||||
vipId: string;
|
||||
title: string;
|
||||
location?: string;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
description?: string;
|
||||
type: string;
|
||||
status: string;
|
||||
driverId?: string;
|
||||
}
|
||||
|
||||
export function EventForm({ event, onSubmit, onCancel, isSubmitting }: EventFormProps) {
|
||||
// Helper to convert ISO datetime to datetime-local format
|
||||
const toDatetimeLocal = (isoString: string | null | undefined) => {
|
||||
if (!isoString) return '';
|
||||
const date = new Date(isoString);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
||||
};
|
||||
|
||||
const [formData, setFormData] = useState<EventFormData>({
|
||||
vipId: event?.vipId || '',
|
||||
title: event?.title || '',
|
||||
location: event?.location || '',
|
||||
startTime: toDatetimeLocal(event?.startTime),
|
||||
endTime: toDatetimeLocal(event?.endTime),
|
||||
description: event?.description || '',
|
||||
type: event?.type || 'TRANSPORT',
|
||||
status: event?.status || 'SCHEDULED',
|
||||
driverId: event?.driverId || '',
|
||||
});
|
||||
|
||||
// Fetch VIPs for dropdown
|
||||
const { data: vips } = useQuery<VIP[]>({
|
||||
queryKey: ['vips'],
|
||||
queryFn: async () => {
|
||||
const { data } = await api.get('/vips');
|
||||
return data;
|
||||
},
|
||||
});
|
||||
|
||||
// Fetch Drivers for dropdown
|
||||
const { data: drivers } = useQuery<Driver[]>({
|
||||
queryKey: ['drivers'],
|
||||
queryFn: async () => {
|
||||
const { data } = await api.get('/drivers');
|
||||
return data;
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Clean up the data - remove empty strings for optional fields and convert datetimes to ISO
|
||||
const cleanedData = {
|
||||
...formData,
|
||||
startTime: new Date(formData.startTime).toISOString(),
|
||||
endTime: new Date(formData.endTime).toISOString(),
|
||||
location: formData.location || undefined,
|
||||
description: formData.description || undefined,
|
||||
driverId: formData.driverId || undefined,
|
||||
};
|
||||
|
||||
onSubmit(cleanedData);
|
||||
};
|
||||
|
||||
const handleChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>
|
||||
) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
||||
<div className="bg-white rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
||||
<div className="flex items-center justify-between p-6 border-b">
|
||||
<h2 className="text-2xl font-bold text-gray-900">
|
||||
{event ? 'Edit Event' : 'Add New Event'}
|
||||
</h2>
|
||||
<button
|
||||
onClick={onCancel}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<X className="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="p-6 space-y-4">
|
||||
{/* VIP Selection */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
VIP *
|
||||
</label>
|
||||
<select
|
||||
name="vipId"
|
||||
required
|
||||
value={formData.vipId}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
>
|
||||
<option value="">Select VIP</option>
|
||||
{vips?.map((vip) => (
|
||||
<option key={vip.id} value={vip.id}>
|
||||
{vip.name} {vip.organization ? `(${vip.organization})` : ''}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Event Title *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="title"
|
||||
required
|
||||
value={formData.title}
|
||||
onChange={handleChange}
|
||||
placeholder="e.g., Airport Pickup, Lunch Meeting"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Location */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Location
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="location"
|
||||
value={formData.location}
|
||||
onChange={handleChange}
|
||||
placeholder="e.g., LaGuardia Airport, Main Conference Room"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Start & End Time */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Start Time *
|
||||
</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
name="startTime"
|
||||
required
|
||||
value={formData.startTime}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
End Time *
|
||||
</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
name="endTime"
|
||||
required
|
||||
value={formData.endTime}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Event Type */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Event Type *
|
||||
</label>
|
||||
<select
|
||||
name="type"
|
||||
required
|
||||
value={formData.type}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
>
|
||||
<option value="TRANSPORT">Transport</option>
|
||||
<option value="MEETING">Meeting</option>
|
||||
<option value="EVENT">Event</option>
|
||||
<option value="MEAL">Meal</option>
|
||||
<option value="ACCOMMODATION">Accommodation</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Status */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Status *
|
||||
</label>
|
||||
<select
|
||||
name="status"
|
||||
required
|
||||
value={formData.status}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
>
|
||||
<option value="SCHEDULED">Scheduled</option>
|
||||
<option value="IN_PROGRESS">In Progress</option>
|
||||
<option value="COMPLETED">Completed</option>
|
||||
<option value="CANCELLED">Cancelled</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Driver Selection */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Assigned Driver
|
||||
</label>
|
||||
<select
|
||||
name="driverId"
|
||||
value={formData.driverId}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
>
|
||||
<option value="">No driver assigned</option>
|
||||
{drivers?.map((driver) => (
|
||||
<option key={driver.id} value={driver.id}>
|
||||
{driver.name} ({driver.phone})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
name="description"
|
||||
value={formData.description}
|
||||
onChange={handleChange}
|
||||
rows={3}
|
||||
placeholder="Additional notes or instructions"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-3 pt-4">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="flex-1 bg-primary text-white py-2 px-4 rounded-md hover:bg-primary/90 disabled:opacity-50"
|
||||
>
|
||||
{isSubmitting ? 'Saving...' : event ? 'Update Event' : 'Create Event'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
className="flex-1 bg-gray-200 text-gray-800 py-2 px-4 rounded-md hover:bg-gray-300"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user