453 lines
19 KiB
JavaScript
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; |