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)