Files
vip-coordinator/frontend-old-20260125/src/components/UserOnboarding.tsx
kyle 868f7efc23
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
Major Enhancement: NestJS Migration + CASL Authorization + Error Handling
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>
2026-01-31 08:50:25 +01:00

257 lines
9.4 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;