From b921e26e483ea14c2778dc4961831e77164d024a Mon Sep 17 00:00:00 2001 From: "Janus C. H. Knudsen" Date: Thu, 22 Jan 2026 16:32:46 +0100 Subject: [PATCH] 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 --- .claude/settings.local.json | 3 +- CLAUDE.md | 38 +--- .../Features/Reports/Pages/Index.cshtml | 157 --------------- PlanTempus.Application/build.js | 24 --- PlanTempus.Application/package-lock.json | 8 +- PlanTempus.Application/package.json | 3 +- .../wwwroot/data/reports-data.json | 48 +++++ PlanTempus.Application/wwwroot/ts/app.ts | 2 +- .../wwwroot/ts/modules/reports.ts | 187 ++++++++++++++++++ README.md | 1 - global.json | 7 - qodana.yaml | 29 --- 12 files changed, 249 insertions(+), 258 deletions(-) delete mode 100644 PlanTempus.Application/build.js create mode 100644 PlanTempus.Application/wwwroot/data/reports-data.json delete mode 100644 README.md delete mode 100644 global.json delete mode 100644 qodana.yaml diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 4568674..a1ab67c 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -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\\\\Calendar\\\\wwwroot\\\\poc*.html\")", "Bash(Get-ChildItem:*)", - "Bash(Select-Object -ExpandProperty FullName)" + "Bash(Select-Object -ExpandProperty FullName)", + "Bash(npm run build:*)" ] } } diff --git a/CLAUDE.md b/CLAUDE.md index 0a06e64..b61619d 100644 --- a/CLAUDE.md +++ b/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 -### 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 -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 +cd PlanTempus.Application + # Install npm dependencies npm install -# Build TypeScript (requires custom build script setup) -# Note: No npm scripts are currently defined in package.json +# Build TypeScript +npm run build ``` ## Architecture Overview diff --git a/PlanTempus.Application/Features/Reports/Pages/Index.cshtml b/PlanTempus.Application/Features/Reports/Pages/Index.cshtml index 74c5f36..7c11b11 100644 --- a/PlanTempus.Application/Features/Reports/Pages/Index.cshtml +++ b/PlanTempus.Application/Features/Reports/Pages/Index.cshtml @@ -496,160 +496,3 @@ - -@section Scripts { - -} diff --git a/PlanTempus.Application/build.js b/PlanTempus.Application/build.js deleted file mode 100644 index 4aa944e..0000000 --- a/PlanTempus.Application/build.js +++ /dev/null @@ -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(); diff --git a/PlanTempus.Application/package-lock.json b/PlanTempus.Application/package-lock.json index e546fd1..603c53b 100644 --- a/PlanTempus.Application/package-lock.json +++ b/PlanTempus.Application/package-lock.json @@ -5,7 +5,7 @@ "packages": { "": { "dependencies": { - "@sevenweirdpeople/swp-charting": "^0.2.2", + "@sevenweirdpeople/swp-charting": "^0.2.5", "fuse.js": "^7.1.0" }, "devDependencies": { @@ -485,9 +485,9 @@ } }, "node_modules/@sevenweirdpeople/swp-charting": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@sevenweirdpeople/swp-charting/-/swp-charting-0.2.2.tgz", - "integrity": "sha512-q9p7TOSMAq6I0t6jGEWpmjR7l2H8q8G0TnXbIpDutCz5a2JEqMDFe0NGBGcCwze2rvvRnRvCz8P2zGMQlHmphw==", + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@sevenweirdpeople/swp-charting/-/swp-charting-0.2.5.tgz", + "integrity": "sha512-bQa5FtAXsTjjFxsE79sD1+A74R7f9YgVp5fC1fsiHoaLXmapDEO2dWuGX/MQ8rEChDZFyN1ZlkV+OLUs6qtfZw==", "license": "MIT" }, "node_modules/ansi-regex": { diff --git a/PlanTempus.Application/package.json b/PlanTempus.Application/package.json index ce08276..97b5fd0 100644 --- a/PlanTempus.Application/package.json +++ b/PlanTempus.Application/package.json @@ -5,10 +5,11 @@ "purgecss": "^6.0.0" }, "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" }, "dependencies": { - "@sevenweirdpeople/swp-charting": "^0.2.2", + "@sevenweirdpeople/swp-charting": "^0.2.5", "fuse.js": "^7.1.0" } } diff --git a/PlanTempus.Application/wwwroot/data/reports-data.json b/PlanTempus.Application/wwwroot/data/reports-data.json new file mode 100644 index 0000000..d9a36c5 --- /dev/null +++ b/PlanTempus.Application/wwwroot/data/reports-data.json @@ -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 } } + ] + } +} diff --git a/PlanTempus.Application/wwwroot/ts/app.ts b/PlanTempus.Application/wwwroot/ts/app.ts index 0b05786..db2d6f7 100644 --- a/PlanTempus.Application/wwwroot/ts/app.ts +++ b/PlanTempus.Application/wwwroot/ts/app.ts @@ -62,7 +62,7 @@ let app: App; function init(): void { app = new App(); - // Expose to window for debugging + // Expose app to window for debugging if (typeof window !== 'undefined') { (window as unknown as { app: App }).app = app; } diff --git a/PlanTempus.Application/wwwroot/ts/modules/reports.ts b/PlanTempus.Application/wwwroot/ts/modules/reports.ts index 0f12a5b..3c5f29e 100644 --- a/PlanTempus.Application/wwwroot/ts/modules/reports.ts +++ b/PlanTempus.Application/wwwroot/ts/modules/reports.ts @@ -6,6 +6,7 @@ */ import Fuse from 'fuse.js'; +import { createChart } from '@sevenweirdpeople/swp-charting'; interface SalesDataItem { 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 { private searchInput: HTMLInputElement | null = null; private dateFromInput: HTMLInputElement | null = null; @@ -59,6 +85,17 @@ export class ReportsController { private salesData: SalesDataItem[] = []; private fuse: Fuse | null = null; + // Chart references for lazy initialization + private revenueChart: ReturnType | null = null; + private paymentChart: ReturnType | null = null; + private hoursChart: ReturnType | null = null; + private absenceChart: ReturnType | 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 private readonly paymentMap: Record = { 'Kort': 'card', @@ -116,7 +153,26 @@ export class ReportsController { } this.setupTabs(); + this.setupPeriodSelector(); 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 { + 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 => { 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 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 | 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 | 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 | 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 | 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('swp-period-selector button'); + buttons.forEach(btn => { + btn.addEventListener('click', () => { + buttons.forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + }); + }); + } } diff --git a/README.md b/README.md deleted file mode 100644 index 03e1d4a..0000000 --- a/README.md +++ /dev/null @@ -1 +0,0 @@ -# PlanTempus diff --git a/global.json b/global.json deleted file mode 100644 index f4fd385..0000000 --- a/global.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "sdk": { - "version": "9.0.0", - "rollForward": "latestMajor", - "allowPrerelease": true - } -} \ No newline at end of file diff --git a/qodana.yaml b/qodana.yaml deleted file mode 100644 index 178e06f..0000000 --- a/qodana.yaml +++ /dev/null @@ -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: - -#Disable inspections -#exclude: -# - name: -# paths: -# - - -#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 can be found at https://plugins.jetbrains.com)