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

@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"WebSearch"
]
}
}

View file

@ -1,35 +1,75 @@
@model PlanTempus.Application.Features.Employees.Components.EmployeeDetailStatsViewModel @model PlanTempus.Application.Features.Employees.Components.EmployeeDetailStatsViewModel
<swp-detail-grid> <swp-detail-grid class="stats-layout">
<swp-card> <!-- Stats Row (4 cards) -->
<swp-stats-row class="cols-4 full-width">
<swp-stat-card class="highlight">
<swp-stat-value id="statBookingsMonth">42</swp-stat-value>
<swp-stat-label>Bookinger denne måned</swp-stat-label>
</swp-stat-card>
<swp-stat-card>
<swp-stat-value id="statBookedValue">30.825 kr</swp-stat-value>
<swp-stat-label>Værdi af bookede services</swp-stat-label>
<swp-stat-subtitle id="statBookedSubtitle">Baseret på 49 bookinger</swp-stat-subtitle>
</swp-stat-card>
<swp-stat-card>
<swp-stat-value id="statRevenueMonth">28.450 kr</swp-stat-value>
<swp-stat-label>Omsætning denne måned</swp-stat-label>
</swp-stat-card>
<swp-stat-card>
<swp-stat-value id="statReturnCustomers">68%</swp-stat-value>
<swp-stat-label>Gengangere</swp-stat-label>
</swp-stat-card>
</swp-stats-row>
<!-- Revenue & Utilization Chart (dual-axis: bars + line) -->
<swp-card class="full-width">
<swp-card-header> <swp-card-header>
<swp-card-title>@Model.LabelPerformance</swp-card-title> <swp-card-title>Omsætning & Belægningsgrad</swp-card-title>
<span class="chart-hint">3 måneder bagud · 3 måneder frem</span>
</swp-card-header> </swp-card-header>
<swp-stats-row> <swp-chart-container id="employeeRevenueUtilizationChart" style="height: 300px;"></swp-chart-container>
<swp-stat-card class="teal">
<swp-stat-value>@Model.BookingsThisYear</swp-stat-value>
<swp-stat-label>@Model.LabelBookingsThisYear</swp-stat-label>
</swp-stat-card>
<swp-stat-card class="purple">
<swp-stat-value>@Model.RevenueThisYear</swp-stat-value>
<swp-stat-label>@Model.LabelRevenueThisYear</swp-stat-label>
</swp-stat-card>
<swp-stat-card class="amber">
<swp-stat-value>@Model.Rating</swp-stat-value>
<swp-stat-label>@Model.LabelAvgRating</swp-stat-label>
</swp-stat-card>
<swp-stat-card>
<swp-stat-value>87%</swp-stat-value>
<swp-stat-label>@Model.LabelOccupancy</swp-stat-label>
</swp-stat-card>
</swp-stats-row>
</swp-card> </swp-card>
<swp-card class="stats-bookings"> <!-- Two column layout: Revenue chart + Recent bookings -->
<swp-card>
<swp-card-header>
<swp-card-title>Omsætning (sidste 6 mdr)</swp-card-title>
<swp-chart-legend>
<swp-chart-legend-item>
<swp-chart-legend-dot class="services"></swp-chart-legend-dot>
<span>Services</span>
</swp-chart-legend-item>
<swp-chart-legend-item>
<swp-chart-legend-dot class="products"></swp-chart-legend-dot>
<span>Produkter</span>
</swp-chart-legend-item>
</swp-chart-legend>
</swp-card-header>
<swp-chart-container id="employeeRevenueChart" style="height: 220px;"></swp-chart-container>
</swp-card>
<swp-card class="recent-bookings">
<swp-card-header>
<swp-card-title>Seneste bookinger</swp-card-title>
</swp-card-header>
<swp-data-table id="recentBookingsTable">
<swp-data-table-header>
<swp-data-table-cell>Kunde</swp-data-table-cell>
<swp-data-table-cell>Service</swp-data-table-cell>
<swp-data-table-cell>Dato</swp-data-table-cell>
<swp-data-table-cell>Beløb</swp-data-table-cell>
</swp-data-table-header>
<!-- Rows populated by JavaScript -->
</swp-data-table>
</swp-card>
<!-- Completed Bookings Table -->
<swp-card class="stats-bookings full-width">
<swp-card-header> <swp-card-header>
<swp-card-title>@Model.LabelCompletedBookings</swp-card-title> <swp-card-title>@Model.LabelCompletedBookings</swp-card-title>
</swp-card-header> </swp-card-header>
<swp-data-table> <swp-data-table id="completedBookingsTable">
<swp-data-table-header> <swp-data-table-header>
<swp-data-table-cell>@Model.LabelDate</swp-data-table-cell> <swp-data-table-cell>@Model.LabelDate</swp-data-table-cell>
<swp-data-table-cell>@Model.LabelTime</swp-data-table-cell> <swp-data-table-cell>@Model.LabelTime</swp-data-table-cell>
@ -39,20 +79,7 @@
<swp-data-table-cell>@Model.LabelAmount</swp-data-table-cell> <swp-data-table-cell>@Model.LabelAmount</swp-data-table-cell>
<swp-data-table-cell>@Model.LabelStatus</swp-data-table-cell> <swp-data-table-cell>@Model.LabelStatus</swp-data-table-cell>
</swp-data-table-header> </swp-data-table-header>
@foreach (var booking in Model.CompletedBookings) <!-- Rows populated by JavaScript -->
{
<swp-data-table-row>
<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>
</swp-data-table-row>
}
</swp-data-table> </swp-data-table>
</swp-card> </swp-card>
</swp-detail-grid> </swp-detail-grid>

View file

@ -79,20 +79,20 @@
<swp-page-container> <swp-page-container>
<!-- Charts Grid --> <!-- Charts Grid -->
<swp-charts-grid> <swp-charts-grid>
<swp-chart-card> <swp-card>
<swp-chart-header> <swp-card-header>
<swp-chart-title>Omsætning pr. måned</swp-chart-title> <swp-card-title>Omsætning pr. måned</swp-card-title>
<swp-chart-hint>Sidste 12 måneder</swp-chart-hint> <span class="chart-hint">Sidste 12 måneder</span>
</swp-chart-header> </swp-card-header>
<swp-chart-container id="revenueChart"></swp-chart-container> <swp-chart-container id="revenueChart"></swp-chart-container>
</swp-chart-card> </swp-card>
<swp-chart-card> <swp-card>
<swp-chart-header> <swp-card-header>
<swp-chart-title>Betalingsmetoder</swp-chart-title> <swp-card-title>Betalingsmetoder</swp-card-title>
<swp-chart-hint>Fordeling</swp-chart-hint> <span class="chart-hint">Fordeling</span>
</swp-chart-header> </swp-card-header>
<swp-chart-container id="paymentChart"></swp-chart-container> <swp-chart-container id="paymentChart"></swp-chart-container>
</swp-chart-card> </swp-card>
</swp-charts-grid> </swp-charts-grid>
<!-- Filter Bar --> <!-- Filter Bar -->
@ -415,20 +415,20 @@
<!-- Charts Grid --> <!-- Charts Grid -->
<swp-charts-grid> <swp-charts-grid>
<swp-chart-card> <swp-card>
<swp-chart-header> <swp-card-header>
<swp-chart-title>Timer pr. uge</swp-chart-title> <swp-card-title>Timer pr. uge</swp-card-title>
<swp-chart-hint>Sidste 5 uger</swp-chart-hint> <span class="chart-hint">Sidste 5 uger</span>
</swp-chart-header> </swp-card-header>
<swp-chart-container id="hoursChart"></swp-chart-container> <swp-chart-container id="hoursChart"></swp-chart-container>
</swp-chart-card> </swp-card>
<swp-chart-card> <swp-card>
<swp-chart-header> <swp-card-header>
<swp-chart-title>Fraværsfordeling</swp-chart-title> <swp-card-title>Fraværsfordeling</swp-card-title>
<swp-chart-hint>Efter type</swp-chart-hint> <span class="chart-hint">Efter type</span>
</swp-chart-header> </swp-card-header>
<swp-chart-container id="absenceChart"></swp-chart-container> <swp-chart-container id="absenceChart"></swp-chart-container>
</swp-chart-card> </swp-card>
</swp-charts-grid> </swp-charts-grid>
<!-- Hours Table --> <!-- Hours Table -->

View file

@ -564,6 +564,40 @@ swp-data-row.focus-highlight {
} }
} }
/* ===========================================
CHART SUBTITLE (for employee stats)
=========================================== */
swp-chart-subtitle {
display: block;
font-size: var(--font-size-sm);
color: var(--color-text-muted);
margin-top: var(--spacing-1);
}
/* ===========================================
RECENT BOOKINGS TABLE
=========================================== */
swp-card.recent-bookings swp-data-table {
grid-template-columns: minmax(100px, 1fr) minmax(80px, 1fr) 90px 80px;
}
swp-card.recent-bookings swp-data-table-row swp-data-table-cell {
&:first-child {
font-weight: var(--font-weight-medium);
}
&:nth-child(3) {
font-size: var(--font-size-sm);
color: var(--color-text-secondary);
}
&:last-child {
font-family: var(--font-mono);
font-size: var(--font-size-sm);
text-align: right;
}
}
/* =========================================== /* ===========================================
STATS BOOKINGS TABLE STATS BOOKINGS TABLE
=========================================== */ =========================================== */

View file

@ -55,7 +55,7 @@ swp-chart-hint {
swp-chart-container { swp-chart-container {
display: block; display: block;
height: 240px; height: 270px;
position: relative; position: relative;
} }

View file

@ -20,17 +20,11 @@ swp-stats-row {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
gap: var(--card-gap); gap: var(--card-gap);
margin-bottom: var(--section-gap);
&.cols-4 { &.cols-4 {
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(4, 1fr);
} }
/* Reset margin when stat-card is inside stats-row */
& swp-stat-card {
margin-top: 0;
}
/* Tab-based visibility for multi-stat rows */ /* Tab-based visibility for multi-stat rows */
&[data-for-tab]:not(.active) { &[data-for-tab]:not(.active) {
display: none; display: none;
@ -46,7 +40,6 @@ swp-stat-card {
background: var(--color-surface); background: var(--color-surface);
border-radius: var(--border-radius-lg); border-radius: var(--border-radius-lg);
padding: var(--card-padding); padding: var(--card-padding);
margin-top: var(--section-gap);
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
} }
@ -239,7 +232,6 @@ swp-quick-stat {
padding: var(--card-padding); padding: var(--card-padding);
background: var(--color-background-alt); background: var(--color-background-alt);
border-radius: var(--radius-md); border-radius: var(--radius-md);
border: 1px solid var(--color-border);
} }
swp-quick-stat swp-stat-value { swp-quick-stat swp-stat-value {

View file

@ -0,0 +1,94 @@
{
"stats": {
"bookingsThisMonth": 42,
"bookedServicesValue": "30.825 kr",
"bookedServicesSubtitle": "Baseret på 49 bookinger",
"revenueThisMonth": "28.450 kr",
"returnCustomers": "68%"
},
"revenueUtilization": {
"categories": ["Okt", "Nov", "Dec", "Jan", "Feb", "Mar"],
"actual": {
"revenue": [
{ "x": "Okt", "y": 24500 },
{ "x": "Nov", "y": 28200 },
{ "x": "Dec", "y": 31800 },
{ "x": "Jan", "y": 28450 },
{ "x": "Feb", "y": null },
{ "x": "Mar", "y": null }
],
"utilization": [
{ "x": "Okt", "y": 72 },
{ "x": "Nov", "y": 78 },
{ "x": "Dec", "y": 85 },
{ "x": "Jan", "y": 68 },
{ "x": "Feb", "y": null },
{ "x": "Mar", "y": null }
]
},
"forecast": {
"revenue": [
{ "x": "Okt", "y": null },
{ "x": "Nov", "y": null },
{ "x": "Dec", "y": null },
{ "x": "Jan", "y": null },
{ "x": "Feb", "y": 26000 },
{ "x": "Mar", "y": 29500 }
],
"utilization": [
{ "x": "Okt", "y": null },
{ "x": "Nov", "y": null },
{ "x": "Dec", "y": null },
{ "x": "Jan", "y": null },
{ "x": "Feb", "y": 65 },
{ "x": "Mar", "y": 72 }
]
}
},
"revenue": {
"categories": ["Jul", "Aug", "Sep", "Okt", "Nov", "Dec"],
"series": [
{
"name": "Services",
"color": "#00897b",
"data": [
{ "x": "Jul", "y": 22000 },
{ "x": "Aug", "y": 28500 },
{ "x": "Sep", "y": 26200 },
{ "x": "Okt", "y": 30500 },
{ "x": "Nov", "y": 27100 },
{ "x": "Dec", "y": 26000 }
]
},
{
"name": "Produkter",
"color": "#1976d2",
"data": [
{ "x": "Jul", "y": 2500 },
{ "x": "Aug", "y": 2700 },
{ "x": "Sep", "y": 2600 },
{ "x": "Okt", "y": 2600 },
{ "x": "Nov", "y": 2500 },
{ "x": "Dec", "y": 2450 }
]
}
]
},
"recentBookings": [
{ "customer": "Maria Hansen", "service": "Klip & Farve", "date": "23. dec 2024", "amount": "995 kr" },
{ "customer": "Louise Nielsen", "service": "Balayage", "date": "22. dec 2024", "amount": "1.495 kr" },
{ "customer": "Sofie Andersen", "service": "Dameklip", "date": "22. dec 2024", "amount": "425 kr" },
{ "customer": "Karen Pedersen", "service": "Klip & Farve", "date": "21. dec 2024", "amount": "1.095 kr" },
{ "customer": "Emma Larsen", "service": "Olaplex", "date": "21. dec 2024", "amount": "350 kr" }
],
"completedBookings": [
{ "date": "23. dec 2024", "time": "10:00", "customer": "Maria Hansen", "services": "Dameklip, Bundfarve", "duration": "2t 30m", "amount": "1.510 kr", "status": "Betalt", "statusClass": "paid" },
{ "date": "23. dec 2024", "time": "13:30", "customer": "Louise Nielsen", "services": "Balayage langt hår, Olaplex", "duration": "3t", "amount": "2.700 kr", "status": "Betalt", "statusClass": "paid" },
{ "date": "22. dec 2024", "time": "09:00", "customer": "Sofie Andersen", "services": "Dameklip", "duration": "1t", "amount": "725 kr", "status": "Betalt", "statusClass": "paid" },
{ "date": "22. dec 2024", "time": "11:00", "customer": "Karen Pedersen", "services": "Striber mellemlangt hår, Klip", "duration": "2t 30m", "amount": "2.390 kr", "status": "Afventer", "statusClass": "pending" },
{ "date": "21. dec 2024", "time": "14:00", "customer": "Emma Larsen", "services": "Olaplex Stand alone", "duration": "1t", "amount": "550 kr", "status": "Betalt", "statusClass": "paid" },
{ "date": "21. dec 2024", "time": "10:00", "customer": "Mette Kristensen", "services": "Herreklip", "duration": "1t", "amount": "645 kr", "status": "Betalt", "statusClass": "paid" },
{ "date": "20. dec 2024", "time": "09:30", "customer": "Anne Thomsen", "services": "Glossing mellemlangt hår", "duration": "1t", "amount": "745 kr", "status": "Betalt", "statusClass": "paid" },
{ "date": "20. dec 2024", "time": "12:00", "customer": "Lise Mortensen", "services": "Dameklip, Farvning vipper & bryn", "duration": "1t 30m", "amount": "1.070 kr", "status": "Betalt", "statusClass": "paid" }
]
}

View file

@ -1,3 +1,5 @@
import { createChart } from '@sevenweirdpeople/swp-charting';
/** /**
* Employees Controller * Employees Controller
* *
@ -6,9 +8,70 @@
* Uses History API for browser back/forward navigation. * 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 { export class EmployeesController {
private ratesSync: RatesSyncController | null = null; private ratesSync: RatesSyncController | null = null;
private scheduleController: ScheduleController | null = null; private scheduleController: ScheduleController | null = null;
private statsController: EmployeeStatsController | null = null;
private listView: HTMLElement | null = null; private listView: HTMLElement | null = null;
private detailView: HTMLElement | null = null; private detailView: HTMLElement | null = null;
@ -27,6 +90,7 @@ export class EmployeesController {
this.restoreStateFromUrl(); this.restoreStateFromUrl();
this.ratesSync = new RatesSyncController(); this.ratesSync = new RatesSyncController();
this.scheduleController = new ScheduleController(); 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);
});
}
}