Backup: 2025-06-08 00:29 - User and admin online ready for dockerhub

[Restore from backup: vip-coordinator-backup-2025-06-08-00-29-user and admin online ready for dockerhub]
This commit is contained in:
2025-06-08 00:29:00 +02:00
parent 035f76fdd3
commit 36cb8e8886
33 changed files with 3676 additions and 3527 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { apiCall } from '../config/api';
import { apiCall } from '../utils/api';
interface ScheduleEvent {
id: string;
@@ -83,28 +83,27 @@ const Dashboard: React.FC = () => {
'Content-Type': 'application/json'
};
const [vipsResponse, driversResponse] = await Promise.all([
const [vipsResult, driversResult] = await Promise.all([
apiCall('/api/vips', { headers: authHeaders }),
apiCall('/api/drivers', { headers: authHeaders })
]);
if (!vipsResponse.ok || !driversResponse.ok) {
const vipsData = vipsResult.data;
const driversData = driversResult.data;
if (!vipsData || !driversData) {
throw new Error('Failed to fetch data');
}
const vipsData = await vipsResponse.json();
const driversData = await driversResponse.json();
// Fetch schedule for each VIP and determine current/next events
const vipsWithSchedules = await Promise.all(
vipsData.map(async (vip: Vip) => {
try {
const scheduleResponse = await apiCall(`/api/vips/${vip.id}/schedule`, {
const { data: scheduleData } = await apiCall(`/api/vips/${vip.id}/schedule`, {
headers: authHeaders
});
if (scheduleResponse.ok) {
const scheduleData = await scheduleResponse.json();
if (scheduleData) {
const currentEvent = getCurrentEvent(scheduleData);
const nextEvent = getNextEvent(scheduleData);

View File

@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react';
import { useParams, Link } from 'react-router-dom';
import { apiCall } from '../config/api';
import { apiCall } from '../utils/api';
import GanttChart from '../components/GanttChart';
interface DriverScheduleEvent {
@@ -42,15 +42,14 @@ const DriverDashboard: React.FC = () => {
const fetchDriverSchedule = async () => {
try {
const token = localStorage.getItem('authToken');
const response = await apiCall(`/api/drivers/${driverId}/schedule`, {
const { data } = await apiCall(`/api/drivers/${driverId}/schedule`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
if (data) {
setScheduleData(data);
} else {
setError('Driver not found');

View File

@@ -1,23 +1,19 @@
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { apiCall } from '../config/api';
import { apiCall } from '../utils/api';
import DriverForm from '../components/DriverForm';
import EditDriverForm from '../components/EditDriverForm';
interface Driver {
id: string;
name: string;
phone: string;
currentLocation: { lat: number; lng: number };
assignedVipIds: string[];
vehicleCapacity?: number;
}
import { Driver, DriverFormData } from '../types';
import { useToast } from '../contexts/ToastContext';
import { LoadingSpinner } from '../components/LoadingSpinner';
const DriverList: React.FC = () => {
const { showToast } = useToast();
const [drivers, setDrivers] = useState<Driver[]>([]);
const [loading, setLoading] = useState(true);
const [showForm, setShowForm] = useState(false);
const [editingDriver, setEditingDriver] = useState<Driver | null>(null);
const [searchTerm, setSearchTerm] = useState('');
// Function to extract last name for sorting
const getLastName = (fullName: string) => {
@@ -38,19 +34,18 @@ const DriverList: React.FC = () => {
const fetchDrivers = async () => {
try {
const token = localStorage.getItem('authToken');
const response = await apiCall('/api/drivers', {
const { data } = await apiCall('/api/drivers', {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
if (data) {
const sortedDrivers = sortDriversByLastName(data);
setDrivers(sortedDrivers);
} else {
console.error('Failed to fetch drivers:', response.status);
console.error('Failed to fetch drivers');
}
} catch (error) {
console.error('Error fetching drivers:', error);
@@ -62,7 +57,7 @@ const DriverList: React.FC = () => {
fetchDrivers();
}, []);
const handleAddDriver = async (driverData: any) => {
const handleAddDriver = async (driverData: DriverFormData) => {
try {
const token = localStorage.getItem('authToken');
const response = await apiCall('/api/drivers', {
@@ -78,15 +73,18 @@ const DriverList: React.FC = () => {
const newDriver = await response.json();
setDrivers(prev => sortDriversByLastName([...prev, newDriver]));
setShowForm(false);
showToast('Driver added successfully!', 'success');
} else {
console.error('Failed to add driver:', response.status);
showToast('Failed to add driver. Please try again.', 'error');
}
} catch (error) {
console.error('Error adding driver:', error);
showToast('An error occurred while adding the driver.', 'error');
}
};
const handleEditDriver = async (driverData: any) => {
const handleEditDriver = async (driverData: DriverFormData) => {
try {
const token = localStorage.getItem('authToken');
const response = await apiCall(`/api/drivers/${driverData.id}`, {
@@ -104,11 +102,14 @@ const DriverList: React.FC = () => {
driver.id === updatedDriver.id ? updatedDriver : driver
)));
setEditingDriver(null);
showToast('Driver updated successfully!', 'success');
} else {
console.error('Failed to update driver:', response.status);
showToast('Failed to update driver. Please try again.', 'error');
}
} catch (error) {
console.error('Error updating driver:', error);
showToast('An error occurred while updating the driver.', 'error');
}
};
@@ -129,30 +130,39 @@ const DriverList: React.FC = () => {
if (response.ok) {
setDrivers(prev => prev.filter(driver => driver.id !== driverId));
showToast('Driver deleted successfully!', 'success');
} else {
console.error('Failed to delete driver:', response.status);
showToast('Failed to delete driver. Please try again.', 'error');
}
} catch (error) {
console.error('Error deleting driver:', error);
showToast('An error occurred while deleting the driver.', 'error');
}
};
if (loading) {
return (
<div className="flex justify-center items-center min-h-64">
<div className="bg-white rounded-2xl shadow-lg p-8 flex items-center space-x-4">
<div className="w-8 h-8 border-4 border-blue-600 border-t-transparent rounded-full animate-spin"></div>
<span className="text-lg font-medium text-slate-700">Loading drivers...</span>
</div>
<div className="flex justify-center items-center min-h-screen bg-gradient-to-br from-slate-50 to-slate-100">
<LoadingSpinner size="lg" message="Loading drivers..." />
</div>
);
}
// Filter drivers based on search term
const filteredDrivers = drivers.filter(driver => {
const searchLower = searchTerm.toLowerCase();
return (
driver.name.toLowerCase().includes(searchLower) ||
driver.phone.toLowerCase().includes(searchLower)
);
});
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 className="flex justify-between items-center mb-6">
<div>
<h1 className="text-3xl font-bold bg-gradient-to-r from-slate-800 to-slate-600 bg-clip-text text-transparent">
Driver Management
@@ -171,16 +181,53 @@ const DriverList: React.FC = () => {
</button>
</div>
</div>
{/* Search Bar */}
<div className="relative">
<input
type="text"
placeholder="Search by name or phone number..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full px-4 py-3 pl-12 border border-slate-200 rounded-lg focus:ring-2 focus:ring-amber-500 focus:border-transparent transition-all"
/>
<svg className="absolute left-4 top-3.5 h-5 w-5 text-slate-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
{searchTerm && (
<button
onClick={() => setSearchTerm('')}
className="absolute right-4 top-3.5 text-slate-400 hover:text-slate-600"
>
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
)}
</div>
</div>
{/* Search Results */}
{searchTerm && (
<div className="bg-amber-50 border border-amber-200 rounded-lg px-4 py-2 mb-4">
<p className="text-sm text-amber-800">
Found {filteredDrivers.length} result{filteredDrivers.length !== 1 ? 's' : ''} for "{searchTerm}"
</p>
</div>
)}
{/* Driver Grid */}
{drivers.length === 0 ? (
{filteredDrivers.length === 0 ? (
<div className="bg-white rounded-2xl shadow-lg p-12 border border-slate-200/60 text-center">
<div className="w-16 h-16 bg-slate-100 rounded-full flex items-center justify-center mx-auto mb-4">
<div className="w-8 h-8 bg-slate-300 rounded-full"></div>
</div>
<h3 className="text-lg font-semibold text-slate-800 mb-2">No Drivers Found</h3>
<p className="text-slate-600 mb-6">Get started by adding your first driver</p>
<h3 className="text-lg font-semibold text-slate-800 mb-2">
{searchTerm ? 'No Drivers Found' : 'No Drivers Added Yet'}
</h3>
<p className="text-slate-600 mb-6">
{searchTerm ? `No drivers match your search for "${searchTerm}"` : 'Get started by adding your first driver'}
</p>
<button
className="btn btn-primary"
onClick={() => setShowForm(true)}
@@ -190,7 +237,7 @@ const DriverList: React.FC = () => {
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{drivers.map((driver) => (
{filteredDrivers.map((driver) => (
<div key={driver.id} className="bg-white rounded-2xl shadow-lg border border-slate-200/60 overflow-hidden hover:shadow-xl transition-shadow duration-200">
<div className="p-6">
{/* Driver Header */}

View File

@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import { useParams, Link } from 'react-router-dom';
import { apiCall } from '../config/api';
import { apiCall } from '../utils/api';
import FlightStatus from '../components/FlightStatus';
import ScheduleManager from '../components/ScheduleManager';
@@ -37,15 +37,14 @@ const VipDetails: React.FC = () => {
const fetchVip = async () => {
try {
const token = localStorage.getItem('authToken');
const response = await apiCall('/api/vips', {
const { data: vips } = await apiCall('/api/vips', {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const vips = await response.json();
if (vips) {
const foundVip = vips.find((v: Vip) => v.id === id);
if (foundVip) {
@@ -74,15 +73,14 @@ const VipDetails: React.FC = () => {
if (vip) {
try {
const token = localStorage.getItem('authToken');
const response = await apiCall(`/api/vips/${vip.id}/schedule`, {
const { data: scheduleData } = await apiCall(`/api/vips/${vip.id}/schedule`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const scheduleData = await response.json();
if (scheduleData) {
setSchedule(scheduleData);
}
} catch (error) {

View File

@@ -1,9 +1,11 @@
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { apiCall } from '../config/api';
import { apiCall } from '../utils/api';
import VipForm from '../components/VipForm';
import EditVipForm from '../components/EditVipForm';
import FlightStatus from '../components/FlightStatus';
import { useToast } from '../contexts/ToastContext';
import { LoadingSpinner } from '../components/LoadingSpinner';
interface Vip {
id: string;
@@ -26,10 +28,13 @@ interface Vip {
}
const VipList: React.FC = () => {
const { showToast } = useToast();
const [vips, setVips] = useState<Vip[]>([]);
const [loading, setLoading] = useState(true);
const [showForm, setShowForm] = useState(false);
const [editingVip, setEditingVip] = useState<Vip | null>(null);
const [submitting, setSubmitting] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
// Function to extract last name for sorting
const getLastName = (fullName: string) => {
@@ -50,19 +55,18 @@ const VipList: React.FC = () => {
const fetchVips = async () => {
try {
const token = localStorage.getItem('authToken');
const response = await apiCall('/api/vips', {
const { data } = await apiCall('/api/vips', {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
if (data) {
const sortedVips = sortVipsByLastName(data);
setVips(sortedVips);
} else {
console.error('Failed to fetch VIPs:', response.status);
console.error('Failed to fetch VIPs');
}
} catch (error) {
console.error('Error fetching VIPs:', error);
@@ -75,6 +79,7 @@ const VipList: React.FC = () => {
}, []);
const handleAddVip = async (vipData: any) => {
setSubmitting(true);
try {
const token = localStorage.getItem('authToken');
const response = await apiCall('/api/vips', {
@@ -90,11 +95,15 @@ const VipList: React.FC = () => {
const newVip = await response.json();
setVips(prev => sortVipsByLastName([...prev, newVip]));
setShowForm(false);
showToast('VIP added successfully!', 'success');
} else {
console.error('Failed to add VIP:', response.status);
showToast('Failed to add VIP. Please try again.', 'error');
}
} catch (error) {
console.error('Error adding VIP:', error);
showToast('An error occurred while adding the VIP.', 'error');
} finally {
setSubmitting(false);
}
};
@@ -114,11 +123,13 @@ const VipList: React.FC = () => {
const updatedVip = await response.json();
setVips(prev => sortVipsByLastName(prev.map(vip => vip.id === updatedVip.id ? updatedVip : vip)));
setEditingVip(null);
showToast('VIP updated successfully!', 'success');
} else {
console.error('Failed to update VIP:', response.status);
showToast('Failed to update VIP. Please try again.', 'error');
}
} catch (error) {
console.error('Error updating VIP:', error);
showToast('An error occurred while updating the VIP.', 'error');
}
};
@@ -139,30 +150,42 @@ const VipList: React.FC = () => {
if (response.ok) {
setVips(prev => prev.filter(vip => vip.id !== vipId));
showToast('VIP deleted successfully!', 'success');
} else {
console.error('Failed to delete VIP:', response.status);
showToast('Failed to delete VIP. Please try again.', 'error');
}
} catch (error) {
console.error('Error deleting VIP:', error);
showToast('An error occurred while deleting the VIP.', 'error');
}
};
if (loading) {
return (
<div className="flex justify-center items-center min-h-64">
<div className="bg-white rounded-2xl shadow-lg p-8 flex items-center space-x-4">
<div className="w-8 h-8 border-4 border-blue-600 border-t-transparent rounded-full animate-spin"></div>
<span className="text-lg font-medium text-slate-700">Loading VIPs...</span>
</div>
<div className="flex justify-center items-center min-h-screen bg-gradient-to-br from-slate-50 to-slate-100">
<LoadingSpinner size="lg" message="Loading VIPs..." />
</div>
);
}
// Filter VIPs based on search term
const filteredVips = vips.filter(vip => {
const searchLower = searchTerm.toLowerCase();
return (
vip.name.toLowerCase().includes(searchLower) ||
vip.organization.toLowerCase().includes(searchLower) ||
vip.department.toLowerCase().includes(searchLower) ||
(vip.transportMode === 'flight' && vip.flights?.some(flight =>
flight.flightNumber.toLowerCase().includes(searchLower)
))
);
});
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 className="flex justify-between items-center mb-6">
<div>
<h1 className="text-3xl font-bold bg-gradient-to-r from-slate-800 to-slate-600 bg-clip-text text-transparent">
VIP Management
@@ -176,16 +199,52 @@ const VipList: React.FC = () => {
Add New VIP
</button>
</div>
{/* Search Bar */}
<div className="relative">
<input
type="text"
placeholder="Search by name, organization, department, or flight number..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full px-4 py-3 pl-12 border border-slate-200 rounded-lg focus:ring-2 focus:ring-amber-500 focus:border-transparent transition-all"
/>
<svg className="absolute left-4 top-3.5 h-5 w-5 text-slate-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
{searchTerm && (
<button
onClick={() => setSearchTerm('')}
className="absolute right-4 top-3.5 text-slate-400 hover:text-slate-600"
>
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
)}
</div>
</div>
{/* VIP List */}
{vips.length === 0 ? (
{searchTerm && (
<div className="bg-amber-50 border border-amber-200 rounded-lg px-4 py-2 mb-4">
<p className="text-sm text-amber-800">
Found {filteredVips.length} result{filteredVips.length !== 1 ? 's' : ''} for "{searchTerm}"
</p>
</div>
)}
{filteredVips.length === 0 ? (
<div className="bg-white rounded-2xl shadow-lg p-12 border border-slate-200/60 text-center">
<div className="w-16 h-16 bg-slate-100 rounded-full flex items-center justify-center mx-auto mb-4">
<div className="w-8 h-8 bg-slate-300 rounded-full"></div>
</div>
<h3 className="text-lg font-semibold text-slate-800 mb-2">No VIPs Found</h3>
<p className="text-slate-600 mb-6">Get started by adding your first VIP</p>
<h3 className="text-lg font-semibold text-slate-800 mb-2">
{searchTerm ? 'No VIPs Found' : 'No VIPs Added Yet'}
</h3>
<p className="text-slate-600 mb-6">
{searchTerm ? `No VIPs match your search for "${searchTerm}"` : 'Get started by adding your first VIP'}
</p>
<button
className="btn btn-primary"
onClick={() => setShowForm(true)}
@@ -195,7 +254,7 @@ const VipList: React.FC = () => {
</div>
) : (
<div className="space-y-4">
{vips.map((vip) => (
{filteredVips.map((vip) => (
<div key={vip.id} className="bg-white rounded-2xl shadow-lg border border-slate-200/60 overflow-hidden hover:shadow-xl transition-shadow duration-200">
<div className="p-6">
<div className="flex justify-between items-start">