Major Enhancement: NestJS Migration + CASL Authorization + Error Handling
Some checks failed
CI/CD Pipeline / Backend Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Tests (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled

Complete rewrite from Express to NestJS with enterprise-grade features:

## Backend Improvements
- Migrated from Express to NestJS 11.0.1 with TypeScript
- Implemented Prisma ORM 7.3.0 for type-safe database access
- Added CASL authorization system replacing role-based guards
- Created global exception filters with structured logging
- Implemented Auth0 JWT authentication with Passport.js
- Added vehicle management with conflict detection
- Enhanced event scheduling with driver/vehicle assignment
- Comprehensive error handling and logging

## Frontend Improvements
- Upgraded to React 19.2.0 with Vite 7.2.4
- Implemented CASL-based permission system
- Added AbilityContext for declarative permissions
- Created ErrorHandler utility for consistent error messages
- Enhanced API client with request/response logging
- Added War Room (Command Center) dashboard
- Created VIP Schedule view with complete itineraries
- Implemented Vehicle Management UI
- Added mock data generators for testing (288 events across 20 VIPs)

## New Features
- Vehicle fleet management (types, capacity, status tracking)
- Complete 3-day Jamboree schedule generation
- Individual VIP schedule pages with PDF export (planned)
- Real-time War Room dashboard with auto-refresh
- Permission-based navigation filtering
- First user auto-approval as administrator

## Documentation
- Created CASL_AUTHORIZATION.md (comprehensive guide)
- Created ERROR_HANDLING.md (error handling patterns)
- Updated CLAUDE.md with new architecture
- Added migration guides and best practices

## Technical Debt Resolved
- Removed custom authentication in favor of Auth0
- Replaced role checks with CASL abilities
- Standardized error responses across API
- Implemented proper TypeScript typing
- Added comprehensive logging

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-31 08:50:25 +01:00
parent 8ace1ab2c1
commit 868f7efc23
351 changed files with 44997 additions and 6276 deletions

View File

@@ -0,0 +1,29 @@
import { Request, Response, NextFunction } from 'express';
declare class AuthService {
private jwtSecret;
private jwtExpiry;
private googleClient;
constructor();
generateToken(user: any): string;
verifyGoogleToken(credential: string): Promise<{
user: any;
token: string;
}>;
verifyToken(token: string): any;
requireAuth: (req: Request & {
user?: any;
}, res: Response, next: NextFunction) => Promise<Response<any, Record<string, any>> | undefined>;
requireRole: (roles: string[]) => (req: Request & {
user?: any;
}, res: Response, next: NextFunction) => Response<any, Record<string, any>> | undefined;
getGoogleAuthUrl(): string;
exchangeGoogleCode(code: string): Promise<any>;
getGoogleUserInfo(accessToken: string): Promise<any>;
handleGoogleAuth(code: string): Promise<{
user: any;
token: string;
}>;
}
declare const _default: AuthService;
export default _default;
//# sourceMappingURL=authService.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"authService.d.ts","sourceRoot":"","sources":["../../src/services/authService.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAK1D,cAAM,WAAW;IACf,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,SAAS,CAAiB;IAClC,OAAO,CAAC,YAAY,CAAe;;IAoBnC,aAAa,CAAC,IAAI,EAAE,GAAG,GAAG,MAAM;IAM1B,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,GAAG,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAqClF,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG;IAS/B,WAAW,GAAU,KAAK,OAAO,GAAG;QAAE,IAAI,CAAC,EAAE,GAAG,CAAA;KAAE,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,6DAoBnF;IAGF,WAAW,GAAI,OAAO,MAAM,EAAE,MACpB,KAAK,OAAO,GAAG;QAAE,IAAI,CAAC,EAAE,GAAG,CAAA;KAAE,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,oDAWxE;IAGF,gBAAgB,IAAI,MAAM;IAiBpB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC;IAoB9C,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC;IAapD,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,GAAG,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CAyB5E;;AAED,wBAAiC"}

View File

@@ -0,0 +1,168 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const jwt = require('jsonwebtoken');
const google_auth_library_1 = require("google-auth-library");
const unifiedDataService_1 = __importDefault(require("./unifiedDataService"));
// Simplified authentication service - removes excessive logging and complexity
class AuthService {
constructor() {
this.jwtExpiry = '24h';
// Middleware to check authentication
this.requireAuth = async (req, res, next) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'Authentication required' });
}
const decoded = this.verifyToken(token);
if (!decoded) {
return res.status(401).json({ error: 'Invalid or expired token' });
}
// Get fresh user data
const user = await unifiedDataService_1.default.getUserById(decoded.id);
if (!user) {
return res.status(401).json({ error: 'User not found' });
}
req.user = user;
next();
};
// Middleware to check role
this.requireRole = (roles) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
};
// Auto-generate a secure JWT secret if not provided
if (process.env.JWT_SECRET) {
this.jwtSecret = process.env.JWT_SECRET;
console.log('Using JWT_SECRET from environment');
}
else {
// Generate a cryptographically secure random secret
const crypto = require('crypto');
this.jwtSecret = crypto.randomBytes(64).toString('hex');
console.log('Generated new JWT_SECRET (this will change on restart)');
console.log('To persist sessions across restarts, set JWT_SECRET in .env');
}
// Initialize Google OAuth client
this.googleClient = new google_auth_library_1.OAuth2Client(process.env.GOOGLE_CLIENT_ID);
}
// Generate JWT token
generateToken(user) {
const payload = { id: user.id, email: user.email, role: user.role };
return jwt.sign(payload, this.jwtSecret, { expiresIn: this.jwtExpiry });
}
// Verify Google ID token from frontend
async verifyGoogleToken(credential) {
try {
// Verify the token with Google
const ticket = await this.googleClient.verifyIdToken({
idToken: credential,
audience: process.env.GOOGLE_CLIENT_ID,
});
const payload = ticket.getPayload();
if (!payload || !payload.email) {
throw new Error('Invalid token payload');
}
// Find or create user
let user = await unifiedDataService_1.default.getUserByEmail(payload.email);
if (!user) {
// Auto-create user with coordinator role
user = await unifiedDataService_1.default.createUser({
email: payload.email,
name: payload.name || payload.email,
role: 'coordinator',
googleId: payload.sub
});
}
// Generate our JWT
const token = this.generateToken(user);
return { user, token };
}
catch (error) {
console.error('Token verification error:', error);
throw new Error('Failed to verify Google token');
}
}
// Verify JWT token
verifyToken(token) {
try {
return jwt.verify(token, this.jwtSecret);
}
catch (error) {
return null;
}
}
// Google OAuth helpers
getGoogleAuthUrl() {
if (!process.env.GOOGLE_CLIENT_ID || !process.env.GOOGLE_REDIRECT_URI) {
throw new Error('Google OAuth not configured. Please set GOOGLE_CLIENT_ID and GOOGLE_REDIRECT_URI in .env file');
}
const params = new URLSearchParams({
client_id: process.env.GOOGLE_CLIENT_ID,
redirect_uri: process.env.GOOGLE_REDIRECT_URI,
response_type: 'code',
scope: 'email profile',
access_type: 'offline',
prompt: 'consent'
});
return `https://accounts.google.com/o/oauth2/v2/auth?${params}`;
}
async exchangeGoogleCode(code) {
const response = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
code,
client_id: process.env.GOOGLE_CLIENT_ID,
client_secret: process.env.GOOGLE_CLIENT_SECRET,
redirect_uri: process.env.GOOGLE_REDIRECT_URI,
grant_type: 'authorization_code'
})
});
if (!response.ok) {
throw new Error('Failed to exchange authorization code');
}
return response.json();
}
async getGoogleUserInfo(accessToken) {
const response = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
headers: { Authorization: `Bearer ${accessToken}` }
});
if (!response.ok) {
throw new Error('Failed to get user info');
}
return response.json();
}
// Simplified login/signup
async handleGoogleAuth(code) {
// Exchange code for tokens
const tokens = await this.exchangeGoogleCode(code);
// Get user info
const googleUser = await this.getGoogleUserInfo(tokens.access_token);
// Find or create user
let user = await unifiedDataService_1.default.getUserByEmail(googleUser.email);
if (!user) {
// Auto-create user with coordinator role
user = await unifiedDataService_1.default.createUser({
email: googleUser.email,
name: googleUser.name,
role: 'coordinator',
googleId: googleUser.id
});
}
// Generate JWT
const token = this.generateToken(user);
return { user, token };
}
}
exports.default = new AuthService();
//# sourceMappingURL=authService.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,39 @@
declare class DataService {
private dataDir;
private dataFile;
private data;
constructor();
private loadData;
private saveData;
getVips(): any[];
addVip(vip: any): any;
updateVip(id: string, updatedVip: any): any | null;
deleteVip(id: string): any | null;
getDrivers(): any[];
addDriver(driver: any): any;
updateDriver(id: string, updatedDriver: any): any | null;
deleteDriver(id: string): any | null;
getSchedule(vipId: string): any[];
addScheduleEvent(vipId: string, event: any): any;
updateScheduleEvent(vipId: string, eventId: string, updatedEvent: any): any | null;
deleteScheduleEvent(vipId: string, eventId: string): any | null;
getAllSchedules(): {
[vipId: string]: any[];
};
getAdminSettings(): any;
updateAdminSettings(settings: any): void;
createBackup(): string;
getUsers(): any[];
getUserByEmail(email: string): any | null;
getUserById(id: string): any | null;
addUser(user: any): any;
updateUser(email: string, updatedUser: any): any | null;
updateUserRole(email: string, role: string): any | null;
updateUserLastSignIn(email: string): any | null;
deleteUser(email: string): any | null;
getUserCount(): number;
getDataStats(): any;
}
declare const _default: DataService;
export default _default;
//# sourceMappingURL=dataService.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"dataService.d.ts","sourceRoot":"","sources":["../../src/services/dataService.ts"],"names":[],"mappings":"AAWA,cAAM,WAAW;IACf,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,IAAI,CAAY;;IAcxB,OAAO,CAAC,QAAQ;IA6ChB,OAAO,CAAC,QAAQ;IAWhB,OAAO,IAAI,GAAG,EAAE;IAIhB,MAAM,CAAC,GAAG,EAAE,GAAG,GAAG,GAAG;IAMrB,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,GAAG,GAAG,GAAG,IAAI;IAUlD,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,GAAG,GAAG,IAAI;IAajC,UAAU,IAAI,GAAG,EAAE;IAInB,SAAS,CAAC,MAAM,EAAE,GAAG,GAAG,GAAG;IAM3B,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,aAAa,EAAE,GAAG,GAAG,GAAG,GAAG,IAAI;IAUxD,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,GAAG,GAAG,IAAI;IAWpC,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,EAAE;IAIjC,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,GAAG,GAAG;IAShD,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,GAAG,GAAG,GAAG,GAAG,IAAI;IAclF,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,GAAG,GAAG,IAAI;IAc/D,eAAe,IAAI;QAAE,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,EAAE,CAAA;KAAE;IAK7C,gBAAgB,IAAI,GAAG;IAIvB,mBAAmB,CAAC,QAAQ,EAAE,GAAG,GAAG,IAAI;IAMxC,YAAY,IAAI,MAAM;IAetB,QAAQ,IAAI,GAAG,EAAE;IAIjB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,GAAG,IAAI;IAIzC,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,GAAG,GAAG,IAAI;IAInC,OAAO,CAAC,IAAI,EAAE,GAAG,GAAG,GAAG;IAcvB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,GAAG,GAAG,GAAG,GAAG,IAAI;IAWvD,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,GAAG,GAAG,IAAI;IAWvD,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,GAAG,IAAI;IAU/C,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,GAAG,IAAI;IAWrC,YAAY,IAAI,MAAM;IAItB,YAAY,IAAI,GAAG;CAWpB;;AAED,wBAAiC"}

View File

@@ -0,0 +1,264 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
class DataService {
constructor() {
this.dataDir = path_1.default.join(process.cwd(), 'data');
this.dataFile = path_1.default.join(this.dataDir, 'vip-coordinator.json');
// Ensure data directory exists
if (!fs_1.default.existsSync(this.dataDir)) {
fs_1.default.mkdirSync(this.dataDir, { recursive: true });
}
this.data = this.loadData();
}
loadData() {
try {
if (fs_1.default.existsSync(this.dataFile)) {
const fileContent = fs_1.default.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
}
}
};
}
saveData() {
try {
const dataToSave = JSON.stringify(this.data, null, 2);
fs_1.default.writeFileSync(this.dataFile, dataToSave, 'utf8');
console.log(`💾 Data saved to ${this.dataFile}`);
}
catch (error) {
console.error('Error saving data file:', error);
}
}
// VIP operations
getVips() {
return this.data.vips;
}
addVip(vip) {
this.data.vips.push(vip);
this.saveData();
return vip;
}
updateVip(id, updatedVip) {
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) {
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() {
return this.data.drivers;
}
addDriver(driver) {
this.data.drivers.push(driver);
this.saveData();
return driver;
}
updateDriver(id, updatedDriver) {
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) {
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) {
return this.data.schedules[vipId] || [];
}
addScheduleEvent(vipId, event) {
if (!this.data.schedules[vipId]) {
this.data.schedules[vipId] = [];
}
this.data.schedules[vipId].push(event);
this.saveData();
return event;
}
updateScheduleEvent(vipId, eventId, updatedEvent) {
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, eventId) {
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() {
return this.data.schedules;
}
// Admin settings operations
getAdminSettings() {
return this.data.adminSettings;
}
updateAdminSettings(settings) {
this.data.adminSettings = { ...this.data.adminSettings, ...settings };
this.saveData();
}
// Backup and restore operations
createBackup() {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupFile = path_1.default.join(this.dataDir, `backup-${timestamp}.json`);
try {
fs_1.default.copyFileSync(this.dataFile, backupFile);
console.log(`📦 Backup created: ${backupFile}`);
return backupFile;
}
catch (error) {
console.error('Error creating backup:', error);
throw error;
}
}
// User operations
getUsers() {
return this.data.users;
}
getUserByEmail(email) {
return this.data.users.find(user => user.email === email) || null;
}
getUserById(id) {
return this.data.users.find(user => user.id === id) || null;
}
addUser(user) {
// 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, updatedUser) {
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, role) {
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) {
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) {
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() {
return this.data.users.length;
}
getDataStats() {
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_1.default.existsSync(this.dataFile) ? fs_1.default.statSync(this.dataFile).mtime : null
};
}
}
exports.default = new DataService();
//# sourceMappingURL=dataService.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,56 @@
import { PoolClient } from 'pg';
declare class EnhancedDatabaseService {
private backupService;
constructor();
query(text: string, params?: any[]): Promise<any>;
getClient(): Promise<PoolClient>;
close(): Promise<void>;
initializeTables(): Promise<void>;
createUser(user: any): Promise<any>;
getUserByEmail(email: string): Promise<any>;
getUserById(id: string): Promise<any>;
updateUserRole(email: string, role: string): Promise<any>;
updateUserLastSignIn(email: string): Promise<any>;
getUserCount(): Promise<number>;
updateUserApprovalStatus(email: string, status: 'pending' | 'approved' | 'denied'): Promise<any>;
getApprovedUserCount(): Promise<number>;
getAllUsers(): Promise<any[]>;
deleteUser(email: string): Promise<boolean>;
getPendingUsers(): Promise<any[]>;
completeUserOnboarding(email: string, onboardingData: any): Promise<any>;
approveUser(userEmail: string, approvedBy: string, newRole?: string): Promise<any>;
rejectUser(userEmail: string, rejectedBy: string, reason?: string): Promise<any>;
deactivateUser(userEmail: string, deactivatedBy: string): Promise<any>;
reactivateUser(userEmail: string, reactivatedBy: string): Promise<any>;
createAuditLog(action: string, userEmail: string, performedBy: string, details: any): Promise<void>;
getUserAuditLog(userEmail: string): Promise<any[]>;
getUsersWithFilters(filters: {
status?: string;
role?: string;
search?: string;
}): Promise<any[]>;
getActiveUserCount(): Promise<number>;
isFirstUser(): Promise<boolean>;
createVip(vip: any): Promise<any>;
getVipById(id: string): Promise<any>;
getAllVips(): Promise<any[]>;
updateVip(id: string, vip: any): Promise<any>;
deleteVip(id: string): Promise<boolean>;
getVipsByDepartment(department: string): Promise<any[]>;
createDriver(driver: any): Promise<any>;
getDriverById(id: string): Promise<any>;
getAllDrivers(): Promise<any[]>;
updateDriver(id: string, driver: any): Promise<any>;
deleteDriver(id: string): Promise<boolean>;
getDriversByDepartment(department: string): Promise<any[]>;
updateDriverLocation(id: string, location: any): Promise<any>;
createScheduleEvent(vipId: string, event: any): Promise<any>;
getScheduleByVipId(vipId: string): Promise<any[]>;
updateScheduleEvent(vipId: string, eventId: string, event: any): Promise<any>;
deleteScheduleEvent(vipId: string, eventId: string): Promise<boolean>;
getAllScheduleEvents(): Promise<any[]>;
getScheduleEventsByDateRange(startDate: Date, endDate: Date): Promise<any[]>;
}
declare const databaseService: EnhancedDatabaseService;
export default databaseService;
//# sourceMappingURL=databaseService.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"databaseService.d.ts","sourceRoot":"","sources":["../../src/services/databaseService.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,265 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
// Import the existing backup service
const databaseService_1 = __importDefault(require("./backup-services/databaseService"));
// Extend the backup service with new user management methods
class EnhancedDatabaseService {
constructor() {
this.backupService = databaseService_1.default;
}
// Delegate all existing methods to backup service
async query(text, params) {
return this.backupService.query(text, params);
}
async getClient() {
return this.backupService.getClient();
}
async close() {
return this.backupService.close();
}
async initializeTables() {
return this.backupService.initializeTables();
}
// User methods from backup service
async createUser(user) {
return this.backupService.createUser(user);
}
async getUserByEmail(email) {
return this.backupService.getUserByEmail(email);
}
async getUserById(id) {
return this.backupService.getUserById(id);
}
async updateUserRole(email, role) {
return this.backupService.updateUserRole(email, role);
}
async updateUserLastSignIn(email) {
return this.backupService.updateUserLastSignIn(email);
}
async getUserCount() {
return this.backupService.getUserCount();
}
async updateUserApprovalStatus(email, status) {
return this.backupService.updateUserApprovalStatus(email, status);
}
async getApprovedUserCount() {
return this.backupService.getApprovedUserCount();
}
async getAllUsers() {
return this.backupService.getAllUsers();
}
async deleteUser(email) {
return this.backupService.deleteUser(email);
}
async getPendingUsers() {
return this.backupService.getPendingUsers();
}
// NEW: Enhanced user management methods
async completeUserOnboarding(email, onboardingData) {
const query = `
UPDATE users
SET phone = $1,
organization = $2,
onboarding_data = $3,
updated_at = CURRENT_TIMESTAMP
WHERE email = $4
RETURNING *
`;
const result = await this.query(query, [
onboardingData.phone,
onboardingData.organization,
JSON.stringify(onboardingData),
email
]);
return result.rows[0] || null;
}
async approveUser(userEmail, approvedBy, newRole) {
const query = `
UPDATE users
SET status = 'active',
approval_status = 'approved',
approved_by = $1,
approved_at = CURRENT_TIMESTAMP,
role = COALESCE($2, role),
updated_at = CURRENT_TIMESTAMP
WHERE email = $3
RETURNING *
`;
const result = await this.query(query, [approvedBy, newRole, userEmail]);
// Log audit
if (result.rows[0]) {
await this.createAuditLog('user_approved', userEmail, approvedBy, { newRole });
}
return result.rows[0] || null;
}
async rejectUser(userEmail, rejectedBy, reason) {
const query = `
UPDATE users
SET status = 'deactivated',
approval_status = 'denied',
rejected_by = $1,
rejected_at = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP
WHERE email = $2
RETURNING *
`;
const result = await this.query(query, [rejectedBy, userEmail]);
// Log audit
if (result.rows[0]) {
await this.createAuditLog('user_rejected', userEmail, rejectedBy, { reason });
}
return result.rows[0] || null;
}
async deactivateUser(userEmail, deactivatedBy) {
const query = `
UPDATE users
SET status = 'deactivated',
deactivated_by = $1,
deactivated_at = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP
WHERE email = $2
RETURNING *
`;
const result = await this.query(query, [deactivatedBy, userEmail]);
// Log audit
if (result.rows[0]) {
await this.createAuditLog('user_deactivated', userEmail, deactivatedBy, {});
}
return result.rows[0] || null;
}
async reactivateUser(userEmail, reactivatedBy) {
const query = `
UPDATE users
SET status = 'active',
deactivated_by = NULL,
deactivated_at = NULL,
updated_at = CURRENT_TIMESTAMP
WHERE email = $1
RETURNING *
`;
const result = await this.query(query, [userEmail]);
// Log audit
if (result.rows[0]) {
await this.createAuditLog('user_reactivated', userEmail, reactivatedBy, {});
}
return result.rows[0] || null;
}
async createAuditLog(action, userEmail, performedBy, details) {
const query = `
INSERT INTO user_audit_log (action, user_email, performed_by, action_details)
VALUES ($1, $2, $3, $4)
`;
await this.query(query, [action, userEmail, performedBy, JSON.stringify(details)]);
}
async getUserAuditLog(userEmail) {
const query = `
SELECT * FROM user_audit_log
WHERE user_email = $1
ORDER BY created_at DESC
`;
const result = await this.query(query, [userEmail]);
return result.rows;
}
async getUsersWithFilters(filters) {
let query = 'SELECT * FROM users WHERE 1=1';
const params = [];
let paramIndex = 1;
if (filters.status) {
query += ` AND status = $${paramIndex}`;
params.push(filters.status);
paramIndex++;
}
if (filters.role) {
query += ` AND role = $${paramIndex}`;
params.push(filters.role);
paramIndex++;
}
if (filters.search) {
query += ` AND (LOWER(name) LIKE LOWER($${paramIndex}) OR LOWER(email) LIKE LOWER($${paramIndex}) OR LOWER(organization) LIKE LOWER($${paramIndex}))`;
params.push(`%${filters.search}%`);
paramIndex++;
}
query += ' ORDER BY created_at DESC';
const result = await this.query(query, params);
return result.rows;
}
// Fix for first user admin issue
async getActiveUserCount() {
const query = "SELECT COUNT(*) as count FROM users WHERE status = 'active'";
const result = await this.query(query);
return parseInt(result.rows[0].count);
}
async isFirstUser() {
// Check if there are any active or approved users
const query = "SELECT COUNT(*) as count FROM users WHERE status = 'active' OR approval_status = 'approved'";
const result = await this.query(query);
return parseInt(result.rows[0].count) === 0;
}
// VIP methods from backup service
async createVip(vip) {
return this.backupService.createVip(vip);
}
async getVipById(id) {
return this.backupService.getVipById(id);
}
async getAllVips() {
return this.backupService.getAllVips();
}
async updateVip(id, vip) {
return this.backupService.updateVip(id, vip);
}
async deleteVip(id) {
return this.backupService.deleteVip(id);
}
async getVipsByDepartment(department) {
return this.backupService.getVipsByDepartment(department);
}
// Driver methods from backup service
async createDriver(driver) {
return this.backupService.createDriver(driver);
}
async getDriverById(id) {
return this.backupService.getDriverById(id);
}
async getAllDrivers() {
return this.backupService.getAllDrivers();
}
async updateDriver(id, driver) {
return this.backupService.updateDriver(id, driver);
}
async deleteDriver(id) {
return this.backupService.deleteDriver(id);
}
async getDriversByDepartment(department) {
return this.backupService.getDriversByDepartment(department);
}
async updateDriverLocation(id, location) {
return this.backupService.updateDriverLocation(id, location);
}
// Schedule methods from backup service
async createScheduleEvent(vipId, event) {
return this.backupService.createScheduleEvent(vipId, event);
}
async getScheduleByVipId(vipId) {
return this.backupService.getScheduleByVipId(vipId);
}
async updateScheduleEvent(vipId, eventId, event) {
return this.backupService.updateScheduleEvent(vipId, eventId, event);
}
async deleteScheduleEvent(vipId, eventId) {
return this.backupService.deleteScheduleEvent(vipId, eventId);
}
async getAllScheduleEvents() {
return this.backupService.getAllScheduleEvents();
}
async getScheduleEventsByDateRange(startDate, endDate) {
return this.backupService.getScheduleEventsByDateRange(startDate, endDate);
}
}
// Export singleton instance
const databaseService = new EnhancedDatabaseService();
exports.default = databaseService;
//# sourceMappingURL=databaseService.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,49 @@
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;
}
interface DriverAvailability {
driverId: string;
driverName: string;
status: 'available' | 'scheduled' | 'overlapping' | 'tight_turnaround';
assignmentCount: number;
conflicts: ConflictInfo[];
currentAssignments: ScheduleEvent[];
}
declare class DriverConflictService {
checkDriverConflicts(driverId: string, newEvent: {
startTime: string;
endTime: string;
location: string;
}, allSchedules: {
[vipId: string]: ScheduleEvent[];
}, drivers: any[]): ConflictInfo[];
getDriverAvailability(eventTime: {
startTime: string;
endTime: string;
location: string;
}, allSchedules: {
[vipId: string]: ScheduleEvent[];
}, drivers: any[]): DriverAvailability[];
private getDriverEvents;
private hasTimeOverlap;
private getTimeBetweenEvents;
getDriverStatusSummary(availability: DriverAvailability): string;
}
declare const _default: DriverConflictService;
export default _default;
export { DriverAvailability, ConflictInfo, ScheduleEvent };
//# sourceMappingURL=driverConflictService.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"driverConflictService.d.ts","sourceRoot":"","sources":["../../src/services/driverConflictService.ts"],"names":[],"mappings":"AAAA,UAAU,aAAa;IACrB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,UAAU,YAAY;IACpB,IAAI,EAAE,SAAS,GAAG,kBAAkB,GAAG,cAAc,CAAC;IACtD,QAAQ,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,gBAAgB,EAAE,aAAa,CAAC;IAChC,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,UAAU,kBAAkB;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,WAAW,GAAG,WAAW,GAAG,aAAa,GAAG,kBAAkB,CAAC;IACvE,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,YAAY,EAAE,CAAC;IAC1B,kBAAkB,EAAE,aAAa,EAAE,CAAC;CACrC;AAED,cAAM,qBAAqB;IAGzB,oBAAoB,CAClB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,EAClE,YAAY,EAAE;QAAE,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,EAAE,CAAA;KAAE,EAClD,OAAO,EAAE,GAAG,EAAE,GACb,YAAY,EAAE;IA8CjB,qBAAqB,CACnB,SAAS,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,EACnE,YAAY,EAAE;QAAE,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,EAAE,CAAA;KAAE,EAClD,OAAO,EAAE,GAAG,EAAE,GACb,kBAAkB,EAAE;IAgCvB,OAAO,CAAC,eAAe;IAsBvB,OAAO,CAAC,cAAc;IAQtB,OAAO,CAAC,oBAAoB;IAiB5B,sBAAsB,CAAC,YAAY,EAAE,kBAAkB,GAAG,MAAM;CAejE;;AAED,wBAA2C;AAC3C,OAAO,EAAE,kBAAkB,EAAE,YAAY,EAAE,aAAa,EAAE,CAAC"}

View File

@@ -0,0 +1,123 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
class DriverConflictService {
// Check for conflicts when assigning a driver to an event
checkDriverConflicts(driverId, newEvent, allSchedules, drivers) {
const conflicts = [];
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, allSchedules, drivers) {
return drivers.map(driver => {
const conflicts = this.checkDriverConflicts(driver.id, eventTime, allSchedules, drivers);
const driverEvents = this.getDriverEvents(driver.id, allSchedules);
let 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
getDriverEvents(driverId, allSchedules) {
const driverEvents = [];
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
hasTimeOverlap(start1, end1, start2, end2) {
return start1 < end2 && start2 < end1;
}
// Get minutes between two events (null if they overlap)
getTimeBetweenEvents(newStart, newEnd, existingStart, existingEnd) {
// 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) {
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';
}
}
}
exports.default = new DriverConflictService();
//# sourceMappingURL=driverConflictService.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"driverConflictService.js","sourceRoot":"","sources":["../../src/services/driverConflictService.ts"],"names":[],"mappings":";;AA4BA,MAAM,qBAAqB;IAEzB,0DAA0D;IAC1D,oBAAoB,CAClB,QAAgB,EAChB,QAAkE,EAClE,YAAkD,EAClD,OAAc;QAEd,MAAM,SAAS,GAAmB,EAAE,CAAC;QACrC,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,QAAQ,CAAC,CAAC;QACpD,IAAI,CAAC,MAAM;YAAE,OAAO,SAAS,CAAC;QAE9B,yCAAyC;QACzC,MAAM,YAAY,GAAG,IAAI,CAAC,eAAe,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC;QAElE,MAAM,YAAY,GAAG,IAAI,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;QAClD,MAAM,UAAU,GAAG,IAAI,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QAE9C,KAAK,MAAM,aAAa,IAAI,YAAY,EAAE,CAAC;YACzC,MAAM,aAAa,GAAG,IAAI,IAAI,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC;YACxD,MAAM,WAAW,GAAG,IAAI,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;YAEpD,gCAAgC;YAChC,IAAI,IAAI,CAAC,cAAc,CAAC,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,WAAW,CAAC,EAAE,CAAC;gBAC9E,SAAS,CAAC,IAAI,CAAC;oBACb,IAAI,EAAE,SAAS;oBACf,QAAQ,EAAE,MAAM;oBAChB,OAAO,EAAE,8BAA8B,aAAa,CAAC,KAAK,SAAS,aAAa,CAAC,OAAO,EAAE;oBAC1F,gBAAgB,EAAE,aAAa;iBAChC,CAAC,CAAC;YACL,CAAC;YACD,mEAAmE;iBAC9D,CAAC;gBACJ,MAAM,WAAW,GAAG,IAAI,CAAC,oBAAoB,CAC3C,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,WAAW,CACrD,CAAC;gBAEF,IAAI,WAAW,KAAK,IAAI,IAAI,WAAW,GAAG,EAAE,EAAE,CAAC;oBAC7C,SAAS,CAAC,IAAI,CAAC;wBACb,IAAI,EAAE,kBAAkB;wBACxB,QAAQ,EAAE,WAAW,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ;wBAC7C,OAAO,EAAE,QAAQ,WAAW,uCAAuC,aAAa,CAAC,KAAK,GAAG;wBACzF,gBAAgB,EAAE,aAAa;wBAC/B,cAAc,EAAE,WAAW;qBAC5B,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;QACH,CAAC;QAED,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,mEAAmE;IACnE,qBAAqB,CACnB,SAAmE,EACnE,YAAkD,EAClD,OAAc;QAEd,OAAO,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE;YAC1B,MAAM,SAAS,GAAG,IAAI,CAAC,oBAAoB,CAAC,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,YAAY,EAAE,OAAO,CAAC,CAAC;YACzF,MAAM,YAAY,GAAG,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,EAAE,EAAE,YAAY,CAAC,CAAC;YAEnE,IAAI,MAAM,GAAiC,WAAW,CAAC;YAEvD,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACzB,MAAM,UAAU,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC;gBAC7D,MAAM,kBAAkB,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,kBAAkB,CAAC,CAAC;gBAE9E,IAAI,UAAU,EAAE,CAAC;oBACf,MAAM,GAAG,aAAa,CAAC;gBACzB,CAAC;qBAAM,IAAI,kBAAkB,EAAE,CAAC;oBAC9B,MAAM,GAAG,kBAAkB,CAAC;gBAC9B,CAAC;YACH,CAAC;iBAAM,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACnC,MAAM,GAAG,WAAW,CAAC;YACvB,CAAC;YAED,OAAO;gBACL,QAAQ,EAAE,MAAM,CAAC,EAAE;gBACnB,UAAU,EAAE,MAAM,CAAC,IAAI;gBACvB,MAAM;gBACN,eAAe,EAAE,YAAY,CAAC,MAAM;gBACpC,SAAS;gBACT,kBAAkB,EAAE,YAAY;aACjC,CAAC;QACJ,CAAC,CAAC,CAAC;IACL,CAAC;IAED,+CAA+C;IACvC,eAAe,CAAC,QAAgB,EAAE,YAAkD;QAC1F,MAAM,YAAY,GAAoB,EAAE,CAAC;QAEzC,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,EAAE,MAAM,CAAC,EAAE,EAAE;YACvD,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE;gBACrB,IAAI,KAAK,CAAC,gBAAgB,KAAK,QAAQ,EAAE,CAAC;oBACxC,YAAY,CAAC,IAAI,CAAC;wBAChB,GAAG,KAAK;wBACR,KAAK;wBACL,OAAO,EAAE,KAAK,CAAC,KAAK,CAAC,kDAAkD;qBACxE,CAAC,CAAC;gBACL,CAAC;YACH,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;QAEH,qBAAqB;QACrB,OAAO,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAChC,IAAI,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,CAClE,CAAC;IACJ,CAAC;IAED,oCAAoC;IAC5B,cAAc,CACpB,MAAY,EAAE,IAAU,EACxB,MAAY,EAAE,IAAU;QAExB,OAAO,MAAM,GAAG,IAAI,IAAI,MAAM,GAAG,IAAI,CAAC;IACxC,CAAC;IAED,wDAAwD;IAChD,oBAAoB,CAC1B,QAAc,EAAE,MAAY,EAC5B,aAAmB,EAAE,WAAiB;QAEtC,uCAAuC;QACvC,IAAI,QAAQ,IAAI,WAAW,EAAE,CAAC;YAC5B,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,OAAO,EAAE,GAAG,WAAW,CAAC,OAAO,EAAE,CAAC,GAAG,CAAC,IAAI,GAAG,EAAE,CAAC,CAAC,CAAC;QAChF,CAAC;QACD,wCAAwC;aACnC,IAAI,MAAM,IAAI,aAAa,EAAE,CAAC;YACjC,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,aAAa,CAAC,OAAO,EAAE,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC,GAAG,CAAC,IAAI,GAAG,EAAE,CAAC,CAAC,CAAC;QAChF,CAAC;QACD,iBAAiB;QACjB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,6CAA6C;IAC7C,sBAAsB,CAAC,YAAgC;QACrD,QAAQ,YAAY,CAAC,MAAM,EAAE,CAAC;YAC5B,KAAK,WAAW;gBACd,OAAO,sBAAsB,YAAY,CAAC,eAAe,eAAe,CAAC;YAC3E,KAAK,WAAW;gBACd,OAAO,UAAU,YAAY,CAAC,eAAe,4CAA4C,CAAC;YAC5F,KAAK,kBAAkB;gBACrB,MAAM,aAAa,GAAG,YAAY,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,kBAAkB,CAAC,CAAC;gBACtF,OAAO,wBAAwB,aAAa,EAAE,cAAc,qBAAqB,CAAC;YACpF,KAAK,aAAa;gBAChB,OAAO,2CAA2C,CAAC;YACrD;gBACE,OAAO,gBAAgB,CAAC;QAC5B,CAAC;IACH,CAAC;CACF;AAED,kBAAe,IAAI,qBAAqB,EAAE,CAAC"}

View File

@@ -0,0 +1,60 @@
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;
}
declare class EnhancedDataService {
getVips(): Promise<VipData[]>;
addVip(vip: VipData): Promise<VipData>;
updateVip(id: string, vip: Partial<VipData>): Promise<VipData | null>;
deleteVip(id: string): Promise<VipData | null>;
getDrivers(): Promise<DriverData[]>;
addDriver(driver: DriverData): Promise<DriverData>;
updateDriver(id: string, driver: Partial<DriverData>): Promise<DriverData | null>;
deleteDriver(id: string): Promise<DriverData | null>;
getSchedule(vipId: string): Promise<ScheduleEventData[]>;
addScheduleEvent(vipId: string, event: ScheduleEventData): Promise<ScheduleEventData>;
updateScheduleEvent(vipId: string, eventId: string, event: ScheduleEventData): Promise<ScheduleEventData | null>;
deleteScheduleEvent(vipId: string, eventId: string): Promise<ScheduleEventData | null>;
getAllSchedules(): Promise<{
[vipId: string]: ScheduleEventData[];
}>;
getAdminSettings(): Promise<any>;
updateAdminSettings(settings: any): Promise<void>;
}
declare const _default: EnhancedDataService;
export default _default;
//# sourceMappingURL=enhancedDataService.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"enhancedDataService.d.ts","sourceRoot":"","sources":["../../src/services/enhancedDataService.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,571 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const database_1 = __importDefault(require("../config/database"));
const databaseService_1 = __importDefault(require("./databaseService"));
class EnhancedDataService {
// VIP operations
async getVips() {
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 database_1.default.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) {
const client = await database_1.default.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, vip) {
const client = await database_1.default.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) {
try {
const query = `
DELETE FROM vips WHERE id = $1 RETURNING *
`;
const result = await database_1.default.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() {
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 database_1.default.query(query);
// Get current locations from Redis
const driversWithLocations = await Promise.all(result.rows.map(async (row) => {
const location = await databaseService_1.default.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) {
try {
const query = `
INSERT INTO drivers (id, name, phone, department)
VALUES ($1, $2, $3, $4)
RETURNING *
`;
const result = await database_1.default.query(query, [
driver.id,
driver.name,
driver.phone,
driver.department || 'Office of Development'
]);
// Store location in Redis if provided
if (driver.currentLocation) {
await databaseService_1.default.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, driver) {
try {
const query = `
UPDATE drivers
SET name = $2, phone = $3, department = $4
WHERE id = $1
RETURNING *
`;
const result = await database_1.default.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_1.default.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) {
try {
const query = `
DELETE FROM drivers WHERE id = $1 RETURNING *
`;
const result = await database_1.default.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) {
try {
const query = `
SELECT * FROM schedule_events
WHERE vip_id = $1
ORDER BY start_time
`;
const result = await database_1.default.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, event) {
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 database_1.default.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, eventId, event) {
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 database_1.default.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, eventId) {
try {
const query = `
DELETE FROM schedule_events
WHERE id = $1 AND vip_id = $2
RETURNING *
`;
const result = await database_1.default.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() {
try {
const query = `
SELECT * FROM schedule_events
ORDER BY vip_id, start_time
`;
const result = await database_1.default.query(query);
const schedules = {};
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() {
try {
const query = `
SELECT setting_key, setting_value FROM admin_settings
`;
const result = await database_1.default.query(query);
// Default settings structure
const defaultSettings = {
apiKeys: {
aviationStackKey: '',
googleMapsKey: '',
twilioKey: '',
googleClientId: '',
googleClientSecret: ''
},
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 = { ...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) {
try {
// Flatten settings and update
const flattenSettings = (obj, prefix = '') => {
const result = [];
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 database_1.default.query(query, [setting.key, setting.value]);
}
}
catch (error) {
console.error('❌ Error updating admin settings:', error);
throw error;
}
}
}
exports.default = new EnhancedDataService();
//# sourceMappingURL=enhancedDataService.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,53 @@
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;
departureAirport?: string;
arrivalAirport?: string;
}
declare class FlightService {
private flightCache;
private updateIntervals;
constructor();
getFlightInfo(params: FlightSearchParams): Promise<FlightData | null>;
private scrapeGoogleFlights;
private getFromAviationStack;
startPeriodicUpdates(params: FlightSearchParams, intervalMinutes?: number): void;
stopPeriodicUpdates(key: string): void;
getMultipleFlights(flightParams: FlightSearchParams[]): Promise<{
[key: string]: FlightData | null;
}>;
private normalizeStatus;
cleanup(): void;
}
declare const _default: FlightService;
export default _default;
export { FlightData, FlightSearchParams };
//# sourceMappingURL=flightService.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"flightService.d.ts","sourceRoot":"","sources":["../../src/services/flightService.ts"],"names":[],"mappings":"AAGA,UAAU,UAAU;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE;QACT,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,CAAC;IACF,OAAO,EAAE;QACP,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,CAAC;IACF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,QAAQ,GAAG,eAAe,GAAG,WAAW,CAAC;CAClD;AAED,UAAU,kBAAkB;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,cAAM,aAAa;IACjB,OAAO,CAAC,WAAW,CAAiE;IACpF,OAAO,CAAC,eAAe,CAA0C;;IAO3D,aAAa,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;YAkC7D,mBAAmB;YAiBnB,oBAAoB;IAiGlC,oBAAoB,CAAC,MAAM,EAAE,kBAAkB,EAAE,eAAe,GAAE,MAAU,GAAG,IAAI;IAoBnF,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAShC,kBAAkB,CAAC,YAAY,EAAE,kBAAkB,EAAE,GAAG,OAAO,CAAC;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAAA;KAAE,CAAC;IAY3G,OAAO,CAAC,eAAe;IAcvB,OAAO,IAAI,IAAI;CAOhB;;AAED,wBAAmC;AACnC,OAAO,EAAE,UAAU,EAAE,kBAAkB,EAAE,CAAC"}

View File

@@ -0,0 +1,196 @@
"use strict";
// Real Flight tracking service with Google scraping
// No mock data - only real flight information
Object.defineProperty(exports, "__esModule", { value: true });
class FlightService {
constructor() {
this.flightCache = new Map();
this.updateIntervals = new Map();
// No API keys needed for Google scraping
}
// Real flight lookup - no mock data
async getFlightInfo(params) {
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
async scrapeGoogleFlights(params) {
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)
async getFromAviationStack(params) {
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 = 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 (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) => 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, intervalMinutes = 5) {
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) {
const interval = this.updateIntervals.get(key);
if (interval) {
clearInterval(interval);
this.updateIntervals.delete(key);
}
}
// Get multiple flights with date specificity
async getMultipleFlights(flightParams) {
const results = {};
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
normalizeStatus(status) {
const statusMap = {
'scheduled': 'scheduled',
'active': 'active',
'landed': 'landed',
'cancelled': 'cancelled',
'incident': 'delayed',
'diverted': 'diverted'
};
return statusMap[status.toLowerCase()] || status;
}
// Clean up resources
cleanup() {
for (const [key, interval] of this.updateIntervals) {
clearInterval(interval);
}
this.updateIntervals.clear();
this.flightCache.clear();
}
}
exports.default = new FlightService();
//# sourceMappingURL=flightService.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,16 @@
declare class FlightTrackingScheduler {
private trackingSchedule;
private checkIntervals;
private flightService;
constructor(flightService: any);
addVipFlights(vipId: string, vipName: string, flights: any[]): void;
removeVipFlights(vipId: string): void;
private updateTrackingSchedules;
private setupDateTracking;
private performBatchCheck;
private stopDateTracking;
getTrackingStatus(): any;
cleanup(): void;
}
export default FlightTrackingScheduler;
//# sourceMappingURL=flightTrackingScheduler.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"flightTrackingScheduler.d.ts","sourceRoot":"","sources":["../../src/services/flightTrackingScheduler.ts"],"names":[],"mappings":"AAoBA,cAAM,uBAAuB;IAC3B,OAAO,CAAC,gBAAgB,CAAwB;IAChD,OAAO,CAAC,cAAc,CAA0C;IAChE,OAAO,CAAC,aAAa,CAAM;gBAEf,aAAa,EAAE,GAAG;IAK9B,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,EAAE;IAoC5D,gBAAgB,CAAC,KAAK,EAAE,MAAM;IAgB9B,OAAO,CAAC,uBAAuB;IAY/B,OAAO,CAAC,iBAAiB;YAsDX,iBAAiB;IAwF/B,OAAO,CAAC,gBAAgB;IAcxB,iBAAiB,IAAI,GAAG;IA0BxB,OAAO;CAKR;AAED,eAAe,uBAAuB,CAAC"}

View File

@@ -0,0 +1,219 @@
"use strict";
// Flight Tracking Scheduler Service
// Efficiently batches flight API calls and manages tracking schedules
Object.defineProperty(exports, "__esModule", { value: true });
class FlightTrackingScheduler {
constructor(flightService) {
this.trackingSchedule = {};
this.checkIntervals = new Map();
this.flightService = flightService;
}
// Add flights for a VIP to the tracking schedule
addVipFlights(vipId, vipName, flights) {
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 = {
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) {
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
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
setupDateTracking(date) {
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 = 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
async performBatchCheck(date) {
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
stopDateTracking(date) {
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() {
const status = {};
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 = {};
}
}
exports.default = FlightTrackingScheduler;
//# sourceMappingURL=flightTrackingScheduler.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,38 @@
export interface User {
id: string;
google_id: string;
email: string;
name: string;
profile_picture_url?: string;
role: 'driver' | 'coordinator' | 'administrator';
status?: 'pending' | 'active' | 'deactivated';
created_at?: string;
last_login?: string;
is_active?: boolean;
updated_at?: string;
approval_status?: string;
onboardingData?: any;
}
declare class JWTKeyManager {
private currentSecret;
private previousSecret;
private rotationInterval;
private gracePeriodTimeout;
constructor();
private generateSecret;
private startRotation;
private rotateKey;
generateToken(user: User): string;
verifyToken(token: string): User | null;
getStatus(): {
hasCurrentKey: boolean;
hasPreviousKey: boolean;
rotationActive: boolean;
gracePeriodActive: boolean;
};
destroy(): void;
forceRotation(): void;
}
export declare const jwtKeyManager: JWTKeyManager;
export default jwtKeyManager;
//# sourceMappingURL=jwtKeyManager.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"jwtKeyManager.d.ts","sourceRoot":"","sources":["../../src/services/jwtKeyManager.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,IAAI;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,IAAI,EAAE,QAAQ,GAAG,aAAa,GAAG,eAAe,CAAC;IACjD,MAAM,CAAC,EAAE,SAAS,GAAG,QAAQ,GAAG,aAAa,CAAC;IAC9C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,GAAG,CAAC;CACtB;AAED,cAAM,aAAa;IACjB,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,cAAc,CAAuB;IAC7C,OAAO,CAAC,gBAAgB,CAA+B;IACvD,OAAO,CAAC,kBAAkB,CAA+B;;IAQzD,OAAO,CAAC,cAAc;IAMtB,OAAO,CAAC,aAAa;IAerB,OAAO,CAAC,SAAS;IAuBjB,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM;IAqBjC,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAqDvC,SAAS;;;;;;IAUT,OAAO;IAiBP,aAAa;CAId;AAGD,eAAO,MAAM,aAAa,eAAsB,CAAC;AAWjD,eAAe,aAAa,CAAC"}

View File

@@ -0,0 +1,158 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.jwtKeyManager = void 0;
const crypto_1 = __importDefault(require("crypto"));
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
class JWTKeyManager {
constructor() {
this.previousSecret = null;
this.rotationInterval = null;
this.gracePeriodTimeout = null;
console.log('🔑 Initializing JWT Key Manager with automatic rotation');
this.currentSecret = this.generateSecret();
this.startRotation();
}
generateSecret() {
const secret = crypto_1.default.randomBytes(64).toString('hex');
console.log('🔄 Generated new JWT signing key (length:', secret.length, 'chars)');
return secret;
}
startRotation() {
// Rotate every 24 hours (86400000 ms)
this.rotationInterval = setInterval(() => {
this.rotateKey();
}, 24 * 60 * 60 * 1000);
console.log('⏰ JWT key rotation scheduled every 24 hours');
// Also rotate on startup after 1 hour to test the system
setTimeout(() => {
console.log('🧪 Performing initial key rotation test...');
this.rotateKey();
}, 60 * 60 * 1000); // 1 hour
}
rotateKey() {
console.log('🔄 Rotating JWT signing key...');
// Store current secret as previous
this.previousSecret = this.currentSecret;
// Generate new current secret
this.currentSecret = this.generateSecret();
console.log('✅ JWT key rotation completed. Grace period: 24 hours');
// Clear any existing grace period timeout
if (this.gracePeriodTimeout) {
clearTimeout(this.gracePeriodTimeout);
}
// Clean up previous secret after 24 hours (grace period)
this.gracePeriodTimeout = setTimeout(() => {
this.previousSecret = null;
console.log('🧹 Grace period ended. Previous JWT key cleaned up');
}, 24 * 60 * 60 * 1000);
}
generateToken(user) {
const payload = {
id: user.id,
google_id: user.google_id,
email: user.email,
name: user.name,
profile_picture_url: user.profile_picture_url,
role: user.role,
status: user.status,
approval_status: user.approval_status,
onboardingData: user.onboardingData,
iat: Math.floor(Date.now() / 1000) // Issued at time
};
return jsonwebtoken_1.default.sign(payload, this.currentSecret, {
expiresIn: '24h',
issuer: 'vip-coordinator',
audience: 'vip-coordinator-users'
});
}
verifyToken(token) {
try {
// Try current secret first
const decoded = jsonwebtoken_1.default.verify(token, this.currentSecret, {
issuer: 'vip-coordinator',
audience: 'vip-coordinator-users'
});
return {
id: decoded.id,
google_id: decoded.google_id,
email: decoded.email,
name: decoded.name,
profile_picture_url: decoded.profile_picture_url,
role: decoded.role,
status: decoded.status,
approval_status: decoded.approval_status,
onboardingData: decoded.onboardingData
};
}
catch (error) {
// Try previous secret during grace period
if (this.previousSecret) {
try {
const decoded = jsonwebtoken_1.default.verify(token, this.previousSecret, {
issuer: 'vip-coordinator',
audience: 'vip-coordinator-users'
});
console.log('🔄 Token verified using previous key (grace period)');
return {
id: decoded.id,
google_id: decoded.google_id,
email: decoded.email,
name: decoded.name,
profile_picture_url: decoded.profile_picture_url,
role: decoded.role,
status: decoded.status,
approval_status: decoded.approval_status,
onboardingData: decoded.onboardingData
};
}
catch (gracePeriodError) {
console.log('❌ Token verification failed with both current and previous keys');
return null;
}
}
console.log('❌ Token verification failed:', error instanceof Error ? error.message : 'Unknown error');
return null;
}
}
// Get status for monitoring/debugging
getStatus() {
return {
hasCurrentKey: !!this.currentSecret,
hasPreviousKey: !!this.previousSecret,
rotationActive: !!this.rotationInterval,
gracePeriodActive: !!this.gracePeriodTimeout
};
}
// Cleanup on shutdown
destroy() {
console.log('🛑 Shutting down JWT Key Manager...');
if (this.rotationInterval) {
clearInterval(this.rotationInterval);
this.rotationInterval = null;
}
if (this.gracePeriodTimeout) {
clearTimeout(this.gracePeriodTimeout);
this.gracePeriodTimeout = null;
}
console.log('✅ JWT Key Manager shutdown complete');
}
// Manual rotation for testing/emergency
forceRotation() {
console.log('🚨 Manual key rotation triggered');
this.rotateKey();
}
}
// Singleton instance
exports.jwtKeyManager = new JWTKeyManager();
// Graceful shutdown handling
process.on('SIGTERM', () => {
exports.jwtKeyManager.destroy();
});
process.on('SIGINT', () => {
exports.jwtKeyManager.destroy();
});
exports.default = exports.jwtKeyManager;
//# sourceMappingURL=jwtKeyManager.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"jwtKeyManager.js","sourceRoot":"","sources":["../../src/services/jwtKeyManager.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,30 @@
interface ValidationError {
field: string;
message: string;
code: string;
}
interface ScheduleEvent {
title: string;
startTime: string;
endTime: string;
location: string;
type: string;
}
declare class ScheduleValidationService {
validateEvent(event: ScheduleEvent, isEdit?: boolean): ValidationError[];
validateEventSequence(events: ScheduleEvent[]): ValidationError[];
getErrorSummary(errors: ValidationError[]): string;
isCriticalError(error: ValidationError): boolean;
categorizeErrors(errors: ValidationError[]): {
critical: ValidationError[];
warnings: ValidationError[];
};
validateTimeFormat(timeString: string): {
isValid: boolean;
suggestion?: string;
};
}
declare const _default: ScheduleValidationService;
export default _default;
export { ValidationError, ScheduleEvent };
//# sourceMappingURL=scheduleValidationService.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"scheduleValidationService.d.ts","sourceRoot":"","sources":["../../src/services/scheduleValidationService.ts"],"names":[],"mappings":"AAAA,UAAU,eAAe;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,UAAU,aAAa;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,cAAM,yBAAyB;IAG7B,aAAa,CAAC,KAAK,EAAE,aAAa,EAAE,MAAM,GAAE,OAAe,GAAG,eAAe,EAAE;IAuJ/E,qBAAqB,CAAC,MAAM,EAAE,aAAa,EAAE,GAAG,eAAe,EAAE;IA6BjE,eAAe,CAAC,MAAM,EAAE,eAAe,EAAE,GAAG,MAAM;IAalD,eAAe,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO;IAMhD,gBAAgB,CAAC,MAAM,EAAE,eAAe,EAAE,GAAG;QAAE,QAAQ,EAAE,eAAe,EAAE,CAAC;QAAC,QAAQ,EAAE,eAAe,EAAE,CAAA;KAAE;IAgBzG,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE;CAYlF;;AAED,wBAA+C;AAC/C,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,CAAC"}

View File

@@ -0,0 +1,200 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
class ScheduleValidationService {
// Validate a single schedule event
validateEvent(event, isEdit = false) {
const errors = [];
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) {
const errors = [];
// 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) {
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) {
const warningCodes = ['OUTSIDE_BUSINESS_HOURS'];
return !warningCodes.includes(error.code);
}
// Separate critical errors from warnings
categorizeErrors(errors) {
const critical = [];
const warnings = [];
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) {
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 };
}
}
exports.default = new ScheduleValidationService();
//# sourceMappingURL=scheduleValidationService.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,30 @@
declare class UnifiedDataService {
private pool;
constructor();
private toCamelCase;
getVips(): Promise<any>;
getVipById(id: string): Promise<any>;
createVip(vipData: any): Promise<any>;
updateVip(id: string, vipData: any): Promise<any>;
deleteVip(id: string): Promise<any>;
getDrivers(): Promise<any>;
getDriverById(id: string): Promise<any>;
createDriver(driverData: any): Promise<any>;
updateDriver(id: string, driverData: any): Promise<any>;
deleteDriver(id: string): Promise<any>;
getScheduleByVipId(vipId: string): Promise<any>;
createScheduleEvent(vipId: string, eventData: any): Promise<any>;
updateScheduleEvent(id: string, eventData: any): Promise<any>;
deleteScheduleEvent(id: string): Promise<any>;
getAllSchedules(): Promise<Record<string, any[]>>;
getUserByEmail(email: string): Promise<any>;
getUserById(id: string): Promise<any>;
createUser(userData: any): Promise<any>;
updateUserRole(email: string, role: string): Promise<any>;
getUserCount(): Promise<number>;
getAdminSettings(): Promise<any>;
updateAdminSetting(key: string, value: string): Promise<void>;
}
declare const _default: UnifiedDataService;
export default _default;
//# sourceMappingURL=unifiedDataService.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"unifiedDataService.d.ts","sourceRoot":"","sources":["../../src/services/unifiedDataService.ts"],"names":[],"mappings":"AAIA,cAAM,kBAAkB;IACtB,OAAO,CAAC,IAAI,CAAO;;IAOnB,OAAO,CAAC,WAAW;IAab,OAAO;IAwBP,UAAU,CAAC,EAAE,EAAE,MAAM;IAwBrB,SAAS,CAAC,OAAO,EAAE,GAAG;IA2CtB,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG;IAkDlC,SAAS,CAAC,EAAE,EAAE,MAAM;IASpB,UAAU;IAOV,aAAa,CAAC,EAAE,EAAE,MAAM;IAQxB,YAAY,CAAC,UAAU,EAAE,GAAG;IAa5B,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG;IAcxC,YAAY,CAAC,EAAE,EAAE,MAAM;IASvB,kBAAkB,CAAC,KAAK,EAAE,MAAM;IAYhC,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG;IAajD,mBAAmB,CAAC,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG;IAe9C,mBAAmB,CAAC,EAAE,EAAE,MAAM;IAQ9B,eAAe;IAuBf,cAAc,CAAC,KAAK,EAAE,MAAM;IAQ5B,WAAW,CAAC,EAAE,EAAE,MAAM;IAQtB,UAAU,CAAC,QAAQ,EAAE,GAAG;IAaxB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;IAW1C,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC;IAM/B,gBAAgB;IAWhB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;CAQpD;;AAED,wBAAwC"}

View File

@@ -0,0 +1,264 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const database_1 = __importDefault(require("../config/database"));
// Simplified, unified data service that replaces the three redundant services
class UnifiedDataService {
constructor() {
this.pool = database_1.default;
}
// Helper to convert snake_case to camelCase
toCamelCase(obj) {
if (!obj)
return obj;
if (Array.isArray(obj))
return obj.map(item => this.toCamelCase(item));
if (typeof obj !== 'object')
return obj;
return Object.keys(obj).reduce((result, key) => {
const camelKey = key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
result[camelKey] = this.toCamelCase(obj[key]);
return result;
}, {});
}
// VIP Operations
async getVips() {
const query = `
SELECT v.*,
COALESCE(
JSON_AGG(
JSON_BUILD_OBJECT(
'flightNumber', f.flight_number,
'airline', f.airline,
'scheduledArrival', f.scheduled_arrival,
'scheduledDeparture', f.scheduled_departure,
'status', f.status
) ORDER BY f.scheduled_arrival
) FILTER (WHERE f.id IS NOT NULL),
'[]'
) as flights
FROM vips v
LEFT JOIN flights f ON v.id = f.vip_id
GROUP BY v.id
ORDER BY v.created_at DESC`;
const result = await this.pool.query(query);
return this.toCamelCase(result.rows);
}
async getVipById(id) {
const query = `
SELECT v.*,
COALESCE(
JSON_AGG(
JSON_BUILD_OBJECT(
'flightNumber', f.flight_number,
'airline', f.airline,
'scheduledArrival', f.scheduled_arrival,
'scheduledDeparture', f.scheduled_departure,
'status', f.status
) ORDER BY f.scheduled_arrival
) FILTER (WHERE f.id IS NOT NULL),
'[]'
) as flights
FROM vips v
LEFT JOIN flights f ON v.id = f.vip_id
WHERE v.id = $1
GROUP BY v.id`;
const result = await this.pool.query(query, [id]);
return this.toCamelCase(result.rows[0]);
}
async createVip(vipData) {
const { name, organization, department, transportMode, flights, expectedArrival, needsAirportPickup, needsVenueTransport, notes } = vipData;
const client = await this.pool.connect();
try {
await client.query('BEGIN');
// Insert VIP
const vipQuery = `
INSERT INTO vips (name, organization, department, transport_mode, expected_arrival,
needs_airport_pickup, needs_venue_transport, notes)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING *`;
const vipResult = await client.query(vipQuery, [
name, organization, department || 'Office of Development', transportMode || 'flight',
expectedArrival, needsAirportPickup !== false, needsVenueTransport !== false, notes || ''
]);
const vip = vipResult.rows[0];
// Insert flights if any
if (transportMode === 'flight' && flights?.length > 0) {
for (const flight of flights) {
await client.query(`INSERT INTO flights (vip_id, flight_number, airline, scheduled_arrival, scheduled_departure)
VALUES ($1, $2, $3, $4, $5)`, [vip.id, flight.flightNumber, flight.airline, flight.scheduledArrival, flight.scheduledDeparture]);
}
}
await client.query('COMMIT');
return this.getVipById(vip.id);
}
catch (error) {
await client.query('ROLLBACK');
throw error;
}
finally {
client.release();
}
}
async updateVip(id, vipData) {
const { name, organization, department, transportMode, flights, expectedArrival, needsAirportPickup, needsVenueTransport, notes } = vipData;
const client = await this.pool.connect();
try {
await client.query('BEGIN');
// Update VIP
const updateQuery = `
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, updated_at = NOW()
WHERE id = $1
RETURNING *`;
const result = await client.query(updateQuery, [
id, name, organization, department, transportMode,
expectedArrival, needsAirportPickup, needsVenueTransport, notes
]);
if (result.rows.length === 0) {
await client.query('ROLLBACK');
return null;
}
// Update flights
await client.query('DELETE FROM flights WHERE vip_id = $1', [id]);
if (transportMode === 'flight' && flights?.length > 0) {
for (const flight of flights) {
await client.query(`INSERT INTO flights (vip_id, flight_number, airline, scheduled_arrival, scheduled_departure)
VALUES ($1, $2, $3, $4, $5)`, [id, flight.flightNumber, flight.airline, flight.scheduledArrival, flight.scheduledDeparture]);
}
}
await client.query('COMMIT');
return this.getVipById(id);
}
catch (error) {
await client.query('ROLLBACK');
throw error;
}
finally {
client.release();
}
}
async deleteVip(id) {
const result = await this.pool.query('DELETE FROM vips WHERE id = $1 RETURNING *', [id]);
return this.toCamelCase(result.rows[0]);
}
// Driver Operations
async getDrivers() {
const result = await this.pool.query('SELECT * FROM drivers ORDER BY name ASC');
return this.toCamelCase(result.rows);
}
async getDriverById(id) {
const result = await this.pool.query('SELECT * FROM drivers WHERE id = $1', [id]);
return this.toCamelCase(result.rows[0]);
}
async createDriver(driverData) {
const { name, email, phone, vehicleInfo, status } = driverData;
const result = await this.pool.query(`INSERT INTO drivers (name, email, phone, vehicle_info, status)
VALUES ($1, $2, $3, $4, $5)
RETURNING *`, [name, email, phone, vehicleInfo, status || 'available']);
return this.toCamelCase(result.rows[0]);
}
async updateDriver(id, driverData) {
const { name, email, phone, vehicleInfo, status } = driverData;
const result = await this.pool.query(`UPDATE drivers
SET name = $2, email = $3, phone = $4, vehicle_info = $5, status = $6, updated_at = NOW()
WHERE id = $1
RETURNING *`, [id, name, email, phone, vehicleInfo, status]);
return this.toCamelCase(result.rows[0]);
}
async deleteDriver(id) {
const result = await this.pool.query('DELETE FROM drivers WHERE id = $1 RETURNING *', [id]);
return this.toCamelCase(result.rows[0]);
}
// Schedule Operations
async getScheduleByVipId(vipId) {
const result = await this.pool.query(`SELECT se.*, d.name as driver_name
FROM schedule_events se
LEFT JOIN drivers d ON se.driver_id = d.id
WHERE se.vip_id = $1
ORDER BY se.event_time ASC`, [vipId]);
return this.toCamelCase(result.rows);
}
async createScheduleEvent(vipId, eventData) {
const { driverId, eventTime, eventType, location, notes } = eventData;
const result = await this.pool.query(`INSERT INTO schedule_events (vip_id, driver_id, event_time, event_type, location, notes)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING *`, [vipId, driverId, eventTime, eventType, location, notes]);
return this.toCamelCase(result.rows[0]);
}
async updateScheduleEvent(id, eventData) {
const { driverId, eventTime, eventType, location, notes, status } = eventData;
const result = await this.pool.query(`UPDATE schedule_events
SET driver_id = $2, event_time = $3, event_type = $4, location = $5,
notes = $6, status = $7, updated_at = NOW()
WHERE id = $1
RETURNING *`, [id, driverId, eventTime, eventType, location, notes, status]);
return this.toCamelCase(result.rows[0]);
}
async deleteScheduleEvent(id) {
const result = await this.pool.query('DELETE FROM schedule_events WHERE id = $1 RETURNING *', [id]);
return this.toCamelCase(result.rows[0]);
}
async getAllSchedules() {
const result = await this.pool.query(`SELECT se.*, d.name as driver_name, v.name as vip_name
FROM schedule_events se
LEFT JOIN drivers d ON se.driver_id = d.id
LEFT JOIN vips v ON se.vip_id = v.id
ORDER BY se.event_time ASC`);
// Group by VIP ID
const schedules = {};
result.rows.forEach((row) => {
const event = this.toCamelCase(row);
if (!schedules[event.vipId]) {
schedules[event.vipId] = [];
}
schedules[event.vipId].push(event);
});
return schedules;
}
// User Operations (simplified)
async getUserByEmail(email) {
const result = await this.pool.query('SELECT * FROM users WHERE email = $1', [email]);
return this.toCamelCase(result.rows[0]);
}
async getUserById(id) {
const result = await this.pool.query('SELECT * FROM users WHERE id = $1', [id]);
return this.toCamelCase(result.rows[0]);
}
async createUser(userData) {
const { email, name, role, department, googleId } = userData;
const result = await this.pool.query(`INSERT INTO users (email, name, role, department, google_id)
VALUES ($1, $2, $3, $4, $5)
RETURNING *`, [email, name, role || 'coordinator', department || 'Office of Development', googleId]);
return this.toCamelCase(result.rows[0]);
}
async updateUserRole(email, role) {
const result = await this.pool.query(`UPDATE users SET role = $2, updated_at = NOW()
WHERE email = $1
RETURNING *`, [email, role]);
return this.toCamelCase(result.rows[0]);
}
async getUserCount() {
const result = await this.pool.query('SELECT COUNT(*) FROM users');
return parseInt(result.rows[0].count, 10);
}
// Admin Settings (simplified)
async getAdminSettings() {
const result = await this.pool.query('SELECT key, value FROM admin_settings');
return result.rows.reduce((settings, row) => {
settings[row.key] = row.value;
return settings;
}, {});
}
async updateAdminSetting(key, value) {
await this.pool.query(`INSERT INTO admin_settings (key, value)
VALUES ($1, $2)
ON CONFLICT (key) DO UPDATE SET value = $2, updated_at = NOW()`, [key, value]);
}
}
exports.default = new UnifiedDataService();
//# sourceMappingURL=unifiedDataService.js.map

File diff suppressed because one or more lines are too long