2026-01-12 22:10:57 +01:00
|
|
|
"use strict";
|
|
|
|
|
(() => {
|
|
|
|
|
// wwwroot/ts/modules/sidebar.ts
|
|
|
|
|
var SidebarController = class {
|
|
|
|
|
constructor() {
|
|
|
|
|
this.menuToggle = null;
|
|
|
|
|
this.appLayout = null;
|
|
|
|
|
this.menuTooltip = null;
|
|
|
|
|
this.menuToggle = document.getElementById("menuToggle");
|
|
|
|
|
this.appLayout = document.querySelector("swp-app-layout");
|
|
|
|
|
this.menuTooltip = document.getElementById("menuTooltip");
|
|
|
|
|
this.setupListeners();
|
|
|
|
|
this.setupTooltips();
|
|
|
|
|
this.restoreState();
|
2026-01-10 20:39:17 +01:00
|
|
|
}
|
2026-01-12 22:10:57 +01:00
|
|
|
/**
|
|
|
|
|
* Check if sidebar is collapsed
|
|
|
|
|
*/
|
|
|
|
|
get isCollapsed() {
|
|
|
|
|
return this.appLayout?.classList.contains("menu-collapsed") ?? false;
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Toggle sidebar collapsed state
|
|
|
|
|
*/
|
|
|
|
|
toggle() {
|
|
|
|
|
if (!this.appLayout) return;
|
|
|
|
|
this.appLayout.classList.toggle("menu-collapsed");
|
|
|
|
|
localStorage.setItem("sidebar-collapsed", String(this.isCollapsed));
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Collapse the sidebar
|
|
|
|
|
*/
|
|
|
|
|
collapse() {
|
|
|
|
|
this.appLayout?.classList.add("menu-collapsed");
|
|
|
|
|
localStorage.setItem("sidebar-collapsed", "true");
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Expand the sidebar
|
|
|
|
|
*/
|
|
|
|
|
expand() {
|
|
|
|
|
this.appLayout?.classList.remove("menu-collapsed");
|
|
|
|
|
localStorage.setItem("sidebar-collapsed", "false");
|
|
|
|
|
}
|
|
|
|
|
setupListeners() {
|
|
|
|
|
this.menuToggle?.addEventListener("click", () => this.toggle());
|
|
|
|
|
}
|
|
|
|
|
setupTooltips() {
|
|
|
|
|
if (!this.menuTooltip) return;
|
|
|
|
|
const menuItems = document.querySelectorAll("swp-side-menu-item[data-tooltip]");
|
|
|
|
|
menuItems.forEach((item) => {
|
|
|
|
|
item.addEventListener("mouseenter", () => this.showTooltip(item));
|
|
|
|
|
item.addEventListener("mouseleave", () => this.hideTooltip());
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
showTooltip(item) {
|
|
|
|
|
if (!this.isCollapsed || !this.menuTooltip) return;
|
|
|
|
|
const rect = item.getBoundingClientRect();
|
|
|
|
|
const tooltipText = item.dataset.tooltip;
|
|
|
|
|
if (!tooltipText) return;
|
|
|
|
|
this.menuTooltip.textContent = tooltipText;
|
|
|
|
|
this.menuTooltip.style.left = `${rect.right + 8}px`;
|
|
|
|
|
this.menuTooltip.style.top = `${rect.top + rect.height / 2}px`;
|
|
|
|
|
this.menuTooltip.style.transform = "translateY(-50%)";
|
|
|
|
|
this.menuTooltip.showPopover();
|
|
|
|
|
}
|
|
|
|
|
hideTooltip() {
|
|
|
|
|
this.menuTooltip?.hidePopover();
|
|
|
|
|
}
|
|
|
|
|
restoreState() {
|
|
|
|
|
if (!this.appLayout) return;
|
|
|
|
|
if (localStorage.getItem("sidebar-collapsed") === "true") {
|
|
|
|
|
this.appLayout.classList.add("menu-collapsed");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-01-10 20:39:17 +01:00
|
|
|
|
2026-01-12 22:10:57 +01:00
|
|
|
// wwwroot/ts/modules/drawers.ts
|
|
|
|
|
var DrawerController = class {
|
|
|
|
|
constructor() {
|
|
|
|
|
this.profileDrawer = null;
|
|
|
|
|
this.notificationDrawer = null;
|
|
|
|
|
this.todoDrawer = null;
|
|
|
|
|
this.newTodoDrawer = null;
|
|
|
|
|
this.overlay = null;
|
|
|
|
|
this.activeDrawer = null;
|
|
|
|
|
this.activeGenericDrawer = null;
|
|
|
|
|
this.profileDrawer = document.getElementById("profileDrawer");
|
|
|
|
|
this.notificationDrawer = document.getElementById("notificationDrawer");
|
|
|
|
|
this.todoDrawer = document.getElementById("todoDrawer");
|
|
|
|
|
this.newTodoDrawer = document.getElementById("newTodoDrawer");
|
|
|
|
|
this.overlay = document.getElementById("drawerOverlay");
|
|
|
|
|
this.setupListeners();
|
|
|
|
|
this.setupGenericDrawers();
|
2026-01-10 20:39:17 +01:00
|
|
|
}
|
2026-01-12 22:10:57 +01:00
|
|
|
/**
|
|
|
|
|
* Get currently active drawer name
|
|
|
|
|
*/
|
|
|
|
|
get active() {
|
|
|
|
|
return this.activeDrawer;
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Open a drawer by name
|
|
|
|
|
*/
|
|
|
|
|
open(name) {
|
|
|
|
|
this.closeAll();
|
|
|
|
|
const drawer = this.getDrawer(name);
|
|
|
|
|
if (drawer && this.overlay) {
|
|
|
|
|
drawer.classList.add("active");
|
|
|
|
|
this.overlay.classList.add("active");
|
|
|
|
|
document.body.style.overflow = "hidden";
|
|
|
|
|
this.activeDrawer = name;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Close a specific drawer
|
|
|
|
|
*/
|
|
|
|
|
close(name) {
|
|
|
|
|
const drawer = this.getDrawer(name);
|
|
|
|
|
drawer?.classList.remove("active");
|
|
|
|
|
if (this.overlay && !document.querySelector('.active[class*="drawer"]')) {
|
|
|
|
|
this.overlay.classList.remove("active");
|
|
|
|
|
document.body.style.overflow = "";
|
|
|
|
|
}
|
|
|
|
|
if (this.activeDrawer === name) {
|
|
|
|
|
this.activeDrawer = null;
|
|
|
|
|
}
|
2026-01-10 20:39:17 +01:00
|
|
|
}
|
2026-01-12 22:10:57 +01:00
|
|
|
/**
|
|
|
|
|
* Close all drawers
|
|
|
|
|
*/
|
|
|
|
|
closeAll() {
|
|
|
|
|
[this.profileDrawer, this.notificationDrawer, this.todoDrawer, this.newTodoDrawer].forEach((drawer) => drawer?.classList.remove("active"));
|
|
|
|
|
this.closeGenericDrawer();
|
|
|
|
|
this.overlay?.classList.remove("active");
|
|
|
|
|
document.body.style.overflow = "";
|
2026-01-10 20:39:17 +01:00
|
|
|
this.activeDrawer = null;
|
|
|
|
|
}
|
2026-01-12 22:10:57 +01:00
|
|
|
/**
|
|
|
|
|
* Open a generic drawer by ID
|
|
|
|
|
*/
|
|
|
|
|
openGenericDrawer(drawerId) {
|
|
|
|
|
this.closeAll();
|
|
|
|
|
const drawer = document.getElementById(drawerId);
|
|
|
|
|
if (drawer && this.overlay) {
|
|
|
|
|
drawer.classList.add("open");
|
|
|
|
|
this.overlay.classList.add("active");
|
|
|
|
|
document.body.style.overflow = "hidden";
|
|
|
|
|
this.activeGenericDrawer = drawer;
|
|
|
|
|
}
|
2026-01-11 18:18:36 +01:00
|
|
|
}
|
2026-01-12 22:10:57 +01:00
|
|
|
/**
|
|
|
|
|
* Close the currently open generic drawer
|
|
|
|
|
*/
|
|
|
|
|
closeGenericDrawer() {
|
|
|
|
|
this.activeGenericDrawer?.classList.remove("open");
|
|
|
|
|
this.activeGenericDrawer = null;
|
2026-01-10 20:39:17 +01:00
|
|
|
}
|
2026-01-12 22:10:57 +01:00
|
|
|
/**
|
|
|
|
|
* Open profile drawer
|
|
|
|
|
*/
|
|
|
|
|
openProfile() {
|
|
|
|
|
this.open("profile");
|
2026-01-10 20:39:17 +01:00
|
|
|
}
|
2026-01-12 22:10:57 +01:00
|
|
|
/**
|
|
|
|
|
* Open notification drawer
|
|
|
|
|
*/
|
|
|
|
|
openNotification() {
|
|
|
|
|
this.open("notification");
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Open todo drawer (slides on top of profile)
|
|
|
|
|
*/
|
|
|
|
|
openTodo() {
|
|
|
|
|
this.todoDrawer?.classList.add("active");
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Close todo drawer
|
|
|
|
|
*/
|
|
|
|
|
closeTodo() {
|
|
|
|
|
this.todoDrawer?.classList.remove("active");
|
|
|
|
|
this.closeNewTodo();
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Open new todo drawer
|
|
|
|
|
*/
|
|
|
|
|
openNewTodo() {
|
|
|
|
|
this.newTodoDrawer?.classList.add("active");
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Close new todo drawer
|
|
|
|
|
*/
|
|
|
|
|
closeNewTodo() {
|
|
|
|
|
this.newTodoDrawer?.classList.remove("active");
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Mark all notifications as read
|
|
|
|
|
*/
|
|
|
|
|
markAllNotificationsRead() {
|
|
|
|
|
if (!this.notificationDrawer) return;
|
|
|
|
|
const unreadItems = this.notificationDrawer.querySelectorAll(
|
|
|
|
|
'swp-notification-item[data-unread="true"]'
|
|
|
|
|
);
|
|
|
|
|
unreadItems.forEach((item) => item.removeAttribute("data-unread"));
|
|
|
|
|
const badge = document.querySelector("swp-notification-badge");
|
|
|
|
|
if (badge) {
|
|
|
|
|
badge.style.display = "none";
|
2026-01-10 20:39:17 +01:00
|
|
|
}
|
|
|
|
|
}
|
2026-01-12 22:10:57 +01:00
|
|
|
getDrawer(name) {
|
|
|
|
|
switch (name) {
|
|
|
|
|
case "profile":
|
|
|
|
|
return this.profileDrawer;
|
|
|
|
|
case "notification":
|
|
|
|
|
return this.notificationDrawer;
|
|
|
|
|
case "todo":
|
|
|
|
|
return this.todoDrawer;
|
|
|
|
|
case "newTodo":
|
|
|
|
|
return this.newTodoDrawer;
|
|
|
|
|
}
|
2026-01-10 20:39:17 +01:00
|
|
|
}
|
2026-01-12 22:10:57 +01:00
|
|
|
setupListeners() {
|
|
|
|
|
document.getElementById("profileTrigger")?.addEventListener("click", () => this.openProfile());
|
|
|
|
|
document.getElementById("drawerClose")?.addEventListener("click", () => this.close("profile"));
|
|
|
|
|
document.getElementById("notificationsBtn")?.addEventListener("click", () => this.openNotification());
|
|
|
|
|
document.getElementById("notificationDrawerClose")?.addEventListener("click", () => this.close("notification"));
|
|
|
|
|
document.getElementById("markAllRead")?.addEventListener("click", () => this.markAllNotificationsRead());
|
|
|
|
|
document.getElementById("openTodoDrawer")?.addEventListener("click", () => this.openTodo());
|
|
|
|
|
document.getElementById("todoDrawerBack")?.addEventListener("click", () => this.closeTodo());
|
|
|
|
|
document.getElementById("addTodoBtn")?.addEventListener("click", () => this.openNewTodo());
|
|
|
|
|
document.getElementById("newTodoDrawerBack")?.addEventListener("click", () => this.closeNewTodo());
|
|
|
|
|
document.getElementById("cancelNewTodo")?.addEventListener("click", () => this.closeNewTodo());
|
|
|
|
|
document.getElementById("saveNewTodo")?.addEventListener("click", () => this.closeNewTodo());
|
|
|
|
|
this.overlay?.addEventListener("click", () => this.closeAll());
|
|
|
|
|
document.addEventListener("keydown", (e) => {
|
|
|
|
|
if (e.key === "Escape") this.closeAll();
|
|
|
|
|
});
|
|
|
|
|
this.todoDrawer?.addEventListener("click", (e) => this.handleTodoClick(e));
|
|
|
|
|
document.addEventListener("click", (e) => this.handleVisibilityClick(e));
|
2026-01-10 20:39:17 +01:00
|
|
|
}
|
2026-01-12 22:10:57 +01:00
|
|
|
handleTodoClick(e) {
|
2026-01-11 18:18:36 +01:00
|
|
|
const target = e.target;
|
2026-01-12 22:10:57 +01:00
|
|
|
const todoItem = target.closest("swp-todo-item");
|
|
|
|
|
const checkbox = target.closest("swp-todo-checkbox");
|
|
|
|
|
if (checkbox && todoItem) {
|
|
|
|
|
const isCompleted = todoItem.dataset.completed === "true";
|
|
|
|
|
if (isCompleted) {
|
|
|
|
|
todoItem.removeAttribute("data-completed");
|
|
|
|
|
} else {
|
|
|
|
|
todoItem.dataset.completed = "true";
|
2026-01-11 18:18:36 +01:00
|
|
|
}
|
|
|
|
|
}
|
2026-01-12 22:10:57 +01:00
|
|
|
const sectionHeader = target.closest("swp-todo-section-header");
|
|
|
|
|
if (sectionHeader) {
|
|
|
|
|
const section = sectionHeader.closest("swp-todo-section");
|
|
|
|
|
section?.classList.toggle("collapsed");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
handleVisibilityClick(e) {
|
2026-01-11 18:18:36 +01:00
|
|
|
const target = e.target;
|
2026-01-12 22:10:57 +01:00
|
|
|
const option = target.closest("swp-visibility-option");
|
|
|
|
|
if (option) {
|
|
|
|
|
document.querySelectorAll("swp-visibility-option").forEach((o) => o.classList.remove("active"));
|
|
|
|
|
option.classList.add("active");
|
2026-01-11 18:18:36 +01:00
|
|
|
}
|
2026-01-12 22:10:57 +01:00
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Setup generic drawer triggers and close buttons
|
|
|
|
|
* Uses data-drawer-trigger="drawer-id" and data-drawer-close attributes
|
|
|
|
|
*/
|
|
|
|
|
setupGenericDrawers() {
|
|
|
|
|
document.addEventListener("click", (e) => {
|
|
|
|
|
const target = e.target;
|
|
|
|
|
const trigger = target.closest("[data-drawer-trigger]");
|
|
|
|
|
if (trigger) {
|
|
|
|
|
const drawerId = trigger.dataset.drawerTrigger;
|
|
|
|
|
if (drawerId) {
|
|
|
|
|
this.openGenericDrawer(drawerId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
document.addEventListener("click", (e) => {
|
|
|
|
|
const target = e.target;
|
|
|
|
|
const closeBtn = target.closest("[data-drawer-close]");
|
|
|
|
|
if (closeBtn) {
|
|
|
|
|
this.closeGenericDrawer();
|
|
|
|
|
this.overlay?.classList.remove("active");
|
|
|
|
|
document.body.style.overflow = "";
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-01-10 20:39:17 +01:00
|
|
|
|
2026-01-12 22:10:57 +01:00
|
|
|
// wwwroot/ts/modules/theme.ts
|
|
|
|
|
var ThemeController = class _ThemeController {
|
|
|
|
|
static {
|
|
|
|
|
this.STORAGE_KEY = "theme-preference";
|
2026-01-10 20:39:17 +01:00
|
|
|
}
|
2026-01-12 22:10:57 +01:00
|
|
|
static {
|
|
|
|
|
this.DARK_CLASS = "dark-mode";
|
|
|
|
|
}
|
|
|
|
|
static {
|
|
|
|
|
this.LIGHT_CLASS = "light-mode";
|
|
|
|
|
}
|
|
|
|
|
constructor() {
|
|
|
|
|
this.root = document.documentElement;
|
|
|
|
|
this.themeOptions = document.querySelectorAll("swp-theme-option");
|
|
|
|
|
this.applyTheme(this.current);
|
|
|
|
|
this.updateUI();
|
|
|
|
|
this.setupListeners();
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Get the current theme setting
|
|
|
|
|
*/
|
|
|
|
|
get current() {
|
|
|
|
|
const stored = localStorage.getItem(_ThemeController.STORAGE_KEY);
|
|
|
|
|
if (stored === "dark" || stored === "light" || stored === "system") {
|
|
|
|
|
return stored;
|
2026-01-10 20:39:17 +01:00
|
|
|
}
|
2026-01-12 22:10:57 +01:00
|
|
|
return "system";
|
2026-01-10 20:39:17 +01:00
|
|
|
}
|
2026-01-12 22:10:57 +01:00
|
|
|
/**
|
|
|
|
|
* Check if dark mode is currently active
|
|
|
|
|
*/
|
|
|
|
|
get isDark() {
|
|
|
|
|
return this.root.classList.contains(_ThemeController.DARK_CLASS) || this.systemPrefersDark && !this.root.classList.contains(_ThemeController.LIGHT_CLASS);
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Check if system prefers dark mode
|
|
|
|
|
*/
|
|
|
|
|
get systemPrefersDark() {
|
|
|
|
|
return window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Set theme and persist preference
|
|
|
|
|
*/
|
|
|
|
|
set(theme) {
|
|
|
|
|
localStorage.setItem(_ThemeController.STORAGE_KEY, theme);
|
|
|
|
|
this.applyTheme(theme);
|
2026-01-10 20:39:17 +01:00
|
|
|
this.updateUI();
|
|
|
|
|
}
|
2026-01-12 22:10:57 +01:00
|
|
|
/**
|
|
|
|
|
* Toggle between light and dark themes
|
|
|
|
|
*/
|
|
|
|
|
toggle() {
|
|
|
|
|
this.set(this.isDark ? "light" : "dark");
|
|
|
|
|
}
|
|
|
|
|
applyTheme(theme) {
|
|
|
|
|
this.root.classList.remove(_ThemeController.DARK_CLASS, _ThemeController.LIGHT_CLASS);
|
|
|
|
|
if (theme === "dark") {
|
|
|
|
|
this.root.classList.add(_ThemeController.DARK_CLASS);
|
|
|
|
|
} else if (theme === "light") {
|
|
|
|
|
this.root.classList.add(_ThemeController.LIGHT_CLASS);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
updateUI() {
|
|
|
|
|
if (!this.themeOptions) return;
|
|
|
|
|
const darkActive = this.isDark;
|
|
|
|
|
this.themeOptions.forEach((option) => {
|
|
|
|
|
const theme = option.dataset.theme;
|
|
|
|
|
const isActive = theme === "dark" && darkActive || theme === "light" && !darkActive;
|
|
|
|
|
option.classList.toggle("active", isActive);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
setupListeners() {
|
|
|
|
|
this.themeOptions.forEach((option) => {
|
|
|
|
|
option.addEventListener("click", (e) => this.handleOptionClick(e));
|
|
|
|
|
});
|
|
|
|
|
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => this.handleSystemChange());
|
|
|
|
|
}
|
|
|
|
|
handleOptionClick(e) {
|
|
|
|
|
const target = e.target;
|
|
|
|
|
const option = target.closest("swp-theme-option");
|
|
|
|
|
if (option) {
|
|
|
|
|
const theme = option.dataset.theme;
|
|
|
|
|
if (theme) {
|
|
|
|
|
this.set(theme);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
handleSystemChange() {
|
|
|
|
|
if (this.current === "system") {
|
|
|
|
|
this.updateUI();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-01-10 20:39:17 +01:00
|
|
|
|
2026-01-12 22:10:57 +01:00
|
|
|
// wwwroot/ts/modules/search.ts
|
|
|
|
|
var SearchController = class {
|
|
|
|
|
constructor() {
|
|
|
|
|
this.input = null;
|
|
|
|
|
this.container = null;
|
|
|
|
|
this.input = document.getElementById("globalSearch");
|
|
|
|
|
this.container = document.querySelector("swp-topbar-search");
|
|
|
|
|
this.setupListeners();
|
2026-01-10 20:39:17 +01:00
|
|
|
}
|
2026-01-12 22:10:57 +01:00
|
|
|
/**
|
|
|
|
|
* Get current search value
|
|
|
|
|
*/
|
|
|
|
|
get value() {
|
|
|
|
|
return this.input?.value ?? "";
|
2026-01-10 20:39:17 +01:00
|
|
|
}
|
2026-01-12 22:10:57 +01:00
|
|
|
/**
|
|
|
|
|
* Set search value
|
|
|
|
|
*/
|
|
|
|
|
set value(val) {
|
|
|
|
|
if (this.input) {
|
|
|
|
|
this.input.value = val;
|
|
|
|
|
}
|
2026-01-10 20:39:17 +01:00
|
|
|
}
|
2026-01-12 22:10:57 +01:00
|
|
|
/**
|
|
|
|
|
* Focus the search input
|
|
|
|
|
*/
|
|
|
|
|
focus() {
|
|
|
|
|
this.input?.focus();
|
2026-01-10 20:39:17 +01:00
|
|
|
}
|
2026-01-12 22:10:57 +01:00
|
|
|
/**
|
|
|
|
|
* Blur the search input
|
|
|
|
|
*/
|
|
|
|
|
blur() {
|
|
|
|
|
this.input?.blur();
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Clear the search input
|
|
|
|
|
*/
|
|
|
|
|
clear() {
|
|
|
|
|
this.value = "";
|
|
|
|
|
}
|
|
|
|
|
setupListeners() {
|
|
|
|
|
document.addEventListener("keydown", (e) => this.handleKeyboard(e));
|
|
|
|
|
if (this.input) {
|
|
|
|
|
this.input.addEventListener("input", (e) => this.handleInput(e));
|
|
|
|
|
const form = this.input.closest("form");
|
|
|
|
|
form?.addEventListener("submit", (e) => this.handleSubmit(e));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
handleKeyboard(e) {
|
|
|
|
|
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
this.focus();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (e.key === "Escape" && document.activeElement === this.input) {
|
|
|
|
|
this.blur();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
handleInput(e) {
|
|
|
|
|
const target = e.target;
|
|
|
|
|
const query = target.value.trim();
|
|
|
|
|
document.dispatchEvent(new CustomEvent("app:search", {
|
|
|
|
|
detail: { query },
|
|
|
|
|
bubbles: true
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
handleSubmit(e) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
const query = this.value.trim();
|
|
|
|
|
if (!query) return;
|
|
|
|
|
document.dispatchEvent(new CustomEvent("app:search-submit", {
|
|
|
|
|
detail: { query },
|
|
|
|
|
bubbles: true
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-01-10 20:39:17 +01:00
|
|
|
|
2026-01-12 22:10:57 +01:00
|
|
|
// wwwroot/ts/modules/lockscreen.ts
|
|
|
|
|
var LockScreenController = class _LockScreenController {
|
|
|
|
|
constructor(drawers) {
|
|
|
|
|
// Demo PIN
|
|
|
|
|
this.lockScreen = null;
|
|
|
|
|
this.pinInput = null;
|
|
|
|
|
this.pinKeypad = null;
|
|
|
|
|
this.lockTimeEl = null;
|
|
|
|
|
this.pinDigits = null;
|
|
|
|
|
this.currentPin = "";
|
|
|
|
|
this.drawers = null;
|
|
|
|
|
this.drawers = drawers ?? null;
|
|
|
|
|
this.lockScreen = document.getElementById("lockScreen");
|
|
|
|
|
this.pinInput = document.getElementById("pinInput");
|
|
|
|
|
this.pinKeypad = document.getElementById("pinKeypad");
|
|
|
|
|
this.lockTimeEl = document.getElementById("lockTime");
|
|
|
|
|
this.pinDigits = this.pinInput?.querySelectorAll("swp-pin-digit") ?? null;
|
|
|
|
|
this.setupListeners();
|
2026-01-10 20:39:17 +01:00
|
|
|
}
|
2026-01-12 22:10:57 +01:00
|
|
|
static {
|
|
|
|
|
this.CORRECT_PIN = "1234";
|
2026-01-10 20:39:17 +01:00
|
|
|
}
|
2026-01-12 22:10:57 +01:00
|
|
|
/**
|
|
|
|
|
* Check if lock screen is active
|
|
|
|
|
*/
|
|
|
|
|
get isActive() {
|
|
|
|
|
return this.lockScreen?.classList.contains("active") ?? false;
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Show the lock screen
|
|
|
|
|
*/
|
|
|
|
|
show() {
|
|
|
|
|
this.drawers?.closeAll();
|
|
|
|
|
if (this.lockScreen) {
|
|
|
|
|
this.lockScreen.classList.add("active");
|
|
|
|
|
document.body.style.overflow = "hidden";
|
|
|
|
|
}
|
|
|
|
|
this.currentPin = "";
|
|
|
|
|
this.updateDisplay();
|
|
|
|
|
if (this.lockTimeEl) {
|
|
|
|
|
this.lockTimeEl.textContent = `L\xE5st kl. ${this.formatTime()}`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Hide the lock screen
|
|
|
|
|
*/
|
|
|
|
|
hide() {
|
|
|
|
|
if (this.lockScreen) {
|
|
|
|
|
this.lockScreen.classList.remove("active");
|
|
|
|
|
document.body.style.overflow = "";
|
|
|
|
|
}
|
|
|
|
|
this.currentPin = "";
|
|
|
|
|
this.updateDisplay();
|
|
|
|
|
}
|
|
|
|
|
formatTime() {
|
|
|
|
|
const now = /* @__PURE__ */ new Date();
|
|
|
|
|
const hours = now.getHours().toString().padStart(2, "0");
|
|
|
|
|
const minutes = now.getMinutes().toString().padStart(2, "0");
|
|
|
|
|
return `${hours}:${minutes}`;
|
|
|
|
|
}
|
|
|
|
|
updateDisplay() {
|
|
|
|
|
if (!this.pinDigits) return;
|
|
|
|
|
this.pinDigits.forEach((digit, index) => {
|
|
|
|
|
digit.classList.remove("filled", "error");
|
|
|
|
|
if (index < this.currentPin.length) {
|
|
|
|
|
digit.textContent = "\u2022";
|
|
|
|
|
digit.classList.add("filled");
|
|
|
|
|
} else {
|
|
|
|
|
digit.textContent = "";
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
showError() {
|
|
|
|
|
if (!this.pinDigits) return;
|
|
|
|
|
this.pinDigits.forEach((digit) => digit.classList.add("error"));
|
|
|
|
|
this.pinInput?.classList.add("shake");
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
this.currentPin = "";
|
|
|
|
|
this.updateDisplay();
|
|
|
|
|
this.pinInput?.classList.remove("shake");
|
|
|
|
|
}, 500);
|
|
|
|
|
}
|
|
|
|
|
verify() {
|
|
|
|
|
if (this.currentPin === _LockScreenController.CORRECT_PIN) {
|
|
|
|
|
this.hide();
|
2026-01-10 20:39:17 +01:00
|
|
|
} else {
|
2026-01-12 22:10:57 +01:00
|
|
|
this.showError();
|
2026-01-10 20:39:17 +01:00
|
|
|
}
|
2026-01-12 22:10:57 +01:00
|
|
|
}
|
|
|
|
|
addDigit(digit) {
|
|
|
|
|
if (this.currentPin.length >= 4) return;
|
|
|
|
|
this.currentPin += digit;
|
|
|
|
|
this.updateDisplay();
|
|
|
|
|
if (this.currentPin.length === 4) {
|
|
|
|
|
setTimeout(() => this.verify(), 200);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
removeDigit() {
|
|
|
|
|
if (this.currentPin.length === 0) return;
|
|
|
|
|
this.currentPin = this.currentPin.slice(0, -1);
|
|
|
|
|
this.updateDisplay();
|
|
|
|
|
}
|
|
|
|
|
clearPin() {
|
2026-01-10 20:39:17 +01:00
|
|
|
this.currentPin = "";
|
|
|
|
|
this.updateDisplay();
|
|
|
|
|
}
|
2026-01-12 22:10:57 +01:00
|
|
|
setupListeners() {
|
|
|
|
|
this.pinKeypad?.addEventListener("click", (e) => this.handleKeypadClick(e));
|
|
|
|
|
document.addEventListener("keydown", (e) => this.handleKeyboard(e));
|
|
|
|
|
document.querySelector("swp-side-menu-action.lock")?.addEventListener("click", () => this.show());
|
2026-01-10 20:39:17 +01:00
|
|
|
}
|
2026-01-12 22:10:57 +01:00
|
|
|
handleKeypadClick(e) {
|
|
|
|
|
const target = e.target;
|
|
|
|
|
const key = target.closest("swp-pin-key");
|
|
|
|
|
if (!key) return;
|
|
|
|
|
const digit = key.dataset.digit;
|
|
|
|
|
const action = key.dataset.action;
|
|
|
|
|
if (digit) {
|
|
|
|
|
this.addDigit(digit);
|
|
|
|
|
} else if (action === "backspace") {
|
|
|
|
|
this.removeDigit();
|
|
|
|
|
} else if (action === "clear") {
|
|
|
|
|
this.clearPin();
|
|
|
|
|
}
|
2026-01-10 20:39:17 +01:00
|
|
|
}
|
2026-01-12 22:10:57 +01:00
|
|
|
handleKeyboard(e) {
|
|
|
|
|
if (!this.isActive) return;
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
if (e.key >= "0" && e.key <= "9") {
|
|
|
|
|
this.addDigit(e.key);
|
|
|
|
|
} else if (e.key === "Backspace") {
|
|
|
|
|
this.removeDigit();
|
|
|
|
|
} else if (e.key === "Escape") {
|
|
|
|
|
this.clearPin();
|
|
|
|
|
}
|
2026-01-10 20:39:17 +01:00
|
|
|
}
|
2026-01-12 22:10:57 +01:00
|
|
|
};
|
2026-01-11 21:08:56 +01:00
|
|
|
|
2026-01-12 22:10:57 +01:00
|
|
|
// wwwroot/ts/modules/cash.ts
|
|
|
|
|
var CashController = class {
|
|
|
|
|
constructor() {
|
|
|
|
|
// Base values (from system - would come from server in real app)
|
|
|
|
|
this.startBalance = 2e3;
|
|
|
|
|
this.cashSales = 3540;
|
|
|
|
|
this.setupTabs();
|
|
|
|
|
this.setupCashCalculation();
|
|
|
|
|
this.setupCheckboxSelection();
|
|
|
|
|
this.setupApprovalCheckbox();
|
|
|
|
|
this.setupDateFilters();
|
|
|
|
|
this.setupRowToggle();
|
|
|
|
|
this.setupDraftRowClick();
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Setup tab switching functionality
|
|
|
|
|
*/
|
|
|
|
|
setupTabs() {
|
|
|
|
|
const tabs = document.querySelectorAll("swp-tab[data-tab]");
|
|
|
|
|
tabs.forEach((tab) => {
|
|
|
|
|
tab.addEventListener("click", () => {
|
|
|
|
|
const targetTab = tab.dataset.tab;
|
|
|
|
|
if (targetTab) {
|
|
|
|
|
this.switchToTab(targetTab);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Switch to a specific tab by name
|
|
|
|
|
*/
|
|
|
|
|
switchToTab(targetTab) {
|
|
|
|
|
const tabs = document.querySelectorAll("swp-tab[data-tab]");
|
|
|
|
|
const contents = document.querySelectorAll("swp-tab-content[data-tab]");
|
|
|
|
|
const statsBars = document.querySelectorAll("swp-cash-stats[data-for-tab]");
|
|
|
|
|
tabs.forEach((t) => {
|
|
|
|
|
if (t.dataset.tab === targetTab) {
|
|
|
|
|
t.classList.add("active");
|
|
|
|
|
} else {
|
|
|
|
|
t.classList.remove("active");
|
2026-01-11 21:08:56 +01:00
|
|
|
}
|
|
|
|
|
});
|
2026-01-12 22:10:57 +01:00
|
|
|
contents.forEach((content) => {
|
|
|
|
|
if (content.dataset.tab === targetTab) {
|
|
|
|
|
content.classList.add("active");
|
|
|
|
|
} else {
|
|
|
|
|
content.classList.remove("active");
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
statsBars.forEach((stats) => {
|
|
|
|
|
if (stats.dataset.forTab === targetTab) {
|
|
|
|
|
stats.classList.add("active");
|
|
|
|
|
} else {
|
|
|
|
|
stats.classList.remove("active");
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Setup cash calculation with real-time updates
|
|
|
|
|
*/
|
|
|
|
|
setupCashCalculation() {
|
|
|
|
|
const payoutsInput = document.getElementById("payouts");
|
|
|
|
|
const toBankInput = document.getElementById("toBank");
|
|
|
|
|
const actualCashInput = document.getElementById("actualCash");
|
|
|
|
|
if (!payoutsInput || !toBankInput || !actualCashInput) return;
|
|
|
|
|
const calculate = () => this.calculateCash(payoutsInput, toBankInput, actualCashInput);
|
|
|
|
|
payoutsInput.addEventListener("input", calculate);
|
|
|
|
|
toBankInput.addEventListener("input", calculate);
|
|
|
|
|
actualCashInput.addEventListener("input", calculate);
|
|
|
|
|
calculate();
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Calculate expected cash and difference
|
|
|
|
|
*/
|
|
|
|
|
calculateCash(payoutsInput, toBankInput, actualCashInput) {
|
|
|
|
|
const payouts = this.parseNumber(payoutsInput.value);
|
|
|
|
|
const toBank = this.parseNumber(toBankInput.value);
|
|
|
|
|
const actual = this.parseNumber(actualCashInput.value);
|
|
|
|
|
const expectedCash = this.startBalance + this.cashSales - payouts - toBank;
|
|
|
|
|
const expectedElement = document.getElementById("expectedCash");
|
|
|
|
|
if (expectedElement) {
|
|
|
|
|
expectedElement.textContent = this.formatNumber(expectedCash);
|
2026-01-11 21:08:56 +01:00
|
|
|
}
|
2026-01-12 22:10:57 +01:00
|
|
|
this.updateDifference(actual, expectedCash, actualCashInput.value);
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Update difference box with color coding
|
|
|
|
|
*/
|
|
|
|
|
updateDifference(actual, expected, rawValue) {
|
|
|
|
|
const box = document.getElementById("differenceBox");
|
|
|
|
|
const value = document.getElementById("differenceValue");
|
|
|
|
|
if (!box || !value) return;
|
|
|
|
|
const diff = actual - expected;
|
|
|
|
|
box.classList.remove("positive", "negative", "neutral");
|
|
|
|
|
if (actual === 0 && rawValue === "") {
|
|
|
|
|
value.textContent = "\u2013 kr";
|
|
|
|
|
box.classList.add("neutral");
|
|
|
|
|
} else if (diff > 0) {
|
|
|
|
|
value.textContent = "+" + this.formatNumber(diff) + " kr";
|
|
|
|
|
box.classList.add("positive");
|
|
|
|
|
} else if (diff < 0) {
|
|
|
|
|
value.textContent = this.formatNumber(diff) + " kr";
|
|
|
|
|
box.classList.add("negative");
|
2026-01-11 21:08:56 +01:00
|
|
|
} else {
|
2026-01-12 22:10:57 +01:00
|
|
|
value.textContent = "0,00 kr";
|
|
|
|
|
box.classList.add("neutral");
|
2026-01-11 21:08:56 +01:00
|
|
|
}
|
|
|
|
|
}
|
2026-01-12 22:10:57 +01:00
|
|
|
/**
|
|
|
|
|
* Setup checkbox selection for table rows
|
|
|
|
|
*/
|
|
|
|
|
setupCheckboxSelection() {
|
|
|
|
|
const selectAll = document.getElementById("selectAll");
|
|
|
|
|
const rowCheckboxes = document.querySelectorAll(".row-select");
|
|
|
|
|
const exportBtn = document.getElementById("exportBtn");
|
|
|
|
|
const selectionCount = document.getElementById("selectionCount");
|
|
|
|
|
if (!selectAll || !exportBtn || !selectionCount) return;
|
|
|
|
|
const updateSelection = () => {
|
|
|
|
|
const checked = document.querySelectorAll(".row-select:checked");
|
|
|
|
|
const count = checked.length;
|
|
|
|
|
selectionCount.textContent = count === 0 ? "0 valgt" : `${count} valgt`;
|
|
|
|
|
exportBtn.disabled = count === 0;
|
|
|
|
|
selectAll.checked = count === rowCheckboxes.length && count > 0;
|
|
|
|
|
selectAll.indeterminate = count > 0 && count < rowCheckboxes.length;
|
|
|
|
|
};
|
|
|
|
|
selectAll.addEventListener("change", () => {
|
|
|
|
|
rowCheckboxes.forEach((cb) => cb.checked = selectAll.checked);
|
|
|
|
|
updateSelection();
|
|
|
|
|
});
|
|
|
|
|
rowCheckboxes.forEach((cb) => {
|
|
|
|
|
cb.addEventListener("change", updateSelection);
|
|
|
|
|
cb.addEventListener("click", (e) => e.stopPropagation());
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Setup approval checkbox to enable/disable approve button
|
|
|
|
|
*/
|
|
|
|
|
setupApprovalCheckbox() {
|
|
|
|
|
const checkbox = document.getElementById("confirmCheckbox");
|
|
|
|
|
const approveBtn = document.getElementById("approveBtn");
|
|
|
|
|
if (!checkbox || !approveBtn) return;
|
|
|
|
|
checkbox.addEventListener("change", () => {
|
|
|
|
|
approveBtn.disabled = !checkbox.checked;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Setup date filter defaults (last 30 days)
|
|
|
|
|
*/
|
|
|
|
|
setupDateFilters() {
|
|
|
|
|
const dateFrom = document.getElementById("dateFrom");
|
|
|
|
|
const dateTo = document.getElementById("dateTo");
|
|
|
|
|
if (!dateFrom || !dateTo) return;
|
|
|
|
|
const today = /* @__PURE__ */ new Date();
|
|
|
|
|
const thirtyDaysAgo = new Date(today);
|
|
|
|
|
thirtyDaysAgo.setDate(today.getDate() - 30);
|
|
|
|
|
dateTo.value = this.formatDateISO(today);
|
|
|
|
|
dateFrom.value = this.formatDateISO(thirtyDaysAgo);
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Format number as Danish currency
|
|
|
|
|
*/
|
|
|
|
|
formatNumber(num) {
|
|
|
|
|
return num.toLocaleString("da-DK", {
|
|
|
|
|
minimumFractionDigits: 2,
|
|
|
|
|
maximumFractionDigits: 2
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Parse Danish number format
|
|
|
|
|
*/
|
|
|
|
|
parseNumber(str) {
|
|
|
|
|
if (!str) return 0;
|
|
|
|
|
return parseFloat(str.replace(/\./g, "").replace(",", ".")) || 0;
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Format date as ISO string (YYYY-MM-DD)
|
|
|
|
|
*/
|
|
|
|
|
formatDateISO(date) {
|
|
|
|
|
return date.toISOString().split("T")[0];
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Setup row toggle for expandable details
|
|
|
|
|
*/
|
|
|
|
|
setupRowToggle() {
|
|
|
|
|
const rows = document.querySelectorAll("swp-cash-table-row[data-id]:not(.draft-row)");
|
|
|
|
|
rows.forEach((row) => {
|
|
|
|
|
const rowId = row.getAttribute("data-id");
|
|
|
|
|
if (!rowId) return;
|
|
|
|
|
const detail = document.querySelector(`swp-cash-row-detail[data-for="${rowId}"]`);
|
|
|
|
|
if (!detail) return;
|
|
|
|
|
row.addEventListener("click", (e) => {
|
|
|
|
|
if (e.target.closest('input[type="checkbox"]')) return;
|
|
|
|
|
const icon = row.querySelector("swp-row-toggle i");
|
|
|
|
|
const isExpanded = row.classList.contains("expanded");
|
|
|
|
|
document.querySelectorAll("swp-cash-table-row.expanded").forEach((r) => {
|
|
|
|
|
if (r !== row) {
|
|
|
|
|
const otherId = r.getAttribute("data-id");
|
|
|
|
|
if (otherId) {
|
|
|
|
|
const otherDetail = document.querySelector(`swp-cash-row-detail[data-for="${otherId}"]`);
|
|
|
|
|
const otherIcon = r.querySelector("swp-row-toggle i");
|
|
|
|
|
if (otherDetail && otherIcon) {
|
|
|
|
|
this.collapseRow(r, otherDetail, otherIcon);
|
|
|
|
|
}
|
2026-01-11 21:08:56 +01:00
|
|
|
}
|
|
|
|
|
}
|
2026-01-12 22:10:57 +01:00
|
|
|
});
|
|
|
|
|
if (isExpanded) {
|
|
|
|
|
this.collapseRow(row, detail, icon);
|
|
|
|
|
} else {
|
|
|
|
|
this.expandRow(row, detail, icon);
|
2026-01-11 21:08:56 +01:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-01-12 22:10:57 +01:00
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Expand a row with animation
|
|
|
|
|
*/
|
|
|
|
|
expandRow(row, detail, icon) {
|
|
|
|
|
row.classList.add("expanded");
|
|
|
|
|
detail.classList.add("expanded");
|
|
|
|
|
icon?.animate([
|
|
|
|
|
{ transform: "rotate(0deg)" },
|
|
|
|
|
{ transform: "rotate(90deg)" }
|
2026-01-11 21:08:56 +01:00
|
|
|
], {
|
2026-01-12 22:10:57 +01:00
|
|
|
duration: 200,
|
2026-01-11 21:08:56 +01:00
|
|
|
easing: "ease-out",
|
|
|
|
|
fill: "forwards"
|
|
|
|
|
});
|
2026-01-12 22:10:57 +01:00
|
|
|
const content = detail.querySelector("swp-row-detail-content");
|
|
|
|
|
if (content) {
|
|
|
|
|
const height = content.offsetHeight;
|
|
|
|
|
detail.animate([
|
|
|
|
|
{ height: "0px", opacity: 0 },
|
|
|
|
|
{ height: `${height}px`, opacity: 1 }
|
|
|
|
|
], {
|
|
|
|
|
duration: 250,
|
|
|
|
|
easing: "ease-out",
|
|
|
|
|
fill: "forwards"
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-01-11 21:08:56 +01:00
|
|
|
}
|
2026-01-12 22:10:57 +01:00
|
|
|
/**
|
|
|
|
|
* Collapse a row with animation
|
|
|
|
|
*/
|
|
|
|
|
collapseRow(row, detail, icon) {
|
|
|
|
|
icon?.animate([
|
|
|
|
|
{ transform: "rotate(90deg)" },
|
|
|
|
|
{ transform: "rotate(0deg)" }
|
2026-01-11 21:08:56 +01:00
|
|
|
], {
|
|
|
|
|
duration: 200,
|
|
|
|
|
easing: "ease-out",
|
|
|
|
|
fill: "forwards"
|
|
|
|
|
});
|
2026-01-12 22:10:57 +01:00
|
|
|
const content = detail.querySelector("swp-row-detail-content");
|
|
|
|
|
if (content) {
|
|
|
|
|
const height = content.offsetHeight;
|
|
|
|
|
const animation = detail.animate([
|
|
|
|
|
{ height: `${height}px`, opacity: 1 },
|
|
|
|
|
{ height: "0px", opacity: 0 }
|
|
|
|
|
], {
|
|
|
|
|
duration: 200,
|
|
|
|
|
easing: "ease-out",
|
|
|
|
|
fill: "forwards"
|
|
|
|
|
});
|
|
|
|
|
animation.onfinish = () => {
|
|
|
|
|
row.classList.remove("expanded");
|
|
|
|
|
detail.classList.remove("expanded");
|
|
|
|
|
};
|
|
|
|
|
} else {
|
2026-01-11 21:08:56 +01:00
|
|
|
row.classList.remove("expanded");
|
|
|
|
|
detail.classList.remove("expanded");
|
2026-01-12 22:10:57 +01:00
|
|
|
}
|
2026-01-11 21:08:56 +01:00
|
|
|
}
|
2026-01-12 22:10:57 +01:00
|
|
|
/**
|
|
|
|
|
* Setup draft row click to navigate to reconciliation tab
|
|
|
|
|
*/
|
|
|
|
|
setupDraftRowClick() {
|
|
|
|
|
const draftRow = document.querySelector("swp-cash-table-row.draft-row");
|
|
|
|
|
if (!draftRow) return;
|
|
|
|
|
draftRow.style.cursor = "pointer";
|
|
|
|
|
draftRow.addEventListener("click", (e) => {
|
|
|
|
|
if (e.target.closest('input[type="checkbox"]')) return;
|
|
|
|
|
this.switchToTab("afstemning");
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-01-10 20:39:17 +01:00
|
|
|
|
2026-01-12 22:10:57 +01:00
|
|
|
// wwwroot/ts/modules/employees.ts
|
|
|
|
|
var EmployeesController = class {
|
|
|
|
|
constructor() {
|
2026-01-13 22:37:29 +01:00
|
|
|
this.ratesSync = null;
|
2026-01-12 22:10:57 +01:00
|
|
|
this.listView = null;
|
|
|
|
|
this.detailView = null;
|
|
|
|
|
this.listView = document.getElementById("employees-list-view");
|
|
|
|
|
this.detailView = document.getElementById("employee-detail-view");
|
|
|
|
|
if (!this.listView) return;
|
|
|
|
|
this.setupListTabs();
|
|
|
|
|
this.setupDetailTabs();
|
|
|
|
|
this.setupChevronNavigation();
|
|
|
|
|
this.setupBackNavigation();
|
|
|
|
|
this.setupHistoryNavigation();
|
|
|
|
|
this.restoreStateFromUrl();
|
2026-01-13 22:37:29 +01:00
|
|
|
this.ratesSync = new RatesSyncController();
|
2026-01-12 22:10:57 +01:00
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Setup popstate listener for browser back/forward
|
|
|
|
|
*/
|
|
|
|
|
setupHistoryNavigation() {
|
|
|
|
|
window.addEventListener("popstate", (e) => {
|
|
|
|
|
if (e.state?.employeeKey) {
|
|
|
|
|
this.showDetailViewInternal(e.state.employeeKey);
|
|
|
|
|
} else {
|
|
|
|
|
this.showListViewInternal();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Restore view state from URL on page load
|
|
|
|
|
*/
|
|
|
|
|
restoreStateFromUrl() {
|
|
|
|
|
const hash = window.location.hash;
|
|
|
|
|
if (hash.startsWith("#employee-")) {
|
|
|
|
|
const employeeKey = hash.substring(1);
|
|
|
|
|
this.showDetailViewInternal(employeeKey);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Setup tab switching for the list view
|
|
|
|
|
*/
|
|
|
|
|
setupListTabs() {
|
|
|
|
|
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
|
|
|
|
|
*/
|
|
|
|
|
setupDetailTabs() {
|
|
|
|
|
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
|
|
|
|
|
*/
|
|
|
|
|
switchTab(container, targetTab) {
|
|
|
|
|
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
|
|
|
|
|
*/
|
|
|
|
|
setupChevronNavigation() {
|
|
|
|
|
document.addEventListener("click", (e) => {
|
|
|
|
|
const target = e.target;
|
|
|
|
|
if (target.closest("swp-icon-btn") || target.closest("swp-table-actions")) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const row = target.closest("swp-employee-row[data-employee-detail]");
|
|
|
|
|
if (row) {
|
|
|
|
|
const employeeKey = row.dataset.employeeDetail;
|
|
|
|
|
if (employeeKey) {
|
|
|
|
|
this.showDetailView(employeeKey);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Setup back button to return to list view
|
|
|
|
|
*/
|
|
|
|
|
setupBackNavigation() {
|
|
|
|
|
document.addEventListener("click", (e) => {
|
|
|
|
|
const target = e.target;
|
|
|
|
|
const backLink = target.closest("[data-employee-back]");
|
|
|
|
|
if (backLink) {
|
|
|
|
|
this.showListView();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Show the detail view and hide list view (with history push)
|
|
|
|
|
*/
|
|
|
|
|
showDetailView(employeeKey) {
|
|
|
|
|
history.pushState(
|
|
|
|
|
{ employeeKey },
|
|
|
|
|
"",
|
|
|
|
|
`#${employeeKey}`
|
|
|
|
|
);
|
|
|
|
|
this.showDetailViewInternal(employeeKey);
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Show detail view without modifying history (for popstate)
|
|
|
|
|
*/
|
|
|
|
|
showDetailViewInternal(employeeKey) {
|
|
|
|
|
if (this.listView && this.detailView) {
|
|
|
|
|
this.listView.style.display = "none";
|
|
|
|
|
this.detailView.style.display = "block";
|
|
|
|
|
this.detailView.dataset.employee = employeeKey;
|
|
|
|
|
this.switchTab(this.detailView, "general");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Show the list view and hide detail view (with history push)
|
|
|
|
|
*/
|
|
|
|
|
showListView() {
|
|
|
|
|
history.pushState(
|
|
|
|
|
{},
|
|
|
|
|
"",
|
|
|
|
|
window.location.pathname
|
|
|
|
|
);
|
|
|
|
|
this.showListViewInternal();
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Show list view without modifying history (for popstate)
|
|
|
|
|
*/
|
|
|
|
|
showListViewInternal() {
|
|
|
|
|
if (this.listView && this.detailView) {
|
|
|
|
|
this.detailView.style.display = "none";
|
|
|
|
|
this.listView.style.display = "block";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-01-13 22:37:29 +01:00
|
|
|
var RatesSyncController = class {
|
|
|
|
|
constructor() {
|
|
|
|
|
this.drawer = null;
|
|
|
|
|
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")
|
|
|
|
|
*/
|
|
|
|
|
extractRateKey(checkboxId) {
|
|
|
|
|
const match = checkboxId.match(/^rate-(.+)-enabled$/);
|
|
|
|
|
return match ? match[1] : null;
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Setup checkbox change listeners in drawer
|
|
|
|
|
*/
|
|
|
|
|
setupCheckboxListeners() {
|
|
|
|
|
if (!this.drawer) return;
|
|
|
|
|
this.drawer.addEventListener("change", (e) => {
|
|
|
|
|
const target = e.target;
|
|
|
|
|
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;
|
|
|
|
|
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);
|
|
|
|
|
this.toggleCardRow(rateKey, isChecked);
|
|
|
|
|
if (isChecked) {
|
|
|
|
|
const textInput = document.getElementById(`rate-${rateKey}`);
|
|
|
|
|
if (textInput) {
|
|
|
|
|
this.syncValueToCard(rateKey, textInput.value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Setup input change listeners in drawer
|
|
|
|
|
*/
|
|
|
|
|
setupInputListeners() {
|
|
|
|
|
if (!this.drawer) return;
|
|
|
|
|
this.drawer.addEventListener("input", (e) => {
|
|
|
|
|
const target = e.target;
|
|
|
|
|
if (target.type !== "text" || !target.id) return;
|
|
|
|
|
const match = target.id.match(/^rate-(.+)$/);
|
|
|
|
|
if (!match) return;
|
|
|
|
|
const rateKey = match[1];
|
|
|
|
|
if (rateKey.endsWith("-enabled")) return;
|
|
|
|
|
this.syncValueToCard(rateKey, target.value);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Toggle card row visibility by ID
|
|
|
|
|
*/
|
|
|
|
|
toggleCardRow(rateKey, visible) {
|
|
|
|
|
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)
|
|
|
|
|
*/
|
|
|
|
|
formatNumber(value) {
|
|
|
|
|
const normalized = value.replace(",", ".");
|
|
|
|
|
const num = parseFloat(normalized);
|
|
|
|
|
if (isNaN(num)) return value;
|
|
|
|
|
return num.toFixed(2).replace(".", ",");
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Sync value from drawer to card by ID
|
|
|
|
|
*/
|
|
|
|
|
syncValueToCard(rateKey, value) {
|
|
|
|
|
const cardInput = document.getElementById(`value-${rateKey}`);
|
|
|
|
|
if (!cardInput) return;
|
|
|
|
|
const textInput = document.getElementById(`rate-${rateKey}`);
|
|
|
|
|
const inputContainer = textInput?.closest("swp-data-input");
|
|
|
|
|
const unit = inputContainer?.textContent?.trim().replace(value, "").trim() || "kr";
|
|
|
|
|
const formattedValue = this.formatNumber(value);
|
|
|
|
|
cardInput.value = `${formattedValue} ${unit}`;
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-01-12 22:10:57 +01:00
|
|
|
|
|
|
|
|
// wwwroot/ts/app.ts
|
|
|
|
|
var App = class {
|
|
|
|
|
constructor() {
|
|
|
|
|
this.sidebar = new SidebarController();
|
|
|
|
|
this.drawers = new DrawerController();
|
|
|
|
|
this.theme = new ThemeController();
|
|
|
|
|
this.search = new SearchController();
|
|
|
|
|
this.lockScreen = new LockScreenController(this.drawers);
|
|
|
|
|
this.cash = new CashController();
|
|
|
|
|
this.employees = new EmployeesController();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
var app;
|
|
|
|
|
function init() {
|
|
|
|
|
app = new App();
|
|
|
|
|
if (typeof window !== "undefined") {
|
|
|
|
|
window.app = app;
|
|
|
|
|
}
|
2026-01-10 20:39:17 +01:00
|
|
|
}
|
2026-01-12 22:10:57 +01:00
|
|
|
if (document.readyState === "loading") {
|
|
|
|
|
document.addEventListener("DOMContentLoaded", init);
|
|
|
|
|
} else {
|
|
|
|
|
init();
|
2026-01-10 20:39:17 +01:00
|
|
|
}
|
2026-01-12 22:10:57 +01:00
|
|
|
var app_default = App;
|
|
|
|
|
})();
|
|
|
|
|
//# sourceMappingURL=app.js.map
|