Backup: 2025-06-07 19:48 - Script test

[Restore from backup: vip-coordinator-backup-2025-06-07-19-48-script-test]
This commit is contained in:
2025-06-07 19:48:00 +02:00
parent 8fb00ec041
commit dc4655cef4
103 changed files with 16396 additions and 6143 deletions

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { apiCall } from '../config/api';
import { generateTestVips, getTestOrganizations, generateVipSchedule } from '../utils/testVipData';
@@ -244,7 +244,7 @@ const AdminDashboard: React.FC = () => {
const vipData = testVips[i];
try {
const scheduleEvents = generateVipSchedule(vipData.name, vipData.department, vipData.transportMode);
const scheduleEvents = generateVipSchedule(vipData.department, vipData.transportMode);
for (const event of scheduleEvents) {
try {
@@ -548,8 +548,8 @@ const AdminDashboard: React.FC = () => {
<li>Create or select a project</li>
<li>Enable the Google+ API</li>
<li>Go to "Credentials" "Create Credentials" "OAuth 2.0 Client IDs"</li>
<li>Set authorized redirect URI: http://bsa.madeamess.online:3000/auth/google/callback</li>
<li>Set authorized JavaScript origins: http://bsa.madeamess.online:5173</li>
<li>Set authorized redirect URI: https://your-domain.com/auth/google/callback</li>
<li>Set authorized JavaScript origins: https://your-domain.com</li>
</ol>
</div>
</div>

View File

@@ -0,0 +1,800 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { apiCall } from '../utils/api';
import { generateTestVips, getTestOrganizations, generateVipSchedule } from '../utils/testVipData';
interface User {
id: string;
email: string;
name: string;
role: string;
}
interface ApiKeys {
aviationStackKey?: string;
googleMapsKey?: string;
twilioKey?: string;
googleClientId?: string;
googleClientSecret?: string;
}
interface SystemSettings {
defaultPickupLocation?: string;
defaultDropoffLocation?: string;
timeZone?: string;
notificationsEnabled?: boolean;
}
const AdminDashboard: React.FC = () => {
const navigate = useNavigate();
const [user, setUser] = useState<User | null>(null);
const [apiKeys, setApiKeys] = useState<ApiKeys>({});
const [systemSettings, setSystemSettings] = useState<SystemSettings>({});
const [testResults, setTestResults] = useState<{ [key: string]: string }>({});
const [loading, setLoading] = useState(false);
const [saveStatus, setSaveStatus] = useState<string | null>(null);
const [showKeys, setShowKeys] = useState<{ [key: string]: boolean }>({});
const [savedKeys, setSavedKeys] = useState<{ [key: string]: boolean }>({});
const [testDataLoading, setTestDataLoading] = useState(false);
const [testDataStatus, setTestDataStatus] = useState<string | null>(null);
useEffect(() => {
// Check if user is authenticated and has admin role
const authToken = localStorage.getItem('authToken');
const userData = localStorage.getItem('user');
if (!authToken || !userData) {
navigate('/');
return;
}
const parsedUser = JSON.parse(userData);
if (parsedUser.role !== 'administrator' && parsedUser.role !== 'coordinator') {
navigate('/dashboard');
return;
}
setUser(parsedUser);
loadSettings();
}, [navigate]);
const loadSettings = async () => {
try {
const response = await apiCall('/api/admin/settings');
if (response.ok) {
const data = await response.json();
// Track which keys are already saved (masked keys start with ***)
const saved: { [key: string]: boolean } = {};
if (data.apiKeys) {
Object.entries(data.apiKeys).forEach(([key, value]) => {
if (value && (value as string).startsWith('***')) {
saved[key] = true;
}
});
}
setSavedKeys(saved);
// Don't load masked keys as actual values - keep them empty
const cleanedApiKeys: ApiKeys = {};
if (data.apiKeys) {
Object.entries(data.apiKeys).forEach(([key, value]) => {
// Only set the value if it's not a masked key
if (value && !(value as string).startsWith('***')) {
cleanedApiKeys[key as keyof ApiKeys] = value as string;
}
});
}
setApiKeys(cleanedApiKeys);
setSystemSettings(data.systemSettings || {});
}
} catch (error) {
console.error('Failed to load settings:', error);
}
};
const handleApiKeyChange = (key: keyof ApiKeys, value: string) => {
setApiKeys(prev => ({ ...prev, [key]: value }));
// If user is typing a new key, mark it as not saved anymore
if (value && !value.startsWith('***')) {
setSavedKeys(prev => ({ ...prev, [key]: false }));
}
};
const handleSettingChange = (key: keyof SystemSettings, value: any) => {
setSystemSettings(prev => ({ ...prev, [key]: value }));
};
const testApiConnection = async (apiType: string) => {
setTestResults(prev => ({ ...prev, [apiType]: 'Testing...' }));
try {
const response = await fetch(`/api/admin/test-api/${apiType}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Admin-Auth': sessionStorage.getItem('adminAuthenticated') || ''
},
body: JSON.stringify({
apiKey: apiKeys[apiType as keyof ApiKeys]
})
});
const result = await response.json();
if (response.ok) {
setTestResults(prev => ({
...prev,
[apiType]: `Success: ${result.message}`
}));
} else {
setTestResults(prev => ({
...prev,
[apiType]: `Failed: ${result.error}`
}));
}
} catch (error) {
setTestResults(prev => ({
...prev,
[apiType]: 'Connection error'
}));
}
};
const saveSettings = async () => {
setLoading(true);
setSaveStatus(null);
try {
const response = await fetch('/api/admin/settings', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Admin-Auth': sessionStorage.getItem('adminAuthenticated') || ''
},
body: JSON.stringify({
apiKeys,
systemSettings
})
});
if (response.ok) {
setSaveStatus('Settings saved successfully!');
// Mark keys as saved if they have values
const newSavedKeys: { [key: string]: boolean } = {};
Object.entries(apiKeys).forEach(([key, value]) => {
if (value && !value.startsWith('***')) {
newSavedKeys[key] = true;
}
});
setSavedKeys(prev => ({ ...prev, ...newSavedKeys }));
// Clear the input fields after successful save
setApiKeys({});
setTimeout(() => setSaveStatus(null), 3000);
} else {
setSaveStatus('Failed to save settings');
}
} catch (error) {
setSaveStatus('Error saving settings');
} finally {
setLoading(false);
}
};
const handleLogout = () => {
sessionStorage.removeItem('adminAuthenticated');
setIsAuthenticated(false);
navigate('/');
};
// Test VIP functions
const createTestVips = async () => {
setTestDataLoading(true);
setTestDataStatus('Creating test VIPs and schedules...');
try {
const token = localStorage.getItem('authToken');
const testVips = generateTestVips();
let vipSuccessCount = 0;
let vipErrorCount = 0;
let scheduleSuccessCount = 0;
let scheduleErrorCount = 0;
const createdVipIds: string[] = [];
// First, create all VIPs
for (const vipData of testVips) {
try {
const response = await apiCall('/api/vips', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(vipData),
});
if (response.ok) {
const createdVip = await response.json();
createdVipIds.push(createdVip.id);
vipSuccessCount++;
} else {
vipErrorCount++;
console.error(`Failed to create VIP: ${vipData.name}`);
}
} catch (error) {
vipErrorCount++;
console.error(`Error creating VIP ${vipData.name}:`, error);
}
}
setTestDataStatus(`Created ${vipSuccessCount} VIPs, now creating schedules...`);
// Then, create schedules for each successfully created VIP
for (let i = 0; i < createdVipIds.length; i++) {
const vipId = createdVipIds[i];
const vipData = testVips[i];
try {
const scheduleEvents = generateVipSchedule(vipData.department, vipData.transportMode);
for (const event of scheduleEvents) {
try {
const eventWithId = {
...event,
id: Date.now().toString() + Math.random().toString(36).substr(2, 9)
};
const scheduleResponse = await apiCall(`/api/vips/${vipId}/schedule`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(eventWithId),
});
if (scheduleResponse.ok) {
scheduleSuccessCount++;
} else {
scheduleErrorCount++;
console.error(`Failed to create schedule event for ${vipData.name}: ${event.title}`);
}
} catch (error) {
scheduleErrorCount++;
console.error(`Error creating schedule event for ${vipData.name}:`, error);
}
}
} catch (error) {
console.error(`Error generating schedule for ${vipData.name}:`, error);
}
}
setTestDataStatus(`✅ Created ${vipSuccessCount} VIPs with ${scheduleSuccessCount} schedule events! ${vipErrorCount > 0 || scheduleErrorCount > 0 ? `(${vipErrorCount + scheduleErrorCount} failed)` : ''}`);
} catch (error) {
setTestDataStatus('❌ Failed to create test VIPs and schedules');
console.error('Error creating test data:', error);
} finally {
setTestDataLoading(false);
setTimeout(() => setTestDataStatus(null), 8000);
}
};
const removeTestVips = async () => {
if (!confirm('Are you sure you want to remove all test VIPs? This will delete VIPs from the test organizations.')) {
return;
}
setTestDataLoading(true);
setTestDataStatus('Removing test VIPs...');
try {
const token = localStorage.getItem('authToken');
// First, get all VIPs
const response = await apiCall('/api/vips', {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error('Failed to fetch VIPs');
}
const allVips = await response.json();
// Filter test VIPs by organization names
const testOrganizations = getTestOrganizations();
const testVips = allVips.filter((vip: any) => testOrganizations.includes(vip.organization));
let successCount = 0;
let errorCount = 0;
for (const vip of testVips) {
try {
const deleteResponse = await apiCall(`/api/vips/${vip.id}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (deleteResponse.ok) {
successCount++;
} else {
errorCount++;
console.error(`Failed to delete VIP: ${vip.name}`);
}
} catch (error) {
errorCount++;
console.error(`Error deleting VIP ${vip.name}:`, error);
}
}
setTestDataStatus(`🗑️ Removed ${successCount} test VIPs successfully! ${errorCount > 0 ? `(${errorCount} failed)` : ''}`);
} catch (error) {
setTestDataStatus('❌ Failed to remove test VIPs');
console.error('Error removing test VIPs:', error);
} finally {
setTestDataLoading(false);
setTimeout(() => setTestDataStatus(null), 5000);
}
};
if (!user) {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 flex justify-center items-center">
<div className="text-center">
<div className="animate-spin rounded-full h-16 w-16 border-4 border-amber-500 border-t-transparent mx-auto"></div>
<p className="mt-4 text-slate-600">Loading...</p>
</div>
</div>
);
}
return (
<div className="space-y-8">
{/* Header */}
<div className="bg-white rounded-2xl shadow-lg p-8 border border-slate-200/60">
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold bg-gradient-to-r from-slate-800 to-slate-600 bg-clip-text text-transparent">
Admin Dashboard
</h1>
<p className="text-slate-600 mt-2">System configuration and API management</p>
</div>
<div className="flex items-center space-x-4">
<button
className="btn btn-secondary"
onClick={() => navigate('/')}
>
Back to Dashboard
</button>
<button
className="btn btn-danger"
onClick={handleLogout}
>
Logout
</button>
</div>
</div>
</div>
{/* API Keys Section */}
<div className="bg-white rounded-2xl shadow-lg border border-slate-200/60 overflow-hidden">
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 px-8 py-6 border-b border-slate-200/60">
<h2 className="text-xl font-bold text-slate-800">API Key Management</h2>
<p className="text-slate-600 mt-1">Configure external service integrations</p>
</div>
<div className="p-8 space-y-8">
{/* AviationStack API */}
<div className="form-section">
<div className="form-section-header">
<h3 className="form-section-title">AviationStack API</h3>
{savedKeys.aviationStackKey && (
<span className="bg-green-100 text-green-800 text-xs font-medium px-2.5 py-0.5 rounded-full">
Configured
</span>
)}
</div>
<div className="grid grid-cols-1 lg:grid-cols-4 gap-4 items-end">
<div className="lg:col-span-2">
<label className="form-label">API Key</label>
<div className="relative">
<input
type={showKeys.aviationStackKey ? 'text' : 'password'}
placeholder={savedKeys.aviationStackKey ? 'Key saved (enter new key to update)' : 'Enter AviationStack API key'}
value={apiKeys.aviationStackKey || ''}
onChange={(e) => handleApiKeyChange('aviationStackKey', e.target.value)}
className="form-input pr-12"
/>
{savedKeys.aviationStackKey && (
<button
type="button"
onClick={() => setShowKeys(prev => ({ ...prev, aviationStackKey: !prev.aviationStackKey }))}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-slate-400 hover:text-slate-600"
>
{showKeys.aviationStackKey ? 'Hide' : 'Show'}
</button>
)}
</div>
<p className="text-xs text-slate-500 mt-1">
Get your key from: https://aviationstack.com/dashboard
</p>
</div>
<div>
<button
className="btn btn-secondary w-full"
onClick={() => testApiConnection('aviationStackKey')}
>
Test Connection
</button>
</div>
<div>
{testResults.aviationStackKey && (
<div className={`p-3 rounded-lg text-sm ${
testResults.aviationStackKey.includes('Success')
? 'bg-green-50 text-green-700 border border-green-200'
: 'bg-red-50 text-red-700 border border-red-200'
}`}>
{testResults.aviationStackKey}
</div>
)}
</div>
</div>
</div>
{/* Google OAuth Credentials */}
<div className="form-section">
<div className="form-section-header">
<h3 className="form-section-title">Google OAuth Credentials</h3>
{(savedKeys.googleClientId && savedKeys.googleClientSecret) && (
<span className="bg-green-100 text-green-800 text-xs font-medium px-2.5 py-0.5 rounded-full">
Configured
</span>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="form-group">
<label className="form-label">Client ID</label>
<div className="relative">
<input
type={showKeys.googleClientId ? 'text' : 'password'}
placeholder={savedKeys.googleClientId ? 'Client ID saved' : 'Enter Google OAuth Client ID'}
value={apiKeys.googleClientId || ''}
onChange={(e) => handleApiKeyChange('googleClientId', e.target.value)}
className="form-input pr-12"
/>
{savedKeys.googleClientId && (
<button
type="button"
onClick={() => setShowKeys(prev => ({ ...prev, googleClientId: !prev.googleClientId }))}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-slate-400 hover:text-slate-600"
>
{showKeys.googleClientId ? 'Hide' : 'Show'}
</button>
)}
</div>
</div>
<div className="form-group">
<label className="form-label">Client Secret</label>
<div className="relative">
<input
type={showKeys.googleClientSecret ? 'text' : 'password'}
placeholder={savedKeys.googleClientSecret ? 'Client Secret saved' : 'Enter Google OAuth Client Secret'}
value={apiKeys.googleClientSecret || ''}
onChange={(e) => handleApiKeyChange('googleClientSecret', e.target.value)}
className="form-input pr-12"
/>
{savedKeys.googleClientSecret && (
<button
type="button"
onClick={() => setShowKeys(prev => ({ ...prev, googleClientSecret: !prev.googleClientSecret }))}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-slate-400 hover:text-slate-600"
>
{showKeys.googleClientSecret ? 'Hide' : 'Show'}
</button>
)}
</div>
</div>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mt-4">
<h4 className="font-semibold text-blue-900 mb-2">Setup Instructions</h4>
<ol className="text-sm text-blue-800 space-y-1 list-decimal list-inside">
<li>Go to Google Cloud Console</li>
<li>Create or select a project</li>
<li>Enable the Google+ API</li>
<li>Go to "Credentials" "Create Credentials" "OAuth 2.0 Client IDs"</li>
<li>Set authorized redirect URI: https://your-domain.com/auth/google/callback</li>
<li>Set authorized JavaScript origins: https://your-domain.com</li>
</ol>
</div>
</div>
{/* Future APIs */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 opacity-50">
<div className="form-section">
<div className="form-section-header">
<h3 className="form-section-title">Google Maps API</h3>
<span className="bg-gray-100 text-gray-600 text-xs font-medium px-2.5 py-0.5 rounded-full">
Coming Soon
</span>
</div>
<input
type="password"
placeholder="Google Maps API key (not yet implemented)"
disabled
className="form-input"
/>
</div>
<div className="form-section">
<div className="form-section-header">
<h3 className="form-section-title">Twilio API</h3>
<span className="bg-gray-100 text-gray-600 text-xs font-medium px-2.5 py-0.5 rounded-full">
Coming Soon
</span>
</div>
<input
type="password"
placeholder="Twilio API key (not yet implemented)"
disabled
className="form-input"
/>
</div>
</div>
</div>
</div>
{/* System Settings Section */}
<div className="bg-white rounded-2xl shadow-lg border border-slate-200/60 overflow-hidden">
<div className="bg-gradient-to-r from-green-50 to-emerald-50 px-8 py-6 border-b border-slate-200/60">
<h2 className="text-xl font-bold text-slate-800">System Settings</h2>
<p className="text-slate-600 mt-1">Configure default system behavior</p>
</div>
<div className="p-8">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="form-group">
<label htmlFor="defaultPickup" className="form-label">Default Pickup Location</label>
<input
type="text"
id="defaultPickup"
value={systemSettings.defaultPickupLocation || ''}
onChange={(e) => handleSettingChange('defaultPickupLocation', e.target.value)}
placeholder="e.g., JFK Airport Terminal 4"
className="form-input"
/>
</div>
<div className="form-group">
<label htmlFor="defaultDropoff" className="form-label">Default Dropoff Location</label>
<input
type="text"
id="defaultDropoff"
value={systemSettings.defaultDropoffLocation || ''}
onChange={(e) => handleSettingChange('defaultDropoffLocation', e.target.value)}
placeholder="e.g., Hilton Downtown"
className="form-input"
/>
</div>
<div className="form-group">
<label htmlFor="timezone" className="form-label">Time Zone</label>
<select
id="timezone"
value={systemSettings.timeZone || 'America/New_York'}
onChange={(e) => handleSettingChange('timeZone', e.target.value)}
className="form-select"
>
<option value="America/New_York">Eastern Time</option>
<option value="America/Chicago">Central Time</option>
<option value="America/Denver">Mountain Time</option>
<option value="America/Los_Angeles">Pacific Time</option>
<option value="UTC">UTC</option>
</select>
</div>
<div className="form-group">
<div className="checkbox-option">
<input
type="checkbox"
checked={systemSettings.notificationsEnabled || false}
onChange={(e) => handleSettingChange('notificationsEnabled', e.target.checked)}
className="form-checkbox mr-3"
/>
<span className="font-medium">Enable Email/SMS Notifications</span>
</div>
</div>
</div>
</div>
</div>
{/* Test VIP Data Section */}
<div className="bg-white rounded-2xl shadow-lg border border-slate-200/60 overflow-hidden">
<div className="bg-gradient-to-r from-orange-50 to-red-50 px-8 py-6 border-b border-slate-200/60">
<h2 className="text-xl font-bold text-slate-800">Test VIP Data Management</h2>
<p className="text-slate-600 mt-1">Create and manage test VIP data for application testing</p>
</div>
<div className="p-8">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-green-50 border border-green-200 rounded-xl p-6">
<h3 className="text-lg font-bold text-slate-800 mb-3">Create Test VIPs</h3>
<p className="text-slate-600 mb-4">
Generate 20 diverse test VIPs (10 Admin department, 10 Office of Development) with realistic data including flights, transport modes, and special requirements.
</p>
<ul className="text-sm text-slate-600 mb-4 space-y-1">
<li> Mixed flight and self-driving transport modes</li>
<li> Single flights, connecting flights, and multi-segment journeys</li>
<li> Diverse organizations and special requirements</li>
<li> Realistic arrival dates (tomorrow and day after)</li>
</ul>
<button
className="btn btn-success w-full"
onClick={createTestVips}
disabled={testDataLoading}
>
{testDataLoading ? (
<>
<span className="animate-spin inline-block w-4 h-4 border-2 border-white border-t-transparent rounded-full mr-2"></span>
Creating Test VIPs...
</>
) : (
'🎭 Create 20 Test VIPs'
)}
</button>
</div>
<div className="bg-red-50 border border-red-200 rounded-xl p-6">
<h3 className="text-lg font-bold text-slate-800 mb-3">Remove Test VIPs</h3>
<p className="text-slate-600 mb-4">
Remove all test VIPs from the system. This will delete VIPs from the following test organizations:
</p>
<div className="text-xs text-slate-500 mb-4 max-h-20 overflow-y-auto">
<div className="grid grid-cols-1 gap-1">
{getTestOrganizations().slice(0, 8).map(org => (
<div key={org}> {org}</div>
))}
<div className="text-slate-400">... and 12 more organizations</div>
</div>
</div>
<button
className="btn btn-danger w-full"
onClick={removeTestVips}
disabled={testDataLoading}
>
{testDataLoading ? (
<>
<span className="animate-spin inline-block w-4 h-4 border-2 border-white border-t-transparent rounded-full mr-2"></span>
Removing Test VIPs...
</>
) : (
'🗑️ Remove All Test VIPs'
)}
</button>
</div>
</div>
{testDataStatus && (
<div className={`mt-6 p-4 rounded-lg text-center font-medium ${
testDataStatus.includes('✅') || testDataStatus.includes('🗑️')
? 'bg-green-50 text-green-700 border border-green-200'
: 'bg-red-50 text-red-700 border border-red-200'
}`}>
{testDataStatus}
</div>
)}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mt-6">
<h4 className="font-semibold text-blue-900 mb-2">💡 Test Data Details</h4>
<div className="text-sm text-blue-800 space-y-1">
<p><strong>Admin Department (10 VIPs):</strong> University officials, ambassadors, ministers, and executives</p>
<p><strong>Office of Development (10 VIPs):</strong> Donors, foundation leaders, and philanthropists</p>
<p><strong>Transport Modes:</strong> Mix of flights (single, connecting, multi-segment) and self-driving</p>
<p><strong>Special Requirements:</strong> Dietary restrictions, accessibility needs, security details, interpreters</p>
<p><strong>Full Day Schedules:</strong> Each VIP gets 5-7 realistic events including meetings, meals, tours, and presentations</p>
<p><strong>Schedule Types:</strong> Airport pickup, welcome breakfast, department meetings, working lunches, campus tours, receptions</p>
</div>
</div>
</div>
</div>
{/* API Documentation Section */}
<div className="bg-white rounded-2xl shadow-lg border border-slate-200/60 overflow-hidden">
<div className="bg-gradient-to-r from-purple-50 to-pink-50 px-8 py-6 border-b border-slate-200/60">
<h2 className="text-xl font-bold text-slate-800">API Documentation</h2>
<p className="text-slate-600 mt-1">Developer resources and API testing</p>
</div>
<div className="p-8">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-blue-50 border border-blue-200 rounded-xl p-6">
<h3 className="text-lg font-bold text-slate-800 mb-3">Interactive API Documentation</h3>
<p className="text-slate-600 mb-4">
Explore and test all API endpoints with the interactive Swagger UI documentation.
</p>
<button
className="btn btn-primary w-full mb-2"
onClick={() => window.open('http://localhost:3000/api-docs.html', '_blank')}
>
Open API Documentation
</button>
<p className="text-xs text-slate-500">
Opens in a new tab with full endpoint documentation and testing capabilities
</p>
</div>
<div className="bg-green-50 border border-green-200 rounded-xl p-6">
<h3 className="text-lg font-bold text-slate-800 mb-3">Quick API Examples</h3>
<div className="space-y-2 text-sm">
<div>
<span className="font-medium">Health Check:</span>
<code className="ml-2 bg-white px-2 py-1 rounded text-xs">GET /api/health</code>
</div>
<div>
<span className="font-medium">Get VIPs:</span>
<code className="ml-2 bg-white px-2 py-1 rounded text-xs">GET /api/vips</code>
</div>
<div>
<span className="font-medium">Get Drivers:</span>
<code className="ml-2 bg-white px-2 py-1 rounded text-xs">GET /api/drivers</code>
</div>
<div>
<span className="font-medium">Flight Info:</span>
<code className="ml-2 bg-white px-2 py-1 rounded text-xs">GET /api/flights/UA1234</code>
</div>
</div>
<button
className="btn btn-secondary w-full mt-4"
onClick={() => window.open('/README-API.md', '_blank')}
>
View API Guide
</button>
</div>
</div>
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4 mt-6">
<p className="text-amber-800">
<strong>Pro Tip:</strong> The interactive documentation allows you to test API endpoints directly in your browser.
Perfect for developers integrating with the VIP Coordinator system!
</p>
</div>
</div>
</div>
{/* Save Button */}
<div className="text-center">
<button
className="btn btn-success text-lg px-8 py-4"
onClick={saveSettings}
disabled={loading}
>
{loading ? 'Saving...' : 'Save All Settings'}
</button>
{saveStatus && (
<div className={`mt-4 p-4 rounded-lg ${
saveStatus.includes('successfully')
? 'bg-green-50 text-green-700 border border-green-200'
: 'bg-red-50 text-red-700 border border-red-200'
}`}>
{saveStatus}
</div>
)}
</div>
</div>
);
};
export default AdminDashboard;

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import { useState, useEffect } from 'react';
import { useParams, Link } from 'react-router-dom';
import { apiCall } from '../config/api';
import GanttChart from '../components/GanttChart';
@@ -90,13 +90,6 @@ const DriverDashboard: React.FC = () => {
});
};
const formatDate = (timeString: string) => {
return new Date(timeString).toLocaleDateString([], {
weekday: 'short',
month: 'short',
day: 'numeric'
});
};
const getNextEvent = () => {
if (!scheduleData?.schedule) return null;

View File

@@ -0,0 +1,112 @@
import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { apiCall } from '../utils/api';
import { User } from '../types';
const PendingApproval: React.FC = () => {
const navigate = useNavigate();
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Get user data from localStorage
const savedUser = localStorage.getItem('user');
if (savedUser) {
setUser(JSON.parse(savedUser));
}
setLoading(false);
// Check status every 30 seconds
const interval = setInterval(checkUserStatus, 30000);
return () => clearInterval(interval);
}, []);
const checkUserStatus = async () => {
try {
const token = localStorage.getItem('authToken');
const { data: userData } = await apiCall('/auth/users/me', {
headers: {
'Authorization': `Bearer ${token}`,
},
});
if (userData) {
setUser(userData);
// If user is approved, redirect to dashboard
if (userData.status === 'active') {
window.location.href = '/';
}
}
} catch (error) {
console.error('Error checking user status:', error);
}
};
const handleLogout = () => {
localStorage.removeItem('authToken');
localStorage.removeItem('user');
window.location.href = '/';
};
if (loading) {
return (
<div className="min-h-screen bg-gradient-to-br from-amber-50 via-orange-50 to-yellow-50 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-16 w-16 border-4 border-amber-500 border-t-transparent mx-auto"></div>
<p className="mt-4 text-slate-600">Loading...</p>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gradient-to-br from-amber-50 via-orange-50 to-yellow-50 flex items-center justify-center p-4">
<div className="bg-white rounded-3xl shadow-2xl max-w-md w-full p-8 text-center relative overflow-hidden">
{/* Decorative element */}
<div className="absolute top-0 right-0 w-40 h-40 bg-gradient-to-br from-amber-200 to-orange-200 rounded-full blur-3xl opacity-30 -translate-y-20 translate-x-20"></div>
<div className="relative z-10">
{/* Icon */}
<div className="w-24 h-24 bg-gradient-to-br from-amber-400 to-orange-500 rounded-full flex items-center justify-center mx-auto mb-6 shadow-lg">
<svg className="w-12 h-12 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
{/* Welcome message */}
<h1 className="text-3xl font-bold bg-gradient-to-r from-amber-600 to-orange-600 bg-clip-text text-transparent mb-2">
Welcome, {user?.name?.split(' ')[0]}!
</h1>
<p className="text-lg text-slate-600 mb-8">
Your account is being reviewed
</p>
{/* Status message */}
<div className="bg-gradient-to-r from-amber-50 to-orange-50 border border-amber-200 rounded-2xl p-6 mb-8">
<p className="text-amber-800 leading-relaxed">
Thank you for signing up! An administrator will review your account request shortly.
We'll notify you once your access has been approved.
</p>
</div>
{/* Auto-refresh notice */}
<p className="text-sm text-slate-500 mb-8">
This page checks for updates automatically
</p>
{/* Logout button */}
<button
onClick={handleLogout}
className="w-full bg-gradient-to-r from-slate-600 to-slate-700 hover:from-slate-700 hover:to-slate-800 text-white font-medium py-3 px-6 rounded-xl transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5"
>
Sign Out
</button>
</div>
</div>
</div>
);
};
export default PendingApproval;

View File

@@ -0,0 +1,111 @@
import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import { useApi, useMutation } from '../hooks/useApi';
import { vipApi } from '../api/client';
import VipForm from '../components/VipForm';
import { LoadingSpinner } from '../components/LoadingSpinner';
import { ErrorMessage } from '../components/ErrorMessage';
// Simplified VIP List - no more manual loading states, error handling, or token management
const SimplifiedVipList: React.FC = () => {
const [showForm, setShowForm] = useState(false);
const { data: vips, loading, error, refetch } = useApi(() => vipApi.list());
const createVip = useMutation(vipApi.create);
const deleteVip = useMutation(vipApi.delete);
const handleAddVip = async (vipData: any) => {
try {
await createVip.mutate(vipData);
setShowForm(false);
refetch(); // Refresh the list
} catch (error) {
// Error is already handled by the hook
}
};
const handleDeleteVip = async (id: string) => {
if (!confirm('Are you sure you want to delete this VIP?')) return;
try {
await deleteVip.mutate(id);
refetch(); // Refresh the list
} catch (error) {
// Error is already handled by the hook
}
};
if (loading) return <LoadingSpinner message="Loading VIPs..." />;
if (error) return <ErrorMessage message={error} onDismiss={() => refetch()} />;
if (!vips) return null;
return (
<div className="p-6">
<div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold">VIP Management</h1>
<button
onClick={() => setShowForm(true)}
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
>
Add VIP
</button>
</div>
{createVip.error && (
<ErrorMessage message={createVip.error} className="mb-4" />
)}
{deleteVip.error && (
<ErrorMessage message={deleteVip.error} className="mb-4" />
)}
<div className="grid gap-4">
{vips.map((vip) => (
<div key={vip.id} className="bg-white p-4 rounded-lg shadow">
<div className="flex justify-between items-start">
<div>
<h3 className="text-lg font-semibold">{vip.name}</h3>
<p className="text-gray-600">{vip.organization}</p>
<p className="text-sm text-gray-500">
{vip.transportMode === 'flight'
? `Flight: ${vip.flights?.[0]?.flightNumber || 'TBD'}`
: `Driving - Arrival: ${new Date(vip.expectedArrival).toLocaleString()}`
}
</p>
</div>
<div className="flex gap-2">
<Link
to={`/vips/${vip.id}`}
className="text-blue-500 hover:text-blue-700"
>
View Details
</Link>
<button
onClick={() => handleDeleteVip(vip.id)}
disabled={deleteVip.loading}
className="text-red-500 hover:text-red-700 disabled:opacity-50"
>
Delete
</button>
</div>
</div>
</div>
))}
</div>
{showForm && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4">
<div className="bg-white rounded-lg p-6 max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<h2 className="text-xl font-bold mb-4">Add New VIP</h2>
<VipForm
onSubmit={handleAddVip}
onCancel={() => setShowForm(false)}
/>
</div>
</div>
)}
</div>
);
};
export default SimplifiedVipList;