282 lines
11 KiB
JavaScript
282 lines
11 KiB
JavaScript
const CaseAnalyticsService = require('./case-analytics-service');
|
|
const cron = require('node-cron');
|
|
const fs = require('fs').promises;
|
|
const path = require('path');
|
|
|
|
class AnalyticsAutoUpdateService {
|
|
constructor() {
|
|
this.analyticsService = new CaseAnalyticsService();
|
|
this.cacheDir = path.join(__dirname, 'analytics-cache');
|
|
this.isRunning = false;
|
|
|
|
// Ensure cache directory exists
|
|
this.initializeCacheDirectory();
|
|
}
|
|
|
|
async initializeCacheDirectory() {
|
|
try {
|
|
await fs.mkdir(this.cacheDir, { recursive: true });
|
|
console.log(`📁 Analytics cache directory ready: ${this.cacheDir}`);
|
|
} catch (error) {
|
|
console.error('Error creating cache directory:', error);
|
|
}
|
|
}
|
|
|
|
// Cache analytics data to files for fast API responses
|
|
async cacheAnalyticsData(data, filename) {
|
|
try {
|
|
const filePath = path.join(this.cacheDir, `${filename}.json`);
|
|
const cacheData = {
|
|
data: data,
|
|
cached_at: new Date().toISOString(),
|
|
expires_at: new Date(Date.now() + 30 * 60 * 1000).toISOString() // 30 minutes
|
|
};
|
|
|
|
await fs.writeFile(filePath, JSON.stringify(cacheData, null, 2));
|
|
console.log(`💾 Cached ${filename} data (${JSON.stringify(cacheData).length} bytes)`);
|
|
|
|
} catch (error) {
|
|
console.error(`Error caching ${filename}:`, error);
|
|
}
|
|
}
|
|
|
|
// Load cached analytics data
|
|
async loadCachedData(filename) {
|
|
try {
|
|
const filePath = path.join(this.cacheDir, `${filename}.json`);
|
|
const fileContent = await fs.readFile(filePath, 'utf8');
|
|
const cacheData = JSON.parse(fileContent);
|
|
|
|
// Check if cache is still valid
|
|
const expiresAt = new Date(cacheData.expires_at);
|
|
if (new Date() > expiresAt) {
|
|
return null; // Cache expired
|
|
}
|
|
|
|
return cacheData.data;
|
|
} catch (error) {
|
|
return null; // File doesn't exist or error reading
|
|
}
|
|
}
|
|
|
|
// Generate and cache comprehensive analytics
|
|
async updateAnalyticsCache() {
|
|
if (this.isRunning) {
|
|
console.log('⏳ Analytics update already running, skipping...');
|
|
return;
|
|
}
|
|
|
|
this.isRunning = true;
|
|
console.log('🔄 Starting analytics cache update...');
|
|
|
|
try {
|
|
const startTime = Date.now();
|
|
|
|
// Generate all analytics data
|
|
console.log('📊 Generating KPIs...');
|
|
const kpis = await this.analyticsService.getOverallKPIs();
|
|
await this.cacheAnalyticsData(kpis, 'kpis');
|
|
|
|
console.log('👥 Generating employee metrics...');
|
|
const employees = await this.analyticsService.getEmployeePerformanceMetrics();
|
|
await this.cacheAnalyticsData(employees, 'employees');
|
|
|
|
console.log('🏢 Generating customer analytics...');
|
|
const customers = await this.analyticsService.getCustomerAnalytics();
|
|
await this.cacheAnalyticsData(customers, 'customers');
|
|
|
|
console.log('🔨 Generating material insights...');
|
|
const materials = await this.analyticsService.getMaterialInsights();
|
|
await this.cacheAnalyticsData(materials, 'materials');
|
|
|
|
console.log('📈 Generating trend data...');
|
|
const trends = await this.analyticsService.getTrendAnalytics(12);
|
|
await this.cacheAnalyticsData(trends, 'trends');
|
|
|
|
console.log('📋 Generating project analytics...');
|
|
const projects = await this.analyticsService.generateProjectAnalytics();
|
|
await this.cacheAnalyticsData(projects, 'projects');
|
|
|
|
// Generate comprehensive report
|
|
console.log('📊 Generating full analytics report...');
|
|
const fullReport = await this.analyticsService.generateFullAnalyticsReport();
|
|
await this.cacheAnalyticsData(fullReport, 'full-report');
|
|
|
|
// Cache summary for quick access
|
|
const summary = {
|
|
last_updated: new Date().toISOString(),
|
|
total_projects: kpis.total_projects,
|
|
total_value: kpis.total_project_value,
|
|
profit_margin: kpis.overall_profit_margin,
|
|
active_employees: kpis.active_employees,
|
|
top_employee: employees[0]?.full_name || 'N/A',
|
|
top_customer: customers[0]?.customer_number || 'N/A',
|
|
update_duration_ms: Date.now() - startTime
|
|
};
|
|
|
|
await this.cacheAnalyticsData(summary, 'summary');
|
|
|
|
console.log(`✅ Analytics cache update completed in ${Date.now() - startTime}ms`);
|
|
console.log(`📈 Summary: ${kpis.total_projects} projects, ${kpis.total_project_value?.toLocaleString()} kr total value`);
|
|
|
|
} catch (error) {
|
|
console.error('❌ Error updating analytics cache:', error);
|
|
} finally {
|
|
this.isRunning = false;
|
|
}
|
|
}
|
|
|
|
// Update quote calculator benchmarks based on fresh analytics
|
|
async updateQuoteCalculatorBenchmarks() {
|
|
try {
|
|
console.log('🎯 Updating quote calculator benchmarks...');
|
|
|
|
const projects = await this.analyticsService.generateProjectAnalytics();
|
|
|
|
// Calculate size-based benchmarks
|
|
const sizeRanges = {
|
|
small: projects.filter(p => p.actual_hours <= 200),
|
|
medium: projects.filter(p => p.actual_hours > 200 && p.actual_hours <= 800),
|
|
large: projects.filter(p => p.actual_hours > 800 && p.actual_hours <= 1500),
|
|
xlarge: projects.filter(p => p.actual_hours > 1500)
|
|
};
|
|
|
|
const benchmarks = {};
|
|
|
|
Object.keys(sizeRanges).forEach(size => {
|
|
const sizeProjects = sizeRanges[size];
|
|
if (sizeProjects.length > 0) {
|
|
const avgHours = sizeProjects.reduce((sum, p) => sum + p.actual_hours, 0) / sizeProjects.length;
|
|
const avgValue = sizeProjects.reduce((sum, p) => sum + p.total_project_value, 0) / sizeProjects.length;
|
|
const avgHourlyRate = avgValue / avgHours;
|
|
|
|
benchmarks[size] = {
|
|
sample_size: sizeProjects.length,
|
|
avg_hours: Math.round(avgHours * 10) / 10,
|
|
avg_value: Math.round(avgValue),
|
|
avg_hourly_rate: Math.round(avgHourlyRate * 10) / 10,
|
|
material_ratio: avgValue > 0 ?
|
|
(sizeProjects.reduce((sum, p) => sum + (p.material_sales || 0), 0) / (avgValue * sizeProjects.length) * 100) : 0
|
|
};
|
|
}
|
|
});
|
|
|
|
await this.cacheAnalyticsData(benchmarks, 'quote-benchmarks');
|
|
console.log('✅ Quote calculator benchmarks updated');
|
|
|
|
} catch (error) {
|
|
console.error('❌ Error updating quote benchmarks:', error);
|
|
}
|
|
}
|
|
|
|
// Check for new/updated cases and trigger analytics update
|
|
async checkForUpdatesAndRefresh() {
|
|
// This would integrate with the Ordrestyring sync process
|
|
// For now, we'll just run analytics update on schedule
|
|
console.log('🔍 Checking for data updates...');
|
|
await this.updateAnalyticsCache();
|
|
await this.updateQuoteCalculatorBenchmarks();
|
|
}
|
|
|
|
// Start scheduled analytics updates
|
|
startScheduledUpdates() {
|
|
console.log('🚀 Starting analytics auto-update service...');
|
|
|
|
// Initial update
|
|
this.updateAnalyticsCache();
|
|
|
|
// Update every 30 minutes
|
|
cron.schedule('*/30 * * * *', async () => {
|
|
console.log('⏰ Scheduled analytics update triggered');
|
|
await this.checkForUpdatesAndRefresh();
|
|
});
|
|
|
|
// Full deep analytics every 4 hours
|
|
cron.schedule('0 */4 * * *', async () => {
|
|
console.log('⏰ Deep analytics update triggered');
|
|
await this.updateAnalyticsCache();
|
|
await this.updateQuoteCalculatorBenchmarks();
|
|
});
|
|
|
|
// Daily comprehensive report generation
|
|
cron.schedule('0 6 * * *', async () => {
|
|
console.log('📊 Daily comprehensive analytics report generation');
|
|
const report = await this.analyticsService.generateFullAnalyticsReport();
|
|
await this.cacheAnalyticsData(report, `daily-report-${new Date().toISOString().split('T')[0]}`);
|
|
});
|
|
|
|
console.log('✅ Analytics auto-update service started');
|
|
console.log('📅 Schedule:');
|
|
console.log(' - Every 30 minutes: Analytics cache refresh');
|
|
console.log(' - Every 4 hours: Deep analytics + benchmarks update');
|
|
console.log(' - Daily at 06:00: Comprehensive report generation');
|
|
}
|
|
|
|
// Stop scheduled updates
|
|
stopScheduledUpdates() {
|
|
// This would stop all cron jobs if needed
|
|
console.log('🛑 Analytics auto-update service stopped');
|
|
}
|
|
|
|
// Enhanced API method to serve cached data or generate fresh
|
|
async getAnalyticsData(type, maxAge = 30 * 60 * 1000) { // 30 minutes default
|
|
try {
|
|
// Try to load from cache first
|
|
const cachedData = await this.loadCachedData(type);
|
|
|
|
if (cachedData) {
|
|
console.log(`📱 Serving cached ${type} data`);
|
|
return { success: true, data: cachedData, source: 'cache' };
|
|
}
|
|
|
|
// If no cache, generate fresh data
|
|
console.log(`🔄 Generating fresh ${type} data`);
|
|
let freshData;
|
|
|
|
switch (type) {
|
|
case 'kpis':
|
|
freshData = await this.analyticsService.getOverallKPIs();
|
|
break;
|
|
case 'employees':
|
|
freshData = await this.analyticsService.getEmployeePerformanceMetrics();
|
|
break;
|
|
case 'customers':
|
|
freshData = await this.analyticsService.getCustomerAnalytics();
|
|
break;
|
|
case 'materials':
|
|
freshData = await this.analyticsService.getMaterialInsights();
|
|
break;
|
|
case 'trends':
|
|
freshData = await this.analyticsService.getTrendAnalytics();
|
|
break;
|
|
case 'projects':
|
|
freshData = await this.analyticsService.generateProjectAnalytics();
|
|
break;
|
|
default:
|
|
throw new Error(`Unknown analytics type: ${type}`);
|
|
}
|
|
|
|
// Cache the fresh data
|
|
await this.cacheAnalyticsData(freshData, type);
|
|
|
|
return { success: true, data: freshData, source: 'fresh' };
|
|
|
|
} catch (error) {
|
|
console.error(`Error getting ${type} analytics:`, error);
|
|
return { success: false, error: error.message };
|
|
}
|
|
}
|
|
|
|
// Get service status and statistics
|
|
getServiceStatus() {
|
|
return {
|
|
service: 'Analytics Auto-Update Service',
|
|
status: 'running',
|
|
cache_directory: this.cacheDir,
|
|
update_in_progress: this.isRunning,
|
|
last_check: new Date().toISOString()
|
|
};
|
|
}
|
|
}
|
|
|
|
module.exports = AnalyticsAutoUpdateService; |