Refactor frontend build and chart initialization
Moves chart data to JSON file for better separation of concerns Implements lazy chart initialization in reports module Updates build script and npm dependencies Removes hardcoded chart scripts from Razor page
This commit is contained in:
parent
097fe7f912
commit
b921e26e48
12 changed files with 249 additions and 258 deletions
|
|
@ -16,7 +16,8 @@
|
||||||
"Bash(dir /s /b \"C:\\\\Users\\\\Janus Knudsen\\\\source\\\\swp-repos\\\\*rapport*.html\")",
|
"Bash(dir /s /b \"C:\\\\Users\\\\Janus Knudsen\\\\source\\\\swp-repos\\\\*rapport*.html\")",
|
||||||
"Bash(dir /s /b \"C:\\\\Users\\\\Janus Knudsen\\\\source\\\\swp-repos\\\\Calendar\\\\wwwroot\\\\poc*.html\")",
|
"Bash(dir /s /b \"C:\\\\Users\\\\Janus Knudsen\\\\source\\\\swp-repos\\\\Calendar\\\\wwwroot\\\\poc*.html\")",
|
||||||
"Bash(Get-ChildItem:*)",
|
"Bash(Get-ChildItem:*)",
|
||||||
"Bash(Select-Object -ExpandProperty FullName)"
|
"Bash(Select-Object -ExpandProperty FullName)",
|
||||||
|
"Bash(npm run build:*)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
38
CLAUDE.md
38
CLAUDE.md
|
|
@ -12,46 +12,18 @@ PlanTempus is a .NET 9 web application built with ASP.NET Core Razor Pages. It u
|
||||||
|
|
||||||
## Build and Development Commands
|
## Build and Development Commands
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
- .NET 9.0 SDK or later
|
|
||||||
- PostgreSQL database
|
|
||||||
|
|
||||||
### Common Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build the solution
|
|
||||||
dotnet build
|
|
||||||
|
|
||||||
# Run the main application
|
|
||||||
dotnet run --project Application/PlanTempus.Application.csproj
|
|
||||||
|
|
||||||
# Run with specific launch profile
|
|
||||||
dotnet run --project Application/PlanTempus.Application.csproj --launch-profile https
|
|
||||||
|
|
||||||
# Run tests
|
|
||||||
dotnet test
|
|
||||||
|
|
||||||
# Run specific test project
|
|
||||||
dotnet test Tests/PlanTempus.X.TDD.csproj
|
|
||||||
dotnet test PlanTempus.X.BDD/PlanTempus.X.BDD.csproj
|
|
||||||
|
|
||||||
# Clean build artifacts
|
|
||||||
dotnet clean
|
|
||||||
|
|
||||||
# Restore dependencies
|
|
||||||
dotnet restore
|
|
||||||
```
|
|
||||||
|
|
||||||
### TypeScript/Frontend Development
|
### TypeScript/Frontend Development
|
||||||
|
|
||||||
The application uses esbuild for TypeScript compilation. From the Application directory:
|
**IMPORTANT:** All TypeScript/frontend commands must be run from `PlanTempus.Application/` folder, not from the solution root.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
cd PlanTempus.Application
|
||||||
|
|
||||||
# Install npm dependencies
|
# Install npm dependencies
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
# Build TypeScript (requires custom build script setup)
|
# Build TypeScript
|
||||||
# Note: No npm scripts are currently defined in package.json
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
## Architecture Overview
|
## Architecture Overview
|
||||||
|
|
|
||||||
|
|
@ -496,160 +496,3 @@
|
||||||
</swp-card>
|
</swp-card>
|
||||||
</swp-page-container>
|
</swp-page-container>
|
||||||
</swp-tab-content>
|
</swp-tab-content>
|
||||||
|
|
||||||
@section Scripts {
|
|
||||||
<script type="module">
|
|
||||||
import { createChart } from '/lib/swp-charting/dist/swp-charting.js';
|
|
||||||
|
|
||||||
// Revenue bar chart
|
|
||||||
createChart(document.getElementById('revenueChart'), {
|
|
||||||
height: 240,
|
|
||||||
xAxis: {
|
|
||||||
categories: ['Feb', 'Mar', 'Apr', 'Maj', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dec', 'Jan']
|
|
||||||
},
|
|
||||||
yAxis: {
|
|
||||||
format: (v) => `${Math.round(v / 1000)}k`
|
|
||||||
},
|
|
||||||
series: [{
|
|
||||||
name: 'Omsætning',
|
|
||||||
color: '#00897b',
|
|
||||||
type: 'bar',
|
|
||||||
unit: ' kr',
|
|
||||||
data: [
|
|
||||||
{ x: 'Feb', y: 142500 },
|
|
||||||
{ x: 'Mar', y: 168200 },
|
|
||||||
{ x: 'Apr', y: 155800 },
|
|
||||||
{ x: 'Maj', y: 178400 },
|
|
||||||
{ x: 'Jun', y: 145600 },
|
|
||||||
{ x: 'Jul', y: 98200 },
|
|
||||||
{ x: 'Aug', y: 134500 },
|
|
||||||
{ x: 'Sep', y: 189300 },
|
|
||||||
{ x: 'Okt', y: 201400 },
|
|
||||||
{ x: 'Nov', y: 178900 },
|
|
||||||
{ x: 'Dec', y: 245600 },
|
|
||||||
{ x: 'Jan', y: 187230 }
|
|
||||||
]
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
|
|
||||||
// Payment methods pie chart
|
|
||||||
createChart(document.getElementById('paymentChart'), {
|
|
||||||
height: 240,
|
|
||||||
series: [
|
|
||||||
{ name: 'Kort', color: '#1976d2', type: 'pie', data: [{ x: '', y: 892400 }], unit: ' kr', pie: { innerRadius: 40, outerRadius: 90 } },
|
|
||||||
{ name: 'MobilePay', color: '#5C6BC0', type: 'pie', data: [{ x: '', y: 445200 }], unit: ' kr', pie: { innerRadius: 40, outerRadius: 90 } },
|
|
||||||
{ name: 'Kontant', color: '#43a047', type: 'pie', data: [{ x: '', y: 234800 }], unit: ' kr', pie: { innerRadius: 40, outerRadius: 90 } },
|
|
||||||
{ name: 'Faktura', color: '#f59e0b', type: 'pie', data: [{ x: '', y: 178500 }], unit: ' kr', pie: { innerRadius: 40, outerRadius: 90 } },
|
|
||||||
{ name: 'Fordelskort', color: '#8b5cf6', type: 'pie', data: [{ x: '', y: 74700 }], unit: ' kr', pie: { innerRadius: 40, outerRadius: 90 } }
|
|
||||||
],
|
|
||||||
tooltip: true,
|
|
||||||
legend: { position: 'right', align: 'center' }
|
|
||||||
});
|
|
||||||
|
|
||||||
// Hours per week grouped bar chart (Timerapport)
|
|
||||||
createChart(document.getElementById('hoursChart'), {
|
|
||||||
height: 240,
|
|
||||||
xAxis: { categories: ['Uge 48', 'Uge 49', 'Uge 50', 'Uge 51', 'Uge 52'] },
|
|
||||||
yAxis: { format: (v) => v + ' t' },
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
name: 'Anna Jensen',
|
|
||||||
color: '#00897b',
|
|
||||||
type: 'bar',
|
|
||||||
data: [
|
|
||||||
{ x: 'Uge 48', y: 32 },
|
|
||||||
{ x: 'Uge 49', y: 40 },
|
|
||||||
{ x: 'Uge 50', y: 38 },
|
|
||||||
{ x: 'Uge 51', y: 40 },
|
|
||||||
{ x: 'Uge 52', y: 20 }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Martin Nielsen',
|
|
||||||
color: '#3b82f6',
|
|
||||||
type: 'bar',
|
|
||||||
data: [
|
|
||||||
{ x: 'Uge 48', y: 30 },
|
|
||||||
{ x: 'Uge 49', y: 40 },
|
|
||||||
{ x: 'Uge 50', y: 35 },
|
|
||||||
{ x: 'Uge 51', y: 40 },
|
|
||||||
{ x: 'Uge 52', y: 16 }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Sofie Larsen',
|
|
||||||
color: '#8b5cf6',
|
|
||||||
type: 'bar',
|
|
||||||
data: [
|
|
||||||
{ x: 'Uge 48', y: 28 },
|
|
||||||
{ x: 'Uge 49', y: 36 },
|
|
||||||
{ x: 'Uge 50', y: 40 },
|
|
||||||
{ x: 'Uge 51', y: 40 },
|
|
||||||
{ x: 'Uge 52', y: 18 }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Peter Hansen',
|
|
||||||
color: '#f59e0b',
|
|
||||||
type: 'bar',
|
|
||||||
data: [
|
|
||||||
{ x: 'Uge 48', y: 34 },
|
|
||||||
{ x: 'Uge 49', y: 38 },
|
|
||||||
{ x: 'Uge 50', y: 32 },
|
|
||||||
{ x: 'Uge 51', y: 40 },
|
|
||||||
{ x: 'Uge 52', y: 14 }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
legend: { position: 'bottom', align: 'center', gap: 0 }
|
|
||||||
});
|
|
||||||
|
|
||||||
// Absence distribution pie chart (Timerapport)
|
|
||||||
createChart(document.getElementById('absenceChart'), {
|
|
||||||
height: 240,
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
name: 'Syg',
|
|
||||||
color: '#e53935',
|
|
||||||
type: 'pie',
|
|
||||||
unit: ' t',
|
|
||||||
data: [
|
|
||||||
{ x: 'Martin Nielsen', y: 8 },
|
|
||||||
{ x: 'Peter Hansen', y: 4 }
|
|
||||||
],
|
|
||||||
pie: { innerRadius: 25, outerRadius: 90 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Ferie',
|
|
||||||
color: '#f59e0b',
|
|
||||||
type: 'pie',
|
|
||||||
unit: ' t',
|
|
||||||
data: [
|
|
||||||
{ x: 'Anna Jensen', y: 4 },
|
|
||||||
{ x: 'Peter Hansen', y: 4 }
|
|
||||||
],
|
|
||||||
pie: { innerRadius: 25, outerRadius: 90 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Fri',
|
|
||||||
color: '#8b5cf6',
|
|
||||||
type: 'pie',
|
|
||||||
unit: ' t',
|
|
||||||
data: [
|
|
||||||
{ x: 'Sofie Larsen', y: 4 }
|
|
||||||
],
|
|
||||||
pie: { innerRadius: 25, outerRadius: 90 }
|
|
||||||
}
|
|
||||||
],
|
|
||||||
legend: { position: 'right', align: 'center' }
|
|
||||||
});
|
|
||||||
|
|
||||||
// Period selector functionality
|
|
||||||
document.querySelectorAll('swp-period-selector button').forEach(btn => {
|
|
||||||
btn.addEventListener('click', () => {
|
|
||||||
document.querySelectorAll('swp-period-selector button').forEach(b => b.classList.remove('active'));
|
|
||||||
btn.classList.add('active');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
import * as esbuild from 'esbuild';
|
|
||||||
|
|
||||||
async function build() {
|
|
||||||
try {
|
|
||||||
await esbuild.build({
|
|
||||||
entryPoints: ['wwwroot/ts/app.ts'],
|
|
||||||
bundle: true,
|
|
||||||
outfile: 'wwwroot/js/app.js',
|
|
||||||
format: 'esm',
|
|
||||||
sourcemap: 'inline',
|
|
||||||
target: 'es2020',
|
|
||||||
minify: false,
|
|
||||||
keepNames: true,
|
|
||||||
platform: 'browser'
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('App bundle created: wwwroot/js/app.js');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Build failed:', error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
build();
|
|
||||||
8
PlanTempus.Application/package-lock.json
generated
8
PlanTempus.Application/package-lock.json
generated
|
|
@ -5,7 +5,7 @@
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sevenweirdpeople/swp-charting": "^0.2.2",
|
"@sevenweirdpeople/swp-charting": "^0.2.5",
|
||||||
"fuse.js": "^7.1.0"
|
"fuse.js": "^7.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
@ -485,9 +485,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sevenweirdpeople/swp-charting": {
|
"node_modules/@sevenweirdpeople/swp-charting": {
|
||||||
"version": "0.2.2",
|
"version": "0.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/@sevenweirdpeople/swp-charting/-/swp-charting-0.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@sevenweirdpeople/swp-charting/-/swp-charting-0.2.5.tgz",
|
||||||
"integrity": "sha512-q9p7TOSMAq6I0t6jGEWpmjR7l2H8q8G0TnXbIpDutCz5a2JEqMDFe0NGBGcCwze2rvvRnRvCz8P2zGMQlHmphw==",
|
"integrity": "sha512-bQa5FtAXsTjjFxsE79sD1+A74R7f9YgVp5fC1fsiHoaLXmapDEO2dWuGX/MQ8rEChDZFyN1ZlkV+OLUs6qtfZw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/ansi-regex": {
|
"node_modules/ansi-regex": {
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,11 @@
|
||||||
"purgecss": "^6.0.0"
|
"purgecss": "^6.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"build": "esbuild wwwroot/ts/app.ts --bundle --format=esm --outfile=wwwroot/js/app.js --sourcemap=inline --target=es2020 --keep-names --platform=browser",
|
||||||
"analyze-css": "node analyze-css.js"
|
"analyze-css": "node analyze-css.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sevenweirdpeople/swp-charting": "^0.2.2",
|
"@sevenweirdpeople/swp-charting": "^0.2.5",
|
||||||
"fuse.js": "^7.1.0"
|
"fuse.js": "^7.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
48
PlanTempus.Application/wwwroot/data/reports-data.json
Normal file
48
PlanTempus.Application/wwwroot/data/reports-data.json
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
{
|
||||||
|
"revenue": {
|
||||||
|
"series": [{
|
||||||
|
"name": "Omsætning",
|
||||||
|
"color": "#00897b",
|
||||||
|
"type": "bar",
|
||||||
|
"unit": " kr",
|
||||||
|
"data": [
|
||||||
|
{ "x": "Feb", "y": 142500 },
|
||||||
|
{ "x": "Mar", "y": 168200 },
|
||||||
|
{ "x": "Apr", "y": 155800 },
|
||||||
|
{ "x": "Maj", "y": 178400 },
|
||||||
|
{ "x": "Jun", "y": 145600 },
|
||||||
|
{ "x": "Jul", "y": 98200 },
|
||||||
|
{ "x": "Aug", "y": 134500 },
|
||||||
|
{ "x": "Sep", "y": 189300 },
|
||||||
|
{ "x": "Okt", "y": 201400 },
|
||||||
|
{ "x": "Nov", "y": 178900 },
|
||||||
|
{ "x": "Dec", "y": 245600 },
|
||||||
|
{ "x": "Jan", "y": 187230 }
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
"payment": {
|
||||||
|
"series": [
|
||||||
|
{ "name": "Kort", "color": "#1976d2", "type": "pie", "data": [{ "x": "", "y": 892400 }], "unit": " kr", "pie": { "innerRadius": 40, "outerRadius": 90 } },
|
||||||
|
{ "name": "MobilePay", "color": "#5C6BC0", "type": "pie", "data": [{ "x": "", "y": 445200 }], "unit": " kr", "pie": { "innerRadius": 40, "outerRadius": 90 } },
|
||||||
|
{ "name": "Kontant", "color": "#43a047", "type": "pie", "data": [{ "x": "", "y": 234800 }], "unit": " kr", "pie": { "innerRadius": 40, "outerRadius": 90 } },
|
||||||
|
{ "name": "Faktura", "color": "#f59e0b", "type": "pie", "data": [{ "x": "", "y": 278500 }], "unit": " kr", "pie": { "innerRadius": 40, "outerRadius": 90 } },
|
||||||
|
{ "name": "Fordelskort", "color": "#8b5cf6", "type": "pie", "data": [{ "x": "", "y": 74700 }], "unit": " kr", "pie": { "innerRadius": 40, "outerRadius": 90 } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hours": {
|
||||||
|
"series": [
|
||||||
|
{ "name": "Anna Jensen", "color": "#00897b", "type": "bar", "data": [{ "x": "Uge 48", "y": 32 }, { "x": "Uge 49", "y": 40 }, { "x": "Uge 50", "y": 38 }, { "x": "Uge 51", "y": 40 }, { "x": "Uge 52", "y": 20 }] },
|
||||||
|
{ "name": "Martin Nielsen", "color": "#3b82f6", "type": "bar", "data": [{ "x": "Uge 48", "y": 30 }, { "x": "Uge 49", "y": 40 }, { "x": "Uge 50", "y": 35 }, { "x": "Uge 51", "y": 40 }, { "x": "Uge 52", "y": 16 }] },
|
||||||
|
{ "name": "Sofie Larsen", "color": "#8b5cf6", "type": "bar", "data": [{ "x": "Uge 48", "y": 28 }, { "x": "Uge 49", "y": 36 }, { "x": "Uge 50", "y": 40 }, { "x": "Uge 51", "y": 40 }, { "x": "Uge 52", "y": 18 }] },
|
||||||
|
{ "name": "Peter Hansen", "color": "#f59e0b", "type": "bar", "data": [{ "x": "Uge 48", "y": 34 }, { "x": "Uge 49", "y": 38 }, { "x": "Uge 50", "y": 32 }, { "x": "Uge 51", "y": 40 }, { "x": "Uge 52", "y": 14 }] }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"absence": {
|
||||||
|
"series": [
|
||||||
|
{ "name": "Syg", "color": "#e53935", "type": "pie", "unit": " t", "data": [{ "x": "Martin Nielsen", "y": 8 }, { "x": "Peter Hansen", "y": 4 }], "pie": { "innerRadius": 40, "outerRadius": 90 } },
|
||||||
|
{ "name": "Ferie", "color": "#f59e0b", "type": "pie", "unit": " t", "data": [{ "x": "Anna Jensen", "y": 4 }, { "x": "Peter Hansen", "y": 4 }], "pie": { "innerRadius": 40, "outerRadius": 90 } },
|
||||||
|
{ "name": "Fri", "color": "#8b5cf6", "type": "pie", "unit": " t", "data": [{ "x": "Sofie Larsen", "y": 4 }], "pie": { "innerRadius": 40, "outerRadius": 90 } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -62,7 +62,7 @@ let app: App;
|
||||||
function init(): void {
|
function init(): void {
|
||||||
app = new App();
|
app = new App();
|
||||||
|
|
||||||
// Expose to window for debugging
|
// Expose app to window for debugging
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
(window as unknown as { app: App }).app = app;
|
(window as unknown as { app: App }).app = app;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Fuse from 'fuse.js';
|
import Fuse from 'fuse.js';
|
||||||
|
import { createChart } from '@sevenweirdpeople/swp-charting';
|
||||||
|
|
||||||
interface SalesDataItem {
|
interface SalesDataItem {
|
||||||
index: number;
|
index: number;
|
||||||
|
|
@ -49,6 +50,31 @@ interface ChartSelectEvent extends CustomEvent {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DataPoint {
|
||||||
|
x: string;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SeriesConfig {
|
||||||
|
name: string;
|
||||||
|
color: string;
|
||||||
|
type: 'bar' | 'pie' | 'line';
|
||||||
|
data: DataPoint[];
|
||||||
|
unit?: string;
|
||||||
|
pie?: { innerRadius: number; outerRadius: number };
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChartDataConfig {
|
||||||
|
series: SeriesConfig[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ReportsData {
|
||||||
|
revenue: ChartDataConfig;
|
||||||
|
payment: ChartDataConfig;
|
||||||
|
hours: ChartDataConfig;
|
||||||
|
absence: ChartDataConfig;
|
||||||
|
}
|
||||||
|
|
||||||
export class ReportsController {
|
export class ReportsController {
|
||||||
private searchInput: HTMLInputElement | null = null;
|
private searchInput: HTMLInputElement | null = null;
|
||||||
private dateFromInput: HTMLInputElement | null = null;
|
private dateFromInput: HTMLInputElement | null = null;
|
||||||
|
|
@ -59,6 +85,17 @@ export class ReportsController {
|
||||||
private salesData: SalesDataItem[] = [];
|
private salesData: SalesDataItem[] = [];
|
||||||
private fuse: Fuse<SalesDataItem> | null = null;
|
private fuse: Fuse<SalesDataItem> | null = null;
|
||||||
|
|
||||||
|
// Chart references for lazy initialization
|
||||||
|
private revenueChart: ReturnType<typeof createChart> | null = null;
|
||||||
|
private paymentChart: ReturnType<typeof createChart> | null = null;
|
||||||
|
private hoursChart: ReturnType<typeof createChart> | null = null;
|
||||||
|
private absenceChart: ReturnType<typeof createChart> | null = null;
|
||||||
|
private salesChartsInitialized = false;
|
||||||
|
private hoursChartsInitialized = false;
|
||||||
|
|
||||||
|
// Chart data loaded from JSON
|
||||||
|
private chartData: ReportsData | null = null;
|
||||||
|
|
||||||
// Map pie chart series names to payment filter values
|
// Map pie chart series names to payment filter values
|
||||||
private readonly paymentMap: Record<string, string> = {
|
private readonly paymentMap: Record<string, string> = {
|
||||||
'Kort': 'card',
|
'Kort': 'card',
|
||||||
|
|
@ -116,7 +153,26 @@ export class ReportsController {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setupTabs();
|
this.setupTabs();
|
||||||
|
this.setupPeriodSelector();
|
||||||
this.setupChartEvents();
|
this.setupChartEvents();
|
||||||
|
|
||||||
|
// Load chart data from JSON and initialize charts
|
||||||
|
this.loadChartData().then(() => {
|
||||||
|
this.initializeSalesCharts();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load chart data from JSON file
|
||||||
|
*/
|
||||||
|
private async loadChartData(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/data/reports-data.json');
|
||||||
|
if (!response.ok) return;
|
||||||
|
this.chartData = await response.json() as ReportsData;
|
||||||
|
} catch {
|
||||||
|
console.error('Failed to load reports chart data');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -332,6 +388,21 @@ export class ReportsController {
|
||||||
statsRows.forEach(stats => {
|
statsRows.forEach(stats => {
|
||||||
stats.classList.toggle('active', stats.dataset.forTab === targetTab);
|
stats.classList.toggle('active', stats.dataset.forTab === targetTab);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Lazy-init charts for the active tab
|
||||||
|
if (targetTab === 'sales') {
|
||||||
|
if (this.chartData) {
|
||||||
|
this.initializeSalesCharts();
|
||||||
|
} else {
|
||||||
|
this.loadChartData().then(() => this.initializeSalesCharts());
|
||||||
|
}
|
||||||
|
} else if (targetTab === 'hours') {
|
||||||
|
if (this.chartData) {
|
||||||
|
this.initializeHoursCharts();
|
||||||
|
} else {
|
||||||
|
this.loadChartData().then(() => this.initializeHoursCharts());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -604,4 +675,120 @@ export class ReportsController {
|
||||||
// Apply filters to update the table
|
// Apply filters to update the table
|
||||||
this.applyAllFilters();
|
this.applyAllFilters();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize sales tab charts (lazy, only when visible)
|
||||||
|
*/
|
||||||
|
private initializeSalesCharts(): void {
|
||||||
|
if (this.salesChartsInitialized) return;
|
||||||
|
this.revenueChart = this.initRevenueChart();
|
||||||
|
this.paymentChart = this.initPaymentChart();
|
||||||
|
this.salesChartsInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize hours tab charts (lazy, only when visible)
|
||||||
|
*/
|
||||||
|
private initializeHoursCharts(): void {
|
||||||
|
if (this.hoursChartsInitialized) return;
|
||||||
|
this.hoursChart = this.initHoursChart();
|
||||||
|
this.absenceChart = this.initAbsenceChart();
|
||||||
|
this.hoursChartsInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize revenue bar chart (Salgsrapport)
|
||||||
|
*/
|
||||||
|
private initRevenueChart(): ReturnType<typeof createChart> | null {
|
||||||
|
const el = document.getElementById('revenueChart');
|
||||||
|
if (!el || !this.chartData?.revenue) return null;
|
||||||
|
|
||||||
|
const series = this.chartData.revenue.series;
|
||||||
|
if (series.length === 0) return null;
|
||||||
|
|
||||||
|
const categories = series[0].data.map(p => p.x);
|
||||||
|
|
||||||
|
return createChart(el, {
|
||||||
|
deferRender: true,
|
||||||
|
height: 240,
|
||||||
|
xAxis: { categories },
|
||||||
|
yAxis: {
|
||||||
|
format: (v: number) => `${Math.round(v / 1000)}k`
|
||||||
|
},
|
||||||
|
series: series
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize payment methods pie chart (Salgsrapport)
|
||||||
|
*/
|
||||||
|
private initPaymentChart(): ReturnType<typeof createChart> | null {
|
||||||
|
const el = document.getElementById('paymentChart');
|
||||||
|
if (!el || !this.chartData?.payment) return null;
|
||||||
|
|
||||||
|
const series = this.chartData.payment.series;
|
||||||
|
if (series.length === 0) return null;
|
||||||
|
|
||||||
|
return createChart(el, {
|
||||||
|
deferRender: true,
|
||||||
|
height: 240,
|
||||||
|
series: series,
|
||||||
|
tooltip: true,
|
||||||
|
legend: { position: 'right', align: 'center' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize hours per week bar chart (Timerapport)
|
||||||
|
*/
|
||||||
|
private initHoursChart(): ReturnType<typeof createChart> | null {
|
||||||
|
const el = document.getElementById('hoursChart');
|
||||||
|
if (!el || !this.chartData?.hours) return null;
|
||||||
|
|
||||||
|
const series = this.chartData.hours.series;
|
||||||
|
if (series.length === 0) return null;
|
||||||
|
|
||||||
|
// Extract categories from first series
|
||||||
|
const categories = series[0]?.data.map(p => p.x) || [];
|
||||||
|
|
||||||
|
return createChart(el, {
|
||||||
|
deferRender: true,
|
||||||
|
height: 240,
|
||||||
|
xAxis: { categories },
|
||||||
|
yAxis: { format: (v: number) => v + ' t' },
|
||||||
|
series: series,
|
||||||
|
legend: { position: 'bottom', align: 'center', gap: 0 }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize absence distribution pie chart (Timerapport)
|
||||||
|
*/
|
||||||
|
private initAbsenceChart(): ReturnType<typeof createChart> | null {
|
||||||
|
const el = document.getElementById('absenceChart');
|
||||||
|
if (!el || !this.chartData?.absence) return null;
|
||||||
|
|
||||||
|
const series = this.chartData.absence.series;
|
||||||
|
if (series.length === 0) return null;
|
||||||
|
|
||||||
|
return createChart(el, {
|
||||||
|
deferRender: true,
|
||||||
|
height: 240,
|
||||||
|
series: series,
|
||||||
|
legend: { position: 'right', align: 'center' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup period selector functionality (Timerapport)
|
||||||
|
*/
|
||||||
|
private setupPeriodSelector(): void {
|
||||||
|
const buttons = document.querySelectorAll<HTMLButtonElement>('swp-period-selector button');
|
||||||
|
buttons.forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
buttons.forEach(b => b.classList.remove('active'));
|
||||||
|
btn.classList.add('active');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
# PlanTempus
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
{
|
|
||||||
"sdk": {
|
|
||||||
"version": "9.0.0",
|
|
||||||
"rollForward": "latestMajor",
|
|
||||||
"allowPrerelease": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
29
qodana.yaml
29
qodana.yaml
|
|
@ -1,29 +0,0 @@
|
||||||
#-------------------------------------------------------------------------------#
|
|
||||||
# Qodana analysis is configured by qodana.yaml file #
|
|
||||||
# https://www.jetbrains.com/help/qodana/qodana-yaml.html #
|
|
||||||
#-------------------------------------------------------------------------------#
|
|
||||||
version: "1.0"
|
|
||||||
|
|
||||||
#Specify IDE code to run analysis without container (Applied in CI/CD pipeline)
|
|
||||||
ide: QDNET
|
|
||||||
|
|
||||||
#Specify inspection profile for code analysis
|
|
||||||
profile:
|
|
||||||
name: qodana.starter
|
|
||||||
|
|
||||||
#Enable inspections
|
|
||||||
#include:
|
|
||||||
# - name: <SomeEnabledInspectionId>
|
|
||||||
|
|
||||||
#Disable inspections
|
|
||||||
#exclude:
|
|
||||||
# - name: <SomeDisabledInspectionId>
|
|
||||||
# paths:
|
|
||||||
# - <path/where/not/run/inspection>
|
|
||||||
|
|
||||||
#Execute shell command before Qodana execution (Applied in CI/CD pipeline)
|
|
||||||
#bootstrap: sh ./prepare-qodana.sh
|
|
||||||
|
|
||||||
#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline)
|
|
||||||
#plugins:
|
|
||||||
# - id: <plugin.id> #(plugin id can be found at https://plugins.jetbrains.com)
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue