Files
tilbudgivern/archive/analysis/simple-analytics-populator.js
T

453 lines
19 KiB
JavaScript

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;