Migrates custom table components to generic data table Improves consistency in table and row implementations Removes legacy custom table elements in favor of more flexible data table approach
322 lines
8.9 KiB
TypeScript
322 lines
8.9 KiB
TypeScript
/**
|
|
* 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<HTMLElement>('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<HTMLElement>('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<HTMLElement>('swp-tab-bar > swp-tab[data-tab]');
|
|
const contents = container.querySelectorAll<HTMLElement>('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<HTMLElement>('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<HTMLElement>('[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<HTMLElement>('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}`;
|
|
}
|
|
}
|