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>
264 lines
9.2 KiB
JavaScript
264 lines
9.2 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 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
|