Add reports page with sales analytics and UI components

Introduces comprehensive reports feature with interactive sales dashboard
Includes dynamic data tables, charts, and filtering capabilities
Enhances application with new statistics and reporting functionality
This commit is contained in:
Janus C. H. Knudsen 2026-01-21 21:37:09 +01:00
parent 6ef001e35f
commit 405dabeb34
15 changed files with 1909 additions and 212 deletions

View file

@ -9,7 +9,8 @@
"Bash(npm run analyze-css:*)",
"Bash(node:*)",
"Bash(npx esbuild:*)",
"mcp__ide__getDiagnostics"
"mcp__ide__getDiagnostics",
"Bash(grep:*)"
]
}
}

2
.gitignore vendored
View file

@ -370,3 +370,5 @@ PlanTempus.Application/tmpclaude*
PlanTempus.Application/wwwroot/js/app.js
PlanTempus.Application/wwwroot/js/app.js.map
PlanTempus.Application/wwwroot/lib/*

View file

@ -155,8 +155,8 @@ public class MockMenuService : IMenuService
{
Id = "reports",
Label = "Statistik & Rapporter",
Icon = "ph-chart-bar",
Url = "/reports",
Icon = "ph-chart-line-up",
Url = "/rapporter",
MinimumRole = UserRole.Manager,
SortOrder = 1
}

View file

@ -0,0 +1,622 @@
@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>
}

View file

@ -0,0 +1,10 @@
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace PlanTempus.Application.Features.Reports.Pages;
public class IndexModel : PageModel
{
public void OnGet()
{
}
}

View file

@ -0,0 +1,64 @@
<swp-new-todo-drawer id="newTodoDrawer">
<swp-drawer-header>
<swp-drawer-back id="newTodoDrawerBack">
<i class="ph ph-caret-left"></i>
</swp-drawer-back>
<swp-drawer-title>Ny opgave</swp-drawer-title>
</swp-drawer-header>
<swp-drawer-content>
<!-- Titel -->
<swp-form-field>
<swp-section-label>Opgave</swp-section-label>
<input type="text" placeholder="Hvad skal du huske?" id="newTodoTitle">
</swp-form-field>
<!-- Dato & Tid -->
<swp-form-row>
<swp-form-field>
<swp-section-label>Dato</swp-section-label>
<input type="date" id="newTodoDate">
</swp-form-field>
<swp-form-field>
<swp-section-label>Tid</swp-section-label>
<input type="time" id="newTodoTime">
</swp-form-field>
</swp-form-row>
<!-- Prioritet -->
<swp-form-field>
<swp-section-label>Prioritet</swp-section-label>
<select id="newTodoPriority">
<option value="normal">Normal</option>
<option value="high">Høj prioritet</option>
<option value="low">Lav prioritet</option>
</select>
</swp-form-field>
<!-- Synlighed -->
<swp-form-field>
<swp-section-label>Synlighed</swp-section-label>
<swp-visibility-toggle>
<swp-visibility-option class="active" data-value="personal">
<i class="ph ph-user"></i>
Kun mig
</swp-visibility-option>
<swp-visibility-option data-value="shared">
<i class="ph ph-users"></i>
Alle
</swp-visibility-option>
</swp-visibility-toggle>
</swp-form-field>
<!-- Noter -->
<swp-form-field>
<swp-section-label>Noter</swp-section-label>
<textarea placeholder="Tilføj noter..." id="newTodoNotes"></textarea>
</swp-form-field>
</swp-drawer-content>
<swp-drawer-footer>
<swp-btn class="secondary" id="cancelNewTodo">Annuller</swp-btn>
<swp-btn class="primary" id="saveNewTodo">Gem opgave</swp-btn>
</swp-drawer-footer>
</swp-new-todo-drawer>

View file

@ -1,49 +1,74 @@
<swp-profile-drawer id="profileDrawer">
<swp-drawer-header>
<swp-drawer-title>Profil</swp-drawer-title>
<swp-drawer-close id="closeProfileDrawer">
<swp-drawer-title localize="profile.title">Min profil</swp-drawer-title>
<swp-drawer-close id="drawerClose">
<i class="ph ph-x"></i>
</swp-drawer-close>
</swp-drawer-header>
<swp-drawer-content>
<swp-profile-section>
<swp-profile-avatar-large>MJ</swp-profile-avatar-large>
<swp-profile-name-large>Maria Jensen</swp-profile-name-large>
<swp-profile-email>maria@salon.dk</swp-profile-email>
</swp-profile-section>
<swp-drawer-profile>
<swp-drawer-avatar>MJ</swp-drawer-avatar>
<swp-drawer-name>Maria Jensen</swp-drawer-name>
<swp-drawer-role>Administrator</swp-drawer-role>
<swp-drawer-email>maria@salon.dk</swp-drawer-email>
</swp-drawer-profile>
<swp-drawer-divider></swp-drawer-divider>
<swp-drawer-section>
<swp-drawer-section-label localize="profile.account">Konto</swp-drawer-section-label>
<swp-drawer-item>
<i class="ph ph-user-circle"></i>
<swp-drawer-item-text localize="profile.editProfile">Rediger profil</swp-drawer-item-text>
<i class="ph ph-caret-right"></i>
</swp-drawer-item>
<swp-drawer-item>
<i class="ph ph-key"></i>
<swp-drawer-item-text localize="profile.changePassword">Skift adgangskode</swp-drawer-item-text>
<i class="ph ph-caret-right"></i>
</swp-drawer-item>
<swp-drawer-item id="openNotificationSettings">
<i class="ph ph-bell"></i>
<swp-drawer-item-text localize="profile.notifications">Notifikationer</swp-drawer-item-text>
<swp-drawer-item-hint>3 ulæste</swp-drawer-item-hint>
</swp-drawer-item>
<swp-drawer-item id="openTodoDrawer">
<i class="ph ph-check-square"></i>
<swp-drawer-item-text localize="profile.myTasks">Mine opgaver</swp-drawer-item-text>
<swp-drawer-item-hint>2 i dag</swp-drawer-item-hint>
</swp-drawer-item>
</swp-drawer-section>
<swp-drawer-menu>
<swp-drawer-menu-item>
<i class="ph ph-user"></i>
<span>Min profil</span>
</swp-drawer-menu-item>
<swp-drawer-menu-item>
<i class="ph ph-gear"></i>
<span>Indstillinger</span>
</swp-drawer-menu-item>
</swp-drawer-menu>
<swp-drawer-divider></swp-drawer-divider>
<swp-theme-toggle>
<swp-theme-label>
<swp-drawer-section>
<swp-drawer-section-label localize="profile.appearance">Udseende</swp-drawer-section-label>
<swp-theme-toggle id="themeToggleDrawer">
<swp-theme-option data-theme="light" title="Lyst tema">
<i class="ph ph-sun"></i>
</swp-theme-option>
<swp-theme-option data-theme="dark" class="active" title="Mørkt tema">
<i class="ph ph-moon"></i>
<span>Mørk tilstand</span>
</swp-theme-label>
<swp-toggle-switch id="themeToggle">
<input type="checkbox" id="themeCheckbox">
<swp-toggle-track></swp-toggle-track>
</swp-toggle-switch>
</swp-theme-option>
</swp-theme-toggle>
</swp-drawer-section>
<swp-drawer-section>
<swp-drawer-section-label localize="profile.support">Support</swp-drawer-section-label>
<swp-drawer-item>
<i class="ph ph-question"></i>
<swp-drawer-item-text localize="profile.helpSupport">Hjælp & Support</swp-drawer-item-text>
<i class="ph ph-caret-right"></i>
</swp-drawer-item>
<swp-drawer-item>
<i class="ph ph-info"></i>
<swp-drawer-item-text localize="profile.about">Om PlanTempus</swp-drawer-item-text>
<swp-drawer-item-hint>v2.1.0</swp-drawer-item-hint>
</swp-drawer-item>
</swp-drawer-section>
</swp-drawer-content>
<swp-drawer-footer>
<swp-drawer-action class="logout" id="logoutBtn">
<i class="ph ph-sign-out"></i>
<span>Log ud</span>
<span localize="profile.logout">Log ud</span>
</swp-drawer-action>
</swp-drawer-footer>
</swp-profile-drawer>

View file

@ -0,0 +1,141 @@
<swp-todo-drawer id="todoDrawer">
<swp-drawer-header>
<swp-drawer-back id="todoDrawerBack">
<i class="ph ph-caret-left"></i>
</swp-drawer-back>
<swp-drawer-title>Mine opgaver</swp-drawer-title>
<swp-drawer-header-actions>
<swp-btn class="primary small" id="addTodoBtn">
<i class="ph ph-plus"></i>
Ny opgave
</swp-btn>
</swp-drawer-header-actions>
</swp-drawer-header>
<swp-drawer-content>
<!-- I dag -->
<swp-todo-section>
<swp-todo-section-header>
<i class="ph ph-caret-down"></i>
<swp-todo-section-title>I dag</swp-todo-section-title>
<swp-todo-section-count>3</swp-todo-section-count>
</swp-todo-section-header>
<swp-todo-items>
<swp-todo-item>
<swp-todo-checkbox>
<i class="ph ph-check"></i>
</swp-todo-checkbox>
<swp-todo-content>
<swp-todo-title>Ring til leverandør om ordre</swp-todo-title>
<swp-todo-meta>
<swp-todo-time>
<i class="ph ph-clock"></i>
10:00
</swp-todo-time>
</swp-todo-meta>
</swp-todo-content>
</swp-todo-item>
<swp-todo-item data-completed="true">
<swp-todo-checkbox>
<i class="ph ph-check"></i>
</swp-todo-checkbox>
<swp-todo-content>
<swp-todo-title>Bestil shampoo fra Wella</swp-todo-title>
</swp-todo-content>
</swp-todo-item>
<swp-todo-item>
<swp-todo-checkbox>
<i class="ph ph-check"></i>
</swp-todo-checkbox>
<swp-todo-content>
<swp-todo-title>Opdater prisliste for 2025</swp-todo-title>
<swp-todo-meta>
<swp-todo-priority class="high">
<i class="ph ph-flag"></i>
Høj
</swp-todo-priority>
</swp-todo-meta>
</swp-todo-content>
</swp-todo-item>
</swp-todo-items>
</swp-todo-section>
<!-- Denne uge -->
<swp-todo-section>
<swp-todo-section-header>
<i class="ph ph-caret-down"></i>
<swp-todo-section-title>Denne uge</swp-todo-section-title>
<swp-todo-section-count>2</swp-todo-section-count>
</swp-todo-section-header>
<swp-todo-items>
<swp-todo-item>
<swp-todo-checkbox>
<i class="ph ph-check"></i>
</swp-todo-checkbox>
<swp-todo-content>
<swp-todo-title>Rengør og vedligehold udstyr</swp-todo-title>
<swp-todo-meta>
<swp-todo-date>
<i class="ph ph-calendar"></i>
Onsdag
</swp-todo-date>
</swp-todo-meta>
</swp-todo-content>
</swp-todo-item>
<swp-todo-item>
<swp-todo-checkbox>
<i class="ph ph-check"></i>
</swp-todo-checkbox>
<swp-todo-content>
<swp-todo-title>Medarbejdersamtale med Jonas</swp-todo-title>
<swp-todo-meta>
<swp-todo-date>
<i class="ph ph-calendar"></i>
Fredag
</swp-todo-date>
<swp-todo-time>
<i class="ph ph-clock"></i>
14:00
</swp-todo-time>
</swp-todo-meta>
</swp-todo-content>
</swp-todo-item>
</swp-todo-items>
</swp-todo-section>
<!-- Udført -->
<swp-todo-section class="collapsed">
<swp-todo-section-header>
<i class="ph ph-caret-down"></i>
<swp-todo-section-title>Udført</swp-todo-section-title>
<swp-todo-section-count>3</swp-todo-section-count>
</swp-todo-section-header>
<swp-todo-items>
<swp-todo-item data-completed="true">
<swp-todo-checkbox>
<i class="ph ph-check"></i>
</swp-todo-checkbox>
<swp-todo-content>
<swp-todo-title>Send faktura til kunde</swp-todo-title>
</swp-todo-content>
</swp-todo-item>
<swp-todo-item data-completed="true">
<swp-todo-checkbox>
<i class="ph ph-check"></i>
</swp-todo-checkbox>
<swp-todo-content>
<swp-todo-title>Opdater åbningstider på Google</swp-todo-title>
</swp-todo-content>
</swp-todo-item>
<swp-todo-item data-completed="true">
<swp-todo-checkbox>
<i class="ph ph-check"></i>
</swp-todo-checkbox>
<swp-todo-content>
<swp-todo-title>Bestil nye håndklæder</swp-todo-title>
</swp-todo-content>
</swp-todo-item>
</swp-todo-items>
</swp-todo-section>
</swp-drawer-content>
</swp-todo-drawer>

View file

@ -35,6 +35,7 @@
<link rel="stylesheet" href="~/css/services.css">
<link rel="stylesheet" href="~/css/customers.css">
<link rel="stylesheet" href="~/css/settings.css">
<link rel="stylesheet" href="~/css/reports.css">
@await RenderSectionAsync("Styles", required: false)
</head>
<body class="has-demo-banner">
@ -62,6 +63,8 @@
</swp-app-layout>
<partial name="_ProfileDrawer" />
<partial name="_TodoDrawer" />
<partial name="_NewTodoDrawer" />
<swp-drawer-overlay id="drawerOverlay"></swp-drawer-overlay>
<script type="module" src="~/js/app.js"></script>

View file

@ -1,49 +0,0 @@
<swp-profile-drawer id="profileDrawer">
<swp-drawer-header>
<swp-drawer-title localize="profile.title">Profil</swp-drawer-title>
<swp-drawer-close id="closeProfileDrawer">
<i class="ph ph-x"></i>
</swp-drawer-close>
</swp-drawer-header>
<swp-drawer-content>
<swp-profile-section>
<swp-profile-avatar-large>MJ</swp-profile-avatar-large>
<swp-profile-name-large>Maria Jensen</swp-profile-name-large>
<swp-profile-email>maria@salon.dk</swp-profile-email>
</swp-profile-section>
<swp-drawer-divider></swp-drawer-divider>
<swp-drawer-menu>
<swp-drawer-menu-item>
<i class="ph ph-user"></i>
<span localize="profile.myProfile">Min profil</span>
</swp-drawer-menu-item>
<swp-drawer-menu-item>
<i class="ph ph-gear"></i>
<span localize="profile.settings">Indstillinger</span>
</swp-drawer-menu-item>
</swp-drawer-menu>
<swp-drawer-divider></swp-drawer-divider>
<swp-theme-toggle>
<swp-theme-label>
<i class="ph ph-moon"></i>
<span localize="profile.darkMode">Mørk tilstand</span>
</swp-theme-label>
<swp-toggle-switch id="themeToggle">
<input type="checkbox" id="themeCheckbox">
<swp-toggle-track></swp-toggle-track>
</swp-toggle-switch>
</swp-theme-toggle>
</swp-drawer-content>
<swp-drawer-footer>
<swp-drawer-action class="logout" id="logoutBtn">
<i class="ph ph-sign-out"></i>
<span localize="profile.logout">Log ud</span>
</swp-drawer-action>
</swp-drawer-footer>
</swp-profile-drawer>

View file

@ -5,6 +5,7 @@
"packages": {
"": {
"dependencies": {
"@sevenweirdpeople/swp-charting": "^0.2.2",
"fuse.js": "^7.1.0"
},
"devDependencies": {
@ -483,6 +484,12 @@
"node": ">=14"
}
},
"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==",
"license": "MIT"
},
"node_modules/ansi-regex": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",

View file

@ -8,6 +8,7 @@
"analyze-css": "node analyze-css.js"
},
"dependencies": {
"@sevenweirdpeople/swp-charting": "^0.2.2",
"fuse.js": "^7.1.0"
}
}

View file

@ -33,18 +33,5 @@ swp-main-content {
/* ===========================================
DRAWER OVERLAY
Styles moved to drawers.css for consistency with calpoc
=========================================== */
swp-drawer-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: var(--z-overlay);
opacity: 0;
visibility: hidden;
transition: opacity var(--transition-normal), visibility var(--transition-normal);
}
swp-drawer-overlay.active {
opacity: 1;
visibility: visible;
}

View file

@ -2,8 +2,27 @@
* Drawers - Slide-in Panels
*
* Profile drawer, notifications drawer, etc.
* Matches calpoc pattern from Calendar/wwwroot/poc-layout.html
*/
/* ===========================================
DRAWER OVERLAY
=========================================== */
swp-drawer-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.2);
opacity: 0;
visibility: hidden;
transition: opacity 200ms ease, visibility 200ms ease;
z-index: 900;
}
swp-drawer-overlay.active {
opacity: 1;
visibility: visible;
}
/* ===========================================
BASE DRAWER (Generic)
=========================================== */
@ -15,12 +34,12 @@
height: 100vh;
background: var(--color-surface);
border-left: 1px solid var(--color-border);
box-shadow: var(--shadow-lg);
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1);
z-index: var(--z-drawer);
display: flex;
flex-direction: column;
transform: translateX(100%);
transition: transform var(--transition-normal);
transition: transform 200ms ease;
}
[data-drawer].active,
@ -46,12 +65,12 @@ swp-todo-drawer {
height: 100vh;
background: var(--color-surface);
border-left: 1px solid var(--color-border);
box-shadow: var(--shadow-lg);
z-index: var(--z-drawer);
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1);
z-index: 1000;
display: flex;
flex-direction: column;
transform: translateX(100%);
transition: transform var(--transition-normal);
transition: transform 200ms ease;
}
swp-profile-drawer.active,
@ -67,17 +86,15 @@ swp-drawer-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--spacing-10) var(--spacing-12);
padding: 14px 16px;
border-bottom: 1px solid var(--color-border);
background: var(--color-background-alt);
flex-shrink: 0;
}
swp-drawer-title {
display: flex;
align-items: center;
gap: var(--spacing-2);
font-size: var(--font-size-lg);
font-weight: var(--font-weight-semibold);
font-size: 14px;
font-weight: 600;
color: var(--color-text);
}
@ -94,11 +111,11 @@ swp-drawer-close {
align-items: center;
justify-content: center;
border: none;
background: var(--color-background-alt);
border-radius: var(--radius-md);
background: transparent;
border-radius: 6px;
cursor: pointer;
color: var(--color-text-secondary);
transition: all var(--transition-fast);
transition: all 150ms ease;
}
swp-drawer-close:hover {
@ -110,6 +127,12 @@ swp-drawer-close i {
font-size: 20px;
}
swp-drawer-header-actions {
display: flex;
align-items: center;
gap: var(--spacing-3);
}
/* ===========================================
DRAWER CONTENT / BODY
=========================================== */
@ -117,10 +140,10 @@ swp-drawer-content,
swp-drawer-body {
flex: 1;
overflow-y: auto;
padding: var(--spacing-8);
padding: 20px 16px;
display: flex;
flex-direction: column;
gap: var(--spacing-5);
gap: 24px;
}
swp-drawer-divider {
@ -130,134 +153,142 @@ swp-drawer-divider {
}
/* ===========================================
PROFILE SECTION
PROFILE SECTION (Centered in drawer)
=========================================== */
swp-profile-section {
swp-drawer-profile {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: var(--spacing-4) 0;
padding-bottom: 20px;
border-bottom: 1px solid var(--color-border);
}
swp-profile-avatar-large {
width: 64px;
height: 64px;
border-radius: var(--radius-full);
background: var(--color-teal);
swp-drawer-avatar {
width: 72px;
height: 72px;
border-radius: 50%;
background: var(--color-purple);
color: white;
font-size: var(--font-size-3xl);
font-weight: var(--font-weight-semibold);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: var(--spacing-3);
font-size: 24px;
font-weight: 600;
margin-bottom: 12px;
}
swp-profile-name-large {
font-size: var(--font-size-lg);
font-weight: var(--font-weight-semibold);
swp-drawer-name {
font-size: 18px;
font-weight: 600;
color: var(--color-text);
margin-bottom: var(--spacing-1);
margin-bottom: 2px;
}
swp-profile-email {
font-size: var(--font-size-sm);
swp-drawer-role {
font-size: 13px;
color: var(--color-text-secondary);
margin-bottom: 8px;
}
swp-drawer-email {
font-size: 13px;
color: var(--color-teal);
}
/* ===========================================
DRAWER MENU
DRAWER SECTIONS
=========================================== */
swp-drawer-menu {
swp-drawer-section {
display: flex;
flex-direction: column;
gap: var(--spacing-1);
gap: 8px;
}
swp-drawer-menu-item {
swp-drawer-section-label {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--color-text-secondary);
margin-bottom: 4px;
}
/* ===========================================
DRAWER ITEMS (Menu items)
=========================================== */
swp-drawer-item {
display: flex;
align-items: center;
gap: var(--spacing-3);
padding: var(--spacing-3) var(--spacing-3);
border-radius: var(--border-radius);
gap: 12px;
padding: 12px;
background: var(--color-background-alt);
border-radius: 8px;
cursor: pointer;
transition: background var(--transition-fast);
color: var(--color-text);
transition: background 150ms ease;
}
swp-drawer-menu-item:hover {
swp-drawer-item:hover {
background: var(--color-background-hover);
}
swp-drawer-menu-item i {
swp-drawer-item i {
font-size: 20px;
color: var(--color-text-secondary);
}
swp-drawer-item-text {
flex: 1;
font-size: 14px;
color: var(--color-text);
}
swp-drawer-item-hint {
font-size: 12px;
color: var(--color-text-secondary);
}
/* ===========================================
THEME TOGGLE
THEME TOGGLE (Two icon buttons)
=========================================== */
swp-theme-toggle {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--spacing-3);
border-radius: var(--border-radius);
background: var(--color-background);
border: 1px solid var(--color-border);
border-radius: 8px;
overflow: hidden;
}
swp-theme-label {
swp-theme-option {
flex: 1;
display: flex;
align-items: center;
gap: var(--spacing-3);
color: var(--color-text);
justify-content: center;
padding: 12px;
background: var(--color-background-alt);
cursor: pointer;
transition: all 150ms ease;
}
swp-theme-label i {
swp-theme-option:first-child {
border-right: 1px solid var(--color-border);
}
swp-theme-option i {
font-size: 20px;
color: var(--color-text-secondary);
transition: color 150ms ease;
}
swp-toggle-switch {
position: relative;
width: 44px;
height: 24px;
swp-theme-option:hover {
background: var(--color-background-hover);
}
swp-toggle-switch input {
opacity: 0;
width: 0;
height: 0;
swp-theme-option.active {
background: color-mix(in srgb, var(--color-teal) 12%, transparent);
}
swp-toggle-track {
position: absolute;
cursor: pointer;
inset: 0;
background: var(--color-border);
border-radius: 12px;
transition: background var(--transition-fast);
}
swp-toggle-track::before {
content: '';
position: absolute;
width: 18px;
height: 18px;
left: 3px;
bottom: 3px;
background: white;
border-radius: 50%;
transition: transform var(--transition-fast);
}
swp-toggle-switch input:checked + swp-toggle-track {
background: var(--color-teal);
}
swp-toggle-switch input:checked + swp-toggle-track::before {
transform: translateX(20px);
swp-theme-option.active i {
color: var(--color-teal);
}
/* ===========================================
@ -265,9 +296,9 @@ swp-toggle-switch input:checked + swp-toggle-track::before {
=========================================== */
swp-drawer-footer {
display: flex;
justify-content: flex-end;
gap: var(--spacing-3);
padding: var(--spacing-4) var(--spacing-5);
flex-direction: column;
gap: 8px;
padding: 16px;
border-top: 1px solid var(--color-border);
flex-shrink: 0;
}
@ -276,28 +307,485 @@ swp-drawer-action {
display: flex;
align-items: center;
justify-content: center;
gap: var(--spacing-2);
width: 100%;
padding: var(--spacing-3);
font-size: var(--font-size-base);
gap: 8px;
padding: 12px;
font-size: 14px;
font-family: var(--font-family);
color: var(--color-text-secondary);
background: transparent;
border: 1px solid var(--color-border);
border-radius: var(--border-radius);
border-radius: 8px;
cursor: pointer;
transition: all var(--transition-fast);
transition: all 150ms ease;
}
& i {
swp-drawer-action i {
font-size: 18px;
}
}
&:hover {
swp-drawer-action:hover {
background: var(--color-background-hover);
}
}
&.logout:hover {
swp-drawer-action.logout:hover {
color: var(--color-red);
border-color: var(--color-red);
}
background: var(--bg-red-subtle);
}
/* ===========================================
MARK ALL READ BUTTON (Notification drawer)
=========================================== */
swp-mark-read-btn {
font-size: 12px;
color: var(--color-teal);
cursor: pointer;
transition: opacity 150ms ease;
}
swp-mark-read-btn:hover {
opacity: 0.8;
}
/* ===========================================
TODO DRAWER (slides out to the left of profile drawer)
=========================================== */
swp-todo-drawer {
position: fixed;
top: 0;
right: 320px; /* Position to the left of profile drawer (320px wide) */
bottom: 0;
width: 380px;
background: var(--color-surface);
border-left: 1px solid var(--color-border);
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1);
transform: translateX(100%);
opacity: 0;
pointer-events: none;
transition: transform 250ms cubic-bezier(0.4, 0, 0.2, 1),
opacity 200ms ease;
display: flex;
flex-direction: column;
z-index: 999; /* Below profile drawer */
}
swp-todo-drawer.active {
transform: translateX(0);
opacity: 1;
pointer-events: auto;
}
swp-todo-drawer swp-drawer-header {
display: flex;
align-items: center;
gap: 8px;
padding: 14px 16px;
border-bottom: 1px solid var(--color-border);
background: var(--color-background-alt);
}
swp-todo-drawer swp-drawer-title {
flex: 1;
}
swp-drawer-back {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border: none;
background: transparent;
border-radius: 6px;
cursor: pointer;
color: var(--color-text-secondary);
transition: all 150ms ease;
}
swp-drawer-back:hover {
background: var(--color-background-hover);
color: var(--color-text);
}
swp-drawer-back i {
font-size: 20px;
}
/* Header button with text */
swp-todo-drawer swp-btn.primary.small {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
font-size: 13px;
font-family: var(--font-family);
font-weight: 500;
background: var(--color-teal);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
transition: all 150ms ease;
}
swp-todo-drawer swp-btn.primary.small:hover {
background: color-mix(in srgb, var(--color-teal) 85%, black);
}
swp-todo-drawer swp-btn.primary.small i {
font-size: 16px;
}
swp-todo-drawer swp-drawer-content {
flex: 1;
overflow-y: auto;
padding: 16px;
display: flex;
flex-direction: column;
gap: 20px;
}
swp-todo-section {
display: flex;
flex-direction: column;
gap: 8px;
}
swp-todo-section-header {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
cursor: pointer;
}
swp-todo-section-header i {
font-size: 14px;
color: var(--color-text-secondary);
transition: transform 200ms ease;
}
swp-todo-section.collapsed swp-todo-section-header i {
transform: rotate(-90deg);
}
swp-todo-section.collapsed swp-todo-items {
display: none;
}
swp-todo-section-title {
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--color-text-secondary);
}
swp-todo-section-count {
font-size: 11px;
padding: 2px 6px;
background: var(--color-background);
border-radius: 10px;
color: var(--color-text-secondary);
}
swp-todo-items {
display: flex;
flex-direction: column;
gap: 6px;
}
swp-todo-item {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 10px 12px;
background: var(--color-background-alt);
border-radius: 8px;
cursor: pointer;
transition: all 150ms ease;
}
swp-todo-item:hover {
background: var(--color-background-hover);
}
swp-todo-item[data-completed="true"] {
opacity: 0.6;
}
swp-todo-item[data-completed="true"] swp-todo-title {
text-decoration: line-through;
color: var(--color-text-secondary);
}
swp-todo-checkbox {
width: 20px;
height: 20px;
border: 2px solid var(--color-border);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-top: 1px;
transition: all 150ms ease;
}
swp-todo-checkbox i {
font-size: 14px;
color: white;
opacity: 0;
}
swp-todo-item[data-completed="true"] swp-todo-checkbox {
background: var(--color-teal);
border-color: var(--color-teal);
}
swp-todo-item[data-completed="true"] swp-todo-checkbox i {
opacity: 1;
}
swp-todo-content {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
swp-todo-title {
font-size: 14px;
color: var(--color-text);
}
swp-todo-meta {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--color-text-secondary);
}
swp-todo-meta i {
font-size: 14px;
}
swp-todo-time {
display: flex;
align-items: center;
gap: 4px;
}
swp-todo-priority {
display: flex;
align-items: center;
gap: 4px;
}
swp-todo-priority.high {
color: var(--color-red);
}
swp-todo-priority.low {
color: var(--color-text-secondary);
opacity: 0.7;
}
swp-todo-date {
display: flex;
align-items: center;
gap: 4px;
}
/* ===========================================
NEW TODO DRAWER (slides out to the left of todo drawer)
=========================================== */
swp-new-todo-drawer {
position: fixed;
top: 0;
right: 700px; /* 320px (profile) + 380px (todo) */
bottom: 0;
width: 340px;
background: var(--color-surface);
border-left: 1px solid var(--color-border);
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1);
transform: translateX(100%);
opacity: 0;
pointer-events: none;
transition: transform 250ms cubic-bezier(0.4, 0, 0.2, 1),
opacity 200ms ease;
display: flex;
flex-direction: column;
z-index: 998; /* Below todo drawer */
}
swp-new-todo-drawer.active {
transform: translateX(0);
opacity: 1;
pointer-events: auto;
}
swp-new-todo-drawer swp-drawer-header {
display: flex;
align-items: center;
gap: 8px;
padding: 14px 16px;
border-bottom: 1px solid var(--color-border);
background: var(--color-background-alt);
}
swp-new-todo-drawer swp-drawer-title {
flex: 1;
}
swp-new-todo-drawer swp-drawer-content {
flex: 1;
overflow-y: auto;
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
}
swp-new-todo-drawer swp-drawer-footer {
flex-direction: row;
justify-content: flex-end;
}
/* Form Elements */
swp-new-todo-drawer swp-section-label {
display: block;
font-size: 11px;
font-weight: 400;
color: var(--color-text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 8px;
}
swp-new-todo-drawer swp-form-field {
display: block;
}
swp-new-todo-drawer swp-form-row {
display: flex;
gap: 12px;
}
swp-new-todo-drawer swp-form-row swp-form-field {
flex: 1;
}
swp-new-todo-drawer input[type="text"],
swp-new-todo-drawer input[type="date"],
swp-new-todo-drawer input[type="time"],
swp-new-todo-drawer select,
swp-new-todo-drawer textarea {
width: 100%;
padding: 10px 12px;
font-size: 14px;
font-family: var(--font-family);
color: var(--color-text);
background: var(--color-background-alt);
border: 1px solid var(--color-border);
border-radius: 4px;
outline: none;
transition: border-color 150ms ease;
}
swp-new-todo-drawer input:focus,
swp-new-todo-drawer select:focus,
swp-new-todo-drawer textarea:focus {
border-color: var(--color-teal);
}
swp-new-todo-drawer input::placeholder,
swp-new-todo-drawer textarea::placeholder {
color: var(--color-text-secondary);
}
swp-new-todo-drawer select {
cursor: pointer;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 256 256'%3E%3Cpath fill='%23666' d='M213.66 101.66l-80 80a8 8 0 0 1-11.32 0l-80-80a8 8 0 0 1 11.32-11.32L128 164.69l74.34-74.35a8 8 0 0 1 11.32 11.32Z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 10px center;
padding-right: 36px;
}
swp-new-todo-drawer textarea {
resize: none;
min-height: 80px;
}
/* Visibility toggle */
swp-visibility-toggle {
display: flex;
border: 1px solid var(--color-border);
border-radius: 4px;
overflow: hidden;
}
swp-visibility-option {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 10px 12px;
font-size: 13px;
color: var(--color-text-secondary);
background: var(--color-background-alt);
cursor: pointer;
transition: all 150ms ease;
border: none;
}
swp-visibility-option:first-child {
border-right: 1px solid var(--color-border);
}
swp-visibility-option i {
font-size: 16px;
}
swp-visibility-option:hover {
background: var(--color-background-hover);
}
swp-visibility-option.active {
background: color-mix(in srgb, var(--color-teal) 12%, transparent);
color: var(--color-teal);
}
/* Footer buttons */
swp-new-todo-drawer swp-btn {
padding: 10px 16px;
font-size: 14px;
font-family: var(--font-family);
border-radius: 6px;
cursor: pointer;
transition: all 150ms ease;
}
swp-new-todo-drawer swp-btn.secondary {
background: transparent;
border: 1px solid var(--color-border);
color: var(--color-text-secondary);
}
swp-new-todo-drawer swp-btn.secondary:hover {
background: var(--color-background-hover);
}
swp-new-todo-drawer swp-btn.primary {
background: var(--color-teal);
border: 1px solid var(--color-teal);
color: white;
}
swp-new-todo-drawer swp-btn.primary:hover {
background: color-mix(in srgb, var(--color-teal) 85%, black);
}

View file

@ -0,0 +1,395 @@
/**
* Reports - Statistik og Rapporter
*
* Feature-specific styling for reports pages.
* Reuses: swp-stats-row (stats.css), swp-stat-card (stats.css),
* swp-tab-bar (tabs.css), swp-data-table (components.css)
*/
/* ===========================================
CHARTS GRID (2-column layout)
=========================================== */
swp-charts-grid {
display: grid;
grid-template-columns: 2fr 1fr;
gap: var(--card-gap);
margin-bottom: var(--section-gap);
}
@media (max-width: 1200px) {
swp-charts-grid {
grid-template-columns: 1fr;
}
}
/* ===========================================
CHART CARD
=========================================== */
swp-chart-card {
display: block;
background: var(--color-surface);
border-radius: var(--border-radius-lg);
border: 1px solid var(--color-border);
overflow: hidden;
}
swp-chart-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--spacing-6) var(--card-padding);
border-bottom: 1px solid var(--color-border);
}
swp-chart-title {
font-size: var(--font-size-base);
font-weight: var(--font-weight-semibold);
color: var(--color-text);
}
swp-chart-hint {
font-size: var(--font-size-sm);
color: var(--color-text-secondary);
}
swp-chart-container {
display: block;
height: 240px;
position: relative;
}
/* ===========================================
FILTER BAR
=========================================== */
swp-filter-bar {
display: flex;
align-items: center;
gap: var(--spacing-6);
padding: var(--spacing-6) var(--card-padding);
background: var(--color-surface);
border-radius: var(--border-radius-lg);
margin-bottom: var(--section-gap);
flex-wrap: wrap;
}
swp-search-input {
display: flex;
align-items: center;
gap: var(--spacing-3);
padding: var(--spacing-3) var(--spacing-4);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-surface);
flex: 1;
max-width: 350px;
& i {
color: var(--color-text-secondary);
font-size: 18px;
}
& input {
border: none;
outline: none;
font-size: var(--font-size-md);
font-family: var(--font-family);
width: 100%;
background: transparent;
color: var(--color-text);
&::placeholder {
color: var(--color-text-muted);
}
}
}
swp-filter-group {
display: flex;
align-items: center;
gap: var(--spacing-3);
}
swp-filter-label {
font-size: var(--font-size-sm);
color: var(--color-text-secondary);
}
swp-filter-bar select,
swp-filter-bar input[type="date"] {
padding: var(--spacing-3) var(--spacing-4);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
font-size: var(--font-size-md);
font-family: var(--font-family);
background: var(--color-surface);
color: var(--color-text);
cursor: pointer;
}
swp-filter-bar input[type="date"] {
font-family: var(--font-mono);
}
/* ===========================================
SALES TABLE - Grid columns
=========================================== */
swp-card.sales-table {
padding: 0;
overflow: hidden;
}
swp-card.sales-table swp-data-table {
grid-template-columns: 100px 140px minmax(120px, 1fr) 120px minmax(140px, 1.2fr) 100px 120px 100px 40px;
}
swp-card.sales-table swp-data-table-header {
padding: var(--spacing-4) var(--card-padding);
}
swp-card.sales-table swp-data-table-row {
padding: var(--spacing-5) var(--card-padding);
cursor: pointer;
}
/* ===========================================
INVOICE CELL
=========================================== */
swp-invoice-cell {
font-family: var(--font-mono);
font-weight: var(--font-weight-medium);
font-size: var(--font-size-md);
color: var(--color-teal);
}
/* ===========================================
DATETIME CELL
=========================================== */
swp-datetime-cell {
display: flex;
flex-direction: column;
gap: 2px;
& .date {
font-size: var(--font-size-md);
color: var(--color-text);
}
& .time {
font-size: var(--font-size-sm);
color: var(--color-text-secondary);
font-family: var(--font-mono);
}
}
/* ===========================================
CUSTOMER CELL
=========================================== */
swp-customer-cell {
display: flex;
flex-direction: column;
gap: 2px;
& .name {
font-weight: var(--font-weight-medium);
color: var(--color-text);
}
& .phone {
font-size: var(--font-size-sm);
color: var(--color-text-secondary);
font-family: var(--font-mono);
}
}
/* ===========================================
SERVICES CELL
=========================================== */
swp-services-cell {
display: flex;
flex-direction: column;
gap: 2px;
& .main {
font-size: var(--font-size-md);
color: var(--color-text);
}
& .more {
font-size: var(--font-size-sm);
color: var(--color-text-secondary);
}
}
/* ===========================================
AMOUNT CELL
=========================================== */
swp-amount-cell {
font-family: var(--font-mono);
font-weight: var(--font-weight-semibold);
font-size: var(--font-size-base);
text-align: right;
display: block;
}
/* ===========================================
PAYMENT BADGE
=========================================== */
swp-payment-badge {
display: inline-flex;
align-items: center;
gap: var(--spacing-2);
padding: var(--spacing-2) var(--spacing-4);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
border-radius: var(--radius-md);
background: var(--color-background-alt);
color: var(--color-text-secondary);
& i {
font-size: 14px;
}
&.card {
background: color-mix(in srgb, var(--color-blue) 12%, transparent);
color: var(--color-blue);
}
&.cash {
background: color-mix(in srgb, var(--color-green) 12%, transparent);
color: var(--color-green);
}
&.mobilepay {
background: color-mix(in srgb, var(--color-blue) 12%, transparent);
color: var(--color-blue);
}
&.invoice {
background: color-mix(in srgb, var(--color-amber) 12%, transparent);
color: var(--color-amber);
}
&.giftcard {
background: color-mix(in srgb, var(--color-purple) 12%, transparent);
color: var(--color-purple);
}
}
/* ===========================================
STATUS BADGE ADDITIONS (paid, credited)
=========================================== */
swp-status-badge.paid {
background: var(--bg-green-strong);
color: var(--color-green);
}
swp-status-badge.credited {
background: var(--bg-purple-strong);
color: var(--color-purple);
}
/* ===========================================
ROW ARROW
=========================================== */
swp-row-arrow {
display: flex;
align-items: center;
justify-content: flex-end;
& i {
font-size: 18px;
color: var(--color-text-secondary);
transition: transform var(--transition-fast), color var(--transition-fast);
}
}
swp-data-table-row:hover swp-row-arrow i {
transform: translateX(4px);
color: var(--color-teal);
}
/* ===========================================
TABLE FOOTER + PAGINATION
=========================================== */
swp-table-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--spacing-5) var(--card-padding);
background: var(--color-background-alt);
border-top: 1px solid var(--color-border);
font-size: var(--font-size-md);
color: var(--color-text-secondary);
}
swp-pagination {
display: flex;
align-items: center;
gap: var(--spacing-1);
}
swp-page-btn {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: var(--radius-md);
font-size: var(--font-size-md);
font-weight: var(--font-weight-medium);
cursor: pointer;
transition: all var(--transition-fast);
background: transparent;
color: var(--color-text-secondary);
border: 1px solid transparent;
&:hover {
background: var(--color-background-hover);
color: var(--color-text);
}
&.active {
background: var(--color-teal);
color: white;
border-color: var(--color-teal);
}
& i {
font-size: 16px;
}
}
/* ===========================================
RESPONSIVE
=========================================== */
@media (max-width: 1200px) {
swp-card.sales-table swp-data-table {
grid-template-columns: 100px 130px 1fr 100px 100px 100px 40px;
}
/* Hide employee and services columns */
swp-card.sales-table swp-data-table-cell:nth-child(4),
swp-card.sales-table swp-data-table-cell:nth-child(5) {
display: none;
}
}
@media (max-width: 900px) {
swp-card.sales-table swp-data-table {
grid-template-columns: 100px 1fr 100px 100px 40px;
}
/* Hide customer column */
swp-card.sales-table swp-data-table-cell:nth-child(3) {
display: none;
}
swp-filter-bar {
flex-direction: column;
align-items: stretch;
}
swp-search-input {
max-width: none;
}
}