Initial commit - Current state of vip-coordinator

This commit is contained in:
2026-01-24 09:30:26 +01:00
commit aa900505b9
96 changed files with 31868 additions and 0 deletions

27
.gitignore vendored Normal file
View File

@@ -0,0 +1,27 @@
# Dependencies
node_modules/
# Build artifacts
dist/
build/
*.map
# IDE files
.vscode/
.idea/
*.swp
*.swo
*~
# OS files
.DS_Store
Thumbs.db
desktop.ini
# Backup directories (exclude from repo)
vip-coordinator-backup-*/
# ZIP files (exclude from repo)
*.zip
# Note: .env files are intentionally included in the repository

View File

@@ -0,0 +1,174 @@
# ✅ CORRECTED Google OAuth Setup Guide
## ⚠️ Issues Found with Previous Setup
The previous coder was using **deprecated Google+ API** which was shut down in 2019. This guide provides the correct modern approach using Google Identity API.
## 🔧 What Was Fixed
1. **Removed Google+ API references** - Now uses Google Identity API
2. **Fixed redirect URI configuration** - Points to backend instead of frontend
3. **Added missing `/auth/setup` endpoint** - Frontend was calling non-existent endpoint
4. **Corrected OAuth flow** - Proper backend callback handling
## 🚀 Correct Setup Instructions
### Step 1: Google Cloud Console Setup
1. **Go to Google Cloud Console**
- Visit: https://console.cloud.google.com/
2. **Create or Select Project**
- Create new project: "VIP Coordinator"
- Or select existing project
3. **Enable Google Identity API** ⚠️ **NOT Google+ API**
- Go to "APIs & Services" → "Library"
- Search for "Google Identity API" or "Google+ API"
- **Important**: Use "Google Identity API" - Google+ is deprecated!
- Click "Enable"
4. **Create OAuth 2.0 Credentials**
- Go to "APIs & Services" → "Credentials"
- Click "Create Credentials" → "OAuth 2.0 Client IDs"
- Application type: "Web application"
- Name: "VIP Coordinator Web App"
5. **Configure Authorized URLs** ⚠️ **CRITICAL: Use Backend URLs**
**Authorized JavaScript origins:**
```
http://localhost:3000
http://bsa.madeamess.online:3000
```
**Authorized redirect URIs:** ⚠️ **Backend callback, NOT frontend**
```
http://localhost:3000/auth/google/callback
http://bsa.madeamess.online:3000/auth/google/callback
```
6. **Save Credentials**
- Copy **Client ID** and **Client Secret**
### Step 2: Update Environment Variables
Edit `backend/.env`:
```bash
# Replace these values with your actual Google OAuth credentials
GOOGLE_CLIENT_ID=your-actual-client-id-here.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=GOCSPX-your-actual-client-secret-here
GOOGLE_REDIRECT_URI=http://localhost:3000/auth/google/callback
# For production, also update:
# GOOGLE_REDIRECT_URI=http://bsa.madeamess.online:3000/auth/google/callback
```
### Step 3: Test the Setup
1. **Restart the backend:**
```bash
cd vip-coordinator
docker-compose -f docker-compose.dev.yml restart backend
```
2. **Test the OAuth flow:**
- Visit: http://localhost:5173 (or your frontend URL)
- Click "Continue with Google"
- Should redirect to Google login
- After login, should redirect back and log you in
3. **Check backend logs:**
```bash
docker-compose -f docker-compose.dev.yml logs backend
```
## 🔍 How the Corrected Flow Works
1. **User clicks "Continue with Google"**
2. **Frontend calls** `/auth/google/url` to get OAuth URL
3. **Frontend redirects** to Google OAuth
4. **Google redirects back** to `http://localhost:3000/auth/google/callback`
5. **Backend handles callback**, exchanges code for user info
6. **Backend creates JWT token** and redirects to frontend with token
7. **Frontend receives token** and authenticates user
## 🛠️ Key Differences from Previous Implementation
| Previous (Broken) | Corrected |
|-------------------|-----------|
| Google+ API (deprecated) | Google Identity API |
| Frontend redirect URI | Backend redirect URI |
| Missing `/auth/setup` endpoint | Added setup status endpoint |
| Inconsistent OAuth flow | Standard OAuth 2.0 flow |
## 🔧 Troubleshooting
### Common Issues:
1. **"OAuth not configured" error:**
- Check `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` in `.env`
- Restart backend after changing environment variables
2. **"Invalid redirect URI" error:**
- Verify redirect URIs in Google Console match exactly:
- `http://localhost:3000/auth/google/callback`
- `http://bsa.madeamess.online:3000/auth/google/callback`
- No trailing slashes!
3. **"API not enabled" error:**
- Make sure you enabled "Google Identity API" (not Google+)
- Wait a few minutes for API to activate
4. **Login button doesn't work:**
- Check browser console for errors
- Verify backend is running on port 3000
- Check `/auth/setup` endpoint returns proper status
### Debug Commands:
```bash
# Check if backend is running
curl http://localhost:3000/api/health
# Check OAuth setup status
curl http://localhost:3000/auth/setup
# Check backend logs
docker-compose -f docker-compose.dev.yml logs backend
# Check environment variables are loaded
docker exec vip-coordinator-backend-1 env | grep GOOGLE
```
## ✅ Verification Steps
1. **Setup status should show configured:**
```bash
curl http://localhost:3000/auth/setup
# Should return: {"setupCompleted": true, "firstAdminCreated": false, "oauthConfigured": true}
```
2. **OAuth URL should be generated:**
```bash
curl http://localhost:3000/auth/google/url
# Should return: {"url": "https://accounts.google.com/o/oauth2/v2/auth?..."}
```
3. **Login flow should work:**
- Visit frontend
- Click "Continue with Google"
- Complete Google login
- Should be redirected back and logged in
## 🎉 Success!
Once working, you should see:
- ✅ Google login button works
- ✅ Redirects to Google OAuth
- ✅ Returns to app after login
- ✅ User is authenticated with JWT token
- ✅ First user becomes administrator
The authentication system now uses modern Google Identity API and follows proper OAuth 2.0 standards!

View File

@@ -0,0 +1,221 @@
# VIP Coordinator Database Migration Summary
## Overview
Successfully migrated the VIP Coordinator application from JSON file storage to a proper database architecture using PostgreSQL and Redis.
## Architecture Changes
### Before (JSON File Storage)
- All data stored in `backend/data/vip-coordinator.json`
- Single file for VIPs, drivers, schedules, and admin settings
- No concurrent access control
- No real-time capabilities
- Risk of data corruption
### After (PostgreSQL + Redis)
- **PostgreSQL**: Persistent business data with ACID compliance
- **Redis**: Real-time data and caching
- Proper data relationships and constraints
- Concurrent access support
- Real-time location tracking
- Flight data caching
## Database Schema
### PostgreSQL Tables
1. **vips** - VIP profiles and basic information
2. **flights** - Flight details linked to VIPs
3. **drivers** - Driver profiles
4. **schedule_events** - Event scheduling with driver assignments
5. **admin_settings** - System configuration (key-value pairs)
### Redis Data Structure
- `driver:{id}:location` - Real-time driver locations
- `event:{id}:status` - Live event status updates
- `flight:{key}` - Cached flight API responses
## Key Features Implemented
### 1. Database Configuration
- **PostgreSQL connection pool** (`backend/src/config/database.ts`)
- **Redis client setup** (`backend/src/config/redis.ts`)
- **Database schema** (`backend/src/config/schema.sql`)
### 2. Data Services
- **DatabaseService** (`backend/src/services/databaseService.ts`)
- Database initialization and migration
- Redis operations for real-time data
- Automatic JSON data migration
- **EnhancedDataService** (`backend/src/services/enhancedDataService.ts`)
- PostgreSQL CRUD operations
- Complex queries with joins
- Transaction support
### 3. Migration Features
- **Automatic migration** from existing JSON data
- **Backup creation** of original JSON file
- **Zero-downtime migration** process
- **Data validation** during migration
### 4. Real-time Capabilities
- **Driver location tracking** in Redis
- **Event status updates** with timestamps
- **Flight data caching** with TTL
- **Performance optimization** through caching
## Data Flow
### VIP Management
```
Frontend → API → EnhancedDataService → PostgreSQL
→ Redis (for real-time data)
```
### Driver Location Updates
```
Frontend → API → DatabaseService → Redis (hSet driver location)
```
### Flight Tracking
```
Flight API → FlightService → Redis (cache) → Database (if needed)
```
## Benefits Achieved
### Performance
- **Faster queries** with PostgreSQL indexes
- **Reduced API calls** through Redis caching
- **Concurrent access** without file locking issues
### Scalability
- **Multiple server instances** supported
- **Database connection pooling**
- **Redis clustering** ready
### Reliability
- **ACID transactions** for data integrity
- **Automatic backups** during migration
- **Error handling** and rollback support
### Real-time Features
- **Live driver locations** via Redis
- **Event status tracking** with timestamps
- **Flight data caching** for performance
## Configuration
### Environment Variables
```bash
DATABASE_URL=postgresql://postgres:changeme@db:5432/vip_coordinator
REDIS_URL=redis://redis:6379
```
### Docker Services
- **PostgreSQL 15** with persistent volume
- **Redis 7** for caching and real-time data
- **Backend** with database connections
## Migration Process
### Automatic Steps
1. **Schema creation** with tables and indexes
2. **Data validation** and transformation
3. **VIP migration** with flight relationships
4. **Driver migration** with location data to Redis
5. **Schedule migration** with proper relationships
6. **Admin settings** flattened to key-value pairs
7. **Backup creation** of original JSON file
### Manual Steps (if needed)
1. Install dependencies: `npm install`
2. Start services: `make dev`
3. Verify migration in logs
## API Changes
### Enhanced Endpoints
- All VIP endpoints now use PostgreSQL
- Driver location updates go to Redis
- Flight data cached in Redis
- Schedule operations with proper relationships
### Backward Compatibility
- All existing API endpoints maintained
- Same request/response formats
- Legacy field support during transition
## Testing
### Database Connection
```bash
# Health check includes database status
curl http://localhost:3000/api/health
```
### Data Verification
```bash
# Check VIPs migrated correctly
curl http://localhost:3000/api/vips
# Check drivers with locations
curl http://localhost:3000/api/drivers
```
## Next Steps
### Immediate
1. **Test the migration** with Docker
2. **Verify all endpoints** work correctly
3. **Check real-time features** function
### Future Enhancements
1. **WebSocket integration** for live updates
2. **Advanced Redis patterns** for pub/sub
3. **Database optimization** with query analysis
4. **Monitoring and metrics** setup
## Files Created/Modified
### New Files
- `backend/src/config/database.ts` - PostgreSQL configuration
- `backend/src/config/redis.ts` - Redis configuration
- `backend/src/config/schema.sql` - Database schema
- `backend/src/services/databaseService.ts` - Migration and Redis ops
- `backend/src/services/enhancedDataService.ts` - PostgreSQL operations
### Modified Files
- `backend/package.json` - Added pg, redis, uuid dependencies
- `backend/src/index.ts` - Updated to use new services
- `docker-compose.dev.yml` - Already configured for databases
## Redis Usage Patterns
### Driver Locations
```typescript
// Update location
await databaseService.updateDriverLocation(driverId, { lat: 39.7392, lng: -104.9903 });
// Get location
const location = await databaseService.getDriverLocation(driverId);
```
### Event Status
```typescript
// Set status
await databaseService.setEventStatus(eventId, 'in-progress');
// Get status
const status = await databaseService.getEventStatus(eventId);
```
### Flight Caching
```typescript
// Cache flight data
await databaseService.cacheFlightData(flightKey, flightData, 300);
// Get cached data
const cached = await databaseService.getCachedFlightData(flightKey);
```
This migration provides a solid foundation for scaling the VIP Coordinator application with proper data persistence, real-time capabilities, and performance optimization.

179
DOCKER_TROUBLESHOOTING.md Normal file
View File

@@ -0,0 +1,179 @@
# Docker Container Stopping Issues - Troubleshooting Guide
## 🚨 Issue Observed
During development, we encountered issues where Docker containers would hang during the stopping process, requiring forceful termination. This is concerning for production stability.
## 🔍 Current System Status
**✅ All containers are currently running properly:**
- Backend: http://localhost:3000 (responding correctly)
- Frontend: http://localhost:5173
- Database: PostgreSQL on port 5432
- Redis: Running on port 6379
**Docker Configuration:**
- Storage Driver: overlay2
- Logging Driver: json-file
- Cgroup Driver: systemd
- Cgroup Version: 2
## 🛠️ Potential Causes & Solutions
### 1. **Graceful Shutdown Issues**
**Problem:** Applications not handling SIGTERM signals properly
**Solution:** Ensure applications handle shutdown gracefully
**For Node.js apps (backend/frontend):**
```javascript
// Add to your main application file
process.on('SIGTERM', () => {
console.log('SIGTERM received, shutting down gracefully');
server.close(() => {
console.log('Process terminated');
process.exit(0);
});
});
process.on('SIGINT', () => {
console.log('SIGINT received, shutting down gracefully');
server.close(() => {
console.log('Process terminated');
process.exit(0);
});
});
```
### 2. **Docker Compose Configuration**
**Current issue:** Using obsolete `version` attribute
**Solution:** Update docker-compose.dev.yml
```yaml
# Remove this line:
# version: '3.8'
# And ensure proper stop configuration:
services:
backend:
stop_grace_period: 30s
stop_signal: SIGTERM
frontend:
stop_grace_period: 30s
stop_signal: SIGTERM
```
### 3. **Resource Constraints**
**Problem:** Insufficient memory/CPU causing hanging
**Solution:** Add resource limits
```yaml
services:
backend:
deploy:
resources:
limits:
memory: 512M
reservations:
memory: 256M
```
### 4. **Database Connection Handling**
**Problem:** Open database connections preventing shutdown
**Solution:** Ensure proper connection cleanup
```javascript
// In your backend application
process.on('SIGTERM', async () => {
console.log('Closing database connections...');
await database.close();
await redis.quit();
process.exit(0);
});
```
## 🔧 Immediate Fixes to Implement
### 1. Update Docker Compose File
```bash
cd /home/kyle/Desktop/vip-coordinator
# Remove the version line and add stop configurations
```
### 2. Add Graceful Shutdown to Backend
```bash
# Update backend/src/index.ts with proper signal handling
```
### 3. Monitor Container Behavior
```bash
# Use these commands to monitor:
docker-compose -f docker-compose.dev.yml logs --follow
docker stats
```
## 🚨 Emergency Commands
If containers hang during stopping:
```bash
# Force stop all containers
docker-compose -f docker-compose.dev.yml kill
# Remove stopped containers
docker-compose -f docker-compose.dev.yml rm -f
# Clean up system
docker system prune -f
# Restart fresh
docker-compose -f docker-compose.dev.yml up -d
```
## 📊 Monitoring Commands
```bash
# Check container status
docker-compose -f docker-compose.dev.yml ps
# Monitor logs in real-time
docker-compose -f docker-compose.dev.yml logs -f backend
# Check resource usage
docker stats
# Check for hanging processes
docker-compose -f docker-compose.dev.yml top
```
## 🎯 Prevention Strategies
1. **Regular Health Checks**
- Implement health check endpoints
- Monitor container resource usage
- Set up automated restarts for failed containers
2. **Proper Signal Handling**
- Ensure all applications handle SIGTERM/SIGINT
- Implement graceful shutdown procedures
- Close database connections properly
3. **Resource Management**
- Set appropriate memory/CPU limits
- Monitor disk space usage
- Regular cleanup of unused images/containers
## 🔄 Current OAuth2 Status
**✅ OAuth2 is now working correctly:**
- Simplified implementation without Passport.js
- Proper domain configuration for bsa.madeamess.online
- Environment variables correctly set
- Backend responding to auth endpoints
**Next steps for OAuth2:**
1. Update Google Cloud Console with redirect URI: `https://bsa.madeamess.online:3000/auth/google/callback`
2. Test the full OAuth flow
3. Integrate with frontend
The container stopping issues are separate from the OAuth2 functionality and should be addressed through the solutions above.

View File

@@ -0,0 +1,108 @@
# Google OAuth2 Domain Setup for bsa.madeamess.online
## 🔧 Current Configuration
Your VIP Coordinator is now configured for your domain:
- **Backend URL**: `https://bsa.madeamess.online:3000`
- **Frontend URL**: `https://bsa.madeamess.online:5173`
- **OAuth Redirect URI**: `https://bsa.madeamess.online:3000/auth/google/callback`
## 📋 Google Cloud Console Setup
You need to update your Google Cloud Console OAuth2 configuration:
### 1. Go to Google Cloud Console
- Visit: https://console.cloud.google.com/
- Select your project (or create one)
### 2. Enable APIs
- Go to "APIs & Services" → "Library"
- Enable "Google+ API" (or "People API")
### 3. Configure OAuth2 Credentials
- Go to "APIs & Services" → "Credentials"
- Find your OAuth 2.0 Client ID: `308004695553-6k34bbq22frc4e76kejnkgq8mncepbbg.apps.googleusercontent.com`
- Click "Edit" (pencil icon)
### 4. Update Authorized Redirect URIs
Add these exact URIs (case-sensitive):
```
https://bsa.madeamess.online:3000/auth/google/callback
```
### 5. Update Authorized JavaScript Origins (if needed)
Add these origins:
```
https://bsa.madeamess.online:3000
https://bsa.madeamess.online:5173
```
## 🚀 Testing the OAuth Flow
Once you've updated Google Cloud Console:
1. **Visit the OAuth endpoint:**
```
https://bsa.madeamess.online:3000/auth/google
```
2. **Expected flow:**
- Redirects to Google login
- After login, Google redirects to: `https://bsa.madeamess.online:3000/auth/google/callback`
- Backend processes the callback and redirects to: `https://bsa.madeamess.online:5173/auth/callback?token=JWT_TOKEN`
3. **Check if backend is running:**
```bash
curl https://bsa.madeamess.online:3000/api/health
```
## 🔍 Troubleshooting
### Common Issues:
1. **"redirect_uri_mismatch" error:**
- Make sure the redirect URI in Google Console exactly matches: `https://bsa.madeamess.online:3000/auth/google/callback`
- No trailing slashes
- Exact case match
- Include the port number `:3000`
2. **SSL/HTTPS issues:**
- Make sure your domain has valid SSL certificates
- Google requires HTTPS for production OAuth
3. **Port access:**
- Ensure ports 3000 and 5173 are accessible from the internet
- Check firewall settings
### Debug Commands:
```bash
# Check if containers are running
docker-compose -f docker-compose.dev.yml ps
# Check backend logs
docker-compose -f docker-compose.dev.yml logs backend
# Test backend health
curl https://bsa.madeamess.online:3000/api/health
# Test auth status
curl https://bsa.madeamess.online:3000/auth/status
```
## 📝 Current Environment Variables
Your `.env` file is configured with:
```bash
GOOGLE_CLIENT_ID=308004695553-6k34bbq22frc4e76kejnkgq8mncepbbg.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=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
GOOGLE_REDIRECT_URI=https://bsa.madeamess.online:3000/auth/google/callback
FRONTEND_URL=https://bsa.madeamess.online:5173
```
## ✅ Next Steps
1. Update Google Cloud Console with the redirect URI above
2. Test the OAuth flow by visiting `https://bsa.madeamess.online:3000/auth/google`
3. Verify the frontend can handle the callback at `https://bsa.madeamess.online:5173/auth/callback`
The OAuth2 system should now work correctly with your domain! 🎉

View File

@@ -0,0 +1,48 @@
# Quick Google OAuth Setup Guide
## Step 1: Get Your Google OAuth Credentials
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
2. Create a new project or select an existing one
3. Enable the Google+ API (or Google Identity API)
4. Go to "Credentials" → "Create Credentials" → "OAuth 2.0 Client IDs"
5. Set Application type to "Web application"
6. Add these Authorized redirect URIs:
- `http://localhost:5173/auth/google/callback`
- `http://bsa.madeamess.online:5173/auth/google/callback`
## Step 2: Update Your .env File
Replace these lines in `/home/kyle/Desktop/vip-coordinator/backend/.env`:
```bash
# REPLACE THESE TWO LINES:
GOOGLE_CLIENT_ID=your-google-client-id-from-console
GOOGLE_CLIENT_SECRET=your-google-client-secret-from-console
# WITH YOUR ACTUAL VALUES:
GOOGLE_CLIENT_ID=123456789-abcdefghijklmnop.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=GOCSPX-your_actual_secret_here
```
## Step 3: Restart the Backend
After updating the .env file, restart the backend container:
```bash
cd /home/kyle/Desktop/vip-coordinator
docker-compose -f docker-compose.dev.yml restart backend
```
## Step 4: Test the Login
Visit: http://bsa.madeamess.online:5173 and click "Sign in with Google"
(The frontend proxies /auth requests to the backend automatically)
## Bypass Option (Temporary)
If you want to skip Google OAuth for now, visit:
http://bsa.madeamess.online:5173/admin-bypass
This will take you directly to the admin dashboard without authentication.
(The frontend will proxy this request to the backend)

108
GOOGLE_OAUTH_SETUP.md Normal file
View File

@@ -0,0 +1,108 @@
# Google OAuth Setup Guide
## Overview
Your VIP Coordinator now includes Google OAuth authentication! This guide will help you set up Google OAuth credentials so users can log in with their Google accounts.
## Step 1: Google Cloud Console Setup
### 1. Go to Google Cloud Console
Visit: https://console.cloud.google.com/
### 2. Create or Select a Project
- If you don't have a project, click "Create Project"
- Give it a name like "VIP Coordinator"
- Select your organization if applicable
### 3. Enable Google+ API
- Go to "APIs & Services" → "Library"
- Search for "Google+ API"
- Click on it and press "Enable"
### 4. Create OAuth 2.0 Credentials
- Go to "APIs & Services" → "Credentials"
- Click "Create Credentials" → "OAuth 2.0 Client IDs"
- Choose "Web application" as the application type
- Give it a name like "VIP Coordinator Web App"
### 5. Configure Authorized URLs
**Authorized JavaScript origins:**
```
http://bsa.madeamess.online:5173
http://localhost:5173
```
**Authorized redirect URIs:**
```
http://bsa.madeamess.online:3000/auth/google/callback
http://localhost:3000/auth/google/callback
```
### 6. Save Your Credentials
- Copy the **Client ID** and **Client Secret**
- You'll need these for the next step
## Step 2: Configure VIP Coordinator
### 1. Access Admin Dashboard
- Go to: http://bsa.madeamess.online:5173/admin
- Enter the admin password: `admin123`
### 2. Add Google OAuth Credentials
- Scroll to the "Google OAuth Credentials" section
- Paste your **Client ID** in the first field
- Paste your **Client Secret** in the second field
- Click "Save All Settings"
## Step 3: Test the Setup
### 1. Access the Application
- Go to: http://bsa.madeamess.online:5173
- You should see a Google login button
### 2. First Login (Admin Setup)
- The first person to log in will automatically become the administrator
- Subsequent users will be assigned the "coordinator" role by default
- Drivers will need to register separately
### 3. User Roles
- **Administrator**: Full system access, user management, settings
- **Coordinator**: VIP and schedule management, driver assignments
- **Driver**: Personal schedule view, location updates
## Troubleshooting
### Common Issues:
1. **"Blocked request" error**
- Make sure your domain is added to authorized JavaScript origins
- Check that the redirect URI matches exactly
2. **"OAuth credentials not configured" warning**
- Verify you've entered both Client ID and Client Secret
- Make sure you clicked "Save All Settings"
3. **Login button not working**
- Check browser console for errors
- Verify the backend is running on port 3000
### Getting Help:
- Check the browser console for error messages
- Verify all URLs match exactly (including http/https)
- Make sure the Google+ API is enabled in your project
## Security Notes
- Keep your Client Secret secure and never share it publicly
- The credentials are stored securely in your database
- Sessions last 24 hours as requested
- Only the frontend (port 5173) is exposed externally for security
## Next Steps
Once Google OAuth is working:
1. Test the login flow with different Google accounts
2. Assign appropriate roles to users through the admin dashboard
3. Create VIPs and schedules to test the full system
4. Set up additional API keys (AviationStack, etc.) as needed
Your VIP Coordinator is now ready for secure, role-based access!

100
HTTPS_SETUP.md Normal file
View File

@@ -0,0 +1,100 @@
# Enabling HTTPS with Certbot and Nginx
The production Docker stack ships with an Nginx front-end (the `frontend` service). Follow these steps to terminate HTTPS traffic with Let's Encrypt certificates.
## 1. Prerequisites
- DNS `A` records for your domain (e.g. `vip.example.com`) pointing to `162.243.171.221`.
- Ports **80** and **443** open in the DigitalOcean firewall.
- Docker Compose production stack deployed.
- Certbot installed on the droplet (`apt-get install -y certbot` already run).
## 2. Obtain certificates
Run Certbot in standalone mode (temporarily stop the `frontend` container during issuance if it is already binding to port 80):
```bash
# Stop the frontend container temporarily
docker compose -f docker-compose.prod.yml stop frontend
# Request the certificate (replace the domain names)
sudo certbot certonly --standalone \
-d vip.example.com \
-d www.vip.example.com
# Restart the frontend container
docker compose -f docker-compose.prod.yml start frontend
```
Certificates will be stored under `/etc/letsencrypt/live/vip.example.com/`.
## 3. Mount certificates into the frontend container
Copy the key pair into the repository (or mount the original directory as a read-only volume). For example:
```bash
sudo mkdir -p /opt/vip-coordinator/certs
sudo cp /etc/letsencrypt/live/vip.example.com/fullchain.pem /opt/vip-coordinator/certs/
sudo cp /etc/letsencrypt/live/vip.example.com/privkey.pem /opt/vip-coordinator/certs/
sudo chown root:root /opt/vip-coordinator/certs/*.pem
sudo chmod 600 /opt/vip-coordinator/certs/privkey.pem
```
Update `docker-compose.prod.yml` to mount the certificate directory (uncomment the example below):
```yaml
frontend:
volumes:
- /opt/vip-coordinator/certs:/etc/nginx/certs:ro
```
## 4. Enable the TLS server block
Edit `frontend/nginx.conf`:
1. Uncomment the TLS server block and point to `/etc/nginx/certs/fullchain.pem` and `privkey.pem`.
2. Change the port 80 server block to redirect to HTTPS (e.g. `return 301 https://$host$request_uri;`).
Example TLS block:
```nginx
server {
listen 443 ssl http2;
server_name vip.example.com www.vip.example.com;
ssl_certificate /etc/nginx/certs/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/privkey.pem;
include /etc/nginx/snippets/ssl-params.conf; # optional hardening
proxy_cache_bypass $http_upgrade;
...
}
```
Rebuild and redeploy the frontend container:
```bash
docker compose -f docker-compose.prod.yml build frontend
docker compose -f docker-compose.prod.yml up -d frontend
```
## 5. Automate renewals
Certbot installs a systemd timer that runs `certbot renew` twice a day. After renewal, copy the updated certificates into `/opt/vip-coordinator/certs/` and reload the frontend container:
```bash
sudo certbot renew --dry-run
sudo cp /etc/letsencrypt/live/vip.example.com/{fullchain.pem,privkey.pem} /opt/vip-coordinator/certs/
docker compose -f docker-compose.prod.yml exec frontend nginx -s reload
```
Consider scripting the copy + reload to run after each renewal (e.g. with a cron job).
## 6. Hardening checklist
- Add `ssl_ciphers`, `ssl_prefer_server_ciphers`, and HSTS headers to Nginx.
- Restrict `ADMIN_PASSWORD` to a strong value and rotate `JWT_SECRET`.
- Enable a firewall (DigitalOcean VPC or `ufw`) allowing only SSH, HTTP, HTTPS.
- Configure automatic backups for PostgreSQL (snapshot or `pg_dump`).

10
Makefile Normal file
View File

@@ -0,0 +1,10 @@
.PHONY: dev build deploy
dev:
docker-compose -f docker-compose.dev.yml up --build
build:
docker-compose -f docker-compose.prod.yml build
deploy:
docker-compose -f docker-compose.prod.yml up -d

View File

@@ -0,0 +1,92 @@
# ✅ OAuth Callback Issue RESOLVED!
## 🎯 Problem Identified & Fixed
**Root Cause:** The Vite proxy configuration was intercepting ALL `/auth/*` routes and forwarding them to the backend, including the OAuth callback route `/auth/google/callback` that needed to be handled by the React frontend.
## 🔧 Solution Applied
**Fixed Vite Configuration** (`frontend/vite.config.ts`):
**BEFORE (Problematic):**
```typescript
proxy: {
'/api': {
target: 'http://backend:3000',
changeOrigin: true,
},
'/auth': { // ❌ This was intercepting ALL /auth routes
target: 'http://backend:3000',
changeOrigin: true,
},
}
```
**AFTER (Fixed):**
```typescript
proxy: {
'/api': {
target: 'http://backend:3000',
changeOrigin: true,
},
// ✅ Only proxy specific auth endpoints, not the callback route
'/auth/setup': {
target: 'http://backend:3000',
changeOrigin: true,
},
'/auth/google/url': {
target: 'http://backend:3000',
changeOrigin: true,
},
'/auth/google/exchange': {
target: 'http://backend:3000',
changeOrigin: true,
},
'/auth/me': {
target: 'http://backend:3000',
changeOrigin: true,
},
'/auth/logout': {
target: 'http://backend:3000',
changeOrigin: true,
},
'/auth/status': {
target: 'http://backend:3000',
changeOrigin: true,
},
}
```
## 🔄 How OAuth Flow Works Now
1. **User clicks "Continue with Google"**
- Frontend calls `/auth/google/url` → Proxied to backend
- Backend returns Google OAuth URL with correct redirect URI
2. **Google Authentication**
- User authenticates with Google
- Google redirects to: `https://bsa.madeamess.online:5173/auth/google/callback?code=...`
3. **Frontend Handles Callback**
- `/auth/google/callback` is NOT proxied to backend
- React Router serves the frontend app
- Login component detects callback route and authorization code
- Calls `/auth/google/exchange` → Proxied to backend
- Backend exchanges code for JWT token
- Frontend receives token and user info, logs user in
## 🎉 Current Status
**✅ All containers running successfully**
**✅ Vite proxy configuration fixed**
**✅ OAuth callback route now handled by frontend**
**✅ Backend OAuth endpoints working correctly**
## 🧪 Test the Fix
1. Visit your domain: `https://bsa.madeamess.online:5173`
2. Click "Continue with Google"
3. Complete Google authentication
4. You should be redirected back and logged in successfully!
The OAuth callback handoff issue has been completely resolved! 🎊

View File

@@ -0,0 +1,216 @@
# OAuth2 Setup for Frontend-Only Port (5173)
## 🎯 Configuration Overview
Since you're only forwarding port 5173, the OAuth flow has been configured to work entirely through the frontend:
**Current Setup:**
- **Frontend**: `https://bsa.madeamess.online:5173` (publicly accessible)
- **Backend**: `http://localhost:3000` (internal only)
- **OAuth Redirect**: `https://bsa.madeamess.online:5173/auth/google/callback`
## 🔧 Google Cloud Console Configuration
**Update your OAuth2 client with this redirect URI:**
```
https://bsa.madeamess.online:5173/auth/google/callback
```
**Authorized JavaScript Origins:**
```
https://bsa.madeamess.online:5173
```
## 🔄 How the OAuth Flow Works
### 1. **Frontend Initiates OAuth**
```javascript
// Frontend calls backend to get OAuth URL
const response = await fetch('/api/auth/google/url');
const { url } = await response.json();
window.location.href = url; // Redirect to Google
```
### 2. **Google Redirects to Frontend**
```
https://bsa.madeamess.online:5173/auth/google/callback?code=AUTHORIZATION_CODE
```
### 3. **Frontend Exchanges Code for Token**
```javascript
// Frontend sends code to backend
const response = await fetch('/api/auth/google/exchange', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code: authorizationCode })
});
const { token, user } = await response.json();
// Store token in localStorage or secure cookie
```
## 🛠️ Backend API Endpoints
### **GET /api/auth/google/url**
Returns the Google OAuth URL for frontend to redirect to.
**Response:**
```json
{
"url": "https://accounts.google.com/o/oauth2/v2/auth?client_id=..."
}
```
### **POST /api/auth/google/exchange**
Exchanges authorization code for JWT token.
**Request:**
```json
{
"code": "authorization_code_from_google"
}
```
**Response:**
```json
{
"token": "jwt_token_here",
"user": {
"id": "user_id",
"email": "user@example.com",
"name": "User Name",
"picture": "profile_picture_url",
"role": "coordinator"
}
}
```
### **GET /api/auth/status**
Check authentication status.
**Headers:**
```
Authorization: Bearer jwt_token_here
```
**Response:**
```json
{
"authenticated": true,
"user": { ... }
}
```
## 📝 Frontend Implementation Example
### **Login Component**
```javascript
const handleGoogleLogin = async () => {
try {
// Get OAuth URL from backend
const response = await fetch('/api/auth/google/url');
const { url } = await response.json();
// Redirect to Google
window.location.href = url;
} catch (error) {
console.error('Login failed:', error);
}
};
```
### **OAuth Callback Handler**
```javascript
// In your callback route component
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const error = urlParams.get('error');
if (error) {
console.error('OAuth error:', error);
return;
}
if (code) {
exchangeCodeForToken(code);
}
}, []);
const exchangeCodeForToken = async (code) => {
try {
const response = await fetch('/api/auth/google/exchange', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code })
});
const { token, user } = await response.json();
// Store token securely
localStorage.setItem('authToken', token);
// Redirect to dashboard
navigate('/dashboard');
} catch (error) {
console.error('Token exchange failed:', error);
}
};
```
### **API Request Helper**
```javascript
const apiRequest = async (url, options = {}) => {
const token = localStorage.getItem('authToken');
return fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
...options.headers
}
});
};
```
## 🚀 Testing the Setup
### 1. **Test OAuth URL Generation**
```bash
curl http://localhost:3000/api/auth/google/url
```
### 2. **Test Full Flow**
1. Visit: `https://bsa.madeamess.online:5173`
2. Click login button
3. Should redirect to Google
4. After Google login, should redirect back to: `https://bsa.madeamess.online:5173/auth/google/callback?code=...`
5. Frontend should exchange code for token
6. User should be logged in
### 3. **Test API Access**
```bash
# Get a token first, then:
curl -H "Authorization: Bearer YOUR_JWT_TOKEN" http://localhost:3000/api/auth/status
```
## ✅ Current Status
**✅ Containers Running:**
- Backend: http://localhost:3000
- Frontend: http://localhost:5173
- Database: PostgreSQL on port 5432
- Redis: Running on port 6379
**✅ OAuth Configuration:**
- Redirect URI: `https://bsa.madeamess.online:5173/auth/google/callback`
- Frontend URL: `https://bsa.madeamess.online:5173`
- Backend endpoints ready for frontend integration
**🔄 Next Steps:**
1. Update Google Cloud Console with the redirect URI
2. Implement frontend OAuth handling
3. Test the complete flow
The OAuth system is now properly configured to work through your frontend-only port setup! 🎉

122
PERMISSION_ISSUES_FIXED.md Normal file
View File

@@ -0,0 +1,122 @@
# User Permission Issues - Debugging Summary
## Issues Found and Fixed
### 1. **Token Storage Inconsistency** ❌ → ✅
**Problem**: Different components were using different localStorage keys for the authentication token:
- `App.tsx` used `localStorage.getItem('authToken')`
- `UserManagement.tsx` used `localStorage.getItem('token')` in one place
**Fix**: Standardized all components to use `'authToken'` as the localStorage key.
**Files Fixed**:
- `frontend/src/components/UserManagement.tsx` - Line 69: Changed `localStorage.getItem('token')` to `localStorage.getItem('authToken')`
### 2. **Missing Authentication Headers in VIP Operations** ❌ → ✅
**Problem**: The VIP management operations (add, edit, delete, fetch) were not including authentication headers, causing 401/403 errors.
**Fix**: Added proper authentication headers to all VIP API calls.
**Files Fixed**:
- `frontend/src/pages/VipList.tsx`:
- Added `apiCall` import from config
- Updated `fetchVips()` to include `Authorization: Bearer ${token}` header
- Updated `handleAddVip()` to include authentication headers
- Updated `handleEditVip()` to include authentication headers
- Updated `handleDeleteVip()` to include authentication headers
- Fixed TypeScript error with EditVipForm props
### 3. **API URL Configuration** ✅
**Status**: Already correctly configured
- Frontend uses `https://api.bsa.madeamess.online` via `apiCall` helper
- Backend has proper CORS configuration for the frontend domain
### 4. **Backend Authentication Middleware** ✅
**Status**: Already properly implemented
- VIP routes are protected with `requireAuth` middleware
- Role-based access control with `requireRole(['coordinator', 'administrator'])`
- User management routes require `administrator` role
## Backend Permission Structure (Already Working)
```typescript
// VIP Operations - Require coordinator or administrator role
app.post('/api/vips', requireAuth, requireRole(['coordinator', 'administrator']))
app.put('/api/vips/:id', requireAuth, requireRole(['coordinator', 'administrator']))
app.delete('/api/vips/:id', requireAuth, requireRole(['coordinator', 'administrator']))
app.get('/api/vips', requireAuth) // All authenticated users can view
// User Management - Require administrator role only
app.get('/auth/users', requireAuth, requireRole(['administrator']))
app.patch('/auth/users/:email/role', requireAuth, requireRole(['administrator']))
app.delete('/auth/users/:email', requireAuth, requireRole(['administrator']))
```
## Role Hierarchy
1. **Administrator**:
- Full access to all features
- Can manage users and change roles
- Can add/edit/delete VIPs
- Can manage drivers and schedules
2. **Coordinator**:
- Can add/edit/delete VIPs
- Can manage drivers and schedules
- Cannot manage users or change roles
3. **Driver**:
- Can view assigned schedules
- Can update status
- Cannot manage VIPs or users
## Testing the Fixes
After these fixes, the admin should now be able to:
1.**Add VIPs**: The "Add New VIP" button will work with proper authentication
2.**Change User Roles**: The role dropdown in User Management will work correctly
3.**View All Data**: All API calls now include proper authentication headers
## What Was Happening Before
1. **VIP Operations Failing**: When clicking "Add New VIP" or trying to edit/delete VIPs, the requests were being sent without authentication headers, causing the backend to return 401 Unauthorized errors.
2. **User Role Changes Failing**: The user management component was using the wrong token storage key, so role update requests were failing with authentication errors.
3. **Silent Failures**: The frontend wasn't showing proper error messages, so it appeared that buttons weren't working when actually the API calls were being rejected.
## Additional Recommendations
1. **Error Handling**: Consider adding user-friendly error messages when API calls fail
2. **Loading States**: Add loading indicators for user actions (role changes, VIP operations)
3. **Token Refresh**: Implement token refresh logic for long-running sessions
4. **Audit Logging**: Consider logging user actions for security auditing
## Files Modified
1. `frontend/src/components/UserManagement.tsx` - Fixed token storage key inconsistency
2. `frontend/src/pages/VipList.tsx` - Added authentication headers to all VIP operations
3. `frontend/src/pages/DriverList.tsx` - Added authentication headers to all driver operations
4. `frontend/src/pages/Dashboard.tsx` - Added authentication headers to dashboard data fetching
5. `vip-coordinator/PERMISSION_ISSUES_FIXED.md` - This documentation
## Site-Wide Authentication Fix
You were absolutely right - this was a site-wide problem! I've now fixed authentication headers across all major components:
### ✅ Fixed Components:
- **VipList**: All CRUD operations (create, read, update, delete) now include auth headers
- **DriverList**: All CRUD operations now include auth headers
- **Dashboard**: Data fetching for VIPs, drivers, and schedules now includes auth headers
- **UserManagement**: Token storage key fixed and all operations include auth headers
### 🔍 Components Still Needing Review:
- `ScheduleManager.tsx` - Schedule operations
- `DriverSelector.tsx` - Driver availability checks
- `VipDetails.tsx` - VIP detail fetching
- `DriverDashboard.tsx` - Driver schedule operations
- `FlightStatus.tsx` - Flight data fetching
- `VipForm.tsx` & `EditVipForm.tsx` - Flight validation
The permission system is now working correctly with proper authentication and authorization for the main management operations!

173
PORT_3000_SETUP_GUIDE.md Normal file
View File

@@ -0,0 +1,173 @@
# 🚀 Port 3000 Direct Access Setup Guide
## Your Optimal Setup (Based on Google's AI Analysis)
Google's AI correctly identified that the OAuth redirect to `localhost:3000` is the issue. Here's the **simplest solution**:
## Option A: Expose Port 3000 Directly (Recommended)
### 1. Router/Firewall Configuration
Configure your router to forward **both ports**:
```
Internet → Router → Your Server
Port 443/80 → Frontend (port 5173) ✅ Already working
Port 3000 → Backend (port 3000) ⚠️ ADD THIS
```
### 2. Google Cloud Console Update
**Authorized JavaScript origins:**
```
https://bsa.madeamess.online
https://bsa.madeamess.online:3000
```
**Authorized redirect URIs:**
```
https://bsa.madeamess.online:3000/auth/google/callback
```
### 3. Environment Variables (Already Updated)
✅ I've already updated your `.env` file:
```bash
GOOGLE_REDIRECT_URI=https://bsa.madeamess.online:3000/auth/google/callback
FRONTEND_URL=https://bsa.madeamess.online
```
### 4. SSL Certificate for Port 3000
You'll need SSL on port 3000. Options:
**Option A: Reverse proxy for port 3000 too**
```nginx
# Frontend (existing)
server {
listen 443 ssl;
server_name bsa.madeamess.online;
location / {
proxy_pass http://localhost:5173;
}
}
# Backend (add this)
server {
listen 3000 ssl;
server_name bsa.madeamess.online;
ssl_certificate /path/to/your/cert.pem;
ssl_certificate_key /path/to/your/key.pem;
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
**Option B: Direct Docker port mapping with SSL termination**
```yaml
# In docker-compose.dev.yml
services:
backend:
ports:
- "3000:3000"
environment:
- SSL_CERT_PATH=/certs/cert.pem
- SSL_KEY_PATH=/certs/key.pem
```
## Option B: Alternative - Use Standard HTTPS Port
If you don't want to expose port 3000, use a subdomain:
### 1. Create Subdomain
Point `api.bsa.madeamess.online` to your server
### 2. Update Environment Variables
```bash
GOOGLE_REDIRECT_URI=https://api.bsa.madeamess.online/auth/google/callback
```
### 3. Configure Reverse Proxy
```nginx
server {
listen 443 ssl;
server_name api.bsa.madeamess.online;
location / {
proxy_pass http://localhost:3000;
# ... headers
}
}
```
## Testing Your Setup
### 1. Restart Containers
```bash
cd /home/kyle/Desktop/vip-coordinator
docker-compose -f docker-compose.dev.yml restart
```
### 2. Test Backend Accessibility
```bash
# Should work from internet
curl https://bsa.madeamess.online:3000/auth/setup
# Should return: {"setupCompleted":true,"firstAdminCreated":false,"oauthConfigured":true}
```
### 3. Test OAuth URL Generation
```bash
curl https://bsa.madeamess.online:3000/auth/google/url
# Should return Google OAuth URL with correct redirect_uri
```
### 4. Test Complete OAuth Flow
1. Visit `https://bsa.madeamess.online` (frontend)
2. Click "Continue with Google"
3. Google redirects to `https://bsa.madeamess.online:3000/auth/google/callback`
4. Backend processes OAuth and redirects back to frontend with token
5. User is authenticated ✅
## Why This Works Better
**Direct backend access** - Google can reach your OAuth callback
**Simpler configuration** - No complex reverse proxy routing
**Easier debugging** - Clear separation of frontend/backend
**Standard OAuth flow** - Follows OAuth 2.0 best practices
## Security Considerations
🔒 **SSL Required**: Port 3000 must use HTTPS for OAuth
🔒 **Firewall Rules**: Only expose necessary ports
🔒 **CORS Configuration**: Already configured for your domain
## Quick Commands
```bash
# 1. Restart containers with new config
docker-compose -f docker-compose.dev.yml restart
# 2. Test backend
curl https://bsa.madeamess.online:3000/auth/setup
# 3. Check OAuth URL
curl https://bsa.madeamess.online:3000/auth/google/url
# 4. Test frontend
curl https://bsa.madeamess.online
```
## Expected Flow After Setup
1. **User visits**: `https://bsa.madeamess.online` (frontend)
2. **Clicks login**: Frontend calls `https://bsa.madeamess.online:3000/auth/google/url`
3. **Redirects to Google**: User authenticates with Google
4. **Google redirects back**: `https://bsa.madeamess.online:3000/auth/google/callback`
5. **Backend processes**: Creates JWT token
6. **Redirects to frontend**: `https://bsa.madeamess.online/auth/callback?token=...`
7. **Frontend receives token**: User is logged in ✅
This setup will resolve the OAuth callback issue you're experiencing!

View File

@@ -0,0 +1,199 @@
# 🐘 PostgreSQL User Management System
## ✅ What We Built
A **production-ready user management system** using your existing PostgreSQL database infrastructure with proper database design, indexing, and transactional operations.
## 🎯 Database Architecture
### **Users Table Schema**
```sql
CREATE TABLE users (
id VARCHAR(255) PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
name VARCHAR(255) NOT NULL,
picture TEXT,
role VARCHAR(50) NOT NULL DEFAULT 'coordinator',
provider VARCHAR(50) NOT NULL DEFAULT 'google',
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
last_sign_in_at TIMESTAMP WITH TIME ZONE
);
-- Optimized indexes for performance
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_role ON users(role);
```
### **Key Features**
-**Primary key constraints** - Unique user identification
-**Email uniqueness** - Prevents duplicate accounts
-**Proper indexing** - Fast lookups by email and role
-**Timezone-aware timestamps** - Accurate time tracking
-**Default values** - Sensible defaults for new users
## 🚀 System Components
### **1. DatabaseService (`databaseService.ts`)**
- **Connection pooling** with PostgreSQL
- **Automatic schema initialization** on startup
- **Transactional operations** for data consistency
- **Error handling** and connection management
- **Future-ready** with VIP and schedule tables
### **2. Enhanced Auth Routes (`simpleAuth.ts`)**
- **Async/await** for all database operations
- **Proper error handling** with database fallbacks
- **User creation** with automatic role assignment
- **Login tracking** with timestamp updates
- **Role-based access control** for admin operations
### **3. User Management API**
```typescript
// List all users (admin only)
GET /auth/users
// Update user role (admin only)
PATCH /auth/users/:email/role
Body: { "role": "administrator" | "coordinator" | "driver" }
// Delete user (admin only)
DELETE /auth/users/:email
// Get specific user (admin only)
GET /auth/users/:email
```
### **4. Frontend Interface (`UserManagement.tsx`)**
- **Real-time data** from PostgreSQL
- **Professional UI** with loading states
- **Error handling** with user feedback
- **Role management** with instant updates
- **Responsive design** for all screen sizes
## 🔧 Technical Advantages
### **Database Benefits:**
-**ACID compliance** - Guaranteed data consistency
-**Concurrent access** - Multiple users safely
-**Backup & recovery** - Enterprise-grade data protection
-**Scalability** - Handles thousands of users
-**Query optimization** - Indexed for performance
### **Security Features:**
-**SQL injection protection** - Parameterized queries
-**Connection pooling** - Efficient resource usage
-**Role validation** - Server-side permission checks
-**Transaction safety** - Atomic operations
### **Production Ready:**
-**Error handling** - Graceful failure recovery
-**Logging** - Comprehensive operation tracking
-**Connection management** - Automatic reconnection
-**Schema migration** - Safe database updates
## 📋 Setup & Usage
### **1. Database Initialization**
The system automatically creates tables on startup:
```bash
# Your existing Docker setup handles this
docker-compose -f docker-compose.dev.yml up
```
### **2. First User Setup**
- **First user** becomes administrator automatically
- **Subsequent users** become coordinators by default
- **Role changes** can be made through admin interface
### **3. User Management Workflow**
1. **Login with Google OAuth** - Users authenticate via Google
2. **Automatic user creation** - New users added to database
3. **Role assignment** - Admin can change user roles
4. **Permission enforcement** - Role-based access control
5. **User lifecycle** - Full CRUD operations for admins
## 🎯 Database Operations
### **User Creation Flow:**
```sql
-- Check if user exists
SELECT * FROM users WHERE email = $1;
-- Create new user if not exists
INSERT INTO users (id, email, name, picture, role, provider, last_sign_in_at)
VALUES ($1, $2, $3, $4, $5, $6, CURRENT_TIMESTAMP)
RETURNING *;
```
### **Role Update Flow:**
```sql
-- Update user role with timestamp
UPDATE users
SET role = $1, updated_at = CURRENT_TIMESTAMP
WHERE email = $2
RETURNING *;
```
### **Login Tracking:**
```sql
-- Update last sign-in timestamp
UPDATE users
SET last_sign_in_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
WHERE email = $1
RETURNING *;
```
## 🔍 Monitoring & Maintenance
### **Database Health:**
- **Connection status** logged on startup
- **Query performance** tracked in logs
- **Error handling** with detailed logging
- **Connection pooling** metrics available
### **User Analytics:**
- **User count** tracking for admin setup
- **Login patterns** via last_sign_in_at
- **Role distribution** via role indexing
- **Account creation** trends via created_at
## 🚀 Future Enhancements
### **Ready for Extension:**
- **User profiles** - Additional metadata fields
- **User groups** - Team-based permissions
- **Audit logging** - Track all user actions
- **Session management** - Advanced security
- **Multi-factor auth** - Enhanced security
### **Database Scaling:**
- **Read replicas** - For high-traffic scenarios
- **Partitioning** - For large user bases
- **Caching** - Redis integration ready
- **Backup strategies** - Automated backups
## 🎉 Production Benefits
### **Enterprise Grade:**
-**Reliable** - PostgreSQL battle-tested reliability
-**Scalable** - Handles growth from 10 to 10,000+ users
-**Secure** - Industry-standard security practices
-**Maintainable** - Clean, documented codebase
### **Developer Friendly:**
-**Type-safe** - Full TypeScript integration
-**Well-documented** - Clear API and database schema
-**Error-handled** - Graceful failure modes
-**Testable** - Isolated database operations
Your user management system is now **production-ready** with enterprise-grade PostgreSQL backing! 🚀
## 🔧 Quick Start
1. **Ensure PostgreSQL is running** (your Docker setup handles this)
2. **Restart your backend** to initialize tables
3. **Login as first user** to become administrator
4. **Manage users** through the beautiful admin interface
All user data is now safely stored in PostgreSQL with proper indexing, relationships, and ACID compliance!

View File

@@ -0,0 +1,24 @@
# Production Environment Variables
Copy this template to your deployment secrets manager or `.env` file before bringing up the production stack.
```bash
# PostgreSQL
DB_PASSWORD=change-me
# Backend application
FRONTEND_URL=https://your-domain.com
# Auth0 configuration
AUTH0_DOMAIN=your-tenant.region.auth0.com
AUTH0_CLIENT_ID=your-auth0-client-id
AUTH0_CLIENT_SECRET=your-auth0-client-secret
AUTH0_AUDIENCE=https://your-api-identifier (create an API in Auth0 and use its identifier)
INITIAL_ADMIN_EMAILS=primary.admin@example.com,another.admin@example.com
# Optional third-party integrations
AVIATIONSTACK_API_KEY=
```
> ⚠️ Never commit real secrets to version control. Use this file as a reference only.

View File

@@ -0,0 +1,29 @@
# Production Verification Checklist
Use this run-book after deploying the production stack.
## 1. Application health
- [ ] `curl http://<server-ip>:3000/api/health` returns `OK`.
- [ ] `docker compose -f docker-compose.prod.yml ps` shows `backend`, `frontend`, `db`, `redis` as healthy.
- [ ] PostgreSQL contains expected tables (`vips`, `drivers`, `schedule_events`, `flights`, `users`).
## 2. HTTPS validation
- [ ] DNS resolves the public domain to the Droplet (`dig vip.example.com +short`).
- [ ] `certbot certificates` shows the Let's Encrypt certificate with a valid expiry date.
- [ ] `curl -I https://vip.example.com` returns `200 OK`.
- [ ] Qualys SSL Labs scan reaches at least grade **A**.
## 3. Front-end smoke tests
- [ ] Load `/` in a private browser window and confirm the login screen renders.
- [ ] Use dev login or OAuth (depending on environment) to access the dashboard.
- [ ] Verify VIP and driver lists render without JavaScript console errors.
## 4. Hardening review
- [ ] UFW/DigitalOcean firewall allows only SSH (22), HTTP (80), HTTPS (443).
- [ ] `ADMIN_PASSWORD` and `JWT_SECRET` rotated to secure values.
- [ ] Scheduled backups for PostgreSQL configured (e.g., `pg_dump` cron or DO backups).
- [ ] Droplet rebooted after kernel updates (`needrestart` output clean).
- [ ] `docker compose logs` reviewed for warnings or stack traces.
Document any deviations above and create follow-up tasks.

218
README-API.md Normal file
View File

@@ -0,0 +1,218 @@
# VIP Coordinator API Documentation
## 📚 Overview
This document provides comprehensive API documentation for the VIP Coordinator system using **OpenAPI 3.0** (Swagger) specification. The API enables management of VIP transportation coordination, including flight tracking, driver management, and event scheduling.
## 🚀 Quick Start
### View API Documentation
1. **Interactive Documentation (Recommended):**
```bash
# Open the interactive Swagger UI documentation
open vip-coordinator/api-docs.html
```
Or visit: `file:///path/to/vip-coordinator/api-docs.html`
2. **Raw OpenAPI Specification:**
```bash
# View the YAML specification file
cat vip-coordinator/api-documentation.yaml
```
### Test the API
The interactive documentation includes a "Try it out" feature that allows you to test endpoints directly:
1. Open `api-docs.html` in your browser
2. Click on any endpoint to expand it
3. Click "Try it out" button
4. Fill in parameters and request body
5. Click "Execute" to make the API call
## 📋 API Categories
### 🏥 Health
- `GET /api/health` - System health check
### 👥 VIPs
- `GET /api/vips` - Get all VIPs
- `POST /api/vips` - Create new VIP
- `PUT /api/vips/{id}` - Update VIP
- `DELETE /api/vips/{id}` - Delete VIP
### 🚗 Drivers
- `GET /api/drivers` - Get all drivers
- `POST /api/drivers` - Create new driver
- `PUT /api/drivers/{id}` - Update driver
- `DELETE /api/drivers/{id}` - Delete driver
- `GET /api/drivers/{driverId}/schedule` - Get driver's schedule
- `POST /api/drivers/availability` - Check driver availability
- `POST /api/drivers/{driverId}/conflicts` - Check driver conflicts
### ✈️ Flights
- `GET /api/flights/{flightNumber}` - Get flight information
- `POST /api/flights/{flightNumber}/track` - Start flight tracking
- `DELETE /api/flights/{flightNumber}/track` - Stop flight tracking
- `POST /api/flights/batch` - Get multiple flights info
- `GET /api/flights/tracking/status` - Get tracking status
### 📅 Schedule
- `GET /api/vips/{vipId}/schedule` - Get VIP's schedule
- `POST /api/vips/{vipId}/schedule` - Add event to schedule
- `PUT /api/vips/{vipId}/schedule/{eventId}` - Update event
- `DELETE /api/vips/{vipId}/schedule/{eventId}` - Delete event
- `PATCH /api/vips/{vipId}/schedule/{eventId}/status` - Update event status
### ⚙️ Admin
- `POST /api/admin/authenticate` - Admin authentication
- `GET /api/admin/settings` - Get admin settings
- `POST /api/admin/settings` - Update admin settings
## 💡 Example API Calls
### Create a VIP with Flight
```bash
curl -X POST http://localhost:3000/api/vips \
-H "Content-Type: application/json" \
-d '{
"name": "John Doe",
"organization": "Tech Corp",
"transportMode": "flight",
"flights": [
{
"flightNumber": "UA1234",
"flightDate": "2025-06-26",
"segment": 1
}
],
"needsAirportPickup": true,
"needsVenueTransport": true,
"notes": "CEO - requires executive transport"
}'
```
### Add Event to VIP Schedule
```bash
curl -X POST http://localhost:3000/api/vips/{vipId}/schedule \
-H "Content-Type: application/json" \
-d '{
"title": "Meeting with CEO",
"location": "Hyatt Regency Denver",
"startTime": "2025-06-26T11:00:00",
"endTime": "2025-06-26T12:30:00",
"type": "meeting",
"assignedDriverId": "1748780965562",
"description": "Important strategic meeting"
}'
```
### Check Driver Availability
```bash
curl -X POST http://localhost:3000/api/drivers/availability \
-H "Content-Type: application/json" \
-d '{
"startTime": "2025-06-26T11:00:00",
"endTime": "2025-06-26T12:30:00",
"location": "Denver Convention Center"
}'
```
### Get Flight Information
```bash
curl "http://localhost:3000/api/flights/UA1234?date=2025-06-26"
```
## 🔧 Tools for API Documentation
### 1. **Swagger UI (Recommended)**
- **What it is:** Interactive web-based API documentation
- **Features:**
- Try endpoints directly in browser
- Auto-generated from OpenAPI spec
- Beautiful, responsive interface
- Request/response examples
- **Access:** Open `api-docs.html` in your browser
### 2. **OpenAPI Specification**
- **What it is:** Industry-standard API specification format
- **Features:**
- Machine-readable API definition
- Can generate client SDKs
- Supports validation and testing
- Compatible with many tools
- **File:** `api-documentation.yaml`
### 3. **Alternative Tools**
You can use the OpenAPI specification with other tools:
#### Postman
1. Import `api-documentation.yaml` into Postman
2. Automatically creates a collection with all endpoints
3. Includes examples and validation
#### Insomnia
1. Import the OpenAPI spec
2. Generate requests automatically
3. Built-in environment management
#### VS Code Extensions
- **OpenAPI (Swagger) Editor** - Edit and preview API specs
- **REST Client** - Test APIs directly in VS Code
## 📖 Documentation Best Practices
### Why OpenAPI/Swagger?
1. **Industry Standard:** Most widely adopted API documentation format
2. **Interactive:** Users can test APIs directly in the documentation
3. **Code Generation:** Can generate client libraries in multiple languages
4. **Validation:** Ensures API requests/responses match specification
5. **Tooling:** Extensive ecosystem of tools and integrations
### Documentation Features
- **Comprehensive:** All endpoints, parameters, and responses documented
- **Examples:** Real-world examples for all operations
- **Schemas:** Detailed data models with validation rules
- **Error Handling:** Clear error response documentation
- **Authentication:** Security requirements clearly specified
## 🔗 Integration Examples
### Frontend Integration
```javascript
// Example: Fetch VIPs in React
const fetchVips = async () => {
const response = await fetch('/api/vips');
const vips = await response.json();
return vips;
};
```
### Backend Integration
```bash
# Example: Using curl to test endpoints
curl -X GET http://localhost:3000/api/health
curl -X GET http://localhost:3000/api/vips
curl -X GET http://localhost:3000/api/drivers
```
## 🚀 Next Steps
1. **Explore the Interactive Docs:** Open `api-docs.html` and try the endpoints
2. **Test with Real Data:** Use the populated test data to explore functionality
3. **Build Integrations:** Use the API specification to build client applications
4. **Extend the API:** Add new endpoints following the established patterns
## 📞 Support
For questions about the API:
- Review the interactive documentation
- Check the OpenAPI specification for detailed schemas
- Test endpoints using the "Try it out" feature
- Refer to the example requests and responses
The API documentation is designed to be self-service and comprehensive, providing everything needed to integrate with the VIP Coordinator system.

155
README.md Normal file
View File

@@ -0,0 +1,155 @@
# VIP Coordinator Dashboard
A comprehensive web application for managing VIP logistics, driver assignments, and real-time tracking during events.
## Features
- **VIP Management**: Create, edit, and manage VIP profiles with flight information and schedules
- **Driver Coordination**: Assign and track drivers with real-time location updates
- **Flight Tracking**: Monitor flight status and arrival times
- **Schedule Management**: Organize VIP itineraries and driver assignments
- **Real-time Dashboard**: Overview of all active VIPs and available drivers
## Tech Stack
### Backend
- Node.js with Express.js
- TypeScript for type safety
- PostgreSQL database
- Redis for caching and real-time updates
- Docker containerization
### Frontend
- React 18 with TypeScript
- Vite for fast development
- React Router for navigation
- Leaflet for mapping (planned)
- Responsive design with CSS Grid/Flexbox
## Getting Started
### Prerequisites
- Docker and Docker Compose
- Node.js 18+ (for local development)
- npm or yarn
### Quick Start with Docker
1. Clone the repository and navigate to the project directory:
```bash
cd vip-coordinator
```
2. Start the development environment:
```bash
make dev
```
This will start all services:
- Frontend: http://localhost:5173
- Backend API: http://localhost:3000
- PostgreSQL: localhost:5432
- Redis: localhost:6379
### Manual Setup
#### Backend Setup
```bash
cd backend
npm install
npm run dev
```
#### Frontend Setup
```bash
cd frontend
npm install
npm run dev
```
## API Endpoints
### VIPs
- `GET /api/vips` - List all VIPs
- `POST /api/vips` - Create new VIP
- `GET /api/vips/:id` - Get VIP details
- `PUT /api/vips/:id` - Update VIP
- `DELETE /api/vips/:id` - Delete VIP
### Drivers
- `GET /api/drivers` - List all drivers
- `POST /api/drivers` - Create new driver
- `GET /api/drivers/:id` - Get driver details
- `PUT /api/drivers/:id` - Update driver
- `DELETE /api/drivers/:id` - Delete driver
### Health Check
- `GET /api/health` - Service health status
## Project Structure
```
vip-coordinator/
├── backend/
│ ├── src/
│ │ └── index.ts # Main server file
│ ├── package.json
│ ├── tsconfig.json
│ └── Dockerfile
├── frontend/
│ ├── src/
│ │ ├── components/ # Reusable components
│ │ ├── pages/ # Page components
│ │ ├── types/ # TypeScript types
│ │ ├── App.tsx # Main app component
│ │ └── main.tsx # Entry point
│ ├── package.json
│ ├── vite.config.ts
│ └── Dockerfile
├── docker-compose.dev.yml # Development environment
├── docker-compose.prod.yml # Production environment
├── Makefile # Development commands
└── README.md
```
## Development Commands
```bash
# Start development environment
make dev
# Build production images
make build
# Deploy to production
make deploy
# Backend only
cd backend && npm run dev
# Frontend only
cd frontend && npm run dev
```
## Planned Features
- [ ] Real-time GPS tracking for drivers
- [ ] Flight API integration for live updates
- [ ] Push notifications for schedule changes
- [ ] Google Sheets import/export
- [ ] Mobile-responsive driver app
- [ ] Advanced scheduling with drag-and-drop
- [ ] Reporting and analytics
- [ ] Multi-tenant support
## Contributing
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Test thoroughly
5. Submit a pull request
## License
This project is licensed under the MIT License.

View File

@@ -0,0 +1,217 @@
# 🌐 Reverse Proxy OAuth Setup Guide
## Your Current Setup
- **Internet** → **Router (ports 80/443)****Reverse Proxy****Frontend (port 5173)**
- **Backend (port 3000)** is only accessible locally
- **OAuth callback fails** because Google can't reach the backend
## The Problem
Google OAuth needs to redirect to your **backend** (`/auth/google/callback`), but your reverse proxy only forwards to the frontend. The backend port 3000 isn't exposed to the internet.
## Solution: Configure Reverse Proxy for Both Frontend and Backend
### Option 1: Single Domain with Path-Based Routing (Recommended)
Configure your reverse proxy to route both frontend and backend on the same domain:
```nginx
# Example Nginx configuration
server {
listen 443 ssl;
server_name bsa.madeamess.online;
# Frontend routes (everything except /auth and /api)
location / {
proxy_pass http://localhost:5173;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Backend API routes
location /api/ {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Backend auth routes (CRITICAL for OAuth)
location /auth/ {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
### Option 2: Subdomain Routing
If you prefer separate subdomains:
```nginx
# Frontend
server {
listen 443 ssl;
server_name bsa.madeamess.online;
location / {
proxy_pass http://localhost:5173;
# ... headers
}
}
# Backend API
server {
listen 443 ssl;
server_name api.bsa.madeamess.online;
location / {
proxy_pass http://localhost:3000;
# ... headers
}
}
```
## Update Environment Variables
### For Option 1 (Path-based - Recommended):
```bash
# backend/.env
GOOGLE_CLIENT_ID=308004695553-6k34bbq22frc4e76kejnkgq8mncepbbg.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=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
GOOGLE_REDIRECT_URI=https://bsa.madeamess.online/auth/google/callback
FRONTEND_URL=https://bsa.madeamess.online
```
### For Option 2 (Subdomain):
```bash
# backend/.env
GOOGLE_CLIENT_ID=308004695553-6k34bbq22frc4e76kejnkgq8mncepbbg.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=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
GOOGLE_REDIRECT_URI=https://api.bsa.madeamess.online/auth/google/callback
FRONTEND_URL=https://bsa.madeamess.online
```
## Update Google Cloud Console
### For Option 1 (Path-based):
**Authorized JavaScript origins:**
```
https://bsa.madeamess.online
```
**Authorized redirect URIs:**
```
https://bsa.madeamess.online/auth/google/callback
```
### For Option 2 (Subdomain):
**Authorized JavaScript origins:**
```
https://bsa.madeamess.online
https://api.bsa.madeamess.online
```
**Authorized redirect URIs:**
```
https://api.bsa.madeamess.online/auth/google/callback
```
## Frontend Configuration Update
If using Option 2 (subdomain), update your frontend to call the API subdomain:
```javascript
// In your frontend code, change API calls from:
fetch('/auth/google/url')
// To:
fetch('https://api.bsa.madeamess.online/auth/google/url')
```
## Testing Your Setup
### 1. Test Backend Accessibility
```bash
# Should work from internet
curl https://bsa.madeamess.online/auth/setup
# or for subdomain:
curl https://api.bsa.madeamess.online/auth/setup
```
### 2. Test OAuth URL Generation
```bash
curl https://bsa.madeamess.online/auth/google/url
# Should return a Google OAuth URL
```
### 3. Test Complete Flow
1. Visit `https://bsa.madeamess.online`
2. Click "Continue with Google"
3. Complete Google login
4. Should redirect back and authenticate
## Common Issues and Solutions
### Issue: "Invalid redirect URI"
- **Cause**: Google Console redirect URI doesn't match exactly
- **Fix**: Ensure exact match including `https://` and no trailing slash
### Issue: "OAuth not configured"
- **Cause**: Backend environment variables not updated
- **Fix**: Update `.env` file and restart containers
### Issue: Frontend can't reach backend
- **Cause**: Reverse proxy not configured for `/auth` and `/api` routes
- **Fix**: Add backend routing to your reverse proxy config
### Issue: CORS errors
- **Cause**: Frontend and backend on different origins
- **Fix**: Update CORS configuration in backend:
```javascript
// In backend/src/index.ts
app.use(cors({
origin: [
'https://bsa.madeamess.online',
'http://localhost:5173' // for local development
],
credentials: true
}));
```
## Recommended: Path-Based Routing
I recommend **Option 1 (path-based routing)** because:
- ✅ Single domain simplifies CORS
- ✅ Easier SSL certificate management
- ✅ Simpler frontend configuration
- ✅ Better for SEO and user experience
## Quick Setup Commands
```bash
# 1. Update environment variables
cd /home/kyle/Desktop/vip-coordinator
# Edit backend/.env with your domain
# 2. Restart containers
docker-compose -f docker-compose.dev.yml restart
# 3. Test the setup
curl https://bsa.madeamess.online/auth/setup
```
Your OAuth should work once you configure your reverse proxy to forward `/auth` and `/api` routes to the backend (port 3000)!

View File

@@ -0,0 +1,300 @@
# Role-Based Access Control (RBAC) System
## Overview
The VIP Coordinator application implements a comprehensive role-based access control system with three distinct user roles, each with specific permissions and access levels.
## User Roles
### 1. System Administrator (`administrator`)
**Highest privilege level - Full system access**
#### Permissions:
-**User Management**: Create, read, update, delete users
-**Role Management**: Assign and modify user roles
-**VIP Management**: Full CRUD operations on VIP records
-**Driver Management**: Full CRUD operations on driver records
-**Schedule Management**: Full CRUD operations on schedules
-**System Settings**: Access to admin panel and API configurations
-**Flight Tracking**: Access to all flight tracking features
-**Reports & Analytics**: Access to all system reports
#### API Endpoints Access:
```
POST /auth/users ✅ Admin only
GET /auth/users ✅ Admin only
PATCH /auth/users/:email/role ✅ Admin only
DELETE /auth/users/:email ✅ Admin only
POST /api/vips ✅ Admin + Coordinator
GET /api/vips ✅ All authenticated users
PUT /api/vips/:id ✅ Admin + Coordinator
DELETE /api/vips/:id ✅ Admin + Coordinator
POST /api/drivers ✅ Admin + Coordinator
GET /api/drivers ✅ All authenticated users
PUT /api/drivers/:id ✅ Admin + Coordinator
DELETE /api/drivers/:id ✅ Admin + Coordinator
POST /api/vips/:vipId/schedule ✅ Admin + Coordinator
GET /api/vips/:vipId/schedule ✅ All authenticated users
PUT /api/vips/:vipId/schedule/:id ✅ Admin + Coordinator
PATCH /api/vips/:vipId/schedule/:id/status ✅ All authenticated users
DELETE /api/vips/:vipId/schedule/:id ✅ Admin + Coordinator
```
### 2. Coordinator (`coordinator`)
**Standard operational access - Can manage VIPs, drivers, and schedules**
#### Permissions:
-**User Management**: Cannot manage users or roles
-**VIP Management**: Full CRUD operations on VIP records
-**Driver Management**: Full CRUD operations on driver records
-**Schedule Management**: Full CRUD operations on schedules
-**System Settings**: No access to admin panel
-**Flight Tracking**: Access to flight tracking features
-**Driver Availability**: Can check driver conflicts and availability
-**Status Updates**: Can update event statuses
#### Typical Use Cases:
- Managing VIP arrivals and departures
- Assigning drivers to VIPs
- Creating and updating schedules
- Monitoring flight statuses
- Coordinating transportation logistics
### 3. Driver (`driver`)
**Limited access - Can view assigned schedules and update status**
#### Permissions:
-**User Management**: Cannot manage users
-**VIP Management**: Cannot create/edit/delete VIPs
-**Driver Management**: Cannot manage other drivers
-**Schedule Creation**: Cannot create or delete schedules
-**View Schedules**: Can view VIP schedules and assigned events
-**Status Updates**: Can update status of assigned events
-**Personal Schedule**: Can view their own complete schedule
-**System Settings**: No access to admin features
#### API Endpoints Access:
```
GET /api/vips ✅ View only
GET /api/drivers ✅ View only
GET /api/vips/:vipId/schedule ✅ View only
PATCH /api/vips/:vipId/schedule/:id/status ✅ Can update status
GET /api/drivers/:driverId/schedule ✅ Own schedule only
```
#### Typical Use Cases:
- Viewing assigned VIP transportation schedules
- Updating event status (en route, completed, delayed)
- Checking personal daily/weekly schedule
- Viewing VIP contact information and notes
## Authentication Flow
### 1. Google OAuth Integration
- Users authenticate via Google OAuth 2.0
- First user automatically becomes `administrator`
- Subsequent users default to `coordinator` role
- Administrators can change user roles after authentication
### 2. JWT Token System
- Secure JWT tokens issued after successful authentication
- Tokens include user role information
- Middleware validates tokens and role permissions on each request
### 3. Role Assignment
```typescript
// First user becomes admin
const userCount = await databaseService.getUserCount();
const role = userCount === 0 ? 'administrator' : 'coordinator';
```
## Security Implementation
### Middleware Protection
```typescript
// Authentication required
app.get('/api/vips', requireAuth, async (req, res) => { ... });
// Role-based access
app.post('/api/vips', requireAuth, requireRole(['coordinator', 'administrator']),
async (req, res) => { ... });
// Admin only
app.get('/auth/users', requireAuth, requireRole(['administrator']),
async (req, res) => { ... });
```
### Frontend Role Checking
```typescript
// User Management component
if (currentUser?.role !== 'administrator') {
return (
<div className="p-6 bg-red-50 border border-red-200 rounded-lg">
<h2 className="text-xl font-semibold text-red-800 mb-2">Access Denied</h2>
<p className="text-red-600">You need administrator privileges to access user management.</p>
</div>
);
}
```
## Database Schema
### Users Table
```sql
CREATE TABLE users (
id VARCHAR(255) PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
name VARCHAR(255) NOT NULL,
picture TEXT,
role VARCHAR(50) NOT NULL DEFAULT 'coordinator',
provider VARCHAR(50) NOT NULL DEFAULT 'google',
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
last_sign_in_at TIMESTAMP WITH TIME ZONE
);
-- Indexes for performance
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_role ON users(role);
```
## Role Transition Guidelines
### Promoting Users
1. **Coordinator → Administrator**
- Grants full system access
- Can manage other users
- Access to system settings
- Should be limited to trusted personnel
2. **Driver → Coordinator**
- Grants VIP and schedule management
- Can assign other drivers
- Suitable for supervisory roles
### Demoting Users
1. **Administrator → Coordinator**
- Removes user management access
- Retains operational capabilities
- Cannot access system settings
2. **Coordinator → Driver**
- Removes management capabilities
- Retains view and status update access
- Suitable for field personnel
## Best Practices
### 1. Principle of Least Privilege
- Users should have minimum permissions necessary for their role
- Regular review of user roles and permissions
- Temporary elevation should be avoided
### 2. Role Assignment Strategy
- **Administrators**: IT staff, senior management (limit to 2-3 users)
- **Coordinators**: Operations staff, event coordinators (primary users)
- **Drivers**: Field personnel, transportation staff
### 3. Security Considerations
- Regular audit of user access logs
- Monitor for privilege escalation attempts
- Implement session timeouts for sensitive operations
- Use HTTPS for all authentication flows
### 4. Emergency Access
- Maintain at least one administrator account
- Document emergency access procedures
- Consider backup authentication methods
## API Security Features
### 1. Token Validation
```typescript
export function requireAuth(req: Request, res: Response, next: NextFunction) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
const token = authHeader.substring(7);
const user = verifyToken(token);
if (!user) {
return res.status(401).json({ error: 'Invalid token' });
}
(req as any).user = user;
next();
}
```
### 2. Role Validation
```typescript
export function requireRole(roles: string[]) {
return (req: Request, res: Response, next: NextFunction) => {
const user = (req as any).user;
if (!user || !roles.includes(user.role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
```
## Monitoring and Auditing
### 1. User Activity Logging
- Track user login/logout events
- Log role changes and who made them
- Monitor sensitive operations (user deletion, role changes)
### 2. Access Attempt Monitoring
- Failed authentication attempts
- Unauthorized access attempts
- Privilege escalation attempts
### 3. Regular Security Reviews
- Quarterly review of user roles
- Annual security audit
- Regular password/token rotation
## Future Enhancements
### 1. Granular Permissions
- Department-based access control
- Resource-specific permissions
- Time-based access restrictions
### 2. Advanced Security Features
- Multi-factor authentication
- IP-based access restrictions
- Session management improvements
### 3. Audit Trail
- Comprehensive activity logging
- Change history tracking
- Compliance reporting
---
## Quick Reference
| Feature | Administrator | Coordinator | Driver |
|---------|--------------|-------------|--------|
| User Management | ✅ | ❌ | ❌ |
| VIP CRUD | ✅ | ✅ | ❌ |
| Driver CRUD | ✅ | ✅ | ❌ |
| Schedule CRUD | ✅ | ✅ | ❌ |
| Status Updates | ✅ | ✅ | ✅ |
| View Data | ✅ | ✅ | ✅ |
| System Settings | ✅ | ❌ | ❌ |
| Flight Tracking | ✅ | ✅ | ❌ |
**Last Updated**: June 2, 2025
**Version**: 1.0

159
SIMPLE_OAUTH_SETUP.md Normal file
View File

@@ -0,0 +1,159 @@
# Simple OAuth2 Setup Guide
## ✅ What's Working Now
The VIP Coordinator now has a **much simpler** OAuth2 implementation that actually works! Here's what I've done:
### 🔧 Simplified Implementation
- **Removed complex Passport.js** - No more confusing middleware chains
- **Simple JWT tokens** - Clean, stateless authentication
- **Direct Google API calls** - Using fetch instead of heavy libraries
- **Clean error handling** - Easy to debug and understand
### 📁 New Files Created
- `backend/src/config/simpleAuth.ts` - Core auth functions
- `backend/src/routes/simpleAuth.ts` - Auth endpoints
## 🚀 How to Set Up Google OAuth2
### Step 1: Get Google OAuth2 Credentials
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
2. Create a new project or select existing one
3. Enable the Google+ API
4. Go to "Credentials" → "Create Credentials" → "OAuth 2.0 Client IDs"
5. Set application type to "Web application"
6. Add these redirect URIs:
- `http://localhost:3000/auth/google/callback`
- `http://localhost:5173/auth/callback`
### Step 2: Update Environment Variables
Edit `backend/.env` and add:
```bash
# Google OAuth2 Settings
GOOGLE_CLIENT_ID=your_google_client_id_here
GOOGLE_CLIENT_SECRET=your_google_client_secret_here
GOOGLE_REDIRECT_URI=http://localhost:3000/auth/google/callback
# JWT Secret (change this!)
JWT_SECRET=your-super-secret-jwt-key-change-this
# Frontend URL
FRONTEND_URL=http://localhost:5173
```
### Step 3: Test the Setup
1. **Start the application:**
```bash
docker-compose -f docker-compose.dev.yml up -d
```
2. **Test auth endpoints:**
```bash
# Check if backend is running
curl http://localhost:3000/api/health
# Check auth status (should return {"authenticated":false})
curl http://localhost:3000/auth/status
```
3. **Test Google OAuth flow:**
- Visit: `http://localhost:3000/auth/google`
- Should redirect to Google login
- After login, redirects back with JWT token
## 🔄 How It Works
### Simple Flow:
1. User clicks "Login with Google"
2. Redirects to `http://localhost:3000/auth/google`
3. Backend redirects to Google OAuth
4. Google redirects back to `/auth/google/callback`
5. Backend exchanges code for user info
6. Backend creates JWT token
7. Frontend receives token and stores it
### API Endpoints:
- `GET /auth/google` - Start OAuth flow
- `GET /auth/google/callback` - Handle OAuth callback
- `GET /auth/status` - Check if user is authenticated
- `GET /auth/me` - Get current user info (requires auth)
- `POST /auth/logout` - Logout (client-side token removal)
## 🛠️ Frontend Integration
The frontend needs to:
1. **Handle the OAuth callback:**
```javascript
// In your React app, handle the callback route
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('token');
if (token) {
localStorage.setItem('authToken', token);
// Redirect to dashboard
}
```
2. **Include token in API requests:**
```javascript
const token = localStorage.getItem('authToken');
fetch('/api/vips', {
headers: {
'Authorization': `Bearer ${token}`
}
});
```
3. **Add login button:**
```javascript
<button onClick={() => window.location.href = '/auth/google'}>
Login with Google
</button>
```
## 🎯 Benefits of This Approach
- **Simple to understand** - No complex middleware
- **Easy to debug** - Clear error messages
- **Lightweight** - Fewer dependencies
- **Secure** - Uses standard JWT tokens
- **Flexible** - Easy to extend or modify
## 🔍 Troubleshooting
### Common Issues:
1. **"OAuth not configured" error:**
- Make sure `GOOGLE_CLIENT_ID` is set in `.env`
- Restart the backend after changing `.env`
2. **"Invalid redirect URI" error:**
- Check Google Console redirect URIs match exactly
- Make sure no trailing slashes
3. **Token verification fails:**
- Check `JWT_SECRET` is set and consistent
- Make sure token is being sent with `Bearer ` prefix
### Debug Commands:
```bash
# Check backend logs
docker-compose -f docker-compose.dev.yml logs backend
# Check if environment variables are loaded
docker exec vip-coordinator-backend-1 env | grep GOOGLE
```
## 🎉 Next Steps
1. Set up your Google OAuth2 credentials
2. Update the `.env` file
3. Test the login flow
4. Integrate with the frontend
5. Customize user roles and permissions
The authentication system is now much simpler and actually works! 🚀

125
SIMPLE_USER_MANAGEMENT.md Normal file
View File

@@ -0,0 +1,125 @@
# 🔐 Simple User Management System
## ✅ What We Built
A **lightweight, persistent user management system** that extends your existing OAuth2 authentication using your existing JSON data storage.
## 🎯 Key Features
### ✅ **Persistent Storage**
- Uses your existing JSON data file storage
- No third-party services required
- Completely self-contained
- Users preserved across server restarts
### 🔧 **New API Endpoints**
- `GET /auth/users` - List all users (admin only)
- `PATCH /auth/users/:email/role` - Update user role (admin only)
- `DELETE /auth/users/:email` - Delete user (admin only)
- `GET /auth/users/:email` - Get specific user (admin only)
### 🎨 **Admin Interface**
- Beautiful React component for user management
- Role-based access control (admin only)
- Change user roles with dropdown
- Delete users with confirmation
- Responsive design
## 🚀 How It Works
### 1. **User Registration**
- First user becomes administrator automatically
- Subsequent users become coordinators by default
- All via your existing Google OAuth flow
### 2. **Role Management**
- **Administrator:** Full access including user management
- **Coordinator:** Can manage VIPs, drivers, schedules
- **Driver:** Can view assigned schedules
### 3. **User Management Interface**
- Only administrators can access user management
- View all users with profile pictures
- Change roles instantly
- Delete users (except yourself)
- Clear role descriptions
## 📋 Usage
### For Administrators:
1. Login with Google (first user becomes admin)
2. Access user management interface
3. View all registered users
4. Change user roles as needed
5. Remove users if necessary
### API Examples:
```bash
# List all users (admin only)
curl -H "Authorization: Bearer YOUR_JWT_TOKEN" \
http://localhost:3000/auth/users
# Update user role
curl -X PATCH \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"role": "administrator"}' \
http://localhost:3000/auth/users/user@example.com/role
# Delete user
curl -X DELETE \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
http://localhost:3000/auth/users/user@example.com
```
## 🔒 Security Features
- **Role-based access control** - Only admins can manage users
- **Self-deletion prevention** - Admins can't delete themselves
- **JWT token validation** - All endpoints require authentication
- **Input validation** - Role validation on updates
## ✅ Important Notes
### **Persistent File Storage**
- Users are stored in your existing JSON data file
- **Users are preserved across server restarts**
- Perfect for development and production
- Integrates seamlessly with your existing data storage
### **Simple & Lightweight**
- No external dependencies
- No complex setup required
- Works with your existing OAuth system
- Easy to understand and modify
## 🎯 Perfect For
- **Development and production environments**
- **Small to medium teams** (< 100 users)
- **Self-hosted applications**
- **When you want full control** over your user data
- **Simple, reliable user management**
## 🔄 Future Enhancements
You can easily extend this to:
- Migrate to your existing PostgreSQL database if needed
- Add user metadata and profiles
- Implement audit logging
- Add email notifications
- Create user groups/teams
- Add Redis caching for better performance
## 🎉 Ready to Use!
Your user management system is now complete and ready to use:
1. **Restart your backend** to pick up the new endpoints
2. **Login as the first user** to become administrator
3. **Access user management** through your admin interface
4. **Manage users** with the beautiful interface we built
**✅ Persistent storage:** All user data is automatically saved to your existing JSON data file and preserved across server restarts!
No external dependencies, no complex setup - just simple, effective, persistent user management! 🚀

View File

@@ -0,0 +1,197 @@
# 🔐 User Management System Recommendations
## Current State Analysis
**You have:** Basic OAuth2 with Google, JWT tokens, role-based access (administrator/coordinator)
**You need:** Comprehensive user management, permissions, user lifecycle, admin interface
## 🏆 Top Recommendations
### 1. **Supabase Auth** (Recommended - Easy Integration)
**Why it's perfect for you:**
- Drop-in replacement for your current auth system
- Built-in user management dashboard
- Row Level Security (RLS) for fine-grained permissions
- Supports Google OAuth (you can keep your current flow)
- Real-time subscriptions
- Built-in user roles and metadata
**Integration effort:** Low (2-3 days)
```bash
npm install @supabase/supabase-js
```
**Features you get:**
- User registration/login/logout
- Email verification
- Password reset
- User metadata and custom claims
- Admin dashboard for user management
- Real-time user presence
- Multi-factor authentication
### 2. **Auth0** (Enterprise-grade)
**Why it's great:**
- Industry standard for enterprise applications
- Extensive user management dashboard
- Advanced security features
- Supports all OAuth providers
- Fine-grained permissions and roles
- Audit logs and analytics
**Integration effort:** Medium (3-5 days)
```bash
npm install auth0 express-oauth-server
```
**Features you get:**
- Complete user lifecycle management
- Advanced role-based access control (RBAC)
- Multi-factor authentication
- Social logins (Google, Facebook, etc.)
- Enterprise SSO
- Comprehensive admin dashboard
### 3. **Firebase Auth + Firestore** (Google Ecosystem)
**Why it fits:**
- You're already using Google OAuth
- Seamless integration with Google services
- Real-time database
- Built-in user management
- Offline support
**Integration effort:** Medium (4-6 days)
```bash
npm install firebase-admin
```
### 4. **Clerk** (Modern Developer Experience)
**Why developers love it:**
- Beautiful pre-built UI components
- Excellent TypeScript support
- Built-in user management dashboard
- Easy role and permission management
- Great documentation
**Integration effort:** Low-Medium (2-4 days)
```bash
npm install @clerk/clerk-sdk-node
```
## 🎯 My Recommendation: **Supabase Auth**
### Why Supabase is perfect for your project:
1. **Minimal code changes** - Can integrate with your existing JWT system
2. **Built-in admin dashboard** - No need to build user management UI
3. **PostgreSQL-based** - Familiar database, easy to extend
4. **Real-time features** - Perfect for your VIP coordination needs
5. **Row Level Security** - Fine-grained permissions per user/role
6. **Free tier** - Great for development and small deployments
### Quick Integration Plan:
#### Step 1: Setup Supabase Project
```bash
# Install Supabase
npm install @supabase/supabase-js
# Create project at https://supabase.com
# Get your project URL and anon key
```
#### Step 2: Replace your user storage
```typescript
// Instead of: const users: Map<string, User> = new Map();
// Use Supabase's built-in auth.users table
```
#### Step 3: Add user management endpoints
```typescript
// Get all users (admin only)
router.get('/users', requireAuth, requireRole(['administrator']), async (req, res) => {
const { data: users } = await supabase.auth.admin.listUsers();
res.json(users);
});
// Update user role
router.patch('/users/:id/role', requireAuth, requireRole(['administrator']), async (req, res) => {
const { role } = req.body;
const { data } = await supabase.auth.admin.updateUserById(req.params.id, {
user_metadata: { role }
});
res.json(data);
});
```
#### Step 4: Add frontend user management
- Use Supabase's built-in dashboard OR
- Build simple admin interface with user list/edit/delete
## 🚀 Implementation Options
### Option A: Quick Integration (Keep your current system + add Supabase)
- Keep your current OAuth flow
- Add Supabase for user storage and management
- Use Supabase dashboard for admin tasks
- **Time:** 2-3 days
### Option B: Full Migration (Replace with Supabase Auth)
- Migrate to Supabase Auth completely
- Use their OAuth providers
- Get all advanced features
- **Time:** 4-5 days
### Option C: Custom Admin Interface
- Keep your current system
- Build custom React admin interface
- Add user CRUD operations
- **Time:** 1-2 weeks
## 📋 Next Steps
1. **Choose your approach** (I recommend Option A - Quick Integration)
2. **Set up Supabase project** (5 minutes)
3. **Integrate user storage** (1 day)
4. **Add admin endpoints** (1 day)
5. **Test and refine** (1 day)
## 🔧 Alternative: Lightweight Custom Solution
If you prefer to keep it simple and custom:
```typescript
// Add these endpoints to your existing auth system:
// List all users (admin only)
router.get('/users', requireAuth, requireRole(['administrator']), (req, res) => {
const userList = Array.from(users.values()).map(user => ({
id: user.id,
email: user.email,
name: user.name,
role: user.role,
lastLogin: user.lastLogin
}));
res.json(userList);
});
// Update user role
router.patch('/users/:email/role', requireAuth, requireRole(['administrator']), (req, res) => {
const { role } = req.body;
const user = users.get(req.params.email);
if (user) {
user.role = role;
users.set(req.params.email, user);
res.json({ success: true });
} else {
res.status(404).json({ error: 'User not found' });
}
});
// Delete user
router.delete('/users/:email', requireAuth, requireRole(['administrator']), (req, res) => {
users.delete(req.params.email);
res.json({ success: true });
});
```
Would you like me to help you implement any of these options?

140
WEB_SERVER_PROXY_SETUP.md Normal file
View File

@@ -0,0 +1,140 @@
# 🌐 Web Server Proxy Configuration for OAuth
## 🎯 Problem Identified
Your domain `bsa.madeamess.online` is not properly configured to proxy requests to your Docker containers. When Google redirects to `https://bsa.madeamess.online:5173/auth/google/callback`, it gets "ERR_CONNECTION_REFUSED" because there's no web server listening on port 5173 for your domain.
## 🔧 Solution Options
### Option 1: Configure Nginx Proxy (Recommended)
If you're using nginx, add this configuration:
```nginx
# /etc/nginx/sites-available/bsa.madeamess.online
server {
listen 443 ssl;
server_name bsa.madeamess.online;
# SSL configuration (your existing SSL setup)
ssl_certificate /path/to/your/certificate.crt;
ssl_certificate_key /path/to/your/private.key;
# Proxy to your Docker frontend container
location / {
proxy_pass http://localhost:5173;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
# Important: Handle all routes for SPA
try_files $uri $uri/ @fallback;
}
# Fallback for SPA routing
location @fallback {
proxy_pass http://localhost:5173;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# Redirect HTTP to HTTPS
server {
listen 80;
server_name bsa.madeamess.online;
return 301 https://$server_name$request_uri;
}
```
### Option 2: Configure Apache Proxy
If you're using Apache, add this to your virtual host:
```apache
<VirtualHost *:443>
ServerName bsa.madeamess.online
# SSL configuration (your existing SSL setup)
SSLEngine on
SSLCertificateFile /path/to/your/certificate.crt
SSLCertificateKeyFile /path/to/your/private.key
# Enable proxy modules
ProxyPreserveHost On
ProxyRequests Off
# Proxy to your Docker frontend container
ProxyPass / http://localhost:5173/
ProxyPassReverse / http://localhost:5173/
# Handle WebSocket connections for Vite HMR
ProxyPass /ws ws://localhost:5173/ws
ProxyPassReverse /ws ws://localhost:5173/ws
</VirtualHost>
<VirtualHost *:80>
ServerName bsa.madeamess.online
Redirect permanent / https://bsa.madeamess.online/
</VirtualHost>
```
### Option 3: Update Google OAuth Redirect URI (Quick Fix)
**Temporary workaround:** Update your Google Cloud Console OAuth settings to use `http://localhost:5173/auth/google/callback` instead of your domain, then access your app directly via `http://localhost:5173`.
## 🔄 Alternative: Use Standard Ports
### Option 4: Configure to use standard ports (80/443)
Modify your docker-compose to use standard ports:
```yaml
# In docker-compose.dev.yml
services:
frontend:
ports:
- "80:5173" # HTTP
# or
- "443:5173" # HTTPS (requires SSL setup in container)
```
Then update Google OAuth redirect URI to:
- `https://bsa.madeamess.online/auth/google/callback` (no port)
## 🧪 Testing Steps
1. **Apply web server configuration**
2. **Restart your web server:**
```bash
# For nginx
sudo systemctl reload nginx
# For Apache
sudo systemctl reload apache2
```
3. **Test the proxy:**
```bash
curl -I https://bsa.madeamess.online
```
4. **Test OAuth flow:**
- Visit `https://bsa.madeamess.online`
- Click "Continue with Google"
- Complete authentication
- Should redirect back successfully
## 🎯 Root Cause Summary
The OAuth callback was failing because:
1. ✅ **Frontend routing** - Fixed (React Router now handles callback)
2. ✅ **CORS configuration** - Fixed (Backend accepts your domain)
3. ❌ **Web server proxy** - **NEEDS FIXING** (Domain not proxying to Docker)
Once you configure your web server to proxy `bsa.madeamess.online` to `localhost:5173`, the OAuth flow will work perfectly!

148
api-docs.html Normal file
View File

@@ -0,0 +1,148 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VIP Coordinator API Documentation</title>
<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui.css" />
<style>
html {
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*, *:before, *:after {
box-sizing: inherit;
}
body {
margin:0;
background: #fafafa;
}
.swagger-ui .topbar {
background-color: #3498db;
}
.swagger-ui .topbar .download-url-wrapper .select-label {
color: white;
}
.swagger-ui .topbar .download-url-wrapper input[type=text] {
border: 2px solid #2980b9;
}
.swagger-ui .info .title {
color: #2c3e50;
}
.custom-header {
background: linear-gradient(135deg, #3498db, #2980b9);
color: white;
padding: 20px;
text-align: center;
margin-bottom: 20px;
}
.custom-header h1 {
margin: 0;
font-size: 2.5em;
font-weight: 300;
}
.custom-header p {
margin: 10px 0 0 0;
font-size: 1.2em;
opacity: 0.9;
}
.quick-links {
background: white;
padding: 20px;
margin: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.quick-links h3 {
color: #2c3e50;
margin-top: 0;
}
.quick-links ul {
list-style: none;
padding: 0;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 10px;
}
.quick-links li {
background: #ecf0f1;
padding: 10px 15px;
border-radius: 5px;
border-left: 4px solid #3498db;
}
.quick-links li strong {
color: #2c3e50;
}
.quick-links li code {
background: #34495e;
color: white;
padding: 2px 6px;
border-radius: 3px;
font-size: 0.9em;
}
</style>
</head>
<body>
<div class="custom-header">
<h1>🚗 VIP Coordinator API</h1>
<p>Comprehensive API for managing VIP transportation coordination</p>
</div>
<div class="quick-links">
<h3>🚀 Quick Start Examples</h3>
<ul>
<li><strong>Health Check:</strong> <code>GET /api/health</code></li>
<li><strong>Get All VIPs:</strong> <code>GET /api/vips</code></li>
<li><strong>Get All Drivers:</strong> <code>GET /api/drivers</code></li>
<li><strong>Flight Info:</strong> <code>GET /api/flights/UA1234?date=2025-06-26</code></li>
<li><strong>VIP Schedule:</strong> <code>GET /api/vips/{vipId}/schedule</code></li>
<li><strong>Driver Availability:</strong> <code>POST /api/drivers/availability</code></li>
</ul>
</div>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui-bundle.js"></script>
<script src="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui-standalone-preset.js"></script>
<script>
window.onload = function() {
// Begin Swagger UI call region
const ui = SwaggerUIBundle({
url: './api-documentation.yaml',
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout",
tryItOutEnabled: true,
requestInterceptor: function(request) {
// Add base URL if not present
if (request.url.startsWith('/api/')) {
request.url = 'http://localhost:3000' + request.url;
}
return request;
},
onComplete: function() {
console.log('VIP Coordinator API Documentation loaded successfully!');
},
docExpansion: 'list',
defaultModelsExpandDepth: 2,
defaultModelExpandDepth: 2,
showExtensions: true,
showCommonExtensions: true,
supportedSubmitMethods: ['get', 'post', 'put', 'delete', 'patch'],
validatorUrl: null
});
// End Swagger UI call region
window.ui = ui;
};
</script>
</body>
</html>

1189
api-documentation.yaml Normal file

File diff suppressed because it is too large Load Diff

23
backend/.env Normal file
View File

@@ -0,0 +1,23 @@
# Database Configuration
DATABASE_URL=postgresql://postgres:password@db:5432/vip_coordinator
# Redis Configuration
REDIS_URL=redis://redis:6379
# Authentication Configuration
JWT_SECRET=your-super-secure-jwt-secret-key-change-in-production-12345
SESSION_SECRET=your-super-secure-session-secret-change-in-production-67890
# Google OAuth Configuration
GOOGLE_CLIENT_ID=308004695553-6k34bbq22frc4e76kejnkgq8mncepbbg.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=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
GOOGLE_REDIRECT_URI=https://api.bsa.madeamess.online/auth/google/callback
# Frontend URL
FRONTEND_URL=https://bsa.madeamess.online
# Flight API Configuration
AVIATIONSTACK_API_KEY=your-aviationstack-api-key
# Admin Configuration
ADMIN_PASSWORD=admin123

22
backend/.env.example Normal file
View File

@@ -0,0 +1,22 @@
# Database Configuration
DATABASE_URL=postgresql://postgres:password@db:5432/vip_coordinator
# Redis Configuration
REDIS_URL=redis://redis:6379
# Authentication Configuration
JWT_SECRET=your-super-secure-jwt-secret-key-change-in-production
SESSION_SECRET=your-super-secure-session-secret-change-in-production
# Google OAuth Configuration
GOOGLE_CLIENT_ID=your-google-client-id-from-console
GOOGLE_CLIENT_SECRET=your-google-client-secret-from-console
# Frontend URL
FRONTEND_URL=http://localhost:5173
# Flight API Configuration
AVIATIONSTACK_API_KEY=your-aviationstack-api-key
# Admin Configuration
ADMIN_PASSWORD=admin123

24
backend/Dockerfile Normal file
View File

@@ -0,0 +1,24 @@
# Multi-stage build for development and production
FROM node:18-alpine AS base
WORKDIR /app
# Copy package files
COPY package*.json ./
# Development stage
FROM base AS development
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "run", "dev"]
# Production stage
FROM base AS production
RUN npm ci
COPY . .
RUN npm run build
RUN npm prune --omit=dev
ENV NODE_ENV=production
EXPOSE 3000
CMD ["npm", "start"]

3569
backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

40
backend/package.json Normal file
View File

@@ -0,0 +1,40 @@
{
"name": "vip-coordinator-backend",
"version": "1.0.0",
"description": "Backend API for VIP Coordinator Dashboard",
"main": "dist/index.js",
"scripts": {
"start": "node dist/index.js",
"dev": "ts-node-dev --respawn --transpile-only src/index.ts",
"build": "tsc",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"vip",
"coordinator",
"dashboard",
"api"
],
"author": "",
"license": "ISC",
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.2",
"jwks-rsa": "^3.2.0",
"pg": "^8.11.3",
"redis": "^4.6.8",
"uuid": "^9.0.0"
},
"devDependencies": {
"@types/cors": "^2.8.13",
"@types/express": "^4.17.17",
"@types/jsonwebtoken": "^9.0.2",
"@types/node": "^20.5.0",
"@types/pg": "^8.10.2",
"@types/uuid": "^9.0.2",
"ts-node-dev": "^2.0.0",
"typescript": "^5.1.6"
}
}

View File

@@ -0,0 +1,148 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VIP Coordinator API Documentation</title>
<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui.css" />
<style>
html {
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*, *:before, *:after {
box-sizing: inherit;
}
body {
margin:0;
background: #fafafa;
}
.swagger-ui .topbar {
background-color: #3498db;
}
.swagger-ui .topbar .download-url-wrapper .select-label {
color: white;
}
.swagger-ui .topbar .download-url-wrapper input[type=text] {
border: 2px solid #2980b9;
}
.swagger-ui .info .title {
color: #2c3e50;
}
.custom-header {
background: linear-gradient(135deg, #3498db, #2980b9);
color: white;
padding: 20px;
text-align: center;
margin-bottom: 20px;
}
.custom-header h1 {
margin: 0;
font-size: 2.5em;
font-weight: 300;
}
.custom-header p {
margin: 10px 0 0 0;
font-size: 1.2em;
opacity: 0.9;
}
.quick-links {
background: white;
padding: 20px;
margin: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.quick-links h3 {
color: #2c3e50;
margin-top: 0;
}
.quick-links ul {
list-style: none;
padding: 0;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 10px;
}
.quick-links li {
background: #ecf0f1;
padding: 10px 15px;
border-radius: 5px;
border-left: 4px solid #3498db;
}
.quick-links li strong {
color: #2c3e50;
}
.quick-links li code {
background: #34495e;
color: white;
padding: 2px 6px;
border-radius: 3px;
font-size: 0.9em;
}
</style>
</head>
<body>
<div class="custom-header">
<h1>🚗 VIP Coordinator API</h1>
<p>Comprehensive API for managing VIP transportation coordination</p>
</div>
<div class="quick-links">
<h3>🚀 Quick Start Examples</h3>
<ul>
<li><strong>Health Check:</strong> <code>GET /api/health</code></li>
<li><strong>Get All VIPs:</strong> <code>GET /api/vips</code></li>
<li><strong>Get All Drivers:</strong> <code>GET /api/drivers</code></li>
<li><strong>Flight Info:</strong> <code>GET /api/flights/UA1234?date=2025-06-26</code></li>
<li><strong>VIP Schedule:</strong> <code>GET /api/vips/{vipId}/schedule</code></li>
<li><strong>Driver Availability:</strong> <code>POST /api/drivers/availability</code></li>
</ul>
</div>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui-bundle.js"></script>
<script src="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui-standalone-preset.js"></script>
<script>
window.onload = function() {
// Begin Swagger UI call region
const ui = SwaggerUIBundle({
url: 'http://localhost:3000/api-documentation.yaml',
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout",
tryItOutEnabled: true,
requestInterceptor: function(request) {
// Add base URL if not present
if (request.url.startsWith('/api/')) {
request.url = 'http://localhost:3000' + request.url;
}
return request;
},
onComplete: function() {
console.log('VIP Coordinator API Documentation loaded successfully!');
},
docExpansion: 'list',
defaultModelsExpandDepth: 2,
defaultModelExpandDepth: 2,
showExtensions: true,
showCommonExtensions: true,
supportedSubmitMethods: ['get', 'post', 'put', 'delete', 'patch'],
validatorUrl: null
});
// End Swagger UI call region
window.ui = ui;
};
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,25 @@
import { Pool } from 'pg';
import dotenv from 'dotenv';
dotenv.config();
const useSSL = process.env.DATABASE_SSL === 'true';
const pool = new Pool({
connectionString: process.env.DATABASE_URL || 'postgresql://postgres:changeme@localhost:5432/vip_coordinator',
ssl: useSSL ? { rejectUnauthorized: false } : false,
max: 20,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});
// Test the connection
pool.on('connect', () => {
console.log('✅ Connected to PostgreSQL database');
});
pool.on('error', (err) => {
console.error('❌ PostgreSQL connection error:', err);
});
export default pool;

View File

@@ -0,0 +1,23 @@
import { createClient } from 'redis';
import dotenv from 'dotenv';
dotenv.config();
const redisClient = createClient({
url: process.env.REDIS_URL || 'redis://localhost:6379'
});
redisClient.on('connect', () => {
console.log('✅ Connected to Redis');
});
redisClient.on('error', (err: Error) => {
console.error('❌ Redis connection error:', err);
});
// Connect to Redis
redisClient.connect().catch((err: Error) => {
console.error('❌ Failed to connect to Redis:', err);
});
export default redisClient;

View File

@@ -0,0 +1,130 @@
-- VIP Coordinator Database Schema
-- Create VIPs table
CREATE TABLE IF NOT EXISTS vips (
id VARCHAR(255) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
organization VARCHAR(255) NOT NULL,
department VARCHAR(255) DEFAULT 'Office of Development',
transport_mode VARCHAR(50) NOT NULL CHECK (transport_mode IN ('flight', 'self-driving')),
expected_arrival TIMESTAMP,
needs_airport_pickup BOOLEAN DEFAULT false,
needs_venue_transport BOOLEAN DEFAULT true,
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Create flights table (for VIPs with flight transport)
CREATE TABLE IF NOT EXISTS flights (
id SERIAL PRIMARY KEY,
vip_id VARCHAR(255) REFERENCES vips(id) ON DELETE CASCADE,
flight_number VARCHAR(50) NOT NULL,
flight_date DATE NOT NULL,
segment INTEGER NOT NULL,
departure_airport VARCHAR(10),
arrival_airport VARCHAR(10),
scheduled_departure TIMESTAMP,
scheduled_arrival TIMESTAMP,
actual_departure TIMESTAMP,
actual_arrival TIMESTAMP,
status VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Create drivers table
CREATE TABLE IF NOT EXISTS drivers (
id VARCHAR(255) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
phone VARCHAR(50) NOT NULL,
department VARCHAR(255) DEFAULT 'Office of Development',
user_id VARCHAR(255) REFERENCES users(id) ON DELETE SET NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Create schedule_events table
CREATE TABLE IF NOT EXISTS schedule_events (
id VARCHAR(255) PRIMARY KEY,
vip_id VARCHAR(255) REFERENCES vips(id) ON DELETE CASCADE,
title VARCHAR(255) NOT NULL,
location VARCHAR(255) NOT NULL,
start_time TIMESTAMP NOT NULL,
end_time TIMESTAMP NOT NULL,
description TEXT,
assigned_driver_id VARCHAR(255) REFERENCES drivers(id) ON DELETE SET NULL,
status VARCHAR(50) DEFAULT 'scheduled' CHECK (status IN ('scheduled', 'in-progress', 'completed', 'cancelled')),
event_type VARCHAR(50) NOT NULL CHECK (event_type IN ('transport', 'meeting', 'event', 'meal', 'accommodation')),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Create users table for authentication
CREATE TABLE IF NOT EXISTS users (
id VARCHAR(255) PRIMARY KEY,
google_id VARCHAR(255) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
name VARCHAR(255) NOT NULL,
role VARCHAR(50) NOT NULL CHECK (role IN ('driver', 'coordinator', 'administrator')),
profile_picture_url TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_login TIMESTAMP,
is_active BOOLEAN DEFAULT true,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Create system_setup table for tracking initial setup
CREATE TABLE IF NOT EXISTS system_setup (
id SERIAL PRIMARY KEY,
setup_completed BOOLEAN DEFAULT false,
first_admin_created BOOLEAN DEFAULT false,
setup_date TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Create admin_settings table
CREATE TABLE IF NOT EXISTS admin_settings (
id SERIAL PRIMARY KEY,
setting_key VARCHAR(255) UNIQUE NOT NULL,
setting_value TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Create indexes for better performance
CREATE INDEX IF NOT EXISTS idx_vips_transport_mode ON vips(transport_mode);
CREATE INDEX IF NOT EXISTS idx_flights_vip_id ON flights(vip_id);
CREATE INDEX IF NOT EXISTS idx_flights_date ON flights(flight_date);
CREATE INDEX IF NOT EXISTS idx_schedule_events_vip_id ON schedule_events(vip_id);
CREATE INDEX IF NOT EXISTS idx_schedule_events_driver_id ON schedule_events(assigned_driver_id);
CREATE INDEX IF NOT EXISTS idx_schedule_events_start_time ON schedule_events(start_time);
CREATE INDEX IF NOT EXISTS idx_schedule_events_status ON schedule_events(status);
CREATE INDEX IF NOT EXISTS idx_users_google_id ON users(google_id);
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
CREATE INDEX IF NOT EXISTS idx_users_role ON users(role);
CREATE INDEX IF NOT EXISTS idx_drivers_user_id ON drivers(user_id);
-- Create updated_at trigger function
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ language 'plpgsql';
-- Create triggers for updated_at (drop if exists first)
DROP TRIGGER IF EXISTS update_vips_updated_at ON vips;
DROP TRIGGER IF EXISTS update_flights_updated_at ON flights;
DROP TRIGGER IF EXISTS update_drivers_updated_at ON drivers;
DROP TRIGGER IF EXISTS update_schedule_events_updated_at ON schedule_events;
DROP TRIGGER IF EXISTS update_users_updated_at ON users;
DROP TRIGGER IF EXISTS update_admin_settings_updated_at ON admin_settings;
CREATE TRIGGER update_vips_updated_at BEFORE UPDATE ON vips FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_flights_updated_at BEFORE UPDATE ON flights FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_drivers_updated_at BEFORE UPDATE ON drivers FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_schedule_events_updated_at BEFORE UPDATE ON schedule_events FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_admin_settings_updated_at BEFORE UPDATE ON admin_settings FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();

View File

@@ -0,0 +1,178 @@
import jwt, { JwtHeader, JwtPayload } from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';
const auth0Domain = process.env.AUTH0_DOMAIN;
const auth0Audience = process.env.AUTH0_AUDIENCE;
if (!auth0Domain) {
console.warn('⚠️ AUTH0_DOMAIN is not set. Authentication routes will reject requests until configured.');
}
if (!auth0Audience) {
console.warn('⚠️ AUTH0_AUDIENCE is not set. Authentication routes will reject requests until configured.');
}
const jwks = auth0Domain
? jwksClient({
jwksUri: `https://${auth0Domain}/.well-known/jwks.json`,
cache: true,
cacheMaxEntries: 5,
cacheMaxAge: 10 * 60 * 1000
})
: null;
const PROFILE_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
const profileCache = new Map<string, { profile: Auth0UserProfile; expiresAt: number }>();
const inflightProfileRequests = new Map<string, Promise<Auth0UserProfile>>();
export interface Auth0UserProfile {
sub: string;
email?: string;
name?: string;
nickname?: string;
picture?: string;
[key: string]: unknown;
}
export interface VerifiedAccessToken extends JwtPayload {
sub: string;
azp?: string;
scope?: string;
}
async function getSigningKey(header: JwtHeader): Promise<string> {
if (!jwks) {
throw new Error('Auth0 JWKS client not initialised');
}
if (!header.kid) {
throw new Error('Token signing key id (kid) is missing');
}
const signingKey = await new Promise<jwksClient.SigningKey>((resolve, reject) => {
jwks.getSigningKey(header.kid as string, (err, key) => {
if (err) {
return reject(err);
}
if (!key) {
return reject(new Error('Signing key not found'));
}
resolve(key);
});
});
const publicKey =
typeof signingKey.getPublicKey === 'function'
? signingKey.getPublicKey()
: (signingKey as any).publicKey || (signingKey as any).rsaPublicKey;
if (!publicKey) {
throw new Error('Unable to derive signing key');
}
return publicKey;
}
export async function verifyAccessToken(token: string): Promise<VerifiedAccessToken> {
if (!auth0Domain || !auth0Audience) {
throw new Error('Auth0 configuration is incomplete');
}
const decoded = jwt.decode(token, { complete: true });
if (!decoded || typeof decoded === 'string') {
throw new Error('Invalid JWT');
}
const signingKey = await getSigningKey(decoded.header);
return jwt.verify(token, signingKey, {
algorithms: ['RS256'],
audience: auth0Audience,
issuer: `https://${auth0Domain}/`
}) as VerifiedAccessToken;
}
export async function fetchAuth0UserProfile(accessToken: string, cacheKey: string, expiresAt?: number): Promise<Auth0UserProfile> {
if (!auth0Domain) {
throw new Error('Auth0 configuration is incomplete');
}
const now = Date.now();
const cached = profileCache.get(cacheKey);
if (cached && cached.expiresAt > now) {
return cached.profile;
}
const ttl = expiresAt ? Math.max(0, expiresAt * 1000 - now) : PROFILE_CACHE_TTL_MS;
if (inflightProfileRequests.has(cacheKey)) {
return inflightProfileRequests.get(cacheKey)!;
}
const fetchPromise = (async () => {
const response = await fetch(`https://${auth0Domain}/userinfo`, {
headers: {
Authorization: `Bearer ${accessToken}`
}
});
if (!response.ok) {
throw new Error(`Failed to fetch Auth0 user profile (${response.status})`);
}
const profile = (await response.json()) as Auth0UserProfile;
profileCache.set(cacheKey, { profile, expiresAt: now + ttl });
inflightProfileRequests.delete(cacheKey);
return profile;
})().catch(error => {
inflightProfileRequests.delete(cacheKey);
throw error;
});
inflightProfileRequests.set(cacheKey, fetchPromise);
return fetchPromise;
}
export function clearAuth0ProfileCache(cacheKey?: string) {
if (cacheKey) {
profileCache.delete(cacheKey);
inflightProfileRequests.delete(cacheKey);
} else {
profileCache.clear();
inflightProfileRequests.clear();
}
}
export function getCachedProfile(cacheKey: string): Auth0UserProfile | undefined {
const cached = profileCache.get(cacheKey);
if (cached && cached.expiresAt > Date.now()) {
return cached.profile;
}
return undefined;
}
export function cacheAuth0Profile(cacheKey: string, profile: Auth0UserProfile, expiresAt?: number) {
const ttl = expiresAt ? Math.max(0, expiresAt * 1000 - Date.now()) : PROFILE_CACHE_TTL_MS;
profileCache.set(cacheKey, { profile, expiresAt: Date.now() + ttl });
}
export async function fetchFreshAuth0Profile(accessToken: string): Promise<Auth0UserProfile> {
if (!auth0Domain) {
throw new Error('Auth0 configuration is incomplete');
}
const response = await fetch(`https://${auth0Domain}/userinfo`, {
headers: {
Authorization: `Bearer ${accessToken}`
}
});
if (!response.ok) {
throw new Error(`Failed to fetch Auth0 user profile (${response.status})`);
}
return (await response.json()) as Auth0UserProfile;
}
export function isAuth0Configured(): boolean {
return Boolean(auth0Domain && auth0Audience);
}

740
backend/src/index.ts Normal file
View File

@@ -0,0 +1,740 @@
import express, { Express, Request, Response } from 'express';
import dotenv from 'dotenv';
import cors from 'cors';
import authRoutes, { requireAuth, requireRole } from './routes/simpleAuth';
import flightService from './services/flightService';
import driverConflictService from './services/driverConflictService';
import scheduleValidationService from './services/scheduleValidationService';
import FlightTrackingScheduler from './services/flightTrackingScheduler';
import enhancedDataService from './services/enhancedDataService';
import databaseService from './services/databaseService';
dotenv.config();
const app: Express = express();
const port: number = process.env.PORT ? parseInt(process.env.PORT) : 3000;
// Middleware
app.use(cors({
origin: [
process.env.FRONTEND_URL || 'http://localhost:5173',
'https://bsa.madeamess.online:5173',
'http://bsa.madeamess.online:5173'
],
credentials: true
}));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Simple JWT-based authentication - no passport needed
// Authentication routes
app.use('/auth', authRoutes);
// Temporary admin bypass route (remove after setup)
app.get('/admin-bypass', (req: Request, res: Response) => {
res.redirect(`${process.env.FRONTEND_URL || 'http://localhost:5173'}/admin?bypass=true`);
});
// Serve static files from public directory
app.use(express.static('public'));
// Health check endpoint
app.get('/api/health', (req: Request, res: Response) => {
res.json({ status: 'OK', timestamp: new Date().toISOString() });
});
// Data is now persisted using dataService - no more in-memory storage!
// Initialize flight tracking scheduler
const flightTracker = new FlightTrackingScheduler(flightService);
// VIP routes (protected)
app.post('/api/vips', requireAuth, requireRole(['coordinator', 'administrator']), async (req: Request, res: Response) => {
// Create a new VIP
const {
name,
organization,
department, // New: Office of Development or Admin
transportMode,
flightNumber, // Legacy single flight
flights, // New: array of flights
expectedArrival,
needsAirportPickup,
needsVenueTransport,
notes
} = req.body;
const newVip = {
id: Date.now().toString(), // Simple ID generation
name,
organization,
department: department || 'Office of Development', // Default to Office of Development
transportMode: transportMode || 'flight',
// Support both legacy single flight and new multiple flights
flightNumber: transportMode === 'flight' && !flights ? flightNumber : undefined,
flights: transportMode === 'flight' && flights ? flights : undefined,
expectedArrival: transportMode === 'self-driving' ? expectedArrival : undefined,
arrivalTime: transportMode === 'flight' ? undefined : expectedArrival, // Legacy field for flight arrivals
needsAirportPickup: transportMode === 'flight' ? (needsAirportPickup !== false) : false,
needsVenueTransport: needsVenueTransport !== false, // Default to true
assignedDriverIds: [],
notes: notes || '',
schedule: []
};
const savedVip = await enhancedDataService.addVip(newVip);
// Add flights to tracking scheduler if applicable
if (savedVip.transportMode === 'flight' && savedVip.flights && savedVip.flights.length > 0) {
flightTracker.addVipFlights(savedVip.id, savedVip.name, savedVip.flights);
}
res.status(201).json(savedVip);
});
app.get('/api/vips', requireAuth, async (req: Request, res: Response) => {
try {
// Fetch all VIPs
const vips = await enhancedDataService.getVips();
res.json(vips);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch VIPs' });
}
});
app.put('/api/vips/:id', requireAuth, requireRole(['coordinator', 'administrator']), async (req: Request, res: Response) => {
// Update a VIP
const { id } = req.params;
const {
name,
organization,
department, // New: Office of Development or Admin
transportMode,
flightNumber, // Legacy single flight
flights, // New: array of flights
expectedArrival,
needsAirportPickup,
needsVenueTransport,
notes
} = req.body;
try {
const updatedVip = {
name,
organization,
department: department || 'Office of Development',
transportMode: transportMode || 'flight',
// Support both legacy single flight and new multiple flights
flights: transportMode === 'flight' && flights ? flights : undefined,
expectedArrival: transportMode === 'self-driving' ? expectedArrival : undefined,
needsAirportPickup: transportMode === 'flight' ? (needsAirportPickup !== false) : false,
needsVenueTransport: needsVenueTransport !== false,
notes: notes || ''
};
const savedVip = await enhancedDataService.updateVip(id, updatedVip);
if (!savedVip) {
return res.status(404).json({ error: 'VIP not found' });
}
// Update flight tracking if needed
if (savedVip.transportMode === 'flight') {
// Remove old flights
flightTracker.removeVipFlights(id);
// Add new flights if any
if (savedVip.flights && savedVip.flights.length > 0) {
flightTracker.addVipFlights(savedVip.id, savedVip.name, savedVip.flights);
}
}
res.json(savedVip);
} catch (error) {
res.status(500).json({ error: 'Failed to update VIP' });
}
});
app.delete('/api/vips/:id', requireAuth, requireRole(['coordinator', 'administrator']), async (req: Request, res: Response) => {
// Delete a VIP
const { id } = req.params;
try {
const deletedVip = await enhancedDataService.deleteVip(id);
if (!deletedVip) {
return res.status(404).json({ error: 'VIP not found' });
}
// Remove from flight tracking
flightTracker.removeVipFlights(id);
res.json({ message: 'VIP deleted successfully', vip: deletedVip });
} catch (error) {
res.status(500).json({ error: 'Failed to delete VIP' });
}
});
// Driver routes (protected)
app.post('/api/drivers', requireAuth, requireRole(['coordinator', 'administrator']), async (req: Request, res: Response) => {
// Create a new driver
const { name, phone, currentLocation, department } = req.body;
const newDriver = {
id: Date.now().toString(),
name,
phone,
department: department || 'Office of Development', // Default to Office of Development
currentLocation: currentLocation || { lat: 0, lng: 0 },
assignedVipIds: []
};
try {
const savedDriver = await enhancedDataService.addDriver(newDriver);
res.status(201).json(savedDriver);
} catch (error) {
res.status(500).json({ error: 'Failed to create driver' });
}
});
app.get('/api/drivers', requireAuth, async (req: Request, res: Response) => {
try {
// Fetch all drivers
const drivers = await enhancedDataService.getDrivers();
res.json(drivers);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch drivers' });
}
});
app.put('/api/drivers/:id', requireAuth, requireRole(['coordinator', 'administrator']), async (req: Request, res: Response) => {
// Update a driver
const { id } = req.params;
const { name, phone, currentLocation, department } = req.body;
try {
const updatedDriver = {
name,
phone,
department: department || 'Office of Development',
currentLocation: currentLocation || { lat: 0, lng: 0 }
};
const savedDriver = await enhancedDataService.updateDriver(id, updatedDriver);
if (!savedDriver) {
return res.status(404).json({ error: 'Driver not found' });
}
res.json(savedDriver);
} catch (error) {
res.status(500).json({ error: 'Failed to update driver' });
}
});
app.delete('/api/drivers/:id', requireAuth, requireRole(['coordinator', 'administrator']), async (req: Request, res: Response) => {
// Delete a driver
const { id } = req.params;
try {
const deletedDriver = await enhancedDataService.deleteDriver(id);
if (!deletedDriver) {
return res.status(404).json({ error: 'Driver not found' });
}
res.json({ message: 'Driver deleted successfully', driver: deletedDriver });
} catch (error) {
res.status(500).json({ error: 'Failed to delete driver' });
}
});
// Enhanced flight tracking routes with date specificity
app.get('/api/flights/:flightNumber', async (req: Request, res: Response) => {
try {
const { flightNumber } = req.params;
const { date, departureAirport, arrivalAirport } = req.query;
// Default to today if no date provided
const flightDate = (date as string) || new Date().toISOString().split('T')[0];
const flightData = await flightService.getFlightInfo({
flightNumber,
date: flightDate,
departureAirport: departureAirport as string,
arrivalAirport: arrivalAirport as string
});
if (flightData) {
// Always return flight data for validation, even if date doesn't match
res.json(flightData);
} else {
// Only return 404 if the flight number itself is invalid
res.status(404).json({ error: 'Invalid flight number - this flight does not exist' });
}
} catch (error) {
res.status(500).json({ error: 'Failed to fetch flight data' });
}
});
// Start periodic updates for a flight
app.post('/api/flights/:flightNumber/track', async (req: Request, res: Response) => {
try {
const { flightNumber } = req.params;
const { date, intervalMinutes = 5 } = req.body;
if (!date) {
return res.status(400).json({ error: 'Flight date is required' });
}
flightService.startPeriodicUpdates({
flightNumber,
date
}, intervalMinutes);
res.json({ message: `Started tracking ${flightNumber} on ${date}` });
} catch (error) {
res.status(500).json({ error: 'Failed to start flight tracking' });
}
});
// Stop periodic updates for a flight
app.delete('/api/flights/:flightNumber/track', async (req: Request, res: Response) => {
try {
const { flightNumber } = req.params;
const { date } = req.query;
if (!date) {
return res.status(400).json({ error: 'Flight date is required' });
}
const key = `${flightNumber}_${date}`;
flightService.stopPeriodicUpdates(key);
res.json({ message: `Stopped tracking ${flightNumber} on ${date}` });
} catch (error) {
res.status(500).json({ error: 'Failed to stop flight tracking' });
}
});
app.post('/api/flights/batch', async (req: Request, res: Response) => {
try {
const { flights } = req.body;
if (!Array.isArray(flights)) {
return res.status(400).json({ error: 'flights must be an array of {flightNumber, date} objects' });
}
// Validate flight objects
for (const flight of flights) {
if (!flight.flightNumber || !flight.date) {
return res.status(400).json({ error: 'Each flight must have flightNumber and date' });
}
}
const flightData = await flightService.getMultipleFlights(flights);
res.json(flightData);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch flight data' });
}
});
// Get flight tracking status
app.get('/api/flights/tracking/status', (req: Request, res: Response) => {
const status = flightTracker.getTrackingStatus();
res.json(status);
});
// Schedule management routes (protected)
app.get('/api/vips/:vipId/schedule', requireAuth, async (req: Request, res: Response) => {
const { vipId } = req.params;
try {
const vipSchedule = await enhancedDataService.getSchedule(vipId);
res.json(vipSchedule);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch schedule' });
}
});
app.post('/api/vips/:vipId/schedule', requireAuth, requireRole(['coordinator', 'administrator']), async (req: Request, res: Response) => {
const { vipId } = req.params;
const { title, location, startTime, endTime, description, type, assignedDriverId } = req.body;
// Validate the event
const validationErrors = scheduleValidationService.validateEvent({
title: title || '',
location: location || '',
startTime: startTime || '',
endTime: endTime || '',
type: type || ''
}, false);
const { critical, warnings } = scheduleValidationService.categorizeErrors(validationErrors);
// Return validation errors if any critical errors exist
if (critical.length > 0) {
return res.status(400).json({
error: 'Validation failed',
validationErrors: critical,
warnings: warnings,
message: scheduleValidationService.getErrorSummary(critical)
});
}
const newEvent = {
id: Date.now().toString(),
title,
location,
startTime,
endTime,
description: description || '',
assignedDriverId: assignedDriverId || '',
status: 'scheduled',
type
};
try {
const savedEvent = await enhancedDataService.addScheduleEvent(vipId, newEvent);
// Include warnings in the response if any
const response: any = { ...savedEvent };
if (warnings.length > 0) {
response.warnings = warnings;
}
res.status(201).json(response);
} catch (error) {
res.status(500).json({ error: 'Failed to create schedule event' });
}
});
app.put('/api/vips/:vipId/schedule/:eventId', requireAuth, requireRole(['coordinator', 'administrator']), async (req: Request, res: Response) => {
const { vipId, eventId } = req.params;
const { title, location, startTime, endTime, description, type, assignedDriverId, status } = req.body;
// Validate the updated event (with edit flag for grace period)
const validationErrors = scheduleValidationService.validateEvent({
title: title || '',
location: location || '',
startTime: startTime || '',
endTime: endTime || '',
type: type || ''
}, true);
const { critical, warnings } = scheduleValidationService.categorizeErrors(validationErrors);
// Return validation errors if any critical errors exist
if (critical.length > 0) {
return res.status(400).json({
error: 'Validation failed',
validationErrors: critical,
warnings: warnings,
message: scheduleValidationService.getErrorSummary(critical)
});
}
const updatedEvent = {
id: eventId,
title,
location,
startTime,
endTime,
description: description || '',
assignedDriverId: assignedDriverId || '',
type,
status: status || 'scheduled'
};
try {
const savedEvent = await enhancedDataService.updateScheduleEvent(vipId, eventId, updatedEvent);
if (!savedEvent) {
return res.status(404).json({ error: 'Event not found' });
}
// Include warnings in the response if any
const response: any = { ...savedEvent };
if (warnings.length > 0) {
response.warnings = warnings;
}
res.json(response);
} catch (error) {
res.status(500).json({ error: 'Failed to update schedule event' });
}
});
app.patch('/api/vips/:vipId/schedule/:eventId/status', requireAuth, async (req: Request, res: Response) => {
const { vipId, eventId } = req.params;
const { status } = req.body;
try {
const currentSchedule = await enhancedDataService.getSchedule(vipId);
const currentEvent = currentSchedule.find((event: any) => event.id === eventId);
if (!currentEvent) {
return res.status(404).json({ error: 'Event not found' });
}
const updatedEvent = { ...currentEvent, status };
const savedEvent = await enhancedDataService.updateScheduleEvent(vipId, eventId, updatedEvent);
if (!savedEvent) {
return res.status(404).json({ error: 'Event not found' });
}
res.json(savedEvent);
} catch (error) {
res.status(500).json({ error: 'Failed to update event status' });
}
});
app.delete('/api/vips/:vipId/schedule/:eventId', requireAuth, requireRole(['coordinator', 'administrator']), async (req: Request, res: Response) => {
const { vipId, eventId } = req.params;
try {
const deletedEvent = await enhancedDataService.deleteScheduleEvent(vipId, eventId);
if (!deletedEvent) {
return res.status(404).json({ error: 'Event not found' });
}
res.json({ message: 'Event deleted successfully', event: deletedEvent });
} catch (error) {
res.status(500).json({ error: 'Failed to delete schedule event' });
}
});
// Driver availability and conflict checking (protected)
app.post('/api/drivers/availability', requireAuth, async (req: Request, res: Response) => {
const { startTime, endTime, location } = req.body;
if (!startTime || !endTime) {
return res.status(400).json({ error: 'startTime and endTime are required' });
}
try {
const allSchedules = await enhancedDataService.getAllSchedules();
const drivers = await enhancedDataService.getDrivers();
const availability = driverConflictService.getDriverAvailability(
{ startTime, endTime, location: location || '' },
allSchedules as any,
drivers
);
res.json(availability);
} catch (error) {
res.status(500).json({ error: 'Failed to check driver availability' });
}
});
// Check conflicts for specific driver assignment (protected)
app.post('/api/drivers/:driverId/conflicts', requireAuth, async (req: Request, res: Response) => {
const { driverId } = req.params;
const { startTime, endTime, location } = req.body;
if (!startTime || !endTime) {
return res.status(400).json({ error: 'startTime and endTime are required' });
}
try {
const allSchedules = await enhancedDataService.getAllSchedules();
const drivers = await enhancedDataService.getDrivers();
const conflicts = driverConflictService.checkDriverConflicts(
driverId,
{ startTime, endTime, location: location || '' },
allSchedules as any,
drivers
);
res.json({ conflicts });
} catch (error) {
res.status(500).json({ error: 'Failed to check driver conflicts' });
}
});
// Get driver's complete schedule (protected)
app.get('/api/drivers/:driverId/schedule', requireAuth, async (req: Request, res: Response) => {
const { driverId } = req.params;
try {
const drivers = await enhancedDataService.getDrivers();
const driver = drivers.find((d: any) => d.id === driverId);
if (!driver) {
return res.status(404).json({ error: 'Driver not found' });
}
// Get all events assigned to this driver across all VIPs
const driverSchedule: any[] = [];
const allSchedules = await enhancedDataService.getAllSchedules();
const vips = await enhancedDataService.getVips();
Object.entries(allSchedules).forEach(([vipId, events]: [string, any]) => {
events.forEach((event: any) => {
if (event.assignedDriverId === driverId) {
// Get VIP name
const vip = vips.find((v: any) => v.id === vipId);
driverSchedule.push({
...event,
vipId,
vipName: vip ? vip.name : 'Unknown VIP'
});
}
});
});
// Sort by start time
driverSchedule.sort((a, b) =>
new Date(a.startTime).getTime() - new Date(b.startTime).getTime()
);
res.json({
driver: {
id: driver.id,
name: driver.name,
phone: driver.phone,
department: driver.department
},
schedule: driverSchedule
});
} catch (error) {
res.status(500).json({ error: 'Failed to fetch driver schedule' });
}
});
// Admin routes
app.get('/api/admin/settings', requireAuth, requireRole(['administrator']), async (req: Request, res: Response) => {
try {
const adminSettings = await enhancedDataService.getAdminSettings();
const apiKeys = adminSettings.apiKeys || {};
// Return settings but mask API keys for display only
// IMPORTANT: Don't return the actual keys, just indicate they exist
const maskedSettings = {
apiKeys: {
aviationStackKey: apiKeys.aviationStackKey ? '***' + apiKeys.aviationStackKey.slice(-4) : '',
googleMapsKey: apiKeys.googleMapsKey ? '***' + apiKeys.googleMapsKey.slice(-4) : '',
twilioKey: apiKeys.twilioKey ? '***' + apiKeys.twilioKey.slice(-4) : '',
auth0Domain: apiKeys.auth0Domain ? '***' + apiKeys.auth0Domain.slice(-4) : '',
auth0ClientId: apiKeys.auth0ClientId ? '***' + apiKeys.auth0ClientId.slice(-4) : '',
auth0ClientSecret: apiKeys.auth0ClientSecret ? '***' + apiKeys.auth0ClientSecret.slice(-4) : ''
},
systemSettings: adminSettings.systemSettings
};
res.json(maskedSettings);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch admin settings' });
}
});
app.post('/api/admin/settings', requireAuth, requireRole(['administrator']), async (req: Request, res: Response) => {
try {
const { apiKeys, systemSettings } = req.body;
const currentSettings = await enhancedDataService.getAdminSettings();
currentSettings.apiKeys = currentSettings.apiKeys || {};
// Update API keys (only if provided and not masked)
if (apiKeys) {
if (apiKeys.aviationStackKey && !apiKeys.aviationStackKey.startsWith('***')) {
currentSettings.apiKeys.aviationStackKey = apiKeys.aviationStackKey;
// Update the environment variable for the flight service
process.env.AVIATIONSTACK_API_KEY = apiKeys.aviationStackKey;
}
if (apiKeys.googleMapsKey && !apiKeys.googleMapsKey.startsWith('***')) {
currentSettings.apiKeys.googleMapsKey = apiKeys.googleMapsKey;
}
if (apiKeys.twilioKey && !apiKeys.twilioKey.startsWith('***')) {
currentSettings.apiKeys.twilioKey = apiKeys.twilioKey;
}
if (apiKeys.auth0Domain && !apiKeys.auth0Domain.startsWith('***')) {
currentSettings.apiKeys.auth0Domain = apiKeys.auth0Domain;
process.env.AUTH0_DOMAIN = apiKeys.auth0Domain;
}
if (apiKeys.auth0ClientId && !apiKeys.auth0ClientId.startsWith('***')) {
currentSettings.apiKeys.auth0ClientId = apiKeys.auth0ClientId;
process.env.AUTH0_CLIENT_ID = apiKeys.auth0ClientId;
}
if (apiKeys.auth0ClientSecret && !apiKeys.auth0ClientSecret.startsWith('***')) {
currentSettings.apiKeys.auth0ClientSecret = apiKeys.auth0ClientSecret;
process.env.AUTH0_CLIENT_SECRET = apiKeys.auth0ClientSecret;
}
}
// Update system settings
if (systemSettings) {
currentSettings.systemSettings = { ...currentSettings.systemSettings, ...systemSettings };
}
// Save the updated settings
await enhancedDataService.updateAdminSettings(currentSettings);
res.json({ success: true });
} catch (error) {
res.status(500).json({ error: 'Failed to update admin settings' });
}
});
app.post('/api/admin/test-api/:apiType', requireAuth, requireRole(['administrator']), async (req: Request, res: Response) => {
const { apiType } = req.params;
const { apiKey } = req.body;
try {
switch (apiType) {
case 'aviationStackKey':
// Test AviationStack API
const testUrl = `http://api.aviationstack.com/v1/flights?access_key=${apiKey}&limit=1`;
const response = await fetch(testUrl);
if (response.ok) {
const data: any = await response.json();
if (data.error) {
res.status(400).json({ error: data.error.message || 'Invalid API key' });
} else {
res.json({ success: true, message: 'API key is valid!' });
}
} else {
res.status(400).json({ error: 'Failed to validate API key' });
}
break;
case 'googleMapsKey':
res.json({ success: true, message: 'Google Maps API testing not yet implemented' });
break;
case 'twilioKey':
res.json({ success: true, message: 'Twilio API testing not yet implemented' });
break;
default:
res.status(400).json({ error: 'Unknown API type' });
}
} catch (error) {
res.status(500).json({ error: 'Failed to test API connection' });
}
});
// Initialize database and start server
async function startServer() {
try {
// Initialize database schema and migrate data
await databaseService.initializeDatabase();
console.log('✅ Database initialization completed');
// Start the server
app.listen(port, () => {
console.log(`🚀 Server is running on port ${port}`);
console.log(`📊 Admin dashboard: http://localhost:${port === 3000 ? 5173 : port}/admin`);
console.log(`🏥 Health check: http://localhost:${port}/api/health`);
console.log(`📚 API docs: http://localhost:${port}/api-docs.html`);
});
} catch (error) {
console.error('❌ Failed to start server:', error);
process.exit(1);
}
}
startServer();

View File

@@ -0,0 +1,324 @@
import express, { Request, Response, NextFunction } from 'express';
import {
fetchAuth0UserProfile,
isAuth0Configured,
verifyAccessToken,
VerifiedAccessToken,
Auth0UserProfile,
getCachedProfile,
cacheAuth0Profile
} from '../config/simpleAuth';
import databaseService from '../services/databaseService';
type AuthedRequest = Request & {
auth?: {
token: string;
claims: VerifiedAccessToken;
profile?: Auth0UserProfile | null;
};
user?: any;
};
const router = express.Router();
function mapUserForResponse(user: any) {
return {
id: user.id,
email: user.email,
name: user.name,
picture: user.profile_picture_url,
role: user.role,
approval_status: user.approval_status,
created_at: user.created_at,
last_login: user.last_login,
provider: 'auth0'
};
}
async function syncUserWithDatabase(claims: VerifiedAccessToken, token: string): Promise<{ user: any; profile: Auth0UserProfile | null }> {
const auth0Id = claims.sub;
const initialAdminEmails = (process.env.INITIAL_ADMIN_EMAILS || '')
.split(',')
.map(email => email.trim().toLowerCase())
.filter(Boolean);
let profile: Auth0UserProfile | null = null;
let user = await databaseService.getUserById(auth0Id);
if (user) {
const updated = await databaseService.updateUserLastSignIn(user.email);
user = updated || user;
const isSeedAdmin = initialAdminEmails.includes((user.email || '').toLowerCase());
if (isSeedAdmin && user.role !== 'administrator') {
user = await databaseService.updateUserRole(user.email, 'administrator');
}
if (isSeedAdmin && user.approval_status !== 'approved') {
user = await databaseService.updateUserApprovalStatus(user.email, 'approved');
}
return { user, profile };
}
const cacheKey = auth0Id;
profile = getCachedProfile(cacheKey) || null;
if (!profile) {
profile = await fetchAuth0UserProfile(token, cacheKey, claims.exp);
cacheAuth0Profile(cacheKey, profile, claims.exp);
}
if (!profile.email) {
throw new Error('Auth0 profile did not include an email address');
}
const existingByEmail = await databaseService.getUserByEmail(profile.email);
if (existingByEmail && existingByEmail.id !== auth0Id) {
await databaseService.migrateUserId(existingByEmail.id, auth0Id);
user = await databaseService.getUserById(auth0Id);
} else if (existingByEmail) {
user = existingByEmail;
}
const displayName = profile.name || profile.nickname || profile.email;
const picture = typeof profile.picture === 'string' ? profile.picture : undefined;
const isSeedAdmin = initialAdminEmails.includes(profile.email.toLowerCase());
if (!user) {
const approvedUserCount = await databaseService.getApprovedUserCount();
const role = isSeedAdmin
? 'administrator'
: approvedUserCount === 0
? 'administrator'
: 'coordinator';
user = await databaseService.createUser({
id: auth0Id,
google_id: auth0Id,
email: profile.email,
name: displayName,
profile_picture_url: picture,
role
});
if (role === 'administrator') {
user = await databaseService.updateUserApprovalStatus(profile.email, 'approved');
}
} else {
const updated = await databaseService.updateUserLastSignIn(user.email);
user = updated || user;
if (isSeedAdmin && user.role !== 'administrator') {
user = await databaseService.updateUserRole(user.email, 'administrator');
}
if (isSeedAdmin && user.approval_status !== 'approved') {
user = await databaseService.updateUserApprovalStatus(user.email, 'approved');
}
}
return { user, profile };
}
export async function requireAuth(req: AuthedRequest, res: Response, next: NextFunction) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
const token = authHeader.substring(7);
try {
const claims = await verifyAccessToken(token);
const { user, profile } = await syncUserWithDatabase(claims, token);
req.auth = { token, claims, profile };
req.user = user;
if (user.approval_status !== 'approved') {
return res.status(403).json({
error: 'pending_approval',
message: 'Your account is pending administrator approval.',
user: mapUserForResponse(user)
});
}
return next();
} catch (error: any) {
console.error('Auth0 token verification failed:', error);
return res.status(401).json({ error: 'Invalid or expired token' });
}
}
export function requireRole(roles: string[]) {
return (req: AuthedRequest, res: Response, next: NextFunction) => {
const user = req.user;
if (!user || !roles.includes(user.role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
router.get('/setup', async (_req: Request, res: Response) => {
try {
const userCount = await databaseService.getUserCount();
res.json({
setupCompleted: isAuth0Configured(),
firstAdminCreated: userCount > 0,
oauthConfigured: isAuth0Configured(),
authProvider: 'auth0'
});
} catch (error) {
console.error('Error checking setup status:', error);
res.status(500).json({ error: 'Database connection error' });
}
});
router.get('/me', requireAuth, async (req: AuthedRequest, res: Response) => {
res.json({
user: mapUserForResponse(req.user),
auth0: {
sub: req.auth?.claims.sub,
scope: req.auth?.claims.scope
}
});
});
router.post('/logout', (_req: Request, res: Response) => {
res.json({ message: 'Logged out successfully' });
});
router.get('/status', requireAuth, (req: AuthedRequest, res: Response) => {
res.json({
authenticated: true,
user: mapUserForResponse(req.user)
});
});
// USER MANAGEMENT ENDPOINTS
// List all users (admin only)
router.get('/users', requireAuth, requireRole(['administrator']), async (_req: Request, res: Response) => {
try {
const users = await databaseService.getAllUsers();
res.json(users.map(mapUserForResponse));
} catch (error) {
console.error('Error fetching users:', error);
res.status(500).json({ error: 'Failed to fetch users' });
}
});
// Update user role (admin only)
router.patch('/users/:email/role', requireAuth, requireRole(['administrator']), async (req: Request, res: Response) => {
const { email } = req.params;
const { role } = req.body;
if (!['administrator', 'coordinator', 'driver'].includes(role)) {
return res.status(400).json({ error: 'Invalid role' });
}
try {
const user = await databaseService.updateUserRole(email, role);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json({
success: true,
user: mapUserForResponse(user)
});
} catch (error) {
console.error('Error updating user role:', error);
res.status(500).json({ error: 'Failed to update user role' });
}
});
// Delete user (admin only)
router.delete('/users/:email', requireAuth, requireRole(['administrator']), async (req: AuthedRequest, res: Response) => {
const { email } = req.params;
const currentUser = req.user;
// Prevent admin from deleting themselves
if (email === currentUser.email) {
return res.status(400).json({ error: 'Cannot delete your own account' });
}
try {
const deletedUser = await databaseService.deleteUser(email);
if (!deletedUser) {
return res.status(404).json({ error: 'User not found' });
}
res.json({ success: true, message: 'User deleted successfully' });
} catch (error) {
console.error('Error deleting user:', error);
res.status(500).json({ error: 'Failed to delete user' });
}
});
// Get user by email (admin only)
router.get('/users/:email', requireAuth, requireRole(['administrator']), async (req: Request, res: Response) => {
const { email } = req.params;
try {
const user = await databaseService.getUserByEmail(email);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(mapUserForResponse(user));
} catch (error) {
console.error('Error fetching user:', error);
res.status(500).json({ error: 'Failed to fetch user' });
}
});
// USER APPROVAL ENDPOINTS
// Get pending users (admin only)
router.get('/users/pending/list', requireAuth, requireRole(['administrator']), async (req: Request, res: Response) => {
try {
const pendingUsers = await databaseService.getPendingUsers();
const userList = pendingUsers.map(mapUserForResponse);
res.json(userList);
} catch (error) {
console.error('Error fetching pending users:', error);
res.status(500).json({ error: 'Failed to fetch pending users' });
}
});
// Approve or deny user (admin only)
router.patch('/users/:email/approval', requireAuth, requireRole(['administrator']), async (req: Request, res: Response) => {
const { email } = req.params;
const { status } = req.body;
if (!['approved', 'denied'].includes(status)) {
return res.status(400).json({ error: 'Invalid approval status. Must be "approved" or "denied"' });
}
try {
const user = await databaseService.updateUserApprovalStatus(email, status);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json({
success: true,
message: `User ${status} successfully`,
user: mapUserForResponse(user)
});
} catch (error) {
console.error('Error updating user approval:', error);
res.status(500).json({ error: 'Failed to update user approval' });
}
});
export default router;

View File

@@ -0,0 +1,306 @@
import fs from 'fs';
import path from 'path';
interface DataStore {
vips: any[];
drivers: any[];
schedules: { [vipId: string]: any[] };
adminSettings: any;
users: any[];
}
class DataService {
private dataDir: string;
private dataFile: string;
private data: DataStore;
constructor() {
this.dataDir = path.join(process.cwd(), 'data');
this.dataFile = path.join(this.dataDir, 'vip-coordinator.json');
// Ensure data directory exists
if (!fs.existsSync(this.dataDir)) {
fs.mkdirSync(this.dataDir, { recursive: true });
}
this.data = this.loadData();
}
private loadData(): DataStore {
try {
if (fs.existsSync(this.dataFile)) {
const fileContent = fs.readFileSync(this.dataFile, 'utf8');
const loadedData = JSON.parse(fileContent);
console.log(`✅ Loaded data from ${this.dataFile}`);
console.log(` - VIPs: ${loadedData.vips?.length || 0}`);
console.log(` - Drivers: ${loadedData.drivers?.length || 0}`);
console.log(` - Users: ${loadedData.users?.length || 0}`);
console.log(` - Schedules: ${Object.keys(loadedData.schedules || {}).length} VIPs with schedules`);
// Ensure users array exists for backward compatibility
if (!loadedData.users) {
loadedData.users = [];
}
return loadedData;
}
} catch (error) {
console.error('Error loading data file:', error);
}
// Return default empty data structure
console.log('📝 Starting with empty data store');
return {
vips: [],
drivers: [],
schedules: {},
users: [],
adminSettings: {
apiKeys: {
aviationStackKey: process.env.AVIATIONSTACK_API_KEY || '',
googleMapsKey: '',
twilioKey: ''
},
systemSettings: {
defaultPickupLocation: '',
defaultDropoffLocation: '',
timeZone: 'America/New_York',
notificationsEnabled: false
}
}
};
}
private saveData(): void {
try {
const dataToSave = JSON.stringify(this.data, null, 2);
fs.writeFileSync(this.dataFile, dataToSave, 'utf8');
console.log(`💾 Data saved to ${this.dataFile}`);
} catch (error) {
console.error('Error saving data file:', error);
}
}
// VIP operations
getVips(): any[] {
return this.data.vips;
}
addVip(vip: any): any {
this.data.vips.push(vip);
this.saveData();
return vip;
}
updateVip(id: string, updatedVip: any): any | null {
const index = this.data.vips.findIndex(vip => vip.id === id);
if (index !== -1) {
this.data.vips[index] = updatedVip;
this.saveData();
return this.data.vips[index];
}
return null;
}
deleteVip(id: string): any | null {
const index = this.data.vips.findIndex(vip => vip.id === id);
if (index !== -1) {
const deletedVip = this.data.vips.splice(index, 1)[0];
// Also delete the VIP's schedule
delete this.data.schedules[id];
this.saveData();
return deletedVip;
}
return null;
}
// Driver operations
getDrivers(): any[] {
return this.data.drivers;
}
addDriver(driver: any): any {
this.data.drivers.push(driver);
this.saveData();
return driver;
}
updateDriver(id: string, updatedDriver: any): any | null {
const index = this.data.drivers.findIndex(driver => driver.id === id);
if (index !== -1) {
this.data.drivers[index] = updatedDriver;
this.saveData();
return this.data.drivers[index];
}
return null;
}
deleteDriver(id: string): any | null {
const index = this.data.drivers.findIndex(driver => driver.id === id);
if (index !== -1) {
const deletedDriver = this.data.drivers.splice(index, 1)[0];
this.saveData();
return deletedDriver;
}
return null;
}
// Schedule operations
getSchedule(vipId: string): any[] {
return this.data.schedules[vipId] || [];
}
addScheduleEvent(vipId: string, event: any): any {
if (!this.data.schedules[vipId]) {
this.data.schedules[vipId] = [];
}
this.data.schedules[vipId].push(event);
this.saveData();
return event;
}
updateScheduleEvent(vipId: string, eventId: string, updatedEvent: any): any | null {
if (!this.data.schedules[vipId]) {
return null;
}
const index = this.data.schedules[vipId].findIndex(event => event.id === eventId);
if (index !== -1) {
this.data.schedules[vipId][index] = updatedEvent;
this.saveData();
return this.data.schedules[vipId][index];
}
return null;
}
deleteScheduleEvent(vipId: string, eventId: string): any | null {
if (!this.data.schedules[vipId]) {
return null;
}
const index = this.data.schedules[vipId].findIndex(event => event.id === eventId);
if (index !== -1) {
const deletedEvent = this.data.schedules[vipId].splice(index, 1)[0];
this.saveData();
return deletedEvent;
}
return null;
}
getAllSchedules(): { [vipId: string]: any[] } {
return this.data.schedules;
}
// Admin settings operations
getAdminSettings(): any {
return this.data.adminSettings;
}
updateAdminSettings(settings: any): void {
this.data.adminSettings = { ...this.data.adminSettings, ...settings };
this.saveData();
}
// Backup and restore operations
createBackup(): string {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupFile = path.join(this.dataDir, `backup-${timestamp}.json`);
try {
fs.copyFileSync(this.dataFile, backupFile);
console.log(`📦 Backup created: ${backupFile}`);
return backupFile;
} catch (error) {
console.error('Error creating backup:', error);
throw error;
}
}
// User operations
getUsers(): any[] {
return this.data.users;
}
getUserByEmail(email: string): any | null {
return this.data.users.find(user => user.email === email) || null;
}
getUserById(id: string): any | null {
return this.data.users.find(user => user.id === id) || null;
}
addUser(user: any): any {
// Add timestamps
const userWithTimestamps = {
...user,
created_at: new Date().toISOString(),
last_sign_in_at: new Date().toISOString()
};
this.data.users.push(userWithTimestamps);
this.saveData();
console.log(`👤 Added user: ${user.name} (${user.email}) as ${user.role}`);
return userWithTimestamps;
}
updateUser(email: string, updatedUser: any): any | null {
const index = this.data.users.findIndex(user => user.email === email);
if (index !== -1) {
this.data.users[index] = { ...this.data.users[index], ...updatedUser };
this.saveData();
console.log(`👤 Updated user: ${this.data.users[index].name} (${email})`);
return this.data.users[index];
}
return null;
}
updateUserRole(email: string, role: string): any | null {
const index = this.data.users.findIndex(user => user.email === email);
if (index !== -1) {
this.data.users[index].role = role;
this.saveData();
console.log(`👤 Updated user role: ${this.data.users[index].name} (${email}) -> ${role}`);
return this.data.users[index];
}
return null;
}
updateUserLastSignIn(email: string): any | null {
const index = this.data.users.findIndex(user => user.email === email);
if (index !== -1) {
this.data.users[index].last_sign_in_at = new Date().toISOString();
this.saveData();
return this.data.users[index];
}
return null;
}
deleteUser(email: string): any | null {
const index = this.data.users.findIndex(user => user.email === email);
if (index !== -1) {
const deletedUser = this.data.users.splice(index, 1)[0];
this.saveData();
console.log(`👤 Deleted user: ${deletedUser.name} (${email})`);
return deletedUser;
}
return null;
}
getUserCount(): number {
return this.data.users.length;
}
getDataStats(): any {
return {
vips: this.data.vips.length,
drivers: this.data.drivers.length,
users: this.data.users.length,
scheduledEvents: Object.values(this.data.schedules).reduce((total, events) => total + events.length, 0),
vipsWithSchedules: Object.keys(this.data.schedules).length,
dataFile: this.dataFile,
lastModified: fs.existsSync(this.dataFile) ? fs.statSync(this.dataFile).mtime : null
};
}
}
export default new DataService();

View File

@@ -0,0 +1,514 @@
import { Pool, PoolClient } from 'pg';
import { createClient, RedisClientType } from 'redis';
class DatabaseService {
private pool: Pool;
private redis: RedisClientType;
constructor() {
const useSSL = process.env.DATABASE_SSL === 'true';
this.pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: useSSL ? { rejectUnauthorized: false } : false
});
// Initialize Redis connection
this.redis = createClient({
socket: {
host: process.env.REDIS_HOST || 'redis',
port: parseInt(process.env.REDIS_PORT || '6379')
}
});
this.redis.on('error', (err) => {
console.error('❌ Redis connection error:', err);
});
// Test connections on startup
this.testConnection();
this.testRedisConnection();
}
private async testConnection(): Promise<void> {
try {
const client = await this.pool.connect();
console.log('✅ Connected to PostgreSQL database');
client.release();
} catch (error) {
console.error('❌ Failed to connect to PostgreSQL database:', error);
}
}
private async testRedisConnection(): Promise<void> {
try {
if (!this.redis.isOpen) {
await this.redis.connect();
}
await this.redis.ping();
console.log('✅ Connected to Redis');
} catch (error) {
console.error('❌ Failed to connect to Redis:', error);
}
}
async query(text: string, params?: any[]): Promise<any> {
const client = await this.pool.connect();
try {
const result = await client.query(text, params);
return result;
} finally {
client.release();
}
}
async getClient(): Promise<PoolClient> {
return await this.pool.connect();
}
async close(): Promise<void> {
await this.pool.end();
if (this.redis.isOpen) {
await this.redis.disconnect();
}
}
// Initialize database tables
async initializeTables(): Promise<void> {
try {
// Create users table (matching the actual schema)
await this.query(`
CREATE TABLE IF NOT EXISTS users (
id VARCHAR(255) PRIMARY KEY,
google_id VARCHAR(255) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
name VARCHAR(255) NOT NULL,
role VARCHAR(50) NOT NULL CHECK (role IN ('driver', 'coordinator', 'administrator')),
profile_picture_url TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_login TIMESTAMP,
is_active BOOLEAN DEFAULT true,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
approval_status VARCHAR(20) DEFAULT 'pending' CHECK (approval_status IN ('pending', 'approved', 'denied'))
)
`);
// Add approval_status column if it doesn't exist (migration for existing databases)
await this.query(`
ALTER TABLE users
ADD COLUMN IF NOT EXISTS approval_status VARCHAR(20) DEFAULT 'pending' CHECK (approval_status IN ('pending', 'approved', 'denied'))
`);
// Admin settings storage table
await this.query(`
CREATE TABLE IF NOT EXISTS admin_settings (
id SERIAL PRIMARY KEY,
setting_key VARCHAR(255) UNIQUE NOT NULL,
setting_value TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
await this.query(`
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ language 'plpgsql';
`);
await this.query(`
DROP TRIGGER IF EXISTS update_admin_settings_updated_at ON admin_settings
`);
await this.query(`
CREATE TRIGGER update_admin_settings_updated_at
BEFORE UPDATE ON admin_settings
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column()
`);
// Create indexes
await this.query(`
CREATE INDEX IF NOT EXISTS idx_users_google_id ON users(google_id)
`);
await this.query(`
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)
`);
await this.query(`
CREATE INDEX IF NOT EXISTS idx_users_role ON users(role)
`);
console.log('✅ Database tables initialized successfully');
} catch (error) {
console.error('❌ Failed to initialize database tables:', error);
throw error;
}
}
// User management methods
async createUser(user: {
id: string;
google_id: string;
email: string;
name: string;
profile_picture_url?: string;
role: string;
}): Promise<any> {
const query = `
INSERT INTO users (id, google_id, email, name, profile_picture_url, role, last_login)
VALUES ($1, $2, $3, $4, $5, $6, CURRENT_TIMESTAMP)
RETURNING *
`;
const values = [
user.id,
user.google_id,
user.email,
user.name,
user.profile_picture_url || null,
user.role
];
const result = await this.query(query, values);
console.log(`👤 Created user: ${user.name} (${user.email}) as ${user.role}`);
return result.rows[0];
}
async getUserByEmail(email: string): Promise<any> {
const query = 'SELECT * FROM users WHERE email = $1';
const result = await this.query(query, [email]);
return result.rows[0] || null;
}
async getUserById(id: string): Promise<any> {
const query = 'SELECT * FROM users WHERE id = $1';
const result = await this.query(query, [id]);
return result.rows[0] || null;
}
async migrateUserId(oldId: string, newId: string): Promise<void> {
const client = await this.pool.connect();
try {
await client.query('BEGIN');
await client.query(
'UPDATE drivers SET user_id = $2 WHERE user_id = $1',
[oldId, newId]
);
await client.query(
'UPDATE users SET id = $2, google_id = $2 WHERE id = $1',
[oldId, newId]
);
await client.query('COMMIT');
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}
async getAllUsers(): Promise<any[]> {
const query = 'SELECT * FROM users ORDER BY created_at ASC';
const result = await this.query(query);
return result.rows;
}
async updateUserRole(email: string, role: string): Promise<any> {
const query = `
UPDATE users
SET role = $1, updated_at = CURRENT_TIMESTAMP
WHERE email = $2
RETURNING *
`;
const result = await this.query(query, [role, email]);
if (result.rows[0]) {
console.log(`👤 Updated user role: ${result.rows[0].name} (${email}) -> ${role}`);
}
return result.rows[0] || null;
}
async updateUserLastSignIn(email: string): Promise<any> {
const query = `
UPDATE users
SET last_login = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
WHERE email = $1
RETURNING *
`;
const result = await this.query(query, [email]);
return result.rows[0] || null;
}
async deleteUser(email: string): Promise<any> {
const query = 'DELETE FROM users WHERE email = $1 RETURNING *';
const result = await this.query(query, [email]);
if (result.rows[0]) {
console.log(`👤 Deleted user: ${result.rows[0].name} (${email})`);
}
return result.rows[0] || null;
}
async getUserCount(): Promise<number> {
const query = 'SELECT COUNT(*) as count FROM users';
const result = await this.query(query);
return parseInt(result.rows[0].count);
}
// User approval methods
async updateUserApprovalStatus(email: string, status: 'pending' | 'approved' | 'denied'): Promise<any> {
const query = `
UPDATE users
SET approval_status = $1, updated_at = CURRENT_TIMESTAMP
WHERE email = $2
RETURNING *
`;
const result = await this.query(query, [status, email]);
if (result.rows[0]) {
console.log(`👤 Updated user approval: ${result.rows[0].name} (${email}) -> ${status}`);
}
return result.rows[0] || null;
}
async getPendingUsers(): Promise<any[]> {
const query = 'SELECT * FROM users WHERE approval_status = $1 ORDER BY created_at ASC';
const result = await this.query(query, ['pending']);
return result.rows;
}
async getApprovedUserCount(): Promise<number> {
const query = 'SELECT COUNT(*) as count FROM users WHERE approval_status = $1';
const result = await this.query(query, ['approved']);
return parseInt(result.rows[0].count);
}
// Initialize all database tables and schema
async initializeDatabase(): Promise<void> {
try {
await this.initializeTables();
await this.initializeVipTables();
// Approve all existing users (migration for approval system)
await this.query(`
UPDATE users
SET approval_status = 'approved'
WHERE approval_status IS NULL OR approval_status = 'pending'
`);
console.log('✅ Approved all existing users');
console.log('✅ Database schema initialization completed');
} catch (error) {
console.error('❌ Failed to initialize database schema:', error);
throw error;
}
}
// VIP schema (flights, drivers, schedules)
async initializeVipTables(): Promise<void> {
try {
await this.query(`
CREATE TABLE IF NOT EXISTS vips (
id VARCHAR(255) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
await this.query(`
ALTER TABLE vips
ADD COLUMN IF NOT EXISTS organization VARCHAR(255),
ADD COLUMN IF NOT EXISTS department VARCHAR(255) DEFAULT 'Office of Development',
ADD COLUMN IF NOT EXISTS transport_mode VARCHAR(50),
ADD COLUMN IF NOT EXISTS expected_arrival TIMESTAMP,
ADD COLUMN IF NOT EXISTS needs_airport_pickup BOOLEAN DEFAULT false,
ADD COLUMN IF NOT EXISTS needs_venue_transport BOOLEAN DEFAULT true
`);
await this.query(`
CREATE TABLE IF NOT EXISTS flights (
id SERIAL PRIMARY KEY,
vip_id VARCHAR(255) REFERENCES vips(id) ON DELETE CASCADE,
flight_number VARCHAR(50) NOT NULL,
flight_date DATE NOT NULL,
segment INTEGER NOT NULL,
departure_airport VARCHAR(10),
arrival_airport VARCHAR(10),
scheduled_departure TIMESTAMP,
scheduled_arrival TIMESTAMP,
actual_departure TIMESTAMP,
actual_arrival TIMESTAMP,
status VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
await this.query(`
CREATE TABLE IF NOT EXISTS drivers (
id VARCHAR(255) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
await this.query(`
ALTER TABLE drivers
ADD COLUMN IF NOT EXISTS phone VARCHAR(50),
ADD COLUMN IF NOT EXISTS department VARCHAR(255) DEFAULT 'Office of Development',
ADD COLUMN IF NOT EXISTS user_id VARCHAR(255) REFERENCES users(id) ON DELETE SET NULL
`);
await this.query(`
CREATE TABLE IF NOT EXISTS schedule_events (
id VARCHAR(255) PRIMARY KEY,
vip_id VARCHAR(255) REFERENCES vips(id) ON DELETE CASCADE,
title VARCHAR(255) NOT NULL,
location VARCHAR(255) NOT NULL,
start_time TIMESTAMP NOT NULL,
end_time TIMESTAMP NOT NULL,
description TEXT,
assigned_driver_id VARCHAR(255) REFERENCES drivers(id) ON DELETE SET NULL,
status VARCHAR(50) DEFAULT 'scheduled' CHECK (status IN ('scheduled', 'in-progress', 'completed', 'cancelled')),
event_type VARCHAR(50) NOT NULL CHECK (event_type IN ('transport', 'meeting', 'event', 'meal', 'accommodation')),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
await this.query(`
DROP TABLE IF EXISTS schedules
`);
await this.query(`
ALTER TABLE schedule_events
ADD COLUMN IF NOT EXISTS description TEXT,
ADD COLUMN IF NOT EXISTS assigned_driver_id VARCHAR(255) REFERENCES drivers(id) ON DELETE SET NULL,
ADD COLUMN IF NOT EXISTS status VARCHAR(50) DEFAULT 'scheduled',
ADD COLUMN IF NOT EXISTS event_type VARCHAR(50),
ADD COLUMN IF NOT EXISTS created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
`);
await this.query(`
CREATE INDEX IF NOT EXISTS idx_vips_transport_mode ON vips(transport_mode)
`);
await this.query(`
CREATE INDEX IF NOT EXISTS idx_flights_vip_id ON flights(vip_id)
`);
await this.query(`
CREATE INDEX IF NOT EXISTS idx_flights_date ON flights(flight_date)
`);
await this.query(`
CREATE INDEX IF NOT EXISTS idx_schedule_events_vip_id ON schedule_events(vip_id)
`);
await this.query(`
CREATE INDEX IF NOT EXISTS idx_schedule_events_driver_id ON schedule_events(assigned_driver_id)
`);
await this.query(`
CREATE INDEX IF NOT EXISTS idx_schedule_events_start_time ON schedule_events(start_time)
`);
console.log('✅ VIP and schedule tables initialized successfully');
} catch (error) {
console.error('❌ Failed to initialize VIP tables:', error);
}
}
// Redis-based driver location tracking
async getDriverLocation(driverId: string): Promise<{ lat: number; lng: number } | null> {
try {
if (!this.redis.isOpen) {
await this.redis.connect();
}
const location = await this.redis.hGetAll(`driver:${driverId}:location`);
if (location && location.lat && location.lng) {
return {
lat: parseFloat(location.lat),
lng: parseFloat(location.lng)
};
}
return null;
} catch (error) {
console.error('❌ Error getting driver location from Redis:', error);
return null;
}
}
async updateDriverLocation(driverId: string, location: { lat: number; lng: number }): Promise<void> {
try {
if (!this.redis.isOpen) {
await this.redis.connect();
}
const key = `driver:${driverId}:location`;
await this.redis.hSet(key, {
lat: location.lat.toString(),
lng: location.lng.toString(),
updated_at: new Date().toISOString()
});
// Set expiration to 24 hours
await this.redis.expire(key, 24 * 60 * 60);
} catch (error) {
console.error('❌ Error updating driver location in Redis:', error);
}
}
async getAllDriverLocations(): Promise<{ [driverId: string]: { lat: number; lng: number } }> {
try {
if (!this.redis.isOpen) {
await this.redis.connect();
}
const keys = await this.redis.keys('driver:*:location');
const locations: { [driverId: string]: { lat: number; lng: number } } = {};
for (const key of keys) {
const driverId = key.split(':')[1];
const location = await this.redis.hGetAll(key);
if (location && location.lat && location.lng) {
locations[driverId] = {
lat: parseFloat(location.lat),
lng: parseFloat(location.lng)
};
}
}
return locations;
} catch (error) {
console.error('❌ Error getting all driver locations from Redis:', error);
return {};
}
}
async removeDriverLocation(driverId: string): Promise<void> {
try {
if (!this.redis.isOpen) {
await this.redis.connect();
}
await this.redis.del(`driver:${driverId}:location`);
} catch (error) {
console.error('❌ Error removing driver location from Redis:', error);
}
}
}
export default new DatabaseService();

View File

@@ -0,0 +1,184 @@
interface ScheduleEvent {
id: string;
title: string;
location: string;
startTime: string;
endTime: string;
assignedDriverId?: string;
vipId: string;
vipName: string;
}
interface ConflictInfo {
type: 'overlap' | 'tight_turnaround' | 'back_to_back';
severity: 'low' | 'medium' | 'high';
message: string;
conflictingEvent: ScheduleEvent;
timeDifference?: number; // minutes
}
interface DriverAvailability {
driverId: string;
driverName: string;
status: 'available' | 'scheduled' | 'overlapping' | 'tight_turnaround';
assignmentCount: number;
conflicts: ConflictInfo[];
currentAssignments: ScheduleEvent[];
}
class DriverConflictService {
// Check for conflicts when assigning a driver to an event
checkDriverConflicts(
driverId: string,
newEvent: { startTime: string; endTime: string; location: string },
allSchedules: { [vipId: string]: ScheduleEvent[] },
drivers: any[]
): ConflictInfo[] {
const conflicts: ConflictInfo[] = [];
const driver = drivers.find(d => d.id === driverId);
if (!driver) return conflicts;
// Get all events assigned to this driver
const driverEvents = this.getDriverEvents(driverId, allSchedules);
const newStartTime = new Date(newEvent.startTime);
const newEndTime = new Date(newEvent.endTime);
for (const existingEvent of driverEvents) {
const existingStart = new Date(existingEvent.startTime);
const existingEnd = new Date(existingEvent.endTime);
// Check for direct time overlap
if (this.hasTimeOverlap(newStartTime, newEndTime, existingStart, existingEnd)) {
conflicts.push({
type: 'overlap',
severity: 'high',
message: `Direct time conflict with "${existingEvent.title}" for ${existingEvent.vipName}`,
conflictingEvent: existingEvent
});
}
// Check for tight turnaround (less than 15 minutes between events)
else {
const timeBetween = this.getTimeBetweenEvents(
newStartTime, newEndTime, existingStart, existingEnd
);
if (timeBetween !== null && timeBetween < 15) {
conflicts.push({
type: 'tight_turnaround',
severity: timeBetween < 5 ? 'high' : 'medium',
message: `Only ${timeBetween} minutes between events. Previous: "${existingEvent.title}"`,
conflictingEvent: existingEvent,
timeDifference: timeBetween
});
}
}
}
return conflicts;
}
// Get availability status for all drivers for a specific time slot
getDriverAvailability(
eventTime: { startTime: string; endTime: string; location: string },
allSchedules: { [vipId: string]: ScheduleEvent[] },
drivers: any[]
): DriverAvailability[] {
return drivers.map(driver => {
const conflicts = this.checkDriverConflicts(driver.id, eventTime, allSchedules, drivers);
const driverEvents = this.getDriverEvents(driver.id, allSchedules);
let status: DriverAvailability['status'] = 'available';
if (conflicts.length > 0) {
const hasOverlap = conflicts.some(c => c.type === 'overlap');
const hasTightTurnaround = conflicts.some(c => c.type === 'tight_turnaround');
if (hasOverlap) {
status = 'overlapping';
} else if (hasTightTurnaround) {
status = 'tight_turnaround';
}
} else if (driverEvents.length > 0) {
status = 'scheduled';
}
return {
driverId: driver.id,
driverName: driver.name,
status,
assignmentCount: driverEvents.length,
conflicts,
currentAssignments: driverEvents
};
});
}
// Get all events assigned to a specific driver
private getDriverEvents(driverId: string, allSchedules: { [vipId: string]: ScheduleEvent[] }): ScheduleEvent[] {
const driverEvents: ScheduleEvent[] = [];
Object.entries(allSchedules).forEach(([vipId, events]) => {
events.forEach(event => {
if (event.assignedDriverId === driverId) {
driverEvents.push({
...event,
vipId,
vipName: event.title // We'll need to get actual VIP name from VIP data
});
}
});
});
// Sort by start time
return driverEvents.sort((a, b) =>
new Date(a.startTime).getTime() - new Date(b.startTime).getTime()
);
}
// Check if two time periods overlap
private hasTimeOverlap(
start1: Date, end1: Date,
start2: Date, end2: Date
): boolean {
return start1 < end2 && start2 < end1;
}
// Get minutes between two events (null if they overlap)
private getTimeBetweenEvents(
newStart: Date, newEnd: Date,
existingStart: Date, existingEnd: Date
): number | null {
// If new event is after existing event
if (newStart >= existingEnd) {
return Math.floor((newStart.getTime() - existingEnd.getTime()) / (1000 * 60));
}
// If new event is before existing event
else if (newEnd <= existingStart) {
return Math.floor((existingStart.getTime() - newEnd.getTime()) / (1000 * 60));
}
// Events overlap
return null;
}
// Generate summary message for driver status
getDriverStatusSummary(availability: DriverAvailability): string {
switch (availability.status) {
case 'available':
return `✅ Fully available (${availability.assignmentCount} assignments)`;
case 'scheduled':
return `🟡 Has ${availability.assignmentCount} assignment(s) but available for this time`;
case 'tight_turnaround':
const tightConflict = availability.conflicts.find(c => c.type === 'tight_turnaround');
return `⚡ Tight turnaround - ${tightConflict?.timeDifference} min between events`;
case 'overlapping':
return `🔴 Time conflict with existing assignment`;
default:
return 'Unknown status';
}
}
}
export default new DriverConflictService();
export { DriverAvailability, ConflictInfo, ScheduleEvent };

View File

@@ -0,0 +1,679 @@
import pool from '../config/database';
import databaseService from './databaseService';
interface VipData {
id: string;
name: string;
organization: string;
department?: string;
transportMode: 'flight' | 'self-driving';
expectedArrival?: string;
needsAirportPickup?: boolean;
needsVenueTransport: boolean;
notes?: string;
flights?: Array<{
flightNumber: string;
flightDate: string;
segment: number;
}>;
}
interface DriverData {
id: string;
name: string;
phone: string;
department?: string;
currentLocation?: { lat: number; lng: number };
assignedVipIds?: string[];
}
interface ScheduleEventData {
id: string;
title: string;
location: string;
startTime: string;
endTime: string;
description?: string;
assignedDriverId?: string;
status: string;
type: string;
}
class EnhancedDataService {
// VIP operations
async getVips(): Promise<VipData[]> {
try {
const query = `
SELECT v.*,
COALESCE(
json_agg(
json_build_object(
'flightNumber', f.flight_number,
'flightDate', f.flight_date,
'segment', f.segment
) ORDER BY f.segment
) FILTER (WHERE f.id IS NOT NULL),
'[]'::json
) as flights
FROM vips v
LEFT JOIN flights f ON v.id = f.vip_id
GROUP BY v.id
ORDER BY v.name
`;
const result = await pool.query(query);
return result.rows.map(row => ({
id: row.id,
name: row.name,
organization: row.organization,
department: row.department,
transportMode: row.transport_mode,
expectedArrival: row.expected_arrival,
needsAirportPickup: row.needs_airport_pickup,
needsVenueTransport: row.needs_venue_transport,
notes: row.notes,
flights: row.flights
}));
} catch (error) {
console.error('❌ Error fetching VIPs:', error);
throw error;
}
}
async addVip(vip: VipData): Promise<VipData> {
const client = await pool.connect();
try {
await client.query('BEGIN');
// Insert VIP
const vipQuery = `
INSERT INTO vips (id, name, organization, department, transport_mode, expected_arrival, needs_airport_pickup, needs_venue_transport, notes)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
RETURNING *
`;
const vipResult = await client.query(vipQuery, [
vip.id,
vip.name,
vip.organization,
vip.department || 'Office of Development',
vip.transportMode,
vip.expectedArrival || null,
vip.needsAirportPickup || false,
vip.needsVenueTransport,
vip.notes || ''
]);
// Insert flights if any
if (vip.flights && vip.flights.length > 0) {
for (const flight of vip.flights) {
const flightQuery = `
INSERT INTO flights (vip_id, flight_number, flight_date, segment)
VALUES ($1, $2, $3, $4)
`;
await client.query(flightQuery, [
vip.id,
flight.flightNumber,
flight.flightDate,
flight.segment
]);
}
}
await client.query('COMMIT');
const savedVip = {
...vip,
department: vipResult.rows[0].department,
transportMode: vipResult.rows[0].transport_mode,
expectedArrival: vipResult.rows[0].expected_arrival,
needsAirportPickup: vipResult.rows[0].needs_airport_pickup,
needsVenueTransport: vipResult.rows[0].needs_venue_transport
};
return savedVip;
} catch (error) {
await client.query('ROLLBACK');
console.error('❌ Error adding VIP:', error);
throw error;
} finally {
client.release();
}
}
async updateVip(id: string, vip: Partial<VipData>): Promise<VipData | null> {
const client = await pool.connect();
try {
await client.query('BEGIN');
// Update VIP
const vipQuery = `
UPDATE vips
SET name = $2, organization = $3, department = $4, transport_mode = $5,
expected_arrival = $6, needs_airport_pickup = $7, needs_venue_transport = $8, notes = $9
WHERE id = $1
RETURNING *
`;
const vipResult = await client.query(vipQuery, [
id,
vip.name,
vip.organization,
vip.department || 'Office of Development',
vip.transportMode,
vip.expectedArrival || null,
vip.needsAirportPickup || false,
vip.needsVenueTransport,
vip.notes || ''
]);
if (vipResult.rows.length === 0) {
await client.query('ROLLBACK');
return null;
}
// Delete existing flights and insert new ones
await client.query('DELETE FROM flights WHERE vip_id = $1', [id]);
if (vip.flights && vip.flights.length > 0) {
for (const flight of vip.flights) {
const flightQuery = `
INSERT INTO flights (vip_id, flight_number, flight_date, segment)
VALUES ($1, $2, $3, $4)
`;
await client.query(flightQuery, [
id,
flight.flightNumber,
flight.flightDate,
flight.segment
]);
}
}
await client.query('COMMIT');
const updatedVip = {
id: vipResult.rows[0].id,
name: vipResult.rows[0].name,
organization: vipResult.rows[0].organization,
department: vipResult.rows[0].department,
transportMode: vipResult.rows[0].transport_mode,
expectedArrival: vipResult.rows[0].expected_arrival,
needsAirportPickup: vipResult.rows[0].needs_airport_pickup,
needsVenueTransport: vipResult.rows[0].needs_venue_transport,
notes: vipResult.rows[0].notes,
flights: vip.flights || []
};
return updatedVip;
} catch (error) {
await client.query('ROLLBACK');
console.error('❌ Error updating VIP:', error);
throw error;
} finally {
client.release();
}
}
async deleteVip(id: string): Promise<VipData | null> {
try {
const query = `
DELETE FROM vips WHERE id = $1 RETURNING *
`;
const result = await pool.query(query, [id]);
if (result.rows.length === 0) {
return null;
}
const deletedVip = {
id: result.rows[0].id,
name: result.rows[0].name,
organization: result.rows[0].organization,
department: result.rows[0].department,
transportMode: result.rows[0].transport_mode,
expectedArrival: result.rows[0].expected_arrival,
needsAirportPickup: result.rows[0].needs_airport_pickup,
needsVenueTransport: result.rows[0].needs_venue_transport,
notes: result.rows[0].notes
};
return deletedVip;
} catch (error) {
console.error('❌ Error deleting VIP:', error);
throw error;
}
}
// Driver operations
async getDrivers(): Promise<DriverData[]> {
try {
const query = `
SELECT d.*,
COALESCE(
json_agg(DISTINCT se.vip_id) FILTER (WHERE se.vip_id IS NOT NULL),
'[]'::json
) as assigned_vip_ids
FROM drivers d
LEFT JOIN schedule_events se ON d.id = se.assigned_driver_id
GROUP BY d.id
ORDER BY d.name
`;
const result = await pool.query(query);
// Get current locations from Redis
const driversWithLocations = await Promise.all(
result.rows.map(async (row) => {
const location = await databaseService.getDriverLocation(row.id);
return {
id: row.id,
name: row.name,
phone: row.phone,
department: row.department,
currentLocation: location ? { lat: location.lat, lng: location.lng } : { lat: 0, lng: 0 },
assignedVipIds: row.assigned_vip_ids || []
};
})
);
return driversWithLocations;
} catch (error) {
console.error('❌ Error fetching drivers:', error);
throw error;
}
}
async addDriver(driver: DriverData): Promise<DriverData> {
try {
const query = `
INSERT INTO drivers (id, name, phone, department)
VALUES ($1, $2, $3, $4)
RETURNING *
`;
const result = await pool.query(query, [
driver.id,
driver.name,
driver.phone,
driver.department || 'Office of Development'
]);
// Store location in Redis if provided
if (driver.currentLocation) {
await databaseService.updateDriverLocation(driver.id, driver.currentLocation);
}
const savedDriver = {
id: result.rows[0].id,
name: result.rows[0].name,
phone: result.rows[0].phone,
department: result.rows[0].department,
currentLocation: driver.currentLocation || { lat: 0, lng: 0 }
};
return savedDriver;
} catch (error) {
console.error('❌ Error adding driver:', error);
throw error;
}
}
async updateDriver(id: string, driver: Partial<DriverData>): Promise<DriverData | null> {
try {
const query = `
UPDATE drivers
SET name = $2, phone = $3, department = $4
WHERE id = $1
RETURNING *
`;
const result = await pool.query(query, [
id,
driver.name,
driver.phone,
driver.department || 'Office of Development'
]);
if (result.rows.length === 0) {
return null;
}
// Update location in Redis if provided
if (driver.currentLocation) {
await databaseService.updateDriverLocation(id, driver.currentLocation);
}
const updatedDriver = {
id: result.rows[0].id,
name: result.rows[0].name,
phone: result.rows[0].phone,
department: result.rows[0].department,
currentLocation: driver.currentLocation || { lat: 0, lng: 0 }
};
return updatedDriver;
} catch (error) {
console.error('❌ Error updating driver:', error);
throw error;
}
}
async deleteDriver(id: string): Promise<DriverData | null> {
try {
const query = `
DELETE FROM drivers WHERE id = $1 RETURNING *
`;
const result = await pool.query(query, [id]);
if (result.rows.length === 0) {
return null;
}
const deletedDriver = {
id: result.rows[0].id,
name: result.rows[0].name,
phone: result.rows[0].phone,
department: result.rows[0].department
};
return deletedDriver;
} catch (error) {
console.error('❌ Error deleting driver:', error);
throw error;
}
}
// Schedule operations
async getSchedule(vipId: string): Promise<ScheduleEventData[]> {
try {
const query = `
SELECT * FROM schedule_events
WHERE vip_id = $1
ORDER BY start_time
`;
const result = await pool.query(query, [vipId]);
return result.rows.map(row => ({
id: row.id,
title: row.title,
location: row.location,
startTime: row.start_time,
endTime: row.end_time,
description: row.description,
assignedDriverId: row.assigned_driver_id,
status: row.status,
type: row.event_type
}));
} catch (error) {
console.error('❌ Error fetching schedule:', error);
throw error;
}
}
async addScheduleEvent(vipId: string, event: ScheduleEventData): Promise<ScheduleEventData> {
try {
const query = `
INSERT INTO schedule_events (id, vip_id, title, location, start_time, end_time, description, assigned_driver_id, status, event_type)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
RETURNING *
`;
const result = await pool.query(query, [
event.id,
vipId,
event.title,
event.location,
event.startTime,
event.endTime,
event.description || '',
event.assignedDriverId || null,
event.status,
event.type
]);
const savedEvent = {
id: result.rows[0].id,
title: result.rows[0].title,
location: result.rows[0].location,
startTime: result.rows[0].start_time,
endTime: result.rows[0].end_time,
description: result.rows[0].description,
assignedDriverId: result.rows[0].assigned_driver_id,
status: result.rows[0].status,
type: result.rows[0].event_type
};
return savedEvent;
} catch (error) {
console.error('❌ Error adding schedule event:', error);
throw error;
}
}
async updateScheduleEvent(vipId: string, eventId: string, event: ScheduleEventData): Promise<ScheduleEventData | null> {
try {
const query = `
UPDATE schedule_events
SET title = $3, location = $4, start_time = $5, end_time = $6, description = $7, assigned_driver_id = $8, status = $9, event_type = $10
WHERE id = $1 AND vip_id = $2
RETURNING *
`;
const result = await pool.query(query, [
eventId,
vipId,
event.title,
event.location,
event.startTime,
event.endTime,
event.description || '',
event.assignedDriverId || null,
event.status,
event.type
]);
if (result.rows.length === 0) {
return null;
}
const updatedEvent = {
id: result.rows[0].id,
title: result.rows[0].title,
location: result.rows[0].location,
startTime: result.rows[0].start_time,
endTime: result.rows[0].end_time,
description: result.rows[0].description,
assignedDriverId: result.rows[0].assigned_driver_id,
status: result.rows[0].status,
type: result.rows[0].event_type
};
return updatedEvent;
} catch (error) {
console.error('❌ Error updating schedule event:', error);
throw error;
}
}
async deleteScheduleEvent(vipId: string, eventId: string): Promise<ScheduleEventData | null> {
try {
const query = `
DELETE FROM schedule_events
WHERE id = $1 AND vip_id = $2
RETURNING *
`;
const result = await pool.query(query, [eventId, vipId]);
if (result.rows.length === 0) {
return null;
}
const deletedEvent = {
id: result.rows[0].id,
title: result.rows[0].title,
location: result.rows[0].location,
startTime: result.rows[0].start_time,
endTime: result.rows[0].end_time,
description: result.rows[0].description,
assignedDriverId: result.rows[0].assigned_driver_id,
status: result.rows[0].status,
type: result.rows[0].event_type
};
return deletedEvent;
} catch (error) {
console.error('❌ Error deleting schedule event:', error);
throw error;
}
}
async getAllSchedules(): Promise<{ [vipId: string]: ScheduleEventData[] }> {
try {
const query = `
SELECT * FROM schedule_events
ORDER BY vip_id, start_time
`;
const result = await pool.query(query);
const schedules: { [vipId: string]: ScheduleEventData[] } = {};
for (const row of result.rows) {
const vipId = row.vip_id;
if (!schedules[vipId]) {
schedules[vipId] = [];
}
schedules[vipId].push({
id: row.id,
title: row.title,
location: row.location,
startTime: row.start_time,
endTime: row.end_time,
description: row.description,
assignedDriverId: row.assigned_driver_id,
status: row.status,
type: row.event_type
});
}
return schedules;
} catch (error) {
console.error('❌ Error fetching all schedules:', error);
throw error;
}
}
// Admin settings operations
async getAdminSettings(): Promise<any> {
try {
const query = `
SELECT setting_key, setting_value FROM admin_settings
`;
const result = await pool.query(query);
// Default settings structure
const defaultSettings = {
apiKeys: {
aviationStackKey: process.env.AVIATIONSTACK_API_KEY || '',
googleMapsKey: '',
twilioKey: '',
auth0Domain: process.env.AUTH0_DOMAIN || '',
auth0ClientId: process.env.AUTH0_CLIENT_ID || '',
auth0ClientSecret: process.env.AUTH0_CLIENT_SECRET || '',
auth0Audience: process.env.AUTH0_AUDIENCE || ''
},
systemSettings: {
defaultPickupLocation: '',
defaultDropoffLocation: '',
timeZone: 'America/New_York',
notificationsEnabled: false
}
};
// If no settings exist, return defaults
if (result.rows.length === 0) {
return defaultSettings;
}
// Reconstruct nested object from flattened keys
const settings: any = { ...defaultSettings };
for (const row of result.rows) {
const keys = row.setting_key.split('.');
let current = settings;
for (let i = 0; i < keys.length - 1; i++) {
if (!current[keys[i]]) {
current[keys[i]] = {};
}
current = current[keys[i]];
}
// Parse boolean values
let value = row.setting_value;
if (value === 'true') value = true;
else if (value === 'false') value = false;
current[keys[keys.length - 1]] = value;
}
return settings;
} catch (error) {
console.error('❌ Error fetching admin settings:', error);
throw error;
}
}
async updateAdminSettings(settings: any): Promise<void> {
try {
// Flatten settings and update
const flattenSettings = (obj: any, prefix = ''): Array<{key: string, value: string}> => {
const result: Array<{key: string, value: string}> = [];
for (const [key, value] of Object.entries(obj)) {
const fullKey = prefix ? `${prefix}.${key}` : key;
if (typeof value === 'object' && value !== null) {
result.push(...flattenSettings(value, fullKey));
} else {
result.push({ key: fullKey, value: String(value) });
}
}
return result;
};
const flatSettings = flattenSettings(settings);
for (const setting of flatSettings) {
const query = `
INSERT INTO admin_settings (setting_key, setting_value)
VALUES ($1, $2)
ON CONFLICT (setting_key) DO UPDATE SET setting_value = $2
`;
await pool.query(query, [setting.key, setting.value]);
}
} catch (error) {
console.error('❌ Error updating admin settings:', error);
throw error;
}
}
}
export default new EnhancedDataService();

View File

@@ -0,0 +1,262 @@
// Real Flight tracking service with Google scraping
// No mock data - only real flight information
interface FlightData {
flightNumber: string;
flightDate: string;
status: string;
airline?: string;
aircraft?: string;
departure: {
airport: string;
airportName?: string;
scheduled: string;
estimated?: string;
actual?: string;
terminal?: string;
gate?: string;
};
arrival: {
airport: string;
airportName?: string;
scheduled: string;
estimated?: string;
actual?: string;
terminal?: string;
gate?: string;
};
delay?: number;
lastUpdated: string;
source: 'google' | 'aviationstack' | 'not_found';
}
interface FlightSearchParams {
flightNumber: string;
date: string; // YYYY-MM-DD format
departureAirport?: string;
arrivalAirport?: string;
}
class FlightService {
private flightCache: Map<string, { data: FlightData; expires: number }> = new Map();
private updateIntervals: Map<string, NodeJS.Timeout> = new Map();
constructor() {
// No API keys needed for Google scraping
}
// Real flight lookup - no mock data
async getFlightInfo(params: FlightSearchParams): Promise<FlightData | null> {
const cacheKey = `${params.flightNumber}_${params.date}`;
// Check cache first (shorter cache for real data)
const cached = this.flightCache.get(cacheKey);
if (cached && cached.expires > Date.now()) {
return cached.data;
}
try {
// Try Google scraping first
let flightData = await this.scrapeGoogleFlights(params);
// If Google fails, try AviationStack (if API key available)
if (!flightData) {
flightData = await this.getFromAviationStack(params);
}
// Cache the result for 2 minutes (shorter for real data)
if (flightData) {
this.flightCache.set(cacheKey, {
data: flightData,
expires: Date.now() + (2 * 60 * 1000)
});
}
return flightData;
} catch (error) {
console.error('Error fetching flight data:', error);
return null; // Return null instead of mock data
}
}
// Google Flights scraping implementation
private async scrapeGoogleFlights(params: FlightSearchParams): Promise<FlightData | null> {
try {
// Google Flights URL format
const googleUrl = `https://www.google.com/travel/flights/search?tfs=CBwQAhoeEgoyMDI1LTA3LTAxagcIARIDTEFYcgcIARIDSkZLQAFIAXABggELCP___________wFAAUgBmAEB&hl=en`;
// For now, return null to indicate no real scraping implementation
// In production, you would implement actual web scraping here
console.log(`Would scrape Google for flight ${params.flightNumber} on ${params.date}`);
return null;
} catch (error) {
console.error('Google scraping error:', error);
return null;
}
}
// AviationStack API integration (only if API key available)
private async getFromAviationStack(params: FlightSearchParams): Promise<FlightData | null> {
const apiKey = process.env.AVIATIONSTACK_API_KEY;
console.log('Checking AviationStack API key:', apiKey ? `Key present (${apiKey.length} chars)` : 'No key');
if (!apiKey || apiKey === 'demo_key' || apiKey === '') {
console.log('No valid AviationStack API key available');
return null; // No API key available
}
try {
// Format flight number: Remove spaces and convert to uppercase
const formattedFlightNumber = params.flightNumber.replace(/\s+/g, '').toUpperCase();
console.log(`Formatted flight number: ${params.flightNumber} -> ${formattedFlightNumber}`);
// Note: Free tier doesn't support date filtering, so we get recent flights
// For future dates, this won't work well - consider upgrading subscription
const url = `http://api.aviationstack.com/v1/flights?access_key=${apiKey}&flight_iata=${formattedFlightNumber}&limit=10`;
console.log('AviationStack API URL:', url.replace(apiKey, '***'));
console.log('Note: Free tier returns recent flights only, not future scheduled flights');
const response = await fetch(url);
const data: any = await response.json();
console.log('AviationStack response status:', response.status);
if (!response.ok) {
console.error('AviationStack API error - HTTP status:', response.status);
return null;
}
// Check for API errors in response
if (data?.error) {
console.error('AviationStack API error:', data.error);
return null;
}
if (Array.isArray(data?.data) && data.data.length > 0) {
// This is a valid flight number that exists!
console.log(`✅ Valid flight number: ${formattedFlightNumber} exists in the system`);
// Try to find a flight matching the requested date
let flight = data.data.find((f: any) => f.flight_date === params.date);
// If no exact date match, use most recent for validation
if (!flight) {
flight = data.data[0];
console.log(` Flight ${formattedFlightNumber} is valid`);
console.log(`Recent flight: ${flight.departure.airport}${flight.arrival.airport}`);
console.log(`Operated by: ${flight.airline?.name || 'Unknown'}`);
console.log(`Note: Showing recent data from ${flight.flight_date} for validation`);
} else {
console.log(`✅ Flight found for exact date: ${params.date}`);
}
console.log('Flight route:', `${flight.departure.iata}${flight.arrival.iata}`);
console.log('Status:', flight.flight_status);
return {
flightNumber: flight.flight.iata,
flightDate: flight.flight_date,
status: this.normalizeStatus(flight.flight_status),
airline: flight.airline?.name,
aircraft: flight.aircraft?.registration,
departure: {
airport: flight.departure.iata,
airportName: flight.departure.airport,
scheduled: flight.departure.scheduled,
estimated: flight.departure.estimated,
actual: flight.departure.actual,
terminal: flight.departure.terminal,
gate: flight.departure.gate
},
arrival: {
airport: flight.arrival.iata,
airportName: flight.arrival.airport,
scheduled: flight.arrival.scheduled,
estimated: flight.arrival.estimated,
actual: flight.arrival.actual,
terminal: flight.arrival.terminal,
gate: flight.arrival.gate
},
delay: flight.departure.delay || 0,
lastUpdated: new Date().toISOString(),
source: 'aviationstack'
};
}
console.log(`❌ Invalid flight number: ${formattedFlightNumber} not found`);
console.log('This flight number does not exist or has not operated recently');
return null;
} catch (error) {
console.error('AviationStack API error:', error);
return null;
}
}
// Start periodic updates for a flight
startPeriodicUpdates(params: FlightSearchParams, intervalMinutes: number = 5): void {
const key = `${params.flightNumber}_${params.date}`;
// Clear existing interval if any
this.stopPeriodicUpdates(key);
// Set up new interval
const interval = setInterval(async () => {
try {
await this.getFlightInfo(params); // This will update the cache
console.log(`Updated flight data for ${params.flightNumber} on ${params.date}`);
} catch (error) {
console.error(`Error updating flight ${params.flightNumber}:`, error);
}
}, intervalMinutes * 60 * 1000);
this.updateIntervals.set(key, interval);
}
// Stop periodic updates for a flight
stopPeriodicUpdates(key: string): void {
const interval = this.updateIntervals.get(key);
if (interval) {
clearInterval(interval);
this.updateIntervals.delete(key);
}
}
// Get multiple flights with date specificity
async getMultipleFlights(flightParams: FlightSearchParams[]): Promise<{ [key: string]: FlightData | null }> {
const results: { [key: string]: FlightData | null } = {};
for (const params of flightParams) {
const key = `${params.flightNumber}_${params.date}`;
results[key] = await this.getFlightInfo(params);
}
return results;
}
// Normalize flight status across different APIs
private normalizeStatus(status: string): string {
const statusMap: { [key: string]: string } = {
'scheduled': 'scheduled',
'active': 'active',
'landed': 'landed',
'cancelled': 'cancelled',
'incident': 'delayed',
'diverted': 'diverted'
};
return statusMap[status.toLowerCase()] || status;
}
// Clean up resources
cleanup(): void {
for (const [key, interval] of this.updateIntervals) {
clearInterval(interval);
}
this.updateIntervals.clear();
this.flightCache.clear();
}
}
export default new FlightService();
export { FlightData, FlightSearchParams };

View File

@@ -0,0 +1,284 @@
// Flight Tracking Scheduler Service
// Efficiently batches flight API calls and manages tracking schedules
interface ScheduledFlight {
vipId: string;
vipName: string;
flightNumber: string;
flightDate: string;
segment: number;
scheduledDeparture?: string;
lastChecked?: Date;
nextCheck?: Date;
status?: string;
hasLanded?: boolean;
}
interface TrackingSchedule {
[date: string]: ScheduledFlight[];
}
class FlightTrackingScheduler {
private trackingSchedule: TrackingSchedule = {};
private checkIntervals: Map<string, NodeJS.Timeout> = new Map();
private flightService: any;
constructor(flightService: any) {
this.flightService = flightService;
}
// Add flights for a VIP to the tracking schedule
addVipFlights(vipId: string, vipName: string, flights: any[]) {
flights.forEach(flight => {
const key = flight.flightDate;
if (!this.trackingSchedule[key]) {
this.trackingSchedule[key] = [];
}
// Check if this flight is already being tracked
const existingIndex = this.trackingSchedule[key].findIndex(
f => f.flightNumber === flight.flightNumber && f.vipId === vipId
);
const scheduledFlight: ScheduledFlight = {
vipId,
vipName,
flightNumber: flight.flightNumber,
flightDate: flight.flightDate,
segment: flight.segment,
scheduledDeparture: flight.validationData?.departure?.scheduled
};
if (existingIndex >= 0) {
// Update existing entry
this.trackingSchedule[key][existingIndex] = scheduledFlight;
} else {
// Add new entry
this.trackingSchedule[key].push(scheduledFlight);
}
});
// Start or update tracking for affected dates
this.updateTrackingSchedules();
}
// Remove VIP flights from tracking
removeVipFlights(vipId: string) {
Object.keys(this.trackingSchedule).forEach(date => {
this.trackingSchedule[date] = this.trackingSchedule[date].filter(
f => f.vipId !== vipId
);
// Remove empty dates
if (this.trackingSchedule[date].length === 0) {
delete this.trackingSchedule[date];
}
});
this.updateTrackingSchedules();
}
// Update tracking schedules based on current flights
private updateTrackingSchedules() {
// Clear existing intervals
this.checkIntervals.forEach(interval => clearInterval(interval));
this.checkIntervals.clear();
// Set up tracking for each date
Object.keys(this.trackingSchedule).forEach(date => {
this.setupDateTracking(date);
});
}
// Set up tracking for a specific date
private setupDateTracking(date: string) {
const flights = this.trackingSchedule[date];
if (!flights || flights.length === 0) return;
// Check if we should start tracking (4 hours before first flight)
const now = new Date();
const dateObj = new Date(date + 'T00:00:00');
// Find earliest departure time
let earliestDeparture: Date | null = null;
flights.forEach(flight => {
if (flight.scheduledDeparture) {
const depTime = new Date(flight.scheduledDeparture);
if (!earliestDeparture || depTime < earliestDeparture) {
earliestDeparture = depTime;
}
}
});
// If no departure times, assume noon
if (!earliestDeparture) {
earliestDeparture = new Date(date + 'T12:00:00');
}
// Start tracking 4 hours before earliest departure
const trackingStartTime = new Date(earliestDeparture.getTime() - 4 * 60 * 60 * 1000);
// If tracking should have started, begin immediately
if (now >= trackingStartTime) {
this.performBatchCheck(date);
// Set up recurring checks every 60 minutes (or 30 if any delays)
const interval = setInterval(() => {
this.performBatchCheck(date);
}, 60 * 60 * 1000); // 60 minutes
this.checkIntervals.set(date, interval);
} else {
// Schedule first check for tracking start time
const timeUntilStart = trackingStartTime.getTime() - now.getTime();
setTimeout(() => {
this.performBatchCheck(date);
// Then set up recurring checks
const interval = setInterval(() => {
this.performBatchCheck(date);
}, 60 * 60 * 1000);
this.checkIntervals.set(date, interval);
}, timeUntilStart);
}
}
// Perform batch check for all flights on a date
private async performBatchCheck(date: string) {
const flights = this.trackingSchedule[date];
if (!flights || flights.length === 0) return;
console.log(`\n=== Batch Flight Check for ${date} ===`);
console.log(`Checking ${flights.length} flights...`);
// Filter out flights that have already landed
const activeFlights = flights.filter(f => !f.hasLanded);
if (activeFlights.length === 0) {
console.log('All flights have landed. Stopping tracking for this date.');
this.stopDateTracking(date);
return;
}
// Get unique flight numbers to check
const uniqueFlights = Array.from(new Set(
activeFlights.map(f => f.flightNumber)
));
console.log(`Unique flight numbers to check: ${uniqueFlights.join(', ')}`);
try {
// Make batch API call
const flightParams = uniqueFlights.map(flightNumber => ({
flightNumber,
date
}));
const results = await this.flightService.getMultipleFlights(flightParams);
// Update flight statuses
let hasDelays = false;
let allLanded = true;
activeFlights.forEach(flight => {
const key = `${flight.flightNumber}_${date}`;
const data = results[key];
if (data) {
flight.lastChecked = new Date();
flight.status = data.status;
if (data.status === 'landed') {
flight.hasLanded = true;
console.log(`${flight.flightNumber} has landed`);
} else {
allLanded = false;
if (data.delay && data.delay > 0) {
hasDelays = true;
console.log(`⚠️ ${flight.flightNumber} is delayed by ${data.delay} minutes`);
}
}
// Log status for each VIP
console.log(` VIP: ${flight.vipName} - Flight ${flight.segment}: ${flight.flightNumber} - Status: ${data.status}`);
}
});
// Update check frequency if delays detected
if (hasDelays && this.checkIntervals.has(date)) {
console.log('Delays detected - increasing check frequency to 30 minutes');
clearInterval(this.checkIntervals.get(date)!);
const interval = setInterval(() => {
this.performBatchCheck(date);
}, 30 * 60 * 1000); // 30 minutes
this.checkIntervals.set(date, interval);
}
// Stop tracking if all flights have landed
if (allLanded) {
console.log('All flights have landed. Stopping tracking for this date.');
this.stopDateTracking(date);
}
// Calculate next check time
const nextCheckTime = new Date(Date.now() + (hasDelays ? 30 : 60) * 60 * 1000);
console.log(`Next check scheduled for: ${nextCheckTime.toLocaleTimeString()}`);
} catch (error) {
console.error('Error performing batch flight check:', error);
}
}
// Stop tracking for a specific date
private stopDateTracking(date: string) {
const interval = this.checkIntervals.get(date);
if (interval) {
clearInterval(interval);
this.checkIntervals.delete(date);
}
// Mark all flights as completed
if (this.trackingSchedule[date]) {
this.trackingSchedule[date].forEach(f => f.hasLanded = true);
}
}
// Get current tracking status
getTrackingStatus(): any {
const status: any = {};
Object.entries(this.trackingSchedule).forEach(([date, flights]) => {
const activeFlights = flights.filter(f => !f.hasLanded);
const landedFlights = flights.filter(f => f.hasLanded);
status[date] = {
totalFlights: flights.length,
activeFlights: activeFlights.length,
landedFlights: landedFlights.length,
flights: flights.map(f => ({
vipName: f.vipName,
flightNumber: f.flightNumber,
segment: f.segment,
status: f.status || 'Not checked yet',
lastChecked: f.lastChecked,
hasLanded: f.hasLanded
}))
};
});
return status;
}
// Clean up all tracking
cleanup() {
this.checkIntervals.forEach(interval => clearInterval(interval));
this.checkIntervals.clear();
this.trackingSchedule = {};
}
}
export default FlightTrackingScheduler;

View File

@@ -0,0 +1,248 @@
interface ValidationError {
field: string;
message: string;
code: string;
}
interface ScheduleEvent {
title: string;
startTime: string;
endTime: string;
location: string;
type: string;
}
class ScheduleValidationService {
// Validate a single schedule event
validateEvent(event: ScheduleEvent, isEdit: boolean = false): ValidationError[] {
const errors: ValidationError[] = [];
const now = new Date();
const startTime = new Date(event.startTime);
const endTime = new Date(event.endTime);
// 1. Check if dates are valid
if (isNaN(startTime.getTime())) {
errors.push({
field: 'startTime',
message: 'Start time is not a valid date',
code: 'INVALID_START_DATE'
});
}
if (isNaN(endTime.getTime())) {
errors.push({
field: 'endTime',
message: 'End time is not a valid date',
code: 'INVALID_END_DATE'
});
}
// If dates are invalid, return early
if (errors.length > 0) {
return errors;
}
// 2. Check if start time is in the future (with 5-minute grace period for edits)
const graceMinutes = isEdit ? 5 : 0;
const minimumStartTime = new Date(now.getTime() + (graceMinutes * 60 * 1000));
if (startTime < minimumStartTime) {
errors.push({
field: 'startTime',
message: isEdit
? 'Start time must be at least 5 minutes in the future for edits'
: 'Start time must be in the future',
code: 'START_TIME_IN_PAST'
});
}
// 3. Check if end time is after start time
if (endTime <= startTime) {
errors.push({
field: 'endTime',
message: 'End time must be after start time',
code: 'END_BEFORE_START'
});
}
// 4. Check minimum event duration (5 minutes)
const durationMinutes = (endTime.getTime() - startTime.getTime()) / (1000 * 60);
if (durationMinutes < 5) {
errors.push({
field: 'endTime',
message: 'Event must be at least 5 minutes long',
code: 'DURATION_TOO_SHORT'
});
}
// 5. Check maximum event duration (24 hours)
if (durationMinutes > (24 * 60)) {
errors.push({
field: 'endTime',
message: 'Event cannot be longer than 24 hours',
code: 'DURATION_TOO_LONG'
});
}
// 6. Check if end time is in the future
if (endTime < now) {
errors.push({
field: 'endTime',
message: 'End time must be in the future',
code: 'END_TIME_IN_PAST'
});
}
// 7. Validate required fields
if (!event.title || event.title.trim().length === 0) {
errors.push({
field: 'title',
message: 'Event title is required',
code: 'TITLE_REQUIRED'
});
}
if (!event.location || event.location.trim().length === 0) {
errors.push({
field: 'location',
message: 'Event location is required',
code: 'LOCATION_REQUIRED'
});
}
if (!event.type || event.type.trim().length === 0) {
errors.push({
field: 'type',
message: 'Event type is required',
code: 'TYPE_REQUIRED'
});
}
// 8. Validate title length
if (event.title && event.title.length > 100) {
errors.push({
field: 'title',
message: 'Event title cannot exceed 100 characters',
code: 'TITLE_TOO_LONG'
});
}
// 9. Validate location length
if (event.location && event.location.length > 200) {
errors.push({
field: 'location',
message: 'Event location cannot exceed 200 characters',
code: 'LOCATION_TOO_LONG'
});
}
// 10. Check for reasonable scheduling (not more than 2 years in the future)
const twoYearsFromNow = new Date();
twoYearsFromNow.setFullYear(twoYearsFromNow.getFullYear() + 2);
if (startTime > twoYearsFromNow) {
errors.push({
field: 'startTime',
message: 'Event cannot be scheduled more than 2 years in the future',
code: 'START_TIME_TOO_FAR'
});
}
// 11. Check for business hours validation (optional warning)
const startHour = startTime.getHours();
const endHour = endTime.getHours();
if (startHour < 6 || startHour > 23) {
// This is a warning, not an error - we'll add it but with a different severity
errors.push({
field: 'startTime',
message: 'Event starts outside typical business hours (6 AM - 11 PM)',
code: 'OUTSIDE_BUSINESS_HOURS'
});
}
return errors;
}
// Validate multiple events for conflicts and logical sequencing
validateEventSequence(events: ScheduleEvent[]): ValidationError[] {
const errors: ValidationError[] = [];
// Sort events by start time
const sortedEvents = events
.map((event, index) => ({ ...event, originalIndex: index }))
.sort((a, b) => new Date(a.startTime).getTime() - new Date(b.startTime).getTime());
// Check for overlapping events
for (let i = 0; i < sortedEvents.length - 1; i++) {
const currentEvent = sortedEvents[i];
const nextEvent = sortedEvents[i + 1];
const currentEnd = new Date(currentEvent.endTime);
const nextStart = new Date(nextEvent.startTime);
if (currentEnd > nextStart) {
errors.push({
field: 'schedule',
message: `Event "${currentEvent.title}" overlaps with "${nextEvent.title}"`,
code: 'EVENTS_OVERLAP'
});
}
}
return errors;
}
// Get user-friendly error messages
getErrorSummary(errors: ValidationError[]): string {
if (errors.length === 0) return '';
const errorMessages = errors.map(error => error.message);
if (errors.length === 1) {
return errorMessages[0];
}
return `Multiple validation errors:\n• ${errorMessages.join('\n• ')}`;
}
// Check if errors are warnings vs critical errors
isCriticalError(error: ValidationError): boolean {
const warningCodes = ['OUTSIDE_BUSINESS_HOURS'];
return !warningCodes.includes(error.code);
}
// Separate critical errors from warnings
categorizeErrors(errors: ValidationError[]): { critical: ValidationError[], warnings: ValidationError[] } {
const critical: ValidationError[] = [];
const warnings: ValidationError[] = [];
errors.forEach(error => {
if (this.isCriticalError(error)) {
critical.push(error);
} else {
warnings.push(error);
}
});
return { critical, warnings };
}
// Validate time format and suggest corrections
validateTimeFormat(timeString: string): { isValid: boolean, suggestion?: string } {
const date = new Date(timeString);
if (isNaN(date.getTime())) {
return {
isValid: false,
suggestion: 'Please use format: YYYY-MM-DDTHH:MM (e.g., 2025-07-01T14:30)'
};
}
return { isValid: true };
}
}
export default new ScheduleValidationService();
export { ValidationError, ScheduleEvent };

19
backend/tsconfig.json Normal file
View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

49
docker-compose.dev.yml Normal file
View File

@@ -0,0 +1,49 @@
version: '3.8'
services:
db:
image: postgres:15
environment:
POSTGRES_DB: vip_coordinator
POSTGRES_PASSWORD: changeme
volumes:
- postgres-data:/var/lib/postgresql/data
ports:
- 5432:5432
redis:
image: redis:7
ports:
- 6379:6379
backend:
build:
context: ./backend
target: development
environment:
DATABASE_URL: postgresql://postgres:changeme@db:5432/vip_coordinator
REDIS_URL: redis://redis:6379
ports:
- 3000:3000
depends_on:
- db
- redis
volumes:
- ./backend:/app
- /app/node_modules
frontend:
build:
context: ./frontend
target: development
ports:
- 5173:5173
depends_on:
- backend
volumes:
- ./frontend:/app
- /app/node_modules
volumes:
postgres-data:

60
docker-compose.prod.yml Normal file
View File

@@ -0,0 +1,60 @@
version: '3.8'
services:
db:
image: postgres:15
environment:
POSTGRES_DB: vip_coordinator
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- postgres-data:/var/lib/postgresql/data
ports:
- 5432:5432
redis:
image: redis:7
ports:
- 6379:6379
backend:
build:
context: ./backend
target: production
environment:
DATABASE_URL: postgresql://postgres:${DB_PASSWORD}@db:5432/vip_coordinator
REDIS_URL: redis://redis:6379
NODE_ENV: production
FRONTEND_URL: ${FRONTEND_URL}
AUTH0_DOMAIN: ${AUTH0_DOMAIN}
AUTH0_CLIENT_ID: ${AUTH0_CLIENT_ID}
AUTH0_CLIENT_SECRET: ${AUTH0_CLIENT_SECRET}
AUTH0_AUDIENCE: ${AUTH0_AUDIENCE}
INITIAL_ADMIN_EMAILS: ${INITIAL_ADMIN_EMAILS:-}
AVIATIONSTACK_API_KEY: ${AVIATIONSTACK_API_KEY:-}
DATABASE_SSL: ${DATABASE_SSL:-false}
PGSSLMODE: disable
ports:
- 3000:3000
depends_on:
- db
- redis
frontend:
build:
context: ./frontend
target: serve
args:
VITE_AUTH0_DOMAIN: ${AUTH0_DOMAIN}
VITE_AUTH0_CLIENT_ID: ${AUTH0_CLIENT_ID}
VITE_AUTH0_AUDIENCE: ${AUTH0_AUDIENCE}
ports:
- 80:80
- 443:443
volumes:
- /opt/vip-coordinator/certs:/etc/nginx/certs:ro
depends_on:
- backend
volumes:
postgres-data:

34
frontend/Dockerfile Normal file
View File

@@ -0,0 +1,34 @@
# Multi-stage build for development and production
FROM node:18-alpine AS base
WORKDIR /app
# Copy package files
COPY package*.json ./
# Development stage
FROM base AS development
RUN npm install
COPY . .
EXPOSE 5173
CMD ["npm", "run", "dev"]
# Production stage
FROM base AS production
ARG VITE_AUTH0_DOMAIN
ARG VITE_AUTH0_CLIENT_ID
ARG VITE_AUTH0_AUDIENCE
ENV VITE_AUTH0_DOMAIN=${VITE_AUTH0_DOMAIN}
ENV VITE_AUTH0_CLIENT_ID=${VITE_AUTH0_CLIENT_ID}
ENV VITE_AUTH0_AUDIENCE=${VITE_AUTH0_AUDIENCE}
RUN npm ci
COPY . .
RUN npm run build
RUN npm prune --omit=dev
# Serve with nginx
FROM nginx:alpine AS serve
COPY --from=production /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

13
frontend/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>VIP Coordinator Dashboard</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

64
frontend/nginx.conf Normal file
View File

@@ -0,0 +1,64 @@
worker_processes auto;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
server {
listen 80;
server_name bsa.madeamess.online _;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name bsa.madeamess.online _;
ssl_certificate /etc/nginx/certs/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/privkey.pem;
ssl_prefer_server_ciphers on;
root /usr/share/nginx/html;
index index.html;
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss image/svg+xml;
gzip_min_length 256;
location /auth/callback {
try_files $uri /index.html;
}
location /api/ {
proxy_pass http://backend:3000/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
}
location /auth/ {
proxy_pass http://backend:3000/auth/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
}
location / {
try_files $uri $uri/ /index.html;
}
}
}

6437
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

36
frontend/package.json Normal file
View File

@@ -0,0 +1,36 @@
{
"name": "vip-coordinator-frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@auth0/auth0-react": "^2.8.0",
"leaflet": "^1.9.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-leaflet": "^4.2.1",
"react-router-dom": "^6.15.0",
"tailwindcss": "^3.4.14",
"vite": "^4.5.14"
},
"devDependencies": {
"@types/leaflet": "^1.9.4",
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@vitejs/plugin-react": "^4.0.3",
"autoprefixer": "^10.4.21",
"eslint": "^8.45.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3",
"postcss": "^8.4.47",
"typescript": "^5.0.2"
}
}

View File

@@ -0,0 +1,9 @@
import tailwindcss from 'tailwindcss';
import autoprefixer from 'autoprefixer';
export default {
plugins: [
tailwindcss,
autoprefixer
],
};

View File

@@ -0,0 +1,218 @@
# VIP Coordinator API Documentation
## 📚 Overview
This document provides comprehensive API documentation for the VIP Coordinator system using **OpenAPI 3.0** (Swagger) specification. The API enables management of VIP transportation coordination, including flight tracking, driver management, and event scheduling.
## 🚀 Quick Start
### View API Documentation
1. **Interactive Documentation (Recommended):**
```bash
# Open the interactive Swagger UI documentation
open vip-coordinator/api-docs.html
```
Or visit: `file:///path/to/vip-coordinator/api-docs.html`
2. **Raw OpenAPI Specification:**
```bash
# View the YAML specification file
cat vip-coordinator/api-documentation.yaml
```
### Test the API
The interactive documentation includes a "Try it out" feature that allows you to test endpoints directly:
1. Open `api-docs.html` in your browser
2. Click on any endpoint to expand it
3. Click "Try it out" button
4. Fill in parameters and request body
5. Click "Execute" to make the API call
## 📋 API Categories
### 🏥 Health
- `GET /api/health` - System health check
### 👥 VIPs
- `GET /api/vips` - Get all VIPs
- `POST /api/vips` - Create new VIP
- `PUT /api/vips/{id}` - Update VIP
- `DELETE /api/vips/{id}` - Delete VIP
### 🚗 Drivers
- `GET /api/drivers` - Get all drivers
- `POST /api/drivers` - Create new driver
- `PUT /api/drivers/{id}` - Update driver
- `DELETE /api/drivers/{id}` - Delete driver
- `GET /api/drivers/{driverId}/schedule` - Get driver's schedule
- `POST /api/drivers/availability` - Check driver availability
- `POST /api/drivers/{driverId}/conflicts` - Check driver conflicts
### ✈️ Flights
- `GET /api/flights/{flightNumber}` - Get flight information
- `POST /api/flights/{flightNumber}/track` - Start flight tracking
- `DELETE /api/flights/{flightNumber}/track` - Stop flight tracking
- `POST /api/flights/batch` - Get multiple flights info
- `GET /api/flights/tracking/status` - Get tracking status
### 📅 Schedule
- `GET /api/vips/{vipId}/schedule` - Get VIP's schedule
- `POST /api/vips/{vipId}/schedule` - Add event to schedule
- `PUT /api/vips/{vipId}/schedule/{eventId}` - Update event
- `DELETE /api/vips/{vipId}/schedule/{eventId}` - Delete event
- `PATCH /api/vips/{vipId}/schedule/{eventId}/status` - Update event status
### ⚙️ Admin
- `POST /api/admin/authenticate` - Admin authentication
- `GET /api/admin/settings` - Get admin settings
- `POST /api/admin/settings` - Update admin settings
## 💡 Example API Calls
### Create a VIP with Flight
```bash
curl -X POST http://localhost:3000/api/vips \
-H "Content-Type: application/json" \
-d '{
"name": "John Doe",
"organization": "Tech Corp",
"transportMode": "flight",
"flights": [
{
"flightNumber": "UA1234",
"flightDate": "2025-06-26",
"segment": 1
}
],
"needsAirportPickup": true,
"needsVenueTransport": true,
"notes": "CEO - requires executive transport"
}'
```
### Add Event to VIP Schedule
```bash
curl -X POST http://localhost:3000/api/vips/{vipId}/schedule \
-H "Content-Type: application/json" \
-d '{
"title": "Meeting with CEO",
"location": "Hyatt Regency Denver",
"startTime": "2025-06-26T11:00:00",
"endTime": "2025-06-26T12:30:00",
"type": "meeting",
"assignedDriverId": "1748780965562",
"description": "Important strategic meeting"
}'
```
### Check Driver Availability
```bash
curl -X POST http://localhost:3000/api/drivers/availability \
-H "Content-Type: application/json" \
-d '{
"startTime": "2025-06-26T11:00:00",
"endTime": "2025-06-26T12:30:00",
"location": "Denver Convention Center"
}'
```
### Get Flight Information
```bash
curl "http://localhost:3000/api/flights/UA1234?date=2025-06-26"
```
## 🔧 Tools for API Documentation
### 1. **Swagger UI (Recommended)**
- **What it is:** Interactive web-based API documentation
- **Features:**
- Try endpoints directly in browser
- Auto-generated from OpenAPI spec
- Beautiful, responsive interface
- Request/response examples
- **Access:** Open `api-docs.html` in your browser
### 2. **OpenAPI Specification**
- **What it is:** Industry-standard API specification format
- **Features:**
- Machine-readable API definition
- Can generate client SDKs
- Supports validation and testing
- Compatible with many tools
- **File:** `api-documentation.yaml`
### 3. **Alternative Tools**
You can use the OpenAPI specification with other tools:
#### Postman
1. Import `api-documentation.yaml` into Postman
2. Automatically creates a collection with all endpoints
3. Includes examples and validation
#### Insomnia
1. Import the OpenAPI spec
2. Generate requests automatically
3. Built-in environment management
#### VS Code Extensions
- **OpenAPI (Swagger) Editor** - Edit and preview API specs
- **REST Client** - Test APIs directly in VS Code
## 📖 Documentation Best Practices
### Why OpenAPI/Swagger?
1. **Industry Standard:** Most widely adopted API documentation format
2. **Interactive:** Users can test APIs directly in the documentation
3. **Code Generation:** Can generate client libraries in multiple languages
4. **Validation:** Ensures API requests/responses match specification
5. **Tooling:** Extensive ecosystem of tools and integrations
### Documentation Features
- **Comprehensive:** All endpoints, parameters, and responses documented
- **Examples:** Real-world examples for all operations
- **Schemas:** Detailed data models with validation rules
- **Error Handling:** Clear error response documentation
- **Authentication:** Security requirements clearly specified
## 🔗 Integration Examples
### Frontend Integration
```javascript
// Example: Fetch VIPs in React
const fetchVips = async () => {
const response = await fetch('/api/vips');
const vips = await response.json();
return vips;
};
```
### Backend Integration
```bash
# Example: Using curl to test endpoints
curl -X GET http://localhost:3000/api/health
curl -X GET http://localhost:3000/api/vips
curl -X GET http://localhost:3000/api/drivers
```
## 🚀 Next Steps
1. **Explore the Interactive Docs:** Open `api-docs.html` and try the endpoints
2. **Test with Real Data:** Use the populated test data to explore functionality
3. **Build Integrations:** Use the API specification to build client applications
4. **Extend the API:** Add new endpoints following the established patterns
## 📞 Support
For questions about the API:
- Review the interactive documentation
- Check the OpenAPI specification for detailed schemas
- Test endpoints using the "Try it out" feature
- Refer to the example requests and responses
The API documentation is designed to be self-service and comprehensive, providing everything needed to integrate with the VIP Coordinator system.

View File

@@ -0,0 +1,148 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VIP Coordinator API Documentation</title>
<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui.css" />
<style>
html {
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*, *:before, *:after {
box-sizing: inherit;
}
body {
margin:0;
background: #fafafa;
}
.swagger-ui .topbar {
background-color: #3498db;
}
.swagger-ui .topbar .download-url-wrapper .select-label {
color: white;
}
.swagger-ui .topbar .download-url-wrapper input[type=text] {
border: 2px solid #2980b9;
}
.swagger-ui .info .title {
color: #2c3e50;
}
.custom-header {
background: linear-gradient(135deg, #3498db, #2980b9);
color: white;
padding: 20px;
text-align: center;
margin-bottom: 20px;
}
.custom-header h1 {
margin: 0;
font-size: 2.5em;
font-weight: 300;
}
.custom-header p {
margin: 10px 0 0 0;
font-size: 1.2em;
opacity: 0.9;
}
.quick-links {
background: white;
padding: 20px;
margin: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.quick-links h3 {
color: #2c3e50;
margin-top: 0;
}
.quick-links ul {
list-style: none;
padding: 0;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 10px;
}
.quick-links li {
background: #ecf0f1;
padding: 10px 15px;
border-radius: 5px;
border-left: 4px solid #3498db;
}
.quick-links li strong {
color: #2c3e50;
}
.quick-links li code {
background: #34495e;
color: white;
padding: 2px 6px;
border-radius: 3px;
font-size: 0.9em;
}
</style>
</head>
<body>
<div class="custom-header">
<h1>🚗 VIP Coordinator API</h1>
<p>Comprehensive API for managing VIP transportation coordination</p>
</div>
<div class="quick-links">
<h3>🚀 Quick Start Examples</h3>
<ul>
<li><strong>Health Check:</strong> <code>GET /api/health</code></li>
<li><strong>Get All VIPs:</strong> <code>GET /api/vips</code></li>
<li><strong>Get All Drivers:</strong> <code>GET /api/drivers</code></li>
<li><strong>Flight Info:</strong> <code>GET /api/flights/UA1234?date=2025-06-26</code></li>
<li><strong>VIP Schedule:</strong> <code>GET /api/vips/{vipId}/schedule</code></li>
<li><strong>Driver Availability:</strong> <code>POST /api/drivers/availability</code></li>
</ul>
</div>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui-bundle.js"></script>
<script src="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui-standalone-preset.js"></script>
<script>
window.onload = function() {
// Begin Swagger UI call region
const ui = SwaggerUIBundle({
url: '/api-documentation.yaml',
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout",
tryItOutEnabled: true,
requestInterceptor: function(request) {
// Add base URL if not present
if (request.url.startsWith('/api/')) {
request.url = 'http://localhost:3000' + request.url;
}
return request;
},
onComplete: function() {
console.log('VIP Coordinator API Documentation loaded successfully!');
},
docExpansion: 'list',
defaultModelsExpandDepth: 2,
defaultModelExpandDepth: 2,
showExtensions: true,
showCommonExtensions: true,
supportedSubmitMethods: ['get', 'post', 'put', 'delete', 'patch'],
validatorUrl: null
});
// End Swagger UI call region
window.ui = ui;
};
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

171
frontend/src/App.css Normal file
View File

@@ -0,0 +1,171 @@
/* Modern App-specific styles using Tailwind utilities */
/* Enhanced button styles */
.btn-modern {
@apply px-6 py-3 rounded-xl font-semibold text-sm transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5;
}
.btn-gradient-blue {
@apply bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 text-white;
}
.btn-gradient-green {
@apply bg-gradient-to-r from-green-500 to-green-600 hover:from-green-600 hover:to-green-700 text-white;
}
.btn-gradient-purple {
@apply bg-gradient-to-r from-purple-500 to-purple-600 hover:from-purple-600 hover:to-purple-700 text-white;
}
.btn-gradient-amber {
@apply bg-gradient-to-r from-amber-500 to-amber-600 hover:from-amber-600 hover:to-amber-700 text-white;
}
/* Status badges */
.status-badge {
@apply inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold;
}
.status-scheduled {
@apply bg-blue-100 text-blue-800 border border-blue-200;
}
.status-in-progress {
@apply bg-amber-100 text-amber-800 border border-amber-200;
}
.status-completed {
@apply bg-green-100 text-green-800 border border-green-200;
}
.status-cancelled {
@apply bg-red-100 text-red-800 border border-red-200;
}
/* Card enhancements */
.card-modern {
@apply bg-white rounded-2xl shadow-lg border border-slate-200/60 overflow-hidden backdrop-blur-sm;
}
.card-header {
@apply bg-gradient-to-r from-slate-50 to-slate-100 px-6 py-4 border-b border-slate-200/60;
}
.card-content {
@apply p-6;
}
/* Loading states */
.loading-spinner {
@apply animate-spin rounded-full border-4 border-blue-600 border-t-transparent;
}
.loading-text {
@apply text-slate-600 animate-pulse;
}
.skeleton {
@apply animate-pulse bg-slate-200 rounded;
}
/* Form enhancements */
.form-modern {
@apply space-y-6;
}
.form-group-modern {
@apply space-y-2;
}
.form-label-modern {
@apply block text-sm font-semibold text-slate-700;
}
.form-input-modern {
@apply w-full px-4 py-3 border border-slate-300 rounded-xl shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200;
}
.form-select-modern {
@apply w-full px-4 py-3 border border-slate-300 rounded-xl shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white transition-all duration-200;
}
/* Animation utilities */
.animate-fade-in {
animation: fadeIn 0.5s ease-in-out;
}
.animate-slide-up {
animation: slideUp 0.3s ease-out;
}
.animate-scale-in {
animation: scaleIn 0.2s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideUp {
from {
transform: translateY(10px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
@keyframes scaleIn {
from {
transform: scale(0.95);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
/* Responsive utilities */
@media (max-width: 768px) {
.mobile-stack {
@apply flex-col space-y-4 space-x-0;
}
.mobile-full {
@apply w-full;
}
.mobile-text-center {
@apply text-center;
}
}
/* Glass morphism effect */
.glass {
@apply bg-white/80 backdrop-blur-lg border border-white/20;
}
.glass-dark {
@apply bg-slate-900/80 backdrop-blur-lg border border-slate-700/20;
}
/* Hover effects */
.hover-lift {
@apply transition-transform duration-200 hover:-translate-y-1;
}
.hover-glow {
@apply transition-shadow duration-200 hover:shadow-2xl;
}
.hover-scale {
@apply transition-transform duration-200 hover:scale-105;
}

272
frontend/src/App.tsx Normal file
View File

@@ -0,0 +1,272 @@
import React, { useEffect, useState } from 'react';
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
import { useAuth0 } from '@auth0/auth0-react';
import { apiCall } from './config/api';
import VipList from './pages/VipList';
import VipDetails from './pages/VipDetails';
import DriverList from './pages/DriverList';
import DriverDashboard from './pages/DriverDashboard';
import Dashboard from './pages/Dashboard';
import AdminDashboard from './pages/AdminDashboard';
import UserManagement from './components/UserManagement';
import Login from './components/Login';
import './App.css';
const AUTH0_AUDIENCE = import.meta.env.VITE_AUTH0_AUDIENCE;
function App() {
const {
isLoading: authLoading,
isAuthenticated,
loginWithRedirect,
logout,
getAccessTokenSilently,
user: auth0User,
error: authError
} = useAuth0();
const [user, setUser] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [statusMessage, setStatusMessage] = useState<string | null>(null);
const [pendingApproval, setPendingApproval] = useState(false);
useEffect(() => {
const bootstrap = async () => {
if (!isAuthenticated) {
setUser(null);
setStatusMessage(null);
setPendingApproval(false);
setLoading(false);
return;
}
setLoading(true);
setPendingApproval(false);
setStatusMessage(null);
try {
const token = await getAccessTokenSilently({
authorizationParams: {
...(AUTH0_AUDIENCE ? { audience: AUTH0_AUDIENCE } : {}),
scope: 'openid profile email'
}
});
localStorage.setItem('authToken', token);
const response = await apiCall('/auth/me', {
headers: {
Authorization: `Bearer ${token}`
}
});
if (response.status === 403) {
const data = await response.json();
setUser(null);
setPendingApproval(true);
setStatusMessage(data.message || 'Your account is pending administrator approval.');
return;
}
if (!response.ok) {
throw new Error(`Failed to load profile (${response.status})`);
}
const data = await response.json();
const userRecord = data.user || data;
const resolvedName =
userRecord.name ||
auth0User?.name ||
auth0User?.nickname ||
auth0User?.email ||
userRecord.email;
setUser({
...userRecord,
name: resolvedName,
role: userRecord.role,
picture: userRecord.picture || auth0User?.picture
});
} catch (error) {
console.error('Authentication bootstrap failed:', error);
setUser(null);
setStatusMessage('Authentication failed. Please try signing in again.');
} finally {
setLoading(false);
}
};
if (!authLoading) {
bootstrap();
}
}, [isAuthenticated, authLoading, getAccessTokenSilently, auth0User]);
const handleLogout = () => {
localStorage.removeItem('authToken');
logout({ logoutParams: { returnTo: window.location.origin } });
};
if (authLoading || loading) {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 flex justify-center items-center">
<div className="bg-white rounded-2xl shadow-xl 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 VIP Coordinator...</span>
</div>
</div>
);
}
if (pendingApproval) {
return (
<div className="min-h-screen bg-gradient-to-br from-amber-50 to-rose-50 flex justify-center items-center px-4">
<div className="bg-white border border-amber-200/60 rounded-2xl shadow-xl max-w-xl w-full p-8 space-y-4 text-center">
<div className="flex justify-center">
<div className="w-16 h-16 rounded-full bg-amber-100 text-amber-600 flex items-center justify-center text-3xl">
</div>
</div>
<h1 className="text-2xl font-bold text-slate-800">Awaiting Administrator Approval</h1>
<p className="text-slate-600">
{statusMessage ||
'Thanks for signing in. An administrator needs to approve your account before you can access the dashboard.'}
</p>
<button
onClick={handleLogout}
className="btn btn-secondary mt-4"
>
Sign out
</button>
</div>
</div>
);
}
const beginLogin = async () => {
try {
await loginWithRedirect({
authorizationParams: {
...(AUTH0_AUDIENCE ? { audience: AUTH0_AUDIENCE } : {}),
scope: 'openid profile email',
redirect_uri: `${window.location.origin}/auth/callback`
}
});
} catch (error: any) {
console.error('Auth0 login failed:', error);
setStatusMessage(error?.message || 'Authentication failed. Please try again.');
}
};
if (!isAuthenticated || !user) {
return (
<Login
onLogin={beginLogin}
errorMessage={statusMessage || authError?.message}
/>
);
}
const displayName =
(user.name && user.name.trim().length > 0)
? user.name
: (user.email || 'User');
const displayInitial = displayName.trim().charAt(0).toUpperCase();
const userRole = user.role || 'user';
return (
<Router>
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-50">
<nav className="bg-white/80 backdrop-blur-lg border-b border-slate-200/60 sticky top-0 z-50">
<div className="max-w-7xl mx-auto px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
<div className="flex items-center space-x-3">
<div className="w-8 h-8 bg-gradient-to-br from-blue-600 to-indigo-600 rounded-lg flex items-center justify-center">
<span className="text-white font-bold text-sm">VC</span>
</div>
<h1 className="text-xl font-bold bg-gradient-to-r from-slate-800 to-slate-600 bg-clip-text text-transparent">
VIP Coordinator
</h1>
</div>
<div className="hidden md:flex items-center space-x-1">
<Link
to="/"
className="px-4 py-2 text-sm font-medium text-slate-700 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-all duration-200"
>
Dashboard
</Link>
<Link
to="/vips"
className="px-4 py-2 text-sm font-medium text-slate-700 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-all duration-200"
>
VIPs
</Link>
<Link
to="/drivers"
className="px-4 py-2 text-sm font-medium text-slate-700 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-all duration-200"
>
Drivers
</Link>
{userRole === 'administrator' && (
<Link
to="/admin"
className="px-4 py-2 text-sm font-medium text-slate-700 hover:text-amber-600 hover:bg-amber-50 rounded-lg transition-all duration-200"
>
Admin
</Link>
)}
{userRole === 'administrator' && (
<Link
to="/users"
className="px-4 py-2 text-sm font-medium text-slate-700 hover:text-purple-600 hover:bg-purple-50 rounded-lg transition-all duration-200"
>
Users
</Link>
)}
</div>
<div className="flex items-center space-x-4">
<div className="hidden sm:flex items-center space-x-3">
<div className="w-8 h-8 bg-gradient-to-br from-slate-400 to-slate-600 rounded-full flex items-center justify-center overflow-hidden">
{user.picture ? (
<img src={user.picture} alt={displayName} className="w-8 h-8 object-cover" />
) : (
<span className="text-white text-xs font-medium">
{displayInitial}
</span>
)}
</div>
<div className="text-sm">
<div className="font-medium text-slate-900">{displayName}</div>
<div className="text-slate-500 capitalize">{userRole}</div>
</div>
</div>
<button
onClick={handleLogout}
className="bg-gradient-to-r from-red-500 to-red-600 hover:from-red-600 hover:to-red-700 text-white px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 shadow-lg hover:shadow-xl"
>
Logout
</button>
</div>
</div>
</div>
</nav>
<main className="max-w-7xl mx-auto px-6 lg:px-8 py-8">
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/vips" element={<VipList />} />
<Route path="/vips/:id" element={<VipDetails />} />
<Route path="/drivers" element={<DriverList />} />
<Route path="/drivers/:driverId" element={<DriverDashboard />} />
<Route path="/admin" element={<AdminDashboard />} />
<Route path="/users" element={<UserManagement currentUser={user} />} />
</Routes>
</main>
</div>
</Router>
);
}
export default App;

View File

@@ -0,0 +1,110 @@
import React, { useState } from 'react';
interface DriverFormData {
name: string;
phone: string;
vehicleCapacity: number;
}
interface DriverFormProps {
onSubmit: (driverData: DriverFormData) => void;
onCancel: () => void;
}
const DriverForm: React.FC<DriverFormProps> = ({ onSubmit, onCancel }) => {
const [formData, setFormData] = useState<DriverFormData>({
name: '',
phone: '',
vehicleCapacity: 4
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSubmit(formData);
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value, type } = e.target;
setFormData(prev => ({
...prev,
[name]: type === 'number' || name === 'vehicleCapacity' ? parseInt(value) || 0 : value
}));
};
return (
<div className="modal-overlay">
<div className="modal-content">
{/* Modal Header */}
<div className="modal-header">
<h2 className="text-2xl font-bold text-slate-800">Add New Driver</h2>
<p className="text-slate-600 mt-2">Enter driver contact information</p>
</div>
{/* Modal Body */}
<div className="modal-body">
<form onSubmit={handleSubmit} className="space-y-6">
<div className="form-group">
<label htmlFor="name" className="form-label">Driver Name *</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
className="form-input"
placeholder="Enter driver's full name"
required
/>
</div>
<div className="form-group">
<label htmlFor="phone" className="form-label">Phone Number *</label>
<input
type="tel"
id="phone"
name="phone"
value={formData.phone}
onChange={handleChange}
className="form-input"
placeholder="Enter phone number"
required
/>
</div>
<div className="form-group">
<label htmlFor="vehicleCapacity" className="form-label">Vehicle Capacity *</label>
<select
id="vehicleCapacity"
name="vehicleCapacity"
value={formData.vehicleCapacity}
onChange={handleChange}
className="form-input"
required
>
<option value={2}>2 passengers (Sedan/Coupe)</option>
<option value={4}>4 passengers (Standard Car)</option>
<option value={6}>6 passengers (SUV/Van)</option>
<option value={8}>8 passengers (Large Van)</option>
<option value={12}>12 passengers (Mini Bus)</option>
</select>
<p className="text-sm text-slate-600 mt-1">
🚗 Select the maximum number of passengers this vehicle can accommodate
</p>
</div>
<div className="form-actions">
<button type="button" className="btn btn-secondary" onClick={onCancel}>
Cancel
</button>
<button type="submit" className="btn btn-primary">
Add Driver
</button>
</div>
</form>
</div>
</div>
</div>
);
};
export default DriverForm;

View File

@@ -0,0 +1,368 @@
import React, { useState, useEffect } from 'react';
import { apiCall } from '../config/api';
interface DriverAvailability {
driverId: string;
driverName: string;
vehicleCapacity: number;
status: 'available' | 'scheduled' | 'overlapping' | 'tight_turnaround';
assignmentCount: number;
conflicts: ConflictInfo[];
currentAssignments: ScheduleEvent[];
}
interface ConflictInfo {
type: 'overlap' | 'tight_turnaround' | 'back_to_back';
severity: 'low' | 'medium' | 'high';
message: string;
conflictingEvent: ScheduleEvent;
timeDifference?: number;
}
interface ScheduleEvent {
id: string;
title: string;
location: string;
startTime: string;
endTime: string;
assignedDriverId?: string;
vipId: string;
vipName: string;
}
interface DriverSelectorProps {
selectedDriverId: string;
onDriverSelect: (driverId: string) => void;
eventTime: {
startTime: string;
endTime: string;
location: string;
};
}
const DriverSelector: React.FC<DriverSelectorProps> = ({
selectedDriverId,
onDriverSelect,
eventTime
}) => {
const [availability, setAvailability] = useState<DriverAvailability[]>([]);
const [loading, setLoading] = useState(false);
const [showConflictModal, setShowConflictModal] = useState(false);
const [selectedDriver, setSelectedDriver] = useState<DriverAvailability | null>(null);
useEffect(() => {
if (eventTime.startTime && eventTime.endTime) {
checkDriverAvailability();
}
}, [eventTime.startTime, eventTime.endTime, eventTime.location]);
const checkDriverAvailability = async () => {
setLoading(true);
try {
const token = localStorage.getItem('authToken');
const response = await apiCall('/api/drivers/availability', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(eventTime),
});
if (response.ok) {
const data = await response.json();
setAvailability(data);
}
} catch (error) {
console.error('Error checking driver availability:', error);
} finally {
setLoading(false);
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'available': return '🟢';
case 'scheduled': return '🟡';
case 'tight_turnaround': return '⚡';
case 'overlapping': return '🔴';
default: return '⚪';
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'available': return 'bg-green-50 border-green-200 text-green-800';
case 'scheduled': return 'bg-amber-50 border-amber-200 text-amber-800';
case 'tight_turnaround': return 'bg-orange-50 border-orange-200 text-orange-800';
case 'overlapping': return 'bg-red-50 border-red-200 text-red-800';
default: return 'bg-slate-50 border-slate-200 text-slate-800';
}
};
const getStatusText = (status: string) => {
switch (status) {
case 'available': return 'Available';
case 'scheduled': return 'Busy';
case 'tight_turnaround': return 'Tight Schedule';
case 'overlapping': return 'Conflict';
default: return 'Unknown';
}
};
const handleDriverClick = (driver: DriverAvailability) => {
if (driver.conflicts.length > 0) {
setSelectedDriver(driver);
setShowConflictModal(true);
} else {
onDriverSelect(driver.driverId);
}
};
const confirmDriverAssignment = () => {
if (selectedDriver) {
onDriverSelect(selectedDriver.driverId);
setShowConflictModal(false);
setSelectedDriver(null);
}
};
const formatTime = (timeString: string) => {
return new Date(timeString).toLocaleString([], {
hour: '2-digit',
minute: '2-digit'
});
};
if (loading) {
return (
<div className="bg-slate-50 rounded-xl p-6 border border-slate-200/60">
<div className="flex items-center gap-3">
<div className="w-6 h-6 border-2 border-blue-600 border-t-transparent rounded-full animate-spin"></div>
<span className="text-slate-700 font-medium">Checking driver availability...</span>
</div>
</div>
);
}
return (
<div className="bg-slate-50 rounded-xl p-6 border border-slate-200/60">
<h3 className="text-lg font-bold text-slate-800 mb-4 flex items-center gap-2">
🚗 Assign Driver
</h3>
{availability.length === 0 ? (
<div className="text-center py-8">
<div className="w-12 h-12 bg-slate-200 rounded-full flex items-center justify-center mx-auto mb-3">
<span className="text-xl">🚗</span>
</div>
<p className="text-slate-500 font-medium">No drivers available</p>
<p className="text-slate-400 text-sm">Check the time and try again</p>
</div>
) : (
<div className="space-y-3">
{availability.map((driver) => (
<div
key={driver.driverId}
className={`relative rounded-xl border-2 p-4 cursor-pointer transition-all duration-200 hover:shadow-lg ${
selectedDriverId === driver.driverId
? 'border-blue-500 bg-blue-50 shadow-lg'
: 'border-slate-200 bg-white hover:border-slate-300'
}`}
onClick={() => handleDriverClick(driver)}
>
{selectedDriverId === driver.driverId && (
<div className="absolute top-2 right-2 w-6 h-6 bg-blue-500 rounded-full flex items-center justify-center">
<span className="text-white text-xs font-bold"></span>
</div>
)}
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<span className="text-xl">{getStatusIcon(driver.status)}</span>
<div>
<h4 className="font-bold text-slate-900">{driver.driverName}</h4>
<div className="flex items-center gap-2 mt-1">
<span className={`px-2 py-1 rounded-full text-xs font-medium border ${getStatusColor(driver.status)}`}>
{getStatusText(driver.status)}
</span>
<span className="bg-slate-100 text-slate-700 px-2 py-1 rounded-full text-xs font-medium">
🚗 {driver.vehicleCapacity} seats
</span>
<span className="bg-blue-100 text-blue-800 px-2 py-1 rounded-full text-xs font-medium">
{driver.assignmentCount} assignments
</span>
</div>
</div>
</div>
{driver.conflicts.length > 0 && (
<div className="space-y-2 mb-3">
{driver.conflicts.map((conflict, index) => (
<div key={index} className={`p-3 rounded-lg border ${
conflict.severity === 'high'
? 'bg-red-50 border-red-200'
: 'bg-amber-50 border-amber-200'
}`}>
<div className="flex items-center gap-2 mb-1">
<span className="text-sm">
{conflict.type === 'overlap' ? '🔴' : '⚡'}
</span>
<span className={`text-sm font-medium ${
conflict.severity === 'high' ? 'text-red-800' : 'text-amber-800'
}`}>
{conflict.type === 'overlap' ? 'Time Overlap' : 'Tight Turnaround'}
</span>
</div>
<p className={`text-sm ${
conflict.severity === 'high' ? 'text-red-700' : 'text-amber-700'
}`}>
{conflict.message}
</p>
</div>
))}
</div>
)}
{driver.currentAssignments.length > 0 && driver.conflicts.length === 0 && (
<div className="bg-slate-100 rounded-lg p-3">
<p className="text-sm font-medium text-slate-700 mb-1">Next Assignment:</p>
<p className="text-sm text-slate-600">
{driver.currentAssignments[0]?.title} at {formatTime(driver.currentAssignments[0]?.startTime)}
</p>
</div>
)}
</div>
{driver.conflicts.length > 0 && (
<div className="ml-4">
<span className="bg-amber-100 text-amber-800 px-3 py-1 rounded-full text-xs font-bold">
CONFLICTS
</span>
</div>
)}
</div>
</div>
))}
{selectedDriverId && (
<button
onClick={() => onDriverSelect('')}
className="w-full bg-slate-100 hover:bg-slate-200 text-slate-700 px-4 py-3 rounded-lg font-medium transition-colors border border-slate-200"
>
Clear Driver Assignment
</button>
)}
</div>
)}
{/* Conflict Resolution Modal */}
{showConflictModal && selectedDriver && (
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl shadow-2xl border border-slate-200/60 w-full max-w-2xl max-h-[90vh] overflow-y-auto">
<div className="bg-gradient-to-r from-amber-50 to-orange-50 px-8 py-6 border-b border-slate-200/60">
<h3 className="text-xl font-bold text-slate-800 flex items-center gap-2">
Driver Assignment Conflict
</h3>
<p className="text-slate-600 mt-1">
<strong>{selectedDriver.driverName}</strong> has scheduling conflicts that need your attention
</p>
</div>
<div className="p-8 space-y-6">
{/* Driver Info */}
<div className="bg-slate-50 rounded-xl p-4 border border-slate-200">
<div className="flex items-center gap-3 mb-2">
<span className="text-2xl">🚗</span>
<div>
<h4 className="font-bold text-slate-900">{selectedDriver.driverName}</h4>
<p className="text-sm text-slate-600">
Vehicle Capacity: {selectedDriver.vehicleCapacity} passengers
Current Assignments: {selectedDriver.assignmentCount}
</p>
</div>
</div>
</div>
{/* Conflicts */}
<div>
<h4 className="font-bold text-slate-800 mb-3">Scheduling Conflicts:</h4>
<div className="space-y-3">
{selectedDriver.conflicts.map((conflict, index) => (
<div key={index} className={`p-4 rounded-xl border ${
conflict.severity === 'high'
? 'bg-red-50 border-red-200'
: 'bg-amber-50 border-amber-200'
}`}>
<div className="flex items-center gap-2 mb-2">
<span className="text-lg">
{conflict.type === 'overlap' ? '🔴' : '⚡'}
</span>
<span className={`font-bold ${
conflict.severity === 'high' ? 'text-red-800' : 'text-amber-800'
}`}>
{conflict.type === 'overlap' ? 'Time Overlap' : 'Tight Turnaround'}
</span>
</div>
<p className={`mb-2 ${
conflict.severity === 'high' ? 'text-red-700' : 'text-amber-700'
}`}>
{conflict.message}
</p>
<div className="text-sm text-slate-600 bg-white/50 rounded-lg p-2">
<strong>Conflicting event:</strong> {conflict.conflictingEvent.title}<br/>
<strong>Time:</strong> {formatTime(conflict.conflictingEvent.startTime)} - {formatTime(conflict.conflictingEvent.endTime)}<br/>
<strong>VIP:</strong> {conflict.conflictingEvent.vipName}
</div>
</div>
))}
</div>
</div>
{/* Current Schedule */}
<div>
<h4 className="font-bold text-slate-800 mb-3">Current Schedule:</h4>
<div className="bg-slate-50 rounded-xl p-4 border border-slate-200">
{selectedDriver.currentAssignments.length === 0 ? (
<p className="text-slate-500 text-sm">No current assignments</p>
) : (
<div className="space-y-2">
{selectedDriver.currentAssignments.map((assignment, index) => (
<div key={index} className="flex items-center gap-2 text-sm">
<span className="w-2 h-2 bg-blue-500 rounded-full"></span>
<span className="font-medium">{assignment.title}</span>
<span className="text-slate-500">
({formatTime(assignment.startTime)} - {formatTime(assignment.endTime)})
</span>
<span className="text-slate-400"> {assignment.vipName}</span>
</div>
))}
</div>
)}
</div>
</div>
</div>
<div className="flex justify-end gap-4 p-8 border-t border-slate-200">
<button
className="px-6 py-3 border border-slate-300 text-slate-700 rounded-lg hover:bg-slate-50 transition-colors font-medium"
onClick={() => setShowConflictModal(false)}
>
Choose Different Driver
</button>
<button
className="bg-gradient-to-r from-red-500 to-red-600 hover:from-red-600 hover:to-red-700 text-white px-6 py-3 rounded-lg font-medium transition-all duration-200 shadow-lg hover:shadow-xl"
onClick={confirmDriverAssignment}
>
Assign Anyway
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default DriverSelector;

View File

@@ -0,0 +1,188 @@
import React, { useState } from 'react';
interface Driver {
id: string;
name: string;
phone: string;
currentLocation: { lat: number; lng: number };
assignedVipIds: string[];
vehicleCapacity?: number;
}
interface EditDriverFormProps {
driver: Driver;
onSubmit: (driverData: any) => void;
onCancel: () => void;
}
const EditDriverForm: React.FC<EditDriverFormProps> = ({ driver, onSubmit, onCancel }) => {
const [formData, setFormData] = useState({
name: driver.name,
phone: driver.phone,
vehicleCapacity: driver.vehicleCapacity || 4,
currentLocation: {
lat: driver.currentLocation.lat,
lng: driver.currentLocation.lng
}
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSubmit({
...formData,
id: driver.id
});
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value, type } = e.target;
if (name === 'lat' || name === 'lng') {
setFormData(prev => ({
...prev,
currentLocation: {
...prev.currentLocation,
[name]: parseFloat(value) || 0
}
}));
} else if (name === 'vehicleCapacity') {
setFormData(prev => ({ ...prev, [name]: parseInt(value) || 0 }));
} else {
setFormData(prev => ({ ...prev, [name]: value }));
}
};
return (
<div className="modal-overlay">
<div className="modal-content">
{/* Modal Header */}
<div className="modal-header">
<h2 className="text-2xl font-bold text-slate-800">Edit Driver</h2>
<p className="text-slate-600 mt-2">Update driver information for {driver.name}</p>
</div>
{/* Modal Body */}
<div className="modal-body">
<form onSubmit={handleSubmit} className="space-y-8">
{/* Basic Information Section */}
<div className="form-section">
<div className="form-section-header">
<h3 className="form-section-title">Basic Information</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="form-group">
<label htmlFor="name" className="form-label">Driver Name *</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
className="form-input"
placeholder="Enter driver's full name"
required
/>
</div>
<div className="form-group">
<label htmlFor="phone" className="form-label">Phone Number *</label>
<input
type="tel"
id="phone"
name="phone"
value={formData.phone}
onChange={handleChange}
className="form-input"
placeholder="Enter phone number"
required
/>
</div>
</div>
<div className="form-group">
<label htmlFor="vehicleCapacity" className="form-label">Vehicle Capacity *</label>
<select
id="vehicleCapacity"
name="vehicleCapacity"
value={formData.vehicleCapacity}
onChange={handleChange}
className="form-input"
required
>
<option value={2}>2 passengers (Sedan/Coupe)</option>
<option value={4}>4 passengers (Standard Car)</option>
<option value={6}>6 passengers (SUV/Van)</option>
<option value={8}>8 passengers (Large Van)</option>
<option value={12}>12 passengers (Mini Bus)</option>
</select>
<p className="text-sm text-slate-600 mt-1">
🚗 Select the maximum number of passengers this vehicle can accommodate
</p>
</div>
</div>
{/* Location Section */}
<div className="form-section">
<div className="form-section-header">
<h3 className="form-section-title">Current Location</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="form-group">
<label htmlFor="lat" className="form-label">Latitude *</label>
<input
type="number"
id="lat"
name="lat"
value={formData.currentLocation.lat}
onChange={handleChange}
className="form-input"
placeholder="Enter latitude"
step="any"
required
/>
</div>
<div className="form-group">
<label htmlFor="lng" className="form-label">Longitude *</label>
<input
type="number"
id="lng"
name="lng"
value={formData.currentLocation.lng}
onChange={handleChange}
className="form-input"
placeholder="Enter longitude"
step="any"
required
/>
</div>
</div>
<div className="mt-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
<p className="text-sm text-blue-700">
<strong>Current coordinates:</strong> {formData.currentLocation.lat.toFixed(6)}, {formData.currentLocation.lng.toFixed(6)}
</p>
<p className="text-xs text-blue-600 mt-1">
You can use GPS coordinates or get them from a mapping service
</p>
</div>
</div>
<div className="form-actions">
<button type="button" className="btn btn-secondary" onClick={onCancel}>
Cancel
</button>
<button type="submit" className="btn btn-primary">
Update Driver
</button>
</div>
</form>
</div>
</div>
</div>
);
};
export default EditDriverForm;

View File

@@ -0,0 +1,550 @@
import React, { useState } from 'react';
interface Flight {
flightNumber: string;
flightDate: string;
segment: number;
validated?: boolean;
validationData?: any;
}
interface VipData {
id: string;
name: string;
organization: string;
transportMode: 'flight' | 'self-driving';
flightNumber?: string; // Legacy
flightDate?: string; // Legacy
flights?: Flight[]; // New
expectedArrival?: string;
arrivalTime?: string;
needsAirportPickup?: boolean;
needsVenueTransport: boolean;
notes: string;
}
interface EditVipFormProps {
vip: VipData;
onSubmit: (vipData: VipData) => void;
onCancel: () => void;
}
const EditVipForm: React.FC<EditVipFormProps> = ({ vip, onSubmit, onCancel }) => {
// Convert legacy single flight to new format if needed
const initialFlights = vip.flights || (vip.flightNumber ? [{
flightNumber: vip.flightNumber,
flightDate: vip.flightDate || '',
segment: 1
}] : [{ flightNumber: '', flightDate: '', segment: 1 }]);
const [formData, setFormData] = useState<VipData>({
id: vip.id,
name: vip.name,
organization: vip.organization,
transportMode: vip.transportMode || 'flight',
flights: initialFlights,
expectedArrival: vip.expectedArrival ? vip.expectedArrival.slice(0, 16) : '',
arrivalTime: vip.arrivalTime ? vip.arrivalTime.slice(0, 16) : '',
needsAirportPickup: vip.needsAirportPickup !== false,
needsVenueTransport: vip.needsVenueTransport !== false,
notes: vip.notes || ''
});
const [flightValidating, setFlightValidating] = useState<{ [key: number]: boolean }>({});
const [flightErrors, setFlightErrors] = useState<{ [key: number]: string }>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
try {
// Only include flights with flight numbers
const validFlights = formData.flights?.filter(f => f.flightNumber) || [];
await onSubmit({
...formData,
flights: validFlights.length > 0 ? validFlights : undefined
});
} catch (error) {
console.error('Error updating VIP:', error);
} finally {
setIsSubmitting(false);
}
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
const { name, value, type } = e.target;
if (type === 'checkbox') {
const checked = (e.target as HTMLInputElement).checked;
setFormData(prev => ({
...prev,
[name]: checked
}));
} else {
setFormData(prev => ({
...prev,
[name]: value
}));
}
};
const handleTransportModeChange = (mode: 'flight' | 'self-driving') => {
setFormData(prev => ({
...prev,
transportMode: mode,
flights: mode === 'flight' ? (prev.flights || [{ flightNumber: '', flightDate: '', segment: 1 }]) : undefined,
expectedArrival: mode === 'self-driving' ? prev.expectedArrival : '',
needsAirportPickup: mode === 'flight' ? true : false
}));
// Clear flight errors when switching away from flight mode
if (mode !== 'flight') {
setFlightErrors({});
}
};
const handleFlightChange = (index: number, field: 'flightNumber' | 'flightDate', value: string) => {
setFormData(prev => ({
...prev,
flights: prev.flights?.map((flight, i) =>
i === index ? { ...flight, [field]: value, validated: false } : flight
) || []
}));
// Clear validation for this flight when it changes
setFlightErrors(prev => ({ ...prev, [index]: '' }));
};
const addConnectingFlight = () => {
const currentFlights = formData.flights || [];
if (currentFlights.length < 3) {
setFormData(prev => ({
...prev,
flights: [...currentFlights, {
flightNumber: '',
flightDate: currentFlights[currentFlights.length - 1]?.flightDate || '',
segment: currentFlights.length + 1
}]
}));
}
};
const removeConnectingFlight = (index: number) => {
setFormData(prev => ({
...prev,
flights: prev.flights?.filter((_, i) => i !== index).map((flight, i) => ({
...flight,
segment: i + 1
})) || []
}));
// Clear errors for removed flight
setFlightErrors(prev => {
const newErrors = { ...prev };
delete newErrors[index];
return newErrors;
});
};
const validateFlight = async (index: number) => {
const flight = formData.flights?.[index];
if (!flight || !flight.flightNumber || !flight.flightDate) {
setFlightErrors(prev => ({ ...prev, [index]: 'Please enter flight number and date' }));
return;
}
setFlightValidating(prev => ({ ...prev, [index]: true }));
setFlightErrors(prev => ({ ...prev, [index]: '' }));
try {
const url = `/api/flights/${flight.flightNumber}?date=${flight.flightDate}`;
const response = await fetch(url);
if (response.ok) {
const data = await response.json();
// Update flight with validation data
setFormData(prev => ({
...prev,
flights: prev.flights?.map((f, i) =>
i === index ? { ...f, validated: true, validationData: data } : f
) || []
}));
setFlightErrors(prev => ({ ...prev, [index]: '' }));
} else {
const errorData = await response.json();
setFlightErrors(prev => ({
...prev,
[index]: errorData.error || 'Invalid flight number'
}));
}
} catch (error) {
setFlightErrors(prev => ({
...prev,
[index]: 'Error validating flight'
}));
} finally {
setFlightValidating(prev => ({ ...prev, [index]: false }));
}
};
const formatFlightTime = (timeString: string) => {
if (!timeString) return '';
return new Date(timeString).toLocaleString([], {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
return (
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl shadow-2xl border border-slate-200/60 w-full max-w-4xl max-h-[90vh] overflow-y-auto">
<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-2xl font-bold text-slate-800 flex items-center gap-2">
Edit VIP: {vip.name}
</h2>
<p className="text-slate-600 mt-1">Update VIP information and travel arrangements</p>
</div>
<form onSubmit={handleSubmit} className="p-8 space-y-8">
{/* Basic Information */}
<div className="bg-slate-50 rounded-xl p-6 border border-slate-200/60">
<h3 className="text-lg font-bold text-slate-800 mb-4 flex items-center gap-2">
👤 Basic Information
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label htmlFor="name" className="block text-sm font-medium text-slate-700 mb-2">
Full Name
</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
className="w-full px-4 py-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
placeholder="Enter full name"
required
/>
</div>
<div>
<label htmlFor="organization" className="block text-sm font-medium text-slate-700 mb-2">
Organization
</label>
<input
type="text"
id="organization"
name="organization"
value={formData.organization}
onChange={handleChange}
className="w-full px-4 py-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
placeholder="Enter organization"
required
/>
</div>
</div>
</div>
{/* Transportation Mode */}
<div className="bg-slate-50 rounded-xl p-6 border border-slate-200/60">
<h3 className="text-lg font-bold text-slate-800 mb-4 flex items-center gap-2">
🚗 Transportation
</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-3">
How are you arriving?
</label>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<label className={`relative flex items-center p-4 border-2 rounded-xl cursor-pointer transition-all duration-200 ${
formData.transportMode === 'flight'
? 'border-blue-500 bg-blue-50'
: 'border-slate-300 bg-white hover:border-slate-400'
}`}>
<input
type="radio"
name="transportMode"
value="flight"
checked={formData.transportMode === 'flight'}
onChange={() => handleTransportModeChange('flight')}
className="sr-only"
/>
<div className="flex items-center gap-3">
<span className="text-2xl"></span>
<div>
<div className="font-semibold text-slate-900">Arriving by Flight</div>
<div className="text-sm text-slate-600">Commercial airline travel</div>
</div>
</div>
{formData.transportMode === 'flight' && (
<div className="absolute top-2 right-2 w-5 h-5 bg-blue-500 rounded-full flex items-center justify-center">
<span className="text-white text-xs"></span>
</div>
)}
</label>
<label className={`relative flex items-center p-4 border-2 rounded-xl cursor-pointer transition-all duration-200 ${
formData.transportMode === 'self-driving'
? 'border-green-500 bg-green-50'
: 'border-slate-300 bg-white hover:border-slate-400'
}`}>
<input
type="radio"
name="transportMode"
value="self-driving"
checked={formData.transportMode === 'self-driving'}
onChange={() => handleTransportModeChange('self-driving')}
className="sr-only"
/>
<div className="flex items-center gap-3">
<span className="text-2xl">🚗</span>
<div>
<div className="font-semibold text-slate-900">Self-Driving</div>
<div className="text-sm text-slate-600">Personal vehicle</div>
</div>
</div>
{formData.transportMode === 'self-driving' && (
<div className="absolute top-2 right-2 w-5 h-5 bg-green-500 rounded-full flex items-center justify-center">
<span className="text-white text-xs"></span>
</div>
)}
</label>
</div>
</div>
</div>
</div>
{/* Flight Information */}
{formData.transportMode === 'flight' && formData.flights && (
<div className="bg-blue-50 rounded-xl p-6 border border-blue-200/60">
<h3 className="text-lg font-bold text-slate-800 mb-4 flex items-center gap-2">
Flight Information
</h3>
<div className="space-y-6">
{formData.flights.map((flight, index) => (
<div key={index} className="bg-white rounded-xl border border-blue-200 p-6">
<div className="flex justify-between items-center mb-4">
<h4 className="text-lg font-semibold text-slate-900 flex items-center gap-2">
{index === 0 ? (
<> Primary Flight</>
) : (
<>🔄 Connecting Flight {index}</>
)}
</h4>
{index > 0 && (
<button
type="button"
onClick={() => removeConnectingFlight(index)}
className="text-red-600 hover:text-red-700 font-medium text-sm flex items-center gap-1"
>
Remove
</button>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div>
<label htmlFor={`flightNumber-${index}`} className="block text-sm font-medium text-slate-700 mb-2">
Flight Number
</label>
<input
type="text"
id={`flightNumber-${index}`}
value={flight.flightNumber}
onChange={(e) => handleFlightChange(index, 'flightNumber', e.target.value)}
className="w-full px-4 py-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
placeholder="e.g., AA123"
required={index === 0}
/>
</div>
<div>
<label htmlFor={`flightDate-${index}`} className="block text-sm font-medium text-slate-700 mb-2">
Flight Date
</label>
<input
type="date"
id={`flightDate-${index}`}
value={flight.flightDate}
onChange={(e) => handleFlightChange(index, 'flightDate', e.target.value)}
className="w-full px-4 py-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
required={index === 0}
min={new Date().toISOString().split('T')[0]}
/>
</div>
</div>
<button
type="button"
onClick={() => validateFlight(index)}
disabled={flightValidating[index] || !flight.flightNumber || !flight.flightDate}
className="w-full bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 disabled:from-slate-400 disabled:to-slate-500 text-white px-4 py-3 rounded-lg font-medium transition-all duration-200 shadow-lg hover:shadow-xl disabled:cursor-not-allowed"
>
{flightValidating[index] ? (
<span className="flex items-center justify-center gap-2">
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
🔍 Validating...
</span>
) : (
'🔍 Validate Flight'
)}
</button>
{/* Flight Validation Results */}
{flightErrors[index] && (
<div className="mt-4 bg-red-50 border border-red-200 rounded-xl p-4">
<div className="text-red-800 font-medium flex items-center gap-2">
{flightErrors[index]}
</div>
</div>
)}
{flight.validated && flight.validationData && (
<div className="mt-4 bg-green-50 border border-green-200 rounded-xl p-4">
<div className="text-green-800 font-medium flex items-center gap-2 mb-2">
Valid Flight: {flight.validationData.airline || 'Flight'} - {flight.validationData.departure?.airport} {flight.validationData.arrival?.airport}
</div>
{flight.validationData.flightDate !== flight.flightDate && (
<div className="text-green-700 text-sm">
Live tracking starts 4 hours before departure on {new Date(flight.flightDate).toLocaleDateString()}
</div>
)}
</div>
)}
</div>
))}
{formData.flights.length < 3 && (
<button
type="button"
onClick={addConnectingFlight}
className="w-full bg-gradient-to-r from-slate-500 to-slate-600 hover:from-slate-600 hover:to-slate-700 text-white px-4 py-3 rounded-lg font-medium transition-all duration-200 shadow-lg hover:shadow-xl"
>
+ Add Connecting Flight
</button>
)}
<div className="bg-white rounded-xl border border-blue-200 p-4">
<label className="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
name="needsAirportPickup"
checked={formData.needsAirportPickup || false}
onChange={handleChange}
className="w-5 h-5 text-blue-600 border-slate-300 rounded focus:ring-blue-500"
/>
<div>
<div className="font-medium text-slate-900"> Needs Airport Pickup</div>
<div className="text-sm text-slate-600">Pickup from final destination airport</div>
</div>
</label>
</div>
</div>
</div>
)}
{/* Self-Driving Information */}
{formData.transportMode === 'self-driving' && (
<div className="bg-green-50 rounded-xl p-6 border border-green-200/60">
<h3 className="text-lg font-bold text-slate-800 mb-4 flex items-center gap-2">
🚗 Arrival Information
</h3>
<div>
<label htmlFor="expectedArrival" className="block text-sm font-medium text-slate-700 mb-2">
Expected Arrival Time
</label>
<input
type="datetime-local"
id="expectedArrival"
name="expectedArrival"
value={formData.expectedArrival}
onChange={handleChange}
className="w-full px-4 py-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-colors"
required
/>
</div>
</div>
)}
{/* Transportation Options */}
<div className="bg-slate-50 rounded-xl p-6 border border-slate-200/60">
<h3 className="text-lg font-bold text-slate-800 mb-4 flex items-center gap-2">
🚐 Transportation Options
</h3>
<div className="bg-white rounded-xl border border-slate-200 p-4">
<label className="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
name="needsVenueTransport"
checked={formData.needsVenueTransport}
onChange={handleChange}
className="w-5 h-5 text-blue-600 border-slate-300 rounded focus:ring-blue-500"
/>
<div>
<div className="font-medium text-slate-900">🚐 Needs Transportation Between Venues</div>
<div className="text-sm text-slate-600">Check this if the VIP needs rides between different event locations</div>
</div>
</label>
</div>
</div>
{/* Additional Notes */}
<div className="bg-slate-50 rounded-xl p-6 border border-slate-200/60">
<h3 className="text-lg font-bold text-slate-800 mb-4 flex items-center gap-2">
📝 Additional Notes
</h3>
<div>
<label htmlFor="notes" className="block text-sm font-medium text-slate-700 mb-2">
Special Requirements
</label>
<textarea
id="notes"
name="notes"
value={formData.notes}
onChange={handleChange}
rows={4}
className="w-full px-4 py-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
placeholder="Special requirements, dietary restrictions, accessibility needs, security details, etc."
/>
</div>
</div>
{/* Form Actions */}
<div className="flex justify-end gap-4 pt-6 border-t border-slate-200">
<button
type="button"
className="px-6 py-3 border border-slate-300 text-slate-700 rounded-lg hover:bg-slate-50 transition-colors font-medium"
onClick={onCancel}
disabled={isSubmitting}
>
Cancel
</button>
<button
type="submit"
className="bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 text-white px-6 py-3 rounded-lg font-medium transition-all duration-200 shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed"
disabled={isSubmitting}
>
{isSubmitting ? (
<span className="flex items-center gap-2">
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
Updating VIP...
</span>
) : (
'✏️ Update VIP'
)}
</button>
</div>
</form>
</div>
</div>
);
};
export default EditVipForm;

View File

@@ -0,0 +1,149 @@
import React, { useState, useEffect } from 'react';
interface FlightData {
flightNumber: string;
status: string;
departure: {
airport: string;
scheduled: string;
estimated?: string;
actual?: string;
};
arrival: {
airport: string;
scheduled: string;
estimated?: string;
actual?: string;
};
delay?: number;
gate?: string;
}
interface FlightStatusProps {
flightNumber: string;
flightDate?: string;
}
const FlightStatus: React.FC<FlightStatusProps> = ({ flightNumber, flightDate }) => {
const [flightData, setFlightData] = useState<FlightData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchFlightData = async () => {
try {
setLoading(true);
const url = flightDate
? `/api/flights/${flightNumber}?date=${flightDate}`
: `/api/flights/${flightNumber}`;
const response = await fetch(url);
if (response.ok) {
const data = await response.json();
setFlightData(data);
setError(null);
} else {
setError('Flight not found');
}
} catch (err) {
setError('Failed to fetch flight data');
} finally {
setLoading(false);
}
};
if (flightNumber) {
fetchFlightData();
// Auto-refresh every 5 minutes
const interval = setInterval(fetchFlightData, 5 * 60 * 1000);
return () => clearInterval(interval);
}
}, [flightNumber, flightDate]);
if (loading) {
return <div className="flight-status loading">Loading flight data...</div>;
}
if (error) {
return <div className="flight-status error"> {error}</div>;
}
if (!flightData) {
return null;
}
const getStatusColor = (status: string) => {
switch (status.toLowerCase()) {
case 'active': return '#2ecc71';
case 'scheduled': return '#3498db';
case 'delayed': return '#f39c12';
case 'cancelled': return '#e74c3c';
default: return '#95a5a6';
}
};
const formatTime = (timeString: string) => {
return new Date(timeString).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit'
});
};
return (
<div className="flight-status">
<div className="flight-header">
<h4> Flight {flightData.flightNumber}</h4>
<span
className="flight-status-badge"
style={{
backgroundColor: getStatusColor(flightData.status),
color: 'white',
padding: '0.25rem 0.5rem',
borderRadius: '4px',
fontSize: '0.8rem',
textTransform: 'uppercase'
}}
>
{flightData.status}
</span>
</div>
<div className="flight-details">
<div className="flight-route">
<div className="departure">
<strong>{flightData.departure.airport}</strong>
<div>Scheduled: {formatTime(flightData.departure.scheduled)}</div>
{flightData.departure.estimated && (
<div>Estimated: {formatTime(flightData.departure.estimated)}</div>
)}
</div>
<div className="route-arrow"></div>
<div className="arrival">
<strong>{flightData.arrival.airport}</strong>
<div>Scheduled: {formatTime(flightData.arrival.scheduled)}</div>
{flightData.arrival.estimated && (
<div>Estimated: {formatTime(flightData.arrival.estimated)}</div>
)}
</div>
</div>
{flightData.delay && flightData.delay > 0 && (
<div className="delay-info" style={{ color: '#f39c12', marginTop: '0.5rem' }}>
Delayed by {flightData.delay} minutes
</div>
)}
{flightData.gate && (
<div className="gate-info" style={{ marginTop: '0.5rem' }}>
🚪 Gate: {flightData.gate}
</div>
)}
</div>
</div>
);
};
export default FlightStatus;

View File

@@ -0,0 +1,282 @@
import React from 'react';
interface GanttEvent {
id: string;
title: string;
startTime: string;
endTime: string;
type: 'transport' | 'meeting' | 'event' | 'meal' | 'accommodation';
vipName: string;
location: string;
status: 'scheduled' | 'in-progress' | 'completed' | 'cancelled';
}
interface GanttChartProps {
events: GanttEvent[];
driverName: string;
}
const GanttChart: React.FC<GanttChartProps> = ({ events, driverName }) => {
// Helper functions
const getTypeColor = (type: string) => {
switch (type) {
case 'transport': return '#3498db';
case 'meeting': return '#9b59b6';
case 'event': return '#e74c3c';
case 'meal': return '#f39c12';
case 'accommodation': return '#2ecc71';
default: return '#95a5a6';
}
};
const getStatusOpacity = (status: string) => {
switch (status) {
case 'completed': return 0.5;
case 'cancelled': return 0.3;
case 'in-progress': return 1;
case 'scheduled': return 0.8;
default: return 0.8;
}
};
const getTypeIcon = (type: string) => {
switch (type) {
case 'transport': return '🚗';
case 'meeting': return '🤝';
case 'event': return '🎉';
case 'meal': return '🍽️';
case 'accommodation': return '🏨';
default: return '📅';
}
};
const formatTime = (timeString: string) => {
return new Date(timeString).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit'
});
};
// Calculate time range for the chart
const getTimeRange = () => {
if (events.length === 0) return { start: new Date(), end: new Date() };
const times = events.flatMap(event => [
new Date(event.startTime),
new Date(event.endTime)
]);
const minTime = new Date(Math.min(...times.map(t => t.getTime())));
const maxTime = new Date(Math.max(...times.map(t => t.getTime())));
// Add padding (30 minutes before and after)
const startTime = new Date(minTime.getTime() - 30 * 60 * 1000);
const endTime = new Date(maxTime.getTime() + 30 * 60 * 1000);
return { start: startTime, end: endTime };
};
// Calculate position and width for each event
const calculateEventPosition = (event: GanttEvent, timeRange: { start: Date; end: Date }) => {
const totalDuration = timeRange.end.getTime() - timeRange.start.getTime();
const eventStart = new Date(event.startTime);
const eventEnd = new Date(event.endTime);
const startOffset = eventStart.getTime() - timeRange.start.getTime();
const eventDuration = eventEnd.getTime() - eventStart.getTime();
const leftPercent = (startOffset / totalDuration) * 100;
const widthPercent = (eventDuration / totalDuration) * 100;
return { left: leftPercent, width: widthPercent };
};
// Generate time labels
const generateTimeLabels = (timeRange: { start: Date; end: Date }) => {
const labels = [];
const current = new Date(timeRange.start);
current.setMinutes(0, 0, 0); // Round to nearest hour
while (current <= timeRange.end) {
labels.push(new Date(current));
current.setHours(current.getHours() + 1);
}
return labels;
};
if (events.length === 0) {
return (
<div className="card">
<h3>📊 Schedule Gantt Chart</h3>
<p>No events to display in Gantt chart.</p>
</div>
);
}
const timeRange = getTimeRange();
const timeLabels = generateTimeLabels(timeRange);
const totalDuration = timeRange.end.getTime() - timeRange.start.getTime();
return (
<div className="card">
<h3>📊 Schedule Gantt Chart - {driverName}</h3>
<div style={{ marginBottom: '1rem', fontSize: '0.9rem', color: '#666' }}>
Timeline: {timeRange.start.toLocaleDateString()} {formatTime(timeRange.start.toISOString())} - {formatTime(timeRange.end.toISOString())}
</div>
<div style={{
border: '1px solid #ddd',
borderRadius: '6px',
overflow: 'hidden',
backgroundColor: '#fff'
}}>
{/* Time axis */}
<div style={{
display: 'flex',
borderBottom: '2px solid #333',
backgroundColor: '#f8f9fa',
position: 'relative',
height: '40px',
alignItems: 'center'
}}>
{timeLabels.map((time, index) => {
const position = ((time.getTime() - timeRange.start.getTime()) / totalDuration) * 100;
return (
<div
key={index}
style={{
position: 'absolute',
left: `${position}%`,
transform: 'translateX(-50%)',
fontSize: '0.8rem',
fontWeight: 'bold',
color: '#333',
whiteSpace: 'nowrap'
}}
>
{formatTime(time.toISOString())}
</div>
);
})}
</div>
{/* Events */}
<div style={{ padding: '1rem 0' }}>
{events.map((event, index) => {
const position = calculateEventPosition(event, timeRange);
return (
<div
key={event.id}
style={{
position: 'relative',
height: '60px',
marginBottom: '8px',
borderRadius: '4px',
border: '1px solid #e9ecef'
}}
>
{/* Event bar */}
<div
style={{
position: 'absolute',
left: `${position.left}%`,
width: `${position.width}%`,
height: '100%',
backgroundColor: getTypeColor(event.type),
opacity: getStatusOpacity(event.status),
borderRadius: '4px',
display: 'flex',
alignItems: 'center',
padding: '0 8px',
color: 'white',
fontSize: '0.8rem',
fontWeight: 'bold',
overflow: 'hidden',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
cursor: 'pointer',
transition: 'transform 0.2s ease',
}}
onMouseEnter={(e) => {
e.currentTarget.style.transform = 'scale(1.02)';
e.currentTarget.style.zIndex = '10';
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = 'scale(1)';
e.currentTarget.style.zIndex = '1';
}}
title={`${event.title}\n${event.location}\n${event.vipName}\n${formatTime(event.startTime)} - ${formatTime(event.endTime)}`}
>
<span style={{ marginRight: '4px' }}>{getTypeIcon(event.type)}</span>
<span style={{
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
flex: 1
}}>
{event.title}
</span>
</div>
{/* Event details (shown to the right of short events) */}
{position.width < 15 && (
<div
style={{
position: 'absolute',
left: `${position.left + position.width + 1}%`,
top: '50%',
transform: 'translateY(-50%)',
fontSize: '0.7rem',
color: '#666',
whiteSpace: 'nowrap',
backgroundColor: '#f8f9fa',
padding: '2px 6px',
borderRadius: '3px',
border: '1px solid #e9ecef'
}}
>
{getTypeIcon(event.type)} {event.title} - {event.vipName}
</div>
)}
</div>
);
})}
</div>
{/* Legend */}
<div style={{
borderTop: '1px solid #ddd',
padding: '1rem',
backgroundColor: '#f8f9fa'
}}>
<div style={{ fontSize: '0.8rem', fontWeight: 'bold', marginBottom: '0.5rem' }}>
Event Types:
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '1rem' }}>
{[
{ type: 'transport', label: 'Transport' },
{ type: 'meeting', label: 'Meetings' },
{ type: 'meal', label: 'Meals' },
{ type: 'event', label: 'Events' },
{ type: 'accommodation', label: 'Accommodation' }
].map(({ type, label }) => (
<div key={type} style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
<div
style={{
width: '16px',
height: '16px',
backgroundColor: getTypeColor(type),
borderRadius: '2px'
}}
/>
<span style={{ fontSize: '0.7rem' }}>{getTypeIcon(type)} {label}</span>
</div>
))}
</div>
</div>
</div>
</div>
);
};
export default GanttChart;

View File

@@ -0,0 +1,221 @@
.login-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px;
}
.login-card {
background: white;
border-radius: 12px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
padding: 40px;
max-width: 400px;
width: 100%;
text-align: center;
}
.login-header h1 {
color: #333;
margin: 0 0 8px 0;
font-size: 28px;
font-weight: 600;
}
.login-header p {
color: #666;
margin: 0 0 30px 0;
font-size: 16px;
}
.setup-notice {
background: #f0f9ff;
border: 1px solid #0ea5e9;
border-radius: 8px;
padding: 20px;
margin-bottom: 30px;
}
.setup-notice h3 {
color: #0369a1;
margin: 0 0 8px 0;
font-size: 18px;
}
.setup-notice p {
color: #0369a1;
margin: 0;
font-size: 14px;
}
.login-content {
margin-bottom: 30px;
}
.google-login-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
width: 100%;
padding: 12px 24px;
background: white;
border: 2px solid #dadce0;
border-radius: 8px;
font-size: 16px;
font-weight: 500;
color: #3c4043;
cursor: pointer;
transition: all 0.2s ease;
margin-bottom: 20px;
}
.google-login-btn:hover:not(:disabled) {
border-color: #1a73e8;
box-shadow: 0 2px 8px rgba(26, 115, 232, 0.2);
}
.google-login-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.google-icon {
width: 20px;
height: 20px;
}
.login-info p {
color: #666;
font-size: 14px;
line-height: 1.5;
margin: 0;
}
.dev-login-divider {
display: flex;
align-items: center;
gap: 12px;
margin: 24px 0;
color: #94a3b8;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.dev-login-divider::before,
.dev-login-divider::after {
content: '';
flex: 1;
height: 1px;
background: #e2e8f0;
}
.dev-login-form {
display: flex;
flex-direction: column;
gap: 16px;
text-align: left;
}
.dev-login-form h3 {
margin: 0;
color: #1e293b;
font-size: 18px;
font-weight: 600;
}
.dev-login-hint {
margin: 0;
font-size: 13px;
color: #64748b;
line-height: 1.4;
}
.dev-login-form label {
display: flex;
flex-direction: column;
gap: 6px;
font-size: 14px;
color: #334155;
}
.dev-login-form input {
width: 100%;
padding: 10px 12px;
border: 1px solid #cbd5f5;
border-radius: 8px;
font-size: 14px;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.dev-login-form input:focus {
outline: none;
border-color: #2563eb;
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
}
.dev-login-error {
background: #fee2e2;
border: 1px solid #fecaca;
color: #dc2626;
padding: 10px 12px;
border-radius: 8px;
font-size: 13px;
}
.dev-login-button {
width: 100%;
padding: 12px;
border: none;
border-radius: 8px;
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
color: white;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.dev-login-button:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 10px 20px rgba(37, 99, 235, 0.2);
}
.dev-login-button:disabled {
opacity: 0.7;
cursor: not-allowed;
}
.login-footer {
border-top: 1px solid #eee;
padding-top: 20px;
}
.login-footer p {
color: #999;
font-size: 12px;
margin: 0;
}
.loading {
color: #666;
font-size: 16px;
padding: 40px;
}
@media (max-width: 480px) {
.login-container {
padding: 10px;
}
.login-card {
padding: 30px 20px;
}
.login-header h1 {
font-size: 24px;
}
}

View File

@@ -0,0 +1,86 @@
import React, { useEffect, useState } from 'react';
import { apiCall } from '../config/api';
import './Login.css';
interface LoginProps {
onLogin: () => void;
errorMessage?: string | null | undefined;
}
const Login: React.FC<LoginProps> = ({ onLogin, errorMessage }) => {
const [setupStatus, setSetupStatus] = useState<any>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
apiCall('/auth/setup')
.then(res => res.json())
.then(data => {
setSetupStatus(data);
setLoading(false);
})
.catch(error => {
console.error('Error checking setup status:', error);
setLoading(false);
});
}, []);
if (loading) {
return (
<div className="login-container">
<div className="login-card">
<div className="loading">Loading...</div>
</div>
</div>
);
}
return (
<div className="login-container">
<div className="login-card">
<div className="login-header">
<h1>VIP Coordinator</h1>
<p>Secure access required</p>
</div>
{!setupStatus?.firstAdminCreated && (
<div className="setup-notice">
<h3>🚀 First Time Setup</h3>
<p>The first person to sign in will be promoted to administrator automatically.</p>
</div>
)}
<div className="login-content">
<button
className="google-login-btn"
onClick={onLogin}
>
<svg className="google-icon" viewBox="0 0 24 24">
<path fill="#635dff" d="M22 12.07c0-5.52-4.48-10-10-10s-10 4.48-10 10a9.97 9.97 0 006.85 9.48.73.73 0 00.95-.7v-3.05c-2.79.61-3.38-1.19-3.38-1.19-.46-1.17-1.12-1.49-1.12-1.49-.91-.62.07-.61.07-.61 1 .07 1.53 1.03 1.53 1.03.9 1.53 2.37 1.09 2.96.83.09-.65.35-1.09.63-1.34-2.23-.25-4.57-1.12-4.57-4.96 0-1.1.39-2 1.03-2.7-.1-.25-.45-1.25.1-2.6 0 0 .84-.27 2.75 1.02a9.53 9.53 0 015 0c1.91-1.29 2.75-1.02 2.75-1.02.55 1.35.2 2.35.1 2.6.64.7 1.03 1.6 1.03 2.7 0 3.85-2.34 4.71-4.58 4.95.36.31.69.92.69 1.86v2.75c0 .39.27.71.66.79a10 10 0 007.61-9.71z"/>
</svg>
Continue with Auth0
</button>
<div className="login-info">
<p>
{setupStatus?.authProvider === 'auth0'
? 'Sign in with your organisation account. We use Auth0 for secure authentication.'
: 'Authentication service is being configured. Please try again later.'}
</p>
</div>
{errorMessage && (
<div className="dev-login-error" style={{ marginTop: '1rem' }}>
{errorMessage}
</div>
)}
</div>
<div className="login-footer">
<p>Secure authentication powered by Auth0</p>
</div>
</div>
</div>
);
};
export default Login;

View File

@@ -0,0 +1,605 @@
import React, { useState, useEffect } from 'react';
import { apiCall } from '../config/api';
import DriverSelector from './DriverSelector';
interface ScheduleEvent {
id: string;
title: string;
location: string;
startTime: string;
endTime: string;
description?: string;
assignedDriverId?: string;
status: 'scheduled' | 'in-progress' | 'completed' | 'cancelled';
type: 'transport' | 'meeting' | 'event' | 'meal' | 'accommodation';
}
interface ScheduleManagerProps {
vipId: string;
vipName: string;
}
const ScheduleManager: React.FC<ScheduleManagerProps> = ({ vipId, vipName }) => {
const [schedule, setSchedule] = useState<ScheduleEvent[]>([]);
const [showAddForm, setShowAddForm] = useState(false);
const [editingEvent, setEditingEvent] = useState<ScheduleEvent | null>(null);
const [drivers, setDrivers] = useState<any[]>([]);
useEffect(() => {
fetchSchedule();
fetchDrivers();
}, [vipId]);
const fetchSchedule = async () => {
try {
const token = localStorage.getItem('authToken');
const response = await apiCall(`/api/vips/${vipId}/schedule`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
setSchedule(data);
}
} catch (error) {
console.error('Error fetching schedule:', error);
}
};
const fetchDrivers = async () => {
try {
const token = localStorage.getItem('authToken');
const response = await apiCall('/api/drivers', {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
setDrivers(data);
}
} catch (error) {
console.error('Error fetching drivers:', error);
}
};
const getDriverName = (driverId: string) => {
const driver = drivers.find(d => d.id === driverId);
return driver ? driver.name : `Driver ID: ${driverId}`;
};
const getStatusColor = (status: string) => {
switch (status) {
case 'scheduled': return '#3498db';
case 'in-progress': return '#f39c12';
case 'completed': return '#2ecc71';
case 'cancelled': return '#e74c3c';
default: return '#95a5a6';
}
};
const getTypeIcon = (type: string) => {
switch (type) {
case 'transport': return '🚗';
case 'meeting': return '🤝';
case 'event': return '🎉';
case 'meal': return '🍽️';
case 'accommodation': return '🏨';
default: return '📅';
}
};
const formatTime = (timeString: string) => {
try {
const date = new Date(timeString);
if (isNaN(date.getTime())) {
return 'Invalid Time';
}
// Safari-compatible time formatting
const hours = date.getHours();
const minutes = date.getMinutes();
const ampm = hours >= 12 ? 'PM' : 'AM';
const displayHours = hours % 12 || 12;
const displayMinutes = minutes.toString().padStart(2, '0');
return `${displayHours}:${displayMinutes} ${ampm}`;
} catch (error) {
console.error('Error formatting time:', error, timeString);
return 'Time Error';
}
};
const groupEventsByDay = (events: ScheduleEvent[]) => {
const grouped: { [key: string]: ScheduleEvent[] } = {};
events.forEach(event => {
const date = new Date(event.startTime).toDateString();
if (!grouped[date]) {
grouped[date] = [];
}
grouped[date].push(event);
});
// Sort events within each day by start time
Object.keys(grouped).forEach(date => {
grouped[date].sort((a, b) => new Date(a.startTime).getTime() - new Date(b.startTime).getTime());
});
return grouped;
};
const groupedSchedule = groupEventsByDay(schedule);
return (
<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">
<div className="flex justify-between items-center">
<div>
<h2 className="text-xl font-bold text-slate-800 flex items-center gap-2">
📅 Schedule for {vipName}
</h2>
<p className="text-slate-600 mt-1">Manage daily events and activities</p>
</div>
<button
className="bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 text-white px-6 py-3 rounded-lg font-medium transition-all duration-200 shadow-lg hover:shadow-xl flex items-center gap-2"
onClick={() => setShowAddForm(true)}
>
Add Event
</button>
</div>
</div>
<div className="p-8">
{Object.keys(groupedSchedule).length === 0 ? (
<div className="text-center py-12">
<div className="w-16 h-16 bg-slate-100 rounded-full flex items-center justify-center mx-auto mb-4">
<span className="text-2xl">📅</span>
</div>
<p className="text-slate-500 font-medium mb-2">No scheduled events</p>
<p className="text-slate-400 text-sm">Click "Add Event" to get started with scheduling</p>
</div>
) : (
<div className="space-y-8">
{Object.entries(groupedSchedule).map(([date, events]) => (
<div key={date} className="space-y-4">
<div className="bg-gradient-to-r from-slate-600 to-slate-700 text-white px-6 py-3 rounded-xl shadow-lg">
<h3 className="text-lg font-bold">
{new Date(date).toLocaleDateString([], {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</h3>
</div>
<div className="grid gap-4">
{events.map((event) => (
<div key={event.id} className="bg-gradient-to-r from-slate-50 to-slate-100 rounded-xl border border-slate-200/60 p-6 hover:shadow-lg transition-all duration-200">
<div className="flex items-start gap-6">
{/* Time Column */}
<div className="flex-shrink-0 text-center">
<div className="bg-white rounded-lg border border-slate-200 p-3 shadow-sm">
<div className="text-sm font-bold text-slate-900">
{formatTime(event.startTime)}
</div>
<div className="text-xs text-slate-500 mt-1">
to
</div>
<div className="text-sm font-bold text-slate-900">
{formatTime(event.endTime)}
</div>
</div>
</div>
{/* Event Content */}
<div className="flex-1">
<div className="flex items-center gap-3 mb-3">
<span className="text-2xl">{getTypeIcon(event.type)}</span>
<h4 className="text-lg font-bold text-slate-900">{event.title}</h4>
<span
className="px-3 py-1 rounded-full text-xs font-bold text-white shadow-sm"
style={{ backgroundColor: getStatusColor(event.status) }}
>
{event.status.toUpperCase()}
</span>
</div>
<div className="flex items-center gap-2 text-slate-600 mb-2">
<span>📍</span>
<span className="font-medium">{event.location}</span>
</div>
{event.description && (
<div className="text-slate-600 mb-3 bg-white/50 rounded-lg p-3 border border-slate-200/50">
{event.description}
</div>
)}
{event.assignedDriverId ? (
<div className="flex items-center gap-2 text-slate-600 mb-4">
<span>👤</span>
<span className="font-medium">Driver: {getDriverName(event.assignedDriverId)}</span>
</div>
) : (
<div className="bg-amber-50 border border-amber-200 rounded-lg p-3 mb-4">
<div className="flex items-center gap-2 text-amber-800 mb-2">
<span></span>
<span className="font-medium text-sm">No Driver Assigned</span>
</div>
<p className="text-amber-700 text-xs mb-2">This event needs a driver to ensure VIP transportation</p>
<button
className="bg-gradient-to-r from-amber-500 to-orange-500 hover:from-amber-600 hover:to-orange-600 text-white px-3 py-1 rounded-lg text-xs font-medium transition-all duration-200 shadow-sm hover:shadow-md"
onClick={() => setEditingEvent(event)}
>
🚗 Assign Driver
</button>
</div>
)}
<div className="flex items-center gap-3">
<button
className="bg-gradient-to-r from-slate-500 to-slate-600 hover:from-slate-600 hover:to-slate-700 text-white px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 shadow-lg hover:shadow-xl"
onClick={() => setEditingEvent(event)}
>
Edit
</button>
{event.status === 'scheduled' && (
<button
className="bg-gradient-to-r from-amber-500 to-orange-500 hover:from-amber-600 hover:to-orange-600 text-white px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 shadow-lg hover:shadow-xl"
onClick={() => updateEventStatus(event.id, 'in-progress')}
>
Start
</button>
)}
{event.status === 'in-progress' && (
<button
className="bg-gradient-to-r from-green-500 to-green-600 hover:from-green-600 hover:to-green-700 text-white px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 shadow-lg hover:shadow-xl"
onClick={() => updateEventStatus(event.id, 'completed')}
>
Complete
</button>
)}
{event.status === 'completed' && (
<span className="bg-green-100 text-green-800 px-3 py-1 rounded-full text-xs font-medium">
Completed
</span>
)}
</div>
</div>
</div>
</div>
))}
</div>
</div>
))}
</div>
)}
</div>
{showAddForm && (
<ScheduleEventForm
vipId={vipId}
onSubmit={handleAddEvent}
onCancel={() => setShowAddForm(false)}
/>
)}
{editingEvent && (
<ScheduleEventForm
vipId={vipId}
event={editingEvent}
onSubmit={handleEditEvent}
onCancel={() => setEditingEvent(null)}
/>
)}
</div>
);
async function handleAddEvent(eventData: any) {
try {
const token = localStorage.getItem('authToken');
const response = await apiCall(`/api/vips/${vipId}/schedule`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(eventData),
});
if (response.ok) {
await fetchSchedule();
setShowAddForm(false);
} else {
const errorData = await response.json();
throw errorData;
}
} catch (error) {
console.error('Error adding event:', error);
throw error;
}
}
async function handleEditEvent(eventData: any) {
try {
const token = localStorage.getItem('authToken');
const response = await apiCall(`/api/vips/${vipId}/schedule/${eventData.id}`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(eventData),
});
if (response.ok) {
await fetchSchedule();
setEditingEvent(null);
} else {
const errorData = await response.json();
throw errorData;
}
} catch (error) {
console.error('Error updating event:', error);
throw error;
}
}
async function updateEventStatus(eventId: string, status: string) {
try {
const token = localStorage.getItem('authToken');
const response = await apiCall(`/api/vips/${vipId}/schedule/${eventId}/status`, {
method: 'PATCH',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ status }),
});
if (response.ok) {
await fetchSchedule();
}
} catch (error) {
console.error('Error updating event status:', error);
}
}
};
// Modern Schedule Event Form Component
interface ScheduleEventFormProps {
vipId: string;
event?: ScheduleEvent;
onSubmit: (eventData: any) => void;
onCancel: () => void;
}
const ScheduleEventForm: React.FC<ScheduleEventFormProps> = ({ vipId, event, onSubmit, onCancel }) => {
const [formData, setFormData] = useState({
title: event?.title || '',
location: event?.location || '',
startTime: event?.startTime?.slice(0, 16) || '',
endTime: event?.endTime?.slice(0, 16) || '',
description: event?.description || '',
type: event?.type || 'event',
assignedDriverId: event?.assignedDriverId || ''
});
const [validationErrors, setValidationErrors] = useState<any[]>([]);
const [warnings, setWarnings] = useState<any[]>([]);
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
setValidationErrors([]);
setWarnings([]);
try {
await onSubmit({
...formData,
id: event?.id,
startTime: new Date(formData.startTime).toISOString(),
endTime: new Date(formData.endTime).toISOString(),
status: event?.status || 'scheduled'
});
} catch (error: any) {
if (error.validationErrors) {
setValidationErrors(error.validationErrors);
}
if (error.warnings) {
setWarnings(error.warnings);
}
} finally {
setIsSubmitting(false);
}
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
return (
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl shadow-2xl border border-slate-200/60 w-full max-w-2xl max-h-[90vh] overflow-y-auto">
<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">
{event ? '✏️ Edit Event' : ' Add New Event'}
</h2>
<p className="text-slate-600 mt-1">
{event ? 'Update event details' : 'Create a new schedule event'}
</p>
</div>
<form onSubmit={handleSubmit} className="p-8 space-y-6">
{validationErrors.length > 0 && (
<div className="bg-red-50 border border-red-200 rounded-xl p-4">
<h4 className="text-red-800 font-semibold mb-2"> Validation Errors:</h4>
<ul className="text-red-700 space-y-1">
{validationErrors.map((error, index) => (
<li key={index} className="text-sm"> {error.message}</li>
))}
</ul>
</div>
)}
{warnings.length > 0 && (
<div className="bg-amber-50 border border-amber-200 rounded-xl p-4">
<h4 className="text-amber-800 font-semibold mb-2"> Warnings:</h4>
<ul className="text-amber-700 space-y-1">
{warnings.map((warning, index) => (
<li key={index} className="text-sm"> {warning.message}</li>
))}
</ul>
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="md:col-span-2">
<label htmlFor="title" className="block text-sm font-medium text-slate-700 mb-2">
Event Title
</label>
<input
type="text"
id="title"
name="title"
value={formData.title}
onChange={handleChange}
className="w-full px-4 py-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
placeholder="Enter event title"
required
/>
</div>
<div>
<label htmlFor="type" className="block text-sm font-medium text-slate-700 mb-2">
Event Type
</label>
<select
id="type"
name="type"
value={formData.type}
onChange={handleChange}
className="w-full px-4 py-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
required
>
<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>
<div>
<label htmlFor="location" className="block text-sm font-medium text-slate-700 mb-2">
Location
</label>
<input
type="text"
id="location"
name="location"
value={formData.location}
onChange={handleChange}
className="w-full px-4 py-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
placeholder="Enter location"
required
/>
</div>
<div>
<label htmlFor="startTime" className="block text-sm font-medium text-slate-700 mb-2">
Start Time
</label>
<input
type="datetime-local"
id="startTime"
name="startTime"
value={formData.startTime}
onChange={handleChange}
className="w-full px-4 py-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
required
/>
</div>
<div>
<label htmlFor="endTime" className="block text-sm font-medium text-slate-700 mb-2">
End Time
</label>
<input
type="datetime-local"
id="endTime"
name="endTime"
value={formData.endTime}
onChange={handleChange}
className="w-full px-4 py-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
required
/>
</div>
<div className="md:col-span-2">
<label htmlFor="description" className="block text-sm font-medium text-slate-700 mb-2">
Description
</label>
<textarea
id="description"
name="description"
value={formData.description}
onChange={handleChange}
rows={3}
className="w-full px-4 py-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
placeholder="Enter event description (optional)"
/>
</div>
<div className="md:col-span-2">
<DriverSelector
selectedDriverId={formData.assignedDriverId}
onDriverSelect={(driverId) => setFormData(prev => ({ ...prev, assignedDriverId: driverId }))}
eventTime={{
startTime: formData.startTime ? new Date(formData.startTime).toISOString() : '',
endTime: formData.endTime ? new Date(formData.endTime).toISOString() : '',
location: formData.location
}}
/>
</div>
</div>
<div className="flex justify-end gap-4 pt-6 border-t border-slate-200">
<button
type="button"
className="px-6 py-3 border border-slate-300 text-slate-700 rounded-lg hover:bg-slate-50 transition-colors font-medium"
onClick={onCancel}
disabled={isSubmitting}
>
Cancel
</button>
<button
type="submit"
className="bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 text-white px-6 py-3 rounded-lg font-medium transition-all duration-200 shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed"
disabled={isSubmitting}
>
{isSubmitting ? (
<span className="flex items-center gap-2">
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
{event ? 'Updating...' : 'Creating...'}
</span>
) : (
event ? '✏️ Update Event' : ' Create Event'
)}
</button>
</div>
</form>
</div>
</div>
);
};
export default ScheduleManager;

View File

@@ -0,0 +1,488 @@
import React, { useState, useEffect } from 'react';
import { API_BASE_URL } from '../config/api';
interface User {
id: string;
email: string;
name: string;
picture: string;
role: string;
created_at: string;
last_sign_in_at?: string;
provider: string;
}
interface UserManagementProps {
currentUser: any;
}
const UserManagement: React.FC<UserManagementProps> = ({ currentUser }) => {
const [users, setUsers] = useState<User[]>([]);
const [pendingUsers, setPendingUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState<'all' | 'pending'>('all');
const [updatingUser, setUpdatingUser] = useState<string | null>(null);
// Check if current user is admin
if (currentUser?.role !== 'administrator') {
return (
<div className="p-6 bg-red-50 border border-red-200 rounded-lg">
<h2 className="text-xl font-semibold text-red-800 mb-2">Access Denied</h2>
<p className="text-red-600">You need administrator privileges to access user management.</p>
</div>
);
}
const fetchUsers = async () => {
try {
const token = localStorage.getItem('authToken');
const response = await fetch(`${API_BASE_URL}/auth/users`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error('Failed to fetch users');
}
const userData = await response.json();
setUsers(userData);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch users');
} finally {
setLoading(false);
}
};
const fetchPendingUsers = async () => {
try {
const token = localStorage.getItem('authToken');
const response = await fetch(`${API_BASE_URL}/auth/users/pending/list`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error('Failed to fetch pending users');
}
const pendingData = await response.json();
setPendingUsers(pendingData);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch pending users');
}
};
const updateUserRole = async (userEmail: string, newRole: string) => {
setUpdatingUser(userEmail);
try {
const token = localStorage.getItem('authToken');
const response = await fetch(`${API_BASE_URL}/auth/users/${userEmail}/role`, {
method: 'PATCH',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ role: newRole })
});
if (!response.ok) {
throw new Error('Failed to update user role');
}
// Refresh users list
await fetchUsers();
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to update user role');
} finally {
setUpdatingUser(null);
}
};
const deleteUser = async (userEmail: string, userName: string) => {
if (!confirm(`Are you sure you want to delete user "${userName}"? This action cannot be undone.`)) {
return;
}
try {
const token = localStorage.getItem('authToken');
const response = await fetch(`${API_BASE_URL}/auth/users/${userEmail}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error('Failed to delete user');
}
// Refresh users list
await fetchUsers();
await fetchPendingUsers();
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to delete user');
}
};
const approveUser = async (userEmail: string, userName: string) => {
setUpdatingUser(userEmail);
try {
const token = localStorage.getItem('authToken');
const response = await fetch(`${API_BASE_URL}/auth/users/${userEmail}/approval`, {
method: 'PATCH',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ status: 'approved' })
});
if (!response.ok) {
throw new Error('Failed to approve user');
}
// Refresh both lists
await fetchUsers();
await fetchPendingUsers();
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to approve user');
} finally {
setUpdatingUser(null);
}
};
const denyUser = async (userEmail: string, userName: string) => {
if (!confirm(`Are you sure you want to deny access for "${userName}"?`)) {
return;
}
setUpdatingUser(userEmail);
try {
const token = localStorage.getItem('authToken');
const response = await fetch(`${API_BASE_URL}/auth/users/${userEmail}/approval`, {
method: 'PATCH',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ status: 'denied' })
});
if (!response.ok) {
throw new Error('Failed to deny user');
}
// Refresh both lists
await fetchUsers();
await fetchPendingUsers();
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to deny user');
} finally {
setUpdatingUser(null);
}
};
useEffect(() => {
fetchUsers();
fetchPendingUsers();
}, []);
useEffect(() => {
if (activeTab === 'pending') {
fetchPendingUsers();
}
}, [activeTab]);
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
const getRoleBadgeColor = (role: string) => {
switch (role) {
case 'administrator':
return 'bg-red-100 text-red-800 border-red-200';
case 'coordinator':
return 'bg-blue-100 text-blue-800 border-blue-200';
case 'driver':
return 'bg-green-100 text-green-800 border-green-200';
default:
return 'bg-gray-100 text-gray-800 border-gray-200';
}
};
if (loading) {
return (
<div className="p-6">
<div className="animate-pulse">
<div className="h-8 bg-gray-200 rounded-lg w-1/4 mb-6"></div>
<div className="space-y-4">
{[1, 2, 3].map(i => (
<div key={i} className="h-20 bg-gray-200 rounded-lg"></div>
))}
</div>
</div>
</div>
);
}
return (
<div className="p-6">
<div className="mb-6">
<h2 className="text-2xl font-bold text-gray-900 mb-2">User Management</h2>
<p className="text-gray-600">Manage user accounts and permissions (PostgreSQL Database)</p>
</div>
{error && (
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg">
<p className="text-red-600">{error}</p>
<button
onClick={() => setError(null)}
className="mt-2 text-sm text-red-500 hover:text-red-700"
>
Dismiss
</button>
</div>
)}
{/* Tab Navigation */}
<div className="mb-6">
<div className="border-b border-gray-200">
<nav className="-mb-px flex space-x-8">
<button
onClick={() => setActiveTab('all')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'all'
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
👥 All Users ({users.length})
</button>
<button
onClick={() => setActiveTab('pending')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'pending'
? 'border-orange-500 text-orange-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
Pending Approval ({pendingUsers.length})
{pendingUsers.length > 0 && (
<span className="ml-2 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-orange-100 text-orange-800">
{pendingUsers.length}
</span>
)}
</button>
</nav>
</div>
</div>
{/* Content based on active tab */}
{activeTab === 'all' && (
<div className="bg-white shadow-sm border border-gray-200 rounded-lg overflow-hidden">
<div className="px-6 py-4 border-b border-gray-200 bg-gray-50">
<h3 className="text-lg font-medium text-gray-900">
All Users ({users.length})
</h3>
</div>
<div className="divide-y divide-gray-200">
{users.map((user) => (
<div key={user.email} className="p-6 hover:bg-gray-50">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
{user.picture ? (
<img
src={user.picture}
alt={user.name}
className="w-12 h-12 rounded-full"
/>
) : (
<div className="w-12 h-12 rounded-full bg-gray-300 flex items-center justify-center">
<span className="text-gray-600 font-medium">
{user.name.charAt(0).toUpperCase()}
</span>
</div>
)}
<div>
<h4 className="text-lg font-medium text-gray-900">{user.name}</h4>
<p className="text-gray-600">{user.email}</p>
<div className="flex items-center space-x-4 mt-1 text-sm text-gray-500">
<span>Joined: {formatDate(user.created_at)}</span>
{user.last_sign_in_at && (
<span>Last login: {formatDate(user.last_sign_in_at)}</span>
)}
<span className="capitalize">via {user.provider}</span>
</div>
</div>
</div>
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-2">
<span className="text-sm text-gray-600">Role:</span>
<select
value={user.role}
onChange={(e) => updateUserRole(user.email, e.target.value)}
disabled={updatingUser === user.email || user.email === currentUser.email}
className={`px-3 py-1 border rounded-md text-sm font-medium ${getRoleBadgeColor(user.role)} ${
updatingUser === user.email ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer hover:bg-opacity-80'
}`}
>
<option value="coordinator">Coordinator</option>
<option value="administrator">Administrator</option>
<option value="driver">Driver</option>
</select>
</div>
{user.email !== currentUser.email && (
<button
onClick={() => deleteUser(user.email, user.name)}
className="px-3 py-1 text-sm text-red-600 hover:text-red-800 hover:bg-red-50 rounded-md border border-red-200 transition-colors"
>
🗑 Delete
</button>
)}
{user.email === currentUser.email && (
<span className="px-3 py-1 text-sm text-blue-600 bg-blue-50 rounded-md border border-blue-200">
👤 You
</span>
)}
</div>
</div>
</div>
))}
</div>
{users.length === 0 && (
<div className="p-6 text-center text-gray-500">
No users found.
</div>
)}
</div>
)}
{/* Pending Users Tab */}
{activeTab === 'pending' && (
<div className="bg-white shadow-sm border border-gray-200 rounded-lg overflow-hidden">
<div className="px-6 py-4 border-b border-gray-200 bg-orange-50">
<h3 className="text-lg font-medium text-gray-900">
Pending Approval ({pendingUsers.length})
</h3>
<p className="text-sm text-gray-600 mt-1">
Users waiting for administrator approval to access the system
</p>
</div>
<div className="divide-y divide-gray-200">
{pendingUsers.map((user) => (
<div key={user.email} className="p-6 hover:bg-gray-50">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
{user.picture ? (
<img
src={user.picture}
alt={user.name}
className="w-12 h-12 rounded-full"
/>
) : (
<div className="w-12 h-12 rounded-full bg-gray-300 flex items-center justify-center">
<span className="text-gray-600 font-medium">
{user.name.charAt(0).toUpperCase()}
</span>
</div>
)}
<div>
<h4 className="text-lg font-medium text-gray-900">{user.name}</h4>
<p className="text-gray-600">{user.email}</p>
<div className="flex items-center space-x-4 mt-1 text-sm text-gray-500">
<span>Requested: {formatDate(user.created_at)}</span>
<span className="capitalize">via {user.provider}</span>
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
getRoleBadgeColor(user.role)
}`}>
{user.role}
</span>
</div>
</div>
</div>
<div className="flex items-center space-x-3">
<button
onClick={() => approveUser(user.email, user.name)}
disabled={updatingUser === user.email}
className={`px-4 py-2 text-sm font-medium text-white bg-green-600 hover:bg-green-700 rounded-md transition-colors ${
updatingUser === user.email ? 'opacity-50 cursor-not-allowed' : ''
}`}
>
{updatingUser === user.email ? '⏳ Approving...' : '✅ Approve'}
</button>
<button
onClick={() => denyUser(user.email, user.name)}
disabled={updatingUser === user.email}
className={`px-4 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-700 rounded-md transition-colors ${
updatingUser === user.email ? 'opacity-50 cursor-not-allowed' : ''
}`}
>
{updatingUser === user.email ? '⏳ Denying...' : '❌ Deny'}
</button>
</div>
</div>
</div>
))}
</div>
{pendingUsers.length === 0 && (
<div className="p-6 text-center text-gray-500">
<div className="text-6xl mb-4"></div>
<p className="text-lg font-medium mb-2">No pending approvals</p>
<p className="text-sm">All users have been processed.</p>
</div>
)}
</div>
)}
<div className="mt-6 p-4 bg-blue-50 border border-blue-200 rounded-lg">
<h4 className="font-medium text-blue-900 mb-2">Role Descriptions:</h4>
<ul className="text-sm text-blue-800 space-y-1">
<li><strong>Administrator:</strong> Full access to all features including user management</li>
<li><strong>Coordinator:</strong> Can manage VIPs, drivers, and schedules</li>
<li><strong>Driver:</strong> Can view assigned schedules and update status</li>
</ul>
</div>
<div className="mt-4 p-4 bg-orange-50 border border-orange-200 rounded-lg">
<h4 className="font-medium text-orange-900 mb-2">🔐 User Approval System:</h4>
<p className="text-sm text-orange-800">
New users (except the first administrator) require approval before accessing the system.
Users with pending approval will see a "pending approval" message when they try to sign in.
</p>
</div>
<div className="mt-4 p-4 bg-green-50 border border-green-200 rounded-lg">
<h4 className="font-medium text-green-900 mb-2"> PostgreSQL Database:</h4>
<p className="text-sm text-green-800">
User data is stored in your PostgreSQL database with proper indexing and relationships.
All user management operations are transactional and fully persistent across server restarts.
</p>
</div>
</div>
);
};
export default UserManagement;

View File

@@ -0,0 +1,459 @@
import React, { useState } from 'react';
interface Flight {
flightNumber: string;
flightDate: string;
segment: number;
validated?: boolean;
validationData?: any;
}
interface VipFormData {
name: string;
organization: string;
department: 'Office of Development' | 'Admin';
transportMode: 'flight' | 'self-driving';
flights?: Flight[];
expectedArrival?: string;
needsAirportPickup?: boolean;
needsVenueTransport: boolean;
notes: string;
}
interface VipFormProps {
onSubmit: (vipData: VipFormData) => void;
onCancel: () => void;
}
const VipForm: React.FC<VipFormProps> = ({ onSubmit, onCancel }) => {
const [formData, setFormData] = useState<VipFormData>({
name: '',
organization: '',
department: 'Office of Development',
transportMode: 'flight',
flights: [{ flightNumber: '', flightDate: '', segment: 1 }],
expectedArrival: '',
needsAirportPickup: true,
needsVenueTransport: true,
notes: ''
});
const [flightValidating, setFlightValidating] = useState<{ [key: number]: boolean }>({});
const [flightErrors, setFlightErrors] = useState<{ [key: number]: string }>({});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// Only include flights with flight numbers
const validFlights = formData.flights?.filter(f => f.flightNumber) || [];
onSubmit({
...formData,
flights: validFlights.length > 0 ? validFlights : undefined
});
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
const { name, value, type } = e.target;
if (type === 'checkbox') {
const checked = (e.target as HTMLInputElement).checked;
setFormData(prev => ({
...prev,
[name]: checked
}));
} else {
setFormData(prev => ({
...prev,
[name]: value
}));
}
};
const handleTransportModeChange = (mode: 'flight' | 'self-driving') => {
setFormData(prev => ({
...prev,
transportMode: mode,
flights: mode === 'flight' ? [{ flightNumber: '', flightDate: '', segment: 1 }] : undefined,
expectedArrival: mode === 'self-driving' ? prev.expectedArrival : '',
needsAirportPickup: mode === 'flight' ? true : false
}));
// Clear flight errors when switching away from flight mode
if (mode !== 'flight') {
setFlightErrors({});
}
};
const handleFlightChange = (index: number, field: 'flightNumber' | 'flightDate', value: string) => {
setFormData(prev => ({
...prev,
flights: prev.flights?.map((flight, i) =>
i === index ? { ...flight, [field]: value, validated: false } : flight
) || []
}));
// Clear validation for this flight when it changes
setFlightErrors(prev => ({ ...prev, [index]: '' }));
};
const addConnectingFlight = () => {
const currentFlights = formData.flights || [];
if (currentFlights.length < 3) {
setFormData(prev => ({
...prev,
flights: [...currentFlights, {
flightNumber: '',
flightDate: currentFlights[currentFlights.length - 1]?.flightDate || '',
segment: currentFlights.length + 1
}]
}));
}
};
const removeConnectingFlight = (index: number) => {
setFormData(prev => ({
...prev,
flights: prev.flights?.filter((_, i) => i !== index).map((flight, i) => ({
...flight,
segment: i + 1
})) || []
}));
// Clear errors for removed flight
setFlightErrors(prev => {
const newErrors = { ...prev };
delete newErrors[index];
return newErrors;
});
};
const validateFlight = async (index: number) => {
const flight = formData.flights?.[index];
if (!flight || !flight.flightNumber || !flight.flightDate) {
setFlightErrors(prev => ({ ...prev, [index]: 'Please enter flight number and date' }));
return;
}
setFlightValidating(prev => ({ ...prev, [index]: true }));
setFlightErrors(prev => ({ ...prev, [index]: '' }));
try {
const url = `/api/flights/${flight.flightNumber}?date=${flight.flightDate}`;
const response = await fetch(url);
if (response.ok) {
const data = await response.json();
// Update flight with validation data
setFormData(prev => ({
...prev,
flights: prev.flights?.map((f, i) =>
i === index ? { ...f, validated: true, validationData: data } : f
) || []
}));
setFlightErrors(prev => ({ ...prev, [index]: '' }));
} else {
const errorData = await response.json();
setFlightErrors(prev => ({
...prev,
[index]: errorData.error || 'Invalid flight number'
}));
}
} catch (error) {
setFlightErrors(prev => ({
...prev,
[index]: 'Error validating flight'
}));
} finally {
setFlightValidating(prev => ({ ...prev, [index]: false }));
}
};
return (
<div className="modal-overlay">
<div className="modal-content">
{/* Modal Header */}
<div className="modal-header">
<h2 className="text-2xl font-bold text-slate-800">
Add New VIP
</h2>
<p className="text-slate-600 mt-2">Enter VIP details and travel information</p>
</div>
{/* Modal Body */}
<div className="modal-body">
<form onSubmit={handleSubmit} className="space-y-8">
{/* Basic Information Section */}
<div className="form-section">
<div className="form-section-header">
<h3 className="form-section-title">Basic Information</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="form-group">
<label htmlFor="name" className="form-label">Full Name *</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
className="form-input"
placeholder="Enter full name"
required
/>
</div>
<div className="form-group">
<label htmlFor="organization" className="form-label">Organization *</label>
<input
type="text"
id="organization"
name="organization"
value={formData.organization}
onChange={handleChange}
className="form-input"
placeholder="Enter organization name"
required
/>
</div>
</div>
<div className="form-group">
<label htmlFor="department" className="form-label">Department *</label>
<select
id="department"
name="department"
value={formData.department}
onChange={handleChange}
className="form-select"
required
>
<option value="Office of Development">Office of Development</option>
<option value="Admin">Admin</option>
</select>
</div>
</div>
{/* Transportation Section */}
<div className="form-section">
<div className="form-section-header">
<h3 className="form-section-title">Transportation Details</h3>
</div>
<div className="form-group">
<label className="form-label">How are you arriving? *</label>
<div className="radio-group">
<div
className={`radio-option ${formData.transportMode === 'flight' ? 'selected' : ''}`}
onClick={() => handleTransportModeChange('flight')}
>
<input
type="radio"
name="transportMode"
value="flight"
checked={formData.transportMode === 'flight'}
onChange={() => handleTransportModeChange('flight')}
className="form-radio mr-3"
/>
<span className="font-medium">Arriving by Flight</span>
</div>
<div
className={`radio-option ${formData.transportMode === 'self-driving' ? 'selected' : ''}`}
onClick={() => handleTransportModeChange('self-driving')}
>
<input
type="radio"
name="transportMode"
value="self-driving"
checked={formData.transportMode === 'self-driving'}
onChange={() => handleTransportModeChange('self-driving')}
className="form-radio mr-3"
/>
<span className="font-medium">Self-Driving</span>
</div>
</div>
</div>
{/* Flight Mode Fields */}
{formData.transportMode === 'flight' && formData.flights && (
<div className="space-y-6">
{formData.flights.map((flight, index) => (
<div key={index} className="bg-white border-2 border-blue-200 rounded-xl p-6 shadow-sm">
<div className="flex justify-between items-center mb-4">
<h4 className="text-lg font-bold text-slate-800">
{index === 0 ? 'Primary Flight' : `Connecting Flight ${index}`}
</h4>
{index > 0 && (
<button
type="button"
onClick={() => removeConnectingFlight(index)}
className="text-red-500 hover:text-red-700 font-medium text-sm bg-red-50 hover:bg-red-100 px-3 py-1 rounded-lg transition-colors duration-200"
>
Remove
</button>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div className="form-group">
<label htmlFor={`flightNumber-${index}`} className="form-label">Flight Number *</label>
<input
type="text"
id={`flightNumber-${index}`}
value={flight.flightNumber}
onChange={(e) => handleFlightChange(index, 'flightNumber', e.target.value)}
className="form-input"
placeholder="e.g., AA123"
required={index === 0}
/>
</div>
<div className="form-group">
<label htmlFor={`flightDate-${index}`} className="form-label">Flight Date *</label>
<input
type="date"
id={`flightDate-${index}`}
value={flight.flightDate}
onChange={(e) => handleFlightChange(index, 'flightDate', e.target.value)}
className="form-input"
required={index === 0}
min={new Date().toISOString().split('T')[0]}
/>
</div>
</div>
<button
type="button"
className="btn btn-secondary w-full"
onClick={() => validateFlight(index)}
disabled={flightValidating[index] || !flight.flightNumber || !flight.flightDate}
>
{flightValidating[index] ? (
<>
<span className="animate-spin inline-block w-4 h-4 border-2 border-white border-t-transparent rounded-full mr-2"></span>
Validating Flight...
</>
) : (
<>Validate Flight</>
)}
</button>
{/* Flight Validation Results */}
{flightErrors[index] && (
<div className="mt-4 bg-red-50 border border-red-200 rounded-lg p-4">
<div className="text-red-700 font-medium">{flightErrors[index]}</div>
</div>
)}
{flight.validated && flight.validationData && (
<div className="mt-4 bg-green-50 border border-green-200 rounded-lg p-4">
<div className="text-green-700 font-medium mb-2">
Valid: {flight.validationData.airline || 'Flight'} - {flight.validationData.departure?.airport} {flight.validationData.arrival?.airport}
</div>
{flight.validationData.flightDate !== flight.flightDate && (
<div className="text-sm text-green-600">
Live tracking starts 4 hours before departure on {new Date(flight.flightDate).toLocaleDateString()}
</div>
)}
</div>
)}
</div>
))}
{formData.flights.length < 3 && (
<button
type="button"
className="btn btn-secondary w-full"
onClick={addConnectingFlight}
>
Add Connecting Flight
</button>
)}
<div className="checkbox-option checked">
<input
type="checkbox"
name="needsAirportPickup"
checked={formData.needsAirportPickup || false}
onChange={handleChange}
className="form-checkbox mr-3"
/>
<span className="font-medium">Needs Airport Pickup (from final destination)</span>
</div>
</div>
)}
{/* Self-Driving Mode Fields */}
{formData.transportMode === 'self-driving' && (
<div className="form-group">
<label htmlFor="expectedArrival" className="form-label">Expected Arrival *</label>
<input
type="datetime-local"
id="expectedArrival"
name="expectedArrival"
value={formData.expectedArrival}
onChange={handleChange}
className="form-input"
required
/>
</div>
)}
{/* Universal Transportation Option */}
<div className={`checkbox-option ${formData.needsVenueTransport ? 'checked' : ''}`}>
<input
type="checkbox"
name="needsVenueTransport"
checked={formData.needsVenueTransport}
onChange={handleChange}
className="form-checkbox mr-3"
/>
<div>
<span className="font-medium">Needs Transportation Between Venues</span>
<div className="text-sm text-slate-500 mt-1">
Check this if the VIP needs rides between different event locations
</div>
</div>
</div>
</div>
{/* Additional Information Section */}
<div className="form-section">
<div className="form-section-header">
<h3 className="form-section-title">Additional Information</h3>
</div>
<div className="form-group">
<label htmlFor="notes" className="form-label">Additional Notes</label>
<textarea
id="notes"
name="notes"
value={formData.notes}
onChange={handleChange}
rows={4}
className="form-textarea"
placeholder="Special requirements, dietary restrictions, accessibility needs, etc."
/>
</div>
</div>
{/* Form Actions */}
<div className="form-actions">
<button type="button" className="btn btn-secondary" onClick={onCancel}>
Cancel
</button>
<button type="submit" className="btn btn-primary">
Add VIP
</button>
</div>
</form>
</div>
</div>
</div>
);
};
export default VipForm;

View File

@@ -0,0 +1,14 @@
const DEFAULT_API_BASE =
typeof window !== 'undefined'
? window.location.origin
: 'http://localhost:3000';
export const API_BASE_URL =
import.meta.env.VITE_API_URL?.replace(/\/$/, '') || DEFAULT_API_BASE;
export const apiCall = (endpoint: string, options?: RequestInit) => {
const url = /^https?:\/\//.test(endpoint)
? endpoint
: `${API_BASE_URL}${endpoint}`;
return fetch(url, options);
};

208
frontend/src/index.css Normal file
View File

@@ -0,0 +1,208 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Custom base styles */
@layer base {
:root {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
line-height: 1.6;
font-weight: 400;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-width: 320px;
min-height: 100vh;
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
color: #1e293b;
}
#root {
width: 100%;
margin: 0 auto;
text-align: left;
}
/* Smooth scrolling */
html {
scroll-behavior: smooth;
}
/* Focus styles */
*:focus {
outline: 2px solid #3b82f6;
outline-offset: 2px;
}
}
/* Custom base styles */
@layer base {
:root {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
line-height: 1.6;
font-weight: 400;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-width: 320px;
min-height: 100vh;
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
color: #1e293b;
}
#root {
width: 100%;
margin: 0 auto;
text-align: left;
}
/* Smooth scrolling */
html {
scroll-behavior: smooth;
}
/* Focus styles */
*:focus {
outline: 2px solid #3b82f6;
outline-offset: 2px;
}
}
/* Custom component styles */
@layer components {
/* Modern Button Styles */
.btn {
@apply px-6 py-3 rounded-xl font-semibold text-sm transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5;
}
.btn-primary {
@apply bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 text-white focus:ring-blue-500;
}
.btn-secondary {
@apply bg-gradient-to-r from-slate-500 to-slate-600 hover:from-slate-600 hover:to-slate-700 text-white focus:ring-slate-500;
}
.btn-danger {
@apply bg-gradient-to-r from-red-500 to-red-600 hover:from-red-600 hover:to-red-700 text-white focus:ring-red-500;
}
.btn-success {
@apply bg-gradient-to-r from-green-500 to-green-600 hover:from-green-600 hover:to-green-700 text-white focus:ring-green-500;
}
/* Modern Card Styles */
.card {
@apply bg-white rounded-2xl shadow-lg border border-slate-200/60 overflow-hidden backdrop-blur-sm;
}
/* Modern Form Styles */
.form-group {
@apply mb-6;
}
.form-label {
@apply block text-sm font-semibold text-slate-700 mb-3;
}
.form-input {
@apply w-full px-4 py-3 border border-slate-300 rounded-xl shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 bg-white;
}
.form-select {
@apply w-full px-4 py-3 border border-slate-300 rounded-xl shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white transition-all duration-200;
}
.form-textarea {
@apply w-full px-4 py-3 border border-slate-300 rounded-xl shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 bg-white resize-none;
}
.form-checkbox {
@apply w-5 h-5 text-blue-600 border-slate-300 rounded focus:ring-blue-500 focus:ring-2;
}
.form-radio {
@apply w-4 h-4 text-blue-600 border-slate-300 focus:ring-blue-500 focus:ring-2;
}
/* Modal Styles */
.modal-overlay {
@apply fixed inset-0 bg-black/50 backdrop-blur-sm flex justify-center items-center z-50 p-4;
}
.modal-content {
@apply bg-white rounded-2xl shadow-2xl max-w-4xl w-full max-h-[90vh] overflow-y-auto;
}
.modal-header {
@apply bg-gradient-to-r from-blue-50 to-indigo-50 px-8 py-6 border-b border-slate-200/60;
}
.modal-body {
@apply p-8;
}
.modal-footer {
@apply bg-slate-50 px-8 py-6 border-t border-slate-200/60 flex justify-end space-x-4;
}
/* Form Actions */
.form-actions {
@apply flex justify-end space-x-4 pt-6 border-t border-slate-200/60 mt-8;
}
/* Form Sections */
.form-section {
@apply bg-slate-50 rounded-xl p-6 mb-6 border border-slate-200/60;
}
.form-section-header {
@apply flex items-center justify-between mb-4;
}
.form-section-title {
@apply text-lg font-bold text-slate-800;
}
/* Radio Group */
.radio-group {
@apply flex gap-6 mt-3;
}
.radio-option {
@apply flex items-center cursor-pointer bg-white rounded-lg px-4 py-3 border border-slate-200 hover:border-blue-300 hover:bg-blue-50 transition-all duration-200;
}
.radio-option.selected {
@apply border-blue-500 bg-blue-50 ring-2 ring-blue-200;
}
/* Checkbox Group */
.checkbox-option {
@apply flex items-center cursor-pointer bg-white rounded-lg px-4 py-3 border border-slate-200 hover:border-blue-300 hover:bg-blue-50 transition-all duration-200;
}
.checkbox-option.checked {
@apply border-blue-500 bg-blue-50;
}
}

36
frontend/src/main.tsx Normal file
View File

@@ -0,0 +1,36 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Auth0Provider } from '@auth0/auth0-react';
import App from './App.tsx';
import './index.css';
const domain = import.meta.env.VITE_AUTH0_DOMAIN;
const clientId = import.meta.env.VITE_AUTH0_CLIENT_ID;
const audience = import.meta.env.VITE_AUTH0_AUDIENCE;
if (!domain || !clientId) {
throw new Error('Auth0 environment variables are missing. Please set VITE_AUTH0_DOMAIN and VITE_AUTH0_CLIENT_ID.');
}
const authorizationParams: Record<string, string> = {
redirect_uri: `${window.location.origin}/auth/callback`,
scope: 'openid profile email'
};
if (audience) {
authorizationParams.audience = audience;
}
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<Auth0Provider
domain={domain}
clientId={clientId}
authorizationParams={authorizationParams}
cacheLocation="localstorage"
useRefreshTokens={true}
>
<App />
</Auth0Provider>
</React.StrictMode>
);

View File

@@ -0,0 +1,825 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { apiCall, API_BASE_URL } from '../config/api';
import { generateTestVips, getTestOrganizations, generateVipSchedule } from '../utils/testVipData';
interface ApiKeys {
aviationStackKey?: string;
googleMapsKey?: string;
twilioKey?: string;
auth0Domain?: string;
auth0ClientId?: string;
auth0ClientSecret?: string;
auth0Audience?: string;
}
interface SystemSettings {
defaultPickupLocation?: string;
defaultDropoffLocation?: string;
timeZone?: string;
notificationsEnabled?: boolean;
}
const AdminDashboard: React.FC = () => {
const navigate = useNavigate();
const [apiKeys, setApiKeys] = useState<ApiKeys>({});
const [systemSettings, setSystemSettings] = useState<SystemSettings>({});
const [testResults, setTestResults] = useState<{ [key: string]: string }>({});
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [saveStatus, setSaveStatus] = useState<string | null>(null);
const [savedKeys, setSavedKeys] = useState<{ [key: string]: boolean }>({});
const [maskedKeyHints, setMaskedKeyHints] = useState<{ [key: string]: string }>({});
const [testDataLoading, setTestDataLoading] = useState(false);
const [testDataStatus, setTestDataStatus] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const buildAuthHeaders = (includeJson = false) => {
const headers: Record<string, string> = {};
const token = typeof window !== 'undefined' ? localStorage.getItem('authToken') : null;
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
if (includeJson) {
headers['Content-Type'] = 'application/json';
}
return headers;
};
const loadSettings = async () => {
try {
setLoading(true);
setError(null);
const response = await apiCall('/api/admin/settings', {
headers: buildAuthHeaders()
});
if (response.ok) {
const data = await response.json();
const saved: { [key: string]: boolean } = {};
const maskedHints: { [key: string]: string } = {};
const cleanedApiKeys: ApiKeys = {};
if (data.apiKeys) {
Object.entries(data.apiKeys).forEach(([key, value]) => {
if (value && (value as string).startsWith('***')) {
saved[key] = true;
maskedHints[key] = value as string;
} else if (value) {
cleanedApiKeys[key as keyof ApiKeys] = value as string;
}
});
}
setSavedKeys(saved);
setMaskedKeyHints(maskedHints);
setApiKeys(cleanedApiKeys);
setSystemSettings(data.systemSettings || {});
} else if (response.status === 403) {
setError('You need administrator access to view this page.');
} else if (response.status === 401) {
setError('Authentication required. Please sign in again.');
} else {
setError('Failed to load admin settings.');
}
} catch (err) {
console.error('Failed to load settings:', err);
setError('Failed to load admin settings.');
} finally {
setLoading(false);
}
};
useEffect(() => {
loadSettings();
}, []);
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 }));
setMaskedKeyHints(prev => {
const next = { ...prev };
delete next[key];
return next;
});
}
};
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 apiCall(`/api/admin/test-api/${apiType}`, {
method: 'POST',
headers: buildAuthHeaders(true),
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 () => {
setSaving(true);
setSaveStatus(null);
try {
const response = await apiCall('/api/admin/settings', {
method: 'POST',
headers: buildAuthHeaders(true),
body: JSON.stringify({
apiKeys,
systemSettings
})
});
if (response.ok) {
setSaveStatus('Settings saved successfully!');
// Refresh the latest settings so saved states/labels stay accurate
await loadSettings();
setTimeout(() => setSaveStatus(null), 3000);
} else {
setSaveStatus('Failed to save settings');
}
} catch (error) {
setSaveStatus('Error saving settings');
} finally {
setSaving(false);
}
};
const handleLogout = () => {
localStorage.removeItem('authToken');
navigate('/');
window.location.reload();
};
// 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.name, 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 (loading) {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 flex justify-center items-center">
<div className="bg-white rounded-2xl shadow-xl p-8 flex items-center space-x-4 border border-slate-200/60">
<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 admin settings...</span>
</div>
</div>
);
}
if (error) {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 flex justify-center items-center">
<div className="bg-white rounded-2xl shadow-xl p-8 w-full max-w-xl border border-rose-200/70">
<h2 className="text-2xl font-bold text-rose-700 mb-4">Admin access required</h2>
<p className="text-slate-600 mb-6">{error}</p>
<button
className="btn btn-primary"
onClick={() => navigate('/')}
>
Return to dashboard
</button>
</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>
<input
type="password"
placeholder={savedKeys.aviationStackKey && maskedKeyHints.aviationStackKey
? `Saved (${maskedKeyHints.aviationStackKey.slice(-4)})`
: 'Enter AviationStack API key'}
value={apiKeys.aviationStackKey || ''}
onChange={(e) => handleApiKeyChange('aviationStackKey', e.target.value)}
className="form-input"
/>
{savedKeys.aviationStackKey && maskedKeyHints.aviationStackKey && !apiKeys.aviationStackKey && (
<p className="text-xs text-slate-500 mt-1">
Currently saved key ends with {maskedKeyHints.aviationStackKey.slice(-4)}. Enter a new value to replace it.
</p>
)}
<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>
{/* Auth0 Credentials */}
<div className="form-section">
<div className="form-section-header">
<h3 className="form-section-title">Auth0 Configuration</h3>
{(savedKeys.auth0Domain || savedKeys.auth0ClientId || savedKeys.auth0ClientSecret) && (
<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">Auth0 Domain</label>
<input
type="text"
placeholder={savedKeys.auth0Domain && maskedKeyHints.auth0Domain
? `Saved (${maskedKeyHints.auth0Domain.slice(-4)})`
: 'e.g. dev-1234abcd.us.auth0.com'}
value={apiKeys.auth0Domain || ''}
onChange={(e) => handleApiKeyChange('auth0Domain', e.target.value)}
className="form-input"
/>
</div>
<div className="form-group">
<label className="form-label">Client ID</label>
<input
type="password"
placeholder={savedKeys.auth0ClientId && maskedKeyHints.auth0ClientId
? `Saved (${maskedKeyHints.auth0ClientId.slice(-4)})`
: 'Enter Auth0 application Client ID'}
value={apiKeys.auth0ClientId || ''}
onChange={(e) => handleApiKeyChange('auth0ClientId', e.target.value)}
className="form-input"
/>
{savedKeys.auth0ClientId && maskedKeyHints.auth0ClientId && !apiKeys.auth0ClientId && (
<p className="text-xs text-slate-500 mt-1">
Saved client ID ends with {maskedKeyHints.auth0ClientId.slice(-4)}. Provide a new ID to update it.
</p>
)}
</div>
<div className="form-group">
<label className="form-label">Client Secret</label>
<input
type="password"
placeholder={savedKeys.auth0ClientSecret && maskedKeyHints.auth0ClientSecret
? `Saved (${maskedKeyHints.auth0ClientSecret.slice(-4)})`
: 'Enter Auth0 application Client Secret'}
value={apiKeys.auth0ClientSecret || ''}
onChange={(e) => handleApiKeyChange('auth0ClientSecret', e.target.value)}
className="form-input"
/>
{savedKeys.auth0ClientSecret && maskedKeyHints.auth0ClientSecret && !apiKeys.auth0ClientSecret && (
<p className="text-xs text-slate-500 mt-1">
Saved client secret ends with {maskedKeyHints.auth0ClientSecret.slice(-4)}. Provide a new secret to rotate it.
</p>
)}
</div>
<div className="form-group">
<label className="form-label">API Audience (Identifier)</label>
<input
type="text"
placeholder={apiKeys.auth0Audience || 'https://your-api-identifier'}
value={apiKeys.auth0Audience || ''}
onChange={(e) => handleApiKeyChange('auth0Audience', e.target.value)}
className="form-input"
/>
<p className="text-xs text-slate-500 mt-1">
Create an API in Auth0 and use its Identifier here (e.g. https://vip-coordinator-api).
</p>
</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>Sign in to the Auth0 Dashboard</li>
<li>Create a <strong>Single Page Application</strong> for the frontend</li>
<li>Set Allowed Callback URL to <code>https://bsa.madeamess.online/auth/callback</code></li>
<li>Set Allowed Logout URL to <code>https://bsa.madeamess.online/</code></li>
<li>Set Allowed Web Origins to <code>https://bsa.madeamess.online</code></li>
<li>Create an <strong>API</strong> in Auth0 for the backend and use its Identifier as the audience</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(`${API_BASE_URL}/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={saving}
>
{saving ? '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

@@ -0,0 +1,378 @@
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { apiCall } from '../config/api';
interface ScheduleEvent {
id: string;
title: string;
location: string;
startTime: string;
endTime: string;
status: 'scheduled' | 'in-progress' | 'completed' | 'cancelled';
type: 'transport' | 'meeting' | 'event' | 'meal' | 'accommodation';
}
interface Vip {
id: string;
name: string;
organization: string;
transportMode: 'flight' | 'self-driving';
flightNumber?: string;
flights?: Array<{
flightNumber: string;
flightDate: string;
segment: number;
}>;
expectedArrival?: string;
arrivalTime?: string;
needsAirportPickup?: boolean;
needsVenueTransport: boolean;
notes?: string;
currentEvent?: ScheduleEvent;
nextEvent?: ScheduleEvent;
nextEventTime?: string;
}
interface Driver {
id: string;
name: string;
phone: string;
currentLocation: { lat: number; lng: number };
assignedVipIds: string[];
}
const Dashboard: React.FC = () => {
const [vips, setVips] = useState<Vip[]>([]);
const [drivers, setDrivers] = useState<Driver[]>([]);
const [loading, setLoading] = useState(true);
// Helper functions for event management
const getCurrentEvent = (events: ScheduleEvent[]) => {
const now = new Date();
return events.find(event =>
new Date(event.startTime) <= now &&
new Date(event.endTime) > now &&
event.status === 'in-progress'
) || null;
};
const getNextEvent = (events: ScheduleEvent[]) => {
const now = new Date();
const upcomingEvents = events.filter(event =>
new Date(event.startTime) > now && event.status === 'scheduled'
).sort((a, b) => new Date(a.startTime).getTime() - new Date(b.startTime).getTime());
return upcomingEvents.length > 0 ? upcomingEvents[0] : null;
};
const formatTime = (timeString: string) => {
return new Date(timeString).toLocaleString([], {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
useEffect(() => {
const fetchData = async () => {
try {
const token = localStorage.getItem('authToken');
const authHeaders = {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
};
const [vipsResponse, driversResponse] = await Promise.all([
apiCall('/api/vips', { headers: authHeaders }),
apiCall('/api/drivers', { headers: authHeaders })
]);
if (!vipsResponse.ok || !driversResponse.ok) {
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`, {
headers: authHeaders
});
if (scheduleResponse.ok) {
const scheduleData = await scheduleResponse.json();
const currentEvent = getCurrentEvent(scheduleData);
const nextEvent = getNextEvent(scheduleData);
return {
...vip,
currentEvent,
nextEvent,
nextEventTime: nextEvent ? nextEvent.startTime : null
};
} else {
return { ...vip, currentEvent: null, nextEvent: null, nextEventTime: null };
}
} catch (error) {
console.error(`Error fetching schedule for VIP ${vip.id}:`, error);
return { ...vip, currentEvent: null, nextEvent: null, nextEventTime: null };
}
})
);
// Sort VIPs by next event time (soonest first), then by name
const sortedVips = vipsWithSchedules.sort((a, b) => {
// VIPs with current events first
if (a.currentEvent && !b.currentEvent) return -1;
if (!a.currentEvent && b.currentEvent) return 1;
// Then by next event time (soonest first)
if (a.nextEventTime && b.nextEventTime) {
return new Date(a.nextEventTime).getTime() - new Date(b.nextEventTime).getTime();
}
if (a.nextEventTime && !b.nextEventTime) return -1;
if (!a.nextEventTime && b.nextEventTime) return 1;
// Finally by name if no events
return a.name.localeCompare(b.name);
});
setVips(sortedVips);
setDrivers(driversData);
} catch (error) {
console.error('Error fetching data:', error);
} finally {
setLoading(false);
}
};
fetchData();
}, []);
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 dashboard...</span>
</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 items-center justify-between">
<div>
<h1 className="text-3xl font-bold bg-gradient-to-r from-slate-800 to-slate-600 bg-clip-text text-transparent">
VIP Coordinator Dashboard
</h1>
<p className="text-slate-600 mt-2">Real-time overview of VIP activities and coordination</p>
</div>
<div className="flex items-center space-x-4">
<div className="bg-gradient-to-r from-blue-500 to-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium">
{vips.length} Active VIPs
</div>
<div className="bg-gradient-to-r from-green-500 to-green-600 text-white px-4 py-2 rounded-lg text-sm font-medium">
{drivers.length} Drivers
</div>
</div>
</div>
</div>
<div className="grid grid-cols-1 xl:grid-cols-3 gap-8">
{/* VIP Status Dashboard */}
<div className="xl:col-span-2">
<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-6 py-4 border-b border-slate-200/60">
<h2 className="text-xl font-bold text-slate-800 flex items-center">
VIP Status Dashboard
<span className="ml-2 bg-blue-100 text-blue-800 text-sm font-medium px-2.5 py-0.5 rounded-full">
{vips.length} VIPs
</span>
</h2>
</div>
<div className="p-6">
{vips.length === 0 ? (
<div className="text-center py-12">
<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>
<p className="text-slate-500 font-medium">No VIPs currently scheduled</p>
</div>
) : (
<div className="space-y-4">
{vips.map((vip) => {
const hasCurrentEvent = !!vip.currentEvent;
const hasNextEvent = !!vip.nextEvent;
return (
<div key={vip.id} className={`
relative rounded-xl border-2 p-6 transition-all duration-200 hover:shadow-lg
${hasCurrentEvent
? 'border-amber-300 bg-gradient-to-r from-amber-50 to-orange-50'
: hasNextEvent
? 'border-blue-300 bg-gradient-to-r from-blue-50 to-indigo-50'
: 'border-slate-200 bg-slate-50'
}
`}>
<div className="flex justify-between items-start">
<div className="flex-1">
<div className="flex items-center gap-3 mb-3">
<h3 className="text-lg font-bold text-slate-900">{vip.name}</h3>
{hasCurrentEvent && (
<span className="bg-gradient-to-r from-amber-500 to-orange-500 text-white px-3 py-1 rounded-full text-xs font-bold animate-pulse">
ACTIVE
</span>
)}
</div>
<p className="text-slate-600 text-sm mb-4">{vip.organization}</p>
{/* Current Event */}
{vip.currentEvent && (
<div className="bg-white border border-amber-200 rounded-lg p-4 mb-3 shadow-sm">
<div className="flex items-center gap-2 mb-2">
<span className="text-amber-600 font-bold text-sm">CURRENT EVENT</span>
</div>
<div className="flex items-center gap-2 mb-1">
<span className="font-semibold text-slate-900">{vip.currentEvent.title}</span>
</div>
<p className="text-slate-600 text-sm mb-1">Location: {vip.currentEvent.location}</p>
<p className="text-slate-500 text-xs">Until {formatTime(vip.currentEvent.endTime)}</p>
</div>
)}
{/* Next Event */}
{vip.nextEvent && (
<div className="bg-white border border-blue-200 rounded-lg p-4 mb-3 shadow-sm">
<div className="flex items-center gap-2 mb-2">
<span className="text-blue-600 font-bold text-sm">NEXT EVENT</span>
</div>
<div className="flex items-center gap-2 mb-1">
<span className="font-semibold text-slate-900">{vip.nextEvent.title}</span>
</div>
<p className="text-slate-600 text-sm mb-1">Location: {vip.nextEvent.location}</p>
<p className="text-slate-500 text-xs">{formatTime(vip.nextEvent.startTime)} - {formatTime(vip.nextEvent.endTime)}</p>
</div>
)}
{/* No Events */}
{!vip.currentEvent && !vip.nextEvent && (
<div className="bg-white border border-slate-200 rounded-lg p-4 mb-3">
<p className="text-slate-500 text-sm italic">No scheduled events</p>
</div>
)}
{/* Transport Info */}
<div className="flex items-center gap-2 text-xs text-slate-500 bg-white/50 rounded-lg px-3 py-2">
{vip.transportMode === 'flight' ? (
<span>Flight: {vip.flights && vip.flights.length > 0 ?
vip.flights.map(f => f.flightNumber).join(' → ') :
vip.flightNumber || 'TBD'}
</span>
) : (
<span>Self-driving | Expected: {vip.expectedArrival ? formatTime(vip.expectedArrival) : 'TBD'}</span>
)}
</div>
</div>
<div className="flex flex-col gap-2 ml-6">
<Link
to={`/vips/${vip.id}`}
className="bg-gradient-to-r from-green-500 to-green-600 hover:from-green-600 hover:to-green-700 text-white px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 text-center shadow-lg hover:shadow-xl"
>
Details
</Link>
<Link
to={`/vips/${vip.id}#schedule`}
className="bg-gradient-to-r from-slate-500 to-slate-600 hover:from-slate-600 hover:to-slate-700 text-white px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 text-center shadow-lg hover:shadow-xl"
>
Schedule
</Link>
</div>
</div>
</div>
);
})}
</div>
)}
</div>
</div>
</div>
{/* Sidebar */}
<div className="space-y-6">
{/* Drivers Card */}
<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-6 py-4 border-b border-slate-200/60">
<h2 className="text-lg font-bold text-slate-800 flex items-center">
Available Drivers
<span className="ml-2 bg-green-100 text-green-800 text-sm font-medium px-2.5 py-0.5 rounded-full">
{drivers.length}
</span>
</h2>
</div>
<div className="p-6">
{drivers.length === 0 ? (
<div className="text-center py-8">
<div className="w-12 h-12 bg-slate-100 rounded-full flex items-center justify-center mx-auto mb-3">
<div className="w-6 h-6 bg-slate-300 rounded-full"></div>
</div>
<p className="text-slate-500 text-sm">No drivers available</p>
</div>
) : (
<div className="space-y-3">
{drivers.map((driver) => (
<div key={driver.id} className="bg-slate-50 rounded-lg p-4 border border-slate-200">
<div className="flex items-center justify-between">
<div>
<h4 className="font-semibold text-slate-900">{driver.name}</h4>
<p className="text-slate-600 text-sm">{driver.phone}</p>
</div>
<div className="text-right">
<span className="bg-blue-100 text-blue-800 text-xs font-medium px-2 py-1 rounded-full">
{driver.assignedVipIds.length} VIPs
</span>
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
{/* Quick Actions Card */}
<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-6 py-4 border-b border-slate-200/60">
<h2 className="text-lg font-bold text-slate-800">Quick Actions</h2>
</div>
<div className="p-6 space-y-3">
<Link
to="/vips"
className="block w-full bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 text-white px-4 py-3 rounded-lg font-medium transition-all duration-200 text-center shadow-lg hover:shadow-xl"
>
Manage VIPs
</Link>
<Link
to="/drivers"
className="block w-full bg-gradient-to-r from-green-500 to-green-600 hover:from-green-600 hover:to-green-700 text-white px-4 py-3 rounded-lg font-medium transition-all duration-200 text-center shadow-lg hover:shadow-xl"
>
Manage Drivers
</Link>
</div>
</div>
</div>
</div>
</div>
);
};
export default Dashboard;

View File

@@ -0,0 +1,763 @@
import React, { useState, useEffect } from 'react';
import { useParams, Link } from 'react-router-dom';
import { apiCall } from '../config/api';
import GanttChart from '../components/GanttChart';
interface DriverScheduleEvent {
id: string;
title: string;
location: string;
startTime: string;
endTime: string;
description?: string;
status: 'scheduled' | 'in-progress' | 'completed' | 'cancelled';
type: 'transport' | 'meeting' | 'event' | 'meal' | 'accommodation';
vipId: string;
vipName: string;
}
interface Driver {
id: string;
name: string;
phone: string;
}
interface DriverScheduleData {
driver: Driver;
schedule: DriverScheduleEvent[];
}
const DriverDashboard: React.FC = () => {
const { driverId } = useParams<{ driverId: string }>();
const [scheduleData, setScheduleData] = useState<DriverScheduleData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (driverId) {
fetchDriverSchedule();
}
}, [driverId]);
const fetchDriverSchedule = async () => {
try {
const token = localStorage.getItem('authToken');
const response = await apiCall(`/api/drivers/${driverId}/schedule`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
setScheduleData(data);
} else {
setError('Driver not found');
}
} catch (err) {
setError('Error loading driver schedule');
} finally {
setLoading(false);
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'scheduled': return '#3498db';
case 'in-progress': return '#f39c12';
case 'completed': return '#2ecc71';
case 'cancelled': return '#e74c3c';
default: return '#95a5a6';
}
};
const getTypeIcon = (type: string) => {
switch (type) {
case 'transport': return '🚗';
case 'meeting': return '🤝';
case 'event': return '🎉';
case 'meal': return '🍽️';
case 'accommodation': return '🏨';
default: return '📅';
}
};
const formatTime = (timeString: string) => {
return new Date(timeString).toLocaleString([], {
hour: '2-digit',
minute: '2-digit'
});
};
const formatDate = (timeString: string) => {
return new Date(timeString).toLocaleDateString([], {
weekday: 'short',
month: 'short',
day: 'numeric'
});
};
const getNextEvent = () => {
if (!scheduleData?.schedule) return null;
const now = new Date();
const upcomingEvents = scheduleData.schedule.filter(event =>
new Date(event.startTime) > now && event.status === 'scheduled'
).sort((a, b) => new Date(a.startTime).getTime() - new Date(b.startTime).getTime());
return upcomingEvents.length > 0 ? upcomingEvents[0] : null;
};
const getCurrentEvent = () => {
if (!scheduleData?.schedule) return null;
const now = new Date();
return scheduleData.schedule.find(event =>
new Date(event.startTime) <= now &&
new Date(event.endTime) > now &&
event.status === 'in-progress'
) || null;
};
const groupEventsByDay = (events: DriverScheduleEvent[]) => {
const grouped: { [key: string]: DriverScheduleEvent[] } = {};
events.forEach(event => {
const date = new Date(event.startTime).toDateString();
if (!grouped[date]) {
grouped[date] = [];
}
grouped[date].push(event);
});
// Sort events within each day by start time
Object.keys(grouped).forEach(date => {
grouped[date].sort((a, b) => new Date(a.startTime).getTime() - new Date(b.startTime).getTime());
});
return grouped;
};
const handlePrintSchedule = () => {
if (!scheduleData) return;
const printWindow = window.open('', '_blank');
if (!printWindow) return;
const groupedSchedule = groupEventsByDay(scheduleData.schedule);
const printContent = `
<!DOCTYPE html>
<html>
<head>
<title>Driver Schedule - ${scheduleData.driver.name}</title>
<meta charset="UTF-8">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: #2d3748;
background: #ffffff;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 40px 30px;
}
.header {
text-align: center;
margin-bottom: 40px;
padding-bottom: 30px;
border-bottom: 3px solid #e2e8f0;
background: linear-gradient(135deg, #e53e3e 0%, #c53030 100%);
color: white;
padding: 40px 30px;
border-radius: 15px;
margin: -40px -30px 40px -30px;
}
.header h1 {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 10px;
text-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.header h2 {
font-size: 1.8rem;
font-weight: 600;
margin-bottom: 20px;
opacity: 0.95;
}
.driver-info {
background: linear-gradient(135deg, #f7fafc 0%, #edf2f7 100%);
padding: 25px;
border-radius: 12px;
margin-bottom: 30px;
border: 1px solid #e2e8f0;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.driver-info p {
margin-bottom: 8px;
font-size: 1rem;
}
.driver-info strong {
color: #4a5568;
font-weight: 600;
}
.day-section {
margin-bottom: 40px;
page-break-inside: avoid;
}
.day-header {
background: linear-gradient(135deg, #e53e3e 0%, #c53030 100%);
color: white;
padding: 20px 25px;
font-size: 1.3rem;
font-weight: 700;
margin-bottom: 20px;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
text-align: center;
}
.event {
background: white;
border: 1px solid #e2e8f0;
margin-bottom: 15px;
padding: 25px;
border-radius: 12px;
display: flex;
align-items: flex-start;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.event-time {
min-width: 120px;
background: linear-gradient(135deg, #edf2f7 0%, #e2e8f0 100%);
padding: 15px;
border-radius: 8px;
text-align: center;
margin-right: 25px;
border: 1px solid #cbd5e0;
}
.event-time .time {
font-weight: 700;
font-size: 1rem;
color: #2d3748;
display: block;
}
.event-time .separator {
font-size: 0.8rem;
color: #718096;
margin: 5px 0;
}
.event-details {
flex: 1;
}
.event-title {
font-weight: 700;
font-size: 1.2rem;
margin-bottom: 10px;
color: #2d3748;
display: flex;
align-items: center;
gap: 8px;
}
.event-icon {
font-size: 1.3rem;
}
.event-location {
color: #4a5568;
margin-bottom: 8px;
font-weight: 500;
display: flex;
align-items: center;
gap: 6px;
}
.event-vip {
color: #e53e3e;
font-weight: 600;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 6px;
}
.event-description {
background: #f7fafc;
padding: 12px 15px;
border-radius: 8px;
font-style: italic;
color: #4a5568;
margin-bottom: 10px;
border-left: 4px solid #cbd5e0;
}
.footer {
margin-top: 50px;
text-align: center;
color: #718096;
font-size: 0.9rem;
padding-top: 20px;
border-top: 1px solid #e2e8f0;
}
.company-logo {
width: 60px;
height: 60px;
background: linear-gradient(135deg, #e53e3e 0%, #c53030 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 20px;
color: white;
font-size: 1.5rem;
font-weight: bold;
}
@media print {
body {
margin: 0;
padding: 0;
}
.container {
padding: 20px;
}
.header {
margin: -20px -20px 30px -20px;
}
.day-section {
page-break-inside: avoid;
}
.event {
page-break-inside: avoid;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="company-logo">🚗</div>
<h1>Driver Schedule</h1>
<h2>${scheduleData.driver.name}</h2>
</div>
<div class="driver-info">
<p><strong>Driver:</strong> ${scheduleData.driver.name}</p>
<p><strong>Phone:</strong> ${scheduleData.driver.phone}</p>
<p><strong>Total Assignments:</strong> ${scheduleData.schedule.length}</p>
</div>
${Object.entries(groupedSchedule).map(([date, events]) => `
<div class="day-section">
<div class="day-header">
${new Date(date).toLocaleDateString([], {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</div>
${events.map(event => `
<div class="event">
<div class="event-time">
<span class="time">${formatTime(event.startTime)}</span>
<div class="separator">to</div>
<span class="time">${formatTime(event.endTime)}</span>
</div>
<div class="event-details">
<div class="event-title">
<span class="event-icon">${getTypeIcon(event.type)}</span>
${event.title}
</div>
<div class="event-vip">
<span>👤</span>
VIP: ${event.vipName}
</div>
<div class="event-location">
<span>📍</span>
${event.location}
</div>
${event.description ? `<div class="event-description">${event.description}</div>` : ''}
</div>
</div>
`).join('')}
</div>
`).join('')}
<div class="footer">
<p><strong>VIP Coordinator System</strong></p>
<p>Generated on ${new Date().toLocaleDateString([], {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}</p>
</div>
</div>
</body>
</html>
`;
printWindow.document.write(printContent);
printWindow.document.close();
printWindow.focus();
setTimeout(() => {
printWindow.print();
printWindow.close();
}, 250);
};
async function updateEventStatus(eventId: string, status: string) {
if (!scheduleData) return;
// Find the event to get the VIP ID
const event = scheduleData.schedule.find(e => e.id === eventId);
if (!event) return;
try {
const token = localStorage.getItem('authToken');
const response = await apiCall(`/api/vips/${event.vipId}/schedule/${eventId}/status`, {
method: 'PATCH',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ status }),
});
if (response.ok) {
await fetchDriverSchedule(); // Refresh the schedule
}
} catch (error) {
console.error('Error updating event status:', 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-red-600 border-t-transparent rounded-full animate-spin"></div>
<span className="text-lg font-medium text-slate-700">Loading driver schedule...</span>
</div>
</div>
);
}
if (error || !scheduleData) {
return (
<div className="space-y-8">
<div className="bg-white rounded-2xl shadow-lg p-8 border border-slate-200/60 text-center">
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
<span className="text-2xl"></span>
</div>
<h1 className="text-2xl font-bold text-slate-800 mb-2">Error</h1>
<p className="text-slate-600 mb-6">{error || 'Driver not found'}</p>
<Link
to="/drivers"
className="bg-gradient-to-r from-slate-500 to-slate-600 hover:from-slate-600 hover:to-slate-700 text-white px-6 py-3 rounded-lg font-medium transition-all duration-200 shadow-lg hover:shadow-xl"
>
Back to Drivers
</Link>
</div>
</div>
);
}
const nextEvent = getNextEvent();
const currentEvent = getCurrentEvent();
const groupedSchedule = groupEventsByDay(scheduleData.schedule);
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 flex items-center gap-3">
🚗 Driver Dashboard: {scheduleData.driver.name}
</h1>
<p className="text-slate-600 mt-2">Real-time schedule and assignment management</p>
</div>
<div className="flex items-center space-x-4">
<button
className="bg-gradient-to-r from-red-500 to-red-600 hover:from-red-600 hover:to-red-700 text-white px-6 py-3 rounded-lg font-medium transition-all duration-200 shadow-lg hover:shadow-xl flex items-center gap-2"
onClick={handlePrintSchedule}
>
🖨 Print Schedule
</button>
<Link
to="/drivers"
className="bg-gradient-to-r from-slate-500 to-slate-600 hover:from-slate-600 hover:to-slate-700 text-white px-6 py-3 rounded-lg font-medium transition-all duration-200 shadow-lg hover:shadow-xl"
>
Back to Drivers
</Link>
</div>
</div>
</div>
{/* Current Status */}
<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 flex items-center gap-2">
📍 Current Status
</h2>
<p className="text-slate-600 mt-1">Real-time driver activity and next assignment</p>
</div>
<div className="p-8 space-y-6">
{currentEvent ? (
<div className="bg-gradient-to-r from-amber-50 to-orange-50 border border-amber-200 rounded-xl p-6">
<div className="flex items-center gap-3 mb-4">
<span className="text-2xl">{getTypeIcon(currentEvent.type)}</span>
<div>
<h3 className="text-lg font-bold text-amber-900">Currently Active</h3>
<p className="text-amber-700 font-semibold">{currentEvent.title}</p>
</div>
<span
className="ml-auto px-3 py-1 rounded-full text-xs font-bold text-white"
style={{ backgroundColor: getStatusColor(currentEvent.status) }}
>
{currentEvent.status.toUpperCase()}
</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
<div className="flex items-center gap-2 text-amber-800">
<span>📍</span>
<span>{currentEvent.location}</span>
</div>
<div className="flex items-center gap-2 text-amber-800">
<span>👤</span>
<span>VIP: {currentEvent.vipName}</span>
</div>
<div className="flex items-center gap-2 text-amber-800">
<span></span>
<span>Until {formatTime(currentEvent.endTime)}</span>
</div>
</div>
</div>
) : (
<div className="bg-gradient-to-r from-green-50 to-emerald-50 border border-green-200 rounded-xl p-6">
<div className="flex items-center gap-3">
<span className="text-2xl"></span>
<div>
<h3 className="text-lg font-bold text-green-900">Currently Available</h3>
<p className="text-green-700">Ready for next assignment</p>
</div>
</div>
</div>
)}
{nextEvent && (
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-200 rounded-xl p-6">
<div className="flex items-center gap-3 mb-4">
<span className="text-2xl">{getTypeIcon(nextEvent.type)}</span>
<div>
<h3 className="text-lg font-bold text-blue-900">Next Assignment</h3>
<p className="text-blue-700 font-semibold">{nextEvent.title}</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm mb-4">
<div className="flex items-center gap-2 text-blue-800">
<span>📍</span>
<span>{nextEvent.location}</span>
</div>
<div className="flex items-center gap-2 text-blue-800">
<span>👤</span>
<span>VIP: {nextEvent.vipName}</span>
</div>
<div className="flex items-center gap-2 text-blue-800">
<span></span>
<span>{formatTime(nextEvent.startTime)} - {formatTime(nextEvent.endTime)}</span>
</div>
</div>
<button
className="bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 text-white px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 shadow-lg hover:shadow-xl flex items-center gap-2"
onClick={() => window.open(`https://maps.google.com/?q=${encodeURIComponent(nextEvent.location)}`, '_blank')}
>
🗺 Get Directions
</button>
</div>
)}
</div>
</div>
{/* Full Schedule */}
<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 flex items-center gap-2">
📅 Complete Schedule
<span className="bg-purple-100 text-purple-800 text-sm font-medium px-2.5 py-0.5 rounded-full">
{scheduleData.schedule.length} assignments
</span>
</h2>
<p className="text-slate-600 mt-1">All scheduled events and assignments</p>
</div>
<div className="p-8">
{scheduleData.schedule.length === 0 ? (
<div className="text-center py-12">
<div className="w-16 h-16 bg-slate-100 rounded-full flex items-center justify-center mx-auto mb-4">
<span className="text-2xl">📅</span>
</div>
<p className="text-slate-500 font-medium">No assignments scheduled</p>
</div>
) : (
<div className="space-y-8">
{Object.entries(groupedSchedule).map(([date, events]) => (
<div key={date} className="space-y-4">
<div className="bg-gradient-to-r from-slate-600 to-slate-700 text-white px-6 py-3 rounded-xl shadow-lg">
<h3 className="text-lg font-bold">
{new Date(date).toLocaleDateString([], {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</h3>
</div>
<div className="grid gap-4">
{events.map((event) => (
<div key={event.id} className="bg-gradient-to-r from-slate-50 to-slate-100 rounded-xl border border-slate-200/60 p-6 hover:shadow-lg transition-all duration-200">
<div className="flex items-start gap-6">
{/* Time Column */}
<div className="flex-shrink-0 text-center">
<div className="bg-white rounded-lg border border-slate-200 p-3 shadow-sm">
<div className="text-sm font-bold text-slate-900">
{formatTime(event.startTime)}
</div>
<div className="text-xs text-slate-500 mt-1">
to
</div>
<div className="text-sm font-bold text-slate-900">
{formatTime(event.endTime)}
</div>
</div>
</div>
{/* Event Content */}
<div className="flex-1">
<div className="flex items-center gap-3 mb-3">
<span className="text-2xl">{getTypeIcon(event.type)}</span>
<h4 className="text-lg font-bold text-slate-900">{event.title}</h4>
<span
className="px-3 py-1 rounded-full text-xs font-bold text-white shadow-sm"
style={{ backgroundColor: getStatusColor(event.status) }}
>
{event.status.toUpperCase()}
</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 mb-4">
<div className="flex items-center gap-2 text-slate-600">
<span>📍</span>
<span className="font-medium">{event.location}</span>
</div>
<div className="flex items-center gap-2 text-slate-600">
<span>👤</span>
<span className="font-medium">VIP: {event.vipName}</span>
</div>
</div>
{event.description && (
<div className="text-slate-600 mb-4 bg-white/50 rounded-lg p-3 border border-slate-200/50">
{event.description}
</div>
)}
<div className="flex items-center gap-3">
<button
className="bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 text-white px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 shadow-lg hover:shadow-xl flex items-center gap-2"
onClick={() => window.open(`https://maps.google.com/?q=${encodeURIComponent(event.location)}`, '_blank')}
>
🗺 Directions
</button>
{event.status === 'scheduled' && (
<button
className="bg-gradient-to-r from-amber-500 to-orange-500 hover:from-amber-600 hover:to-orange-600 text-white px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 shadow-lg hover:shadow-xl flex items-center gap-2"
onClick={() => updateEventStatus(event.id, 'in-progress')}
>
Start
</button>
)}
{event.status === 'in-progress' && (
<button
className="bg-gradient-to-r from-green-500 to-green-600 hover:from-green-600 hover:to-green-700 text-white px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 shadow-lg hover:shadow-xl flex items-center gap-2"
onClick={() => updateEventStatus(event.id, 'completed')}
>
Complete
</button>
)}
{event.status === 'completed' && (
<span className="bg-green-100 text-green-800 px-3 py-1 rounded-full text-xs font-medium flex items-center gap-1">
Completed
</span>
)}
</div>
</div>
</div>
</div>
))}
</div>
</div>
))}
</div>
)}
</div>
</div>
{/* Gantt Chart */}
<div className="bg-white rounded-2xl shadow-lg border border-slate-200/60 overflow-hidden">
<div className="bg-gradient-to-r from-indigo-50 to-purple-50 px-8 py-6 border-b border-slate-200/60">
<h2 className="text-xl font-bold text-slate-800 flex items-center gap-2">
📊 Schedule Timeline
</h2>
<p className="text-slate-600 mt-1">Visual timeline of all assignments</p>
</div>
<div className="p-8">
<GanttChart
events={scheduleData.schedule}
driverName={scheduleData.driver.name}
/>
</div>
</div>
</div>
);
};
export default DriverDashboard;

View File

@@ -0,0 +1,293 @@
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { apiCall } from '../config/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;
}
const DriverList: React.FC = () => {
const [drivers, setDrivers] = useState<Driver[]>([]);
const [loading, setLoading] = useState(true);
const [showForm, setShowForm] = useState(false);
const [editingDriver, setEditingDriver] = useState<Driver | null>(null);
// Function to extract last name for sorting
const getLastName = (fullName: string) => {
const nameParts = fullName.trim().split(' ');
return nameParts[nameParts.length - 1].toLowerCase();
};
// Function to sort drivers by last name
const sortDriversByLastName = (driverList: Driver[]) => {
return [...driverList].sort((a, b) => {
const lastNameA = getLastName(a.name);
const lastNameB = getLastName(b.name);
return lastNameA.localeCompare(lastNameB);
});
};
useEffect(() => {
const fetchDrivers = async () => {
try {
const token = localStorage.getItem('authToken');
const response = await apiCall('/api/drivers', {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
const sortedDrivers = sortDriversByLastName(data);
setDrivers(sortedDrivers);
} else {
console.error('Failed to fetch drivers:', response.status);
}
} catch (error) {
console.error('Error fetching drivers:', error);
} finally {
setLoading(false);
}
};
fetchDrivers();
}, []);
const handleAddDriver = async (driverData: any) => {
try {
const token = localStorage.getItem('authToken');
const response = await apiCall('/api/drivers', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(driverData),
});
if (response.ok) {
const newDriver = await response.json();
setDrivers(prev => sortDriversByLastName([...prev, newDriver]));
setShowForm(false);
} else {
console.error('Failed to add driver:', response.status);
}
} catch (error) {
console.error('Error adding driver:', error);
}
};
const handleEditDriver = async (driverData: any) => {
try {
const token = localStorage.getItem('authToken');
const response = await apiCall(`/api/drivers/${driverData.id}`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(driverData),
});
if (response.ok) {
const updatedDriver = await response.json();
setDrivers(prev => sortDriversByLastName(prev.map(driver =>
driver.id === updatedDriver.id ? updatedDriver : driver
)));
setEditingDriver(null);
} else {
console.error('Failed to update driver:', response.status);
}
} catch (error) {
console.error('Error updating driver:', error);
}
};
const handleDeleteDriver = async (driverId: string) => {
if (!confirm('Are you sure you want to delete this driver?')) {
return;
}
try {
const token = localStorage.getItem('authToken');
const response = await apiCall(`/api/drivers/${driverId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
setDrivers(prev => prev.filter(driver => driver.id !== driverId));
} else {
console.error('Failed to delete driver:', response.status);
}
} catch (error) {
console.error('Error deleting 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>
);
}
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">
Driver Management
</h1>
<p className="text-slate-600 mt-2">Manage driver profiles and assignments</p>
</div>
<div className="flex items-center space-x-4">
<div className="bg-gradient-to-r from-green-500 to-green-600 text-white px-4 py-2 rounded-lg text-sm font-medium">
{drivers.length} Active Drivers
</div>
<button
className="btn btn-primary"
onClick={() => setShowForm(true)}
>
Add New Driver
</button>
</div>
</div>
</div>
{/* Driver Grid */}
{drivers.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>
<button
className="btn btn-primary"
onClick={() => setShowForm(true)}
>
Add New Driver
</button>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{drivers.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 */}
<div className="flex items-center justify-between mb-4">
<h3 className="text-xl font-bold text-slate-900">{driver.name}</h3>
<div className="w-10 h-10 bg-gradient-to-br from-green-400 to-green-600 rounded-full flex items-center justify-center">
<span className="text-white text-sm font-bold">
{driver.name.charAt(0).toUpperCase()}
</span>
</div>
</div>
{/* Driver Information */}
<div className="space-y-3 mb-6">
<div className="bg-slate-50 rounded-lg p-3">
<div className="text-sm font-medium text-slate-700 mb-1">Contact</div>
<div className="text-slate-600">{driver.phone}</div>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<div className="text-sm font-medium text-slate-700 mb-1">Current Location</div>
<div className="text-slate-600 text-sm">
{driver.currentLocation.lat.toFixed(4)}, {driver.currentLocation.lng.toFixed(4)}
</div>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<div className="text-sm font-medium text-slate-700 mb-1">Vehicle Capacity</div>
<div className="flex items-center gap-2 text-slate-600">
<span>🚗</span>
<span className="font-medium">{driver.vehicleCapacity || 4} passengers</span>
</div>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<div className="text-sm font-medium text-slate-700 mb-1">Assignments</div>
<div className="flex items-center gap-2">
<span className="bg-blue-100 text-blue-800 text-xs font-medium px-2 py-1 rounded-full">
{driver.assignedVipIds.length} VIPs
</span>
<span className={`text-xs font-medium px-2 py-1 rounded-full ${
driver.assignedVipIds.length === 0
? 'bg-green-100 text-green-800'
: 'bg-amber-100 text-amber-800'
}`}>
{driver.assignedVipIds.length === 0 ? 'Available' : 'Assigned'}
</span>
</div>
</div>
</div>
{/* Action Buttons */}
<div className="space-y-3">
<Link
to={`/drivers/${driver.id}`}
className="bg-gradient-to-r from-green-500 to-green-600 hover:from-green-600 hover:to-green-700 text-white px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 shadow-sm hover:shadow-md w-full text-center block"
>
View Dashboard
</Link>
<div className="flex gap-2">
<button
className="bg-gradient-to-r from-slate-500 to-slate-600 hover:from-slate-600 hover:to-slate-700 text-white px-3 py-2 rounded-lg text-xs font-medium transition-all duration-200 shadow-sm hover:shadow-md flex-1"
onClick={() => setEditingDriver(driver)}
>
Edit
</button>
<button
className="bg-gradient-to-r from-red-500 to-red-600 hover:from-red-600 hover:to-red-700 text-white px-3 py-2 rounded-lg text-xs font-medium transition-all duration-200 shadow-sm hover:shadow-md flex-1"
onClick={() => handleDeleteDriver(driver.id)}
>
Delete
</button>
</div>
</div>
</div>
</div>
))}
</div>
)}
{/* Modals */}
{showForm && (
<DriverForm
onSubmit={handleAddDriver}
onCancel={() => setShowForm(false)}
/>
)}
{editingDriver && (
<EditDriverForm
driver={editingDriver}
onSubmit={handleEditDriver}
onCancel={() => setEditingDriver(null)}
/>
)}
</div>
);
};
export default DriverList;

View File

@@ -0,0 +1,683 @@
import React, { useState, useEffect } from 'react';
import { useParams, Link } from 'react-router-dom';
import { apiCall } from '../config/api';
import FlightStatus from '../components/FlightStatus';
import ScheduleManager from '../components/ScheduleManager';
interface Flight {
flightNumber: string;
flightDate: string;
segment: number;
}
interface Vip {
id: string;
name: string;
organization: string;
transportMode: 'flight' | 'self-driving';
flightNumber?: string; // Legacy
flightDate?: string; // Legacy
flights?: Flight[]; // New
expectedArrival?: string;
arrivalTime?: string; // Legacy
needsAirportPickup?: boolean;
needsVenueTransport: boolean;
notes: string;
assignedDriverIds: string[];
}
const VipDetails: React.FC = () => {
const { id } = useParams<{ id: string }>();
const [vip, setVip] = useState<Vip | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [schedule, setSchedule] = useState<any[]>([]);
useEffect(() => {
const fetchVip = async () => {
try {
const token = localStorage.getItem('authToken');
const response = await apiCall('/api/vips', {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const vips = await response.json();
const foundVip = vips.find((v: Vip) => v.id === id);
if (foundVip) {
setVip(foundVip);
} else {
setError('VIP not found');
}
} else {
setError('Failed to fetch VIP data');
}
} catch (err) {
setError('Error loading VIP data');
} finally {
setLoading(false);
}
};
if (id) {
fetchVip();
}
}, [id]);
// Fetch schedule data
useEffect(() => {
const fetchSchedule = async () => {
if (vip) {
try {
const token = localStorage.getItem('authToken');
const response = await apiCall(`/api/vips/${vip.id}/schedule`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const scheduleData = await response.json();
setSchedule(scheduleData);
}
} catch (error) {
console.error('Error fetching schedule:', error);
}
}
};
fetchSchedule();
}, [vip]);
// Auto-scroll to schedule section if accessed via #schedule anchor
useEffect(() => {
if (vip && window.location.hash === '#schedule') {
setTimeout(() => {
const scheduleElement = document.getElementById('schedule-section');
if (scheduleElement) {
scheduleElement.scrollIntoView({ behavior: 'smooth' });
}
}, 100);
}
}, [vip]);
// Helper function to get flight info
const getFlightInfo = () => {
if (!vip) return null;
if (vip.transportMode === 'flight') {
if (vip.flights && vip.flights.length > 0) {
return {
flights: vip.flights,
primaryFlight: vip.flights[0]
};
} else if (vip.flightNumber) {
// Legacy support
return {
flights: [{
flightNumber: vip.flightNumber,
flightDate: vip.flightDate || '',
segment: 1
}],
primaryFlight: {
flightNumber: vip.flightNumber,
flightDate: vip.flightDate || '',
segment: 1
}
};
}
}
return null;
};
const handlePrintSchedule = () => {
if (!vip) return;
const printWindow = window.open('', '_blank');
if (!printWindow) return;
const groupEventsByDay = (events: any[]) => {
const grouped: { [key: string]: any[] } = {};
events.forEach(event => {
const date = new Date(event.startTime).toDateString();
if (!grouped[date]) {
grouped[date] = [];
}
grouped[date].push(event);
});
Object.keys(grouped).forEach(date => {
grouped[date].sort((a, b) => new Date(a.startTime).getTime() - new Date(b.startTime).getTime());
});
return grouped;
};
const formatTime = (timeString: string) => {
return new Date(timeString).toLocaleString([], {
hour: '2-digit',
minute: '2-digit'
});
};
const getTypeIcon = (type: string) => {
switch (type) {
case 'transport': return '🚗';
case 'meeting': return '🤝';
case 'event': return '🎉';
case 'meal': return '🍽️';
case 'accommodation': return '🏨';
default: return '📅';
}
};
const groupedSchedule = groupEventsByDay(schedule);
const flightInfo = getFlightInfo();
const printContent = `
<!DOCTYPE html>
<html>
<head>
<title>VIP Schedule - ${vip.name}</title>
<meta charset="UTF-8">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: #2d3748;
background: #ffffff;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 40px 30px;
}
.header {
text-align: center;
margin-bottom: 40px;
padding-bottom: 30px;
border-bottom: 3px solid #e2e8f0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 40px 30px;
border-radius: 15px;
margin: -40px -30px 40px -30px;
}
.header h1 {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 10px;
text-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.header h2 {
font-size: 1.8rem;
font-weight: 600;
margin-bottom: 20px;
opacity: 0.95;
}
.vip-info {
background: linear-gradient(135deg, #f7fafc 0%, #edf2f7 100%);
padding: 25px;
border-radius: 12px;
margin-bottom: 30px;
border: 1px solid #e2e8f0;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.vip-info p {
margin-bottom: 8px;
font-size: 1rem;
}
.vip-info strong {
color: #4a5568;
font-weight: 600;
}
.day-section {
margin-bottom: 40px;
page-break-inside: avoid;
}
.day-header {
background: linear-gradient(135deg, #4a5568 0%, #2d3748 100%);
color: white;
padding: 20px 25px;
font-size: 1.3rem;
font-weight: 700;
margin-bottom: 20px;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
text-align: center;
}
.event {
background: white;
border: 1px solid #e2e8f0;
margin-bottom: 15px;
padding: 25px;
border-radius: 12px;
display: flex;
align-items: flex-start;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
transition: all 0.2s ease;
}
.event:hover {
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.event-time {
min-width: 120px;
background: linear-gradient(135deg, #edf2f7 0%, #e2e8f0 100%);
padding: 15px;
border-radius: 8px;
text-align: center;
margin-right: 25px;
border: 1px solid #cbd5e0;
}
.event-time .time {
font-weight: 700;
font-size: 1rem;
color: #2d3748;
display: block;
}
.event-time .separator {
font-size: 0.8rem;
color: #718096;
margin: 5px 0;
}
.event-details {
flex: 1;
}
.event-title {
font-weight: 700;
font-size: 1.2rem;
margin-bottom: 10px;
color: #2d3748;
display: flex;
align-items: center;
gap: 8px;
}
.event-icon {
font-size: 1.3rem;
}
.event-location {
color: #4a5568;
margin-bottom: 8px;
font-weight: 500;
display: flex;
align-items: center;
gap: 6px;
}
.event-description {
background: #f7fafc;
padding: 12px 15px;
border-radius: 8px;
font-style: italic;
color: #4a5568;
margin-bottom: 10px;
border-left: 4px solid #cbd5e0;
}
.event-driver {
color: #3182ce;
font-weight: 600;
font-size: 0.9rem;
background: #ebf8ff;
padding: 8px 12px;
border-radius: 6px;
display: inline-flex;
align-items: center;
gap: 6px;
}
.status-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.status-scheduled {
background: #bee3f8;
color: #2b6cb0;
}
.status-in-progress {
background: #fbd38d;
color: #c05621;
}
.status-completed {
background: #c6f6d5;
color: #276749;
}
.footer {
margin-top: 50px;
text-align: center;
color: #718096;
font-size: 0.9rem;
padding-top: 20px;
border-top: 1px solid #e2e8f0;
}
.company-logo {
width: 60px;
height: 60px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 20px;
color: white;
font-size: 1.5rem;
font-weight: bold;
}
@media print {
body {
margin: 0;
padding: 0;
}
.container {
padding: 20px;
}
.header {
margin: -20px -20px 30px -20px;
}
.day-section {
page-break-inside: avoid;
}
.event {
page-break-inside: avoid;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="company-logo">VC</div>
<h1>📅 VIP Schedule</h1>
<h2>${vip.name}</h2>
</div>
<div class="vip-info">
<p><strong>Organization:</strong> ${vip.organization}</p>
${vip.transportMode === 'flight' && flightInfo ? `
<p><strong>Flight Information:</strong> ${flightInfo.flights.map(f => f.flightNumber).join(' → ')}</p>
<p><strong>Flight Date:</strong> ${flightInfo.primaryFlight.flightDate ? new Date(flightInfo.primaryFlight.flightDate).toLocaleDateString() : 'TBD'}</p>
` : vip.transportMode === 'self-driving' ? `
<p><strong>Transport Mode:</strong> 🚗 Self-Driving</p>
<p><strong>Expected Arrival:</strong> ${vip.expectedArrival ? new Date(vip.expectedArrival).toLocaleString() : 'TBD'}</p>
` : ''}
<p><strong>Airport Pickup:</strong> ${vip.needsAirportPickup ? '✅ Required' : '❌ Not Required'}</p>
<p><strong>Venue Transport:</strong> ${vip.needsVenueTransport ? '✅ Required' : '❌ Not Required'}</p>
${vip.notes ? `<p><strong>Special Notes:</strong> ${vip.notes}</p>` : ''}
</div>
${Object.entries(groupedSchedule).map(([date, events]) => `
<div class="day-section">
<div class="day-header">
${new Date(date).toLocaleDateString([], {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</div>
${events.map(event => `
<div class="event">
<div class="event-time">
<span class="time">${formatTime(event.startTime)}</span>
<div class="separator">to</div>
<span class="time">${formatTime(event.endTime)}</span>
</div>
<div class="event-details">
<div class="event-title">
<span class="event-icon">${getTypeIcon(event.type)}</span>
${event.title}
<span class="status-badge status-${event.status}">${event.status}</span>
</div>
<div class="event-location">
<span>📍</span>
${event.location}
</div>
${event.description ? `<div class="event-description">${event.description}</div>` : ''}
${event.assignedDriverId ? `<div class="event-driver"><span>👤</span> Driver: ${event.assignedDriverId}</div>` : ''}
</div>
</div>
`).join('')}
</div>
`).join('')}
<div class="footer">
<p><strong>VIP Coordinator System</strong></p>
<p>Generated on ${new Date().toLocaleDateString([], {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}</p>
</div>
</div>
</body>
</html>
`;
printWindow.document.write(printContent);
printWindow.document.close();
printWindow.focus();
setTimeout(() => {
printWindow.print();
printWindow.close();
}, 250);
};
if (loading) {
return <div>Loading VIP details...</div>;
}
if (error || !vip) {
return (
<div>
<h1>Error</h1>
<p>{error || 'VIP not found'}</p>
<Link to="/vips" className="btn">Back to VIP List</Link>
</div>
);
}
const flightInfo = getFlightInfo();
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">
VIP Details: {vip.name}
</h1>
<p className="text-slate-600 mt-2">Complete profile and schedule management</p>
</div>
<div className="flex items-center space-x-4">
<button
className="bg-gradient-to-r from-purple-500 to-purple-600 hover:from-purple-600 hover:to-purple-700 text-white px-6 py-3 rounded-lg font-medium transition-all duration-200 shadow-lg hover:shadow-xl flex items-center gap-2"
onClick={handlePrintSchedule}
>
🖨 Print Schedule
</button>
<Link
to="/vips"
className="bg-gradient-to-r from-slate-500 to-slate-600 hover:from-slate-600 hover:to-slate-700 text-white px-6 py-3 rounded-lg font-medium transition-all duration-200 shadow-lg hover:shadow-xl"
>
Back to VIP List
</Link>
</div>
</div>
</div>
{/* VIP Information Card */}
<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 flex items-center gap-2">
📋 VIP Information
</h2>
<p className="text-slate-600 mt-1">Personal details and travel arrangements</p>
</div>
<div className="p-8">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-slate-50 rounded-xl p-4 border border-slate-200/60">
<div className="text-sm font-medium text-slate-500 mb-1">Name</div>
<div className="text-lg font-bold text-slate-900">{vip.name}</div>
</div>
<div className="bg-slate-50 rounded-xl p-4 border border-slate-200/60">
<div className="text-sm font-medium text-slate-500 mb-1">Organization</div>
<div className="text-lg font-bold text-slate-900">{vip.organization}</div>
</div>
{vip.transportMode === 'flight' && flightInfo ? (
<>
<div className="bg-blue-50 rounded-xl p-4 border border-blue-200/60">
<div className="text-sm font-medium text-blue-600 mb-1">Flight{flightInfo.flights.length > 1 ? 's' : ''}</div>
<div className="text-lg font-bold text-blue-900">{flightInfo.flights.map(f => f.flightNumber).join(' → ')}</div>
</div>
<div className="bg-blue-50 rounded-xl p-4 border border-blue-200/60">
<div className="text-sm font-medium text-blue-600 mb-1">Flight Date</div>
<div className="text-lg font-bold text-blue-900">
{flightInfo.primaryFlight.flightDate ? new Date(flightInfo.primaryFlight.flightDate).toLocaleDateString() : 'TBD'}
</div>
</div>
</>
) : vip.transportMode === 'self-driving' ? (
<>
<div className="bg-green-50 rounded-xl p-4 border border-green-200/60">
<div className="text-sm font-medium text-green-600 mb-1">Transport Mode</div>
<div className="text-lg font-bold text-green-900 flex items-center gap-2">
🚗 Self-Driving
</div>
</div>
<div className="bg-green-50 rounded-xl p-4 border border-green-200/60">
<div className="text-sm font-medium text-green-600 mb-1">Expected Arrival</div>
<div className="text-lg font-bold text-green-900">
{vip.expectedArrival ? new Date(vip.expectedArrival).toLocaleString() : 'TBD'}
</div>
</div>
</>
) : null}
<div className={`rounded-xl p-4 border ${vip.needsAirportPickup ? 'bg-green-50 border-green-200/60' : 'bg-red-50 border-red-200/60'}`}>
<div className={`text-sm font-medium mb-1 ${vip.needsAirportPickup ? 'text-green-600' : 'text-red-600'}`}>Airport Pickup</div>
<div className={`text-lg font-bold flex items-center gap-2 ${vip.needsAirportPickup ? 'text-green-900' : 'text-red-900'}`}>
{vip.needsAirportPickup ? '✅ Required' : '❌ Not Required'}
</div>
</div>
<div className={`rounded-xl p-4 border ${vip.needsVenueTransport ? 'bg-green-50 border-green-200/60' : 'bg-red-50 border-red-200/60'}`}>
<div className={`text-sm font-medium mb-1 ${vip.needsVenueTransport ? 'text-green-600' : 'text-red-600'}`}>Venue Transport</div>
<div className={`text-lg font-bold flex items-center gap-2 ${vip.needsVenueTransport ? 'text-green-900' : 'text-red-900'}`}>
{vip.needsVenueTransport ? '✅ Required' : '❌ Not Required'}
</div>
</div>
</div>
{vip.notes && (
<div className="mt-6">
<div className="text-sm font-medium text-slate-500 mb-2">Special Notes</div>
<div className="bg-amber-50 border border-amber-200 rounded-xl p-4">
<p className="text-amber-800">{vip.notes}</p>
</div>
</div>
)}
{vip.assignedDriverIds && vip.assignedDriverIds.length > 0 && (
<div className="mt-6">
<div className="text-sm font-medium text-slate-500 mb-2">Assigned Drivers</div>
<div className="flex flex-wrap gap-2">
{vip.assignedDriverIds.map(driverId => (
<span
key={driverId}
className="bg-gradient-to-r from-blue-500 to-blue-600 text-white px-4 py-2 rounded-full text-sm font-medium flex items-center gap-2"
>
👤 {driverId}
</span>
))}
</div>
</div>
)}
</div>
</div>
{/* Flight Status */}
{vip.transportMode === 'flight' && flightInfo && (
<div className="bg-white rounded-2xl shadow-lg border border-slate-200/60 overflow-hidden">
<div className="bg-gradient-to-r from-sky-50 to-blue-50 px-8 py-6 border-b border-slate-200/60">
<h2 className="text-xl font-bold text-slate-800 flex items-center gap-2">
Flight Information
</h2>
<p className="text-slate-600 mt-1">Real-time flight status and details</p>
</div>
<div className="p-8 space-y-6">
{flightInfo.flights.map((flight, index) => (
<div key={index} className="bg-slate-50 rounded-xl p-6 border border-slate-200/60">
<h3 className="text-lg font-bold text-slate-900 mb-4">
{index === 0 ? 'Primary Flight' : `Connecting Flight ${index}`}: {flight.flightNumber}
</h3>
<FlightStatus
flightNumber={flight.flightNumber}
flightDate={flight.flightDate}
/>
</div>
))}
</div>
</div>
)}
{/* Schedule Management */}
<div id="schedule-section">
<ScheduleManager vipId={vip.id} vipName={vip.name} />
</div>
</div>
);
};
export default VipDetails;

View File

@@ -0,0 +1,311 @@
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { apiCall } from '../config/api';
import VipForm from '../components/VipForm';
import EditVipForm from '../components/EditVipForm';
import FlightStatus from '../components/FlightStatus';
interface Vip {
id: string;
name: string;
organization: string;
department: 'Office of Development' | 'Admin';
transportMode: 'flight' | 'self-driving';
flightNumber?: string; // Legacy
flightDate?: string; // Legacy
flights?: Array<{
flightNumber: string;
flightDate: string;
segment: number;
}>; // New
expectedArrival?: string;
arrivalTime?: string;
needsAirportPickup?: boolean;
needsVenueTransport: boolean;
notes?: string;
}
const VipList: React.FC = () => {
const [vips, setVips] = useState<Vip[]>([]);
const [loading, setLoading] = useState(true);
const [showForm, setShowForm] = useState(false);
const [editingVip, setEditingVip] = useState<Vip | null>(null);
// Function to extract last name for sorting
const getLastName = (fullName: string) => {
const nameParts = fullName.trim().split(' ');
return nameParts[nameParts.length - 1].toLowerCase();
};
// Function to sort VIPs by last name
const sortVipsByLastName = (vipList: Vip[]) => {
return [...vipList].sort((a, b) => {
const lastNameA = getLastName(a.name);
const lastNameB = getLastName(b.name);
return lastNameA.localeCompare(lastNameB);
});
};
useEffect(() => {
const fetchVips = async () => {
try {
const token = localStorage.getItem('authToken');
const response = await apiCall('/api/vips', {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
const sortedVips = sortVipsByLastName(data);
setVips(sortedVips);
} else {
console.error('Failed to fetch VIPs:', response.status);
}
} catch (error) {
console.error('Error fetching VIPs:', error);
} finally {
setLoading(false);
}
};
fetchVips();
}, []);
const handleAddVip = async (vipData: any) => {
try {
const token = localStorage.getItem('authToken');
const response = await apiCall('/api/vips', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(vipData),
});
if (response.ok) {
const newVip = await response.json();
setVips(prev => sortVipsByLastName([...prev, newVip]));
setShowForm(false);
} else {
console.error('Failed to add VIP:', response.status);
}
} catch (error) {
console.error('Error adding VIP:', error);
}
};
const handleEditVip = async (vipData: any) => {
try {
const token = localStorage.getItem('authToken');
const response = await apiCall(`/api/vips/${vipData.id}`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(vipData),
});
if (response.ok) {
const updatedVip = await response.json();
setVips(prev => sortVipsByLastName(prev.map(vip => vip.id === updatedVip.id ? updatedVip : vip)));
setEditingVip(null);
} else {
console.error('Failed to update VIP:', response.status);
}
} catch (error) {
console.error('Error updating VIP:', error);
}
};
const handleDeleteVip = async (vipId: string) => {
if (!confirm('Are you sure you want to delete this VIP?')) {
return;
}
try {
const token = localStorage.getItem('authToken');
const response = await apiCall(`/api/vips/${vipId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
setVips(prev => prev.filter(vip => vip.id !== vipId));
} else {
console.error('Failed to delete VIP:', response.status);
}
} catch (error) {
console.error('Error deleting 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>
);
}
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">
VIP Management
</h1>
<p className="text-slate-600 mt-2">Manage VIP profiles and travel arrangements</p>
</div>
<button
className="btn btn-primary"
onClick={() => setShowForm(true)}
>
Add New VIP
</button>
</div>
</div>
{/* VIP List */}
{vips.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>
<button
className="btn btn-primary"
onClick={() => setShowForm(true)}
>
Add New VIP
</button>
</div>
) : (
<div className="space-y-4">
{vips.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">
<div className="flex-1">
<div className="flex items-center gap-3 mb-3">
<h3 className="text-xl font-bold text-slate-900">{vip.name}</h3>
<span className="bg-blue-100 text-blue-800 text-xs font-medium px-2.5 py-0.5 rounded-full">
{vip.department}
</span>
</div>
<p className="text-slate-600 text-sm mb-4">{vip.organization}</p>
{/* Transport Information */}
<div className="bg-slate-50 rounded-lg p-4 mb-4">
{vip.transportMode === 'flight' ? (
<div className="space-y-2">
<div className="flex items-center gap-2 text-sm">
<span className="font-medium text-slate-700">Flight:</span>
<span className="text-slate-600">
{vip.flights && vip.flights.length > 0 ?
vip.flights.map(f => f.flightNumber).join(' → ') :
vip.flightNumber || 'No flight'}
</span>
</div>
<div className="flex items-center gap-2 text-sm">
<span className="font-medium text-slate-700">Airport Pickup:</span>
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
vip.needsAirportPickup
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
}`}>
{vip.needsAirportPickup ? 'Required' : 'Not needed'}
</span>
</div>
</div>
) : (
<div className="flex items-center gap-2 text-sm">
<span className="font-medium text-slate-700">Self-driving, Expected:</span>
<span className="text-slate-600">
{vip.expectedArrival ? new Date(vip.expectedArrival).toLocaleString() : 'TBD'}
</span>
</div>
)}
<div className="flex items-center gap-2 text-sm mt-2">
<span className="font-medium text-slate-700">Venue Transport:</span>
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
vip.needsVenueTransport
? 'bg-blue-100 text-blue-800'
: 'bg-gray-100 text-gray-800'
}`}>
{vip.needsVenueTransport ? 'Required' : 'Not needed'}
</span>
</div>
</div>
</div>
{/* Action Buttons */}
<div className="flex flex-col gap-2 ml-6">
<Link
to={`/vips/${vip.id}`}
className="btn btn-success text-center"
>
View Details
</Link>
<button
className="btn btn-secondary"
onClick={() => setEditingVip(vip)}
>
Edit
</button>
<button
className="btn btn-danger"
onClick={() => handleDeleteVip(vip.id)}
>
Delete
</button>
</div>
</div>
{/* Flight Status */}
{vip.transportMode === 'flight' && vip.flightNumber && (
<div className="mt-4 pt-4 border-t border-slate-200">
<FlightStatus flightNumber={vip.flightNumber} />
</div>
)}
</div>
</div>
))}
</div>
)}
{/* Modals */}
{showForm && (
<VipForm
onSubmit={handleAddVip}
onCancel={() => setShowForm(false)}
/>
)}
{editingVip && (
<EditVipForm
vip={{...editingVip, notes: editingVip.notes || ''}}
onSubmit={handleEditVip}
onCancel={() => setEditingVip(null)}
/>
)}
</div>
);
};
export default VipList;

View File

@@ -0,0 +1,386 @@
// Test VIP data generation utilities
export const generateTestVips = () => {
const today = new Date();
const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1);
const dayAfter = new Date(today);
dayAfter.setDate(dayAfter.getDate() + 2);
const formatDate = (date: Date) => date.toISOString().split('T')[0];
const formatDateTime = (date: Date) => {
const d = new Date(date);
d.setHours(14, 30, 0, 0); // 2:30 PM
return d.toISOString().slice(0, 16);
};
return [
// Admin Department VIPs (10)
{
name: 'Dr. Sarah Chen',
organization: 'Stanford University',
department: 'Admin',
transportMode: 'flight',
flights: [
{ flightNumber: 'UA1234', flightDate: formatDate(tomorrow), segment: 1 },
{ flightNumber: 'DL5678', flightDate: formatDate(tomorrow), segment: 2 }
],
needsAirportPickup: true,
needsVenueTransport: true,
notes: 'Vegetarian meals, requires wheelchair assistance'
},
{
name: 'Ambassador Michael Rodriguez',
organization: 'Embassy of Spain',
department: 'Admin',
transportMode: 'self-driving',
expectedArrival: formatDateTime(tomorrow),
needsVenueTransport: true,
notes: 'Security detail required, diplomatic immunity'
},
{
name: 'Prof. Aisha Patel',
organization: 'MIT Technology Review',
department: 'Admin',
transportMode: 'flight',
flights: [{ flightNumber: 'AA9876', flightDate: formatDate(dayAfter), segment: 1 }],
needsAirportPickup: true,
needsVenueTransport: false,
notes: 'Allergic to shellfish, prefers ground floor rooms'
},
{
name: 'CEO James Thompson',
organization: 'TechCorp Industries',
department: 'Admin',
transportMode: 'flight',
flights: [{ flightNumber: 'SW2468', flightDate: formatDate(tomorrow), segment: 1 }],
needsAirportPickup: false,
needsVenueTransport: true,
notes: 'Private jet arrival, has own security team'
},
{
name: 'Dr. Elena Volkov',
organization: 'Russian Academy of Sciences',
department: 'Admin',
transportMode: 'self-driving',
expectedArrival: formatDateTime(dayAfter),
needsVenueTransport: true,
notes: 'Interpreter required, kosher meals'
},
{
name: 'Minister David Kim',
organization: 'South Korean Ministry of Education',
department: 'Admin',
transportMode: 'flight',
flights: [
{ flightNumber: 'KE0123', flightDate: formatDate(tomorrow), segment: 1 },
{ flightNumber: 'UA7890', flightDate: formatDate(tomorrow), segment: 2 },
{ flightNumber: 'DL3456', flightDate: formatDate(tomorrow), segment: 3 }
],
needsAirportPickup: true,
needsVenueTransport: true,
notes: 'Long international flight, may need rest upon arrival'
},
{
name: 'Dr. Maria Santos',
organization: 'University of São Paulo',
department: 'Admin',
transportMode: 'flight',
flights: [{ flightNumber: 'LH4567', flightDate: formatDate(dayAfter), segment: 1 }],
needsAirportPickup: true,
needsVenueTransport: false,
notes: 'Speaks Portuguese and English, lactose intolerant'
},
{
name: 'Sheikh Ahmed Al-Rashid',
organization: 'UAE University',
department: 'Admin',
transportMode: 'self-driving',
expectedArrival: formatDateTime(tomorrow),
needsVenueTransport: true,
notes: 'Halal meals required, prayer room access needed'
},
{
name: 'Prof. Catherine Williams',
organization: 'Oxford University',
department: 'Admin',
transportMode: 'flight',
flights: [{ flightNumber: 'BA1357', flightDate: formatDate(tomorrow), segment: 1 }],
needsAirportPickup: false,
needsVenueTransport: true,
notes: 'Prefers tea over coffee, has mobility issues'
},
{
name: 'Dr. Hiroshi Tanaka',
organization: 'Tokyo Institute of Technology',
department: 'Admin',
transportMode: 'flight',
flights: [
{ flightNumber: 'NH0246', flightDate: formatDate(dayAfter), segment: 1 },
{ flightNumber: 'UA8642', flightDate: formatDate(dayAfter), segment: 2 }
],
needsAirportPickup: true,
needsVenueTransport: true,
notes: 'Jet lag concerns, prefers Japanese cuisine when available'
},
// Office of Development VIPs (10)
{
name: 'Ms. Jennifer Walsh',
organization: 'Walsh Foundation',
department: 'Office of Development',
transportMode: 'self-driving',
expectedArrival: formatDateTime(tomorrow),
needsVenueTransport: false,
notes: 'Major donor, prefers informal settings'
},
{
name: 'Mr. Robert Sterling',
organization: 'Sterling Philanthropies',
department: 'Office of Development',
transportMode: 'flight',
flights: [{ flightNumber: 'JB1122', flightDate: formatDate(tomorrow), segment: 1 }],
needsAirportPickup: true,
needsVenueTransport: true,
notes: 'Potential $10M donation, wine enthusiast'
},
{
name: 'Mrs. Elizabeth Hartwell',
organization: 'Hartwell Family Trust',
department: 'Office of Development',
transportMode: 'flight',
flights: [{ flightNumber: 'AS3344', flightDate: formatDate(dayAfter), segment: 1 }],
needsAirportPickup: false,
needsVenueTransport: true,
notes: 'Alumni donor, interested in scholarship programs'
},
{
name: 'Mr. Charles Montgomery',
organization: 'Montgomery Industries',
department: 'Office of Development',
transportMode: 'self-driving',
expectedArrival: formatDateTime(dayAfter),
needsVenueTransport: true,
notes: 'Corporate partnership opportunity, golf enthusiast'
},
{
name: 'Dr. Patricia Lee',
organization: 'Lee Medical Foundation',
department: 'Office of Development',
transportMode: 'flight',
flights: [
{ flightNumber: 'F91234', flightDate: formatDate(tomorrow), segment: 1 },
{ flightNumber: 'UA5555', flightDate: formatDate(tomorrow), segment: 2 }
],
needsAirportPickup: true,
needsVenueTransport: false,
notes: 'Medical research funding, diabetic dietary needs'
},
{
name: 'Mr. Thomas Anderson',
organization: 'Anderson Capital Group',
department: 'Office of Development',
transportMode: 'flight',
flights: [{ flightNumber: 'VX7788', flightDate: formatDate(tomorrow), segment: 1 }],
needsAirportPickup: true,
needsVenueTransport: true,
notes: 'Venture capital investor, tech startup focus'
},
{
name: 'Mrs. Grace Chen-Williams',
organization: 'Chen-Williams Foundation',
department: 'Office of Development',
transportMode: 'self-driving',
expectedArrival: formatDateTime(tomorrow),
needsVenueTransport: true,
notes: 'Arts and culture patron, vegan diet'
},
{
name: 'Mr. Daniel Foster',
organization: 'Foster Energy Solutions',
department: 'Office of Development',
transportMode: 'flight',
flights: [{ flightNumber: 'WN9999', flightDate: formatDate(dayAfter), segment: 1 }],
needsAirportPickup: false,
needsVenueTransport: false,
notes: 'Renewable energy focus, environmental sustainability'
},
{
name: 'Mrs. Victoria Blackstone',
organization: 'Blackstone Charitable Trust',
department: 'Office of Development',
transportMode: 'flight',
flights: [
{ flightNumber: 'B61111', flightDate: formatDate(dayAfter), segment: 1 },
{ flightNumber: 'AA2222', flightDate: formatDate(dayAfter), segment: 2 }
],
needsAirportPickup: true,
needsVenueTransport: true,
notes: 'Education advocate, prefers luxury accommodations'
},
{
name: 'Mr. Alexander Petrov',
organization: 'Petrov International Holdings',
department: 'Office of Development',
transportMode: 'self-driving',
expectedArrival: formatDateTime(dayAfter),
needsVenueTransport: true,
notes: 'International business, speaks Russian and English'
}
];
};
export const getTestOrganizations = () => [
'Stanford University', 'Embassy of Spain', 'MIT Technology Review', 'TechCorp Industries',
'Russian Academy of Sciences', 'South Korean Ministry of Education', 'University of São Paulo',
'UAE University', 'Oxford University', 'Tokyo Institute of Technology',
'Walsh Foundation', 'Sterling Philanthropies', 'Hartwell Family Trust', 'Montgomery Industries',
'Lee Medical Foundation', 'Anderson Capital Group', 'Chen-Williams Foundation',
'Foster Energy Solutions', 'Blackstone Charitable Trust', 'Petrov International Holdings'
];
// Generate realistic daily schedules for VIPs
export const generateVipSchedule = (vipName: string, department: string, transportMode: string) => {
const today = new Date();
const eventDate = new Date(today);
eventDate.setDate(eventDate.getDate() + 1); // Tomorrow
const formatDateTime = (hour: number, minute: number = 0) => {
const date = new Date(eventDate);
date.setHours(hour, minute, 0, 0);
return date.toISOString();
};
const baseSchedule = [
// Morning arrival and setup
{
title: transportMode === 'flight' ? 'Airport Pickup' : 'Arrival Check-in',
location: transportMode === 'flight' ? 'Airport Terminal' : 'Hotel Lobby',
startTime: formatDateTime(8, 0),
endTime: formatDateTime(9, 0),
description: transportMode === 'flight' ? 'Meet and greet at airport, transport to hotel' : 'Check-in and welcome briefing',
type: 'transport',
status: 'scheduled'
},
{
title: 'Welcome Breakfast',
location: 'Executive Dining Room',
startTime: formatDateTime(9, 0),
endTime: formatDateTime(10, 0),
description: 'Welcome breakfast with key stakeholders and orientation materials',
type: 'meal',
status: 'scheduled'
}
];
// Department-specific schedules
if (department === 'Admin') {
return [
...baseSchedule,
{
title: 'Academic Leadership Meeting',
location: 'Board Room A',
startTime: formatDateTime(10, 30),
endTime: formatDateTime(12, 0),
description: 'Strategic planning session with academic leadership team',
type: 'meeting',
status: 'scheduled'
},
{
title: 'Working Lunch',
location: 'Faculty Club',
startTime: formatDateTime(12, 0),
endTime: formatDateTime(13, 30),
description: 'Lunch meeting with department heads and key faculty',
type: 'meal',
status: 'scheduled'
},
{
title: 'Campus Tour',
location: 'Main Campus',
startTime: formatDateTime(14, 0),
endTime: formatDateTime(15, 30),
description: 'Guided tour of campus facilities and research centers',
type: 'event',
status: 'scheduled'
},
{
title: 'Research Presentation',
location: 'Auditorium B',
startTime: formatDateTime(16, 0),
endTime: formatDateTime(17, 30),
description: 'Presentation of current research initiatives and future plans',
type: 'meeting',
status: 'scheduled'
},
{
title: 'Reception Dinner',
location: 'University Club',
startTime: formatDateTime(19, 0),
endTime: formatDateTime(21, 0),
description: 'Formal dinner reception with university leadership',
type: 'event',
status: 'scheduled'
}
];
} else {
// Office of Development schedule
return [
...baseSchedule,
{
title: 'Donor Relations Meeting',
location: 'Development Office',
startTime: formatDateTime(10, 30),
endTime: formatDateTime(12, 0),
description: 'Private meeting with development team about giving opportunities',
type: 'meeting',
status: 'scheduled'
},
{
title: 'Scholarship Recipients Lunch',
location: 'Student Center',
startTime: formatDateTime(12, 0),
endTime: formatDateTime(13, 30),
description: 'Meet with current scholarship recipients and hear their stories',
type: 'meal',
status: 'scheduled'
},
{
title: 'Facility Naming Ceremony',
location: 'New Science Building',
startTime: formatDateTime(14, 0),
endTime: formatDateTime(15, 0),
description: 'Dedication ceremony for newly named facility',
type: 'event',
status: 'scheduled'
},
{
title: 'Impact Presentation',
location: 'Conference Room C',
startTime: formatDateTime(15, 30),
endTime: formatDateTime(16, 30),
description: 'Presentation on the impact of philanthropic giving',
type: 'meeting',
status: 'scheduled'
},
{
title: 'Private Dinner',
location: 'Presidents House',
startTime: formatDateTime(18, 30),
endTime: formatDateTime(20, 30),
description: 'Intimate dinner with university president and spouse',
type: 'meal',
status: 'scheduled'
},
{
title: 'Evening Cultural Event',
location: 'Arts Center',
startTime: formatDateTime(21, 0),
endTime: formatDateTime(22, 30),
description: 'Special performance by university arts programs',
type: 'event',
status: 'scheduled'
}
];
}
};

View File

@@ -0,0 +1,12 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
};

25
frontend/tsconfig.json Normal file
View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

54
frontend/vite.config.ts Normal file
View File

@@ -0,0 +1,54 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
css: {
postcss: './postcss.config.js',
},
server: {
host: '0.0.0.0',
port: 5173,
allowedHosts: [
'localhost',
'127.0.0.1',
'bsa.madeamess.online'
],
proxy: {
'/api': {
target: 'http://backend:3000',
changeOrigin: true,
},
// Only proxy specific auth endpoints, not the callback route
'/auth/setup': {
target: 'http://backend:3000',
changeOrigin: true,
},
'/auth/google/url': {
target: 'http://backend:3000',
changeOrigin: true,
},
'/auth/google/exchange': {
target: 'http://backend:3000',
changeOrigin: true,
},
'/auth/me': {
target: 'http://backend:3000',
changeOrigin: true,
},
'/auth/logout': {
target: 'http://backend:3000',
changeOrigin: true,
},
'/auth/status': {
target: 'http://backend:3000',
changeOrigin: true,
},
'/auth/dev-login': {
target: 'http://backend:3000',
changeOrigin: true,
},
},
},
})

217
populate-events-dynamic.js Normal file
View File

@@ -0,0 +1,217 @@
// Dynamic script to populate Events and Meetings for current VIPs in VIP Coordinator
const API_BASE = 'http://localhost:3000/api';
// Function to get all current VIPs and drivers
async function getCurrentData() {
try {
const [vipsResponse, driversResponse] = await Promise.all([
fetch(`${API_BASE}/vips`),
fetch(`${API_BASE}/drivers`)
]);
if (!vipsResponse.ok || !driversResponse.ok) {
throw new Error('Failed to fetch current data');
}
const vips = await vipsResponse.json();
const drivers = await driversResponse.json();
return { vips, drivers };
} catch (error) {
console.error('Error fetching current data:', error);
throw error;
}
}
// Function to add events for a specific VIP
async function addEventsForVip(vip, driverIndex, drivers) {
const assignedDriver = drivers[driverIndex % drivers.length];
const events = [];
// Different event templates based on VIP characteristics
if (vip.transportMode === 'flight') {
// Airport arrival event
events.push({
title: 'Arrival at DEN',
location: 'Denver International Airport',
startTime: '2025-06-26T09:00:00',
endTime: '2025-06-26T10:00:00',
type: 'transport',
assignedDriverId: assignedDriver.id,
description: `Airport pickup for ${vip.name}`
});
// Business meeting
events.push({
title: `Meeting with ${vip.organization} Partners`,
location: 'Denver Convention Center',
startTime: '2025-06-26T11:00:00',
endTime: '2025-06-26T12:30:00',
type: 'meeting',
assignedDriverId: assignedDriver.id,
description: `Strategic meeting for ${vip.name}`
});
}
// Lunch event (for all VIPs)
const restaurants = [
"Elway's Downtown",
"Linger",
"Guard and Grace",
"The Capital Grille",
"Mercantile Dining & Provision"
];
const lunchTitles = [
"Lunch with Board Members",
"Executive Lunch Meeting",
"Networking Lunch",
"Business Lunch",
"Partnership Lunch"
];
events.push({
title: lunchTitles[driverIndex % lunchTitles.length],
location: restaurants[driverIndex % restaurants.length],
startTime: '2025-06-26T13:00:00',
endTime: '2025-06-26T14:30:00',
type: 'meal',
assignedDriverId: assignedDriver.id,
description: `Fine dining experience for ${vip.name}`
});
// Afternoon event
const afternoonEvents = [
{ title: 'Presentation to Stakeholders', location: 'Denver Tech Center' },
{ title: 'Innovation Workshop', location: 'National Ballpark Museum' },
{ title: 'Industry Panel Discussion', location: 'Denver Art Museum' },
{ title: 'Strategic Planning Session', location: 'Wells Fargo Center' },
{ title: 'Product Demo Session', location: 'Colorado Convention Center' }
];
const afternoonEvent = afternoonEvents[driverIndex % afternoonEvents.length];
events.push({
title: afternoonEvent.title,
location: afternoonEvent.location,
startTime: '2025-06-26T15:00:00',
endTime: '2025-06-26T16:30:00',
type: 'event',
assignedDriverId: assignedDriver.id,
description: `Professional engagement for ${vip.name}`
});
// Evening event (for some VIPs)
if (driverIndex % 2 === 0) {
const dinnerEvents = [
{ title: 'VIP Reception', location: 'Four Seasons Hotel Denver' },
{ title: 'Awards Dinner', location: 'Denver Art Museum' },
{ title: 'Networking Dinner', location: 'Guard and Grace' },
{ title: 'Gala Event', location: 'Brown Palace Hotel' }
];
const dinnerEvent = dinnerEvents[driverIndex % dinnerEvents.length];
events.push({
title: dinnerEvent.title,
location: dinnerEvent.location,
startTime: '2025-06-26T18:30:00',
endTime: '2025-06-26T20:30:00',
type: 'event',
assignedDriverId: assignedDriver.id,
description: `Evening engagement for ${vip.name}`
});
}
// Add all events for this VIP
let successCount = 0;
for (const event of events) {
try {
const response = await fetch(`${API_BASE}/vips/${vip.id}/schedule`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(event)
});
if (response.ok) {
const result = await response.json();
console.log(`✅ Added "${event.title}" for ${vip.name}`);
successCount++;
} else {
const error = await response.text();
console.error(`❌ Failed to add "${event.title}" for ${vip.name}: ${error}`);
}
} catch (error) {
console.error(`❌ Error adding "${event.title}" for ${vip.name}:`, error);
}
}
return successCount;
}
// Main function to populate events for all VIPs
async function populateEventsForAllVips() {
console.log('🚀 Starting dynamic events population...\n');
// Check if API is available
try {
const healthCheck = await fetch(`${API_BASE}/health`);
if (!healthCheck.ok) {
console.error('❌ API is not responding. Make sure the backend is running on port 3000');
return;
}
} catch (error) {
console.error('❌ Cannot connect to API. Make sure the backend is running on port 3000');
return;
}
try {
// Get current VIPs and drivers
const { vips, drivers } = await getCurrentData();
console.log(`📋 Found ${vips.length} VIPs and ${drivers.length} drivers`);
console.log(`👥 VIPs: ${vips.map(v => v.name).join(', ')}`);
console.log(`🚗 Drivers: ${drivers.map(d => d.name).join(', ')}\n`);
if (vips.length === 0) {
console.error('❌ No VIPs found in the system');
return;
}
if (drivers.length === 0) {
console.error('❌ No drivers found in the system');
return;
}
let totalEvents = 0;
// Add events for each VIP
for (let i = 0; i < vips.length; i++) {
const vip = vips[i];
console.log(`\n📅 Creating events for ${vip.name} (${vip.organization})...`);
const eventsAdded = await addEventsForVip(vip, i, drivers);
totalEvents += eventsAdded;
// Small delay to avoid overwhelming the API
await new Promise(resolve => setTimeout(resolve, 100));
}
console.log(`\n✅ Dynamic events population complete!`);
console.log(`📊 Total events created: ${totalEvents}`);
console.log(`📈 Average events per VIP: ${(totalEvents / vips.length).toFixed(1)}`);
console.log('\n🎯 You can now:');
console.log('1. View all VIPs with schedules at http://localhost:5173/vips');
console.log('2. Check individual VIP schedules');
console.log('3. Manage driver assignments');
console.log('4. Test the schedule management features');
} catch (error) {
console.error('❌ Error during events population:', error);
}
}
// Run the script
populateEventsForAllVips().catch(console.error);

View File

@@ -0,0 +1,37 @@
#!/bin/bash
echo "🚀 Starting Dynamic Events Population..."
echo "This script will create events for all current VIPs using their actual IDs"
echo ""
cd "$(dirname "$0")"
# Check if Node.js is available
if ! command -v node &> /dev/null; then
echo "❌ Node.js is not installed. Please install Node.js to run this script."
exit 1
fi
# Check if the backend is running
echo "🔍 Checking if backend is running..."
if curl -s http://localhost:3000/api/health > /dev/null; then
echo "✅ Backend is running"
else
echo "❌ Backend is not running. Please start the backend first:"
echo " cd vip-coordinator && docker-compose up -d"
exit 1
fi
# Run the dynamic events population script
echo ""
echo "📅 Running dynamic events population..."
node populate-events-dynamic.js
echo ""
echo "✅ Dynamic events population script completed!"
echo ""
echo "🎯 Next steps:"
echo "1. Visit http://localhost:5173/vips to see all VIPs"
echo "2. Click on any VIP to view their populated schedule"
echo "3. Check that driver names are displayed correctly"
echo "4. Test the schedule management features"

299
populate-events.js Normal file
View File

@@ -0,0 +1,299 @@
// Script to populate Events and Meetings for VIPs in VIP Coordinator
const API_BASE = 'http://localhost:3000/api';
// Function to add events and meetings for VIPs
async function addEventsAndMeetings() {
console.log('\n🚀 Adding Events and Meetings...');
let successCount = 0;
// VIP 1 - Sarah Johnson
let response = await fetch(`${API_BASE}/vips/1/schedule`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: 'Arrival at DEN',
location: 'Denver International Airport',
startTime: '2025-06-26T09:00:00',
endTime: '2025-06-26T10:00:00',
type: 'transport',
assignedDriverId: '1'
})
});
if (response.ok) {
console.log(' ✅ Added arrival event for Sarah Johnson');
successCount++;
} else {
console.error(' ❌ Failed to add arrival event for Sarah Johnson');
}
response = await fetch(`${API_BASE}/vips/1/schedule`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: 'Meeting with CEO',
location: 'Hyatt Regency Denver',
startTime: '2025-06-26T11:00:00',
endTime: '2025-06-26T12:30:00',
type: 'meeting',
assignedDriverId: '1'
})
});
if (response.ok) {
console.log(' ✅ Added meeting event for Sarah Johnson');
successCount++;
} else {
console.error(' ❌ Failed to add meeting event for Sarah Johnson');
}
response = await fetch(`${API_BASE}/vips/1/schedule`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: 'Lunch with Board Members',
location: 'Elway\'s Downtown',
startTime: '2025-06-26T13:00:00',
endTime: '2025-06-26T14:30:00',
type: 'meal',
assignedDriverId: '1'
})
});
if (response.ok) {
console.log(' ✅ Added lunch event for Sarah Johnson');
successCount++;
} else {
console.error(' ❌ Failed to add lunch event for Sarah Johnson');
}
// VIP 2 - Michael Chen
response = await fetch(`${API_BASE}/vips/2/schedule`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: 'Arrival at DEN',
location: 'Denver International Airport',
startTime: '2025-06-26T10:00:00',
endTime: '2025-06-26T11:00:00',
type: 'transport',
assignedDriverId: '2'
})
});
if (response.ok) {
console.log(' ✅ Added arrival event for Michael Chen');
successCount++;
} else {
console.error(' ❌ Failed to add arrival event for Michael Chen');
}
response = await fetch(`${API_BASE}/vips/2/schedule`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: 'Meeting with Investors',
location: 'Denver Marriott City Center',
startTime: '2025-06-26T11:30:00',
endTime: '2025-06-26T13:00:00',
type: 'meeting',
assignedDriverId: '2'
})
});
if (response.ok) {
console.log(' ✅ Added meeting event for Michael Chen');
successCount++;
} else {
console.error(' ❌ Failed to add meeting event for Michael Chen');
}
response = await fetch(`${API_BASE}/vips/2/schedule`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: 'Lunch with Colleagues',
location: 'Linger',
startTime: '2025-06-26T13:30:00',
endTime: '2025-06-26T15:00:00',
type: 'meal',
assignedDriverId: '2'
})
});
if (response.ok) {
console.log(' ✅ Added lunch event for Michael Chen');
successCount++;
} else {
console.error(' ❌ Failed to add lunch event for Michael Chen');
}
// VIP 3 - Emily Rodriguez
response = await fetch(`${API_BASE}/vips/3/schedule`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: 'Arrival at DEN',
location: 'Denver International Airport',
startTime: '2025-06-27T09:00:00',
endTime: '2025-06-27T10:00:00',
type: 'transport',
assignedDriverId: '3'
})
});
if (response.ok) {
console.log(' ✅ Added arrival event for Emily Rodriguez');
successCount++;
} else {
console.error(' ❌ Failed to add arrival event for Emily Rodriguez');
}
response = await fetch(`${API_BASE}/vips/3/schedule`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: 'Meeting with Healthcare Partners',
location: 'Denver Convention Center',
startTime: '2025-06-27T10:30:00',
endTime: '2025-06-27T12:00:00',
type: 'meeting',
assignedDriverId: '3'
})
});
if (response.ok) {
console.log(' ✅ Added meeting event for Emily Rodriguez');
successCount++;
} else {
console.error(' ❌ Failed to add meeting event for Emily Rodriguez');
}
response = await fetch(`${API_BASE}/vips/3/schedule`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: 'Lunch with Healthcare Executives',
location: 'Linger',
startTime: '2025-06-27T12:30:00',
endTime: '2025-06-27T14:00:00',
type: 'meal',
assignedDriverId: '3'
})
});
if (response.ok) {
console.log(' ✅ Added lunch event for Emily Rodriguez');
successCount++;
} else {
console.error(' ❌ Failed to add lunch event for Emily Rodriguez');
}
// VIP 4 - David Thompson (self-driving)
// No events needed
// VIP 5 - Lisa Wang
response = await fetch(`${API_BASE}/vips/5/schedule`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: 'Arrival at DEN',
location: 'Denver International Airport',
startTime: '2025-06-26T08:00:00',
endTime: '2025-06-26T09:00:00',
type: 'transport',
assignedDriverId: '4'
})
});
if (response.ok) {
console.log(' ✅ Added arrival event for Lisa Wang');
successCount++;
} else {
console.error(' ❌ Failed to add arrival event for Lisa Wang');
}
response = await fetch(`${API_BASE}/vips/5/schedule`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: 'Keynote Presentation',
location: 'Denver Convention Center',
startTime: '2025-06-26T10:00:00',
endTime: '2025-06-26T11:30:00',
type: 'event',
assignedDriverId: '4'
})
});
if (response.ok) {
console.log(' ✅ Added keynote event for Lisa Wang');
successCount++;
} else {
console.error(' ❌ Failed to add keynote event for Lisa Wang');
}
response = await fetch(`${API_BASE}/vips/5/schedule`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: 'Lunch with Conference Organizers',
location: 'Elway\'s Downtown',
startTime: '2025-06-26T12:00:00',
endTime: '2025-06-26T13:30:00',
type: 'meal',
assignedDriverId: '4'
})
});
if (response.ok) {
console.log(' ✅ Added lunch event for Lisa Wang');
successCount++;
} else {
console.error(' ❌ Failed to add lunch event for Lisa Wang');
}
console.log(`\nAdded ${successCount} events successfully`);
}
// Main function
async function populateEventsAndMeetings() {
console.log('🚀 Starting to populate Events and Meetings...\n');
// Check if API is available
try {
const healthCheck = await fetch(`${API_BASE}/health`);
if (!healthCheck.ok) {
console.error('❌ API is not responding. Make sure the backend is running on port 3000');
return;
}
} catch (error) {
console.error('❌ Cannot connect to API. Make sure the backend is running on port 3000');
return;
}
await addEventsAndMeetings();
console.log('\n✅ Events and Meetings population complete!');
console.log('\nYou can now:');
console.log('1. View all VIPs at http://localhost:5173/vips');
console.log('2. Manage drivers at http://localhost:5173/drivers');
console.log('3. Create schedules for the VIPs');
console.log('4. Test flight tracking and validation');
}
// Run the script
populateEventsAndMeetings().catch(console.error);

238
populate-events.sh Normal file
View File

@@ -0,0 +1,238 @@
#!/bin/bash
# Script to populate Events and Meetings for VIPs in VIP Coordinator
API_BASE="http://localhost:3000/api"
echo "🚀 Adding Events and Meetings..."
echo ""
# VIP 1 - Sarah Johnson (ID: 1748780965379)
curl -X POST $API_BASE/vips/1748780965379/schedule -H "Content-Type: application/json" -d '{
"title": "Arrival at DEN",
"location": "Denver International Airport",
"startTime": "2025-06-26T09:00:00",
"endTime": "2025-06-26T10:00:00",
"type": "transport",
"assignedDriverId": "1748780965562"
}' && echo " ✅ Added arrival event for Sarah Johnson"
curl -X POST $API_BASE/vips/1748780965379/schedule -H "Content-Type: application/json" -d '{
"title": "Meeting with CEO",
"location": "Hyatt Regency Denver",
"startTime": "2025-06-26T11:00:00",
"endTime": "2025-06-26T12:30:00",
"type": "meeting",
"assignedDriverId": "1748780965562"
}' && echo " ✅ Added meeting event for Sarah Johnson"
curl -X POST $API_BASE/vips/1748780965379/schedule -H "Content-Type: application/json" -d '{
"title": "Lunch with Board Members",
"location": "Elway'\''s Downtown",
"startTime": "2025-06-26T13:00:00",
"endTime": "2025-06-26T14:30:00",
"type": "meal",
"assignedDriverId": "1748780965562"
}' && echo " ✅ Added lunch event for Sarah Johnson"
curl -X POST $API_BASE/vips/1748780965379/schedule -H "Content-Type: application/json" -d '{
"title": "Afternoon Presentation",
"location": "Denver Convention Center",
"startTime": "2025-06-26T15:00:00",
"endTime": "2025-06-26T16:30:00",
"type": "event",
"assignedDriverId": "1748780965562"
}' && echo " ✅ Added afternoon event for Sarah Johnson"
curl -X POST $API_BASE/vips/1748780965379/schedule -H "Content-Type: application/json" -d '{
"title": "Dinner with Investors",
"location": "The Capital Grille",
"startTime": "2025-06-26T18:00:00",
"endTime": "2025-06-26T20:00:00",
"type": "meal",
"assignedDriverId": "1748780965562"
}' && echo " ✅ Added dinner event for Sarah Johnson"
# VIP 2 - Michael Chen (ID: 1748780965388)
curl -X POST $API_BASE/vips/1748780965388/schedule -H "Content-Type: application/json" -d '{
"title": "Arrival at DEN",
"location": "Denver International Airport",
"startTime": "2025-06-26T10:00:00",
"endTime": "2025-06-26T11:00:00",
"type": "transport",
"assignedDriverId": "1748780965570"
}' && echo " ✅ Added arrival event for Michael Chen"
curl -X POST $API_BASE/vips/1748780965388/schedule -H "Content-Type: application/json" -d '{
"title": "Meeting with Investors",
"location": "Denver Marriott City Center",
"startTime": "2025-06-26T11:30:00",
"endTime": "2025-06-26T13:00:00",
"type": "meeting",
"assignedDriverId": "1748780965570"
}' && echo " ✅ Added meeting event for Michael Chen"
curl -X POST $API_BASE/vips/1748780965388/schedule -H "Content-Type: application/json" -d '{
"title": "Lunch with Colleagues",
"location": "Linger",
"startTime": "2025-06-26T13:30:00",
"endTime": "2025-06-26T15:00:00",
"type": "meal",
"assignedDriverId": "1748780965570"
}' && echo " ✅ Added lunch event for Michael Chen"
curl -X POST $API_BASE/vips/1748780965388/schedule -H "Content-Type: application/json" -d '{
"title": "Financial Review Session",
"location": "Wells Fargo Center",
"startTime": "2025-06-26T15:30:00",
"endTime": "2025-06-26T17:00:00",
"type": "meeting",
"assignedDriverId": "1748780965570"
}' && echo " ✅ Added afternoon meeting for Michael Chen"
curl -X POST $API_BASE/vips/1748780965388/schedule -H "Content-Type: application/json" -d '{
"title": "Networking Dinner",
"location": "Guard and Grace",
"startTime": "2025-06-26T19:00:00",
"endTime": "2025-06-26T21:00:00",
"type": "meal",
"assignedDriverId": "1748780965570"
}' && echo " ✅ Added dinner event for Michael Chen"
# VIP 3 - Emily Rodriguez (ID: 1748780965395)
curl -X POST $API_BASE/vips/1748780965395/schedule -H "Content-Type: application/json" -d '{
"title": "Arrival at DEN",
"location": "Denver International Airport",
"startTime": "2025-06-27T09:00:00",
"endTime": "2025-06-27T10:00:00",
"type": "transport",
"assignedDriverId": "1748780965577"
}' && echo " ✅ Added arrival event for Emily Rodriguez"
curl -X POST $API_BASE/vips/1748780965395/schedule -H "Content-Type: application/json" -d '{
"title": "Meeting with Healthcare Partners",
"location": "Denver Convention Center",
"startTime": "2025-06-27T10:30:00",
"endTime": "2025-06-27T12:00:00",
"type": "meeting",
"assignedDriverId": "1748780965577"
}' && echo " ✅ Added meeting event for Emily Rodriguez"
curl -X POST $API_BASE/vips/1748780965395/schedule -H "Content-Type: application/json" -d '{
"title": "Lunch with Healthcare Executives",
"location": "Linger",
"startTime": "2025-06-27T12:30:00",
"endTime": "2025-06-27T14:00:00",
"type": "meal",
"assignedDriverId": "1748780965577"
}' && echo " ✅ Added lunch event for Emily Rodriguez"
curl -X POST $API_BASE/vips/1748780965395/schedule -H "Content-Type: application/json" -d '{
"title": "Healthcare Innovation Panel",
"location": "National Ballpark Museum",
"startTime": "2025-06-27T14:30:00",
"endTime": "2025-06-27T16:00:00",
"type": "event",
"assignedDriverId": "1748780965577"
}' && echo " ✅ Added panel event for Emily Rodriguez"
# VIP 5 - Lisa Wang (ID: 1748780965413)
curl -X POST $API_BASE/vips/1748780965413/schedule -H "Content-Type: application/json" -d '{
"title": "Arrival at DEN",
"location": "Denver International Airport",
"startTime": "2025-06-26T08:00:00",
"endTime": "2025-06-26T09:00:00",
"type": "transport",
"assignedDriverId": "1748780965584"
}' && echo " ✅ Added arrival event for Lisa Wang"
curl -X POST $API_BASE/vips/1748780965413/schedule -H "Content-Type: application/json" -d '{
"title": "Keynote Presentation",
"location": "Denver Convention Center",
"startTime": "2025-06-26T10:00:00",
"endTime": "2025-06-26T11:30:00",
"type": "event",
"assignedDriverId": "1748780965584"
}' && echo " ✅ Added keynote event for Lisa Wang"
curl -X POST $API_BASE/vips/1748780965413/schedule -H "Content-Type: application/json" -d '{
"title": "Lunch with Conference Organizers",
"location": "Elway'\''s Downtown",
"startTime": "2025-06-26T12:00:00",
"endTime": "2025-06-26T13:30:00",
"type": "meal",
"assignedDriverId": "1748780965584"
}' && echo " ✅ Added lunch event for Lisa Wang"
curl -X POST $API_BASE/vips/1748780965413/schedule -H "Content-Type: application/json" -d '{
"title": "AI Workshop Session",
"location": "Denver Tech Center",
"startTime": "2025-06-26T14:00:00",
"endTime": "2025-06-26T16:00:00",
"type": "event",
"assignedDriverId": "1748780965584"
}' && echo " ✅ Added workshop event for Lisa Wang"
curl -X POST $API_BASE/vips/1748780965413/schedule -H "Content-Type: application/json" -d '{
"title": "VIP Reception",
"location": "Four Seasons Hotel Denver",
"startTime": "2025-06-26T17:30:00",
"endTime": "2025-06-26T19:30:00",
"type": "event",
"assignedDriverId": "1748780965584"
}' && echo " ✅ Added reception event for Lisa Wang"
# VIP 7 - Jennifer Adams (ID: 1748780965432)
curl -X POST $API_BASE/vips/1748780965432/schedule -H "Content-Type: application/json" -d '{
"title": "Arrival at DEN",
"location": "Denver International Airport",
"startTime": "2025-06-26T09:30:00",
"endTime": "2025-06-26T10:30:00",
"type": "transport",
"assignedDriverId": "1748780965590"
}' && echo " ✅ Added arrival event for Jennifer Adams"
curl -X POST $API_BASE/vips/1748780965432/schedule -H "Content-Type: application/json" -d '{
"title": "Media Network Meeting",
"location": "Denver Convention Center",
"startTime": "2025-06-26T11:00:00",
"endTime": "2025-06-26T12:30:00",
"type": "meeting",
"assignedDriverId": "1748780965590"
}' && echo " ✅ Added meeting event for Jennifer Adams"
curl -X POST $API_BASE/vips/1748780965432/schedule -H "Content-Type: application/json" -d '{
"title": "Lunch with Media Executives",
"location": "Linger",
"startTime": "2025-06-26T13:00:00",
"endTime": "2025-06-26T14:30:00",
"type": "meal",
"assignedDriverId": "1748780965590"
}' && echo " ✅ Added lunch event for Jennifer Adams"
curl -X POST $API_BASE/vips/1748780965432/schedule -H "Content-Type: application/json" -d '{
"title": "Press Conference",
"location": "Denver Press Club",
"startTime": "2025-06-26T15:00:00",
"endTime": "2025-06-26T16:00:00",
"type": "event",
"assignedDriverId": "1748780965590"
}' && echo " ✅ Added press conference for Jennifer Adams"
curl -X POST $API_BASE/vips/1748780965432/schedule -H "Content-Type: application/json" -d '{
"title": "Media Awards Dinner",
"location": "Denver Art Museum",
"startTime": "2025-06-26T18:30:00",
"endTime": "2025-06-26T21:00:00",
"type": "event",
"assignedDriverId": "1748780965590"
}' && echo " ✅ Added awards dinner for Jennifer Adams"
echo ""
echo "✅ Events and Meetings population complete!"
echo ""
echo "You can now:"
echo "1. View all VIPs at http://localhost:5173/vips"
echo "2. Manage drivers at http://localhost:5173/drivers"
echo "3. Create schedules for the VIPs"
echo "4. Test flight tracking and validation"

425
populate-test-data.js Normal file
View File

@@ -0,0 +1,425 @@
// Script to populate VIP Coordinator with test data
// All VIPs flying into Denver International Airport (DEN)
const API_BASE = 'http://localhost:3000/api';
// Function to add events and meetings for VIPs
async function addEventsAndMeetings() {
console.log('\nAdding Events and Meetings...');
let successCount = 0;
// VIP 1 - Sarah Johnson
let response = await fetch(`${API_BASE}/vips/1/schedule`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: 'Arrival at DEN',
location: 'Denver International Airport',
startTime: '2025-06-26T09:00:00',
endTime: '2025-06-26T10:00:00',
type: 'transport',
assignedDriverId: '1'
})
});
if (response.ok) {
console.log(' ✅ Added arrival event for Sarah Johnson');
successCount++;
} else {
console.error(' ❌ Failed to add arrival event for Sarah Johnson');
}
response = await fetch(`${API_BASE}/vips/1/schedule`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: 'Meeting with CEO',
location: 'Hyatt Regency Denver',
startTime: '2025-06-26T11:00:00',
endTime: '2025-06-26T12:30:00',
type: 'meeting',
assignedDriverId: '1'
})
});
if (response.ok) {
console.log(' ✅ Added meeting event for Sarah Johnson');
successCount++;
} else {
console.error(' ❌ Failed to add meeting event for Sarah Johnson');
}
response = await fetch(`${API_BASE}/vips/1/schedule`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: 'Lunch with Board Members',
location: 'Elway\'s Downtown',
startTime: '2025-06-26T13:00:00',
endTime: '2025-06-26T14:30:00',
type: 'meal',
assignedDriverId: '1'
})
});
if (response.ok) {
console.log(' ✅ Added lunch event for Sarah Johnson');
successCount++;
} else {
console.error(' ❌ Failed to add lunch event for Sarah Johnson');
}
// VIP 2 - Michael Chen
response = await fetch(`${API_BASE}/vips/2/schedule`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: 'Arrival at DEN',
location: 'Denver International Airport',
startTime: '2025-06-26T10:00:00',
endTime: '2025-06-26T11:00:00',
type: 'transport',
assignedDriverId: '2'
})
});
if (response.ok) {
console.log(' ✅ Added arrival event for Michael Chen');
successCount++;
} else {
console.error(' ❌ Failed to add arrival event for Michael Chen');
}
response = await fetch(`${API_BASE}/vips/2/schedule`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: 'Meeting with Investors',
location: 'Denver Marriott City Center',
startTime: '2025-06-26T11:30:00',
endTime: '2025-06-26T13:00:00',
type: 'meeting',
assignedDriverId: '2'
})
});
if (response.ok) {
console.log(' ✅ Added meeting event for Michael Chen');
successCount++;
} else {
console.error(' ❌ Failed to add meeting event for Michael Chen');
}
response = await fetch(`${API_BASE}/vips/2/schedule`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: 'Lunch with Colleagues',
location: 'Linger',
startTime: '2025-06-26T13:30:00',
endTime: '2025-06-26T15:00:00',
type: 'meal',
assignedDriverId: '2'
})
});
if (response.ok) {
console.log(' ✅ Added lunch event for Michael Chen');
successCount++;
} else {
console.error(' ❌ Failed to add lunch event for Michael Chen');
}
// VIP 3 - Emily Rodriguez
response = await fetch(`${API_BASE}/vips/3/schedule`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: 'Arrival at DEN',
location: 'Denver International Airport',
startTime: '2025-06-27T09:00:00',
endTime: '2025-06-27T10:00:00',
type: 'transport',
assignedDriverId: '3'
})
});
if (response.ok) {
console.log(' ✅ Added arrival event for Emily Rodriguez');
successCount++;
} else {
console.error(' ❌ Failed to add arrival event for Emily Rodriguez');
}
response = await fetch(`${API_BASE}/vips/3/schedule`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: 'Meeting with Healthcare Partners',
location: 'Denver Convention Center',
startTime: '2025-06-27T10:30:00',
endTime: '2025-06-27T12:00:00',
type: 'meeting',
assignedDriverId: '3'
})
});
if (response.ok) {
console.log(' ✅ Added meeting event for Emily Rodriguez');
successCount++;
} else {
console.error(' ❌ Failed to add meeting event for Emily Rodriguez');
}
response = await fetch(`${API_BASE}/vips/3/schedule`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: 'Lunch with Healthcare Executives',
location: 'Linger',
startTime: '2025-06-27T12:30:00',
endTime: '2025-06-27T14:00:00',
type: 'meal',
assignedDriverId: '3'
})
});
if (response.ok) {
console.log(' ✅ Added lunch event for Emily Rodriguez');
successCount++;
} else {
console.error(' ❌ Failed to add lunch event for Emily Rodriguez');
}
// VIP 4 - David Thompson (self-driving)
// No events needed
// VIP 5 - Lisa Wang
response = await fetch(`${API_BASE}/vips/5/schedule`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: 'Arrival at DEN',
location: 'Denver International Airport',
startTime: '2025-06-26T08:00:00',
endTime: '2025-06-26T09:00:00',
type: 'transport',
assignedDriverId: '4'
})
});
if (response.ok) {
console.log(' ✅ Added arrival event for Lisa Wang');
successCount++;
} else {
console.error(' ❌ Failed to add arrival event for Lisa Wang');
}
response = await fetch(`${API_BASE}/vips/5/schedule`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: 'Keynote Presentation',
location: 'Denver Convention Center',
startTime: '2025-06-26T10:00:00',
endTime: '2025-06-26T11:30:00',
type: 'event',
assignedDriverId: '4'
})
});
if (response.ok) {
console.log(' ✅ Added keynote event for Lisa Wang');
successCount++;
} else {
console.error(' ❌ Failed to add keynote event for Lisa Wang');
}
response = await fetch(`${API_BASE}/vips/5/schedule`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: 'Lunch with Conference Organizers',
location: 'Elway\'s Downtown',
startTime: '2025-06-26T12:00:00',
endTime: '2025-06-26T13:30:00',
type: 'meal',
assignedDriverId: '4'
})
});
if (response.ok) {
console.log(' ✅ Added lunch event for Lisa Wang');
successCount++;
} else {
console.error(' ❌ Failed to add lunch event for Lisa Wang');
}
// VIP 6 - Robert Martinez
response = await fetch(`${API_BASE}/vips/6/schedule`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: 'Arrival at DEN',
location: 'Denver International Airport',
startTime: '2025-06-27T10:00:00',
endTime: '2025-06-27T11:00:00',
type: 'transport',
assignedDriverId: '5'
})
});
if (response.ok) {
console.log(' ✅ Added arrival event for Robert Martinez');
successCount++;
} else {
console.error(' ❌ Failed to add arrival event for Robert Martinez');
}
response = await fetch(`${API_BASE}/vips/6/schedule`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: 'Meeting with Venture Capitalists',
location: 'Denver Marriott City Center',
startTime: '2025-06-27T11:30:00',
endTime: '2025-06-27T13:00:00',
type: 'meeting',
assignedDriverId: '5'
})
});
if (response.ok) {
console.log(' ✅ Added meeting event for Robert Martinez');
successCount++;
} else {
console.error(' ❌ Failed to add meeting event for Robert Martinez');
}
response = await fetch(`${API_BASE}/vips/6/schedule`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: 'Lunch with Startup Founders',
location: 'Linger',
startTime: '2025-06-27T13:30:00',
endTime: '2025-06-27T15:00:00',
type: 'meal',
assignedDriverId: '5'
})
});
if (response.ok) {
console.log(' ✅ Added lunch event for Robert Martinez');
successCount++;
} else {
console.error(' ❌ Failed to add lunch event for Robert Martinez');
}
// VIP 7 - Jennifer Adams
response = await fetch(`${API_BASE}/vips/7/schedule`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: 'Arrival at DEN',
location: 'Denver International Airport',
startTime: '2025-06-26T09:30:00',
endTime: '2025-06-26T10:30:00',
type: 'transport',
assignedDriverId: '6'
})
});
if (response.ok) {
console.log(' ✅ Added arrival event for Jennifer Adams');
successCount++;
} else {
console.error(' ❌ Failed to add arrival event for Jennifer Adams');
}
response = await fetch(`${API_BASE}/vips/7/schedule`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: 'Media Network Meeting',
location: 'Denver Convention Center',
startTime: '2025-06-26T11:00:00',
endTime: '2025-06-26T12:30:00',
type: 'meeting',
assignedDriverId: '6'
})
});
if (response.ok) {
console.log(' ✅ Added meeting event for Jennifer Adams');
successCount++;
} else {
console.error(' ❌ Failed to add meeting event for Jennifer Adams');
}
response = await fetch(`${API_BASE}/vips/7/schedule`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: 'Lunch with Media Executives',
location: 'Linger',
startTime: '2025-06-26T13:00:00',
endTime: '2025-06-26T14:30:00',
type: 'meal',
assignedDriverId: '6'
})
});
if (response.ok) {
console.log(' ✅ Added lunch event for Jennifer Adams');
successCount++;
} else {
console.error(' ❌ Failed to add lunch event for Jennifer Adams');
}
// VIP 8 - James Wilson
response = await fetch(`${API_BASE}/vips/8/schedule`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: 'Arrival at DEN',
location: 'Denver International Airport',
startTime: '2025-06-27T09:00:00',
endTime: '2025-06-27T10:00:00',
type: 'transport',
assignedDriverId: '7'
})
});
if (response.ok) {
console.log(' ✅ Added arrival event for James Wilson');
successCount++;
} else {
console.error(' ❌ Failed to add arrival event for James Wilson');
}
console.log(`\nAdded ${successCount} events successfully`);
}
// Main function
async function populateTestData() {
console.log('🚀 Starting to populate test data...\n');

313
populate-test-data.sh Normal file
View File

@@ -0,0 +1,313 @@
#!/bin/bash
# Script to populate VIP Coordinator with test data
# All VIPs flying into Denver International Airport (DEN)
API_BASE="http://localhost:3000/api"
echo "🚀 Starting to populate test data..."
echo ""
# Add VIPs
echo "Adding VIPs..."
# VIP 1
curl -X POST $API_BASE/vips -H "Content-Type: application/json" -d '{
"name": "Sarah Johnson",
"organization": "Tech Innovations Inc",
"transportMode": "flight",
"flights": [{"flightNumber": "UA1234", "flightDate": "2025-06-26", "segment": 1}],
"needsAirportPickup": true,
"needsVenueTransport": true,
"notes": "CEO - Requires executive transport"
}' && echo " ✅ Added Sarah Johnson"
# VIP 2
curl -X POST $API_BASE/vips -H "Content-Type: application/json" -d '{
"name": "Michael Chen",
"organization": "Global Finance Corp",
"transportMode": "flight",
"flights": [
{"flightNumber": "AA2547", "flightDate": "2025-06-26", "segment": 1},
{"flightNumber": "AA789", "flightDate": "2025-06-26", "segment": 2}
],
"needsAirportPickup": true,
"needsVenueTransport": true,
"notes": "Has connecting flight through Dallas"
}' && echo " ✅ Added Michael Chen"
# VIP 3
curl -X POST $API_BASE/vips -H "Content-Type: application/json" -d '{
"name": "Emily Rodriguez",
"organization": "Healthcare Solutions",
"transportMode": "flight",
"flights": [{"flightNumber": "DL456", "flightDate": "2025-06-27", "segment": 1}],
"needsAirportPickup": true,
"needsVenueTransport": false,
"notes": "Will have rental car for venue transport"
}' && echo " ✅ Added Emily Rodriguez"
# VIP 4 (Self-driving)
curl -X POST $API_BASE/vips -H "Content-Type: application/json" -d '{
"name": "David Thompson",
"organization": "Energy Dynamics",
"transportMode": "self-driving",
"expectedArrival": "2025-06-26T14:00:00",
"needsAirportPickup": false,
"needsVenueTransport": true,
"notes": "Driving from Colorado Springs"
}' && echo " ✅ Added David Thompson"
# VIP 5
curl -X POST $API_BASE/vips -H "Content-Type: application/json" -d '{
"name": "Lisa Wang",
"organization": "AI Research Lab",
"transportMode": "flight",
"flights": [{"flightNumber": "UA852", "flightDate": "2025-06-26", "segment": 1}],
"needsAirportPickup": true,
"needsVenueTransport": true,
"notes": "Keynote speaker - VIP treatment required"
}' && echo " ✅ Added Lisa Wang"
# VIP 6
curl -X POST $API_BASE/vips -H "Content-Type: application/json" -d '{
"name": "Robert Martinez",
"organization": "Venture Capital Partners",
"transportMode": "flight",
"flights": [{"flightNumber": "SW1122", "flightDate": "2025-06-27", "segment": 1}],
"needsAirportPickup": false,
"needsVenueTransport": true,
"notes": "Taking Uber from airport"
}' && echo " ✅ Added Robert Martinez"
# VIP 7
curl -X POST $API_BASE/vips -H "Content-Type: application/json" -d '{
"name": "Jennifer Adams",
"organization": "Media Networks",
"transportMode": "flight",
"flights": [{"flightNumber": "F9567", "flightDate": "2025-06-26", "segment": 1}],
"needsAirportPickup": true,
"needsVenueTransport": true,
"notes": "Frontier flight from Las Vegas"
}' && echo " ✅ Added Jennifer Adams"
# VIP 8
curl -X POST $API_BASE/vips -H "Content-Type: application/json" -d '{
"name": "James Wilson",
"organization": "Automotive Industries",
"transportMode": "flight",
"flights": [
{"flightNumber": "UA1745", "flightDate": "2025-06-27", "segment": 1},
{"flightNumber": "UA234", "flightDate": "2025-06-27", "segment": 2}
],
"needsAirportPickup": true,
"needsVenueTransport": true,
"notes": "Connecting through Chicago"
}' && echo " ✅ Added James Wilson"
# VIP 9 (Self-driving)
curl -X POST $API_BASE/vips -H "Content-Type: application/json" -d '{
"name": "Patricia Brown",
"organization": "Education Foundation",
"transportMode": "self-driving",
"expectedArrival": "2025-06-26T10:00:00",
"needsAirportPickup": false,
"needsVenueTransport": false,
"notes": "Local - knows the area well"
}' && echo " ✅ Added Patricia Brown"
# VIP 10
curl -X POST $API_BASE/vips -H "Content-Type: application/json" -d '{
"name": "Christopher Lee",
"organization": "Biotech Ventures",
"transportMode": "flight",
"flights": [{"flightNumber": "AA1893", "flightDate": "2025-06-26", "segment": 1}],
"needsAirportPickup": true,
"needsVenueTransport": true,
"notes": "First time in Denver"
}' && echo " ✅ Added Christopher Lee"
# VIP 11
curl -X POST $API_BASE/vips -H "Content-Type: application/json" -d '{
"name": "Amanda Taylor",
"organization": "Renewable Energy Co",
"transportMode": "flight",
"flights": [{"flightNumber": "DL2341", "flightDate": "2025-06-27", "segment": 1}],
"needsAirportPickup": true,
"needsVenueTransport": true,
"notes": "Panel moderator"
}' && echo " ✅ Added Amanda Taylor"
# VIP 12
curl -X POST $API_BASE/vips -H "Content-Type: application/json" -d '{
"name": "Daniel Garcia",
"organization": "Software Solutions",
"transportMode": "flight",
"flights": [{"flightNumber": "UA5678", "flightDate": "2025-06-26", "segment": 1}],
"needsAirportPickup": false,
"needsVenueTransport": true,
"notes": "Has colleague picking up from airport"
}' && echo " ✅ Added Daniel Garcia"
# VIP 13
curl -X POST $API_BASE/vips -H "Content-Type: application/json" -d '{
"name": "Michelle Anderson",
"organization": "Investment Group",
"transportMode": "flight",
"flights": [{"flightNumber": "SW435", "flightDate": "2025-06-27", "segment": 1}],
"needsAirportPickup": true,
"needsVenueTransport": true,
"notes": "Prefers window seat in transport"
}' && echo " ✅ Added Michelle Anderson"
# VIP 14
curl -X POST $API_BASE/vips -H "Content-Type: application/json" -d '{
"name": "Kevin Patel",
"organization": "Tech Startups Inc",
"transportMode": "flight",
"flights": [
{"flightNumber": "F9234", "flightDate": "2025-06-26", "segment": 1},
{"flightNumber": "F9789", "flightDate": "2025-06-26", "segment": 2}
],
"needsAirportPickup": true,
"needsVenueTransport": true,
"notes": "Two Frontier flights with connection"
}' && echo " ✅ Added Kevin Patel"
# VIP 15 (Self-driving)
curl -X POST $API_BASE/vips -H "Content-Type: application/json" -d '{
"name": "Rachel Green",
"organization": "Marketing Experts",
"transportMode": "self-driving",
"expectedArrival": "2025-06-27T08:00:00",
"needsAirportPickup": false,
"needsVenueTransport": true,
"notes": "Driving from Wyoming"
}' && echo " ✅ Added Rachel Green"
# VIP 16 (3 connections!)
curl -X POST $API_BASE/vips -H "Content-Type: application/json" -d '{
"name": "Steven Kim",
"organization": "International Trade",
"transportMode": "flight",
"flights": [
{"flightNumber": "UA789", "flightDate": "2025-06-26", "segment": 1},
{"flightNumber": "UA456", "flightDate": "2025-06-26", "segment": 2},
{"flightNumber": "UA123", "flightDate": "2025-06-26", "segment": 3}
],
"needsAirportPickup": true,
"needsVenueTransport": true,
"notes": "International flight with 2 connections"
}' && echo " ✅ Added Steven Kim"
# VIP 17
curl -X POST $API_BASE/vips -H "Content-Type: application/json" -d '{
"name": "Nancy White",
"organization": "Legal Associates",
"transportMode": "flight",
"flights": [{"flightNumber": "AA567", "flightDate": "2025-06-27", "segment": 1}],
"needsAirportPickup": true,
"needsVenueTransport": false,
"notes": "Staying at airport hotel"
}' && echo " ✅ Added Nancy White"
# VIP 18
curl -X POST $API_BASE/vips -H "Content-Type: application/json" -d '{
"name": "Brian Davis",
"organization": "Construction Corp",
"transportMode": "flight",
"flights": [{"flightNumber": "DL789", "flightDate": "2025-06-26", "segment": 1}],
"needsAirportPickup": true,
"needsVenueTransport": true,
"notes": "Bringing presentation materials"
}' && echo " ✅ Added Brian Davis"
# VIP 19
curl -X POST $API_BASE/vips -H "Content-Type: application/json" -d '{
"name": "Jessica Miller",
"organization": "Fashion Industries",
"transportMode": "flight",
"flights": [{"flightNumber": "UA2468", "flightDate": "2025-06-27", "segment": 1}],
"needsAirportPickup": true,
"needsVenueTransport": true,
"notes": "Requires luggage assistance"
}' && echo " ✅ Added Jessica Miller"
# VIP 20
curl -X POST $API_BASE/vips -H "Content-Type: application/json" -d '{
"name": "Thomas Johnson",
"organization": "Sports Management",
"transportMode": "flight",
"flights": [{"flightNumber": "SW789", "flightDate": "2025-06-26", "segment": 1}],
"needsAirportPickup": true,
"needsVenueTransport": true,
"notes": "Former athlete - may be recognized"
}' && echo " ✅ Added Thomas Johnson"
echo ""
echo "Adding Drivers..."
# Driver 1
curl -X POST $API_BASE/drivers -H "Content-Type: application/json" -d '{
"name": "Carlos Rodriguez",
"phone": "(303) 555-0101",
"currentLocation": {"lat": 39.8561, "lng": -104.6737}
}' && echo " ✅ Added Carlos Rodriguez"
# Driver 2
curl -X POST $API_BASE/drivers -H "Content-Type: application/json" -d '{
"name": "Maria Gonzalez",
"phone": "(303) 555-0102",
"currentLocation": {"lat": 39.7392, "lng": -104.9903}
}' && echo " ✅ Added Maria Gonzalez"
# Driver 3
curl -X POST $API_BASE/drivers -H "Content-Type: application/json" -d '{
"name": "John Smith",
"phone": "(303) 555-0103",
"currentLocation": {"lat": 39.7817, "lng": -104.8883}
}' && echo " ✅ Added John Smith"
# Driver 4
curl -X POST $API_BASE/drivers -H "Content-Type: application/json" -d '{
"name": "Ashley Williams",
"phone": "(303) 555-0104",
"currentLocation": {"lat": 39.7294, "lng": -104.8319}
}' && echo " ✅ Added Ashley Williams"
# Driver 5
curl -X POST $API_BASE/drivers -H "Content-Type: application/json" -d '{
"name": "Marcus Thompson",
"phone": "(303) 555-0105",
"currentLocation": {"lat": 39.7555, "lng": -105.0022}
}' && echo " ✅ Added Marcus Thompson"
# Driver 6
curl -X POST $API_BASE/drivers -H "Content-Type: application/json" -d '{
"name": "Linda Chen",
"phone": "(303) 555-0106",
"currentLocation": {"lat": 39.6777, "lng": -104.9619}
}' && echo " ✅ Added Linda Chen"
# Driver 7
curl -X POST $API_BASE/drivers -H "Content-Type: application/json" -d '{
"name": "Robert Jackson",
"phone": "(303) 555-0107",
"currentLocation": {"lat": 39.8028, "lng": -105.0875}
}' && echo " ✅ Added Robert Jackson"
# Driver 8
curl -X POST $API_BASE/drivers -H "Content-Type: application/json" -d '{
"name": "Sofia Martinez",
"phone": "(303) 555-0108",
"currentLocation": {"lat": 39.7047, "lng": -105.0814}
}' && echo " ✅ Added Sofia Martinez"
echo ""
echo "✅ Test data population complete!"
echo ""
echo "You can now:"
echo "1. View all VIPs at http://localhost:5173/vips"
echo "2. Manage drivers at http://localhost:5173/drivers"
echo "3. Create schedules for the VIPs"
echo "4. Test flight tracking and validation"

159
populate-vips.js Normal file
View File

@@ -0,0 +1,159 @@
// Script to populate VIPs in VIP Coordinator
// All VIPs flying into Denver International Airport (DEN)
const API_BASE = 'http://localhost:3000/api';
// Function to add VIPs
async function addVips() {
console.log('🚀 Adding VIPs...');
let successCount = 0;
// VIP 1 - Sarah Johnson
let response = await fetch(`${API_BASE}/vips`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: "Sarah Johnson",
organization: "Tech Innovations Inc",
transportMode: "flight",
flights: [{"flightNumber": "UA1234", "flightDate": "2025-06-26", "segment": 1}],
needsAirportPickup: true,
needsVenueTransport: true,
notes: "CEO - Requires executive transport"
})
});
if (response.ok) {
console.log(' ✅ Added Sarah Johnson');
successCount++;
} else {
console.error(' ❌ Failed to add Sarah Johnson');
}
// VIP 2 - Michael Chen
response = await fetch(`${API_BASE}/vips`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: "Michael Chen",
organization: "Global Finance Corp",
transportMode: "flight",
flights: [
{"flightNumber": "AA2547", "flightDate": "2025-06-26", "segment": 1},
{"flightNumber": "AA789", "flightDate": "2025-06-26", "segment": 2}
],
needsAirportPickup: true,
needsVenueTransport: true,
notes: "Has connecting flight through Dallas"
})
});
if (response.ok) {
console.log(' ✅ Added Michael Chen');
successCount++;
} else {
console.error(' ❌ Failed to add Michael Chen');
}
// VIP 3 - Emily Rodriguez
response = await fetch(`${API_BASE}/vips`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: "Emily Rodriguez",
organization: "Healthcare Solutions",
transportMode: "flight",
flights: [{"flightNumber": "DL456", "flightDate": "2025-06-27", "segment": 1}],
needsAirportPickup: true,
needsVenueTransport: false,
notes: "Will have rental car for venue transport"
})
});
if (response.ok) {
console.log(' ✅ Added Emily Rodriguez');
successCount++;
} else {
console.error(' ❌ Failed to add Emily Rodriguez');
}
// VIP 4 - David Thompson (self-driving)
response = await fetch(`${API_BASE}/vips`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: "David Thompson",
organization: "Energy Dynamics",
transportMode: "self-driving",
expectedArrival: "2025-06-26T14:00:00",
needsAirportPickup: false,
needsVenueTransport: true,
notes: "Driving from Colorado Springs"
})
});
if (response.ok) {
console.log(' ✅ Added David Thompson');
successCount++;
} else {
console.error(' ❌ Failed to add David Thompson');
}
// VIP 5 - Lisa Wang
response = await fetch(`${API_BASE}/vips`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: "Lisa Wang",
organization: "AI Research Lab",
transportMode: "flight",
flights: [{"flightNumber": "UA852", "flightDate": "2025-06-26", "segment": 1}],
needsAirportPickup: true,
needsVenueTransport: true,
notes: "Keynote speaker - VIP treatment required"
})
});
if (response.ok) {
console.log(' ✅ Added Lisa Wang');
successCount++;
} else {
console.error(' ❌ Failed to add Lisa Wang');
}
console.log(`\nAdded ${successCount} VIPs successfully`);
}
// Main function
async function populateVips() {
console.log('🚀 Starting to populate VIPs...\n');
// Check if API is available
try {
const healthCheck = await fetch(`${API_BASE}/health`);
if (!healthCheck.ok) {
console.error('❌ API is not responding. Make sure the backend is running on port 3000');
return;
}
} catch (error) {
console.error('❌ Cannot connect to API. Make sure the backend is running on port 3000');
return;
}
await addVips();
console.log('\n✅ VIP population complete!');
console.log('\nYou can now:');
console.log('1. View all VIPs at http://localhost:5173/vips');
console.log('2. Manage drivers at http://localhost:5173/drivers');
console.log('3. Create schedules for the VIPs');
console.log('4. Test flight tracking and validation');
}
// Run the script
populateVips().catch(console.error);

79
quick-populate-events.sh Normal file
View File

@@ -0,0 +1,79 @@
#!/bin/bash
echo "🚀 Quick Events Population for Current VIPs..."
# Get current VIP IDs
VIP_IDS=($(curl -s http://localhost:3000/api/vips | grep -o '"id":"[^"]*"' | sed 's/"id":"//' | sed 's/"//' | head -10))
DRIVER_IDS=($(curl -s http://localhost:3000/api/drivers | grep -o '"id":"[^"]*"' | sed 's/"id":"//' | sed 's/"//' | head -8))
echo "Found ${#VIP_IDS[@]} VIPs and ${#DRIVER_IDS[@]} drivers"
# Function to add an event
add_event() {
local vip_id=$1
local title=$2
local location=$3
local start_time=$4
local end_time=$5
local type=$6
local driver_id=$7
local description=$8
curl -s -X POST "http://localhost:3000/api/vips/$vip_id/schedule" \
-H "Content-Type: application/json" \
-d "{
\"title\": \"$title\",
\"location\": \"$location\",
\"startTime\": \"$start_time\",
\"endTime\": \"$end_time\",
\"type\": \"$type\",
\"assignedDriverId\": \"$driver_id\",
\"description\": \"$description\"
}" > /dev/null
if [ $? -eq 0 ]; then
echo "✅ Added: $title for VIP $vip_id"
else
echo "❌ Failed: $title for VIP $vip_id"
fi
}
# Add events for first 5 VIPs
for i in {0..4}; do
if [ $i -lt ${#VIP_IDS[@]} ]; then
vip_id=${VIP_IDS[$i]}
driver_id=${DRIVER_IDS[$((i % ${#DRIVER_IDS[@]}))]}
echo ""
echo "📅 Creating events for VIP $vip_id with driver $driver_id..."
# Airport arrival
add_event "$vip_id" "Airport Arrival" "Denver International Airport" "2025-06-26T09:00:00" "2025-06-26T10:00:00" "transport" "$driver_id" "Airport pickup service"
# Business meeting
add_event "$vip_id" "Executive Meeting" "Denver Convention Center" "2025-06-26T11:00:00" "2025-06-26T12:30:00" "meeting" "$driver_id" "Strategic business meeting"
# Lunch
restaurants=("Elway's Downtown" "Linger" "Guard and Grace" "The Capital Grille" "Mercantile Dining")
restaurant=${restaurants[$((i % ${#restaurants[@]}))]}
add_event "$vip_id" "Business Lunch" "$restaurant" "2025-06-26T13:00:00" "2025-06-26T14:30:00" "meal" "$driver_id" "Fine dining experience"
# Afternoon event
add_event "$vip_id" "Presentation Session" "Denver Tech Center" "2025-06-26T15:00:00" "2025-06-26T16:30:00" "event" "$driver_id" "Professional presentation"
# Evening event (for some VIPs)
if [ $((i % 2)) -eq 0 ]; then
add_event "$vip_id" "VIP Reception" "Four Seasons Hotel Denver" "2025-06-26T18:30:00" "2025-06-26T20:00:00" "event" "$driver_id" "Networking reception"
fi
sleep 0.2 # Small delay between VIPs
fi
done
echo ""
echo "✅ Quick events population completed!"
echo ""
echo "🎯 Next steps:"
echo "1. Visit http://localhost:5173/vips"
echo "2. Click on any VIP to see their schedule"
echo "3. Verify driver names are showing correctly"

View File

@@ -0,0 +1,96 @@
// Test different AviationStack endpoints to see what works with your subscription
const https = require('https');
const API_KEY = 'b3ca0325a7a342a8f7b214f348dc1d50';
const FLIGHT = 'AA4563';
const DATE = '2025-06-02';
function testEndpoint(name, url) {
return new Promise((resolve) => {
console.log(`\n=== Testing ${name} ===`);
console.log('URL:', url.replace(API_KEY, '***'));
https.get(url, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
const parsed = JSON.parse(data);
console.log('Status:', res.statusCode);
console.log('Response:', JSON.stringify(parsed, null, 2));
resolve({ name, status: res.statusCode, data: parsed });
} catch (e) {
console.error('Parse error:', e);
resolve({ name, error: e.message });
}
});
}).on('error', (err) => {
console.error('Request error:', err);
resolve({ name, error: err.message });
});
});
}
async function runTests() {
console.log('Testing AviationStack API endpoints...');
console.log('Flight:', FLIGHT);
console.log('Date:', DATE);
// Test different endpoints
const tests = [
// Real-time flights (current)
testEndpoint(
'Real-time Flights',
`https://api.aviationstack.com/v1/flights?access_key=${API_KEY}&limit=1`
),
// Flights with IATA code
testEndpoint(
'Flights by IATA',
`https://api.aviationstack.com/v1/flights?access_key=${API_KEY}&flight_iata=${FLIGHT}&limit=1`
),
// Flights with date
testEndpoint(
'Flights by IATA + Date',
`https://api.aviationstack.com/v1/flights?access_key=${API_KEY}&flight_iata=${FLIGHT}&flight_date=${DATE}&limit=1`
),
// Flight schedules
testEndpoint(
'Flight Schedules',
`https://api.aviationstack.com/v1/schedules?access_key=${API_KEY}&flight_iata=${FLIGHT}&limit=1`
),
// Airlines endpoint
testEndpoint(
'Airlines (test access)',
`https://api.aviationstack.com/v1/airlines?access_key=${API_KEY}&limit=1`
),
// Airports endpoint
testEndpoint(
'Airports (test access)',
`https://api.aviationstack.com/v1/airports?access_key=${API_KEY}&limit=1`
)
];
const results = await Promise.all(tests);
console.log('\n\n=== SUMMARY ===');
results.forEach(result => {
if (result.data && !result.data.error) {
console.log(`${result.name}: SUCCESS`);
} else if (result.data && result.data.error) {
console.log(`${result.name}: ${result.data.error.message || result.data.error}`);
} else {
console.log(`${result.name}: ${result.error}`);
}
});
}
runTests();

68
test-flight-api.js Normal file
View File

@@ -0,0 +1,68 @@
// Test script to verify AviationStack API
const https = require('https');
// You'll need to replace this with your actual API key
const API_KEY = process.env.AVIATIONSTACK_API_KEY || 'YOUR_API_KEY_HERE';
function testFlight(flightNumber, date) {
console.log(`\nTesting flight: ${flightNumber} on ${date}`);
console.log('API Key:', API_KEY ? `${API_KEY.substring(0, 4)}...` : 'Not set');
if (!API_KEY || API_KEY === 'YOUR_API_KEY_HERE') {
console.error('Please set your AviationStack API key!');
console.log('Run: export AVIATIONSTACK_API_KEY=your_actual_key');
return;
}
// Format flight number
const formattedFlight = flightNumber.replace(/\s+/g, '').toUpperCase();
console.log('Formatted flight:', formattedFlight);
const url = `https://api.aviationstack.com/v1/flights?access_key=${API_KEY}&flight_iata=${formattedFlight}&flight_date=${date}&limit=1`;
console.log('URL:', url.replace(API_KEY, '***'));
https.get(url, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
const parsed = JSON.parse(data);
console.log('\nResponse:', JSON.stringify(parsed, null, 2));
if (parsed.error) {
console.error('\nAPI Error:', parsed.error);
} else if (parsed.data && parsed.data.length > 0) {
const flight = parsed.data[0];
console.log('\n✅ Flight Found!');
console.log('Flight:', flight.flight.iata);
console.log('Airline:', flight.airline && flight.airline.name);
console.log('Status:', flight.flight_status);
console.log('Departure:', flight.departure.airport, 'at', flight.departure.scheduled);
console.log('Arrival:', flight.arrival.airport, 'at', flight.arrival.scheduled);
} else {
console.log('\n❌ No flights found');
}
} catch (e) {
console.error('Parse error:', e);
console.log('Raw response:', data);
}
});
}).on('error', (err) => {
console.error('Request error:', err);
});
}
// Test cases
console.log('=== AviationStack API Test ===');
// Test with AA 4563 on June 2nd
testFlight('AA 4563', '2025-06-02');
// You can also test with today's date
const today = new Date().toISOString().split('T')[0];
console.log('\nYou can also test with today\'s date:', today);
console.log('Example: node test-flight-api.js');

124
update-departments.js Normal file
View File

@@ -0,0 +1,124 @@
// Script to add department field to existing VIPs and drivers
const API_BASE = 'http://localhost:3000/api';
async function updateVipsWithDepartments() {
console.log('🔄 Updating VIPs with department field...');
try {
// Get all VIPs
const vipsResponse = await fetch(`${API_BASE}/vips`);
const vips = await vipsResponse.json();
console.log(`📋 Found ${vips.length} VIPs to update`);
// Update each VIP with department field
for (const vip of vips) {
if (!vip.department) {
// Assign departments based on organization or name patterns
let department = 'Office of Development'; // Default
// Simple logic to assign departments
if (vip.organization && (
vip.organization.toLowerCase().includes('admin') ||
vip.organization.toLowerCase().includes('system') ||
vip.organization.toLowerCase().includes('tech')
)) {
department = 'Admin';
}
const updateData = {
...vip,
department: department
};
const updateResponse = await fetch(`${API_BASE}/vips/${vip.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updateData)
});
if (updateResponse.ok) {
console.log(`✅ Updated ${vip.name}${department}`);
} else {
console.log(`❌ Failed to update ${vip.name}`);
}
} else {
console.log(`⏭️ ${vip.name} already has department: ${vip.department}`);
}
}
} catch (error) {
console.error('❌ Error updating VIPs:', error);
}
}
async function updateDriversWithDepartments() {
console.log('\n🔄 Updating drivers with department field...');
try {
// Get all drivers
const driversResponse = await fetch(`${API_BASE}/drivers`);
const drivers = await driversResponse.json();
console.log(`🚗 Found ${drivers.length} drivers to update`);
// Update each driver with department field
for (let i = 0; i < drivers.length; i++) {
const driver = drivers[i];
if (!driver.department) {
// Alternate between departments for variety
const department = i % 2 === 0 ? 'Office of Development' : 'Admin';
const updateData = {
...driver,
department: department
};
const updateResponse = await fetch(`${API_BASE}/drivers/${driver.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updateData)
});
if (updateResponse.ok) {
console.log(`✅ Updated ${driver.name}${department}`);
} else {
console.log(`❌ Failed to update ${driver.name}`);
}
} else {
console.log(`⏭️ ${driver.name} already has department: ${driver.department}`);
}
}
} catch (error) {
console.error('❌ Error updating drivers:', error);
}
}
async function main() {
console.log('🚀 Starting department field updates...\n');
// Check if API is available
try {
const healthCheck = await fetch(`${API_BASE}/health`);
if (!healthCheck.ok) {
console.error('❌ API is not responding. Make sure the backend is running on port 3000');
return;
}
} catch (error) {
console.error('❌ Cannot connect to API. Make sure the backend is running on port 3000');
return;
}
await updateVipsWithDepartments();
await updateDriversWithDepartments();
console.log('\n✅ Department field updates completed!');
console.log('\n🎯 Department assignments:');
console.log('🏢 Office of Development - Main VIP coordination department');
console.log('⚙️ Admin - Administrative and technical support department');
console.log('\nYou can now filter and organize VIPs and drivers by their departments!');
}
// Run the script
main().catch(console.error);