PlanTempusApp/PlanTempus.Application/wwwroot/ts/modules/employees.ts
Janus C. H. Knudsen 679c3fb3a6 Refactor employee table and row components
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
2026-01-14 16:53:42 +01:00

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}`;
}
}