Enhances employee details with comprehensive salary and HR data

Adds detailed salary rates, commission structures, and HR-related records

Introduces new data models and view components for:
- Salary rates and supplements
- Commissions and rate configurations
- Employee HR tracking (certifications, courses, absence)

Implements dynamic rate synchronization between drawer and card views
This commit is contained in:
Janus C. H. Knudsen 2026-01-13 22:37:29 +01:00
parent 2e6207bb0b
commit f71f00099a
15 changed files with 1589 additions and 137 deletions

View file

@ -7,6 +7,7 @@
*/
export class EmployeesController {
private ratesSync: RatesSyncController | null = null;
private listView: HTMLElement | null = null;
private detailView: HTMLElement | null = null;
@ -23,6 +24,7 @@ export class EmployeesController {
this.setupBackNavigation();
this.setupHistoryNavigation();
this.restoreStateFromUrl();
this.ratesSync = new RatesSyncController();
}
/**
@ -189,3 +191,132 @@ export class EmployeesController {
}
}
}
/**
* 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}`;
}
}