const mysql = require('mysql2/promise'); class CaseAnalyticsService { constructor() { this.dbConfig = { host: 'localhost', user: 'analytics_user', password: process.env.DB_PASSWORD, database: 'tilbudgivern' }; } async getConnection() { return await mysql.createConnection(this.dbConfig); } // ==================== // CORE ANALYTICS METHODS // ==================== async getSingleProjectAnalytics(projectId) { const connection = await this.getConnection(); try { // Get detailed project analytics const [project] = await connection.execute(` SELECT pa.*, CASE WHEN pa.is_active = 1 THEN 'Active' ELSE 'Completed' END as status_text FROM project_analytics pa WHERE pa.project_id = ? `, [projectId]); if (project.length === 0) { throw new Error(`Project ${projectId} not found`); } return { project: project[0], analysis: { profitability: project[0].profit_margin > 35 ? 'Excellent' : project[0].profit_margin > 25 ? 'Good' : project[0].profit_margin > 15 ? 'Fair' : 'Poor', efficiency: project[0].total_hours > 0 ? project[0].total_quote_value / project[0].total_hours : 0, accuracy: project[0].quote_accuracy_percentage > 90 ? 'Excellent' : project[0].quote_accuracy_percentage > 80 ? 'Good' : project[0].quote_accuracy_percentage > 70 ? 'Fair' : 'Poor' }, recommendations: this.generateProjectRecommendations(project[0]) }; } finally { await connection.end(); } } generateProjectRecommendations(project) { const recommendations = []; if (project.profit_margin < 25) { recommendations.push('Consider increasing markup or reducing material costs'); } if (project.quote_accuracy_percentage < 85) { recommendations.push('Review estimation process to improve accuracy'); } if (project.total_hours > 0 && (project.total_quote_value / project.total_hours) < 1500) { recommendations.push('Focus on higher value activities to improve hourly rate'); } return recommendations; } async generateProjectAnalytics(projectId = null) { const connection = await this.getConnection(); try { let whereClause = projectId ? `WHERE pa.project_id = ${projectId}` : 'LIMIT 100'; const [projectMetrics] = await connection.execute(` SELECT pa.project_id, pa.project_name, oc.customer_number, oc.total_amount as quoted_amount, oc.start_date, oc.end_date, -- Timer metrics (SELECT SUM(duration_hours) FROM ordrestyring_hours WHERE case_number = oc.case_number) as actual_hours, (SELECT COUNT(*) FROM ordrestyring_hours WHERE case_number = oc.case_number) as hour_entries, (SELECT COUNT(DISTINCT employee_id) FROM ordrestyring_hours WHERE case_number = oc.case_number) as employees_count, (SELECT AVG(duration_hours) FROM ordrestyring_hours WHERE case_number = oc.case_number) as avg_daily_hours, -- Material metrics (SELECT SUM(total_cost) FROM ordrestyring_case_materials WHERE case_number = oc.case_number) as material_cost, (SELECT SUM(total_sales) FROM ordrestyring_case_materials WHERE case_number = oc.case_number) as material_sales, (SELECT COUNT(*) FROM ordrestyring_case_materials WHERE case_number = oc.case_number) as material_entries, (SELECT COUNT(DISTINCT supplier) FROM ordrestyring_case_materials WHERE case_number = oc.case_number) as suppliers_count, -- Calculated KPIs CASE WHEN (SELECT SUM(duration_hours) FROM ordrestyring_hours WHERE case_number = oc.case_number) > 0 THEN (SELECT SUM(duration_hours) FROM ordrestyring_hours WHERE case_number = oc.case_number) * 450 ELSE 0 END as estimated_labor_cost, CASE WHEN (SELECT SUM(total_cost) FROM ordrestyring_case_materials WHERE case_number = oc.case_number) > 0 AND (SELECT SUM(total_sales) FROM ordrestyring_case_materials WHERE case_number = oc.case_number) > 0 THEN ((SELECT SUM(total_sales) FROM ordrestyring_case_materials WHERE case_number = oc.case_number) - (SELECT SUM(total_cost) FROM ordrestyring_case_materials WHERE case_number = oc.case_number)) / (SELECT SUM(total_cost) FROM ordrestyring_case_materials WHERE case_number = oc.case_number) * 100 ELSE 0 END as material_margin_percent, -- Date calculations CASE WHEN oc.start_date IS NOT NULL AND oc.end_date IS NOT NULL THEN DATEDIFF(FROM_UNIXTIME(oc.end_date), FROM_UNIXTIME(oc.start_date)) ELSE NULL END as project_duration_days, DATE(FROM_UNIXTIME(oc.start_date)) as start_date_formatted, DATE(FROM_UNIXTIME(oc.end_date)) as end_date_formatted FROM ordrestyring_cases oc ${whereClause} HAVING actual_hours > 0 OR material_cost > 0 ORDER BY oc.case_number DESC `); // Calculate additional derived metrics const enrichedMetrics = projectMetrics.map(project => { const laborCost = Number(project.estimated_labor_cost) || 0; const materialSales = Number(project.material_sales) || 0; const materialCost = Number(project.material_cost) || 0; const totalProjectValue = laborCost + materialSales; const materialProfit = materialSales - materialCost; const totalProfit = laborCost * 0.2 + materialProfit; // Assume 20% labor margin const profitMargin = totalProjectValue > 0 ? (totalProfit / totalProjectValue) * 100 : 0; return { ...project, total_project_value: Math.round(totalProjectValue), material_profit: Math.round(materialProfit), estimated_total_profit: Math.round(totalProfit), overall_profit_margin: Math.round(profitMargin * 10) / 10, hours_per_day: project.project_duration_days > 0 ? Math.round((project.actual_hours / project.project_duration_days) * 10) / 10 : null, hourly_rate_achieved: project.actual_hours > 0 ? Math.round((totalProjectValue / project.actual_hours) * 10) / 10 : null }; }); return enrichedMetrics; } finally { await connection.end(); } } async getOverallKPIs() { const connection = await this.getConnection(); try { // Get KPIs from analytics tables const [totals] = await connection.execute(` SELECT COUNT(*) as totalProjects, COUNT(DISTINCT customer_id) as activeCustomers, SUM(total_quote_value) as totalRevenue, COUNT(DISTINCT employee_id) as activeEmployees, AVG(profit_margin) as avgProfitMargin, SUM(total_hours) as totalHours FROM project_analytics WHERE project_start_date >= DATE_SUB(CURRENT_DATE, INTERVAL 12 MONTH) `); const kpis = totals[0]; return { totalProjects: kpis.totalProjects || 0, totalRevenue: kpis.totalRevenue || 0, activeCustomers: kpis.activeCustomers || 0, activeEmployees: kpis.activeEmployees || 0, avgProfitMargin: kpis.avgProfitMargin || 0, totalHours: kpis.totalHours || 0, avgProjectValue: kpis.totalProjects > 0 ? (kpis.totalRevenue / kpis.totalProjects) : 0, lastUpdated: new Date().toISOString() }; } finally { await connection.end(); } } async getEmployeePerformanceMetrics() { const connection = await this.getConnection(); try { const [performance] = await connection.execute(` SELECT oh.employee_id, ou.full_name, COUNT(DISTINCT oh.case_number) as projects_worked, COUNT(*) as total_entries, SUM(oh.duration_hours) as total_hours, AVG(oh.duration_hours) as avg_daily_hours, MIN(DATE(FROM_UNIXTIME(oh.start_time))) as first_project_date, MAX(DATE(FROM_UNIXTIME(oh.start_time))) as last_project_date, -- Productivity metrics SUM(oh.duration_hours) * 450 as total_labor_value, DATEDIFF(MAX(FROM_UNIXTIME(oh.start_time)), MIN(FROM_UNIXTIME(oh.start_time))) + 1 as active_days, COUNT(DISTINCT DATE(FROM_UNIXTIME(oh.start_time))) as working_days FROM ordrestyring_hours oh LEFT JOIN ordrestyring_users ou ON oh.employee_id = ou.id WHERE oh.duration_hours > 0 GROUP BY oh.employee_id, ou.full_name HAVING total_hours > 10 ORDER BY total_hours DESC `); return performance.map(emp => ({ ...emp, avg_hours_per_working_day: emp.working_days > 0 ? Math.round((emp.total_hours / emp.working_days) * 10) / 10 : 0, utilization_rate: emp.active_days > 0 ? Math.round((emp.working_days / emp.active_days) * 1000) / 10 : 0, value_per_hour: Math.round((emp.total_labor_value / emp.total_hours) * 10) / 10 })); } finally { await connection.end(); } } async getCustomerAnalytics() { const connection = await this.getConnection(); try { const [customers] = await connection.execute(` SELECT oc.customer_number, COUNT(DISTINCT oc.case_number) as total_projects, SUM(COALESCE((SELECT SUM(duration_hours) FROM ordrestyring_hours WHERE case_number = oc.case_number), 0)) as total_hours, SUM(COALESCE((SELECT SUM(total_cost) FROM ordrestyring_case_materials WHERE case_number = oc.case_number), 0)) as total_material_costs, SUM(COALESCE((SELECT SUM(total_sales) FROM ordrestyring_case_materials WHERE case_number = oc.case_number), 0)) as total_material_sales, MIN(DATE(FROM_UNIXTIME(oc.start_date))) as first_project, MAX(DATE(FROM_UNIXTIME(oc.end_date))) as last_project, AVG(COALESCE((SELECT SUM(duration_hours) FROM ordrestyring_hours WHERE case_number = oc.case_number), 0)) as avg_project_hours, AVG(COALESCE((SELECT SUM(total_sales) FROM ordrestyring_case_materials WHERE case_number = oc.case_number), 0)) as avg_project_value FROM ordrestyring_cases oc WHERE oc.customer_number IS NOT NULL GROUP BY oc.customer_number HAVING total_projects > 0 ORDER BY total_material_sales DESC `); return customers.map(customer => { const laborValue = Number(customer.total_hours) * 450; const totalValue = laborValue + Number(customer.total_material_sales); const materialProfit = Number(customer.total_material_sales) - Number(customer.total_material_costs); return { ...customer, total_labor_value: Math.round(laborValue), total_customer_value: Math.round(totalValue), material_profit: Math.round(materialProfit), avg_project_total_value: Math.round(totalValue / customer.total_projects), customer_lifetime_months: customer.first_project && customer.last_project ? Math.round((new Date(customer.last_project) - new Date(customer.first_project)) / (1000 * 60 * 60 * 24 * 30.44)) : 0 }; }); } finally { await connection.end(); } } async getMaterialInsights() { const connection = await this.getConnection(); try { const [materials] = await connection.execute(` SELECT ocm.supplier, COUNT(DISTINCT ocm.case_number) as projects_used, COUNT(*) as total_line_items, SUM(ocm.total_cost) as total_purchases, SUM(ocm.total_sales) as total_sales, AVG(CASE WHEN ocm.cost_price > 0 THEN ((ocm.sales_price - ocm.cost_price) / ocm.cost_price) * 100 ELSE 0 END) as avg_margin_percent, AVG(ocm.cost_price) as avg_unit_cost, AVG(ocm.sales_price) as avg_unit_price, SUM(ocm.total_sales - ocm.total_cost) as total_profit FROM ordrestyring_case_materials ocm WHERE ocm.cost_price > 0 AND ocm.sales_price > 0 GROUP BY ocm.supplier HAVING total_purchases > 1000 ORDER BY total_purchases DESC `); return materials.map(material => ({ ...material, profit_margin_percent: Number(material.total_purchases) > 0 ? Math.round((Number(material.total_profit) / Number(material.total_purchases)) * 1000) / 10 : 0, avg_order_value: Math.round(Number(material.total_purchases) / material.projects_used), items_per_project: Math.round(material.total_line_items / material.projects_used * 10) / 10 })); } finally { await connection.end(); } } async getTrendAnalytics(months = 12) { const connection = await this.getConnection(); try { const [trends] = await connection.execute(` SELECT YEAR(FROM_UNIXTIME(oh.start_time)) as year, MONTH(FROM_UNIXTIME(oh.start_time)) as month, DATE_FORMAT(FROM_UNIXTIME(oh.start_time), '%Y-%m') as month_year, COUNT(DISTINCT oh.case_number) as active_projects, COUNT(DISTINCT oh.employee_id) as active_employees, SUM(oh.duration_hours) as total_hours, AVG(oh.duration_hours) as avg_daily_hours, COUNT(*) as total_entries FROM ordrestyring_hours oh WHERE oh.start_time >= UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL ? MONTH)) GROUP BY YEAR(FROM_UNIXTIME(oh.start_time)), MONTH(FROM_UNIXTIME(oh.start_time)) ORDER BY year DESC, month DESC `, [months]); return trends.map(trend => ({ ...trend, estimated_revenue: Math.round(Number(trend.total_hours) * 450), productivity_score: Math.round((trend.total_hours / trend.active_employees / trend.active_projects) * 10) / 10, utilization_rate: Math.round((trend.total_entries / (trend.active_employees * 22)) * 1000) / 10 // Assuming 22 working days })); } finally { await connection.end(); } } // ==================== // QUOTE ACCURACY TRACKING // ==================== async calculateQuoteAccuracyMetrics() { const connection = await this.getConnection(); try { // Get projects with enough data for accuracy analysis const projects = await this.generateProjectAnalytics(); const FixedHistoricalQuoteCalculator = require('./historical-quote-calculator.js'); const calculator = new FixedHistoricalQuoteCalculator(); const accuracyResults = []; for (const project of projects.slice(0, 20)) { // Analyze top 20 projects if (project.actual_hours > 50 && project.material_sales > 10000) { try { const quote = await calculator.generateIntelligentQuote( project.customer_number, project.description, 3, // medium complexity project.actual_hours > 1000 ? 'large' : 'medium' ); if (quote.success) { const hourAccuracy = (quote.quote.estimatedHours / project.actual_hours) * 100; const valueAccuracy = (quote.quote.finalQuote / project.total_project_value) * 100; accuracyResults.push({ case_number: project.case_number, description: project.description, actual_hours: project.actual_hours, estimated_hours: quote.quote.estimatedHours, actual_value: project.total_project_value, estimated_value: quote.quote.finalQuote, hour_accuracy: Math.round(hourAccuracy * 10) / 10, value_accuracy: Math.round(valueAccuracy * 10) / 10, confidence: quote.confidence }); } } catch (error) { console.log(`Skipped analysis for case ${project.case_number}: ${error.message}`); } } } await calculator.close(); // Calculate overall accuracy stats if (accuracyResults.length > 0) { const avgHourAccuracy = accuracyResults.reduce((sum, r) => sum + r.hour_accuracy, 0) / accuracyResults.length; const avgValueAccuracy = accuracyResults.reduce((sum, r) => sum + r.value_accuracy, 0) / accuracyResults.length; const avgConfidence = accuracyResults.reduce((sum, r) => sum + r.confidence, 0) / accuracyResults.length; return { total_analyzed: accuracyResults.length, avg_hour_accuracy: Math.round(avgHourAccuracy * 10) / 10, avg_value_accuracy: Math.round(avgValueAccuracy * 10) / 10, avg_confidence: Math.round(avgConfidence * 10) / 10, detailed_results: accuracyResults }; } return { total_analyzed: 0, message: "Insufficient data for accuracy analysis" }; } finally { await connection.end(); } } // ==================== // COMPREHENSIVE ANALYTICS REPORT // ==================== async generateFullAnalyticsReport() { console.log('🔄 Generating comprehensive analytics report...'); const [ overallKPIs, employeeMetrics, customerAnalytics, materialInsights, trendData, accuracyMetrics ] = await Promise.all([ this.getOverallKPIs(), this.getEmployeePerformanceMetrics(), this.getCustomerAnalytics(), this.getMaterialInsights(), this.getTrendAnalytics(12), this.calculateQuoteAccuracyMetrics() ]); return { generated_at: new Date().toISOString(), overview: overallKPIs, employees: employeeMetrics, customers: customerAnalytics.slice(0, 20), // Top 20 customers suppliers: materialInsights, trends: trendData, quote_accuracy: accuracyMetrics, summary: { total_business_value: overallKPIs.total_project_value, active_projects: overallKPIs.total_projects, team_size: overallKPIs.active_employees, avg_profit_margin: overallKPIs.overall_profit_margin, quote_accuracy_score: accuracyMetrics.avg_value_accuracy || 0 } }; } } module.exports = CaseAnalyticsService;