/** * 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 scheduleController: ScheduleController | 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(); this.scheduleController = new ScheduleController(); } /** * 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-data-table-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) { // Fade out list view this.listView.classList.add('view-fade-out'); // After fade, switch views setTimeout(() => { this.listView!.style.display = 'none'; this.listView!.classList.remove('view-fade-out'); // Show detail view with fade in this.detailView!.style.display = 'block'; this.detailView!.classList.add('view-fade-out'); this.detailView!.dataset.employee = employeeKey; // Reset to first tab this.switchTab(this.detailView!, 'general'); // Trigger fade in requestAnimationFrame(() => { this.detailView!.classList.remove('view-fade-out'); }); }, 100); } } /** * 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) { // Fade out detail view this.detailView.classList.add('view-fade-out'); // After fade, switch views setTimeout(() => { this.detailView!.style.display = 'none'; this.detailView!.classList.remove('view-fade-out'); // Show list view with fade in this.listView!.style.display = 'block'; this.listView!.classList.add('view-fade-out'); // Trigger fade in requestAnimationFrame(() => { this.listView!.classList.remove('view-fade-out'); }); }, 100); } } } /** * 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(); this.setupDoubleClickToEdit(); } /** * 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}`; } /** * Setup double-click on salary card inputs to open drawer and focus field */ private setupDoubleClickToEdit(): void { document.addEventListener('dblclick', (e: Event) => { const target = e.target as HTMLElement; const input = target.closest('input[id^="value-"]'); if (!input || !input.id) return; // Extract key from value-{key} const match = input.id.match(/^value-(.+)$/); if (!match) return; const rateKey = match[1]; this.openDrawerAndFocus(rateKey); }); } /** * Open drawer and focus the corresponding field with highlight */ private openDrawerAndFocus(rateKey: string): void { // Open the drawer const trigger = document.querySelector('[data-drawer-trigger="rates-drawer"]'); trigger?.click(); // Wait for drawer to open, then focus field requestAnimationFrame(() => { const drawerInput = document.getElementById(`rate-${rateKey}`) as HTMLInputElement | null; if (!drawerInput) return; // Focus the input drawerInput.focus(); drawerInput.select(); // Add highlight to row const row = drawerInput.closest('swp-data-row'); if (row) { row.classList.add('focus-highlight'); // Remove class after animation setTimeout(() => row.classList.remove('focus-highlight'), 1000); } }); } } /** * Schedule Controller * * Handles work schedule (vagtplan) functionality: * - Edit mode toggle * - Cell selection (single, ctrl+click, shift+click) * - Drawer interaction * - Time range slider * - Status options */ class ScheduleController { private isEditMode = false; private selectedCells: HTMLElement[] = []; private anchorCell: HTMLElement | null = null; private drawer: HTMLElement | null = null; private editBtn: HTMLElement | null = null; private scheduleTable: HTMLElement | null = null; // Time range constants private readonly TIME_RANGE_MAX = 60; // 15 hours (06:00-21:00) * 4 intervals constructor() { this.drawer = document.getElementById('schedule-drawer'); this.editBtn = document.getElementById('scheduleEditBtn'); this.scheduleTable = document.getElementById('scheduleTable'); if (!this.scheduleTable) return; this.setupEditModeToggle(); this.setupCellSelection(); this.setupStatusOptions(); this.setupTypeToggle(); this.setupTimeRangeSlider(); this.setupDrawerSave(); } /** * Setup edit mode toggle button */ private setupEditModeToggle(): void { this.editBtn?.addEventListener('click', () => { this.isEditMode = !this.isEditMode; document.body.classList.toggle('schedule-edit-mode', this.isEditMode); if (this.editBtn) { const icon = this.editBtn.querySelector('i'); const text = this.editBtn.childNodes[this.editBtn.childNodes.length - 1]; if (this.isEditMode) { icon?.classList.replace('ph-pencil-simple', 'ph-check'); if (text && text.nodeType === Node.TEXT_NODE) { text.textContent = ' Færdig'; } this.openDrawer(); this.showEmptyState(); } else { icon?.classList.replace('ph-check', 'ph-pencil-simple'); if (text && text.nodeType === Node.TEXT_NODE) { text.textContent = ' Rediger'; } this.closeDrawer(); this.clearSelection(); } } }); } /** * Setup cell click selection */ private setupCellSelection(): void { if (!this.scheduleTable) return; const dayCells = this.scheduleTable.querySelectorAll('swp-schedule-cell.day'); dayCells.forEach(cell => { // Double-click to enter edit mode cell.addEventListener('dblclick', () => { if (!this.isEditMode) { this.isEditMode = true; document.body.classList.add('schedule-edit-mode'); if (this.editBtn) { const icon = this.editBtn.querySelector('i'); const text = this.editBtn.childNodes[this.editBtn.childNodes.length - 1]; icon?.classList.replace('ph-pencil-simple', 'ph-check'); if (text && text.nodeType === Node.TEXT_NODE) { text.textContent = ' Færdig'; } } this.openDrawer(); this.clearSelection(); cell.classList.add('selected'); this.selectedCells = [cell]; this.anchorCell = cell; this.updateDrawerFields(); } }); // Click selection in edit mode cell.addEventListener('click', (e: MouseEvent) => { if (!this.isEditMode) return; if (e.shiftKey && this.anchorCell) { // Shift+click: range selection this.selectRange(this.anchorCell, cell); } else if (e.ctrlKey || e.metaKey) { // Ctrl/Cmd+click: toggle selection if (cell.classList.contains('selected')) { cell.classList.remove('selected'); this.selectedCells = this.selectedCells.filter(c => c !== cell); } else { cell.classList.add('selected'); this.selectedCells.push(cell); this.anchorCell = cell; } } else { // Single click: replace selection this.clearSelection(); cell.classList.add('selected'); this.selectedCells = [cell]; this.anchorCell = cell; } this.updateDrawerFields(); }); }); } /** * Select a range of cells between anchor and target */ private selectRange(anchor: HTMLElement, target: HTMLElement): void { if (!this.scheduleTable) return; const allDayCells = Array.from(this.scheduleTable.querySelectorAll('swp-schedule-cell.day')); const anchorIdx = allDayCells.indexOf(anchor); const targetIdx = allDayCells.indexOf(target); // Calculate grid positions (7 columns for days) const anchorRow = Math.floor(anchorIdx / 7); const anchorCol = anchorIdx % 7; const targetRow = Math.floor(targetIdx / 7); const targetCol = targetIdx % 7; const minRow = Math.min(anchorRow, targetRow); const maxRow = Math.max(anchorRow, targetRow); const minCol = Math.min(anchorCol, targetCol); const maxCol = Math.max(anchorCol, targetCol); this.clearSelection(); allDayCells.forEach((c, idx) => { const row = Math.floor(idx / 7); const col = idx % 7; if (row >= minRow && row <= maxRow && col >= minCol && col <= maxCol) { c.classList.add('selected'); this.selectedCells.push(c); } }); } /** * Clear all selected cells */ private clearSelection(): void { this.selectedCells.forEach(c => c.classList.remove('selected')); this.selectedCells = []; } /** * Get initials from name */ private getInitials(name: string): string { return name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2); } /** * Show empty state in drawer */ private showEmptyState(): void { const employeeDisplay = document.getElementById('scheduleFieldEmployee'); const avatar = document.getElementById('scheduleFieldAvatar'); const employeeName = document.getElementById('scheduleFieldEmployeeName'); const dateField = document.getElementById('scheduleFieldDate'); const weekdayField = document.getElementById('scheduleFieldWeekday'); const drawerBody = this.drawer?.querySelector('swp-drawer-body') as HTMLElement; const drawerFooter = this.drawer?.querySelector('swp-drawer-footer') as HTMLElement; if (employeeDisplay) employeeDisplay.classList.add('empty'); if (employeeDisplay) employeeDisplay.classList.remove('multi'); if (avatar) avatar.textContent = '?'; if (employeeName) employeeName.textContent = 'Vælg celle...'; if (dateField) dateField.textContent = '—'; if (weekdayField) weekdayField.textContent = '—'; if (drawerBody) { drawerBody.style.opacity = '0.5'; drawerBody.style.pointerEvents = 'none'; } if (drawerFooter) drawerFooter.style.display = 'none'; } /** * Show edit state in drawer */ private showEditState(): void { const drawerBody = this.drawer?.querySelector('swp-drawer-body') as HTMLElement; const drawerFooter = this.drawer?.querySelector('swp-drawer-footer') as HTMLElement; if (drawerBody) { drawerBody.style.opacity = '1'; drawerBody.style.pointerEvents = 'auto'; } if (drawerFooter) drawerFooter.style.display = 'flex'; } /** * Update drawer fields based on selected cells */ private updateDrawerFields(): void { if (this.selectedCells.length === 0) { this.showEmptyState(); return; } this.showEditState(); const employeeDisplay = document.getElementById('scheduleFieldEmployee'); const avatar = document.getElementById('scheduleFieldAvatar'); const employeeName = document.getElementById('scheduleFieldEmployeeName'); const dateField = document.getElementById('scheduleFieldDate'); const weekdayField = document.getElementById('scheduleFieldWeekday'); employeeDisplay?.classList.remove('empty', 'multi'); if (this.selectedCells.length === 1) { const cell = this.selectedCells[0]; const name = cell.dataset.employee || ''; const date = cell.dataset.date || ''; const dayName = this.getDayName(cell.dataset.day || ''); if (employeeName) employeeName.textContent = name; if (avatar) avatar.textContent = this.getInitials(name); if (dateField) dateField.textContent = this.formatDate(date); if (weekdayField) weekdayField.textContent = dayName; this.prefillFormFromCell(cell); } else { const employees = [...new Set(this.selectedCells.map(c => c.dataset.employee))]; const days = [...new Set(this.selectedCells.map(c => c.dataset.day))]; if (employees.length === 1) { if (employeeName) employeeName.textContent = employees[0] || ''; if (avatar) avatar.textContent = this.getInitials(employees[0] || ''); if (dateField) dateField.textContent = `${this.selectedCells.length} dage valgt`; } else { if (employeeName) employeeName.textContent = `${this.selectedCells.length} valgt`; if (avatar) avatar.textContent = String(this.selectedCells.length); employeeDisplay?.classList.add('multi'); if (dateField) dateField.textContent = `${employees.length} medarbejdere, ${days.length} dage`; } if (days.length === 1) { if (weekdayField) weekdayField.textContent = this.getDayName(days[0] || ''); } else { if (weekdayField) weekdayField.textContent = 'Flere dage'; } this.resetFormToDefault(); } } /** * Format ISO date to display format */ private formatDate(isoDate: string): string { const date = new Date(isoDate); return `${date.getDate()}/${date.getMonth() + 1}/${date.getFullYear()}`; } /** * Get full day name from short name */ private getDayName(shortName: string): string { const dayMap: Record = { 'Man': 'Mandag', 'Tir': 'Tirsdag', 'Ons': 'Onsdag', 'Tor': 'Torsdag', 'Fre': 'Fredag', 'Lør': 'Lørdag', 'Søn': 'Søndag' }; return dayMap[shortName] || shortName; } /** * Prefill form from cell data */ private prefillFormFromCell(cell: HTMLElement): void { const timeDisplay = cell.querySelector('swp-time-display'); if (!timeDisplay) return; let status = 'work'; if (timeDisplay.classList.contains('off')) status = 'off'; else if (timeDisplay.classList.contains('vacation')) status = 'vacation'; else if (timeDisplay.classList.contains('sick')) status = 'sick'; // Update status options document.querySelectorAll('#scheduleStatusOptions swp-status-option').forEach(opt => { opt.classList.toggle('selected', (opt as HTMLElement).dataset.status === status); }); // Show/hide time row const timeRow = document.getElementById('scheduleTimeRow'); if (timeRow) timeRow.style.display = status === 'work' ? 'flex' : 'none'; // Parse time if work status if (status === 'work') { const timeText = timeDisplay.textContent?.trim() || ''; const timeMatch = timeText.match(/(\d{2}:\d{2})\s*-\s*(\d{2}:\d{2})/); if (timeMatch) { this.setTimeRange(timeMatch[1], timeMatch[2]); } } // Reset type to template document.querySelectorAll('#scheduleTypeOptions swp-toggle-option').forEach(opt => { opt.classList.toggle('selected', (opt as HTMLElement).dataset.value === 'template'); }); const repeatGroup = document.getElementById('scheduleRepeatGroup'); if (repeatGroup) repeatGroup.style.display = 'block'; } /** * Reset form to default values */ private resetFormToDefault(): void { document.querySelectorAll('#scheduleStatusOptions swp-status-option').forEach(opt => { opt.classList.toggle('selected', (opt as HTMLElement).dataset.status === 'work'); }); const timeRow = document.getElementById('scheduleTimeRow'); if (timeRow) timeRow.style.display = 'flex'; this.setTimeRange('09:00', '17:00'); document.querySelectorAll('#scheduleTypeOptions swp-toggle-option').forEach(opt => { opt.classList.toggle('selected', (opt as HTMLElement).dataset.value === 'template'); }); const repeatGroup = document.getElementById('scheduleRepeatGroup'); if (repeatGroup) repeatGroup.style.display = 'block'; const noteField = document.getElementById('scheduleFieldNote') as HTMLInputElement; if (noteField) noteField.value = ''; } /** * Setup status options click handlers */ private setupStatusOptions(): void { document.querySelectorAll('#scheduleStatusOptions swp-status-option').forEach(option => { option.addEventListener('click', () => { document.querySelectorAll('#scheduleStatusOptions swp-status-option').forEach(o => o.classList.remove('selected') ); option.classList.add('selected'); const status = (option as HTMLElement).dataset.status; const timeRow = document.getElementById('scheduleTimeRow'); if (timeRow) timeRow.style.display = status === 'work' ? 'flex' : 'none'; this.updateSelectedCellsStatus(); }); }); } /** * Setup type toggle (single/repeat) */ private setupTypeToggle(): void { document.querySelectorAll('#scheduleTypeOptions swp-toggle-option').forEach(option => { option.addEventListener('click', () => { document.querySelectorAll('#scheduleTypeOptions swp-toggle-option').forEach(o => o.classList.remove('selected') ); option.classList.add('selected'); const isTemplate = (option as HTMLElement).dataset.value === 'template'; const repeatGroup = document.getElementById('scheduleRepeatGroup'); if (repeatGroup) repeatGroup.style.display = isTemplate ? 'block' : 'none'; }); }); } /** * Convert slider value to time string */ private valueToTime(value: number): string { const totalMinutes = (value * 15) + (6 * 60); const hours = Math.floor(totalMinutes / 60); const minutes = totalMinutes % 60; return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`; } /** * Convert time string to slider value */ private timeToValue(timeStr: string): number { const [hours, minutes] = timeStr.split(':').map(Number); const totalMinutes = hours * 60 + minutes; return Math.round((totalMinutes - 6 * 60) / 15); } /** * Set time range slider values */ private setTimeRange(startTime: string, endTime: string): void { const timeRange = document.getElementById('scheduleTimeRange'); if (!timeRange) return; const startInput = timeRange.querySelector('.range-start'); const endInput = timeRange.querySelector('.range-end'); if (startInput) startInput.value = String(this.timeToValue(startTime)); if (endInput) endInput.value = String(this.timeToValue(endTime)); this.updateTimeRangeDisplay(timeRange); } /** * Update time range display */ private updateTimeRangeDisplay(container: HTMLElement): void { const startInput = container.querySelector('.range-start'); const endInput = container.querySelector('.range-end'); const fill = container.querySelector('swp-time-range-fill'); const timesEl = container.querySelector('swp-time-range-times'); const durationEl = container.querySelector('swp-time-range-duration'); if (!startInput || !endInput) return; let startVal = parseInt(startInput.value); let endVal = parseInt(endInput.value); // Ensure start doesn't exceed end if (startVal > endVal) { if (startInput === document.activeElement) { startInput.value = String(endVal); startVal = endVal; } else { endInput.value = String(startVal); endVal = startVal; } } // Update fill bar if (fill) { const startPercent = (startVal / this.TIME_RANGE_MAX) * 100; const endPercent = (endVal / this.TIME_RANGE_MAX) * 100; fill.style.left = startPercent + '%'; fill.style.width = (endPercent - startPercent) + '%'; } // Calculate duration const durationMinutes = (endVal - startVal) * 15; const durationHours = durationMinutes / 60; const durationText = durationHours % 1 === 0 ? `${durationHours} timer` : `${durationHours.toFixed(1).replace('.', ',')} timer`; if (timesEl) timesEl.textContent = `${this.valueToTime(startVal)} – ${this.valueToTime(endVal)}`; if (durationEl) durationEl.textContent = durationText; } /** * Setup time range slider */ private setupTimeRangeSlider(): void { const timeRange = document.getElementById('scheduleTimeRange'); if (!timeRange) return; const startInput = timeRange.querySelector('.range-start'); const endInput = timeRange.querySelector('.range-end'); const fill = timeRange.querySelector('swp-time-range-fill'); const track = timeRange.querySelector('swp-time-range-track'); this.updateTimeRangeDisplay(timeRange); startInput?.addEventListener('input', () => { this.updateTimeRangeDisplay(timeRange); this.updateSelectedCellsTime(); }); endInput?.addEventListener('input', () => { this.updateTimeRangeDisplay(timeRange); this.updateSelectedCellsTime(); }); // Drag fill bar to move entire range if (fill && track && startInput && endInput) { let isDragging = false; let dragStartX = 0; let dragStartValues = { start: 0, end: 0 }; fill.addEventListener('mousedown', (e: MouseEvent) => { isDragging = true; dragStartX = e.clientX; dragStartValues.start = parseInt(startInput.value); dragStartValues.end = parseInt(endInput.value); e.preventDefault(); }); document.addEventListener('mousemove', (e: MouseEvent) => { if (!isDragging) return; const sliderWidth = track.offsetWidth; const deltaX = e.clientX - dragStartX; const deltaValue = Math.round((deltaX / sliderWidth) * this.TIME_RANGE_MAX); const duration = dragStartValues.end - dragStartValues.start; let newStart = dragStartValues.start + deltaValue; let newEnd = dragStartValues.end + deltaValue; if (newStart < 0) { newStart = 0; newEnd = duration; } if (newEnd > this.TIME_RANGE_MAX) { newEnd = this.TIME_RANGE_MAX; newStart = this.TIME_RANGE_MAX - duration; } startInput.value = String(newStart); endInput.value = String(newEnd); this.updateTimeRangeDisplay(timeRange); this.updateSelectedCellsTime(); }); document.addEventListener('mouseup', () => { isDragging = false; }); } } /** * Update selected cells with current time */ private updateSelectedCellsTime(): void { const selectedStatus = document.querySelector('#scheduleStatusOptions swp-status-option.selected') as HTMLElement; const status = selectedStatus?.dataset.status || 'work'; if (status !== 'work') return; const timeRange = document.getElementById('scheduleTimeRange'); if (!timeRange) return; const startInput = timeRange.querySelector('.range-start'); const endInput = timeRange.querySelector('.range-end'); if (!startInput || !endInput) return; const startTime = this.valueToTime(parseInt(startInput.value)); const endTime = this.valueToTime(parseInt(endInput.value)); const formattedTime = `${startTime} - ${endTime}`; this.selectedCells.forEach(cell => { const timeDisplay = cell.querySelector('swp-time-display'); if (timeDisplay && !timeDisplay.classList.contains('off') && !timeDisplay.classList.contains('vacation') && !timeDisplay.classList.contains('sick')) { timeDisplay.textContent = formattedTime; } }); } /** * Update selected cells with current status */ private updateSelectedCellsStatus(): void { const selectedStatus = document.querySelector('#scheduleStatusOptions swp-status-option.selected') as HTMLElement; const status = selectedStatus?.dataset.status || 'work'; const timeRange = document.getElementById('scheduleTimeRange'); const startInput = timeRange?.querySelector('.range-start'); const endInput = timeRange?.querySelector('.range-end'); const startTime = startInput ? this.valueToTime(parseInt(startInput.value)) : '09:00'; const endTime = endInput ? this.valueToTime(parseInt(endInput.value)) : '17:00'; this.selectedCells.forEach(cell => { const timeDisplay = cell.querySelector('swp-time-display'); if (!timeDisplay) return; timeDisplay.classList.remove('off', 'off-override', 'vacation', 'sick'); switch (status) { case 'work': timeDisplay.textContent = `${startTime} - ${endTime}`; break; case 'off': timeDisplay.classList.add('off'); timeDisplay.textContent = '—'; break; case 'vacation': timeDisplay.classList.add('vacation'); timeDisplay.textContent = 'Ferie'; break; case 'sick': timeDisplay.classList.add('sick'); timeDisplay.textContent = 'Syg'; break; } }); } /** * Setup drawer save button */ private setupDrawerSave(): void { const saveBtn = document.getElementById('scheduleDrawerSave'); saveBtn?.addEventListener('click', () => { // Changes are already applied in real-time this.clearSelection(); this.showEmptyState(); }); } /** * Open the schedule drawer (no overlay - user can still interact with table) */ private openDrawer(): void { // Lås tabelbredde før drawer åbner for at undgå "hop" const table = document.getElementById('scheduleTable'); if (table) { const rect = table.getBoundingClientRect(); table.style.width = `${rect.width}px`; } // Animate container med Web Animations API const container = document.querySelector('swp-tab-content[data-tab="schedule"] swp-page-container') as HTMLElement; if (container) { const currentStyles = getComputedStyle(container); const currentMaxWidth = currentStyles.maxWidth; const currentMargin = currentStyles.margin; const currentPaddingRight = currentStyles.paddingRight; container.animate([ { maxWidth: currentMaxWidth, margin: currentMargin, paddingRight: currentPaddingRight }, { maxWidth: 'none', margin: '0px', paddingRight: '420px' } ], { duration: 300, easing: 'ease', fill: 'forwards' }); } this.drawer?.classList.add('open'); document.body.classList.add('schedule-drawer-open'); } /** * Close the schedule drawer */ private closeDrawer(): void { const container = document.querySelector('swp-tab-content[data-tab="schedule"] swp-page-container') as HTMLElement; if (container) { // Hent nuværende computed styles for animation const animation = container.getAnimations()[0]; if (animation) { animation.cancel(); } } // Fjern låst bredde så tabellen kan tilpasse sig igen const table = document.getElementById('scheduleTable'); if (table) { table.style.width = ''; } this.drawer?.classList.remove('open'); document.body.classList.remove('schedule-drawer-open'); } }