From f71f00099a2189d8fbcd984b3c453e2d97ed8ef8 Mon Sep 17 00:00:00 2001 From: "Janus C. H. Knudsen" Date: Tue, 13 Jan 2026 22:37:29 +0100 Subject: [PATCH] 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 --- .claude/settings.local.json | 3 +- .../Components/EmployeeDetailCatalog.cs | 54 +++ .../EmployeeDetailGeneral/Default.cshtml | 24 +- .../EmployeeDetailHR/Default.cshtml | 191 +++++++++-- .../EmployeeDetailHRViewComponent.cs | 157 ++++++++- .../EmployeeDetailSalary/Default.cshtml | 240 +++++++++++-- .../EmployeeDetailSalaryViewComponent.cs | 163 ++++++++- .../Localization/Translations/da.json | 65 +++- .../Localization/Translations/en.json | 76 +++++ .../wwwroot/css/COMPONENT-CATALOG.md | 153 ++++++++- .../wwwroot/css/components.css | 55 +++ .../wwwroot/css/employees.css | 321 ++++++++++++++++-- PlanTempus.Application/wwwroot/js/app.js | 89 +++++ PlanTempus.Application/wwwroot/js/app.js.map | 4 +- .../wwwroot/ts/modules/employees.ts | 131 +++++++ 15 files changed, 1589 insertions(+), 137 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 822b0ef..47b3665 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -7,7 +7,8 @@ "Bash(find:*)", "Bash(tree:*)", "Bash(npm run analyze-css:*)", - "Bash(node:*)" + "Bash(node:*)", + "Bash(npx esbuild:*)" ] } } diff --git a/PlanTempus.Application/Features/Employees/Components/EmployeeDetailCatalog.cs b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailCatalog.cs index 6aa1517..4d7c5c2 100644 --- a/PlanTempus.Application/Features/Employees/Components/EmployeeDetailCatalog.cs +++ b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailCatalog.cs @@ -220,8 +220,62 @@ public record EmployeeDetailRecord public required string HourlyRate { get; init; } public required string MonthlyFixedSalary { get; init; } + // Salary - Rates + public string NormalRate { get; init; } = "131,49 kr"; + public string OvertimeRate { get; init; } = "280,50 kr"; + public string VacationRate { get; init; } = "140,25 kr"; + + // Salary - Commission + public string MinimumPerHour { get; init; } = "220 kr"; + public string ServiceCommission { get; init; } = "15%"; + public string ProductCommission { get; init; } = "15%"; + + // Salary - Supplements + public string WeekdaySupplement { get; init; } = "28,03 kr"; + public string SaturdaySupplement { get; init; } = "56,02 kr"; + public string SundaySupplement { get; init; } = "112,07 kr"; + + // Salary - Rate values (numeric only, for drawer inputs) + public string NormalRateValue { get; init; } = "131,49"; + public string OvertimeRateValue { get; init; } = "280,50"; + public string CourseRateValue { get; init; } = "140,25"; + public string TimeOffRateValue { get; init; } = "140,25"; + public string PaidLeaveRateValue { get; init; } = "140,25"; + public string VacationRateValue { get; init; } = "140,25"; + public string OfficeRateValue { get; init; } = "140,25"; + public string ChildSickRateValue { get; init; } = "140,25"; + public string ChildHospitalRateValue { get; init; } = "140,25"; + public string MaternityRateValue { get; init; } = "140,25"; + public string WeekdaySupplementValue { get; init; } = "28,03"; + public string SaturdaySupplementValue { get; init; } = "56,02"; + public string SundaySupplementValue { get; init; } = "112,07"; + public string ProductCommissionValue { get; init; } = "15"; + public string ServiceCommissionValue { get; init; } = "15"; + + // HR - Contract + public string ContractType { get; init; } = "Fastansættelse"; + public string TerminationNotice { get; init; } = "1 måned"; + public string ContractExpiry { get; init; } = "— (ingen udløb)"; + + // HR - Vacation + public string VacationEarned { get; init; } = "25 dage"; + public string VacationUsed { get; init; } = "12 dage"; + public string VacationRemaining { get; init; } = "13 dage"; + + // HR - Absence + public string SickDays2025 { get; init; } = "3 dage"; + public string SickDays2024 { get; init; } = "7 dage"; + public string ChildSickDays2025 { get; init; } = "1 dag"; + public string MaternityLeave { get; init; } = "— (ingen planlagt)"; + // Tags (certifications, specialties) public List Tags { get; init; } = new(); } public record EmployeeTag(string Text, string CssClass); + +// HR data records +public record CertificationRecord(string Name, string ExpiryDate, string Status, string StatusClass); +public record CourseRecord(string Name, string Provider, string Date, string? Status = null); +public record DocumentRecord(string Name, string UploadDate); +public record PlannedAbsenceRecord(string Dates, string Type, string TypeClass); diff --git a/PlanTempus.Application/Features/Employees/Components/EmployeeDetailGeneral/Default.cshtml b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailGeneral/Default.cshtml index 9b4d08a..b8d7f3c 100644 --- a/PlanTempus.Application/Features/Employees/Components/EmployeeDetailGeneral/Default.cshtml +++ b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailGeneral/Default.cshtml @@ -9,23 +9,23 @@ @Model.LabelFullName - @Model.Name + @Model.LabelEmail - @Model.Email + @Model.LabelPhone - @Model.Phone + @Model.LabelAddress - @Model.Address + @Model.LabelPostalCity - @Model.PostalCity + @@ -36,15 +36,15 @@ @Model.LabelBirthDate - @Model.BirthDate + @Model.LabelEmergencyContact - @Model.EmergencyContact + @Model.LabelEmergencyPhone - @Model.EmergencyPhone + @@ -58,19 +58,19 @@ @Model.LabelEmploymentDate - @Model.EmploymentDate + @Model.LabelPosition - @Model.Position + @Model.LabelEmploymentType - @Model.EmploymentType + @Model.LabelHoursPerWeek - @Model.HoursPerWeek + diff --git a/PlanTempus.Application/Features/Employees/Components/EmployeeDetailHR/Default.cshtml b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailHR/Default.cshtml index 819af4d..3fcbda2 100644 --- a/PlanTempus.Application/Features/Employees/Components/EmployeeDetailHR/Default.cshtml +++ b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailHR/Default.cshtml @@ -1,44 +1,183 @@ @model PlanTempus.Application.Features.Employees.Components.EmployeeDetailHRViewModel - - @Model.LabelDocuments - - - - @Model.LabelContract - 15. aug 2019 - - - - Lønaftale 2024 - 1. jan 2024 - - - + +
+ + + @Model.LabelContractDocuments + + + @Model.LabelContractType + + + + + + @Model.LabelTerminationNotice + + + + + + @Model.LabelContractExpiry + @Model.ContractExpiry + + + + + @foreach (var doc in Model.Documents) + { + + + + @doc.Name + @doc.UploadDate + + + Vis + + + } + + + + @Model.LabelUploadDocument + + + + + @Model.LabelPlannedAbsence + + @foreach (var absence in Model.PlannedAbsences) + { + + @absence.Dates + @absence.Type + + } + + + @Model.LabelAddAbsence + +
+ + +
+ + + @Model.LabelCertifications + + @foreach (var cert in Model.Certifications) + { + + + + @cert.Name + @cert.ExpiryDate + + + @cert.Status + + + } + + + @Model.LabelAddCertification + + + + + @Model.LabelCourses + + + @Model.LabelCompletedCourses + + @foreach (var course in Model.CompletedCourses) + { + + + @course.Name + @course.Provider · @course.Date + + + } + + + + + @Model.LabelPlannedCourses + + @foreach (var course in Model.PlannedCourses) + { + + + @course.Name + @course.Provider · @course.Date + + @if (!string.IsNullOrEmpty(course.Status)) + { + + @course.Status + + } + + } + + + + + @Model.LabelAddCourse + +
+
+ + + - @Model.LabelVacation + @Model.LabelVacationBalance - Optjent ferie - 25 dage + @Model.LabelVacationEarned + @Model.VacationEarned - Afholdt ferie - 12 dage + @Model.LabelVacationUsed + @Model.VacationUsed - Resterende - 13 dage + @Model.LabelVacationRemaining + @Model.VacationRemaining - @Model.LabelNotes - - Ingen noter tilføjet endnu... - + @Model.LabelAbsenceSickness + + + @Model.LabelSickDays2025 + @Model.SickDays2025 + + + @Model.LabelSickDays2024 + @Model.SickDays2024 + + + @Model.LabelChildSickDays2025 + @Model.ChildSickDays2025 + + + @Model.LabelMaternityLeave + @Model.MaternityLeave + + diff --git a/PlanTempus.Application/Features/Employees/Components/EmployeeDetailHR/EmployeeDetailHRViewComponent.cs b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailHR/EmployeeDetailHRViewComponent.cs index 269dcc3..58c007a 100644 --- a/PlanTempus.Application/Features/Employees/Components/EmployeeDetailHR/EmployeeDetailHRViewComponent.cs +++ b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailHR/EmployeeDetailHRViewComponent.cs @@ -14,13 +14,95 @@ public class EmployeeDetailHRViewComponent : ViewComponent public IViewComponentResult Invoke(string key) { + var employee = EmployeeDetailCatalog.Get(key); + var model = new EmployeeDetailHRViewModel { - LabelDocuments = _localization.Get("employees.detail.hr.documents"), - LabelContract = _localization.Get("employees.detail.hr.contract"), - LabelVacation = _localization.Get("employees.detail.hr.vacation"), - LabelSickLeave = _localization.Get("employees.detail.hr.sickleave"), - LabelNotes = _localization.Get("employees.detail.hr.notes") + // Contract data + ContractType = employee.ContractType, + TerminationNotice = employee.TerminationNotice, + ContractExpiry = employee.ContractExpiry, + + // Vacation data + VacationEarned = employee.VacationEarned, + VacationUsed = employee.VacationUsed, + VacationRemaining = employee.VacationRemaining, + + // Absence data + SickDays2025 = employee.SickDays2025, + SickDays2024 = employee.SickDays2024, + ChildSickDays2025 = employee.ChildSickDays2025, + MaternityLeave = employee.MaternityLeave, + + // Labels - Contract & Documents + LabelContractDocuments = _localization.Get("employees.detail.hr.contractdocuments"), + LabelContractType = _localization.Get("employees.detail.hr.contracttype"), + LabelTerminationNotice = _localization.Get("employees.detail.hr.terminationnotice"), + LabelContractExpiry = _localization.Get("employees.detail.hr.contractexpiry"), + LabelUploadDocument = _localization.Get("employees.detail.hr.uploaddocument"), + + // Labels - Certifications + LabelCertifications = _localization.Get("employees.detail.hr.certifications"), + LabelAddCertification = _localization.Get("employees.detail.hr.addcertification"), + + // Labels - Courses + LabelCourses = _localization.Get("employees.detail.hr.courses"), + LabelCompletedCourses = _localization.Get("employees.detail.hr.completedcourses"), + LabelPlannedCourses = _localization.Get("employees.detail.hr.plannedcourses"), + LabelAddCourse = _localization.Get("employees.detail.hr.addcourse"), + + // Labels - Vacation + LabelVacationBalance = _localization.Get("employees.detail.hr.vacationbalance"), + LabelVacationEarned = _localization.Get("employees.detail.hr.vacationearned"), + LabelVacationUsed = _localization.Get("employees.detail.hr.vacationused"), + LabelVacationRemaining = _localization.Get("employees.detail.hr.vacationremaining"), + + // Labels - Absence + LabelAbsenceSickness = _localization.Get("employees.detail.hr.absencesickness"), + LabelSickDays2025 = _localization.Get("employees.detail.hr.sickdays2025"), + LabelSickDays2024 = _localization.Get("employees.detail.hr.sickdays2024"), + LabelChildSickDays2025 = _localization.Get("employees.detail.hr.childsickdays2025"), + LabelMaternityLeave = _localization.Get("employees.detail.hr.maternityleave"), + + // Labels - Planned absence + LabelPlannedAbsence = _localization.Get("employees.detail.hr.plannedabsence"), + LabelAddAbsence = _localization.Get("employees.detail.hr.addabsence"), + + // Contract type options + ContractTypeOptions = new List { "Fastansættelse", "Tidsbegrænset", "Freelance", "Elev/Lærling" }, + TerminationNoticeOptions = new List { "14 dage", "1 måned", "3 måneder" }, + + // Mock data - Documents + Documents = new List + { + new("Ansættelseskontrakt.pdf", "Uploadet 1. aug 2019"), + new("Tillæg - Lønforhøjelse 2023.pdf", "Uploadet 15. jan 2023") + }, + + // Mock data - Certifications + Certifications = new List + { + new("Balayage Specialist", "Udløber: 15. juni 2026", "Gyldig", "valid"), + new("Farvecertificering (Wella)", "Udløber: 1. marts 2025", "Udløber snart", "expiring") + }, + + // Mock data - Courses + CompletedCourses = new List + { + new("Avanceret balayage teknikker", "Wella Academy", "Marts 2024"), + new("Kundeservice & mersalg", "SalonUp", "November 2023") + }, + PlannedCourses = new List + { + new("Olaplex certificering", "Olaplex DK", "15. februar 2026", "Tilmeldt") + }, + + // Mock data - Planned absence + PlannedAbsences = new List + { + new("23. dec – 2. jan 2026", "Ferie", "ferie"), + new("14. feb 2025", "Fri", "fri") + } }; return View(model); @@ -29,9 +111,64 @@ public class EmployeeDetailHRViewComponent : ViewComponent public class EmployeeDetailHRViewModel { - public required string LabelDocuments { get; init; } - public required string LabelContract { get; init; } - public required string LabelVacation { get; init; } - public required string LabelSickLeave { get; init; } - public required string LabelNotes { get; init; } + // Contract data + public required string ContractType { get; init; } + public required string TerminationNotice { get; init; } + public required string ContractExpiry { get; init; } + + // Vacation data + public required string VacationEarned { get; init; } + public required string VacationUsed { get; init; } + public required string VacationRemaining { get; init; } + + // Absence data + public required string SickDays2025 { get; init; } + public required string SickDays2024 { get; init; } + public required string ChildSickDays2025 { get; init; } + public required string MaternityLeave { get; init; } + + // Labels - Contract & Documents + public required string LabelContractDocuments { get; init; } + public required string LabelContractType { get; init; } + public required string LabelTerminationNotice { get; init; } + public required string LabelContractExpiry { get; init; } + public required string LabelUploadDocument { get; init; } + + // Labels - Certifications + public required string LabelCertifications { get; init; } + public required string LabelAddCertification { get; init; } + + // Labels - Courses + public required string LabelCourses { get; init; } + public required string LabelCompletedCourses { get; init; } + public required string LabelPlannedCourses { get; init; } + public required string LabelAddCourse { get; init; } + + // Labels - Vacation + public required string LabelVacationBalance { get; init; } + public required string LabelVacationEarned { get; init; } + public required string LabelVacationUsed { get; init; } + public required string LabelVacationRemaining { get; init; } + + // Labels - Absence + public required string LabelAbsenceSickness { get; init; } + public required string LabelSickDays2025 { get; init; } + public required string LabelSickDays2024 { get; init; } + public required string LabelChildSickDays2025 { get; init; } + public required string LabelMaternityLeave { get; init; } + + // Labels - Planned absence + public required string LabelPlannedAbsence { get; init; } + public required string LabelAddAbsence { get; init; } + + // Contract type options + public List ContractTypeOptions { get; init; } = new(); + public List TerminationNoticeOptions { get; init; } = new(); + + // Data collections + public List Documents { get; init; } = new(); + public List Certifications { get; init; } = new(); + public List CompletedCourses { get; init; } = new(); + public List PlannedCourses { get; init; } = new(); + public List PlannedAbsences { get; init; } = new(); } diff --git a/PlanTempus.Application/Features/Employees/Components/EmployeeDetailSalary/Default.cshtml b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailSalary/Default.cshtml index 1bcb33c..074f145 100644 --- a/PlanTempus.Application/Features/Employees/Components/EmployeeDetailSalary/Default.cshtml +++ b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailSalary/Default.cshtml @@ -1,39 +1,215 @@ @model PlanTempus.Application.Features.Employees.Components.EmployeeDetailSalaryViewModel - - @Model.LabelPaymentInfo - - - @Model.LabelBankAccount - @Model.BankAccount - - - @Model.LabelTaxCard - @Model.TaxCard - - - +
+ + + + @Model.LabelRates + @Model.LabelEdit + + + + @Model.LabelNormalRate + + + + @Model.LabelOvertimeRate + + + + + + + @Model.LabelVacationRate + + + + + + + + + + + + @Model.LabelSupplements + + + @Model.LabelWeekdaySupplement + + + + @Model.LabelSaturdaySupplement + + + + @Model.LabelSundaySupplement + + + + + + + + @Model.LabelCommission + + + @Model.LabelProductCommission + + + + @Model.LabelServiceCommission + + + + +
- @Model.LabelSalarySettings - - - @Model.LabelHourlyRate - @Model.HourlyRate - - - @Model.LabelMonthlyFixed - @Model.MonthlyFixedSalary - - - @Model.LabelCommission - 10% - - - @Model.LabelProductCommission - 5% - - + @Model.LabelSalaryHistory + + + @Model.LabelPeriod + @Model.LabelGrossSalary + + + + @foreach (var item in Model.SalaryHistory) + { + + @item.Period + @item.GrossSalary + + + } + +
+ + +
+ + @Model.LabelRatesDrawerTitle + + + + + + + @Model.LabelBaseRates + + + + @Model.LabelNormalRate + kr + + + + @Model.LabelOvertimeRate + kr + + + + @Model.LabelCourseRate + kr + + + + @Model.LabelTimeOffRate + kr + + + + @Model.LabelPaidLeaveRate + kr + + + + @Model.LabelVacationRate + kr + + + + @Model.LabelOfficeRate + kr + + + + @Model.LabelChildSickRate + kr + + + + @Model.LabelChildHospitalRate + kr + + + + @Model.LabelMaternityRate + kr + + + + + + @Model.LabelSupplements + + + + @Model.LabelWeekdaySupplementFull + kr + + + + @Model.LabelSaturdaySupplementFull + kr + + + + @Model.LabelSundaySupplement + kr + + + + + + + @Model.LabelCommission + + + + @Model.LabelProductCommissionFull + % + + + + @Model.LabelServiceCommissionFull + % + + + + +
diff --git a/PlanTempus.Application/Features/Employees/Components/EmployeeDetailSalary/EmployeeDetailSalaryViewComponent.cs b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailSalary/EmployeeDetailSalaryViewComponent.cs index 48349bc..e9a5b52 100644 --- a/PlanTempus.Application/Features/Employees/Components/EmployeeDetailSalary/EmployeeDetailSalaryViewComponent.cs +++ b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailSalary/EmployeeDetailSalaryViewComponent.cs @@ -18,18 +18,86 @@ public class EmployeeDetailSalaryViewComponent : ViewComponent var model = new EmployeeDetailSalaryViewModel { + // Data BankAccount = employee.BankAccount, TaxCard = employee.TaxCard, HourlyRate = employee.HourlyRate, MonthlyFixedSalary = employee.MonthlyFixedSalary, - LabelPaymentInfo = _localization.Get("employees.detail.salary.paymentinfo"), - LabelBankAccount = _localization.Get("employees.detail.salary.bankaccount"), - LabelTaxCard = _localization.Get("employees.detail.salary.taxcard"), - LabelSalarySettings = _localization.Get("employees.detail.salary.settings"), - LabelHourlyRate = _localization.Get("employees.detail.salary.hourlyrate"), - LabelMonthlyFixed = _localization.Get("employees.detail.salary.monthlyfixed"), + + // Rates + NormalRate = employee.NormalRate, + OvertimeRate = employee.OvertimeRate, + VacationRate = employee.VacationRate, + + // Commission + MinimumPerHour = employee.MinimumPerHour, + ServiceCommission = employee.ServiceCommission, + ProductCommission = employee.ProductCommission, + + // Supplements + WeekdaySupplement = employee.WeekdaySupplement, + SaturdaySupplement = employee.SaturdaySupplement, + SundaySupplement = employee.SundaySupplement, + + // Labels + LabelRates = _localization.Get("employees.detail.salary.rates"), + LabelNormalRate = _localization.Get("employees.detail.salary.normalrate"), + LabelOvertimeRate = _localization.Get("employees.detail.salary.overtimerate"), + LabelVacationRate = _localization.Get("employees.detail.salary.vacationrate"), + LabelProvision = _localization.Get("employees.detail.salary.provision"), + LabelMinimumPerHour = _localization.Get("employees.detail.salary.minimumperhour"), + LabelServiceCommission = _localization.Get("employees.detail.salary.servicecommission"), + LabelProductCommission = _localization.Get("employees.detail.salary.productcommission"), + LabelSupplements = _localization.Get("employees.detail.salary.supplements"), + LabelWeekdaySupplement = _localization.Get("employees.detail.salary.weekdaysupplement"), + LabelSaturdaySupplement = _localization.Get("employees.detail.salary.saturdaysupplement"), + LabelSundaySupplement = _localization.Get("employees.detail.salary.sundaysupplement"), + LabelSalaryHistory = _localization.Get("employees.detail.salary.history"), + LabelPeriod = _localization.Get("employees.detail.salary.period"), + LabelGrossSalary = _localization.Get("employees.detail.salary.grosssalary"), + LabelView = _localization.Get("employees.detail.salary.view"), + LabelEdit = _localization.Get("common.edit"), + LabelRatesDrawerTitle = _localization.Get("employees.detail.salary.ratesdrawertitle"), + LabelBaseRates = _localization.Get("employees.detail.salary.baserates"), + LabelCourseRate = _localization.Get("employees.detail.salary.courserate"), + LabelTimeOffRate = _localization.Get("employees.detail.salary.timeoffrate"), + LabelPaidLeaveRate = _localization.Get("employees.detail.salary.paidleaverate"), + LabelOfficeRate = _localization.Get("employees.detail.salary.officerate"), + LabelChildSickRate = _localization.Get("employees.detail.salary.childsickrate"), + LabelChildHospitalRate = _localization.Get("employees.detail.salary.childhospitalrate"), + LabelMaternityRate = _localization.Get("employees.detail.salary.maternityrate"), + LabelWeekdaySupplementFull = _localization.Get("employees.detail.salary.weekdaysupplementfull"), + LabelSaturdaySupplementFull = _localization.Get("employees.detail.salary.saturdaysupplementfull"), LabelCommission = _localization.Get("employees.detail.salary.commission"), - LabelProductCommission = _localization.Get("employees.detail.salary.productcommission") + LabelProductCommissionFull = _localization.Get("employees.detail.salary.productcommissionfull"), + LabelServiceCommissionFull = _localization.Get("employees.detail.salary.servicecommissionfull"), + + // Rate values (numeric only for drawer inputs) + NormalRateValue = employee.NormalRateValue, + OvertimeRateValue = employee.OvertimeRateValue, + CourseRateValue = employee.CourseRateValue, + TimeOffRateValue = employee.TimeOffRateValue, + PaidLeaveRateValue = employee.PaidLeaveRateValue, + VacationRateValue = employee.VacationRateValue, + OfficeRateValue = employee.OfficeRateValue, + ChildSickRateValue = employee.ChildSickRateValue, + ChildHospitalRateValue = employee.ChildHospitalRateValue, + MaternityRateValue = employee.MaternityRateValue, + WeekdaySupplementValue = employee.WeekdaySupplementValue, + SaturdaySupplementValue = employee.SaturdaySupplementValue, + SundaySupplementValue = employee.SundaySupplementValue, + ProductCommissionValue = employee.ProductCommissionValue, + ServiceCommissionValue = employee.ServiceCommissionValue, + + // Mock salary history + SalaryHistory = new List + { + new() { Period = "Januar 2026", GrossSalary = "34.063,50 kr" }, + new() { Period = "December 2025", GrossSalary = "31.845,00 kr" }, + new() { Period = "November 2025", GrossSalary = "33.290,25 kr" }, + new() { Period = "Oktober 2025", GrossSalary = "32.156,75 kr" }, + new() { Period = "September 2025", GrossSalary = "34.520,00 kr" } + } }; return View(model); @@ -38,16 +106,83 @@ public class EmployeeDetailSalaryViewComponent : ViewComponent public class EmployeeDetailSalaryViewModel { + // Data public required string BankAccount { get; init; } public required string TaxCard { get; init; } public required string HourlyRate { get; init; } public required string MonthlyFixedSalary { get; init; } - public required string LabelPaymentInfo { get; init; } - public required string LabelBankAccount { get; init; } - public required string LabelTaxCard { get; init; } - public required string LabelSalarySettings { get; init; } - public required string LabelHourlyRate { get; init; } - public required string LabelMonthlyFixed { get; init; } - public required string LabelCommission { get; init; } + + // Rates + public required string NormalRate { get; init; } + public required string OvertimeRate { get; init; } + public required string VacationRate { get; init; } + + // Commission + public required string MinimumPerHour { get; init; } + public required string ServiceCommission { get; init; } + public required string ProductCommission { get; init; } + + // Supplements + public required string WeekdaySupplement { get; init; } + public required string SaturdaySupplement { get; init; } + public required string SundaySupplement { get; init; } + + // Labels + public required string LabelRates { get; init; } + public required string LabelNormalRate { get; init; } + public required string LabelOvertimeRate { get; init; } + public required string LabelVacationRate { get; init; } + public required string LabelProvision { get; init; } + public required string LabelMinimumPerHour { get; init; } + public required string LabelServiceCommission { get; init; } public required string LabelProductCommission { get; init; } + public required string LabelSupplements { get; init; } + public required string LabelWeekdaySupplement { get; init; } + public required string LabelSaturdaySupplement { get; init; } + public required string LabelSundaySupplement { get; init; } + public required string LabelSalaryHistory { get; init; } + public required string LabelPeriod { get; init; } + public required string LabelGrossSalary { get; init; } + public required string LabelView { get; init; } + public required string LabelEdit { get; init; } + public required string LabelRatesDrawerTitle { get; init; } + public required string LabelBaseRates { get; init; } + public required string LabelCourseRate { get; init; } + public required string LabelTimeOffRate { get; init; } + public required string LabelPaidLeaveRate { get; init; } + public required string LabelOfficeRate { get; init; } + public required string LabelChildSickRate { get; init; } + public required string LabelChildHospitalRate { get; init; } + public required string LabelMaternityRate { get; init; } + public required string LabelWeekdaySupplementFull { get; init; } + public required string LabelSaturdaySupplementFull { get; init; } + public required string LabelCommission { get; init; } + public required string LabelProductCommissionFull { get; init; } + public required string LabelServiceCommissionFull { get; init; } + + // Rate values (for drawer inputs) + public required string NormalRateValue { get; init; } + public required string OvertimeRateValue { get; init; } + public required string CourseRateValue { get; init; } + public required string TimeOffRateValue { get; init; } + public required string PaidLeaveRateValue { get; init; } + public required string VacationRateValue { get; init; } + public required string OfficeRateValue { get; init; } + public required string ChildSickRateValue { get; init; } + public required string ChildHospitalRateValue { get; init; } + public required string MaternityRateValue { get; init; } + public required string WeekdaySupplementValue { get; init; } + public required string SaturdaySupplementValue { get; init; } + public required string SundaySupplementValue { get; init; } + public required string ProductCommissionValue { get; init; } + public required string ServiceCommissionValue { get; init; } + + // Salary History (mock data) + public List SalaryHistory { get; init; } = new(); +} + +public class SalaryHistoryItem +{ + public required string Period { get; init; } + public required string GrossSalary { get; init; } } diff --git a/PlanTempus.Application/Features/Localization/Translations/da.json b/PlanTempus.Application/Features/Localization/Translations/da.json index 53bfaf5..ac9be69 100644 --- a/PlanTempus.Application/Features/Localization/Translations/da.json +++ b/PlanTempus.Application/Features/Localization/Translations/da.json @@ -309,21 +309,60 @@ "assigned": "Tildelte services" }, "salary": { - "paymentinfo": "Betalingsoplysninger", - "bankaccount": "Bankkonto", - "taxcard": "Skattekort", - "settings": "Lønindstillinger", - "hourlyrate": "Timesats", - "monthlyfixed": "Fast månedsløn", - "commission": "Provision (services)", - "productcommission": "Provision (produkter)" + "rates": "Satser", + "normalrate": "Normal (timeløn)", + "overtimerate": "Overarbejde (100%)", + "vacationrate": "Ferie m. løn", + "provision": "Provision", + "minimumperhour": "Minimum pr. time", + "servicecommission": "På services", + "productcommission": "På produktsalg", + "supplements": "Tillæg", + "weekdaysupplement": "8-21 Hverdage", + "saturdaysupplement": "8-21 Lørdage", + "sundaysupplement": "Søndag", + "history": "Lønspecifikationer", + "period": "Periode", + "grosssalary": "Bruttoløn", + "view": "Vis", + "ratesdrawertitle": "Lønsatser", + "baserates": "Grundsatser", + "courserate": "Kursus/skole", + "timeoffrate": "Afspadsering", + "paidleaverate": "Fri m. løn", + "officerate": "Kontor", + "childsickrate": "Barns 1. sygedag", + "childhospitalrate": "Barns hospitalsindlæggelse", + "maternityrate": "Barsel", + "weekdaysupplementfull": "8-21 Hverdage (udenfor arbejdstid)", + "saturdaysupplementfull": "8-21 Lørdage (udenfor arbejdstid)", + "commission": "Provisionsberegning", + "productcommissionfull": "Provision på produktsalg", + "servicecommissionfull": "Provision på servicesalg" }, "hr": { - "documents": "Dokumenter", - "contract": "Ansættelseskontrakt", - "vacation": "Ferie", - "sickleave": "Sygefravær", - "notes": "Noter" + "contractdocuments": "Kontrakt & Dokumenter", + "contracttype": "Kontrakttype", + "terminationnotice": "Opsigelsesvarsel", + "contractexpiry": "Kontraktudløb", + "uploaddocument": "Upload dokument", + "certifications": "Certificeringer", + "addcertification": "Tilføj certificering", + "courses": "Kurser", + "completedcourses": "Gennemførte kurser", + "plannedcourses": "Planlagte kurser", + "addcourse": "Tilføj kursus", + "vacationbalance": "Ferie-saldo", + "vacationearned": "Optjente feriedage", + "vacationused": "Brugte feriedage", + "vacationremaining": "Resterende", + "absencesickness": "Fravær & Sygdom", + "sickdays2025": "Sygefravær 2025", + "sickdays2024": "Sygefravær 2024", + "childsickdays2025": "Børns sygdom 2025", + "maternityleave": "Barsel", + "plannedabsence": "Planlagt fravær", + "addabsence": "Tilføj fravær" }, "stats": { "performance": "Performance", diff --git a/PlanTempus.Application/Features/Localization/Translations/en.json b/PlanTempus.Application/Features/Localization/Translations/en.json index fa41c98..7811257 100644 --- a/PlanTempus.Application/Features/Localization/Translations/en.json +++ b/PlanTempus.Application/Features/Localization/Translations/en.json @@ -295,6 +295,82 @@ "revenue": "revenue this year", "rating": "rating", "employedsince": "employed since", + "hours": { + "weekly": "Weekly working hours", + "monday": "Monday", + "tuesday": "Tuesday", + "wednesday": "Wednesday", + "thursday": "Thursday", + "friday": "Friday", + "saturday": "Saturday", + "sunday": "Sunday" + }, + "services": { + "assigned": "Assigned services" + }, + "salary": { + "rates": "Rates", + "normalrate": "Normal (hourly)", + "overtimerate": "Overtime (100%)", + "vacationrate": "Vacation pay", + "provision": "Commission", + "minimumperhour": "Minimum per hour", + "servicecommission": "On services", + "productcommission": "On product sales", + "supplements": "Supplements", + "weekdaysupplement": "8-21 Weekdays", + "saturdaysupplement": "8-21 Saturdays", + "sundaysupplement": "Sunday", + "history": "Salary specifications", + "period": "Period", + "grosssalary": "Gross salary", + "view": "View", + "ratesdrawertitle": "Salary rates", + "baserates": "Base rates", + "courserate": "Course/training", + "timeoffrate": "Time off in lieu", + "paidleaverate": "Paid leave", + "officerate": "Office work", + "childsickrate": "Child's first sick day", + "childhospitalrate": "Child hospitalization", + "maternityrate": "Maternity leave", + "weekdaysupplementfull": "8-21 Weekdays (outside working hours)", + "saturdaysupplementfull": "8-21 Saturdays (outside working hours)", + "commission": "Commission calculation", + "productcommissionfull": "Commission on product sales", + "servicecommissionfull": "Commission on service sales" + }, + "hr": { + "contractdocuments": "Contract & Documents", + "contracttype": "Contract type", + "terminationnotice": "Termination notice", + "contractexpiry": "Contract expiry", + "uploaddocument": "Upload document", + "certifications": "Certifications", + "addcertification": "Add certification", + "courses": "Courses", + "completedcourses": "Completed courses", + "plannedcourses": "Planned courses", + "addcourse": "Add course", + "vacationbalance": "Vacation balance", + "vacationearned": "Earned vacation days", + "vacationused": "Used vacation days", + "vacationremaining": "Remaining", + "absencesickness": "Absence & Sickness", + "sickdays2025": "Sick days 2025", + "sickdays2024": "Sick days 2024", + "childsickdays2025": "Child sick days 2025", + "maternityleave": "Maternity leave", + "plannedabsence": "Planned absence", + "addabsence": "Add absence" + }, + "stats": { + "performance": "Performance", + "bookingsyear": "Bookings this year", + "revenueyear": "Revenue this year", + "avgrating": "Avg. rating", + "occupancy": "Occupancy rate" + }, "settings": { "label": "Settings", "showinbooking": { diff --git a/PlanTempus.Application/wwwroot/css/COMPONENT-CATALOG.md b/PlanTempus.Application/wwwroot/css/COMPONENT-CATALOG.md index d1ae087..8584c11 100644 --- a/PlanTempus.Application/wwwroot/css/COMPONENT-CATALOG.md +++ b/PlanTempus.Application/wwwroot/css/COMPONENT-CATALOG.md @@ -190,6 +190,7 @@ swp-[feature]-row { |---------|-----------|-----|---------| | Cash | `swp-cash-table` | `swp-cash-table-row` | cash.css | | Employees | `swp-employee-table` | `swp-employee-row` | employees.css | +| Salary | `swp-salary-table` | `swp-salary-table-row` | employees.css | | Bookings | `swp-booking-list` | `swp-booking-item` | bookings.css | | Notifications | `swp-notification-list` | `swp-notification-item` | notifications.css | | Attentions | `swp-attention-list` | `swp-attention-item` | attentions.css | @@ -233,6 +234,49 @@ swp-[feature]-cell { --- +## Edit Forms (employees.css) + +Key-value display med valgfri redigering. Bruger Grid + Subgrid for alignment. + +### Basis struktur + +```html + + + Label tekst + Værdi + + + Med redigering + Redigerbar værdi + + +``` + +### Med select dropdown + +```html + + + Vælg type + + + + + +``` + +### Mono-font for tal + +```html +131,49 kr +``` + +--- + ## User Info Pattern (employees.css) ```html @@ -249,6 +293,109 @@ swp-[feature]-cell { --- +## Document List (employees.css) + +Genbruges til dokumenter, certificeringer, kurser og lignende lister. + +```html + + + + + Ansættelseskontrakt.pdf + Uploadet 1. aug 2019 + + + Vis + + + +``` + +**Med badge i stedet for knap:** +```html + + + + Balayage Specialist + Udløber: 15. juni 2026 + + + Gyldig + + +``` + +--- + +## Subsection (employees.css) + +Til gruppering af lister (f.eks. "Gennemførte kurser" / "Planlagte kurser"). + +```html + + Gennemførte kurser + + + + +``` + +--- + +## Simple List (employees.css) + +Simpel liste med tekst + badge (f.eks. planlagt fravær). + +```html + + + 23. dec – 2. jan 2026 + Ferie + + +``` + +--- + +## Salary Table (employees.css) + +Bruger Grid + Subgrid mønsteret. + +```html + + + Periode + Bruttoløn + + + + + Januar 2026 + 34.063,50 kr + + + + +``` + +Rækker har hover-effekt og chevron bliver teal ved hover. + +--- + +## Add Button (components.css) + +Dashed border knap til tilføjelse af elementer. + +```html ++ Upload dokument ++ Tilføj certificering +``` + +**Styling:** Dashed border, centreret, hover → teal border og tekst. + +--- + ## Design Tokens (design-tokens.css) ### Farver @@ -317,11 +464,11 @@ swp-[feature]-cell { |-----|---------| | `design-tokens.css` | Farver, spacing, fonts, shadows | | `design-system.css` | Base resets, typography | -| `page.css` | Page structure, cards | +| `page.css` | Page structure | +| `components.css` | Buttons, badges, cards, section-label, add-button, avatars, icon-btn | | `stats.css` | Stat cards, stat rows | | `tabs.css` | Tab bar, tab content | -| `cash.css` | Buttons, status badges, tables | -| `employees.css` | User info, role badges, employee table | +| `employees.css` | Employee table, user info, edit forms, document lists, salary table | | `bookings.css` | Booking list items | | `notifications.css` | Notification items | | `attentions.css` | Attention items | diff --git a/PlanTempus.Application/wwwroot/css/components.css b/PlanTempus.Application/wwwroot/css/components.css index bd0ac48..9dee006 100644 --- a/PlanTempus.Application/wwwroot/css/components.css +++ b/PlanTempus.Application/wwwroot/css/components.css @@ -14,6 +14,7 @@ * - swp-section-label (card section headers) * - swp-section-header (section header with action link) * - swp-section-action (action link in section header) + * - swp-add-button (dashed border add button) */ /* =========================================== @@ -194,6 +195,36 @@ swp-status-badge.employee { color: var(--color-text-secondary); } +/* Additional status variants */ +swp-status-badge.valid { + background: color-mix(in srgb, var(--color-green) 15%, transparent); + color: var(--color-green); +} + +swp-status-badge.expiring, +swp-status-badge.warning { + background: color-mix(in srgb, var(--color-amber) 15%, transparent); + color: #b45309; +} + +swp-status-badge.enrolled, +swp-status-badge.ferie { + background: color-mix(in srgb, var(--color-teal) 15%, transparent); + color: var(--color-teal); +} + +swp-status-badge.fri, +swp-status-badge.info { + background: color-mix(in srgb, var(--color-blue) 15%, transparent); + color: var(--color-blue); +} + +swp-status-badge.sygdom, +swp-status-badge.danger { + background: color-mix(in srgb, var(--color-red) 15%, transparent); + color: var(--color-red); +} + /* =========================================== PLAN CARDS (swp-plan-card) =========================================== */ @@ -432,3 +463,27 @@ swp-card-footer { margin: var(--spacing-6) calc(-1 * var(--card-padding)) calc(-1 * var(--card-padding)); border-radius: 0 0 var(--border-radius-lg) var(--border-radius-lg); } + +/* =========================================== + ADD BUTTON (dashed border style) + =========================================== */ +swp-add-button { + display: flex; + align-items: center; + justify-content: center; + gap: var(--spacing-2); + padding: var(--spacing-5); + margin-top: var(--spacing-5); + border: 2px dashed var(--color-border); + border-radius: var(--radius-md); + color: var(--color-text-secondary); + font-size: var(--font-size-md); + cursor: pointer; + transition: all var(--transition-fast); +} + +swp-add-button:hover { + border-color: var(--color-teal); + color: var(--color-teal); + background: color-mix(in srgb, var(--color-teal) 5%, transparent); +} diff --git a/PlanTempus.Application/wwwroot/css/employees.css b/PlanTempus.Application/wwwroot/css/employees.css index 976604a..10176fa 100644 --- a/PlanTempus.Application/wwwroot/css/employees.css +++ b/PlanTempus.Application/wwwroot/css/employees.css @@ -3,14 +3,17 @@ * * Employees-specific styling only. * Reuses: swp-stat-card (stats.css), swp-stats-row (stats.css), swp-tab-bar (tabs.css), - * swp-btn, swp-status-badge, swp-icon-btn, swp-card, swp-section-label (components.css), + * swp-btn, swp-status-badge, swp-icon-btn, swp-card, swp-section-label, + * swp-add-button (components.css), * swp-row-toggle (cash.css), * swp-sticky-header, swp-header-content (page.css), * swp-toggle-slider, swp-checkbox-list (controls.css) * * Creates: swp-employee-table, swp-employee-row, swp-user-info, * swp-employee-avatar-large, swp-employee-detail-header, - * swp-fact-inline, swp-edit-row, swp-detail-grid + * swp-fact-inline, swp-edit-section/row/label/value/select, swp-detail-grid, + * swp-salary-table, swp-document-list/item/info/name/meta/actions, + * swp-subsection/title, swp-simple-list/item/text */ /* =========================================== @@ -434,7 +437,7 @@ swp-edit-label { swp-edit-value { font-size: var(--font-size-base); color: var(--color-text); - padding: var(--spacing-2) var(--spacing-4); + padding: var(--spacing-4) var(--spacing-5); border-radius: var(--radius-sm); background: var(--color-background-alt); border: 1px solid transparent; @@ -585,48 +588,62 @@ swp-service-price { /* =========================================== DOCUMENT LIST (HR tab) + Also used for certs, courses, and similar lists =========================================== */ swp-document-list { display: flex; flex-direction: column; + gap: var(--spacing-2); } swp-document-item { display: flex; align-items: center; gap: var(--spacing-3); - padding: var(--spacing-4) 0; - border-bottom: 1px solid var(--color-border); - cursor: pointer; - transition: background var(--transition-fast); -} - -swp-document-item:last-child { - border-bottom: none; -} - -swp-document-item:hover { + padding: 12px 16px; background: var(--color-background-alt); - margin: 0 calc(-1 * var(--spacing-3)); - padding-left: var(--spacing-3); - padding-right: var(--spacing-3); - border-radius: var(--radius-sm); + border-radius: var(--radius-md); + border: 1px solid var(--color-border); } -swp-document-item i { - font-size: 24px; +/* Icon (optional) */ +swp-document-item i, +swp-document-icon { + font-size: 20px; + color: var(--color-text-secondary); + flex-shrink: 0; +} + +swp-document-item i.ph-file-pdf { color: var(--color-red); } -swp-document-name { +/* Info wrapper (name + meta) */ +swp-document-info { flex: 1; - font-size: var(--font-size-base); - font-weight: var(--font-weight-medium); + min-width: 0; } -swp-document-date { +swp-document-name { + display: block; + font-size: var(--font-size-base); + font-weight: var(--font-weight-medium); + color: var(--color-text); +} + +swp-document-meta { + display: block; font-size: var(--font-size-sm); color: var(--color-text-secondary); + margin-top: 2px; +} + +/* Actions (buttons/badges at end) */ +swp-document-actions { + display: flex; + align-items: center; + gap: var(--spacing-2); + flex-shrink: 0; } /* Notes area */ @@ -646,6 +663,262 @@ swp-notes-area:focus { color: var(--color-text); } +/* =========================================== + EDIT SELECT (dropdown in edit rows) + =========================================== */ +swp-edit-select { + display: block; +} + +swp-edit-select select { + width: 100%; + padding: var(--spacing-2) var(--spacing-4); + font-size: var(--font-size-base); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + background: var(--color-surface); + cursor: pointer; +} + +swp-edit-select select:focus { + outline: none; + border-color: var(--color-teal); +} + +/* Mono variant for edit-value */ +swp-edit-value.mono { + font-family: var(--font-mono); + width: 150px; + text-align: right; + justify-self: end; +} + +/* Input in edit-row */ +swp-edit-row input { + font-size: var(--font-size-base); + padding: var(--spacing-4) var(--spacing-5); + border-radius: var(--radius-sm); + background: var(--color-background-alt); + border: 1px solid var(--color-border); + color: var(--color-text); + transition: all var(--transition-fast); + cursor: text; +} + +swp-edit-row input:hover { + background: var(--color-background); +} + +swp-edit-row input:focus { + outline: none; + background: var(--color-surface); + border-color: var(--color-teal); +} + +/* Number input variant */ +swp-edit-row input[data-type="number"] { + font-family: var(--font-mono); + text-align: right; + width: 150px; + justify-self: end; +} + +/* =========================================== + SALARY TABLE (Grid + Subgrid) + =========================================== */ +swp-salary-table { + display: grid; + grid-template-columns: 1fr 120px 60px; +} + +swp-salary-table-header, +swp-salary-table-body { + display: grid; + grid-column: 1 / -1; + grid-template-columns: subgrid; +} + +swp-salary-table-header { + border-bottom: 1px solid var(--color-border); +} + +swp-salary-table-row { + display: grid; + grid-column: 1 / -1; + grid-template-columns: subgrid; + align-items: center; + border-bottom: 1px solid var(--color-border); +} + +swp-salary-table-row:last-child { + border-bottom: none; +} + +swp-salary-table-cell { + padding: var(--spacing-4) var(--spacing-2); + font-size: var(--font-size-base); + color: var(--color-text); +} + +swp-salary-table-cell:first-child { + padding-left: 0; +} + +swp-salary-table-cell:last-child { + padding-right: 0; + text-align: right; +} + +swp-salary-table-header swp-salary-table-cell { + font-size: var(--font-size-xs); + font-weight: var(--font-weight-semibold); + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--color-text-secondary); +} + +swp-salary-table-cell.mono { + font-family: var(--font-mono); +} + +/* Chevron for row actions */ +swp-salary-table-cell i { + color: var(--color-text-muted); + font-size: 16px; +} + +swp-salary-table-row { + cursor: pointer; + transition: background var(--transition-fast); +} + +swp-salary-table-row:hover { + background: var(--color-background-hover); +} + +swp-salary-table-row:hover swp-salary-table-cell i { + color: var(--color-teal); +} + +/* =========================================== + SUBSECTION TITLE (for grouped lists like courses) + =========================================== */ +swp-subsection { + display: block; + margin-bottom: var(--spacing-6); +} + +swp-subsection:last-of-type { + margin-bottom: 0; +} + +swp-subsection-title { + display: block; + font-size: var(--font-size-sm); + font-weight: var(--font-weight-semibold); + color: var(--color-text-secondary); + margin-bottom: var(--spacing-3); +} + +/* =========================================== + SIMPLE LIST (dates + badge, like vacation) + =========================================== */ +swp-simple-list { + display: flex; + flex-direction: column; + gap: var(--spacing-2); +} + +swp-simple-item { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--spacing-3); + padding: 12px 16px; + background: var(--color-background-alt); + border-radius: var(--radius-md); + border: 1px solid var(--color-border); +} + +swp-simple-item-text { + flex: 1; + font-size: var(--font-size-base); + font-family: var(--font-mono); + color: var(--color-text); +} + +/* swp-add-button styles in components.css */ + +/* =========================================== + RATES DRAWER CONTENT (overrides for swp-data-*) + =========================================== */ +.rates-content swp-data-table { + display: grid; + grid-template-columns: 28px 1fr 100px; +} + +.rates-content swp-data-row { + display: grid; + grid-column: 1 / -1; + grid-template-columns: subgrid; + align-items: center; + gap: 12px; + padding: 12px 0; + border-bottom: 1px solid var(--color-border); +} + +.rates-content swp-data-row:last-child { + border-bottom: none; +} + +.rates-content swp-data-row input[type="checkbox"] { + width: 18px; + height: 18px; + accent-color: var(--color-teal); +} + +.rates-content swp-data-label { + font-size: var(--font-size-base); +} + +.rates-content swp-data-label.disabled { + opacity: 0.4; +} + +.rates-content swp-data-input { + display: flex; + align-items: center; + justify-self: end; + gap: 4px; + font-size: var(--font-size-sm); + color: var(--color-text-secondary); +} + +.rates-content swp-data-input input { + width: 100px; + padding: 6px 8px; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + font-size: var(--font-size-sm); + font-family: var(--font-mono); + text-align: right; +} + +.rates-content swp-data-input.disabled input { + opacity: 0.4; + background: var(--color-background); +} + +.rates-content swp-section-label { + margin-bottom: 12px; +} + +.rates-content swp-data-section { + margin-top: 24px; + padding-top: 16px; + border-top: 1px solid var(--color-border); +} + /* =========================================== RESPONSIVE =========================================== */ diff --git a/PlanTempus.Application/wwwroot/js/app.js b/PlanTempus.Application/wwwroot/js/app.js index e6abe7f..239ccf5 100644 --- a/PlanTempus.Application/wwwroot/js/app.js +++ b/PlanTempus.Application/wwwroot/js/app.js @@ -890,6 +890,7 @@ // wwwroot/ts/modules/employees.ts var EmployeesController = class { constructor() { + this.ratesSync = null; this.listView = null; this.detailView = null; this.listView = document.getElementById("employees-list-view"); @@ -901,6 +902,7 @@ this.setupBackNavigation(); this.setupHistoryNavigation(); this.restoreStateFromUrl(); + this.ratesSync = new RatesSyncController(); } /** * Setup popstate listener for browser back/forward @@ -1041,6 +1043,93 @@ } } }; + 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}`; + } + }; // wwwroot/ts/app.ts var App = class { diff --git a/PlanTempus.Application/wwwroot/js/app.js.map b/PlanTempus.Application/wwwroot/js/app.js.map index 344ec30..f5adfaa 100644 --- a/PlanTempus.Application/wwwroot/js/app.js.map +++ b/PlanTempus.Application/wwwroot/js/app.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["../ts/modules/sidebar.ts", "../ts/modules/drawers.ts", "../ts/modules/theme.ts", "../ts/modules/search.ts", "../ts/modules/lockscreen.ts", "../ts/modules/cash.ts", "../ts/modules/employees.ts", "../ts/app.ts"], - "sourcesContent": ["/**\n * Sidebar Controller\n *\n * Handles sidebar collapse/expand and tooltip functionality\n */\n\nexport class SidebarController {\n private menuToggle: HTMLElement | null = null;\n private appLayout: HTMLElement | null = null;\n private menuTooltip: HTMLElement | null = null;\n\n constructor() {\n this.menuToggle = document.getElementById('menuToggle');\n this.appLayout = document.querySelector('swp-app-layout');\n this.menuTooltip = document.getElementById('menuTooltip');\n\n this.setupListeners();\n this.setupTooltips();\n this.restoreState();\n }\n\n /**\n * Check if sidebar is collapsed\n */\n get isCollapsed(): boolean {\n return this.appLayout?.classList.contains('menu-collapsed') ?? false;\n }\n\n /**\n * Toggle sidebar collapsed state\n */\n toggle(): void {\n if (!this.appLayout) return;\n\n this.appLayout.classList.toggle('menu-collapsed');\n localStorage.setItem('sidebar-collapsed', String(this.isCollapsed));\n }\n\n /**\n * Collapse the sidebar\n */\n collapse(): void {\n this.appLayout?.classList.add('menu-collapsed');\n localStorage.setItem('sidebar-collapsed', 'true');\n }\n\n /**\n * Expand the sidebar\n */\n expand(): void {\n this.appLayout?.classList.remove('menu-collapsed');\n localStorage.setItem('sidebar-collapsed', 'false');\n }\n\n private setupListeners(): void {\n this.menuToggle?.addEventListener('click', () => this.toggle());\n }\n\n private setupTooltips(): void {\n if (!this.menuTooltip) return;\n\n const menuItems = document.querySelectorAll('swp-side-menu-item[data-tooltip]');\n\n menuItems.forEach(item => {\n item.addEventListener('mouseenter', () => this.showTooltip(item));\n item.addEventListener('mouseleave', () => this.hideTooltip());\n });\n }\n\n private showTooltip(item: HTMLElement): void {\n if (!this.isCollapsed || !this.menuTooltip) return;\n\n const rect = item.getBoundingClientRect();\n const tooltipText = item.dataset.tooltip;\n\n if (!tooltipText) return;\n\n this.menuTooltip.textContent = tooltipText;\n this.menuTooltip.style.left = `${rect.right + 8}px`;\n this.menuTooltip.style.top = `${rect.top + rect.height / 2}px`;\n this.menuTooltip.style.transform = 'translateY(-50%)';\n this.menuTooltip.showPopover();\n }\n\n private hideTooltip(): void {\n this.menuTooltip?.hidePopover();\n }\n\n private restoreState(): void {\n if (!this.appLayout) return;\n\n if (localStorage.getItem('sidebar-collapsed') === 'true') {\n this.appLayout.classList.add('menu-collapsed');\n }\n }\n}\n", "/**\n * Drawer Controller\n *\n * Handles all drawer functionality including profile, notifications, and todo drawers\n */\n\nexport type DrawerName = 'profile' | 'notification' | 'todo' | 'newTodo';\n\nexport class DrawerController {\n private profileDrawer: HTMLElement | null = null;\n private notificationDrawer: HTMLElement | null = null;\n private todoDrawer: HTMLElement | null = null;\n private newTodoDrawer: HTMLElement | null = null;\n private overlay: HTMLElement | null = null;\n private activeDrawer: DrawerName | null = null;\n private activeGenericDrawer: HTMLElement | null = null;\n\n constructor() {\n this.profileDrawer = document.getElementById('profileDrawer');\n this.notificationDrawer = document.getElementById('notificationDrawer');\n this.todoDrawer = document.getElementById('todoDrawer');\n this.newTodoDrawer = document.getElementById('newTodoDrawer');\n this.overlay = document.getElementById('drawerOverlay');\n\n this.setupListeners();\n this.setupGenericDrawers();\n }\n\n /**\n * Get currently active drawer name\n */\n get active(): DrawerName | null {\n return this.activeDrawer;\n }\n\n /**\n * Open a drawer by name\n */\n open(name: DrawerName): void {\n this.closeAll();\n\n const drawer = this.getDrawer(name);\n if (drawer && this.overlay) {\n drawer.classList.add('active');\n this.overlay.classList.add('active');\n document.body.style.overflow = 'hidden';\n this.activeDrawer = name;\n }\n }\n\n /**\n * Close a specific drawer\n */\n close(name: DrawerName): void {\n const drawer = this.getDrawer(name);\n drawer?.classList.remove('active');\n\n // Only hide overlay if no drawers are active\n if (this.overlay && !document.querySelector('.active[class*=\"drawer\"]')) {\n this.overlay.classList.remove('active');\n document.body.style.overflow = '';\n }\n\n if (this.activeDrawer === name) {\n this.activeDrawer = null;\n }\n }\n\n /**\n * Close all drawers\n */\n closeAll(): void {\n [this.profileDrawer, this.notificationDrawer, this.todoDrawer, this.newTodoDrawer]\n .forEach(drawer => drawer?.classList.remove('active'));\n\n // Close any generic drawers\n this.closeGenericDrawer();\n\n this.overlay?.classList.remove('active');\n document.body.style.overflow = '';\n this.activeDrawer = null;\n }\n\n /**\n * Open a generic drawer by ID\n */\n openGenericDrawer(drawerId: string): void {\n this.closeAll();\n\n const drawer = document.getElementById(drawerId);\n if (drawer && this.overlay) {\n drawer.classList.add('open');\n this.overlay.classList.add('active');\n document.body.style.overflow = 'hidden';\n this.activeGenericDrawer = drawer;\n }\n }\n\n /**\n * Close the currently open generic drawer\n */\n closeGenericDrawer(): void {\n this.activeGenericDrawer?.classList.remove('open');\n this.activeGenericDrawer = null;\n }\n\n /**\n * Open profile drawer\n */\n openProfile(): void {\n this.open('profile');\n }\n\n /**\n * Open notification drawer\n */\n openNotification(): void {\n this.open('notification');\n }\n\n /**\n * Open todo drawer (slides on top of profile)\n */\n openTodo(): void {\n this.todoDrawer?.classList.add('active');\n }\n\n /**\n * Close todo drawer\n */\n closeTodo(): void {\n this.todoDrawer?.classList.remove('active');\n this.closeNewTodo();\n }\n\n /**\n * Open new todo drawer\n */\n openNewTodo(): void {\n this.newTodoDrawer?.classList.add('active');\n }\n\n /**\n * Close new todo drawer\n */\n closeNewTodo(): void {\n this.newTodoDrawer?.classList.remove('active');\n }\n\n /**\n * Mark all notifications as read\n */\n markAllNotificationsRead(): void {\n if (!this.notificationDrawer) return;\n\n const unreadItems = this.notificationDrawer.querySelectorAll(\n 'swp-notification-item[data-unread=\"true\"]'\n );\n unreadItems.forEach(item => item.removeAttribute('data-unread'));\n\n const badge = document.querySelector('swp-notification-badge');\n if (badge) {\n badge.style.display = 'none';\n }\n }\n\n private getDrawer(name: DrawerName): HTMLElement | null {\n switch (name) {\n case 'profile': return this.profileDrawer;\n case 'notification': return this.notificationDrawer;\n case 'todo': return this.todoDrawer;\n case 'newTodo': return this.newTodoDrawer;\n }\n }\n\n private setupListeners(): void {\n // Profile drawer triggers\n document.getElementById('profileTrigger')\n ?.addEventListener('click', () => this.openProfile());\n document.getElementById('drawerClose')\n ?.addEventListener('click', () => this.close('profile'));\n\n // Notification drawer triggers\n document.getElementById('notificationsBtn')\n ?.addEventListener('click', () => this.openNotification());\n document.getElementById('notificationDrawerClose')\n ?.addEventListener('click', () => this.close('notification'));\n document.getElementById('markAllRead')\n ?.addEventListener('click', () => this.markAllNotificationsRead());\n\n // Todo drawer triggers\n document.getElementById('openTodoDrawer')\n ?.addEventListener('click', () => this.openTodo());\n document.getElementById('todoDrawerBack')\n ?.addEventListener('click', () => this.closeTodo());\n\n // New todo drawer triggers\n document.getElementById('addTodoBtn')\n ?.addEventListener('click', () => this.openNewTodo());\n document.getElementById('newTodoDrawerBack')\n ?.addEventListener('click', () => this.closeNewTodo());\n document.getElementById('cancelNewTodo')\n ?.addEventListener('click', () => this.closeNewTodo());\n document.getElementById('saveNewTodo')\n ?.addEventListener('click', () => this.closeNewTodo());\n\n // Overlay click closes all\n this.overlay?.addEventListener('click', () => this.closeAll());\n\n // Escape key closes all\n document.addEventListener('keydown', (e: KeyboardEvent) => {\n if (e.key === 'Escape') this.closeAll();\n });\n\n // Todo interactions\n this.todoDrawer?.addEventListener('click', (e) => this.handleTodoClick(e));\n\n // Visibility options\n document.addEventListener('click', (e) => this.handleVisibilityClick(e));\n }\n\n private handleTodoClick(e: Event): void {\n const target = e.target as HTMLElement;\n const todoItem = target.closest('swp-todo-item');\n const checkbox = target.closest('swp-todo-checkbox');\n\n if (checkbox && todoItem) {\n const isCompleted = todoItem.dataset.completed === 'true';\n if (isCompleted) {\n todoItem.removeAttribute('data-completed');\n } else {\n todoItem.dataset.completed = 'true';\n }\n }\n\n // Toggle section collapse\n const sectionHeader = target.closest('swp-todo-section-header');\n if (sectionHeader) {\n const section = sectionHeader.closest('swp-todo-section');\n section?.classList.toggle('collapsed');\n }\n }\n\n private handleVisibilityClick(e: Event): void {\n const target = e.target as HTMLElement;\n const option = target.closest('swp-visibility-option');\n\n if (option) {\n document.querySelectorAll('swp-visibility-option')\n .forEach(o => o.classList.remove('active'));\n option.classList.add('active');\n }\n }\n\n /**\n * Setup generic drawer triggers and close buttons\n * Uses data-drawer-trigger=\"drawer-id\" and data-drawer-close attributes\n */\n private setupGenericDrawers(): void {\n // Handle drawer triggers\n document.addEventListener('click', (e: Event) => {\n const target = e.target as HTMLElement;\n const trigger = target.closest('[data-drawer-trigger]');\n\n if (trigger) {\n const drawerId = trigger.dataset.drawerTrigger;\n if (drawerId) {\n this.openGenericDrawer(drawerId);\n }\n }\n });\n\n // Handle drawer close buttons\n document.addEventListener('click', (e: Event) => {\n const target = e.target as HTMLElement;\n const closeBtn = target.closest('[data-drawer-close]');\n\n if (closeBtn) {\n this.closeGenericDrawer();\n this.overlay?.classList.remove('active');\n document.body.style.overflow = '';\n }\n });\n }\n}\n", "/**\n * Theme Controller\n *\n * Handles dark/light mode switching and system preference detection\n */\n\nexport type Theme = 'light' | 'dark' | 'system';\n\nexport class ThemeController {\n private static readonly STORAGE_KEY = 'theme-preference';\n private static readonly DARK_CLASS = 'dark-mode';\n private static readonly LIGHT_CLASS = 'light-mode';\n\n private root: HTMLElement;\n private themeOptions: NodeListOf;\n\n constructor() {\n this.root = document.documentElement;\n this.themeOptions = document.querySelectorAll('swp-theme-option');\n\n this.applyTheme(this.current);\n this.updateUI();\n this.setupListeners();\n }\n\n /**\n * Get the current theme setting\n */\n get current(): Theme {\n const stored = localStorage.getItem(ThemeController.STORAGE_KEY) as Theme | null;\n if (stored === 'dark' || stored === 'light' || stored === 'system') {\n return stored;\n }\n return 'system';\n }\n\n /**\n * Check if dark mode is currently active\n */\n get isDark(): boolean {\n return this.root.classList.contains(ThemeController.DARK_CLASS) ||\n (this.systemPrefersDark && !this.root.classList.contains(ThemeController.LIGHT_CLASS));\n }\n\n /**\n * Check if system prefers dark mode\n */\n get systemPrefersDark(): boolean {\n return window.matchMedia('(prefers-color-scheme: dark)').matches;\n }\n\n /**\n * Set theme and persist preference\n */\n set(theme: Theme): void {\n localStorage.setItem(ThemeController.STORAGE_KEY, theme);\n this.applyTheme(theme);\n this.updateUI();\n }\n\n /**\n * Toggle between light and dark themes\n */\n toggle(): void {\n this.set(this.isDark ? 'light' : 'dark');\n }\n\n private applyTheme(theme: Theme): void {\n this.root.classList.remove(ThemeController.DARK_CLASS, ThemeController.LIGHT_CLASS);\n\n if (theme === 'dark') {\n this.root.classList.add(ThemeController.DARK_CLASS);\n } else if (theme === 'light') {\n this.root.classList.add(ThemeController.LIGHT_CLASS);\n }\n // 'system' leaves both classes off, letting CSS media query handle it\n }\n\n private updateUI(): void {\n if (!this.themeOptions) return;\n\n const darkActive = this.isDark;\n\n this.themeOptions.forEach(option => {\n const theme = option.dataset.theme as Theme;\n const isActive = (theme === 'dark' && darkActive) || (theme === 'light' && !darkActive);\n option.classList.toggle('active', isActive);\n });\n }\n\n private setupListeners(): void {\n // Theme option clicks\n this.themeOptions.forEach(option => {\n option.addEventListener('click', (e) => this.handleOptionClick(e));\n });\n\n // System theme changes\n window.matchMedia('(prefers-color-scheme: dark)')\n .addEventListener('change', () => this.handleSystemChange());\n }\n\n private handleOptionClick(e: Event): void {\n const target = e.target as HTMLElement;\n const option = target.closest('swp-theme-option');\n\n if (option) {\n const theme = option.dataset.theme as Theme;\n if (theme) {\n this.set(theme);\n }\n }\n }\n\n private handleSystemChange(): void {\n // Only react to system changes if we're using system preference\n if (this.current === 'system') {\n this.updateUI();\n }\n }\n}\n", "/**\n * Search Controller\n *\n * Handles global search functionality and keyboard shortcuts\n */\n\nexport class SearchController {\n private input: HTMLInputElement | null = null;\n private container: HTMLElement | null = null;\n\n constructor() {\n this.input = document.getElementById('globalSearch') as HTMLInputElement | null;\n this.container = document.querySelector('swp-topbar-search');\n\n this.setupListeners();\n }\n\n /**\n * Get current search value\n */\n get value(): string {\n return this.input?.value ?? '';\n }\n\n /**\n * Set search value\n */\n set value(val: string) {\n if (this.input) {\n this.input.value = val;\n }\n }\n\n /**\n * Focus the search input\n */\n focus(): void {\n this.input?.focus();\n }\n\n /**\n * Blur the search input\n */\n blur(): void {\n this.input?.blur();\n }\n\n /**\n * Clear the search input\n */\n clear(): void {\n this.value = '';\n }\n\n private setupListeners(): void {\n // Keyboard shortcuts\n document.addEventListener('keydown', (e) => this.handleKeyboard(e));\n\n // Input handlers\n if (this.input) {\n this.input.addEventListener('input', (e) => this.handleInput(e));\n\n // Prevent form submission if wrapped in form\n const form = this.input.closest('form');\n form?.addEventListener('submit', (e) => this.handleSubmit(e));\n }\n }\n\n private handleKeyboard(e: KeyboardEvent): void {\n // Cmd/Ctrl + K to focus search\n if ((e.metaKey || e.ctrlKey) && e.key === 'k') {\n e.preventDefault();\n this.focus();\n return;\n }\n\n // Escape to blur search when focused\n if (e.key === 'Escape' && document.activeElement === this.input) {\n this.blur();\n }\n }\n\n private handleInput(e: Event): void {\n const target = e.target as HTMLInputElement;\n const query = target.value.trim();\n\n // Emit custom event for search\n document.dispatchEvent(new CustomEvent('app:search', {\n detail: { query },\n bubbles: true\n }));\n }\n\n private handleSubmit(e: Event): void {\n e.preventDefault();\n\n const query = this.value.trim();\n if (!query) return;\n\n // Emit custom event for search submit\n document.dispatchEvent(new CustomEvent('app:search-submit', {\n detail: { query },\n bubbles: true\n }));\n }\n}\n", "/**\n * Lock Screen Controller\n *\n * Handles PIN-based lock screen functionality\n */\n\nimport { DrawerController } from './drawers';\n\nexport class LockScreenController {\n private static readonly CORRECT_PIN = '1234'; // Demo PIN\n\n private lockScreen: HTMLElement | null = null;\n private pinInput: HTMLElement | null = null;\n private pinKeypad: HTMLElement | null = null;\n private lockTimeEl: HTMLElement | null = null;\n private pinDigits: NodeListOf | null = null;\n private currentPin = '';\n private drawers: DrawerController | null = null;\n\n constructor(drawers?: DrawerController) {\n this.drawers = drawers ?? null;\n this.lockScreen = document.getElementById('lockScreen');\n this.pinInput = document.getElementById('pinInput');\n this.pinKeypad = document.getElementById('pinKeypad');\n this.lockTimeEl = document.getElementById('lockTime');\n this.pinDigits = this.pinInput?.querySelectorAll('swp-pin-digit') ?? null;\n\n this.setupListeners();\n }\n\n /**\n * Check if lock screen is active\n */\n get isActive(): boolean {\n return this.lockScreen?.classList.contains('active') ?? false;\n }\n\n /**\n * Show the lock screen\n */\n show(): void {\n this.drawers?.closeAll();\n\n if (this.lockScreen) {\n this.lockScreen.classList.add('active');\n document.body.style.overflow = 'hidden';\n }\n\n this.currentPin = '';\n this.updateDisplay();\n\n // Update lock time\n if (this.lockTimeEl) {\n this.lockTimeEl.textContent = `L\u00E5st kl. ${this.formatTime()}`;\n }\n }\n\n /**\n * Hide the lock screen\n */\n hide(): void {\n if (this.lockScreen) {\n this.lockScreen.classList.remove('active');\n document.body.style.overflow = '';\n }\n\n this.currentPin = '';\n this.updateDisplay();\n }\n\n private formatTime(): string {\n const now = new Date();\n const hours = now.getHours().toString().padStart(2, '0');\n const minutes = now.getMinutes().toString().padStart(2, '0');\n return `${hours}:${minutes}`;\n }\n\n private updateDisplay(): void {\n if (!this.pinDigits) return;\n\n this.pinDigits.forEach((digit, index) => {\n digit.classList.remove('filled', 'error');\n if (index < this.currentPin.length) {\n digit.textContent = '\u2022';\n digit.classList.add('filled');\n } else {\n digit.textContent = '';\n }\n });\n }\n\n private showError(): void {\n if (!this.pinDigits) return;\n\n this.pinDigits.forEach(digit => digit.classList.add('error'));\n\n // Shake animation\n this.pinInput?.classList.add('shake');\n\n setTimeout(() => {\n this.currentPin = '';\n this.updateDisplay();\n this.pinInput?.classList.remove('shake');\n }, 500);\n }\n\n private verify(): void {\n if (this.currentPin === LockScreenController.CORRECT_PIN) {\n this.hide();\n } else {\n this.showError();\n }\n }\n\n private addDigit(digit: string): void {\n if (this.currentPin.length >= 4) return;\n\n this.currentPin += digit;\n this.updateDisplay();\n\n // Auto-verify when 4 digits entered\n if (this.currentPin.length === 4) {\n setTimeout(() => this.verify(), 200);\n }\n }\n\n private removeDigit(): void {\n if (this.currentPin.length === 0) return;\n this.currentPin = this.currentPin.slice(0, -1);\n this.updateDisplay();\n }\n\n private clearPin(): void {\n this.currentPin = '';\n this.updateDisplay();\n }\n\n private setupListeners(): void {\n // Keypad click handler\n this.pinKeypad?.addEventListener('click', (e) => this.handleKeypadClick(e));\n\n // Keyboard input\n document.addEventListener('keydown', (e) => this.handleKeyboard(e));\n\n // Lock button in sidebar\n document.querySelector('swp-side-menu-action.lock')\n ?.addEventListener('click', () => this.show());\n }\n\n private handleKeypadClick(e: Event): void {\n const target = e.target as HTMLElement;\n const key = target.closest('swp-pin-key');\n\n if (!key) return;\n\n const digit = key.dataset.digit;\n const action = key.dataset.action;\n\n if (digit) {\n this.addDigit(digit);\n } else if (action === 'backspace') {\n this.removeDigit();\n } else if (action === 'clear') {\n this.clearPin();\n }\n }\n\n private handleKeyboard(e: KeyboardEvent): void {\n if (!this.isActive) return;\n\n // Prevent default to avoid other interactions\n e.preventDefault();\n\n if (e.key >= '0' && e.key <= '9') {\n this.addDigit(e.key);\n } else if (e.key === 'Backspace') {\n this.removeDigit();\n } else if (e.key === 'Escape') {\n this.clearPin();\n }\n }\n}\n", "/**\n * Cash Controller\n *\n * Handles tab switching, cash calculations, and form interactions\n * for the Cash Register page.\n */\n\nexport class CashController {\n // Base values (from system - would come from server in real app)\n private readonly startBalance = 2000;\n private readonly cashSales = 3540;\n\n constructor() {\n this.setupTabs();\n this.setupCashCalculation();\n this.setupCheckboxSelection();\n this.setupApprovalCheckbox();\n this.setupDateFilters();\n this.setupRowToggle();\n this.setupDraftRowClick();\n }\n\n /**\n * Setup tab switching functionality\n */\n private setupTabs(): void {\n const tabs = document.querySelectorAll('swp-tab[data-tab]');\n\n tabs.forEach(tab => {\n tab.addEventListener('click', () => {\n const targetTab = tab.dataset.tab;\n if (targetTab) {\n this.switchToTab(targetTab);\n }\n });\n });\n }\n\n /**\n * Switch to a specific tab by name\n */\n private switchToTab(targetTab: string): void {\n const tabs = document.querySelectorAll('swp-tab[data-tab]');\n const contents = document.querySelectorAll('swp-tab-content[data-tab]');\n const statsBars = document.querySelectorAll('swp-cash-stats[data-for-tab]');\n\n // Update tab states\n tabs.forEach(t => {\n if (t.dataset.tab === targetTab) {\n t.classList.add('active');\n } else {\n t.classList.remove('active');\n }\n });\n\n // Update content visibility\n contents.forEach(content => {\n if (content.dataset.tab === targetTab) {\n content.classList.add('active');\n } else {\n content.classList.remove('active');\n }\n });\n\n // Update stats bar visibility\n statsBars.forEach(stats => {\n if (stats.dataset.forTab === targetTab) {\n stats.classList.add('active');\n } else {\n stats.classList.remove('active');\n }\n });\n }\n\n /**\n * Setup cash calculation with real-time updates\n */\n private setupCashCalculation(): void {\n const payoutsInput = document.getElementById('payouts') as HTMLInputElement;\n const toBankInput = document.getElementById('toBank') as HTMLInputElement;\n const actualCashInput = document.getElementById('actualCash') as HTMLInputElement;\n\n if (!payoutsInput || !toBankInput || !actualCashInput) return;\n\n const calculate = () => this.calculateCash(payoutsInput, toBankInput, actualCashInput);\n\n payoutsInput.addEventListener('input', calculate);\n toBankInput.addEventListener('input', calculate);\n actualCashInput.addEventListener('input', calculate);\n\n // Initial calculation\n calculate();\n }\n\n /**\n * Calculate expected cash and difference\n */\n private calculateCash(\n payoutsInput: HTMLInputElement,\n toBankInput: HTMLInputElement,\n actualCashInput: HTMLInputElement\n ): void {\n const payouts = this.parseNumber(payoutsInput.value);\n const toBank = this.parseNumber(toBankInput.value);\n const actual = this.parseNumber(actualCashInput.value);\n\n // Expected = start + sales - payouts - to bank\n const expectedCash = this.startBalance + this.cashSales - payouts - toBank;\n\n const expectedElement = document.getElementById('expectedCash');\n if (expectedElement) {\n expectedElement.textContent = this.formatNumber(expectedCash);\n }\n\n // Calculate and display difference\n this.updateDifference(actual, expectedCash, actualCashInput.value);\n }\n\n /**\n * Update difference box with color coding\n */\n private updateDifference(actual: number, expected: number, rawValue: string): void {\n const box = document.getElementById('differenceBox');\n const value = document.getElementById('differenceValue');\n if (!box || !value) return;\n\n const diff = actual - expected;\n\n // Remove all state classes\n box.classList.remove('positive', 'negative', 'neutral');\n\n if (actual === 0 && rawValue === '') {\n // No input yet\n value.textContent = '\u2013 kr';\n box.classList.add('neutral');\n } else if (diff > 0) {\n // More cash than expected\n value.textContent = '+' + this.formatNumber(diff) + ' kr';\n box.classList.add('positive');\n } else if (diff < 0) {\n // Less cash than expected\n value.textContent = this.formatNumber(diff) + ' kr';\n box.classList.add('negative');\n } else {\n // Exact match\n value.textContent = '0,00 kr';\n box.classList.add('neutral');\n }\n }\n\n /**\n * Setup checkbox selection for table rows\n */\n private setupCheckboxSelection(): void {\n const selectAll = document.getElementById('selectAll') as HTMLInputElement;\n const rowCheckboxes = document.querySelectorAll('.row-select');\n const exportBtn = document.getElementById('exportBtn') as HTMLButtonElement;\n const selectionCount = document.getElementById('selectionCount');\n\n if (!selectAll || !exportBtn || !selectionCount) return;\n\n const updateSelection = () => {\n const checked = document.querySelectorAll('.row-select:checked');\n const count = checked.length;\n\n selectionCount.textContent = count === 0 ? '0 valgt' : `${count} valgt`;\n exportBtn.disabled = count === 0;\n\n // Update select all state\n selectAll.checked = count === rowCheckboxes.length && count > 0;\n selectAll.indeterminate = count > 0 && count < rowCheckboxes.length;\n };\n\n selectAll.addEventListener('change', () => {\n rowCheckboxes.forEach(cb => cb.checked = selectAll.checked);\n updateSelection();\n });\n\n rowCheckboxes.forEach(cb => {\n cb.addEventListener('change', updateSelection);\n // Stop click from bubbling to row\n cb.addEventListener('click', e => e.stopPropagation());\n });\n }\n\n /**\n * Setup approval checkbox to enable/disable approve button\n */\n private setupApprovalCheckbox(): void {\n const checkbox = document.getElementById('confirmCheckbox') as HTMLInputElement;\n const approveBtn = document.getElementById('approveBtn') as HTMLButtonElement;\n\n if (!checkbox || !approveBtn) return;\n\n checkbox.addEventListener('change', () => {\n approveBtn.disabled = !checkbox.checked;\n });\n }\n\n /**\n * Setup date filter defaults (last 30 days)\n */\n private setupDateFilters(): void {\n const dateFrom = document.getElementById('dateFrom') as HTMLInputElement;\n const dateTo = document.getElementById('dateTo') as HTMLInputElement;\n\n if (!dateFrom || !dateTo) return;\n\n const today = new Date();\n const thirtyDaysAgo = new Date(today);\n thirtyDaysAgo.setDate(today.getDate() - 30);\n\n dateTo.value = this.formatDateISO(today);\n dateFrom.value = this.formatDateISO(thirtyDaysAgo);\n }\n\n /**\n * Format number as Danish currency\n */\n private formatNumber(num: number): string {\n return num.toLocaleString('da-DK', {\n minimumFractionDigits: 2,\n maximumFractionDigits: 2\n });\n }\n\n /**\n * Parse Danish number format\n */\n private parseNumber(str: string): number {\n if (!str) return 0;\n return parseFloat(str.replace(/\\./g, '').replace(',', '.')) || 0;\n }\n\n /**\n * Format date as ISO string (YYYY-MM-DD)\n */\n private formatDateISO(date: Date): string {\n return date.toISOString().split('T')[0];\n }\n\n /**\n * Setup row toggle for expandable details\n */\n private setupRowToggle(): void {\n const rows = document.querySelectorAll('swp-cash-table-row[data-id]:not(.draft-row)');\n\n rows.forEach(row => {\n const rowId = row.getAttribute('data-id');\n if (!rowId) return;\n\n const detail = document.querySelector(`swp-cash-row-detail[data-for=\"${rowId}\"]`);\n if (!detail) return;\n\n row.addEventListener('click', (e) => {\n // Don't toggle if clicking on checkbox\n if ((e.target as HTMLElement).closest('input[type=\"checkbox\"]')) return;\n\n const icon = row.querySelector('swp-row-toggle i');\n const isExpanded = row.classList.contains('expanded');\n\n // Close other expanded rows\n document.querySelectorAll('swp-cash-table-row.expanded').forEach(r => {\n if (r !== row) {\n const otherId = r.getAttribute('data-id');\n if (otherId) {\n const otherDetail = document.querySelector(`swp-cash-row-detail[data-for=\"${otherId}\"]`);\n const otherIcon = r.querySelector('swp-row-toggle i');\n if (otherDetail && otherIcon) {\n this.collapseRow(r, otherDetail, otherIcon as HTMLElement);\n }\n }\n }\n });\n\n // Toggle current row\n if (isExpanded) {\n this.collapseRow(row, detail, icon);\n } else {\n this.expandRow(row, detail, icon);\n }\n });\n });\n }\n\n /**\n * Expand a row with animation\n */\n private expandRow(row: Element, detail: HTMLElement, icon: Element | null): void {\n row.classList.add('expanded');\n detail.classList.add('expanded');\n\n // Animate icon rotation\n icon?.animate([\n { transform: 'rotate(0deg)' },\n { transform: 'rotate(90deg)' }\n ], {\n duration: 200,\n easing: 'ease-out',\n fill: 'forwards'\n });\n\n // Animate detail expansion\n const content = detail.querySelector('swp-row-detail-content') as HTMLElement;\n if (content) {\n const height = content.offsetHeight;\n detail.animate([\n { height: '0px', opacity: 0 },\n { height: `${height}px`, opacity: 1 }\n ], {\n duration: 250,\n easing: 'ease-out',\n fill: 'forwards'\n });\n }\n }\n\n /**\n * Collapse a row with animation\n */\n private collapseRow(row: Element, detail: HTMLElement, icon: Element | null): void {\n // Animate icon rotation\n icon?.animate([\n { transform: 'rotate(90deg)' },\n { transform: 'rotate(0deg)' }\n ], {\n duration: 200,\n easing: 'ease-out',\n fill: 'forwards'\n });\n\n // Animate detail collapse\n const content = detail.querySelector('swp-row-detail-content') as HTMLElement;\n if (content) {\n const height = content.offsetHeight;\n const animation = detail.animate([\n { height: `${height}px`, opacity: 1 },\n { height: '0px', opacity: 0 }\n ], {\n duration: 200,\n easing: 'ease-out',\n fill: 'forwards'\n });\n\n animation.onfinish = () => {\n row.classList.remove('expanded');\n detail.classList.remove('expanded');\n };\n } else {\n row.classList.remove('expanded');\n detail.classList.remove('expanded');\n }\n }\n\n /**\n * Setup draft row click to navigate to reconciliation tab\n */\n private setupDraftRowClick(): void {\n const draftRow = document.querySelector('swp-cash-table-row.draft-row');\n if (!draftRow) return;\n\n draftRow.style.cursor = 'pointer';\n draftRow.addEventListener('click', (e) => {\n // Don't navigate if clicking on checkbox\n if ((e.target as HTMLElement).closest('input[type=\"checkbox\"]')) return;\n\n this.switchToTab('afstemning');\n });\n }\n}\n", "/**\n * Employees Controller\n *\n * Handles content swap between list view and detail view,\n * plus tab switching within each view.\n * Uses History API for browser back/forward navigation.\n */\n\nexport class EmployeesController {\n private listView: HTMLElement | null = null;\n private detailView: HTMLElement | null = null;\n\n constructor() {\n this.listView = document.getElementById('employees-list-view');\n this.detailView = document.getElementById('employee-detail-view');\n\n // Only initialize if we're on the employees page\n if (!this.listView) return;\n\n this.setupListTabs();\n this.setupDetailTabs();\n this.setupChevronNavigation();\n this.setupBackNavigation();\n this.setupHistoryNavigation();\n this.restoreStateFromUrl();\n }\n\n /**\n * Setup popstate listener for browser back/forward\n */\n private setupHistoryNavigation(): void {\n window.addEventListener('popstate', (e: PopStateEvent) => {\n if (e.state?.employeeKey) {\n this.showDetailViewInternal(e.state.employeeKey);\n } else {\n this.showListViewInternal();\n }\n });\n }\n\n /**\n * Restore view state from URL on page load\n */\n private restoreStateFromUrl(): void {\n const hash = window.location.hash;\n if (hash.startsWith('#employee-')) {\n const employeeKey = hash.substring(1); // Remove #\n this.showDetailViewInternal(employeeKey);\n }\n }\n\n /**\n * Setup tab switching for the list view\n */\n private setupListTabs(): void {\n if (!this.listView) return;\n\n const tabs = this.listView.querySelectorAll('swp-tab-bar > swp-tab[data-tab]');\n\n tabs.forEach(tab => {\n tab.addEventListener('click', () => {\n const targetTab = tab.dataset.tab;\n if (targetTab) {\n this.switchTab(this.listView!, targetTab);\n }\n });\n });\n }\n\n /**\n * Setup tab switching for the detail view\n */\n private setupDetailTabs(): void {\n if (!this.detailView) return;\n\n const tabs = this.detailView.querySelectorAll('swp-tab-bar > swp-tab[data-tab]');\n\n tabs.forEach(tab => {\n tab.addEventListener('click', () => {\n const targetTab = tab.dataset.tab;\n if (targetTab) {\n this.switchTab(this.detailView!, targetTab);\n }\n });\n });\n }\n\n /**\n * Switch to a specific tab within a container\n */\n private switchTab(container: HTMLElement, targetTab: string): void {\n const tabs = container.querySelectorAll('swp-tab-bar > swp-tab[data-tab]');\n const contents = container.querySelectorAll('swp-tab-content[data-tab]');\n\n tabs.forEach(t => {\n t.classList.toggle('active', t.dataset.tab === targetTab);\n });\n\n contents.forEach(content => {\n content.classList.toggle('active', content.dataset.tab === targetTab);\n });\n }\n\n /**\n * Setup row click to show detail view\n * Ignores clicks on action buttons\n */\n private setupChevronNavigation(): void {\n document.addEventListener('click', (e: Event) => {\n const target = e.target as HTMLElement;\n\n // Ignore clicks on action buttons\n if (target.closest('swp-icon-btn') || target.closest('swp-table-actions')) {\n return;\n }\n\n const row = target.closest('swp-employee-row[data-employee-detail]');\n\n if (row) {\n const employeeKey = row.dataset.employeeDetail;\n if (employeeKey) {\n this.showDetailView(employeeKey);\n }\n }\n });\n }\n\n /**\n * Setup back button to return to list view\n */\n private setupBackNavigation(): void {\n document.addEventListener('click', (e: Event) => {\n const target = e.target as HTMLElement;\n const backLink = target.closest('[data-employee-back]');\n\n if (backLink) {\n this.showListView();\n }\n });\n }\n\n /**\n * Show the detail view and hide list view (with history push)\n */\n private showDetailView(employeeKey: string): void {\n // Push state to history\n history.pushState(\n { employeeKey },\n '',\n `#${employeeKey}`\n );\n this.showDetailViewInternal(employeeKey);\n }\n\n /**\n * Show detail view without modifying history (for popstate)\n */\n private showDetailViewInternal(employeeKey: string): void {\n if (this.listView && this.detailView) {\n this.listView.style.display = 'none';\n this.detailView.style.display = 'block';\n this.detailView.dataset.employee = employeeKey;\n\n // Reset to first tab\n this.switchTab(this.detailView, 'general');\n }\n }\n\n /**\n * Show the list view and hide detail view (with history push)\n */\n private showListView(): void {\n // Push state to history (clear hash)\n history.pushState(\n {},\n '',\n window.location.pathname\n );\n this.showListViewInternal();\n }\n\n /**\n * Show list view without modifying history (for popstate)\n */\n private showListViewInternal(): void {\n if (this.listView && this.detailView) {\n this.detailView.style.display = 'none';\n this.listView.style.display = 'block';\n }\n }\n}\n", "/**\n * Salon OS App\n *\n * Main application class that orchestrates all UI controllers\n */\n\nimport { SidebarController } from './modules/sidebar';\nimport { DrawerController } from './modules/drawers';\nimport { ThemeController } from './modules/theme';\nimport { SearchController } from './modules/search';\nimport { LockScreenController } from './modules/lockscreen';\nimport { CashController } from './modules/cash';\nimport { EmployeesController } from './modules/employees';\n\n/**\n * Main application class\n */\nexport class App {\n readonly sidebar: SidebarController;\n readonly drawers: DrawerController;\n readonly theme: ThemeController;\n readonly search: SearchController;\n readonly lockScreen: LockScreenController;\n readonly cash: CashController;\n readonly employees: EmployeesController;\n\n constructor() {\n // Initialize controllers\n this.sidebar = new SidebarController();\n this.drawers = new DrawerController();\n this.theme = new ThemeController();\n this.search = new SearchController();\n this.lockScreen = new LockScreenController(this.drawers);\n this.cash = new CashController();\n this.employees = new EmployeesController();\n }\n}\n\n/**\n * Global app instance\n */\nlet app: App;\n\n/**\n * Initialize the application\n */\nfunction init(): void {\n app = new App();\n\n // Expose to window for debugging\n if (typeof window !== 'undefined') {\n (window as unknown as { app: App }).app = app;\n }\n}\n\n// Wait for DOM ready\nif (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', init);\n} else {\n init();\n}\n\nexport { app };\nexport default App;\n"], - "mappings": "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", + "sourcesContent": ["/**\n * Sidebar Controller\n *\n * Handles sidebar collapse/expand and tooltip functionality\n */\n\nexport class SidebarController {\n private menuToggle: HTMLElement | null = null;\n private appLayout: HTMLElement | null = null;\n private menuTooltip: HTMLElement | null = null;\n\n constructor() {\n this.menuToggle = document.getElementById('menuToggle');\n this.appLayout = document.querySelector('swp-app-layout');\n this.menuTooltip = document.getElementById('menuTooltip');\n\n this.setupListeners();\n this.setupTooltips();\n this.restoreState();\n }\n\n /**\n * Check if sidebar is collapsed\n */\n get isCollapsed(): boolean {\n return this.appLayout?.classList.contains('menu-collapsed') ?? false;\n }\n\n /**\n * Toggle sidebar collapsed state\n */\n toggle(): void {\n if (!this.appLayout) return;\n\n this.appLayout.classList.toggle('menu-collapsed');\n localStorage.setItem('sidebar-collapsed', String(this.isCollapsed));\n }\n\n /**\n * Collapse the sidebar\n */\n collapse(): void {\n this.appLayout?.classList.add('menu-collapsed');\n localStorage.setItem('sidebar-collapsed', 'true');\n }\n\n /**\n * Expand the sidebar\n */\n expand(): void {\n this.appLayout?.classList.remove('menu-collapsed');\n localStorage.setItem('sidebar-collapsed', 'false');\n }\n\n private setupListeners(): void {\n this.menuToggle?.addEventListener('click', () => this.toggle());\n }\n\n private setupTooltips(): void {\n if (!this.menuTooltip) return;\n\n const menuItems = document.querySelectorAll('swp-side-menu-item[data-tooltip]');\n\n menuItems.forEach(item => {\n item.addEventListener('mouseenter', () => this.showTooltip(item));\n item.addEventListener('mouseleave', () => this.hideTooltip());\n });\n }\n\n private showTooltip(item: HTMLElement): void {\n if (!this.isCollapsed || !this.menuTooltip) return;\n\n const rect = item.getBoundingClientRect();\n const tooltipText = item.dataset.tooltip;\n\n if (!tooltipText) return;\n\n this.menuTooltip.textContent = tooltipText;\n this.menuTooltip.style.left = `${rect.right + 8}px`;\n this.menuTooltip.style.top = `${rect.top + rect.height / 2}px`;\n this.menuTooltip.style.transform = 'translateY(-50%)';\n this.menuTooltip.showPopover();\n }\n\n private hideTooltip(): void {\n this.menuTooltip?.hidePopover();\n }\n\n private restoreState(): void {\n if (!this.appLayout) return;\n\n if (localStorage.getItem('sidebar-collapsed') === 'true') {\n this.appLayout.classList.add('menu-collapsed');\n }\n }\n}\n", "/**\n * Drawer Controller\n *\n * Handles all drawer functionality including profile, notifications, and todo drawers\n */\n\nexport type DrawerName = 'profile' | 'notification' | 'todo' | 'newTodo';\n\nexport class DrawerController {\n private profileDrawer: HTMLElement | null = null;\n private notificationDrawer: HTMLElement | null = null;\n private todoDrawer: HTMLElement | null = null;\n private newTodoDrawer: HTMLElement | null = null;\n private overlay: HTMLElement | null = null;\n private activeDrawer: DrawerName | null = null;\n private activeGenericDrawer: HTMLElement | null = null;\n\n constructor() {\n this.profileDrawer = document.getElementById('profileDrawer');\n this.notificationDrawer = document.getElementById('notificationDrawer');\n this.todoDrawer = document.getElementById('todoDrawer');\n this.newTodoDrawer = document.getElementById('newTodoDrawer');\n this.overlay = document.getElementById('drawerOverlay');\n\n this.setupListeners();\n this.setupGenericDrawers();\n }\n\n /**\n * Get currently active drawer name\n */\n get active(): DrawerName | null {\n return this.activeDrawer;\n }\n\n /**\n * Open a drawer by name\n */\n open(name: DrawerName): void {\n this.closeAll();\n\n const drawer = this.getDrawer(name);\n if (drawer && this.overlay) {\n drawer.classList.add('active');\n this.overlay.classList.add('active');\n document.body.style.overflow = 'hidden';\n this.activeDrawer = name;\n }\n }\n\n /**\n * Close a specific drawer\n */\n close(name: DrawerName): void {\n const drawer = this.getDrawer(name);\n drawer?.classList.remove('active');\n\n // Only hide overlay if no drawers are active\n if (this.overlay && !document.querySelector('.active[class*=\"drawer\"]')) {\n this.overlay.classList.remove('active');\n document.body.style.overflow = '';\n }\n\n if (this.activeDrawer === name) {\n this.activeDrawer = null;\n }\n }\n\n /**\n * Close all drawers\n */\n closeAll(): void {\n [this.profileDrawer, this.notificationDrawer, this.todoDrawer, this.newTodoDrawer]\n .forEach(drawer => drawer?.classList.remove('active'));\n\n // Close any generic drawers\n this.closeGenericDrawer();\n\n this.overlay?.classList.remove('active');\n document.body.style.overflow = '';\n this.activeDrawer = null;\n }\n\n /**\n * Open a generic drawer by ID\n */\n openGenericDrawer(drawerId: string): void {\n this.closeAll();\n\n const drawer = document.getElementById(drawerId);\n if (drawer && this.overlay) {\n drawer.classList.add('open');\n this.overlay.classList.add('active');\n document.body.style.overflow = 'hidden';\n this.activeGenericDrawer = drawer;\n }\n }\n\n /**\n * Close the currently open generic drawer\n */\n closeGenericDrawer(): void {\n this.activeGenericDrawer?.classList.remove('open');\n this.activeGenericDrawer = null;\n }\n\n /**\n * Open profile drawer\n */\n openProfile(): void {\n this.open('profile');\n }\n\n /**\n * Open notification drawer\n */\n openNotification(): void {\n this.open('notification');\n }\n\n /**\n * Open todo drawer (slides on top of profile)\n */\n openTodo(): void {\n this.todoDrawer?.classList.add('active');\n }\n\n /**\n * Close todo drawer\n */\n closeTodo(): void {\n this.todoDrawer?.classList.remove('active');\n this.closeNewTodo();\n }\n\n /**\n * Open new todo drawer\n */\n openNewTodo(): void {\n this.newTodoDrawer?.classList.add('active');\n }\n\n /**\n * Close new todo drawer\n */\n closeNewTodo(): void {\n this.newTodoDrawer?.classList.remove('active');\n }\n\n /**\n * Mark all notifications as read\n */\n markAllNotificationsRead(): void {\n if (!this.notificationDrawer) return;\n\n const unreadItems = this.notificationDrawer.querySelectorAll(\n 'swp-notification-item[data-unread=\"true\"]'\n );\n unreadItems.forEach(item => item.removeAttribute('data-unread'));\n\n const badge = document.querySelector('swp-notification-badge');\n if (badge) {\n badge.style.display = 'none';\n }\n }\n\n private getDrawer(name: DrawerName): HTMLElement | null {\n switch (name) {\n case 'profile': return this.profileDrawer;\n case 'notification': return this.notificationDrawer;\n case 'todo': return this.todoDrawer;\n case 'newTodo': return this.newTodoDrawer;\n }\n }\n\n private setupListeners(): void {\n // Profile drawer triggers\n document.getElementById('profileTrigger')\n ?.addEventListener('click', () => this.openProfile());\n document.getElementById('drawerClose')\n ?.addEventListener('click', () => this.close('profile'));\n\n // Notification drawer triggers\n document.getElementById('notificationsBtn')\n ?.addEventListener('click', () => this.openNotification());\n document.getElementById('notificationDrawerClose')\n ?.addEventListener('click', () => this.close('notification'));\n document.getElementById('markAllRead')\n ?.addEventListener('click', () => this.markAllNotificationsRead());\n\n // Todo drawer triggers\n document.getElementById('openTodoDrawer')\n ?.addEventListener('click', () => this.openTodo());\n document.getElementById('todoDrawerBack')\n ?.addEventListener('click', () => this.closeTodo());\n\n // New todo drawer triggers\n document.getElementById('addTodoBtn')\n ?.addEventListener('click', () => this.openNewTodo());\n document.getElementById('newTodoDrawerBack')\n ?.addEventListener('click', () => this.closeNewTodo());\n document.getElementById('cancelNewTodo')\n ?.addEventListener('click', () => this.closeNewTodo());\n document.getElementById('saveNewTodo')\n ?.addEventListener('click', () => this.closeNewTodo());\n\n // Overlay click closes all\n this.overlay?.addEventListener('click', () => this.closeAll());\n\n // Escape key closes all\n document.addEventListener('keydown', (e: KeyboardEvent) => {\n if (e.key === 'Escape') this.closeAll();\n });\n\n // Todo interactions\n this.todoDrawer?.addEventListener('click', (e) => this.handleTodoClick(e));\n\n // Visibility options\n document.addEventListener('click', (e) => this.handleVisibilityClick(e));\n }\n\n private handleTodoClick(e: Event): void {\n const target = e.target as HTMLElement;\n const todoItem = target.closest('swp-todo-item');\n const checkbox = target.closest('swp-todo-checkbox');\n\n if (checkbox && todoItem) {\n const isCompleted = todoItem.dataset.completed === 'true';\n if (isCompleted) {\n todoItem.removeAttribute('data-completed');\n } else {\n todoItem.dataset.completed = 'true';\n }\n }\n\n // Toggle section collapse\n const sectionHeader = target.closest('swp-todo-section-header');\n if (sectionHeader) {\n const section = sectionHeader.closest('swp-todo-section');\n section?.classList.toggle('collapsed');\n }\n }\n\n private handleVisibilityClick(e: Event): void {\n const target = e.target as HTMLElement;\n const option = target.closest('swp-visibility-option');\n\n if (option) {\n document.querySelectorAll('swp-visibility-option')\n .forEach(o => o.classList.remove('active'));\n option.classList.add('active');\n }\n }\n\n /**\n * Setup generic drawer triggers and close buttons\n * Uses data-drawer-trigger=\"drawer-id\" and data-drawer-close attributes\n */\n private setupGenericDrawers(): void {\n // Handle drawer triggers\n document.addEventListener('click', (e: Event) => {\n const target = e.target as HTMLElement;\n const trigger = target.closest('[data-drawer-trigger]');\n\n if (trigger) {\n const drawerId = trigger.dataset.drawerTrigger;\n if (drawerId) {\n this.openGenericDrawer(drawerId);\n }\n }\n });\n\n // Handle drawer close buttons\n document.addEventListener('click', (e: Event) => {\n const target = e.target as HTMLElement;\n const closeBtn = target.closest('[data-drawer-close]');\n\n if (closeBtn) {\n this.closeGenericDrawer();\n this.overlay?.classList.remove('active');\n document.body.style.overflow = '';\n }\n });\n }\n}\n", "/**\n * Theme Controller\n *\n * Handles dark/light mode switching and system preference detection\n */\n\nexport type Theme = 'light' | 'dark' | 'system';\n\nexport class ThemeController {\n private static readonly STORAGE_KEY = 'theme-preference';\n private static readonly DARK_CLASS = 'dark-mode';\n private static readonly LIGHT_CLASS = 'light-mode';\n\n private root: HTMLElement;\n private themeOptions: NodeListOf;\n\n constructor() {\n this.root = document.documentElement;\n this.themeOptions = document.querySelectorAll('swp-theme-option');\n\n this.applyTheme(this.current);\n this.updateUI();\n this.setupListeners();\n }\n\n /**\n * Get the current theme setting\n */\n get current(): Theme {\n const stored = localStorage.getItem(ThemeController.STORAGE_KEY) as Theme | null;\n if (stored === 'dark' || stored === 'light' || stored === 'system') {\n return stored;\n }\n return 'system';\n }\n\n /**\n * Check if dark mode is currently active\n */\n get isDark(): boolean {\n return this.root.classList.contains(ThemeController.DARK_CLASS) ||\n (this.systemPrefersDark && !this.root.classList.contains(ThemeController.LIGHT_CLASS));\n }\n\n /**\n * Check if system prefers dark mode\n */\n get systemPrefersDark(): boolean {\n return window.matchMedia('(prefers-color-scheme: dark)').matches;\n }\n\n /**\n * Set theme and persist preference\n */\n set(theme: Theme): void {\n localStorage.setItem(ThemeController.STORAGE_KEY, theme);\n this.applyTheme(theme);\n this.updateUI();\n }\n\n /**\n * Toggle between light and dark themes\n */\n toggle(): void {\n this.set(this.isDark ? 'light' : 'dark');\n }\n\n private applyTheme(theme: Theme): void {\n this.root.classList.remove(ThemeController.DARK_CLASS, ThemeController.LIGHT_CLASS);\n\n if (theme === 'dark') {\n this.root.classList.add(ThemeController.DARK_CLASS);\n } else if (theme === 'light') {\n this.root.classList.add(ThemeController.LIGHT_CLASS);\n }\n // 'system' leaves both classes off, letting CSS media query handle it\n }\n\n private updateUI(): void {\n if (!this.themeOptions) return;\n\n const darkActive = this.isDark;\n\n this.themeOptions.forEach(option => {\n const theme = option.dataset.theme as Theme;\n const isActive = (theme === 'dark' && darkActive) || (theme === 'light' && !darkActive);\n option.classList.toggle('active', isActive);\n });\n }\n\n private setupListeners(): void {\n // Theme option clicks\n this.themeOptions.forEach(option => {\n option.addEventListener('click', (e) => this.handleOptionClick(e));\n });\n\n // System theme changes\n window.matchMedia('(prefers-color-scheme: dark)')\n .addEventListener('change', () => this.handleSystemChange());\n }\n\n private handleOptionClick(e: Event): void {\n const target = e.target as HTMLElement;\n const option = target.closest('swp-theme-option');\n\n if (option) {\n const theme = option.dataset.theme as Theme;\n if (theme) {\n this.set(theme);\n }\n }\n }\n\n private handleSystemChange(): void {\n // Only react to system changes if we're using system preference\n if (this.current === 'system') {\n this.updateUI();\n }\n }\n}\n", "/**\n * Search Controller\n *\n * Handles global search functionality and keyboard shortcuts\n */\n\nexport class SearchController {\n private input: HTMLInputElement | null = null;\n private container: HTMLElement | null = null;\n\n constructor() {\n this.input = document.getElementById('globalSearch') as HTMLInputElement | null;\n this.container = document.querySelector('swp-topbar-search');\n\n this.setupListeners();\n }\n\n /**\n * Get current search value\n */\n get value(): string {\n return this.input?.value ?? '';\n }\n\n /**\n * Set search value\n */\n set value(val: string) {\n if (this.input) {\n this.input.value = val;\n }\n }\n\n /**\n * Focus the search input\n */\n focus(): void {\n this.input?.focus();\n }\n\n /**\n * Blur the search input\n */\n blur(): void {\n this.input?.blur();\n }\n\n /**\n * Clear the search input\n */\n clear(): void {\n this.value = '';\n }\n\n private setupListeners(): void {\n // Keyboard shortcuts\n document.addEventListener('keydown', (e) => this.handleKeyboard(e));\n\n // Input handlers\n if (this.input) {\n this.input.addEventListener('input', (e) => this.handleInput(e));\n\n // Prevent form submission if wrapped in form\n const form = this.input.closest('form');\n form?.addEventListener('submit', (e) => this.handleSubmit(e));\n }\n }\n\n private handleKeyboard(e: KeyboardEvent): void {\n // Cmd/Ctrl + K to focus search\n if ((e.metaKey || e.ctrlKey) && e.key === 'k') {\n e.preventDefault();\n this.focus();\n return;\n }\n\n // Escape to blur search when focused\n if (e.key === 'Escape' && document.activeElement === this.input) {\n this.blur();\n }\n }\n\n private handleInput(e: Event): void {\n const target = e.target as HTMLInputElement;\n const query = target.value.trim();\n\n // Emit custom event for search\n document.dispatchEvent(new CustomEvent('app:search', {\n detail: { query },\n bubbles: true\n }));\n }\n\n private handleSubmit(e: Event): void {\n e.preventDefault();\n\n const query = this.value.trim();\n if (!query) return;\n\n // Emit custom event for search submit\n document.dispatchEvent(new CustomEvent('app:search-submit', {\n detail: { query },\n bubbles: true\n }));\n }\n}\n", "/**\n * Lock Screen Controller\n *\n * Handles PIN-based lock screen functionality\n */\n\nimport { DrawerController } from './drawers';\n\nexport class LockScreenController {\n private static readonly CORRECT_PIN = '1234'; // Demo PIN\n\n private lockScreen: HTMLElement | null = null;\n private pinInput: HTMLElement | null = null;\n private pinKeypad: HTMLElement | null = null;\n private lockTimeEl: HTMLElement | null = null;\n private pinDigits: NodeListOf | null = null;\n private currentPin = '';\n private drawers: DrawerController | null = null;\n\n constructor(drawers?: DrawerController) {\n this.drawers = drawers ?? null;\n this.lockScreen = document.getElementById('lockScreen');\n this.pinInput = document.getElementById('pinInput');\n this.pinKeypad = document.getElementById('pinKeypad');\n this.lockTimeEl = document.getElementById('lockTime');\n this.pinDigits = this.pinInput?.querySelectorAll('swp-pin-digit') ?? null;\n\n this.setupListeners();\n }\n\n /**\n * Check if lock screen is active\n */\n get isActive(): boolean {\n return this.lockScreen?.classList.contains('active') ?? false;\n }\n\n /**\n * Show the lock screen\n */\n show(): void {\n this.drawers?.closeAll();\n\n if (this.lockScreen) {\n this.lockScreen.classList.add('active');\n document.body.style.overflow = 'hidden';\n }\n\n this.currentPin = '';\n this.updateDisplay();\n\n // Update lock time\n if (this.lockTimeEl) {\n this.lockTimeEl.textContent = `L\u00E5st kl. ${this.formatTime()}`;\n }\n }\n\n /**\n * Hide the lock screen\n */\n hide(): void {\n if (this.lockScreen) {\n this.lockScreen.classList.remove('active');\n document.body.style.overflow = '';\n }\n\n this.currentPin = '';\n this.updateDisplay();\n }\n\n private formatTime(): string {\n const now = new Date();\n const hours = now.getHours().toString().padStart(2, '0');\n const minutes = now.getMinutes().toString().padStart(2, '0');\n return `${hours}:${minutes}`;\n }\n\n private updateDisplay(): void {\n if (!this.pinDigits) return;\n\n this.pinDigits.forEach((digit, index) => {\n digit.classList.remove('filled', 'error');\n if (index < this.currentPin.length) {\n digit.textContent = '\u2022';\n digit.classList.add('filled');\n } else {\n digit.textContent = '';\n }\n });\n }\n\n private showError(): void {\n if (!this.pinDigits) return;\n\n this.pinDigits.forEach(digit => digit.classList.add('error'));\n\n // Shake animation\n this.pinInput?.classList.add('shake');\n\n setTimeout(() => {\n this.currentPin = '';\n this.updateDisplay();\n this.pinInput?.classList.remove('shake');\n }, 500);\n }\n\n private verify(): void {\n if (this.currentPin === LockScreenController.CORRECT_PIN) {\n this.hide();\n } else {\n this.showError();\n }\n }\n\n private addDigit(digit: string): void {\n if (this.currentPin.length >= 4) return;\n\n this.currentPin += digit;\n this.updateDisplay();\n\n // Auto-verify when 4 digits entered\n if (this.currentPin.length === 4) {\n setTimeout(() => this.verify(), 200);\n }\n }\n\n private removeDigit(): void {\n if (this.currentPin.length === 0) return;\n this.currentPin = this.currentPin.slice(0, -1);\n this.updateDisplay();\n }\n\n private clearPin(): void {\n this.currentPin = '';\n this.updateDisplay();\n }\n\n private setupListeners(): void {\n // Keypad click handler\n this.pinKeypad?.addEventListener('click', (e) => this.handleKeypadClick(e));\n\n // Keyboard input\n document.addEventListener('keydown', (e) => this.handleKeyboard(e));\n\n // Lock button in sidebar\n document.querySelector('swp-side-menu-action.lock')\n ?.addEventListener('click', () => this.show());\n }\n\n private handleKeypadClick(e: Event): void {\n const target = e.target as HTMLElement;\n const key = target.closest('swp-pin-key');\n\n if (!key) return;\n\n const digit = key.dataset.digit;\n const action = key.dataset.action;\n\n if (digit) {\n this.addDigit(digit);\n } else if (action === 'backspace') {\n this.removeDigit();\n } else if (action === 'clear') {\n this.clearPin();\n }\n }\n\n private handleKeyboard(e: KeyboardEvent): void {\n if (!this.isActive) return;\n\n // Prevent default to avoid other interactions\n e.preventDefault();\n\n if (e.key >= '0' && e.key <= '9') {\n this.addDigit(e.key);\n } else if (e.key === 'Backspace') {\n this.removeDigit();\n } else if (e.key === 'Escape') {\n this.clearPin();\n }\n }\n}\n", "/**\n * Cash Controller\n *\n * Handles tab switching, cash calculations, and form interactions\n * for the Cash Register page.\n */\n\nexport class CashController {\n // Base values (from system - would come from server in real app)\n private readonly startBalance = 2000;\n private readonly cashSales = 3540;\n\n constructor() {\n this.setupTabs();\n this.setupCashCalculation();\n this.setupCheckboxSelection();\n this.setupApprovalCheckbox();\n this.setupDateFilters();\n this.setupRowToggle();\n this.setupDraftRowClick();\n }\n\n /**\n * Setup tab switching functionality\n */\n private setupTabs(): void {\n const tabs = document.querySelectorAll('swp-tab[data-tab]');\n\n tabs.forEach(tab => {\n tab.addEventListener('click', () => {\n const targetTab = tab.dataset.tab;\n if (targetTab) {\n this.switchToTab(targetTab);\n }\n });\n });\n }\n\n /**\n * Switch to a specific tab by name\n */\n private switchToTab(targetTab: string): void {\n const tabs = document.querySelectorAll('swp-tab[data-tab]');\n const contents = document.querySelectorAll('swp-tab-content[data-tab]');\n const statsBars = document.querySelectorAll('swp-cash-stats[data-for-tab]');\n\n // Update tab states\n tabs.forEach(t => {\n if (t.dataset.tab === targetTab) {\n t.classList.add('active');\n } else {\n t.classList.remove('active');\n }\n });\n\n // Update content visibility\n contents.forEach(content => {\n if (content.dataset.tab === targetTab) {\n content.classList.add('active');\n } else {\n content.classList.remove('active');\n }\n });\n\n // Update stats bar visibility\n statsBars.forEach(stats => {\n if (stats.dataset.forTab === targetTab) {\n stats.classList.add('active');\n } else {\n stats.classList.remove('active');\n }\n });\n }\n\n /**\n * Setup cash calculation with real-time updates\n */\n private setupCashCalculation(): void {\n const payoutsInput = document.getElementById('payouts') as HTMLInputElement;\n const toBankInput = document.getElementById('toBank') as HTMLInputElement;\n const actualCashInput = document.getElementById('actualCash') as HTMLInputElement;\n\n if (!payoutsInput || !toBankInput || !actualCashInput) return;\n\n const calculate = () => this.calculateCash(payoutsInput, toBankInput, actualCashInput);\n\n payoutsInput.addEventListener('input', calculate);\n toBankInput.addEventListener('input', calculate);\n actualCashInput.addEventListener('input', calculate);\n\n // Initial calculation\n calculate();\n }\n\n /**\n * Calculate expected cash and difference\n */\n private calculateCash(\n payoutsInput: HTMLInputElement,\n toBankInput: HTMLInputElement,\n actualCashInput: HTMLInputElement\n ): void {\n const payouts = this.parseNumber(payoutsInput.value);\n const toBank = this.parseNumber(toBankInput.value);\n const actual = this.parseNumber(actualCashInput.value);\n\n // Expected = start + sales - payouts - to bank\n const expectedCash = this.startBalance + this.cashSales - payouts - toBank;\n\n const expectedElement = document.getElementById('expectedCash');\n if (expectedElement) {\n expectedElement.textContent = this.formatNumber(expectedCash);\n }\n\n // Calculate and display difference\n this.updateDifference(actual, expectedCash, actualCashInput.value);\n }\n\n /**\n * Update difference box with color coding\n */\n private updateDifference(actual: number, expected: number, rawValue: string): void {\n const box = document.getElementById('differenceBox');\n const value = document.getElementById('differenceValue');\n if (!box || !value) return;\n\n const diff = actual - expected;\n\n // Remove all state classes\n box.classList.remove('positive', 'negative', 'neutral');\n\n if (actual === 0 && rawValue === '') {\n // No input yet\n value.textContent = '\u2013 kr';\n box.classList.add('neutral');\n } else if (diff > 0) {\n // More cash than expected\n value.textContent = '+' + this.formatNumber(diff) + ' kr';\n box.classList.add('positive');\n } else if (diff < 0) {\n // Less cash than expected\n value.textContent = this.formatNumber(diff) + ' kr';\n box.classList.add('negative');\n } else {\n // Exact match\n value.textContent = '0,00 kr';\n box.classList.add('neutral');\n }\n }\n\n /**\n * Setup checkbox selection for table rows\n */\n private setupCheckboxSelection(): void {\n const selectAll = document.getElementById('selectAll') as HTMLInputElement;\n const rowCheckboxes = document.querySelectorAll('.row-select');\n const exportBtn = document.getElementById('exportBtn') as HTMLButtonElement;\n const selectionCount = document.getElementById('selectionCount');\n\n if (!selectAll || !exportBtn || !selectionCount) return;\n\n const updateSelection = () => {\n const checked = document.querySelectorAll('.row-select:checked');\n const count = checked.length;\n\n selectionCount.textContent = count === 0 ? '0 valgt' : `${count} valgt`;\n exportBtn.disabled = count === 0;\n\n // Update select all state\n selectAll.checked = count === rowCheckboxes.length && count > 0;\n selectAll.indeterminate = count > 0 && count < rowCheckboxes.length;\n };\n\n selectAll.addEventListener('change', () => {\n rowCheckboxes.forEach(cb => cb.checked = selectAll.checked);\n updateSelection();\n });\n\n rowCheckboxes.forEach(cb => {\n cb.addEventListener('change', updateSelection);\n // Stop click from bubbling to row\n cb.addEventListener('click', e => e.stopPropagation());\n });\n }\n\n /**\n * Setup approval checkbox to enable/disable approve button\n */\n private setupApprovalCheckbox(): void {\n const checkbox = document.getElementById('confirmCheckbox') as HTMLInputElement;\n const approveBtn = document.getElementById('approveBtn') as HTMLButtonElement;\n\n if (!checkbox || !approveBtn) return;\n\n checkbox.addEventListener('change', () => {\n approveBtn.disabled = !checkbox.checked;\n });\n }\n\n /**\n * Setup date filter defaults (last 30 days)\n */\n private setupDateFilters(): void {\n const dateFrom = document.getElementById('dateFrom') as HTMLInputElement;\n const dateTo = document.getElementById('dateTo') as HTMLInputElement;\n\n if (!dateFrom || !dateTo) return;\n\n const today = new Date();\n const thirtyDaysAgo = new Date(today);\n thirtyDaysAgo.setDate(today.getDate() - 30);\n\n dateTo.value = this.formatDateISO(today);\n dateFrom.value = this.formatDateISO(thirtyDaysAgo);\n }\n\n /**\n * Format number as Danish currency\n */\n private formatNumber(num: number): string {\n return num.toLocaleString('da-DK', {\n minimumFractionDigits: 2,\n maximumFractionDigits: 2\n });\n }\n\n /**\n * Parse Danish number format\n */\n private parseNumber(str: string): number {\n if (!str) return 0;\n return parseFloat(str.replace(/\\./g, '').replace(',', '.')) || 0;\n }\n\n /**\n * Format date as ISO string (YYYY-MM-DD)\n */\n private formatDateISO(date: Date): string {\n return date.toISOString().split('T')[0];\n }\n\n /**\n * Setup row toggle for expandable details\n */\n private setupRowToggle(): void {\n const rows = document.querySelectorAll('swp-cash-table-row[data-id]:not(.draft-row)');\n\n rows.forEach(row => {\n const rowId = row.getAttribute('data-id');\n if (!rowId) return;\n\n const detail = document.querySelector(`swp-cash-row-detail[data-for=\"${rowId}\"]`);\n if (!detail) return;\n\n row.addEventListener('click', (e) => {\n // Don't toggle if clicking on checkbox\n if ((e.target as HTMLElement).closest('input[type=\"checkbox\"]')) return;\n\n const icon = row.querySelector('swp-row-toggle i');\n const isExpanded = row.classList.contains('expanded');\n\n // Close other expanded rows\n document.querySelectorAll('swp-cash-table-row.expanded').forEach(r => {\n if (r !== row) {\n const otherId = r.getAttribute('data-id');\n if (otherId) {\n const otherDetail = document.querySelector(`swp-cash-row-detail[data-for=\"${otherId}\"]`);\n const otherIcon = r.querySelector('swp-row-toggle i');\n if (otherDetail && otherIcon) {\n this.collapseRow(r, otherDetail, otherIcon as HTMLElement);\n }\n }\n }\n });\n\n // Toggle current row\n if (isExpanded) {\n this.collapseRow(row, detail, icon);\n } else {\n this.expandRow(row, detail, icon);\n }\n });\n });\n }\n\n /**\n * Expand a row with animation\n */\n private expandRow(row: Element, detail: HTMLElement, icon: Element | null): void {\n row.classList.add('expanded');\n detail.classList.add('expanded');\n\n // Animate icon rotation\n icon?.animate([\n { transform: 'rotate(0deg)' },\n { transform: 'rotate(90deg)' }\n ], {\n duration: 200,\n easing: 'ease-out',\n fill: 'forwards'\n });\n\n // Animate detail expansion\n const content = detail.querySelector('swp-row-detail-content') as HTMLElement;\n if (content) {\n const height = content.offsetHeight;\n detail.animate([\n { height: '0px', opacity: 0 },\n { height: `${height}px`, opacity: 1 }\n ], {\n duration: 250,\n easing: 'ease-out',\n fill: 'forwards'\n });\n }\n }\n\n /**\n * Collapse a row with animation\n */\n private collapseRow(row: Element, detail: HTMLElement, icon: Element | null): void {\n // Animate icon rotation\n icon?.animate([\n { transform: 'rotate(90deg)' },\n { transform: 'rotate(0deg)' }\n ], {\n duration: 200,\n easing: 'ease-out',\n fill: 'forwards'\n });\n\n // Animate detail collapse\n const content = detail.querySelector('swp-row-detail-content') as HTMLElement;\n if (content) {\n const height = content.offsetHeight;\n const animation = detail.animate([\n { height: `${height}px`, opacity: 1 },\n { height: '0px', opacity: 0 }\n ], {\n duration: 200,\n easing: 'ease-out',\n fill: 'forwards'\n });\n\n animation.onfinish = () => {\n row.classList.remove('expanded');\n detail.classList.remove('expanded');\n };\n } else {\n row.classList.remove('expanded');\n detail.classList.remove('expanded');\n }\n }\n\n /**\n * Setup draft row click to navigate to reconciliation tab\n */\n private setupDraftRowClick(): void {\n const draftRow = document.querySelector('swp-cash-table-row.draft-row');\n if (!draftRow) return;\n\n draftRow.style.cursor = 'pointer';\n draftRow.addEventListener('click', (e) => {\n // Don't navigate if clicking on checkbox\n if ((e.target as HTMLElement).closest('input[type=\"checkbox\"]')) return;\n\n this.switchToTab('afstemning');\n });\n }\n}\n", "/**\n * Employees Controller\n *\n * Handles content swap between list view and detail view,\n * plus tab switching within each view.\n * Uses History API for browser back/forward navigation.\n */\n\nexport class EmployeesController {\n private ratesSync: RatesSyncController | null = null;\n private listView: HTMLElement | null = null;\n private detailView: HTMLElement | null = null;\n\n constructor() {\n this.listView = document.getElementById('employees-list-view');\n this.detailView = document.getElementById('employee-detail-view');\n\n // Only initialize if we're on the employees page\n if (!this.listView) return;\n\n this.setupListTabs();\n this.setupDetailTabs();\n this.setupChevronNavigation();\n this.setupBackNavigation();\n this.setupHistoryNavigation();\n this.restoreStateFromUrl();\n this.ratesSync = new RatesSyncController();\n }\n\n /**\n * Setup popstate listener for browser back/forward\n */\n private setupHistoryNavigation(): void {\n window.addEventListener('popstate', (e: PopStateEvent) => {\n if (e.state?.employeeKey) {\n this.showDetailViewInternal(e.state.employeeKey);\n } else {\n this.showListViewInternal();\n }\n });\n }\n\n /**\n * Restore view state from URL on page load\n */\n private restoreStateFromUrl(): void {\n const hash = window.location.hash;\n if (hash.startsWith('#employee-')) {\n const employeeKey = hash.substring(1); // Remove #\n this.showDetailViewInternal(employeeKey);\n }\n }\n\n /**\n * Setup tab switching for the list view\n */\n private setupListTabs(): void {\n if (!this.listView) return;\n\n const tabs = this.listView.querySelectorAll('swp-tab-bar > swp-tab[data-tab]');\n\n tabs.forEach(tab => {\n tab.addEventListener('click', () => {\n const targetTab = tab.dataset.tab;\n if (targetTab) {\n this.switchTab(this.listView!, targetTab);\n }\n });\n });\n }\n\n /**\n * Setup tab switching for the detail view\n */\n private setupDetailTabs(): void {\n if (!this.detailView) return;\n\n const tabs = this.detailView.querySelectorAll('swp-tab-bar > swp-tab[data-tab]');\n\n tabs.forEach(tab => {\n tab.addEventListener('click', () => {\n const targetTab = tab.dataset.tab;\n if (targetTab) {\n this.switchTab(this.detailView!, targetTab);\n }\n });\n });\n }\n\n /**\n * Switch to a specific tab within a container\n */\n private switchTab(container: HTMLElement, targetTab: string): void {\n const tabs = container.querySelectorAll('swp-tab-bar > swp-tab[data-tab]');\n const contents = container.querySelectorAll('swp-tab-content[data-tab]');\n\n tabs.forEach(t => {\n t.classList.toggle('active', t.dataset.tab === targetTab);\n });\n\n contents.forEach(content => {\n content.classList.toggle('active', content.dataset.tab === targetTab);\n });\n }\n\n /**\n * Setup row click to show detail view\n * Ignores clicks on action buttons\n */\n private setupChevronNavigation(): void {\n document.addEventListener('click', (e: Event) => {\n const target = e.target as HTMLElement;\n\n // Ignore clicks on action buttons\n if (target.closest('swp-icon-btn') || target.closest('swp-table-actions')) {\n return;\n }\n\n const row = target.closest('swp-employee-row[data-employee-detail]');\n\n if (row) {\n const employeeKey = row.dataset.employeeDetail;\n if (employeeKey) {\n this.showDetailView(employeeKey);\n }\n }\n });\n }\n\n /**\n * Setup back button to return to list view\n */\n private setupBackNavigation(): void {\n document.addEventListener('click', (e: Event) => {\n const target = e.target as HTMLElement;\n const backLink = target.closest('[data-employee-back]');\n\n if (backLink) {\n this.showListView();\n }\n });\n }\n\n /**\n * Show the detail view and hide list view (with history push)\n */\n private showDetailView(employeeKey: string): void {\n // Push state to history\n history.pushState(\n { employeeKey },\n '',\n `#${employeeKey}`\n );\n this.showDetailViewInternal(employeeKey);\n }\n\n /**\n * Show detail view without modifying history (for popstate)\n */\n private showDetailViewInternal(employeeKey: string): void {\n if (this.listView && this.detailView) {\n this.listView.style.display = 'none';\n this.detailView.style.display = 'block';\n this.detailView.dataset.employee = employeeKey;\n\n // Reset to first tab\n this.switchTab(this.detailView, 'general');\n }\n }\n\n /**\n * Show the list view and hide detail view (with history push)\n */\n private showListView(): void {\n // Push state to history (clear hash)\n history.pushState(\n {},\n '',\n window.location.pathname\n );\n this.showListViewInternal();\n }\n\n /**\n * Show list view without modifying history (for popstate)\n */\n private showListViewInternal(): void {\n if (this.listView && this.detailView) {\n this.detailView.style.display = 'none';\n this.listView.style.display = 'block';\n }\n }\n}\n\n/**\n * Rates Sync Controller\n *\n * Syncs changes between the rates drawer and the salary tab cards.\n * Uses ID-based lookups:\n * - Checkbox: id=\"rate-{key}-enabled\"\n * - Text input: id=\"rate-{key}\"\n * - Card row: id=\"card-{key}\"\n */\nclass RatesSyncController {\n private drawer: HTMLElement | null = null;\n\n constructor() {\n this.drawer = document.getElementById('rates-drawer');\n\n if (!this.drawer) return;\n\n this.setupCheckboxListeners();\n this.setupInputListeners();\n }\n\n /**\n * Extract rate key from checkbox ID (e.g., \"rate-normal-enabled\" \u2192 \"normal\")\n */\n private extractRateKey(checkboxId: string): string | null {\n const match = checkboxId.match(/^rate-(.+)-enabled$/);\n return match ? match[1] : null;\n }\n\n /**\n * Setup checkbox change listeners in drawer\n */\n private setupCheckboxListeners(): void {\n if (!this.drawer) return;\n\n this.drawer.addEventListener('change', (e: Event) => {\n const target = e.target as HTMLInputElement;\n if (target.type !== 'checkbox' || !target.id) return;\n\n const rateKey = this.extractRateKey(target.id);\n if (!rateKey) return;\n\n const isChecked = target.checked;\n const row = target.closest('swp-data-row');\n if (!row) return;\n\n // Toggle disabled class in drawer row\n const label = row.querySelector('swp-data-label');\n const input = row.querySelector('swp-data-input');\n if (label) label.classList.toggle('disabled', !isChecked);\n if (input) input.classList.toggle('disabled', !isChecked);\n\n // Toggle visibility in card\n this.toggleCardRow(rateKey, isChecked);\n\n // If enabling, also sync the current value\n if (isChecked) {\n const textInput = document.getElementById(`rate-${rateKey}`) as HTMLInputElement | null;\n if (textInput) {\n this.syncValueToCard(rateKey, textInput.value);\n }\n }\n });\n }\n\n /**\n * Setup input change listeners in drawer\n */\n private setupInputListeners(): void {\n if (!this.drawer) return;\n\n this.drawer.addEventListener('input', (e: Event) => {\n const target = e.target as HTMLInputElement;\n if (target.type !== 'text' || !target.id) return;\n\n // Extract rate key from input ID (e.g., \"rate-normal\" \u2192 \"normal\")\n const match = target.id.match(/^rate-(.+)$/);\n if (!match) return;\n\n const rateKey = match[1];\n // Skip if this matches the checkbox pattern\n if (rateKey.endsWith('-enabled')) return;\n\n this.syncValueToCard(rateKey, target.value);\n });\n }\n\n /**\n * Toggle card row visibility by ID\n */\n private toggleCardRow(rateKey: string, visible: boolean): void {\n const cardRow = document.getElementById(`card-${rateKey}`);\n if (cardRow) {\n cardRow.style.display = visible ? '' : 'none';\n }\n }\n\n /**\n * Format number with 2 decimals using Danish locale (comma as decimal separator)\n */\n private formatNumber(value: string): string {\n // Parse the input (handle both dot and comma as decimal separator)\n const normalized = value.replace(',', '.');\n const num = parseFloat(normalized);\n\n if (isNaN(num)) return value;\n\n // Format with 2 decimals and comma as decimal separator\n return num.toFixed(2).replace('.', ',');\n }\n\n /**\n * Sync value from drawer to card by ID\n */\n private syncValueToCard(rateKey: string, value: string): void {\n const cardInput = document.getElementById(`value-${rateKey}`) as HTMLInputElement | null;\n if (!cardInput) return;\n\n // Get the unit from drawer input container\n const textInput = document.getElementById(`rate-${rateKey}`);\n const inputContainer = textInput?.closest('swp-data-input');\n const unit = inputContainer?.textContent?.trim().replace(value, '').trim() || 'kr';\n\n // Format with 2 decimals\n const formattedValue = this.formatNumber(value);\n cardInput.value = `${formattedValue} ${unit}`;\n }\n}\n", "/**\n * Salon OS App\n *\n * Main application class that orchestrates all UI controllers\n */\n\nimport { SidebarController } from './modules/sidebar';\nimport { DrawerController } from './modules/drawers';\nimport { ThemeController } from './modules/theme';\nimport { SearchController } from './modules/search';\nimport { LockScreenController } from './modules/lockscreen';\nimport { CashController } from './modules/cash';\nimport { EmployeesController } from './modules/employees';\n\n/**\n * Main application class\n */\nexport class App {\n readonly sidebar: SidebarController;\n readonly drawers: DrawerController;\n readonly theme: ThemeController;\n readonly search: SearchController;\n readonly lockScreen: LockScreenController;\n readonly cash: CashController;\n readonly employees: EmployeesController;\n\n constructor() {\n // Initialize controllers\n this.sidebar = new SidebarController();\n this.drawers = new DrawerController();\n this.theme = new ThemeController();\n this.search = new SearchController();\n this.lockScreen = new LockScreenController(this.drawers);\n this.cash = new CashController();\n this.employees = new EmployeesController();\n }\n}\n\n/**\n * Global app instance\n */\nlet app: App;\n\n/**\n * Initialize the application\n */\nfunction init(): void {\n app = new App();\n\n // Expose to window for debugging\n if (typeof window !== 'undefined') {\n (window as unknown as { app: App }).app = app;\n }\n}\n\n// Wait for DOM ready\nif (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', init);\n} else {\n init();\n}\n\nexport { app };\nexport default App;\n"], + "mappings": "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", "names": [] } diff --git a/PlanTempus.Application/wwwroot/ts/modules/employees.ts b/PlanTempus.Application/wwwroot/ts/modules/employees.ts index 9c78e02..d400efd 100644 --- a/PlanTempus.Application/wwwroot/ts/modules/employees.ts +++ b/PlanTempus.Application/wwwroot/ts/modules/employees.ts @@ -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('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}`; + } +}