From f3c54dde3597a925bbc27cbd68787c68a4656e52 Mon Sep 17 00:00:00 2001 From: "Janus C. H. Knudsen" Date: Fri, 23 Jan 2026 16:25:43 +0100 Subject: [PATCH] Adds Z-Report feature with PDF generation and UI components Introduces Z-Report page and related functionality for cash register reconciliation - Adds new Z-Report page template with comprehensive financial reporting - Updates reconciliation table with Z-Report download buttons - Implements print-optimized CSS for report styling - Adds TypeScript handler for Z-Report button interactions --- .../.claude/settings.local.json | 3 +- .../ReconciliationTable/Default.cshtml | 24 +- .../CashRegister/Pages/ZReport.cshtml | 173 ++++++++ .../CashRegister/Pages/ZReport.cshtml.cs | 13 + PlanTempus.Application/wwwroot/css/print.css | 391 ++++++++++++++++++ .../wwwroot/ts/modules/cash.ts | 20 + 6 files changed, 603 insertions(+), 21 deletions(-) create mode 100644 PlanTempus.Application/Features/CashRegister/Pages/ZReport.cshtml create mode 100644 PlanTempus.Application/Features/CashRegister/Pages/ZReport.cshtml.cs create mode 100644 PlanTempus.Application/wwwroot/css/print.css diff --git a/PlanTempus.Application/.claude/settings.local.json b/PlanTempus.Application/.claude/settings.local.json index 0a37a01..3cb57dd 100644 --- a/PlanTempus.Application/.claude/settings.local.json +++ b/PlanTempus.Application/.claude/settings.local.json @@ -1,7 +1,8 @@ { "permissions": { "allow": [ - "WebSearch" + "WebSearch", + "Bash(npm run build:*)" ] } } diff --git a/PlanTempus.Application/Features/CashRegister/Components/ReconciliationTable/Default.cshtml b/PlanTempus.Application/Features/CashRegister/Components/ReconciliationTable/Default.cshtml index 3a385e3..a341694 100644 --- a/PlanTempus.Application/Features/CashRegister/Components/ReconciliationTable/Default.cshtml +++ b/PlanTempus.Application/Features/CashRegister/Components/ReconciliationTable/Default.cshtml @@ -62,11 +62,7 @@ - - - Download CSV - - + Download PDF @@ -97,11 +93,7 @@ - - - Download CSV - - + Download PDF @@ -132,11 +124,7 @@ - - - Download CSV - - + Download PDF @@ -167,11 +155,7 @@ - - - Download CSV - - + Download PDF diff --git a/PlanTempus.Application/Features/CashRegister/Pages/ZReport.cshtml b/PlanTempus.Application/Features/CashRegister/Pages/ZReport.cshtml new file mode 100644 index 0000000..df922b1 --- /dev/null +++ b/PlanTempus.Application/Features/CashRegister/Pages/ZReport.cshtml @@ -0,0 +1,173 @@ +@page "/kasse/z-rapport/{id}" +@model PlanTempus.Application.Features.CashRegister.Pages.ZReportModel +@{ + Layout = null; +} + + + + + + Z-Rapport Z-@Model.ReportId - Salon OS + + + + + + + + + + + + Print / Gem som PDF + + + + Luk + + + + + + + Salon Harmoni + Nørregade 42, 8000 Aarhus C + CVR: 12345678 + + + Z-Rapport + Z-@Model.ReportId + + + + + + + Periode + + + Fra + 28. dec 2025 kl. 18:00 + + + Til + 29. dec 2025 kl. 17:45 + + + Kassepunkt + Kasse 1 - Reception + + + Åbnet af + Anna Jensen + + + + + + Omsætning + + + Kortbetalinger + 12.875,50 kr + + + MobilePay / Online + 2.450,00 kr + + + Kontantsalg + 3.540,00 kr + + + Total omsætning + 18.865,50 kr + + + Heraf moms (25%) + 3.773,10 kr + + + + + + + + Kontanter + + + Startbeholdning + 2.000,00 kr + + + + Kontantsalg + 3.540,00 kr + + + - Udbetalinger / Bilag + 320,00 kr + + + - Udtaget til bank + 2.000,00 kr + + + + Forventet beholdning + 3.220,00 kr + + + Optalt beholdning + 3.195,00 kr + + + Kassedifference + -25,00 kr + + + + + + + + Note + + Difference skyldes vekslepenge givet forkert. + + + + + Godkendelse + + + Status + + + Godkendt + + + + Afsluttet af + Anna Jensen + + + Godkendt af + Karina Knudsen + + + Tidspunkt + 29. dec 2025 kl. 17:52 + + + + + + + + Genereret: 29. december 2025 kl. 17:55 + Afstemnings-ID: KA-2025-12-29 + + + + diff --git a/PlanTempus.Application/Features/CashRegister/Pages/ZReport.cshtml.cs b/PlanTempus.Application/Features/CashRegister/Pages/ZReport.cshtml.cs new file mode 100644 index 0000000..549d4d7 --- /dev/null +++ b/PlanTempus.Application/Features/CashRegister/Pages/ZReport.cshtml.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace PlanTempus.Application.Features.CashRegister.Pages; + +public class ZReportModel : PageModel +{ + public string ReportId { get; set; } = string.Empty; + + public void OnGet(string id) + { + ReportId = id ?? string.Empty; + } +} diff --git a/PlanTempus.Application/wwwroot/css/print.css b/PlanTempus.Application/wwwroot/css/print.css new file mode 100644 index 0000000..2609465 --- /dev/null +++ b/PlanTempus.Application/wwwroot/css/print.css @@ -0,0 +1,391 @@ +/** + * Print Styles - Z-Rapport + * + * Print-optimeret layout der matcher app'ens design system. + * Genbruger tokens fra design-tokens.css. + */ + +/* =========================================== + BASE STYLES + =========================================== */ +body { + margin: 0; + padding: 0; + font-family: var(--font-family); + font-size: var(--font-size-base); + line-height: var(--line-height-normal); + color: var(--color-text); + background: var(--color-background); +} + +/* =========================================== + Z-RAPPORT CONTAINER + =========================================== */ +swp-z-report { + display: block; + max-width: 700px; + margin: 0 auto; + padding: var(--spacing-16); + background: var(--color-surface); +} + +/* =========================================== + PRINT ACTIONS (Screen only) + =========================================== */ +swp-z-actions { + display: flex; + gap: var(--spacing-6); + justify-content: center; + margin-bottom: var(--spacing-16); + padding-bottom: var(--spacing-12); + border-bottom: 1px dashed var(--color-border); +} + +swp-z-actions swp-btn { + display: inline-flex; + align-items: center; + gap: var(--spacing-4); + padding: var(--spacing-6) var(--spacing-12); + font-size: var(--font-size-base); + font-weight: var(--font-weight-medium); + font-family: var(--font-family); + border: none; + border-radius: var(--radius-lg); + cursor: pointer; + transition: all var(--transition-fast); +} + +swp-z-actions swp-btn.primary { + background: var(--color-teal); + color: #fff; +} + +swp-z-actions swp-btn.primary:hover { + filter: brightness(0.95); +} + +swp-z-actions swp-btn.secondary { + background: var(--color-background); + color: var(--color-text); + border: 1px solid var(--color-border); +} + +swp-z-actions swp-btn.secondary:hover { + background: var(--color-background-hover); +} + +/* =========================================== + REPORT HEADER + =========================================== */ +swp-z-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + padding-bottom: var(--spacing-10); + margin-bottom: var(--spacing-10); + border-bottom: 1px solid var(--color-border); +} + +swp-z-company { + display: block; +} + +swp-z-company-name { + display: block; + font-size: var(--font-size-base); + font-weight: var(--font-weight-semibold); + color: var(--color-text); + margin-bottom: var(--spacing-1); +} + +swp-z-company-address, +swp-z-company-cvr { + display: block; + font-size: var(--font-size-xs); + color: var(--color-text-secondary); + line-height: var(--line-height-relaxed); +} + +/* =========================================== + REPORT TITLE + =========================================== */ +swp-z-title { + display: block; + text-align: right; +} + +swp-z-title-label { + display: block; + font-size: var(--font-size-xs); + font-weight: var(--font-weight-medium); + color: var(--color-text-secondary); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +swp-z-id { + display: block; + font-size: var(--font-size-lg); + font-weight: var(--font-weight-semibold); + font-family: var(--font-mono); + color: var(--color-text); +} + +/* =========================================== + ROW GRID (Side-by-side sections) + =========================================== */ +swp-z-row-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--spacing-8); + margin-bottom: var(--spacing-12); + margin-top: 20px; +} + +swp-z-row-grid swp-z-section { + margin-bottom: 0; +} + +swp-z-row-grid swp-z-label, +swp-z-row-grid swp-z-value, +swp-z-row-grid swp-z-amount, +swp-z-row-grid swp-z-note { + font-size: var(--font-size-sm); +} + +swp-z-row-grid swp-z-row.total swp-z-label, +swp-z-row-grid swp-z-row.total swp-z-amount { + font-size: var(--font-size-sm); +} + +swp-z-row-grid swp-z-value.approved { + font-size: var(--font-size-xs); +} + +/* =========================================== + SECTIONS + =========================================== */ +swp-z-section { + display: block; + background: var(--color-background-alt); + border-radius: var(--radius-lg); + padding: var(--spacing-8); + margin-bottom: var(--spacing-8); +} + +swp-z-section:last-of-type { + margin-bottom: 0; +} + +swp-z-section-title { + display: block; + font-size: var(--font-size-xs); + font-weight: var(--font-weight-semibold); + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--color-text-secondary); + margin-bottom: var(--spacing-6); + padding-bottom: var(--spacing-3); + border-bottom: 1px solid var(--color-border); +} + +swp-z-section-content { + display: block; +} + +/* =========================================== + ROWS + =========================================== */ +swp-z-row { + display: flex; + justify-content: space-between; + align-items: baseline; + padding: var(--spacing-3) 0; +} + +swp-z-row.total { + font-weight: var(--font-weight-semibold); + padding-top: var(--spacing-6); + margin-top: var(--spacing-4); + border-top: 2px solid var(--color-border); +} + +swp-z-row.muted { + color: var(--color-text-secondary); + font-size: var(--font-size-sm); +} + +swp-z-row.difference { + font-weight: var(--font-weight-semibold); + padding: var(--spacing-6); + margin-top: var(--spacing-4); + border-radius: var(--radius-md); +} + +swp-z-row.difference.negative { + background: var(--bg-red-subtle); +} + +swp-z-row.difference.negative swp-z-amount { + color: var(--color-red); +} + +swp-z-row.difference.positive { + background: var(--bg-green-subtle); +} + +swp-z-row.difference.positive swp-z-amount { + color: var(--color-green); +} + +swp-z-row.difference.neutral { + background: var(--bg-teal-subtle); +} + +swp-z-row.difference.neutral swp-z-amount { + color: var(--color-teal); +} + +swp-z-label { + font-size: var(--font-size-base); + color: var(--color-text); +} + +swp-z-value { + font-size: var(--font-size-base); + color: var(--color-text); +} + +swp-z-value.approved { + display: inline-flex; + align-items: center; + gap: var(--spacing-3); + padding: var(--spacing-2) var(--spacing-4); + background: var(--bg-green-strong); + color: var(--color-green); + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); + border-radius: var(--radius-pill); +} + +swp-z-amount { + font-family: var(--font-mono); + font-size: var(--font-size-base); + text-align: right; + color: var(--color-text); +} + +/* =========================================== + DIVIDER + =========================================== */ +swp-z-divider { + display: block; + height: 1px; + background: var(--color-border); + margin: var(--spacing-6) 0; +} + +/* =========================================== + NOTE + =========================================== */ +swp-z-note { + display: block; + font-size: var(--font-size-base); + color: var(--color-text-secondary); + font-style: italic; + padding: var(--spacing-4) 0; +} + +swp-z-note.empty { + color: var(--color-text-muted); +} + +/* =========================================== + FOOTER + =========================================== */ +swp-z-footer { + display: block; + text-align: center; + padding-top: var(--spacing-12); + margin-top: var(--spacing-12); + border-top: 1px solid var(--color-border); +} + +swp-z-generated, +swp-z-ref { + display: block; + font-size: var(--font-size-xs); + color: var(--color-text-muted); +} + +swp-z-ref { + font-family: var(--font-mono); + margin-top: var(--spacing-2); +} + +/* =========================================== + PRINT STYLES + =========================================== */ +@media print { + @page { + size: A4; + margin: 15mm; + } + + body { + -webkit-print-color-adjust: exact; + print-color-adjust: exact; + background: #fff; + } + + .no-print, + swp-z-actions { + display: none !important; + } + + swp-z-report { + padding: 0; + max-width: none; + box-shadow: none; + } + + /* Ensure colors print */ + swp-z-section { + background: #fafafa !important; + } + + swp-z-row.difference.negative { + background: #ffebee !important; + } + + swp-z-row.difference.negative swp-z-amount { + color: #e53935 !important; + } + + swp-z-row.difference.positive { + background: #e8f5e9 !important; + } + + swp-z-row.difference.positive swp-z-amount { + color: #43a047 !important; + } + + swp-z-value.approved { + background: #c8e6c9 !important; + color: #2e7d32 !important; + } +} + +/* =========================================== + SCREEN PREVIEW + =========================================== */ +@media screen { + body { + padding: var(--spacing-16); + } + + swp-z-report { + box-shadow: var(--shadow-lg); + border-radius: var(--radius-xl); + } +} diff --git a/PlanTempus.Application/wwwroot/ts/modules/cash.ts b/PlanTempus.Application/wwwroot/ts/modules/cash.ts index 2892122..9c3d955 100644 --- a/PlanTempus.Application/wwwroot/ts/modules/cash.ts +++ b/PlanTempus.Application/wwwroot/ts/modules/cash.ts @@ -18,6 +18,7 @@ export class CashController { this.setupDateFilters(); this.setupRowToggle(); this.setupDraftRowClick(); + this.setupZReportButtons(); } /** @@ -384,4 +385,23 @@ export class CashController { this.switchToTab('afstemning'); }); } + + /** + * Setup Z-report PDF download buttons + */ + private setupZReportButtons(): void { + const buttons = document.querySelectorAll('[data-zreport]'); + + buttons.forEach(btn => { + btn.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + + const reportId = btn.dataset.zreport; + if (reportId) { + window.open(`/kasse/z-rapport/${reportId}`, '_blank'); + } + }); + }); + } }