const mysql = require('mysql2/promise'); // Database connection configurations const tilbudgivernConfig = { host: 'localhost', user: 'analytics_user', password: process.env.DB_PASSWORD, database: 'tilbudgivern' }; const ordrestyringConfig = { host: 'localhost', user: 'analytics_user', password: process.env.DB_PASSWORD, database: 'ordrestyring_local' }; class SimpleAnalyticsPopulator { constructor() { this.tilbudgivernConn = null; this.ordrestyringConn = null; } async connect() { try { this.tilbudgivernConn = await mysql.createConnection(tilbudgivernConfig); this.ordrestyringConn = await mysql.createConnection(ordrestyringConfig); console.log('āœ“ Connected to both databases'); } catch (error) { console.error('Database connection failed:', error.message); process.exit(1); } } async disconnect() { if (this.tilbudgivernConn) await this.tilbudgivernConn.end(); if (this.ordrestyringConn) await this.ordrestyringConn.end(); console.log('āœ“ Database connections closed'); } // ====================================== // 1. POPULATE PROJECT ANALYTICS (Simplified) // ====================================== async populateProjectAnalytics() { console.log('\nšŸ”„ Populating Project Analytics (using hours-based estimates)...'); const query = ` INSERT INTO project_analytics ( project_id, project_name, customer_id, customer_name, employee_id, employee_name, total_hours, total_labor_cost, total_quote_value, actual_cost, profit_amount, profit_margin, quote_accuracy_percentage, project_start_date, quote_created_date, is_active ) SELECT DISTINCT oc.id as project_id, COALESCE(LEFT(oc.description, 450), CONCAT('Case ', oc.case_number)) as project_name, oc.customer_number as customer_id, COALESCE(od.customer_name, CONCAT('Customer ', oc.customer_number)) as customer_name, -- Most active employee for this case COALESCE(( SELECT oh.employee_id FROM ordrestyring_local.ordrestyring_hours oh WHERE oh.case_number = oc.case_number GROUP BY oh.employee_id ORDER BY SUM(oh.duration_hours) DESC LIMIT 1 ), 1) as employee_id, CONCAT('Employee ', COALESCE(( SELECT oh.employee_id FROM ordrestyring_local.ordrestyring_hours oh WHERE oh.case_number = oc.case_number GROUP BY oh.employee_id ORDER BY SUM(oh.duration_hours) DESC LIMIT 1 ), 1)) as employee_name, -- Calculate total hours COALESCE(( SELECT SUM(oh.duration_hours) FROM ordrestyring_local.ordrestyring_hours oh WHERE oh.case_number = oc.case_number ), 0) as total_hours, -- Estimated labor cost (hours * 650 DKK/hour) COALESCE(( SELECT SUM(oh.duration_hours) * 650 FROM ordrestyring_local.ordrestyring_hours oh WHERE oh.case_number = oc.case_number ), 0) as total_labor_cost, -- Estimated quote value (labor + 50% markup for materials/profit) COALESCE(( SELECT SUM(oh.duration_hours) * 650 * 2.5 FROM ordrestyring_local.ordrestyring_hours oh WHERE oh.case_number = oc.case_number ), 0) as total_quote_value, -- Actual cost (labor + 40% materials) COALESCE(( SELECT SUM(oh.duration_hours) * 650 * 1.4 FROM ordrestyring_local.ordrestyring_hours oh WHERE oh.case_number = oc.case_number ), 0) as actual_cost, -- Profit amount (quote - actual) GREATEST(0, COALESCE(( SELECT SUM(oh.duration_hours) * 650 * 2.5 FROM ordrestyring_local.ordrestyring_hours oh WHERE oh.case_number = oc.case_number ), 0) - COALESCE(( SELECT SUM(oh.duration_hours) * 650 * 1.4 FROM ordrestyring_local.ordrestyring_hours oh WHERE oh.case_number = oc.case_number ), 0)) as profit_amount, -- Profit margin (44% = (2.5 - 1.4) / 2.5 * 100) 44.0 as profit_margin, -- Quote accuracy (assume 85% average) 85.0 as quote_accuracy_percentage, -- Project dates CASE WHEN oc.start_date > 0 THEN FROM_UNIXTIME(oc.start_date/1000) WHEN oc.created_at > 0 THEN FROM_UNIXTIME(oc.created_at/1000) ELSE CURRENT_DATE - INTERVAL 30 DAY END as project_start_date, CASE WHEN oc.created_at > 0 THEN FROM_UNIXTIME(oc.created_at/1000) ELSE CURRENT_DATE - INTERVAL 30 DAY END as quote_created_date, CASE WHEN oc.status_id = 1 THEN 1 ELSE 0 END as is_active FROM ordrestyring_local.ordrestyring_cases oc LEFT JOIN ordrestyring_local.ordrestyring_debtors od ON oc.customer_number = od.customer_number WHERE oc.id IS NOT NULL AND EXISTS ( SELECT 1 FROM ordrestyring_local.ordrestyring_hours oh WHERE oh.case_number = oc.case_number AND oh.duration_hours > 0 ) ON DUPLICATE KEY UPDATE total_hours = VALUES(total_hours), total_labor_cost = VALUES(total_labor_cost), total_quote_value = VALUES(total_quote_value), profit_amount = VALUES(profit_amount), last_calculated = CURRENT_TIMESTAMP `; try { const [result] = await this.tilbudgivernConn.execute(query); console.log(`āœ“ Project Analytics: ${result.affectedRows} records processed`); return result.affectedRows; } catch (error) { console.error('Error populating project analytics:', error.message); throw error; } } // ====================================== // 2. POPULATE EMPLOYEE ANALYTICS // ====================================== async populateEmployeeAnalytics() { console.log('\nšŸ”„ Populating Employee Analytics...'); const query = ` INSERT INTO employee_performance_analytics ( employee_id, employee_name, period_month, period_year, total_projects, total_revenue, total_profit, avg_profit_margin, total_hours_worked, revenue_per_hour ) SELECT pa.employee_id, pa.employee_name, MONTH(pa.project_start_date) as period_month, YEAR(pa.project_start_date) as period_year, COUNT(DISTINCT pa.project_id) as total_projects, SUM(pa.total_quote_value) as total_revenue, SUM(pa.profit_amount) as total_profit, AVG(pa.profit_margin) as avg_profit_margin, SUM(pa.total_hours) as total_hours_worked, CASE WHEN SUM(pa.total_hours) > 0 THEN SUM(pa.total_quote_value) / SUM(pa.total_hours) ELSE 0 END as revenue_per_hour FROM project_analytics pa WHERE pa.employee_id IS NOT NULL AND pa.project_start_date >= DATE_SUB(CURRENT_DATE, INTERVAL 24 MONTH) GROUP BY pa.employee_id, pa.employee_name, YEAR(pa.project_start_date), MONTH(pa.project_start_date) HAVING total_projects > 0 ON DUPLICATE KEY UPDATE total_projects = VALUES(total_projects), total_revenue = VALUES(total_revenue), total_profit = VALUES(total_profit), avg_profit_margin = VALUES(avg_profit_margin), revenue_per_hour = VALUES(revenue_per_hour), last_calculated = CURRENT_TIMESTAMP `; try { const [result] = await this.tilbudgivernConn.execute(query); console.log(`āœ“ Employee Analytics: ${result.affectedRows} records processed`); return result.affectedRows; } catch (error) { console.error('Error populating employee analytics:', error.message); return 0; } } // ====================================== // 3. POPULATE CUSTOMER ANALYTICS // ====================================== async populateCustomerAnalytics() { console.log('\nšŸ”„ Populating Customer Analytics...'); const query = ` INSERT INTO customer_analytics ( customer_id, customer_name, total_projects, total_revenue, avg_project_value, total_profit, avg_profit_margin, first_project_date, last_project_date, customer_tier, is_high_value ) SELECT pa.customer_id, pa.customer_name, COUNT(*) as total_projects, SUM(pa.total_quote_value) as total_revenue, AVG(pa.total_quote_value) as avg_project_value, SUM(pa.profit_amount) as total_profit, AVG(pa.profit_margin) as avg_profit_margin, MIN(pa.project_start_date) as first_project_date, MAX(pa.project_start_date) as last_project_date, CASE WHEN SUM(pa.total_quote_value) >= 500000 THEN 'Platinum' WHEN SUM(pa.total_quote_value) >= 200000 THEN 'Gold' WHEN SUM(pa.total_quote_value) >= 50000 THEN 'Silver' ELSE 'Bronze' END as customer_tier, CASE WHEN SUM(pa.total_quote_value) >= 100000 THEN 1 ELSE 0 END as is_high_value FROM project_analytics pa WHERE pa.customer_id IS NOT NULL GROUP BY pa.customer_id, pa.customer_name ON DUPLICATE KEY UPDATE total_projects = VALUES(total_projects), total_revenue = VALUES(total_revenue), avg_project_value = VALUES(avg_project_value), total_profit = VALUES(total_profit), customer_tier = VALUES(customer_tier), is_high_value = VALUES(is_high_value), last_calculated = CURRENT_TIMESTAMP `; try { const [result] = await this.tilbudgivernConn.execute(query); console.log(`āœ“ Customer Analytics: ${result.affectedRows} records processed`); return result.affectedRows; } catch (error) { console.error('Error populating customer analytics:', error.message); return 0; } } // ====================================== // 4. POPULATE DAILY METRICS // ====================================== async populateDailyMetrics() { console.log('\nšŸ”„ Populating Daily Business Metrics...'); const query = ` INSERT INTO daily_business_metrics ( metric_date, new_projects, total_daily_revenue, total_daily_profit, avg_daily_profit_margin, active_projects ) SELECT DATE(pa.project_start_date) as metric_date, COUNT(*) as new_projects, SUM(pa.total_quote_value) as total_daily_revenue, SUM(pa.profit_amount) as total_daily_profit, AVG(pa.profit_margin) as avg_daily_profit_margin, (SELECT COUNT(*) FROM project_analytics WHERE is_active = 1) as active_projects FROM project_analytics pa WHERE pa.project_start_date >= DATE_SUB(CURRENT_DATE, INTERVAL 90 DAY) GROUP BY DATE(pa.project_start_date) ON DUPLICATE KEY UPDATE new_projects = VALUES(new_projects), total_daily_revenue = VALUES(total_daily_revenue), total_daily_profit = VALUES(total_daily_profit), avg_daily_profit_margin = VALUES(avg_daily_profit_margin), last_calculated = CURRENT_TIMESTAMP `; try { const [result] = await this.tilbudgivernConn.execute(query); console.log(`āœ“ Daily Metrics: ${result.affectedRows} records processed`); return result.affectedRows; } catch (error) { console.error('Error populating daily metrics:', error.message); return 0; } } // ====================================== // 5. UPDATE BENCHMARKS // ====================================== async updateBenchmarks() { console.log('\nšŸ”„ Updating Business Benchmarks...'); const benchmarkUpdates = [ { name: 'avg_profit_margin', query: 'SELECT AVG(profit_margin) as value FROM project_analytics WHERE profit_margin > 0' }, { name: 'employee_productivity', query: 'SELECT AVG(revenue_per_hour) as value FROM employee_performance_analytics WHERE revenue_per_hour > 0' } ]; for (const benchmark of benchmarkUpdates) { try { const [rows] = await this.tilbudgivernConn.execute(benchmark.query); const currentValue = rows[0]?.value || 0; await this.tilbudgivernConn.execute(` UPDATE business_benchmarks SET current_value = ?, variance_from_target = current_value - target_value, performance_score = CASE WHEN target_value > 0 THEN (current_value / target_value) * 100 ELSE 100 END, last_updated = CURRENT_TIMESTAMP WHERE metric_name = ? `, [currentValue, benchmark.name]); console.log(` āœ“ ${benchmark.name}: ${currentValue.toFixed(2)}`); } catch (error) { console.error(` ! Error updating ${benchmark.name}:`, error.message); } } } // ====================================== // 6. MAIN POPULATION PROCESS // ====================================== async populateAllAnalytics() { const startTime = Date.now(); let totalRecords = 0; console.log('šŸš€ Starting Simplified Analytics Population...\n'); try { await this.connect(); // Show source data stats first console.log('šŸ“Š SOURCE DATA:'); const [caseCount] = await this.ordrestyringConn.execute('SELECT COUNT(*) as count FROM ordrestyring_cases'); const [hourStats] = await this.ordrestyringConn.execute('SELECT COUNT(*) as entries, SUM(duration_hours) as total_hours FROM ordrestyring_hours'); const [customerCount] = await this.ordrestyringConn.execute('SELECT COUNT(*) as count FROM ordrestyring_debtors'); console.log(` Cases: ${caseCount[0].count}`); console.log(` Hour Entries: ${hourStats[0].entries} (${Math.round(hourStats[0].total_hours)} hours)`); console.log(` Customers: ${customerCount[0].count}\n`); // 1. Project Analytics (foundation) totalRecords += await this.populateProjectAnalytics(); // 2. Employee Analytics totalRecords += await this.populateEmployeeAnalytics(); // 3. Customer Analytics totalRecords += await this.populateCustomerAnalytics(); // 4. Daily Metrics totalRecords += await this.populateDailyMetrics(); // 5. Update Benchmarks await this.updateBenchmarks(); const endTime = Date.now(); console.log('\nāœ… Analytics Population Complete!'); console.log(`šŸ“Š Total Records Processed: ${totalRecords}`); console.log(`ā±ļø Duration: ${Math.round((endTime - startTime) / 1000)}s`); // Show summary stats await this.showSummaryStats(); } catch (error) { console.error('\nāŒ Analytics Population Failed:', error.message); throw error; } finally { await this.disconnect(); } } async showSummaryStats() { console.log('\nšŸ“ˆ ANALYTICS SUMMARY:'); try { // Project stats const [projectStats] = await this.tilbudgivernConn.execute(` SELECT COUNT(*) as total_projects, SUM(total_quote_value) as total_revenue, AVG(profit_margin) as avg_profit_margin, SUM(total_hours) as total_hours FROM project_analytics `); const stats = projectStats[0]; console.log(` Projects: ${stats.total_projects}`); console.log(` Revenue: ${(stats.total_revenue / 1000000).toFixed(1)}M DKK`); console.log(` Avg Profit: ${stats.avg_profit_margin?.toFixed(1)}%`); console.log(` Total Hours: ${Math.round(stats.total_hours)}`); // Employee & Customer counts const [employeeCount] = await this.tilbudgivernConn.execute('SELECT COUNT(DISTINCT employee_id) as count FROM employee_performance_analytics'); const [customerCount] = await this.tilbudgivernConn.execute('SELECT COUNT(DISTINCT customer_id) as count FROM customer_analytics'); console.log(` Active Employees: ${employeeCount[0].count}`); console.log(` Customers: ${customerCount[0].count}`); } catch (error) { console.log(' (Summary stats unavailable)'); } } } // Main execution async function main() { const populator = new SimpleAnalyticsPopulator(); try { await populator.populateAllAnalytics(); process.exit(0); } catch (error) { console.error('Population failed:', error.message); process.exit(1); } } // Run if called directly if (require.main === module) { main(); } module.exports = SimpleAnalyticsPopulator;