/** * Employees Controller * * Handles content swap between list view and detail view, * plus tab switching within each view. * Uses History API for browser back/forward navigation. */ export class EmployeesController { private ratesSync: RatesSyncController | null = null; private listView: HTMLElement | null = null; private detailView: HTMLElement | null = null; constructor() { this.listView = document.getElementById('employees-list-view'); this.detailView = document.getElementById('employee-detail-view'); // Only initialize if we're on the employees page if (!this.listView) return; this.setupListTabs(); this.setupDetailTabs(); this.setupChevronNavigation(); this.setupBackNavigation(); this.setupHistoryNavigation(); this.restoreStateFromUrl(); this.ratesSync = new RatesSyncController(); } /** * Setup popstate listener for browser back/forward */ private setupHistoryNavigation(): void { window.addEventListener('popstate', (e: PopStateEvent) => { if (e.state?.employeeKey) { this.showDetailViewInternal(e.state.employeeKey); } else { this.showListViewInternal(); } }); } /** * Restore view state from URL on page load */ private restoreStateFromUrl(): void { const hash = window.location.hash; if (hash.startsWith('#employee-')) { const employeeKey = hash.substring(1); // Remove # this.showDetailViewInternal(employeeKey); } } /** * Setup tab switching for the list view */ private setupListTabs(): void { if (!this.listView) return; const tabs = this.listView.querySelectorAll('swp-tab-bar > swp-tab[data-tab]'); tabs.forEach(tab => { tab.addEventListener('click', () => { const targetTab = tab.dataset.tab; if (targetTab) { this.switchTab(this.listView!, targetTab); } }); }); } /** * Setup tab switching for the detail view */ private setupDetailTabs(): void { if (!this.detailView) return; const tabs = this.detailView.querySelectorAll('swp-tab-bar > swp-tab[data-tab]'); tabs.forEach(tab => { tab.addEventListener('click', () => { const targetTab = tab.dataset.tab; if (targetTab) { this.switchTab(this.detailView!, targetTab); } }); }); } /** * Switch to a specific tab within a container */ private switchTab(container: HTMLElement, targetTab: string): void { const tabs = container.querySelectorAll('swp-tab-bar > swp-tab[data-tab]'); const contents = container.querySelectorAll('swp-tab-content[data-tab]'); tabs.forEach(t => { t.classList.toggle('active', t.dataset.tab === targetTab); }); contents.forEach(content => { content.classList.toggle('active', content.dataset.tab === targetTab); }); } /** * Setup row click to show detail view * Ignores clicks on action buttons */ private setupChevronNavigation(): void { document.addEventListener('click', (e: Event) => { const target = e.target as HTMLElement; // Ignore clicks on action buttons if (target.closest('swp-icon-btn') || target.closest('swp-table-actions')) { return; } const row = target.closest('swp-employee-row[data-employee-detail]'); if (row) { const employeeKey = row.dataset.employeeDetail; if (employeeKey) { this.showDetailView(employeeKey); } } }); } /** * Setup back button to return to list view */ private setupBackNavigation(): void { document.addEventListener('click', (e: Event) => { const target = e.target as HTMLElement; const backLink = target.closest('[data-employee-back]'); if (backLink) { this.showListView(); } }); } /** * Show the detail view and hide list view (with history push) */ private showDetailView(employeeKey: string): void { // Push state to history history.pushState( { employeeKey }, '', `#${employeeKey}` ); this.showDetailViewInternal(employeeKey); } /** * Show detail view without modifying history (for popstate) */ private showDetailViewInternal(employeeKey: string): void { if (this.listView && this.detailView) { this.listView.style.display = 'none'; this.detailView.style.display = 'block'; this.detailView.dataset.employee = employeeKey; // Reset to first tab this.switchTab(this.detailView, 'general'); } } /** * Show the list view and hide detail view (with history push) */ private showListView(): void { // Push state to history (clear hash) history.pushState( {}, '', window.location.pathname ); this.showListViewInternal(); } /** * Show list view without modifying history (for popstate) */ private showListViewInternal(): void { if (this.listView && this.detailView) { this.detailView.style.display = 'none'; this.listView.style.display = 'block'; } } } /** * Rates Sync Controller * * Syncs changes between the rates drawer and the salary tab cards. * Uses ID-based lookups: * - Checkbox: id="rate-{key}-enabled" * - Text input: id="rate-{key}" * - Card row: id="card-{key}" */ class RatesSyncController { private drawer: HTMLElement | null = null; constructor() { this.drawer = document.getElementById('rates-drawer'); if (!this.drawer) return; this.setupCheckboxListeners(); this.setupInputListeners(); } /** * Extract rate key from checkbox ID (e.g., "rate-normal-enabled" → "normal") */ private extractRateKey(checkboxId: string): string | null { const match = checkboxId.match(/^rate-(.+)-enabled$/); return match ? match[1] : null; } /** * Setup checkbox change listeners in drawer */ private setupCheckboxListeners(): void { if (!this.drawer) return; this.drawer.addEventListener('change', (e: Event) => { const target = e.target as HTMLInputElement; if (target.type !== 'checkbox' || !target.id) return; const rateKey = this.extractRateKey(target.id); if (!rateKey) return; const isChecked = target.checked; const row = target.closest('swp-data-row'); if (!row) return; // Toggle disabled class in drawer row const label = row.querySelector('swp-data-label'); const input = row.querySelector('swp-data-input'); if (label) label.classList.toggle('disabled', !isChecked); if (input) input.classList.toggle('disabled', !isChecked); // Toggle visibility in card this.toggleCardRow(rateKey, isChecked); // If enabling, also sync the current value if (isChecked) { const textInput = document.getElementById(`rate-${rateKey}`) as HTMLInputElement | null; if (textInput) { this.syncValueToCard(rateKey, textInput.value); } } }); } /** * Setup input change listeners in drawer */ private setupInputListeners(): void { if (!this.drawer) return; this.drawer.addEventListener('input', (e: Event) => { const target = e.target as HTMLInputElement; if (target.type !== 'text' || !target.id) return; // Extract rate key from input ID (e.g., "rate-normal" → "normal") const match = target.id.match(/^rate-(.+)$/); if (!match) return; const rateKey = match[1]; // Skip if this matches the checkbox pattern if (rateKey.endsWith('-enabled')) return; this.syncValueToCard(rateKey, target.value); }); } /** * Toggle card row visibility by ID */ private toggleCardRow(rateKey: string, visible: boolean): void { const cardRow = document.getElementById(`card-${rateKey}`); if (cardRow) { cardRow.style.display = visible ? '' : 'none'; } } /** * Format number with 2 decimals using Danish locale (comma as decimal separator) */ private formatNumber(value: string): string { // Parse the input (handle both dot and comma as decimal separator) const normalized = value.replace(',', '.'); const num = parseFloat(normalized); if (isNaN(num)) return value; // Format with 2 decimals and comma as decimal separator return num.toFixed(2).replace('.', ','); } /** * Sync value from drawer to card by ID */ private syncValueToCard(rateKey: string, value: string): void { const cardInput = document.getElementById(`value-${rateKey}`) as HTMLInputElement | null; if (!cardInput) return; // Get the unit from drawer input container const textInput = document.getElementById(`rate-${rateKey}`); const inputContainer = textInput?.closest('swp-data-input'); const unit = inputContainer?.textContent?.trim().replace(value, '').trim() || 'kr'; // Format with 2 decimals const formattedValue = this.formatNumber(value); cardInput.value = `${formattedValue} ${unit}`; } }