387 lines
13 KiB
JavaScript
387 lines
13 KiB
JavaScript
const express = require('express');
|
|
const cors = require('cors');
|
|
const CaseAnalyticsService = require('./case-analytics-service');
|
|
|
|
const app = express();
|
|
const PORT = process.env.ANALYTICS_PORT || 3001;
|
|
|
|
// Middleware
|
|
app.use(cors());
|
|
app.use(express.json());
|
|
|
|
// Initialize analytics service
|
|
const analyticsService = new CaseAnalyticsService();
|
|
|
|
// Health check
|
|
app.get('/health', (req, res) => {
|
|
res.json({ status: 'OK', service: 'Case Analytics API', timestamp: new Date().toISOString() });
|
|
});
|
|
|
|
// ====================
|
|
// DASHBOARD API ENDPOINTS
|
|
// ====================
|
|
|
|
// Overall KPIs - Homepage dashboard cards
|
|
app.get('/api/analytics/kpis', async (req, res) => {
|
|
try {
|
|
const kpis = await analyticsService.getOverallKPIs();
|
|
res.json({
|
|
success: true,
|
|
data: kpis,
|
|
cached_at: new Date().toISOString()
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
error: error.message,
|
|
endpoint: '/api/analytics/kpis'
|
|
});
|
|
}
|
|
});
|
|
|
|
// Project analytics - Project performance dashboard
|
|
// Project analytics with filtering and pagination
|
|
app.get('/api/analytics/projects', async (req, res) => {
|
|
try {
|
|
const { limit = 50, offset = 0, sortBy = 'profit_margin', sortDir = 'desc' } = req.query;
|
|
const projects = await analyticsService.generateProjectAnalytics();
|
|
|
|
res.json({
|
|
success: true,
|
|
data: projects,
|
|
pagination: { limit: parseInt(limit), offset: parseInt(offset) },
|
|
cached_at: new Date().toISOString()
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
error: error.message,
|
|
endpoint: '/api/analytics/projects'
|
|
});
|
|
}
|
|
});
|
|
|
|
// Single project detailed analytics
|
|
app.get('/api/analytics/projects/:id', async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const projectAnalytics = await analyticsService.getSingleProjectAnalytics(parseInt(id));
|
|
|
|
res.json({
|
|
success: true,
|
|
data: projectAnalytics,
|
|
cached_at: new Date().toISOString()
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
error: error.message,
|
|
endpoint: `/api/analytics/projects/${req.params.id}`
|
|
});
|
|
}
|
|
});
|
|
|
|
// Employee performance metrics
|
|
app.get('/api/analytics/employees', async (req, res) => {
|
|
try {
|
|
const employees = await analyticsService.getEmployeePerformanceMetrics();
|
|
|
|
res.json({
|
|
success: true,
|
|
data: employees,
|
|
count: employees.length
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
error: error.message,
|
|
endpoint: '/api/analytics/employees'
|
|
});
|
|
}
|
|
});
|
|
|
|
// Customer analytics and insights
|
|
app.get('/api/analytics/customers', async (req, res) => {
|
|
try {
|
|
const { top = 25 } = req.query;
|
|
let customers = await analyticsService.getCustomerAnalytics();
|
|
|
|
// Get top customers by value
|
|
customers = customers.slice(0, parseInt(top));
|
|
|
|
res.json({
|
|
success: true,
|
|
data: customers,
|
|
count: customers.length,
|
|
showing_top: parseInt(top)
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
error: error.message,
|
|
endpoint: '/api/analytics/customers'
|
|
});
|
|
}
|
|
});
|
|
|
|
// Material and supplier insights
|
|
app.get('/api/analytics/materials', async (req, res) => {
|
|
try {
|
|
const materials = await analyticsService.getMaterialInsights();
|
|
|
|
res.json({
|
|
success: true,
|
|
data: materials,
|
|
count: materials.length
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
error: error.message,
|
|
endpoint: '/api/analytics/materials'
|
|
});
|
|
}
|
|
});
|
|
|
|
// Trend analysis - charts and time series
|
|
app.get('/api/analytics/trends', async (req, res) => {
|
|
try {
|
|
const { months = 12 } = req.query;
|
|
const trends = await analyticsService.getTrendAnalytics(parseInt(months));
|
|
|
|
res.json({
|
|
success: true,
|
|
data: trends,
|
|
period_months: parseInt(months),
|
|
count: trends.length
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
error: error.message,
|
|
endpoint: '/api/analytics/trends'
|
|
});
|
|
}
|
|
});
|
|
|
|
// Quote accuracy metrics - tilbudsberegner performance
|
|
app.get('/api/analytics/quote-accuracy', async (req, res) => {
|
|
try {
|
|
const accuracy = await analyticsService.calculateQuoteAccuracyMetrics();
|
|
|
|
res.json({
|
|
success: true,
|
|
data: accuracy,
|
|
generated_at: new Date().toISOString()
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
error: error.message,
|
|
endpoint: '/api/analytics/quote-accuracy'
|
|
});
|
|
}
|
|
});
|
|
|
|
// Comprehensive analytics report - full dashboard data
|
|
app.get('/api/analytics/report/full', async (req, res) => {
|
|
try {
|
|
const report = await analyticsService.generateFullAnalyticsReport();
|
|
|
|
res.json({
|
|
success: true,
|
|
data: report
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
error: error.message,
|
|
endpoint: '/api/analytics/report/full'
|
|
});
|
|
}
|
|
});
|
|
|
|
// Single project deep dive analysis
|
|
app.get('/api/analytics/projects/:caseNumber', async (req, res) => {
|
|
try {
|
|
const { caseNumber } = req.params;
|
|
const projects = await analyticsService.generateProjectAnalytics(caseNumber);
|
|
|
|
if (projects.length === 0) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: `Case ${caseNumber} not found or has no data`
|
|
});
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
data: projects[0],
|
|
case_number: caseNumber
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
error: error.message,
|
|
endpoint: `/api/analytics/projects/${req.params.caseNumber}`
|
|
});
|
|
}
|
|
});
|
|
|
|
// ====================
|
|
// BUSINESS INTELLIGENCE ENDPOINTS
|
|
// ====================
|
|
|
|
// Executive summary for management
|
|
app.get('/api/analytics/executive-summary', async (req, res) => {
|
|
try {
|
|
const kpis = await analyticsService.getOverallKPIs();
|
|
const trends = await analyticsService.getTrendAnalytics(3); // Last 3 months
|
|
const topCustomers = await analyticsService.getCustomerAnalytics();
|
|
|
|
const currentMonth = trends[0] || {};
|
|
const previousMonth = trends[1] || {};
|
|
|
|
const summary = {
|
|
period: "Last 30 days vs Previous 30 days",
|
|
revenue: {
|
|
current: kpis.total_project_value,
|
|
trend: currentMonth.estimated_revenue && previousMonth.estimated_revenue ?
|
|
((currentMonth.estimated_revenue - previousMonth.estimated_revenue) / previousMonth.estimated_revenue * 100).toFixed(1) + '%' : 'N/A'
|
|
},
|
|
projects: {
|
|
active: kpis.total_projects,
|
|
trend: currentMonth.active_projects && previousMonth.active_projects ?
|
|
((currentMonth.active_projects - previousMonth.active_projects) / previousMonth.active_projects * 100).toFixed(1) + '%' : 'N/A'
|
|
},
|
|
profit_margin: kpis.overall_profit_margin,
|
|
top_customers: topCustomers.slice(0, 5),
|
|
team_utilization: currentMonth.utilization_rate || 0,
|
|
key_metrics: {
|
|
avg_project_value: kpis.avg_project_value,
|
|
avg_hours_per_project: kpis.avg_hours_per_project,
|
|
material_margin: kpis.avg_material_margin
|
|
}
|
|
};
|
|
|
|
res.json({
|
|
success: true,
|
|
data: summary,
|
|
generated_at: new Date().toISOString()
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
error: error.message,
|
|
endpoint: '/api/analytics/executive-summary'
|
|
});
|
|
}
|
|
});
|
|
|
|
// Performance benchmarks for quote calibration
|
|
app.get('/api/analytics/benchmarks', async (req, res) => {
|
|
try {
|
|
const { project_size = 'medium', complexity = 3 } = req.query;
|
|
|
|
// Filter projects by similar characteristics
|
|
const allProjects = await analyticsService.generateProjectAnalytics();
|
|
let similarProjects = allProjects;
|
|
|
|
// Size filtering
|
|
if (project_size === 'small') {
|
|
similarProjects = similarProjects.filter(p => p.actual_hours <= 200);
|
|
} else if (project_size === 'medium') {
|
|
similarProjects = similarProjects.filter(p => p.actual_hours > 200 && p.actual_hours <= 800);
|
|
} else if (project_size === 'large') {
|
|
similarProjects = similarProjects.filter(p => p.actual_hours > 800 && p.actual_hours <= 1500);
|
|
} else if (project_size === 'xlarge') {
|
|
similarProjects = similarProjects.filter(p => p.actual_hours > 1500);
|
|
}
|
|
|
|
if (similarProjects.length === 0) {
|
|
return res.json({
|
|
success: true,
|
|
data: { message: "No similar projects found for benchmarking" },
|
|
filters: { project_size, complexity }
|
|
});
|
|
}
|
|
|
|
// Calculate benchmarks
|
|
const totalHours = similarProjects.reduce((sum, p) => sum + p.actual_hours, 0);
|
|
const totalValue = similarProjects.reduce((sum, p) => sum + p.total_project_value, 0);
|
|
const avgHours = totalHours / similarProjects.length;
|
|
const avgValue = totalValue / similarProjects.length;
|
|
const avgHourlyRate = totalValue / totalHours;
|
|
|
|
const benchmarks = {
|
|
sample_size: similarProjects.length,
|
|
filters_applied: { project_size, complexity },
|
|
benchmarks: {
|
|
avg_hours: Math.round(avgHours * 10) / 10,
|
|
avg_project_value: Math.round(avgValue),
|
|
avg_hourly_rate: Math.round(avgHourlyRate * 10) / 10,
|
|
hours_range: {
|
|
min: Math.min(...similarProjects.map(p => p.actual_hours)),
|
|
max: Math.max(...similarProjects.map(p => p.actual_hours)),
|
|
percentile_25: this.calculatePercentile(similarProjects.map(p => p.actual_hours), 25),
|
|
percentile_75: this.calculatePercentile(similarProjects.map(p => p.actual_hours), 75)
|
|
},
|
|
material_ratio: totalValue > 0 ?
|
|
(similarProjects.reduce((sum, p) => sum + (p.material_sales || 0), 0) / totalValue * 100) : 0
|
|
},
|
|
use_case: "Quote calculator calibration and accuracy improvement"
|
|
};
|
|
|
|
res.json({
|
|
success: true,
|
|
data: benchmarks
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
error: error.message,
|
|
endpoint: '/api/analytics/benchmarks'
|
|
});
|
|
}
|
|
});
|
|
|
|
// Helper function for percentile calculation
|
|
app.calculatePercentile = function(arr, percentile) {
|
|
const sorted = arr.slice().sort((a, b) => a - b);
|
|
const index = (percentile / 100) * (sorted.length - 1);
|
|
const lower = Math.floor(index);
|
|
const upper = Math.ceil(index);
|
|
const weight = index % 1;
|
|
|
|
if (upper >= sorted.length) return sorted[sorted.length - 1];
|
|
return sorted[lower] * (1 - weight) + sorted[upper] * weight;
|
|
};
|
|
|
|
// Error handling middleware
|
|
app.use((error, req, res, next) => {
|
|
console.error('Analytics API Error:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Internal server error',
|
|
message: error.message,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
});
|
|
|
|
// Start server
|
|
app.listen(PORT, () => {
|
|
console.log(`🚀 Case Analytics API server running on port ${PORT}`);
|
|
console.log(`📊 Available endpoints:`);
|
|
console.log(` GET /api/analytics/kpis - Overall business KPIs`);
|
|
console.log(` GET /api/analytics/projects - Project analytics`);
|
|
console.log(` GET /api/analytics/employees - Employee performance`);
|
|
console.log(` GET /api/analytics/customers - Customer insights`);
|
|
console.log(` GET /api/analytics/materials - Material/supplier analysis`);
|
|
console.log(` GET /api/analytics/trends - Time-based trends`);
|
|
console.log(` GET /api/analytics/quote-accuracy - Quote calculator performance`);
|
|
console.log(` GET /api/analytics/report/full - Comprehensive analytics report`);
|
|
console.log(` GET /api/analytics/executive-summary - Executive dashboard`);
|
|
console.log(` GET /api/analytics/benchmarks - Performance benchmarks`);
|
|
});
|
|
|
|
module.exports = app; |