PlanTempusApp/PlanTempus.Application/Features/Reports/Pages/Index.cshtml

623 lines
29 KiB
Text
Raw Normal View History

@page "/rapporter"
@model PlanTempus.Application.Features.Reports.Pages.IndexModel
@{
ViewData["Title"] = "Statistik og Rapporter";
}
<!-- Sticky Header with Tabs -->
<swp-sticky-header>
<swp-header-content>
<swp-page-header>
<swp-page-title>
<i class="ph ph-chart-line-up"></i>
<span>Statistik og Rapporter</span>
</swp-page-title>
<swp-page-actions>
<swp-btn class="secondary" id="exportBtn">
<i class="ph ph-export"></i>
Eksporter
</swp-btn>
</swp-page-actions>
</swp-page-header>
</swp-header-content>
<!-- Tab Bar -->
<swp-tab-bar>
<swp-tab class="active" data-tab="sales">
<i class="ph ph-receipt"></i>
<span>Salgsrapport</span>
</swp-tab>
<swp-tab data-tab="hours">
<i class="ph ph-clock"></i>
<span>Timerapport</span>
</swp-tab>
</swp-tab-bar>
</swp-sticky-header>
<!-- Tab Content: Salgsrapport -->
<swp-tab-content data-tab="sales" class="active">
<swp-page-container>
<!-- Stats Bar -->
<swp-stats-row class="cols-4">
<swp-stat-card class="highlight">
<swp-stat-value>12.450 kr</swp-stat-value>
<swp-stat-label>Omsætning i dag</swp-stat-label>
</swp-stat-card>
<swp-stat-card class="success">
<swp-stat-value>187.230 kr</swp-stat-value>
<swp-stat-label>Omsætning denne måned</swp-stat-label>
</swp-stat-card>
<swp-stat-card>
<swp-stat-value>18</swp-stat-value>
<swp-stat-label>Antal salg i dag</swp-stat-label>
</swp-stat-card>
<swp-stat-card>
<swp-stat-value>692 kr</swp-stat-value>
<swp-stat-label>Gns. ordreværdi</swp-stat-label>
</swp-stat-card>
</swp-stats-row>
<!-- Charts Grid -->
<swp-charts-grid>
<swp-chart-card>
<swp-chart-header>
<swp-chart-title>Omsætning pr. måned</swp-chart-title>
<swp-chart-hint>Sidste 12 måneder</swp-chart-hint>
</swp-chart-header>
<swp-chart-container id="revenueChart"></swp-chart-container>
</swp-chart-card>
<swp-chart-card>
<swp-chart-header>
<swp-chart-title>Betalingsmetoder</swp-chart-title>
<swp-chart-hint>Fordeling</swp-chart-hint>
</swp-chart-header>
<swp-chart-container id="paymentChart"></swp-chart-container>
</swp-chart-card>
</swp-charts-grid>
<!-- Filter Bar -->
<swp-filter-bar>
<swp-search-input>
<i class="ph ph-magnifying-glass"></i>
<input type="search" id="searchInput" placeholder="Søg fakturanr, kunde, medarbejder..." />
</swp-search-input>
<swp-filter-group>
<swp-filter-label>Fra</swp-filter-label>
<input type="date" id="dateFrom" value="2025-01-01" />
</swp-filter-group>
<swp-filter-group>
<swp-filter-label>Til</swp-filter-label>
<input type="date" id="dateTo" value="2025-01-06" />
</swp-filter-group>
<swp-filter-group>
<swp-filter-label>Status</swp-filter-label>
<select id="statusFilter">
<option value="">Alle</option>
<option value="paid">Betalt</option>
<option value="pending">Afventer</option>
<option value="credited">Krediteret</option>
</select>
</swp-filter-group>
<swp-filter-group>
<swp-filter-label>Betaling</swp-filter-label>
<select id="paymentFilter">
<option value="">Alle</option>
<option value="card">Kort</option>
<option value="cash">Kontant</option>
<option value="mobilepay">MobilePay</option>
<option value="invoice">Faktura</option>
<option value="giftcard">Fordelskort</option>
</select>
</swp-filter-group>
</swp-filter-bar>
<!-- Sales Table -->
<swp-card class="sales-table">
<swp-data-table>
<swp-data-table-header>
<swp-data-table-cell>Faktura</swp-data-table-cell>
<swp-data-table-cell>Dato/tid</swp-data-table-cell>
<swp-data-table-cell>Kunde</swp-data-table-cell>
<swp-data-table-cell>Medarbejder</swp-data-table-cell>
<swp-data-table-cell>Ydelser</swp-data-table-cell>
<swp-data-table-cell class="right">Beløb</swp-data-table-cell>
<swp-data-table-cell>Betaling</swp-data-table-cell>
<swp-data-table-cell>Status</swp-data-table-cell>
<swp-data-table-cell></swp-data-table-cell>
</swp-data-table-header>
<!-- Row 1 -->
<swp-data-table-row>
<swp-data-table-cell>
<swp-invoice-cell>#1847</swp-invoice-cell>
</swp-data-table-cell>
<swp-data-table-cell>
<swp-datetime-cell>
<span class="date">6. jan 2025</span>
<span class="time">14:32</span>
</swp-datetime-cell>
</swp-data-table-cell>
<swp-data-table-cell>
<swp-customer-cell>
<span class="name">Maria Hansen</span>
<span class="phone">+45 23 45 67 89</span>
</swp-customer-cell>
</swp-data-table-cell>
<swp-data-table-cell class="muted">Louise P.</swp-data-table-cell>
<swp-data-table-cell>
<swp-services-cell>
<span class="main">Dameklip, Farve</span>
<span class="more">+ 1 produkt</span>
</swp-services-cell>
</swp-data-table-cell>
<swp-data-table-cell><swp-amount-cell>1.450 kr</swp-amount-cell></swp-data-table-cell>
<swp-data-table-cell><swp-payment-badge class="card"><i class="ph ph-credit-card"></i> Kort</swp-payment-badge></swp-data-table-cell>
<swp-data-table-cell><swp-status-badge class="paid">Betalt</swp-status-badge></swp-data-table-cell>
<swp-data-table-cell><swp-row-arrow><i class="ph ph-caret-right"></i></swp-row-arrow></swp-data-table-cell>
</swp-data-table-row>
<!-- Row 2 -->
<swp-data-table-row>
<swp-data-table-cell>
<swp-invoice-cell>#1846</swp-invoice-cell>
</swp-data-table-cell>
<swp-data-table-cell>
<swp-datetime-cell>
<span class="date">6. jan 2025</span>
<span class="time">13:15</span>
</swp-datetime-cell>
</swp-data-table-cell>
<swp-data-table-cell>
<swp-customer-cell>
<span class="name">Peter Sørensen</span>
<span class="phone">+45 30 12 34 56</span>
</swp-customer-cell>
</swp-data-table-cell>
<swp-data-table-cell class="muted">Anna J.</swp-data-table-cell>
<swp-data-table-cell>
<swp-services-cell>
<span class="main">Herreklip</span>
</swp-services-cell>
</swp-data-table-cell>
<swp-data-table-cell><swp-amount-cell>295 kr</swp-amount-cell></swp-data-table-cell>
<swp-data-table-cell><swp-payment-badge class="mobilepay"><i class="ph ph-device-mobile"></i> MobilePay</swp-payment-badge></swp-data-table-cell>
<swp-data-table-cell><swp-status-badge class="paid">Betalt</swp-status-badge></swp-data-table-cell>
<swp-data-table-cell><swp-row-arrow><i class="ph ph-caret-right"></i></swp-row-arrow></swp-data-table-cell>
</swp-data-table-row>
<!-- Row 3 -->
<swp-data-table-row>
<swp-data-table-cell>
<swp-invoice-cell>#1845</swp-invoice-cell>
</swp-data-table-cell>
<swp-data-table-cell>
<swp-datetime-cell>
<span class="date">6. jan 2025</span>
<span class="time">11:45</span>
</swp-datetime-cell>
</swp-data-table-cell>
<swp-data-table-cell>
<swp-customer-cell>
<span class="name">Lise Andersen</span>
<span class="phone">+45 42 56 78 90</span>
</swp-customer-cell>
</swp-data-table-cell>
<swp-data-table-cell class="muted">Louise P.</swp-data-table-cell>
<swp-data-table-cell>
<swp-services-cell>
<span class="main">Dameklip, Balayage</span>
<span class="more">+ 2 produkter</span>
</swp-services-cell>
</swp-data-table-cell>
<swp-data-table-cell><swp-amount-cell>2.350 kr</swp-amount-cell></swp-data-table-cell>
<swp-data-table-cell><swp-payment-badge class="card"><i class="ph ph-credit-card"></i> Kort</swp-payment-badge></swp-data-table-cell>
<swp-data-table-cell><swp-status-badge class="paid">Betalt</swp-status-badge></swp-data-table-cell>
<swp-data-table-cell><swp-row-arrow><i class="ph ph-caret-right"></i></swp-row-arrow></swp-data-table-cell>
</swp-data-table-row>
<!-- Row 4 -->
<swp-data-table-row>
<swp-data-table-cell>
<swp-invoice-cell>#1844</swp-invoice-cell>
</swp-data-table-cell>
<swp-data-table-cell>
<swp-datetime-cell>
<span class="date">5. jan 2025</span>
<span class="time">16:20</span>
</swp-datetime-cell>
</swp-data-table-cell>
<swp-data-table-cell>
<swp-customer-cell>
<span class="name">Thomas Nielsen</span>
<span class="phone">+45 51 23 45 67</span>
</swp-customer-cell>
</swp-data-table-cell>
<swp-data-table-cell class="muted">Mikkel H.</swp-data-table-cell>
<swp-data-table-cell>
<swp-services-cell>
<span class="main">Herreklip, Skægtrim</span>
</swp-services-cell>
</swp-data-table-cell>
<swp-data-table-cell><swp-amount-cell>395 kr</swp-amount-cell></swp-data-table-cell>
<swp-data-table-cell><swp-payment-badge class="cash"><i class="ph ph-money"></i> Kontant</swp-payment-badge></swp-data-table-cell>
<swp-data-table-cell><swp-status-badge class="paid">Betalt</swp-status-badge></swp-data-table-cell>
<swp-data-table-cell><swp-row-arrow><i class="ph ph-caret-right"></i></swp-row-arrow></swp-data-table-cell>
</swp-data-table-row>
<!-- Row 5 -->
<swp-data-table-row>
<swp-data-table-cell>
<swp-invoice-cell>#1843</swp-invoice-cell>
</swp-data-table-cell>
<swp-data-table-cell>
<swp-datetime-cell>
<span class="date">5. jan 2025</span>
<span class="time">14:00</span>
</swp-datetime-cell>
</swp-data-table-cell>
<swp-data-table-cell>
<swp-customer-cell>
<span class="name">Sofia Madsen</span>
<span class="phone">+45 60 78 90 12</span>
</swp-customer-cell>
</swp-data-table-cell>
<swp-data-table-cell class="muted">Anna J.</swp-data-table-cell>
<swp-data-table-cell>
<swp-services-cell>
<span class="main">Extensions</span>
<span class="more">+ 1 produkt</span>
</swp-services-cell>
</swp-data-table-cell>
<swp-data-table-cell><swp-amount-cell>4.500 kr</swp-amount-cell></swp-data-table-cell>
<swp-data-table-cell><swp-payment-badge class="invoice"><i class="ph ph-file-text"></i> Faktura</swp-payment-badge></swp-data-table-cell>
<swp-data-table-cell><swp-status-badge class="pending">Afventer</swp-status-badge></swp-data-table-cell>
<swp-data-table-cell><swp-row-arrow><i class="ph ph-caret-right"></i></swp-row-arrow></swp-data-table-cell>
</swp-data-table-row>
<!-- Row 6 -->
<swp-data-table-row>
<swp-data-table-cell>
<swp-invoice-cell>#1842</swp-invoice-cell>
</swp-data-table-cell>
<swp-data-table-cell>
<swp-datetime-cell>
<span class="date">5. jan 2025</span>
<span class="time">11:30</span>
</swp-datetime-cell>
</swp-data-table-cell>
<swp-data-table-cell>
<swp-customer-cell>
<span class="name">Emma Jensen</span>
<span class="phone">+45 71 23 45 67</span>
</swp-customer-cell>
</swp-data-table-cell>
<swp-data-table-cell class="muted">Louise P.</swp-data-table-cell>
<swp-data-table-cell>
<swp-services-cell>
<span class="main">Dameklip</span>
</swp-services-cell>
</swp-data-table-cell>
<swp-data-table-cell><swp-amount-cell>-450 kr</swp-amount-cell></swp-data-table-cell>
<swp-data-table-cell><swp-payment-badge class="card"><i class="ph ph-credit-card"></i> Kort</swp-payment-badge></swp-data-table-cell>
<swp-data-table-cell><swp-status-badge class="credited">Krediteret</swp-status-badge></swp-data-table-cell>
<swp-data-table-cell><swp-row-arrow><i class="ph ph-caret-right"></i></swp-row-arrow></swp-data-table-cell>
</swp-data-table-row>
<!-- Row 7 -->
<swp-data-table-row>
<swp-data-table-cell>
<swp-invoice-cell>#1841</swp-invoice-cell>
</swp-data-table-cell>
<swp-data-table-cell>
<swp-datetime-cell>
<span class="date">4. jan 2025</span>
<span class="time">15:45</span>
</swp-datetime-cell>
</swp-data-table-cell>
<swp-data-table-cell>
<swp-customer-cell>
<span class="name">Katrine Olsen</span>
<span class="phone">+45 82 34 56 78</span>
</swp-customer-cell>
</swp-data-table-cell>
<swp-data-table-cell class="muted">Mikkel H.</swp-data-table-cell>
<swp-data-table-cell>
<swp-services-cell>
<span class="main">Dameklip, Highlights</span>
<span class="more">+ 3 produkter</span>
</swp-services-cell>
</swp-data-table-cell>
<swp-data-table-cell><swp-amount-cell>1.895 kr</swp-amount-cell></swp-data-table-cell>
<swp-data-table-cell><swp-payment-badge class="giftcard"><i class="ph ph-gift"></i> Fordelskort</swp-payment-badge></swp-data-table-cell>
<swp-data-table-cell><swp-status-badge class="paid">Betalt</swp-status-badge></swp-data-table-cell>
<swp-data-table-cell><swp-row-arrow><i class="ph ph-caret-right"></i></swp-row-arrow></swp-data-table-cell>
</swp-data-table-row>
<!-- Row 8 -->
<swp-data-table-row>
<swp-data-table-cell>
<swp-invoice-cell>#1840</swp-invoice-cell>
</swp-data-table-cell>
<swp-data-table-cell>
<swp-datetime-cell>
<span class="date">4. jan 2025</span>
<span class="time">10:00</span>
</swp-datetime-cell>
</swp-data-table-cell>
<swp-data-table-cell>
<swp-customer-cell>
<span class="name">Mads Christensen</span>
<span class="phone">+45 93 45 67 89</span>
</swp-customer-cell>
</swp-data-table-cell>
<swp-data-table-cell class="muted">Anna J.</swp-data-table-cell>
<swp-data-table-cell>
<swp-services-cell>
<span class="main">Herreklip</span>
</swp-services-cell>
</swp-data-table-cell>
<swp-data-table-cell><swp-amount-cell>275 kr</swp-amount-cell></swp-data-table-cell>
<swp-data-table-cell><swp-payment-badge class="mobilepay"><i class="ph ph-device-mobile"></i> MobilePay</swp-payment-badge></swp-data-table-cell>
<swp-data-table-cell><swp-status-badge class="paid">Betalt</swp-status-badge></swp-data-table-cell>
<swp-data-table-cell><swp-row-arrow><i class="ph ph-caret-right"></i></swp-row-arrow></swp-data-table-cell>
</swp-data-table-row>
</swp-data-table>
<swp-table-footer>
<span>Viser 1-8 af 1.847 fakturaer</span>
<swp-pagination>
<swp-page-btn><i class="ph ph-caret-left"></i></swp-page-btn>
<swp-page-btn class="active">1</swp-page-btn>
<swp-page-btn>2</swp-page-btn>
<swp-page-btn>3</swp-page-btn>
<swp-page-btn>...</swp-page-btn>
<swp-page-btn>231</swp-page-btn>
<swp-page-btn><i class="ph ph-caret-right"></i></swp-page-btn>
</swp-pagination>
</swp-table-footer>
</swp-card>
</swp-page-container>
</swp-tab-content>
<!-- Tab Content: Timerapport -->
<swp-tab-content data-tab="hours">
<swp-page-container>
<swp-card>
<swp-card-header>
<swp-card-title>
<i class="ph ph-clock"></i>
<span>Timerapport</span>
</swp-card-title>
</swp-card-header>
<swp-empty-state>
<i class="ph ph-clock-counter-clockwise"></i>
<span>Timerapport kommer snart</span>
</swp-empty-state>
</swp-card>
</swp-page-container>
</swp-tab-content>
@section Scripts {
<script type="module">
import { createChart } from '/lib/swp-charting/dist/swp-charting.js';
import Fuse from '/lib/fuse/fuse.mjs';
// === SEARCH FUNCTIONALITY ===
const searchInput = document.getElementById('searchInput');
const tableRows = document.querySelectorAll('swp-card.sales-table swp-data-table-row');
// Build searchable data from table rows
const salesData = Array.from(tableRows).map((row, index) => {
const cells = row.querySelectorAll('swp-data-table-cell');
return {
index,
invoice: cells[0]?.textContent?.trim() || '',
date: cells[1]?.textContent?.trim() || '',
customer: cells[2]?.textContent?.trim() || '',
employee: cells[3]?.textContent?.trim() || '',
services: cells[4]?.textContent?.trim() || '',
amount: cells[5]?.textContent?.trim() || '',
payment: cells[6]?.textContent?.trim() || '',
status: cells[7]?.textContent?.trim() || ''
};
});
// Initialize Fuse.js
const fuse = new Fuse(salesData, {
keys: ['invoice', 'customer', 'employee', 'services', 'amount', 'payment', 'status'],
threshold: 0.3,
ignoreLocation: true
});
// Parse amount string "1.450 kr" -> 1450
function parseAmountFromText(text) {
const match = text.match(/-?([\d.]+)/);
if (!match) return null;
// Remove thousand separators (Danish format uses . for thousands)
return parseFloat(match[1].replace(/\./g, ''));
}
// Parse invoice number "#1847" -> 1847
function parseInvoiceNumber(text) {
const match = text.match(/#(\d+)/);
return match ? parseInt(match[1], 10) : null;
}
// Parse range operators from query (supports both amount and invoice ranges)
function parseRangeQuery(query) {
let textQuery = query;
let minAmount = null;
let maxAmount = null;
let minInvoice = null;
let maxInvoice = null;
// === INVOICE NUMBER RANGES (with # prefix) ===
// Match #1840-1845
let match = textQuery.match(/#(\d+)-(\d+)/);
if (match) {
minInvoice = parseInt(match[1], 10);
maxInvoice = parseInt(match[2], 10);
textQuery = textQuery.replace(match[0], '').trim();
}
// Match #>=
match = textQuery.match(/#>=\s*(\d+)/);
if (match) {
minInvoice = parseInt(match[1], 10);
textQuery = textQuery.replace(match[0], '').trim();
}
// Match #> (but not #>=)
match = textQuery.match(/#>\s*(\d+)/);
if (match) {
minInvoice = parseInt(match[1], 10) + 1;
textQuery = textQuery.replace(match[0], '').trim();
}
// Match #<=
match = textQuery.match(/#<=\s*(\d+)/);
if (match) {
maxInvoice = parseInt(match[1], 10);
textQuery = textQuery.replace(match[0], '').trim();
}
// Match #< (but not #<=)
match = textQuery.match(/#<\s*(\d+)/);
if (match) {
maxInvoice = parseInt(match[1], 10) - 1;
textQuery = textQuery.replace(match[0], '').trim();
}
// === AMOUNT RANGES (no prefix) ===
// Match range syntax: 400-1000
match = textQuery.match(/(\d+)-(\d+)/);
if (match) {
minAmount = parseFloat(match[1]);
maxAmount = parseFloat(match[2]);
textQuery = textQuery.replace(match[0], '').trim();
}
// Match >=
match = textQuery.match(/>=\s*(\d+(?:\.\d+)?)/);
if (match) {
minAmount = parseFloat(match[1]);
textQuery = textQuery.replace(match[0], '').trim();
}
// Match > (but not >=)
match = textQuery.match(/>\s*(\d+(?:\.\d+)?)/);
if (match) {
minAmount = parseFloat(match[1]) + 0.01;
textQuery = textQuery.replace(match[0], '').trim();
}
// Match <=
match = textQuery.match(/<=\s*(\d+(?:\.\d+)?)/);
if (match) {
maxAmount = parseFloat(match[1]);
textQuery = textQuery.replace(match[0], '').trim();
}
// Match < (but not <=)
match = textQuery.match(/<\s*(\d+(?:\.\d+)?)/);
if (match) {
maxAmount = parseFloat(match[1]) - 0.01;
textQuery = textQuery.replace(match[0], '').trim();
}
return { textQuery, minAmount, maxAmount, minInvoice, maxInvoice };
}
// Search handler with range support
searchInput.addEventListener('input', (e) => {
const query = e.target.value.trim();
if (!query) {
// Show all rows
tableRows.forEach(row => row.style.display = '');
return;
}
const { textQuery, minAmount, maxAmount, minInvoice, maxInvoice } = parseRangeQuery(query);
// Start with all indices
let matchedIndices = new Set(salesData.map((_, i) => i));
// Apply invoice number range filter
if (minInvoice !== null || maxInvoice !== null) {
matchedIndices = new Set(
salesData
.filter((item) => {
const invoiceNum = parseInvoiceNumber(item.invoice);
if (invoiceNum === null) return false;
if (minInvoice !== null && invoiceNum < minInvoice) return false;
if (maxInvoice !== null && invoiceNum > maxInvoice) return false;
return true;
})
.map(item => item.index)
);
}
// Apply amount range filter
if (minAmount !== null || maxAmount !== null) {
matchedIndices = new Set(
[...matchedIndices].filter(i => {
const amount = parseAmountFromText(salesData[i].amount);
if (amount === null) return false;
if (minAmount !== null && amount < minAmount) return false;
if (maxAmount !== null && amount > maxAmount) return false;
return true;
})
);
}
// Apply Fuse.js text search if there's remaining text
if (textQuery) {
const fuseResults = fuse.search(textQuery);
const textMatches = new Set(fuseResults.map(r => r.item.index));
matchedIndices = new Set([...matchedIndices].filter(i => textMatches.has(i)));
}
tableRows.forEach((row, index) => {
row.style.display = matchedIndices.has(index) ? '' : 'none';
});
});
// 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' }
});
</script>
}