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>
571 lines
21 KiB
JavaScript
571 lines
21 KiB
JavaScript
"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
|