Files
tilbudgivern/react-app/src/components/AnalyticsDashboard.jsx
T
alexpolo1 1d79f4710d feat: Implement comprehensive analytics population and data synchronization
- Added SimpleAnalyticsPopulator class for populating project, employee, customer analytics, and daily metrics.
- Integrated MySQL database connections for tilbudgivern and ordrestyring_local databases.
- Developed sync_ordrestyring_data_old.sh script for comprehensive data synchronization from Ordrestyring API.
- Created test scripts for analytics API and services to validate functionality and data integrity.
- Introduced FixedHistoricalQuoteCalculator and HistoricalQuoteCalculator for generating quotes based on historical data.
- Implemented vary-profit-margins.js to introduce variability in profit margins based on project size and customer type.
2025-10-14 13:06:02 +00:00

304 lines
14 KiB
React

import React, { useState, useEffect } from 'react';
const ANALYTICS_API_BASE = 'http://localhost:3001/api/analytics';
const AnalyticsDashboard = () => {
const [kpis, setKpis] = useState(null);
const [employees, setEmployees] = useState([]);
const [customers, setCustomers] = useState([]);
const [materials, setMaterials] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [activeTab, setActiveTab] = useState('overview');
// Fetch analytics data
useEffect(() => {
const fetchAnalyticsData = async () => {
try {
setLoading(true);
const [kpisRes, employeesRes, customersRes, materialsRes] = await Promise.all([
fetch(`${ANALYTICS_API_BASE}/kpis`),
fetch(`${ANALYTICS_API_BASE}/employees`),
fetch(`${ANALYTICS_API_BASE}/customers?top=10`),
fetch(`${ANALYTICS_API_BASE}/materials`)
]);
const [kpisData, employeesData, customersData, materialsData] = await Promise.all([
kpisRes.json(),
employeesRes.json(),
customersRes.json(),
materialsRes.json()
]);
setKpis(kpisData.data);
setEmployees(employeesData.data?.slice(0, 10) || []);
setCustomers(customersData.data || []);
setMaterials(materialsData.data?.slice(0, 8) || []);
} catch (err) {
setError(err.message);
console.error('Analytics fetch error:', err);
} finally {
setLoading(false);
}
};
fetchAnalyticsData();
}, []);
const formatCurrency = (amount) => {
if (!amount) return '0 kr';
if (amount >= 1000000) {
return `${(amount / 1000000).toFixed(1)}M kr`;
} else if (amount >= 1000) {
return `${(amount / 1000).toFixed(0)}k kr`;
}
return `${amount.toLocaleString()} kr`;
};
const formatNumber = (num) => {
if (!num) return '0';
return num.toLocaleString();
};
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-lg">📊 Indlæser nøgletal...</div>
</div>
);
}
if (error) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-red-500">❌ Fejl ved indlæsning: {error}</div>
</div>
);
}
const KPICard = ({ title, value, icon, color = 'blue' }) => (
<div className={`bg-white rounded-lg shadow-md p-6 border-l-4 border-${color}-500`}>
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">{title}</p>
<p className="text-2xl font-bold text-gray-900">{value}</p>
</div>
<div className="text-3xl">{icon}</div>
</div>
</div>
);
const TabButton = ({ id, label, isActive, onClick }) => (
<button
className={`px-4 py-2 text-sm font-medium rounded-lg transition-colors ${
isActive
? 'bg-blue-500 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
onClick={() => onClick(id)}
>
{label}
</button>
);
const DataTable = ({ title, data, columns }) => (
<div className="bg-white rounded-lg shadow-md p-6">
<h3 className="text-lg font-semibold mb-4">{title}</h3>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
{columns.map((col) => (
<th key={col.key} className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
{col.label}
</th>
))}
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{data.map((item, index) => (
<tr key={index} className="hover:bg-gray-50">
{columns.map((col) => (
<td key={col.key} className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{col.format ? col.format(item[col.key]) : item[col.key]}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
);
return (
<div className="min-h-screen bg-gray-50 p-6">
<div className="max-w-7xl mx-auto space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-3xl font-bold text-gray-900">📊 Business Nøgletal</h1>
<div className="text-sm text-gray-500">
Sidst opdateret: {new Date().toLocaleString('da-DK')}
</div>
</div>
{/* KPI Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<KPICard
title="Samlet Projekt Værdi"
value={formatCurrency(kpis?.total_project_value)}
icon="💰"
color="green"
/>
<KPICard
title="Aktive Projekter"
value={formatNumber(kpis?.total_projects)}
icon="📋"
color="blue"
/>
<KPICard
title="Profit Margin"
value={`${kpis?.overall_profit_margin}%`}
icon="📈"
color="green"
/>
<KPICard
title="Aktive Medarbejdere"
value={formatNumber(kpis?.active_employees)}
icon="👥"
color="purple"
/>
</div>
{/* Tabs */}
<div className="bg-white rounded-lg shadow-md p-6">
<div className="flex space-x-2 mb-6 border-b">
<TabButton
id="overview"
label="Oversigt"
isActive={activeTab === 'overview'}
onClick={setActiveTab}
/>
<TabButton
id="employees"
label="Medarbejdere"
isActive={activeTab === 'employees'}
onClick={setActiveTab}
/>
<TabButton
id="customers"
label="Kunder"
isActive={activeTab === 'customers'}
onClick={setActiveTab}
/>
<TabButton
id="materials"
label="Materialer"
isActive={activeTab === 'materials'}
onClick={setActiveTab}
/>
</div>
{/* Tab Content */}
{activeTab === 'overview' && (
<div className="space-y-6">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-gray-50 rounded-lg p-6">
<h3 className="text-lg font-semibold mb-4">📈 Business Metrics</h3>
<div className="space-y-3">
<div className="flex justify-between">
<span className="text-gray-600">Unikke Kunder</span>
<span className="font-semibold">{formatNumber(kpis?.unique_customers)}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Gennemsnit Projekt Værdi</span>
<span className="font-semibold">{formatCurrency(kpis?.avg_project_value)}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Total Timer Logget</span>
<span className="font-semibold">{formatNumber(kpis?.total_hours_logged)}t</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Gennemsnit Timer/Projekt</span>
<span className="font-semibold">{kpis?.avg_hours_per_project}t</span>
</div>
</div>
</div>
<div className="bg-gray-50 rounded-lg p-6">
<h3 className="text-lg font-semibold mb-4">🔨 Material Overview</h3>
<div className="space-y-3">
<div className="flex justify-between">
<span className="text-gray-600">Total Material Indkøb</span>
<span className="font-semibold">{formatCurrency(kpis?.total_material_costs)}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Total Material Salg</span>
<span className="font-semibold">{formatCurrency(kpis?.total_material_sales)}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Material Profit</span>
<span className="font-semibold text-green-600">
{formatCurrency(kpis?.total_material_sales - kpis?.total_material_costs)}
</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Unikke Leverandører</span>
<span className="font-semibold">{formatNumber(kpis?.unique_suppliers)}</span>
</div>
</div>
</div>
</div>
</div>
)}
{activeTab === 'employees' && (
<DataTable
title="👥 Top Medarbejder Performance"
data={employees}
columns={[
{ key: 'full_name', label: 'Navn' },
{ key: 'total_hours', label: 'Timer', format: (val) => `${val}t` },
{ key: 'projects_worked', label: 'Projekter' },
{ key: 'total_labor_value', label: 'Værdi', format: formatCurrency },
{ key: 'avg_daily_hours', label: 'Ø Timer/Dag', format: (val) => `${val}t` }
]}
/>
)}
{activeTab === 'customers' && (
<DataTable
title="🏢 Top Kunder (Værdi)"
data={customers}
columns={[
{ key: 'customer_number', label: 'Kunde Nr.' },
{ key: 'total_projects', label: 'Projekter' },
{ key: 'total_customer_value', label: 'Total Værdi', format: formatCurrency },
{ key: 'avg_project_total_value', label: 'Ø Projekt Værdi', format: formatCurrency },
{ key: 'total_hours', label: 'Timer', format: (val) => `${val}t` }
]}
/>
)}
{activeTab === 'materials' && (
<DataTable
title="🔨 Top Leverandører"
data={materials}
columns={[
{ key: 'supplier', label: 'Leverandør' },
{ key: 'projects_used', label: 'Projekter' },
{ key: 'total_purchases', label: 'Indkøb', format: formatCurrency },
{ key: 'total_profit', label: 'Profit', format: formatCurrency },
{ key: 'profit_margin_percent', label: 'Margin %', format: (val) => `${val}%` }
]}
/>
)}
</div>
</div>
</div>
);
};
export default AnalyticsDashboard;