Adds employee revenue and utilization chart

Updates package dependencies and charts for employee performance visualization

Includes:
- Upgrade to latest charting library version
- New chart displaying revenue and utilization metrics
- Forecast and actual data visualization with dual axis support
This commit is contained in:
Janus C. H. Knudsen 2025-12-28 23:31:31 +01:00
parent 85b006e0d6
commit 6c3662f571
4 changed files with 156 additions and 5 deletions

8
package-lock.json generated
View file

@ -10,7 +10,7 @@
"dependencies": { "dependencies": {
"@novadi/core": "^0.6.0", "@novadi/core": "^0.6.0",
"@rollup/rollup-win32-x64-msvc": "^4.52.2", "@rollup/rollup-win32-x64-msvc": "^4.52.2",
"@sevenweirdpeople/swp-charting": "^0.1.7", "@sevenweirdpeople/swp-charting": "^0.2.1",
"dayjs": "^1.11.19", "dayjs": "^1.11.19",
"fuse.js": "^7.1.0", "fuse.js": "^7.1.0",
"json-diff-ts": "^4.8.2", "json-diff-ts": "^4.8.2",
@ -1175,9 +1175,9 @@
] ]
}, },
"node_modules/@sevenweirdpeople/swp-charting": { "node_modules/@sevenweirdpeople/swp-charting": {
"version": "0.1.7", "version": "0.2.1",
"resolved": "https://registry.npmjs.org/@sevenweirdpeople/swp-charting/-/swp-charting-0.1.7.tgz", "resolved": "https://registry.npmjs.org/@sevenweirdpeople/swp-charting/-/swp-charting-0.2.1.tgz",
"integrity": "sha512-i3HEQMQmltmxykPGXCRcN8VMJZXD1sI8urb7rN8eTjB9EscDsACdkS+WyvxOEEEhyHj1hMdtULeX3BIZ1ZNrng==", "integrity": "sha512-QtY77Dyv4Vs/rWfBVSDTmuxgD4L8tGu4pmTF0l3i8HDwK6qtT2wEtH35UHD1RDFE1VtOGcnU0/dTdqjNWCqzxA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/chai": { "node_modules/@types/chai": {

View file

@ -41,7 +41,7 @@
"dependencies": { "dependencies": {
"@novadi/core": "^0.6.0", "@novadi/core": "^0.6.0",
"@rollup/rollup-win32-x64-msvc": "^4.52.2", "@rollup/rollup-win32-x64-msvc": "^4.52.2",
"@sevenweirdpeople/swp-charting": "^0.1.7", "@sevenweirdpeople/swp-charting": "^0.2.1",
"dayjs": "^1.11.19", "dayjs": "^1.11.19",
"fuse.js": "^7.1.0", "fuse.js": "^7.1.0",
"json-diff-ts": "^4.8.2", "json-diff-ts": "^4.8.2",

View file

@ -0,0 +1,67 @@
{
"categories": ["Uge 40", "Uge 41", "Uge 42", "Uge 43", "Uge 44", "Uge 45", "Uge 46", "Uge 47", "Uge 48", "Uge 49", "Uge 50", "Uge 51", "Uge 52", "Uge 1", "Uge 2", "Uge 3", "Uge 4", "Uge 5", "Uge 6", "Uge 7", "Uge 8", "Uge 9", "Uge 10", "Uge 11", "Uge 12"],
"nowCategory": "Uge 52",
"forecastStartCategory": "Uge 1",
"actual": {
"revenue": [
{ "x": "Uge 40", "y": 38500 },
{ "x": "Uge 41", "y": 42000 },
{ "x": "Uge 42", "y": 35200 },
{ "x": "Uge 43", "y": 44800 },
{ "x": "Uge 44", "y": 41500 },
{ "x": "Uge 45", "y": 39200 },
{ "x": "Uge 46", "y": 46100 },
{ "x": "Uge 47", "y": 43800 },
{ "x": "Uge 48", "y": 40500 },
{ "x": "Uge 49", "y": 45200 },
{ "x": "Uge 50", "y": 42800 },
{ "x": "Uge 51", "y": 44500 },
{ "x": "Uge 52", "y": 28450 }
],
"utilization": [
{ "x": "Uge 40", "y": 82 },
{ "x": "Uge 41", "y": 88 },
{ "x": "Uge 42", "y": 75 },
{ "x": "Uge 43", "y": 91 },
{ "x": "Uge 44", "y": 85 },
{ "x": "Uge 45", "y": 80 },
{ "x": "Uge 46", "y": 94 },
{ "x": "Uge 47", "y": 89 },
{ "x": "Uge 48", "y": 83 },
{ "x": "Uge 49", "y": 92 },
{ "x": "Uge 50", "y": 87 },
{ "x": "Uge 51", "y": 90 },
{ "x": "Uge 52", "y": 84 }
]
},
"forecast": {
"revenue": [
{ "x": "Uge 1", "y": 0 },
{ "x": "Uge 2", "y": 32500 },
{ "x": "Uge 3", "y": 28800 },
{ "x": "Uge 4", "y": 22400 },
{ "x": "Uge 5", "y": 15200 },
{ "x": "Uge 6", "y": 9800 },
{ "x": "Uge 7", "y": 6500 },
{ "x": "Uge 8", "y": 4200 },
{ "x": "Uge 9", "y": 2800 },
{ "x": "Uge 10", "y": 1500 },
{ "x": "Uge 11", "y": 800 },
{ "x": "Uge 12", "y": 0 }
],
"utilization": [
{ "x": "Uge 1", "y": 0 },
{ "x": "Uge 2", "y": 74 },
{ "x": "Uge 3", "y": 58 },
{ "x": "Uge 4", "y": 39 },
{ "x": "Uge 5", "y": 21 },
{ "x": "Uge 6", "y": 11 },
{ "x": "Uge 7", "y": 8 },
{ "x": "Uge 8", "y": 5 },
{ "x": "Uge 9", "y": 3 },
{ "x": "Uge 10", "y": 2 },
{ "x": "Uge 11", "y": 1 },
{ "x": "Uge 12", "y": 0 }
]
}
}

View file

@ -1895,6 +1895,13 @@
color: var(--color-text-secondary); color: var(--color-text-secondary);
} }
swp-chart-subtitle {
display: block;
font-size: 12px;
color: var(--color-text-muted);
margin-top: 2px;
}
swp-chart-legend { swp-chart-legend {
display: flex; display: flex;
gap: 16px; gap: 16px;
@ -2658,6 +2665,17 @@
</swp-stat-card> </swp-stat-card>
</div> </div>
<!-- Omsætning & Belægningsgrad chart -->
<swp-card style="margin-bottom: 24px;">
<swp-chart-section>
<swp-chart-header>
<swp-chart-title>Omsætning & Belægningsgrad</swp-chart-title>
<swp-chart-subtitle>3 måneder bagud · 3 måneder frem</swp-chart-subtitle>
</swp-chart-header>
<swp-chart-container id="revenueUtilizationChart" style="height: 300px;"></swp-chart-container>
</swp-chart-section>
</swp-card>
<div class="grid-2"> <div class="grid-2">
<swp-card> <swp-card>
<swp-chart-section> <swp-chart-section>
@ -3489,6 +3507,72 @@
height: 200, height: 200,
legend: false legend: false
}); });
// Revenue & Utilization chart - dual axis (3 mdr bagud + 3 mdr frem)
fetch('data/employee-revenue-utilization.json')
.then(res => res.json())
.then(data => {
createChart(document.getElementById('revenueUtilizationChart'), {
xAxis: { categories: data.categories },
yAxis: [
{ min: 0, max: 50000, format: v => `${(v/1000).toFixed(0)}k` }, // Left: Revenue
{ min: 0, max: 100, format: v => `${v}%` }, // Right: Utilization
],
series: [
// Actual revenue (solid bars)
{
name: 'Omsætning',
color: '#3b82f6',
type: 'bar',
yAxisIndex: 0,
unit: 'kr',
data: data.actual.revenue,
bar: { radius: 2 },
},
// Forecast revenue (transparent bars)
{
name: 'Omsætning (forecast)',
color: '#3b82f6',
type: 'bar',
yAxisIndex: 0,
unit: 'kr',
data: data.forecast.revenue,
bar: { radius: 2, opacity: 0.35 },
},
// Actual utilization (solid line)
{
name: 'Belægning',
color: '#00897b',
type: 'line',
yAxisIndex: 1,
unit: '%',
data: data.actual.utilization,
line: { width: 2.5 },
point: { radius: 0 },
showArea: false,
},
// Forecast utilization (dashed line)
{
name: 'Belægning (forecast)',
color: '#00897b',
type: 'line',
yAxisIndex: 1,
unit: '%',
data: data.forecast.utilization,
line: { width: 2.5, dashArray: '4 4' },
point: { radius: 0 },
showArea: false,
},
],
annotations: [
{ type: 'region', x: data.forecastStartCategory, x2: data.categories[data.categories.length - 1], backgroundColor: 'rgba(0,0,0,0.03)' },
{ type: 'verticalLine', x: data.nowCategory, dashArray: '4 4', label: 'Nu', labelPosition: 'top' },
],
legend: { position: 'top', align: 'end' },
padding: { right: 60 },
height: 280,
});
});
</script> </script>
</body> </body>