Enhances employee statistics page with rich dashboard

Refactors employee statistics view with comprehensive charts and tables
Adds detailed revenue, utilization, and booking tracking components
Introduces dynamic data loading and chart visualization for employee performance
This commit is contained in:
Janus C. H. Knudsen 2026-01-22 23:28:33 +01:00
parent b921e26e48
commit eaae745c42
8 changed files with 543 additions and 70 deletions

View file

@ -1,3 +1,5 @@
import { createChart } from '@sevenweirdpeople/swp-charting';
/**
* Employees Controller
*
@ -6,9 +8,70 @@
* Uses History API for browser back/forward navigation.
*/
interface DataPoint {
x: string;
y: number | null;
}
interface RevenueSeriesConfig {
name: string;
color: string;
data: DataPoint[];
}
interface RevenueChartData {
categories: string[];
series: RevenueSeriesConfig[];
}
interface RevenueUtilizationData {
categories: string[];
actual: {
revenue: DataPoint[];
utilization: DataPoint[];
};
forecast: {
revenue: DataPoint[];
utilization: DataPoint[];
};
}
interface RecentBooking {
customer: string;
service: string;
date: string;
amount: string;
}
interface CompletedBooking {
date: string;
time: string;
customer: string;
services: string;
duration: string;
amount: string;
status: string;
statusClass: string;
}
interface EmployeeStatsData {
stats: {
bookingsThisMonth: number;
bookedServicesValue: string;
bookedServicesSubtitle: string;
revenueThisMonth: string;
returnCustomers: string;
};
revenueUtilization: RevenueUtilizationData;
revenue: RevenueChartData;
recentBookings: RecentBooking[];
completedBookings: CompletedBooking[];
}
export class EmployeesController {
private ratesSync: RatesSyncController | null = null;
private scheduleController: ScheduleController | null = null;
private statsController: EmployeeStatsController | null = null;
private listView: HTMLElement | null = null;
private detailView: HTMLElement | null = null;
@ -27,6 +90,7 @@ export class EmployeesController {
this.restoreStateFromUrl();
this.ratesSync = new RatesSyncController();
this.scheduleController = new ScheduleController();
this.statsController = new EmployeeStatsController();
}
/**
@ -1058,3 +1122,258 @@ class ScheduleController {
}
}
}
/**
* Employee Stats Controller
*
* Handles the Statistics tab:
* - Loads data from JSON
* - Initializes charts
* - Populates tables
*/
class EmployeeStatsController {
private data: EmployeeStatsData | null = null;
private dataPromise: Promise<void> | null = null;
private chartsInitialized = false;
private revenueUtilizationChart: ReturnType<typeof createChart> | null = null;
private revenueChart: ReturnType<typeof createChart> | null = null;
constructor() {
this.setupTabListener();
}
/**
* Listen for stats tab activation
*/
private setupTabListener(): void {
document.addEventListener('click', (e: Event) => {
const target = e.target as HTMLElement;
const tab = target.closest<HTMLElement>('swp-tab[data-tab="stats"]');
if (tab) {
this.initializeStats();
}
});
}
/**
* Initialize stats when tab is shown
*/
private initializeStats(): void {
this.loadData().then(() => {
if (!this.chartsInitialized) {
this.populateStats();
this.initializeCharts();
this.populateTables();
this.chartsInitialized = true;
}
});
}
/**
* Load data from JSON file (cached)
*/
private loadData(): Promise<void> {
if (this.dataPromise) return this.dataPromise;
this.dataPromise = (async () => {
try {
const response = await fetch('/data/employee-stats-data.json');
if (!response.ok) return;
this.data = await response.json() as EmployeeStatsData;
} catch {
console.error('Failed to load employee stats data');
}
})();
return this.dataPromise;
}
/**
* Populate stat card values
*/
private populateStats(): void {
if (!this.data) return;
const { stats } = this.data;
const bookingsEl = document.getElementById('statBookingsMonth');
const bookedValueEl = document.getElementById('statBookedValue');
const bookedSubtitleEl = document.getElementById('statBookedSubtitle');
const revenueEl = document.getElementById('statRevenueMonth');
const returnEl = document.getElementById('statReturnCustomers');
if (bookingsEl) bookingsEl.textContent = String(stats.bookingsThisMonth);
if (bookedValueEl) bookedValueEl.textContent = stats.bookedServicesValue;
if (bookedSubtitleEl) bookedSubtitleEl.textContent = stats.bookedServicesSubtitle;
if (revenueEl) revenueEl.textContent = stats.revenueThisMonth;
if (returnEl) returnEl.textContent = stats.returnCustomers;
}
/**
* Initialize charts
*/
private initializeCharts(): void {
if (!this.data) return;
this.revenueUtilizationChart = this.initRevenueUtilizationChart();
this.revenueChart = this.initRevenueChart();
}
/**
* Initialize revenue & utilization chart (dual-axis: bars + lines)
*/
private initRevenueUtilizationChart(): ReturnType<typeof createChart> | null {
const el = document.getElementById('employeeRevenueUtilizationChart');
if (!el || !this.data?.revenueUtilization) return null;
const data = this.data.revenueUtilization;
return createChart(el, {
height: 300,
xAxis: { categories: data.categories },
yAxis: [
{ min: 0, max: 50000, format: (v: number) => `${(v / 1000).toFixed(0)}k` }, // Left: Revenue
{ min: 0, max: 100, format: (v: number) => `${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
}
],
legend: false
});
}
/**
* Initialize revenue chart (line chart with area gradient)
*/
private initRevenueChart(): ReturnType<typeof createChart> | null {
const el = document.getElementById('employeeRevenueChart');
if (!el || !this.data?.revenue) return null;
const data = this.data.revenue;
return createChart(el, {
height: 200,
xAxis: { categories: data.categories },
yAxis: { format: (v: number) => `${(v / 1000).toFixed(0)}k` },
series: data.series.map(s => ({
name: s.name,
color: s.color,
data: s.data,
showArea: true,
area: { gradient: { startOpacity: 0.3, endOpacity: 0.05 } }
})),
legend: false
});
}
/**
* Populate tables with data
*/
private populateTables(): void {
if (!this.data) return;
this.populateRecentBookings();
this.populateCompletedBookings();
}
/**
* Populate recent bookings table
*/
private populateRecentBookings(): void {
if (!this.data) return;
const table = document.getElementById('recentBookingsTable');
if (!table) return;
// Remove existing rows (keep header)
const existingRows = table.querySelectorAll('swp-data-table-row');
existingRows.forEach(row => row.remove());
// Add new rows
this.data.recentBookings.forEach(booking => {
const row = document.createElement('swp-data-table-row');
row.innerHTML = `
<swp-data-table-cell>${booking.customer}</swp-data-table-cell>
<swp-data-table-cell>${booking.service}</swp-data-table-cell>
<swp-data-table-cell>${booking.date}</swp-data-table-cell>
<swp-data-table-cell>${booking.amount}</swp-data-table-cell>
`;
table.appendChild(row);
});
}
/**
* Populate completed bookings table
*/
private populateCompletedBookings(): void {
if (!this.data) return;
const table = document.getElementById('completedBookingsTable');
if (!table) return;
// Remove existing rows (keep header)
const existingRows = table.querySelectorAll('swp-data-table-row');
existingRows.forEach(row => row.remove());
// Add new rows
this.data.completedBookings.forEach(booking => {
const row = document.createElement('swp-data-table-row');
row.innerHTML = `
<swp-data-table-cell>${booking.date}</swp-data-table-cell>
<swp-data-table-cell>${booking.time}</swp-data-table-cell>
<swp-data-table-cell>${booking.customer}</swp-data-table-cell>
<swp-data-table-cell>${booking.services}</swp-data-table-cell>
<swp-data-table-cell>${booking.duration}</swp-data-table-cell>
<swp-data-table-cell>${booking.amount}</swp-data-table-cell>
<swp-data-table-cell>
<swp-status-badge class="${booking.statusClass}">${booking.status}</swp-status-badge>
</swp-data-table-cell>
`;
table.appendChild(row);
});
}
}