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>
257 lines
9.4 KiB
TypeScript
257 lines
9.4 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
||
import { useNavigate } from 'react-router-dom';
|
||
import { apiCall } from '../utils/api';
|
||
import { useToast } from '../contexts/ToastContext';
|
||
import { LoadingSpinner } from './LoadingSpinner';
|
||
|
||
interface OnboardingData {
|
||
requestedRole: 'coordinator' | 'driver' | 'viewer';
|
||
phone: string;
|
||
organization: string;
|
||
reason: string;
|
||
// Driver-specific fields
|
||
vehicleType?: string;
|
||
vehicleCapacity?: number;
|
||
licensePlate?: string;
|
||
}
|
||
|
||
const UserOnboarding: React.FC = () => {
|
||
const navigate = useNavigate();
|
||
const { showToast } = useToast();
|
||
const [loading, setLoading] = useState(false);
|
||
const [formData, setFormData] = useState<OnboardingData>({
|
||
requestedRole: 'viewer',
|
||
phone: '',
|
||
organization: '',
|
||
reason: '',
|
||
});
|
||
|
||
const handleSubmit = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
setLoading(true);
|
||
|
||
try {
|
||
const token = localStorage.getItem('authToken');
|
||
const response = await apiCall('/api/users/complete-onboarding', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${token}`,
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
onboardingData: formData,
|
||
phone: formData.phone,
|
||
organization: formData.organization,
|
||
}),
|
||
});
|
||
|
||
if (response.ok) {
|
||
showToast('Onboarding completed! Your account is pending approval.', 'success');
|
||
navigate('/pending-approval');
|
||
} else {
|
||
showToast('Failed to complete onboarding. Please try again.', 'error');
|
||
}
|
||
} catch (error) {
|
||
showToast('An error occurred. Please try again.', 'error');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const handleRoleChange = (role: 'coordinator' | 'driver' | 'viewer') => {
|
||
setFormData(prev => ({
|
||
...prev,
|
||
requestedRole: role,
|
||
// Clear driver fields if not driver
|
||
vehicleType: role === 'driver' ? prev.vehicleType : undefined,
|
||
vehicleCapacity: role === 'driver' ? prev.vehicleCapacity : undefined,
|
||
licensePlate: role === 'driver' ? prev.licensePlate : undefined,
|
||
}));
|
||
};
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 flex items-center justify-center p-4">
|
||
<div className="bg-white rounded-2xl shadow-xl max-w-2xl w-full p-8">
|
||
<div className="text-center mb-8">
|
||
<h1 className="text-3xl font-bold text-slate-800 mb-2">Welcome to VIP Coordinator</h1>
|
||
<p className="text-slate-600">Please complete your profile to request access</p>
|
||
</div>
|
||
|
||
<form onSubmit={handleSubmit} className="space-y-6">
|
||
{/* Role Selection */}
|
||
<div className="form-section">
|
||
<label className="block text-sm font-medium text-slate-700 mb-3">
|
||
What type of access do you need?
|
||
</label>
|
||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||
<button
|
||
type="button"
|
||
onClick={() => handleRoleChange('coordinator')}
|
||
className={`p-4 rounded-lg border-2 transition-all ${
|
||
formData.requestedRole === 'coordinator'
|
||
? 'border-amber-500 bg-amber-50'
|
||
: 'border-slate-200 hover:border-slate-300'
|
||
}`}
|
||
>
|
||
<div className="text-2xl mb-2">📋</div>
|
||
<div className="font-semibold text-slate-800">Coordinator</div>
|
||
<div className="text-xs text-slate-600 mt-1">Manage VIPs & schedules</div>
|
||
</button>
|
||
|
||
<button
|
||
type="button"
|
||
onClick={() => handleRoleChange('driver')}
|
||
className={`p-4 rounded-lg border-2 transition-all ${
|
||
formData.requestedRole === 'driver'
|
||
? 'border-amber-500 bg-amber-50'
|
||
: 'border-slate-200 hover:border-slate-300'
|
||
}`}
|
||
>
|
||
<div className="text-2xl mb-2">🚗</div>
|
||
<div className="font-semibold text-slate-800">Driver</div>
|
||
<div className="text-xs text-slate-600 mt-1">Transport VIPs</div>
|
||
</button>
|
||
|
||
<button
|
||
type="button"
|
||
onClick={() => handleRoleChange('viewer')}
|
||
className={`p-4 rounded-lg border-2 transition-all ${
|
||
formData.requestedRole === 'viewer'
|
||
? 'border-amber-500 bg-amber-50'
|
||
: 'border-slate-200 hover:border-slate-300'
|
||
}`}
|
||
>
|
||
<div className="text-2xl mb-2">👁️</div>
|
||
<div className="font-semibold text-slate-800">Viewer</div>
|
||
<div className="text-xs text-slate-600 mt-1">View-only access</div>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Common Fields */}
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||
Phone Number *
|
||
</label>
|
||
<input
|
||
type="tel"
|
||
required
|
||
value={formData.phone}
|
||
onChange={(e) => setFormData(prev => ({ ...prev, phone: e.target.value }))}
|
||
className="form-input w-full"
|
||
placeholder="+1 (555) 123-4567"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||
Organization *
|
||
</label>
|
||
<input
|
||
type="text"
|
||
required
|
||
value={formData.organization}
|
||
onChange={(e) => setFormData(prev => ({ ...prev, organization: e.target.value }))}
|
||
className="form-input w-full"
|
||
placeholder="Your company or department"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Driver-specific Fields */}
|
||
{formData.requestedRole === 'driver' && (
|
||
<div className="space-y-4 p-4 bg-blue-50 rounded-lg border border-blue-200">
|
||
<h3 className="font-semibold text-slate-800 mb-3">Driver Information</h3>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||
Vehicle Type *
|
||
</label>
|
||
<select
|
||
required
|
||
value={formData.vehicleType || ''}
|
||
onChange={(e) => setFormData(prev => ({ ...prev, vehicleType: e.target.value }))}
|
||
className="form-select w-full"
|
||
>
|
||
<option value="">Select vehicle type</option>
|
||
<option value="sedan">Sedan</option>
|
||
<option value="suv">SUV</option>
|
||
<option value="van">Van</option>
|
||
<option value="minibus">Minibus</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||
Passenger Capacity *
|
||
</label>
|
||
<input
|
||
type="number"
|
||
required
|
||
min="1"
|
||
max="20"
|
||
value={formData.vehicleCapacity || ''}
|
||
onChange={(e) => setFormData(prev => ({ ...prev, vehicleCapacity: parseInt(e.target.value) }))}
|
||
className="form-input w-full"
|
||
placeholder="4"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||
License Plate *
|
||
</label>
|
||
<input
|
||
type="text"
|
||
required
|
||
value={formData.licensePlate || ''}
|
||
onChange={(e) => setFormData(prev => ({ ...prev, licensePlate: e.target.value }))}
|
||
className="form-input w-full"
|
||
placeholder="ABC-1234"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Reason for Access */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||
Why do you need access? *
|
||
</label>
|
||
<textarea
|
||
required
|
||
rows={3}
|
||
value={formData.reason}
|
||
onChange={(e) => setFormData(prev => ({ ...prev, reason: e.target.value }))}
|
||
className="form-textarea w-full"
|
||
placeholder="Please explain your role and why you need access to the VIP Coordinator system..."
|
||
/>
|
||
</div>
|
||
|
||
{/* Submit Button */}
|
||
<div className="flex justify-end space-x-3 pt-4">
|
||
<button
|
||
type="button"
|
||
onClick={() => navigate('/')}
|
||
className="btn btn-secondary"
|
||
>
|
||
Cancel
|
||
</button>
|
||
<button
|
||
type="submit"
|
||
disabled={loading}
|
||
className="btn btn-primary"
|
||
>
|
||
{loading ? <LoadingSpinner size="sm" /> : 'Submit Request'}
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default UserOnboarding; |