/** * Cash Controller * * Handles tab switching, cash calculations, and form interactions * for the Cash Register page. */ export class CashController { // Base values (from system - would come from server in real app) private readonly startBalance = 2000; private readonly cashSales = 3540; constructor() { this.setupTabs(); this.setupCashCalculation(); this.setupCheckboxSelection(); this.setupApprovalCheckbox(); this.setupDateFilters(); this.setupRowToggle(); this.setupDraftRowClick(); this.setupZReportButtons(); } /** * Setup tab switching functionality */ private setupTabs(): void { const tabs = document.querySelectorAll('swp-tab[data-tab]'); tabs.forEach(tab => { tab.addEventListener('click', () => { const targetTab = tab.dataset.tab; if (targetTab) { this.switchToTab(targetTab); } }); }); } /** * Switch to a specific tab by name */ private switchToTab(targetTab: string): void { const tabs = document.querySelectorAll('swp-tab[data-tab]'); const contents = document.querySelectorAll('swp-tab-content[data-tab]'); const statsBars = document.querySelectorAll('swp-cash-stats[data-for-tab]'); // Update tab states tabs.forEach(t => { if (t.dataset.tab === targetTab) { t.classList.add('active'); } else { t.classList.remove('active'); } }); // Update content visibility contents.forEach(content => { if (content.dataset.tab === targetTab) { content.classList.add('active'); } else { content.classList.remove('active'); } }); // Update stats bar visibility statsBars.forEach(stats => { if (stats.dataset.forTab === targetTab) { stats.classList.add('active'); } else { stats.classList.remove('active'); } }); } /** * Setup cash calculation with real-time updates */ private setupCashCalculation(): void { const payoutsInput = document.getElementById('payouts') as HTMLInputElement; const toBankInput = document.getElementById('toBank') as HTMLInputElement; const actualCashInput = document.getElementById('actualCash') as HTMLInputElement; if (!payoutsInput || !toBankInput || !actualCashInput) return; const calculate = () => this.calculateCash(payoutsInput, toBankInput, actualCashInput); payoutsInput.addEventListener('input', calculate); toBankInput.addEventListener('input', calculate); actualCashInput.addEventListener('input', calculate); // Setup Enter key navigation between fields this.setupEnterNavigation([payoutsInput, toBankInput, actualCashInput]); // Initial calculation calculate(); } /** * Setup Enter key to move focus to next input field */ private setupEnterNavigation(inputs: HTMLInputElement[]): void { inputs.forEach((input, index) => { input.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); const nextIndex = index + 1; if (nextIndex < inputs.length) { inputs[nextIndex].focus(); inputs[nextIndex].select(); } } }); }); } /** * Calculate expected cash and difference */ private calculateCash( payoutsInput: HTMLInputElement, toBankInput: HTMLInputElement, actualCashInput: HTMLInputElement ): void { const payouts = this.parseNumber(payoutsInput.value); const toBank = this.parseNumber(toBankInput.value); const actual = this.parseNumber(actualCashInput.value); // Expected = start + sales - payouts - to bank const expectedCash = this.startBalance + this.cashSales - payouts - toBank; const expectedElement = document.getElementById('expectedCash'); if (expectedElement) { expectedElement.textContent = this.formatNumber(expectedCash); } // Calculate and display difference this.updateDifference(actual, expectedCash); } /** * Update difference box with color coding */ private updateDifference(actual: number, expected: number): void { const box = document.getElementById('differenceBox'); const value = document.getElementById('differenceValue'); if (!box || !value) return; const diff = actual - expected; // Remove all state classes box.classList.remove('positive', 'negative', 'neutral'); if (diff > 0) { // More cash than expected value.textContent = '+' + this.formatNumber(diff) + ' kr'; box.classList.add('positive'); } else if (diff < 0) { // Less cash than expected value.textContent = this.formatNumber(diff) + ' kr'; box.classList.add('negative'); } else { // Exact match value.textContent = '0,00 kr'; box.classList.add('neutral'); } } /** * Setup checkbox selection for table rows */ private setupCheckboxSelection(): void { const selectAll = document.getElementById('selectAll') as HTMLInputElement; const rowCheckboxes = document.querySelectorAll('.row-select'); const exportBtn = document.getElementById('exportBtn') as HTMLButtonElement; const selectionCount = document.getElementById('selectionCount'); if (!selectAll || !exportBtn || !selectionCount) return; const updateSelection = () => { const checked = document.querySelectorAll('.row-select:checked'); const count = checked.length; selectionCount.textContent = count === 0 ? '0 valgt' : `${count} valgt`; exportBtn.disabled = count === 0; // Update select all state selectAll.checked = count === rowCheckboxes.length && count > 0; selectAll.indeterminate = count > 0 && count < rowCheckboxes.length; }; selectAll.addEventListener('change', () => { rowCheckboxes.forEach(cb => cb.checked = selectAll.checked); updateSelection(); }); rowCheckboxes.forEach(cb => { cb.addEventListener('change', updateSelection); // Stop click from bubbling to row cb.addEventListener('click', e => e.stopPropagation()); }); } /** * Setup approval checkbox to enable/disable approve button */ private setupApprovalCheckbox(): void { const checkbox = document.getElementById('confirmCheckbox') as HTMLInputElement; const approveBtn = document.getElementById('approveBtn') as HTMLButtonElement; if (!checkbox || !approveBtn) return; checkbox.addEventListener('change', () => { approveBtn.disabled = !checkbox.checked; }); } /** * Setup date filter defaults (last 30 days) */ private setupDateFilters(): void { const dateFrom = document.getElementById('dateFrom') as HTMLInputElement; const dateTo = document.getElementById('dateTo') as HTMLInputElement; if (!dateFrom || !dateTo) return; const today = new Date(); const thirtyDaysAgo = new Date(today); thirtyDaysAgo.setDate(today.getDate() - 30); dateTo.value = this.formatDateISO(today); dateFrom.value = this.formatDateISO(thirtyDaysAgo); } /** * Format number as Danish currency */ private formatNumber(num: number): string { return num.toLocaleString('da-DK', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } /** * Parse Danish number format */ private parseNumber(str: string): number { if (!str) return 0; return parseFloat(str.replace(/\./g, '').replace(',', '.')) || 0; } /** * Format date as ISO string (YYYY-MM-DD) */ private formatDateISO(date: Date): string { return date.toISOString().split('T')[0]; } /** * Setup row toggle for expandable details */ private setupRowToggle(): void { const rows = document.querySelectorAll('swp-cash-table-row[data-id]:not(.draft-row)'); rows.forEach(row => { const rowId = row.getAttribute('data-id'); if (!rowId) return; const detail = document.querySelector(`swp-cash-row-detail[data-for="${rowId}"]`); if (!detail) return; row.addEventListener('click', (e) => { // Don't toggle if clicking on checkbox if ((e.target as HTMLElement).closest('input[type="checkbox"]')) return; const icon = row.querySelector('swp-row-toggle i'); const isExpanded = row.classList.contains('expanded'); // Close other expanded rows document.querySelectorAll('swp-cash-table-row.expanded').forEach(r => { if (r !== row) { const otherId = r.getAttribute('data-id'); if (otherId) { const otherDetail = document.querySelector(`swp-cash-row-detail[data-for="${otherId}"]`); const otherIcon = r.querySelector('swp-row-toggle i'); if (otherDetail && otherIcon) { this.collapseRow(r, otherDetail, otherIcon as HTMLElement); } } } }); // Toggle current row if (isExpanded) { this.collapseRow(row, detail, icon); } else { this.expandRow(row, detail, icon); } }); }); } /** * Expand a row with animation */ private expandRow(row: Element, detail: HTMLElement, icon: Element | null): void { row.classList.add('expanded'); detail.classList.add('expanded'); // Animate icon rotation icon?.animate([ { transform: 'rotate(0deg)' }, { transform: 'rotate(90deg)' } ], { duration: 200, easing: 'ease-out', fill: 'forwards' }); // Animate detail expansion const content = detail.querySelector('swp-row-detail-content') as HTMLElement; if (content) { const height = content.offsetHeight; detail.animate([ { height: '0px', opacity: 0 }, { height: `${height}px`, opacity: 1 } ], { duration: 250, easing: 'ease-out', fill: 'forwards' }); } } /** * Collapse a row with animation */ private collapseRow(row: Element, detail: HTMLElement, icon: Element | null): void { // Animate icon rotation icon?.animate([ { transform: 'rotate(90deg)' }, { transform: 'rotate(0deg)' } ], { duration: 200, easing: 'ease-out', fill: 'forwards' }); // Animate detail collapse const content = detail.querySelector('swp-row-detail-content') as HTMLElement; if (content) { const height = content.offsetHeight; const animation = detail.animate([ { height: `${height}px`, opacity: 1 }, { height: '0px', opacity: 0 } ], { duration: 200, easing: 'ease-out', fill: 'forwards' }); animation.onfinish = () => { row.classList.remove('expanded'); detail.classList.remove('expanded'); }; } else { row.classList.remove('expanded'); detail.classList.remove('expanded'); } } /** * Setup draft row click to navigate to reconciliation tab */ private setupDraftRowClick(): void { const draftRow = document.querySelector('swp-cash-table-row.draft-row'); if (!draftRow) return; draftRow.style.cursor = 'pointer'; draftRow.addEventListener('click', (e) => { // Don't navigate if clicking on checkbox if ((e.target as HTMLElement).closest('input[type="checkbox"]')) return; 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'); } }); }); } }