Add services feature with mock data and components

Introduces comprehensive services management module with:
- Dynamic service and category tables
- Localization support for services section
- Mock data for services and categories
- Responsive UI components for services listing
- Menu navigation and styling updates

Enhances application's service management capabilities
This commit is contained in:
Janus C. H. Knudsen 2026-01-15 23:29:26 +01:00
parent 408e590922
commit 4cf30e1f27
20 changed files with 951 additions and 0 deletions

View file

@ -12,6 +12,7 @@ import { LockScreenController } from './modules/lockscreen';
import { CashController } from './modules/cash';
import { EmployeesController } from './modules/employees';
import { ControlsController } from './modules/controls';
import { ServicesController } from './modules/services';
/**
* Main application class
@ -25,6 +26,7 @@ export class App {
readonly cash: CashController;
readonly employees: EmployeesController;
readonly controls: ControlsController;
readonly services: ServicesController;
constructor() {
// Initialize controllers
@ -36,6 +38,7 @@ export class App {
this.cash = new CashController();
this.employees = new EmployeesController();
this.controls = new ControlsController();
this.services = new ServicesController();
}
}

View file

@ -0,0 +1,111 @@
/**
* Services Controller
* Handles category collapse/expand animations
*/
export class ServicesController {
constructor() {
this.init();
}
private init(): void {
this.setupCategoryToggle();
}
private setupCategoryToggle(): void {
document.addEventListener('click', (e) => {
const categoryRow = (e.target as HTMLElement).closest('swp-category-row');
if (!categoryRow) return;
const isExpanded = categoryRow.getAttribute('data-expanded') !== 'false';
const categoryId = categoryRow.getAttribute('data-category');
// Find all service rows belonging to this category
const serviceRows = this.getServiceRowsForCategory(categoryRow);
if (isExpanded) {
// Collapse - set attribute immediately so chevron animates with rows
categoryRow.setAttribute('data-expanded', 'false');
this.collapseRows(serviceRows);
} else {
// Expand - set attribute immediately so chevron animates with rows
categoryRow.setAttribute('data-expanded', 'true');
this.expandRows(serviceRows);
}
});
}
private getServiceRowsForCategory(categoryRow: Element): HTMLElement[] {
const rows: HTMLElement[] = [];
let sibling = categoryRow.nextElementSibling;
while (sibling && sibling.tagName.toLowerCase() === 'swp-data-table-row') {
rows.push(sibling as HTMLElement);
sibling = sibling.nextElementSibling;
}
return rows;
}
private collapseRows(rows: HTMLElement[]): void {
if (rows.length === 0) return;
// Animate each row
rows.forEach((row) => {
const height = row.offsetHeight;
row.style.height = `${height}px`;
row.style.overflow = 'hidden';
// Force reflow
row.offsetHeight;
row.style.transition = 'height 0.2s ease, opacity 0.2s ease';
row.style.height = '0';
row.style.opacity = '0';
});
// After animation completes
setTimeout(() => {
rows.forEach(row => {
row.style.display = 'none';
row.style.height = '';
row.style.opacity = '';
row.style.overflow = '';
row.style.transition = '';
});
}, 200);
}
private expandRows(rows: HTMLElement[]): void {
rows.forEach((row, index) => {
// First make visible but with 0 height
row.style.display = 'grid';
row.style.height = '0';
row.style.opacity = '0';
row.style.overflow = 'hidden';
// Measure natural height
row.style.height = 'auto';
const naturalHeight = row.offsetHeight;
row.style.height = '0';
// Force reflow
row.offsetHeight;
// Animate to natural height
row.style.transition = 'height 0.2s ease, opacity 0.2s ease';
row.style.height = `${naturalHeight}px`;
row.style.opacity = '1';
});
// Cleanup after animation
setTimeout(() => {
rows.forEach(row => {
row.style.height = '';
row.style.opacity = '';
row.style.overflow = '';
row.style.transition = '';
});
}, 200);
}
}