diff --git a/CLAUDE.md b/CLAUDE.md
index b61619d..0b8bff7 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -8,7 +8,7 @@ PlanTempus is a .NET 9 web application built with ASP.NET Core Razor Pages. It u
## Related Projects
-- **calpoc** = Calendar POC projekt located at `../Calendar` (TypeScript calendar component with offline-first architecture, drag-and-drop, NovaDI, EventBus). When user mentions "calpoc", refer to this folder.
+- **calpoc** = Calendar POC projekt located at `../Calendar/wwwroot` (TypeScript calendar component with offline-first architecture, drag-and-drop, NovaDI, EventBus). When user mentions "calpoc", refer to this folder.
## Build and Development Commands
diff --git a/PlanTempus.Application/Features/Employees/Components/EmployeeDetailSalary/Default.cshtml b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailSalary/Default.cshtml
index 3d1053b..ff5dbc8 100644
--- a/PlanTempus.Application/Features/Employees/Components/EmployeeDetailSalary/Default.cshtml
+++ b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailSalary/Default.cshtml
@@ -52,7 +52,24 @@
-
+
+
+
+ @Model.LabelCommission
+
+
+
+ @Model.LabelProductCommission
+
+
+
+ @Model.LabelServiceCommission
+
+
+
+
+
+
@Model.LabelSupplements
@@ -72,47 +89,111 @@
-
-
-
-
- @Model.LabelCommission
-
-
-
- @Model.LabelProductCommission
-
-
-
- @Model.LabelServiceCommission
-
-
-
-
-
-
-
- @Model.LabelSalaryHistory
-
-
-
- @Model.LabelPeriod
- @Model.LabelGrossSalary
-
-
- @foreach (var item in Model.SalaryHistory)
- {
-
- @item.Period
- @item.GrossSalary
-
-
- }
-
-
+
+
+
+ @Model.LabelSpecifications
+
+
+ @foreach (var spec in Model.Specifications)
+ {
+
+
+
+ @spec.Period
+ @spec.Weeks.Sum(w => w.NormalHours + w.OvertimeHours)t
+
+
+
+ @spec.GrossSalaryFormatted
+ @Model.LabelTotal
+
+
+
+
+
+
+
+
+
+
+ @Model.LabelNormalRate:
+ @spec.Config.HourlyRateFormatted
+
+
+ @Model.LabelWeeklyNorm:
+ @spec.Config.WeeklyHoursFormatted
+
+
+ @Model.LabelOvertimeMultiplier:
+ @spec.Config.OvertimeFormatted
+
+
+ @Model.LabelMinimum:
+ @spec.Config.MinimumFormatted
+
+
+ @Model.LabelProvision:
+ @spec.Config.CommissionFormatted
+
+
+
+
+
+
+
+ @Model.LabelWeek
+ Timer
+ Overtid
+ Ferie
+ Services
+ Produkter
+ Minimum
+ Provision
+ I alt
+
+ @foreach (var week in spec.Weeks)
+ {
+
+ Uge @week.WeekNumber
+ @week.NormalHoursFormatted
+ @week.OvertimeHoursFormatted
+ @week.VacationDaysFormatted
+ @week.ServiceRevenueFormatted
+ @week.ProductRevenueFormatted
+ @week.MinimumThresholdFormatted
+ @week.CommissionFormatted
+ @week.TotalPayFormatted
+
+ }
+
+ TOTAL
+ @(spec.Weeks.Sum(w => w.NormalHours))t
+ @(spec.Weeks.Sum(w => w.OvertimeHours))t
+ @(spec.Weeks.Sum(w => w.VacationDays)) dg
+ @(spec.Weeks.Sum(w => w.ServiceRevenue).ToString("N0", System.Globalization.CultureInfo.GetCultureInfo("da-DK"))) kr
+ @(spec.Weeks.Sum(w => w.ProductRevenue).ToString("N0", System.Globalization.CultureInfo.GetCultureInfo("da-DK"))) kr
+ -
+ @(spec.Weeks.Sum(w => w.Commission).ToString("N2", System.Globalization.CultureInfo.GetCultureInfo("da-DK"))) kr
+ @(spec.Weeks.Sum(w => w.TotalPay).ToString("N2", System.Globalization.CultureInfo.GetCultureInfo("da-DK"))) kr
+
+
+
+
+
+
+ Se lønberegning
+
+
+
+
+ }
+
+
+
@@ -177,7 +258,7 @@
-
+
@Model.LabelSupplements
diff --git a/PlanTempus.Application/Features/Employees/Components/EmployeeDetailSalary/EmployeeDetailSalaryViewComponent.cs b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailSalary/EmployeeDetailSalaryViewComponent.cs
index e9a5b52..8d1a5c8 100644
--- a/PlanTempus.Application/Features/Employees/Components/EmployeeDetailSalary/EmployeeDetailSalaryViewComponent.cs
+++ b/PlanTempus.Application/Features/Employees/Components/EmployeeDetailSalary/EmployeeDetailSalaryViewComponent.cs
@@ -1,3 +1,4 @@
+using System.Text.Json;
using Microsoft.AspNetCore.Mvc;
using PlanTempus.Application.Features.Localization.Services;
@@ -6,18 +7,23 @@ namespace PlanTempus.Application.Features.Employees.Components;
public class EmployeeDetailSalaryViewComponent : ViewComponent
{
private readonly ILocalizationService _localization;
+ private readonly IWebHostEnvironment _environment;
- public EmployeeDetailSalaryViewComponent(ILocalizationService localization)
+ public EmployeeDetailSalaryViewComponent(ILocalizationService localization, IWebHostEnvironment environment)
{
_localization = localization;
+ _environment = environment;
}
public IViewComponentResult Invoke(string key)
{
var employee = EmployeeDetailCatalog.Get(key);
+ var salaryData = LoadSalarySpecifications();
var model = new EmployeeDetailSalaryViewModel
{
+ // Salary specifications from JSON
+ Specifications = salaryData.Specifications,
// Data
BankAccount = employee.BankAccount,
TaxCard = employee.TaxCard,
@@ -89,7 +95,21 @@ public class EmployeeDetailSalaryViewComponent : ViewComponent
ProductCommissionValue = employee.ProductCommissionValue,
ServiceCommissionValue = employee.ServiceCommissionValue,
- // Mock salary history
+ // Labels for specifications
+ LabelSpecifications = _localization.Get("employees.detail.salary.specifications"),
+ LabelWeek = _localization.Get("employees.detail.salary.week"),
+ LabelNormalHours = _localization.Get("employees.detail.salary.normalhours"),
+ LabelOvertimeHours = _localization.Get("employees.detail.salary.overtimehours"),
+ LabelVacationDays = _localization.Get("employees.detail.salary.vacationdays"),
+ LabelServiceRevenue = _localization.Get("employees.detail.salary.servicerevenue"),
+ LabelProductRevenue = _localization.Get("employees.detail.salary.productrevenue"),
+ LabelMinimumThreshold = _localization.Get("employees.detail.salary.minimumthreshold"),
+ LabelTotal = _localization.Get("employees.detail.salary.total"),
+ LabelWeeklyNorm = _localization.Get("employees.detail.salary.weeklynorm"),
+ LabelOvertimeMultiplier = _localization.Get("employees.detail.salary.overtimemultiplier"),
+ LabelMinimum = _localization.Get("employees.detail.salary.minimum"),
+
+ // Mock salary history (kept for backwards compatibility)
SalaryHistory = new List
{
new() { Period = "Januar 2026", GrossSalary = "34.063,50 kr" },
@@ -102,10 +122,23 @@ public class EmployeeDetailSalaryViewComponent : ViewComponent
return View(model);
}
+
+ private SalarySpecificationRoot LoadSalarySpecifications()
+ {
+ var jsonPath = Path.Combine(_environment.ContentRootPath, "Features", "Employees", "Data", "salarySpecificationMock.json");
+ var json = System.IO.File.ReadAllText(jsonPath);
+ return JsonSerializer.Deserialize(json, new JsonSerializerOptions
+ {
+ PropertyNameCaseInsensitive = true
+ })!;
+ }
}
public class EmployeeDetailSalaryViewModel
{
+ // Salary Specifications (from JSON)
+ public List Specifications { get; init; } = new();
+
// Data
public required string BankAccount { get; init; }
public required string TaxCard { get; init; }
@@ -160,6 +193,20 @@ public class EmployeeDetailSalaryViewModel
public required string LabelProductCommissionFull { get; init; }
public required string LabelServiceCommissionFull { get; init; }
+ // Labels for specifications accordion
+ public required string LabelSpecifications { get; init; }
+ public required string LabelWeek { get; init; }
+ public required string LabelNormalHours { get; init; }
+ public required string LabelOvertimeHours { get; init; }
+ public required string LabelVacationDays { get; init; }
+ public required string LabelServiceRevenue { get; init; }
+ public required string LabelProductRevenue { get; init; }
+ public required string LabelMinimumThreshold { get; init; }
+ public required string LabelTotal { get; init; }
+ public required string LabelWeeklyNorm { get; init; }
+ public required string LabelOvertimeMultiplier { get; init; }
+ public required string LabelMinimum { get; init; }
+
// Rate values (for drawer inputs)
public required string NormalRateValue { get; init; }
public required string OvertimeRateValue { get; init; }
@@ -186,3 +233,70 @@ public class SalaryHistoryItem
public required string Period { get; init; }
public required string GrossSalary { get; init; }
}
+
+// DTOs for salary specification JSON
+public class SalarySpecificationRoot
+{
+ public List Specifications { get; init; } = new();
+}
+
+public class SalarySpecificationDto
+{
+ public required string Period { get; init; }
+ public required string PeriodKey { get; init; }
+ public decimal GrossSalary { get; init; }
+ public decimal BasePay { get; init; }
+ public decimal OvertimePay { get; init; }
+ public decimal ServiceCommission { get; init; }
+ public decimal ProductCommission { get; init; }
+ public SalaryConfigDto Config { get; init; } = new();
+ public List Weeks { get; init; } = new();
+
+ // Formatted values for display
+ public string GrossSalaryFormatted => GrossSalary.ToString("N2", System.Globalization.CultureInfo.GetCultureInfo("da-DK")) + " kr";
+ public string BasePayFormatted => BasePay.ToString("N2", System.Globalization.CultureInfo.GetCultureInfo("da-DK")) + " kr";
+ public string OvertimePayFormatted => OvertimePay.ToString("N2", System.Globalization.CultureInfo.GetCultureInfo("da-DK")) + " kr";
+ public string TotalCommission => (ServiceCommission + ProductCommission).ToString("N2", System.Globalization.CultureInfo.GetCultureInfo("da-DK")) + " kr";
+}
+
+public class SalaryConfigDto
+{
+ public decimal HourlyRate { get; init; }
+ public int WeeklyHours { get; init; }
+ public decimal OvertimeMultiplier { get; init; }
+ public decimal MinimumPerHour { get; init; }
+ public int ServiceCommissionPct { get; init; }
+ public int ProductCommissionPct { get; init; }
+
+ // Formatted values
+ public string HourlyRateFormatted => HourlyRate.ToString("N0", System.Globalization.CultureInfo.GetCultureInfo("da-DK")) + " kr";
+ public string WeeklyHoursFormatted => WeeklyHours + "t/uge";
+ public string OvertimeFormatted => "+" + Math.Round((OvertimeMultiplier - 1) * 100) + "%";
+ public string MinimumFormatted => MinimumPerHour.ToString("N0", System.Globalization.CultureInfo.GetCultureInfo("da-DK")) + " kr/time";
+ public string CommissionFormatted => ServiceCommissionPct + "% services · " + ProductCommissionPct + "% produkter";
+}
+
+public class SalaryWeekDto
+{
+ public int WeekNumber { get; init; }
+ public int NormalHours { get; init; }
+ public int OvertimeHours { get; init; }
+ public int VacationDays { get; init; }
+ public decimal ServiceRevenue { get; init; }
+ public decimal ProductRevenue { get; init; }
+ public decimal MinimumThreshold { get; init; }
+ public decimal Commission { get; init; }
+ public decimal TotalPay { get; init; }
+
+ // Formatted values
+ public string NormalHoursFormatted => NormalHours + "t";
+ public string OvertimeHoursFormatted => OvertimeHours + "t";
+ public string VacationDaysFormatted => VacationDays + " dg";
+ public string ServiceRevenueFormatted => ServiceRevenue.ToString("N0", System.Globalization.CultureInfo.GetCultureInfo("da-DK")) + " kr";
+ public string ProductRevenueFormatted => ProductRevenue.ToString("N0", System.Globalization.CultureInfo.GetCultureInfo("da-DK")) + " kr";
+ public string MinimumThresholdFormatted => MinimumThreshold.ToString("N0", System.Globalization.CultureInfo.GetCultureInfo("da-DK")) + " kr";
+ public string CommissionFormatted => Commission.ToString("N2", System.Globalization.CultureInfo.GetCultureInfo("da-DK")) + " kr";
+ public string TotalPayFormatted => TotalPay.ToString("N2", System.Globalization.CultureInfo.GetCultureInfo("da-DK")) + " kr";
+ public bool HasOvertime => OvertimeHours > 0;
+ public bool HasCommission => Commission > 0;
+}
diff --git a/PlanTempus.Application/Features/Employees/Data/salarySpecificationMock.json b/PlanTempus.Application/Features/Employees/Data/salarySpecificationMock.json
new file mode 100644
index 0000000..578d88e
--- /dev/null
+++ b/PlanTempus.Application/Features/Employees/Data/salarySpecificationMock.json
@@ -0,0 +1,330 @@
+{
+ "specifications": [
+ {
+ "period": "Januar 2026",
+ "periodKey": "2026-01",
+ "grossSalary": 34063.50,
+ "basePay": 28800.00,
+ "overtimePay": 1800.00,
+ "serviceCommission": 2463.50,
+ "productCommission": 1000.00,
+ "config": {
+ "hourlyRate": 185,
+ "weeklyHours": 37,
+ "overtimeMultiplier": 1.5,
+ "minimumPerHour": 220,
+ "serviceCommissionPct": 15,
+ "productCommissionPct": 15
+ },
+ "weeks": [
+ {
+ "weekNumber": 1,
+ "normalHours": 37,
+ "overtimeHours": 2,
+ "vacationDays": 0,
+ "serviceRevenue": 12500,
+ "productRevenue": 3200,
+ "minimumThreshold": 8580,
+ "commission": 1068.00,
+ "totalPay": 8643.00
+ },
+ {
+ "weekNumber": 2,
+ "normalHours": 37,
+ "overtimeHours": 0,
+ "vacationDays": 0,
+ "serviceRevenue": 10800,
+ "productRevenue": 2100,
+ "minimumThreshold": 8140,
+ "commission": 712.50,
+ "totalPay": 7558.00
+ },
+ {
+ "weekNumber": 3,
+ "normalHours": 37,
+ "overtimeHours": 3,
+ "vacationDays": 0,
+ "serviceRevenue": 14200,
+ "productRevenue": 4500,
+ "minimumThreshold": 8800,
+ "commission": 1485.00,
+ "totalPay": 9318.00
+ },
+ {
+ "weekNumber": 4,
+ "normalHours": 32,
+ "overtimeHours": 0,
+ "vacationDays": 1,
+ "serviceRevenue": 9200,
+ "productRevenue": 1800,
+ "minimumThreshold": 7040,
+ "commission": 594.00,
+ "totalPay": 6544.50
+ }
+ ]
+ },
+ {
+ "period": "December 2025",
+ "periodKey": "2025-12",
+ "grossSalary": 31845.00,
+ "basePay": 27380.00,
+ "overtimePay": 555.00,
+ "serviceCommission": 2910.00,
+ "productCommission": 1000.00,
+ "config": {
+ "hourlyRate": 185,
+ "weeklyHours": 37,
+ "overtimeMultiplier": 1.5,
+ "minimumPerHour": 220,
+ "serviceCommissionPct": 15,
+ "productCommissionPct": 15
+ },
+ "weeks": [
+ {
+ "weekNumber": 49,
+ "normalHours": 37,
+ "overtimeHours": 0,
+ "vacationDays": 0,
+ "serviceRevenue": 11200,
+ "productRevenue": 2800,
+ "minimumThreshold": 8140,
+ "commission": 879.00,
+ "totalPay": 7724.00
+ },
+ {
+ "weekNumber": 50,
+ "normalHours": 37,
+ "overtimeHours": 2,
+ "vacationDays": 0,
+ "serviceRevenue": 13500,
+ "productRevenue": 3600,
+ "minimumThreshold": 8580,
+ "commission": 1278.00,
+ "totalPay": 8833.00
+ },
+ {
+ "weekNumber": 51,
+ "normalHours": 37,
+ "overtimeHours": 0,
+ "vacationDays": 0,
+ "serviceRevenue": 12800,
+ "productRevenue": 2400,
+ "minimumThreshold": 8140,
+ "commission": 1059.00,
+ "totalPay": 7904.00
+ },
+ {
+ "weekNumber": 52,
+ "normalHours": 30,
+ "overtimeHours": 0,
+ "vacationDays": 2,
+ "serviceRevenue": 8500,
+ "productRevenue": 1500,
+ "minimumThreshold": 6600,
+ "commission": 510.00,
+ "totalPay": 6060.00
+ }
+ ]
+ },
+ {
+ "period": "November 2025",
+ "periodKey": "2025-11",
+ "grossSalary": 33290.25,
+ "basePay": 27380.00,
+ "overtimePay": 1110.00,
+ "serviceCommission": 3550.25,
+ "productCommission": 1250.00,
+ "config": {
+ "hourlyRate": 185,
+ "weeklyHours": 37,
+ "overtimeMultiplier": 1.5,
+ "minimumPerHour": 220,
+ "serviceCommissionPct": 15,
+ "productCommissionPct": 15
+ },
+ "weeks": [
+ {
+ "weekNumber": 45,
+ "normalHours": 37,
+ "overtimeHours": 0,
+ "vacationDays": 0,
+ "serviceRevenue": 11800,
+ "productRevenue": 3100,
+ "minimumThreshold": 8140,
+ "commission": 1014.00,
+ "totalPay": 7859.00
+ },
+ {
+ "weekNumber": 46,
+ "normalHours": 37,
+ "overtimeHours": 4,
+ "vacationDays": 0,
+ "serviceRevenue": 15200,
+ "productRevenue": 4200,
+ "minimumThreshold": 9020,
+ "commission": 1557.00,
+ "totalPay": 9667.00
+ },
+ {
+ "weekNumber": 47,
+ "normalHours": 37,
+ "overtimeHours": 0,
+ "vacationDays": 0,
+ "serviceRevenue": 10500,
+ "productRevenue": 2600,
+ "minimumThreshold": 8140,
+ "commission": 744.00,
+ "totalPay": 7589.00
+ },
+ {
+ "weekNumber": 48,
+ "normalHours": 37,
+ "overtimeHours": 0,
+ "vacationDays": 0,
+ "serviceRevenue": 12300,
+ "productRevenue": 2900,
+ "minimumThreshold": 8140,
+ "commission": 1059.00,
+ "totalPay": 7904.00
+ }
+ ]
+ },
+ {
+ "period": "Oktober 2025",
+ "periodKey": "2025-10",
+ "grossSalary": 32156.75,
+ "basePay": 27380.00,
+ "overtimePay": 832.50,
+ "serviceCommission": 2944.25,
+ "productCommission": 1000.00,
+ "config": {
+ "hourlyRate": 185,
+ "weeklyHours": 37,
+ "overtimeMultiplier": 1.5,
+ "minimumPerHour": 220,
+ "serviceCommissionPct": 15,
+ "productCommissionPct": 15
+ },
+ "weeks": [
+ {
+ "weekNumber": 40,
+ "normalHours": 37,
+ "overtimeHours": 0,
+ "vacationDays": 0,
+ "serviceRevenue": 10200,
+ "productRevenue": 2400,
+ "minimumThreshold": 8140,
+ "commission": 669.00,
+ "totalPay": 7514.00
+ },
+ {
+ "weekNumber": 41,
+ "normalHours": 37,
+ "overtimeHours": 3,
+ "vacationDays": 0,
+ "serviceRevenue": 13800,
+ "productRevenue": 3200,
+ "minimumThreshold": 8800,
+ "commission": 1230.00,
+ "totalPay": 9063.00
+ },
+ {
+ "weekNumber": 42,
+ "normalHours": 0,
+ "overtimeHours": 0,
+ "vacationDays": 5,
+ "serviceRevenue": 0,
+ "productRevenue": 0,
+ "minimumThreshold": 0,
+ "commission": 0,
+ "totalPay": 0
+ },
+ {
+ "weekNumber": 43,
+ "normalHours": 37,
+ "overtimeHours": 0,
+ "vacationDays": 0,
+ "serviceRevenue": 11500,
+ "productRevenue": 2700,
+ "minimumThreshold": 8140,
+ "commission": 909.00,
+ "totalPay": 7754.00
+ },
+ {
+ "weekNumber": 44,
+ "normalHours": 37,
+ "overtimeHours": 0,
+ "vacationDays": 0,
+ "serviceRevenue": 10900,
+ "productRevenue": 2500,
+ "minimumThreshold": 8140,
+ "commission": 789.00,
+ "totalPay": 7634.00
+ }
+ ]
+ },
+ {
+ "period": "September 2025",
+ "periodKey": "2025-09",
+ "grossSalary": 34520.00,
+ "basePay": 27380.00,
+ "overtimePay": 1387.50,
+ "serviceCommission": 4252.50,
+ "productCommission": 1500.00,
+ "config": {
+ "hourlyRate": 185,
+ "weeklyHours": 37,
+ "overtimeMultiplier": 1.5,
+ "minimumPerHour": 220,
+ "serviceCommissionPct": 15,
+ "productCommissionPct": 15
+ },
+ "weeks": [
+ {
+ "weekNumber": 36,
+ "normalHours": 37,
+ "overtimeHours": 2,
+ "vacationDays": 0,
+ "serviceRevenue": 14500,
+ "productRevenue": 4800,
+ "minimumThreshold": 8580,
+ "commission": 1608.00,
+ "totalPay": 9163.00
+ },
+ {
+ "weekNumber": 37,
+ "normalHours": 37,
+ "overtimeHours": 3,
+ "vacationDays": 0,
+ "serviceRevenue": 16200,
+ "productRevenue": 5100,
+ "minimumThreshold": 8800,
+ "commission": 1875.00,
+ "totalPay": 9708.00
+ },
+ {
+ "weekNumber": 38,
+ "normalHours": 37,
+ "overtimeHours": 0,
+ "vacationDays": 0,
+ "serviceRevenue": 12100,
+ "productRevenue": 3200,
+ "minimumThreshold": 8140,
+ "commission": 1074.00,
+ "totalPay": 7919.00
+ },
+ {
+ "weekNumber": 39,
+ "normalHours": 37,
+ "overtimeHours": 0,
+ "vacationDays": 0,
+ "serviceRevenue": 11800,
+ "productRevenue": 2900,
+ "minimumThreshold": 8140,
+ "commission": 984.00,
+ "totalPay": 7829.00
+ }
+ ]
+ }
+ ]
+}
diff --git a/PlanTempus.Application/Features/Employees/Pages/SalarySpecification.cshtml b/PlanTempus.Application/Features/Employees/Pages/SalarySpecification.cshtml
new file mode 100644
index 0000000..b667108
--- /dev/null
+++ b/PlanTempus.Application/Features/Employees/Pages/SalarySpecification.cshtml
@@ -0,0 +1,461 @@
+@page "/medarbejdere/loenspecifikation/{period}"
+@model PlanTempus.Application.Features.Employees.Pages.SalarySpecificationModel
+@{
+ Layout = null;
+ var spec = Model.Specification!;
+ var culture = System.Globalization.CultureInfo.GetCultureInfo("da-DK");
+}
+
+
+
+
+
+ Lønspecifikation – @spec.Period
+
+
+
+
+
+
+
+
+
+
+
+
Lønspecifikation
+
Periode: @spec.Period
+
+
+
+
+
+
+
+
Bruttoløn (@spec.Period)
+
@spec.GrossSalary.ToString("N2", culture) kr
+
+
+
+
+
+
+
+
Samlet lønopgørelse
+
+
+
+
+
+ | Løndel |
+ Beløb |
+
+
+
+
+ | Grundløn inkl. overarbejde |
+ @((spec.BasePay + spec.OvertimePay).ToString("N2", culture)) kr |
+
+
+ | Provision i alt |
+ @((spec.ServiceCommission + spec.ProductCommission).ToString("N2", culture)) kr |
+
+
+ | Bruttoløn |
+ @spec.GrossSalary.ToString("N2", culture) kr |
+
+
+
+
+
+
+
+
+
Satser
+
+
+
+
+
+ | Type |
+ Værdi |
+
+
+
+
+ | Timeløn |
+ @spec.Config.HourlyRate.ToString("N0", culture) kr |
+
+
+ | Normtid |
+ @spec.Config.WeeklyHours t/uge |
+
+
+ | Overtidstillæg |
+ @spec.Config.OvertimeFormatted |
+
+
+ | Minimum pr. time |
+ @spec.Config.MinimumPerHour.ToString("N0", culture) kr |
+
+
+
+
+
+
+
+
+
+
+
Hurtigt resumé
+
+
+
+
+
+ | Nøglepunkt |
+ Værdi |
+
+
+
+
+ | Normaltimer |
+ @spec.Weeks.Sum(w => w.NormalHours) t |
+
+
+ | Overarbejde |
+ @spec.Weeks.Sum(w => w.OvertimeHours) t |
+
+
+ | Provision (services + produkter) |
+ @((spec.ServiceCommission + spec.ProductCommission).ToString("N2", culture)) kr |
+
+
+ | Feriedage |
+ @spec.Weeks.Sum(w => w.VacationDays) dage |
+
+
+
+
+
+
+
+
Overblik
+
Lønspecifikation · @spec.Period
+
+
+
+
+
+
+
+
Arbejdstid pr. uge
+
+
+
+
+
+ | Uge |
+ Normaltimer |
+ Overtid |
+ Services |
+ Produkter |
+ Provision |
+ I alt |
+
+
+
+ @foreach (var week in spec.Weeks)
+ {
+
+ | Uge @week.WeekNumber |
+ @week.NormalHours t |
+ @week.OvertimeHours t |
+ @week.ServiceRevenue.ToString("N0", culture) kr |
+ @week.ProductRevenue.ToString("N0", culture) kr |
+ @week.Commission.ToString("N2", culture) kr |
+ @week.TotalPay.ToString("N2", culture) kr |
+
+ }
+
+ | I alt |
+ @spec.Weeks.Sum(w => w.NormalHours) t |
+ @spec.Weeks.Sum(w => w.OvertimeHours) t |
+ @spec.Weeks.Sum(w => w.ServiceRevenue).ToString("N0", culture) kr |
+ @spec.Weeks.Sum(w => w.ProductRevenue).ToString("N0", culture) kr |
+ @spec.Weeks.Sum(w => w.Commission).ToString("N2", culture) kr |
+ @spec.Weeks.Sum(w => w.TotalPay).ToString("N2", culture) kr |
+
+
+
+
+ Satser: Normal @spec.Config.HourlyRate.ToString("N0", culture) kr/time.
+ Overtid (@((spec.Config.OvertimeMultiplier - 1) * 100)%) @((spec.Config.HourlyRate * spec.Config.OvertimeMultiplier).ToString("N2", culture)) kr/time.
+
+
+
+
+
+
+
Provision
+
+
+
+ Services: @spec.Config.ServiceCommissionPct% af omsætning over minimum (@spec.Config.MinimumPerHour.ToString("N0", culture) kr/time).
+ Produkter: @spec.Config.ProductCommissionPct% af salg.
+
+
+
+
+ | Uge |
+ Service oms. |
+ Produkt oms. |
+ Minimum |
+ Provision |
+
+
+
+ @foreach (var week in spec.Weeks)
+ {
+
+ | Uge @week.WeekNumber |
+ @week.ServiceRevenue.ToString("N0", culture) kr |
+ @week.ProductRevenue.ToString("N0", culture) kr |
+ @week.MinimumThreshold.ToString("N0", culture) kr |
+ @week.Commission.ToString("N2", culture) kr |
+
+ }
+
+ | I alt |
+ @spec.Weeks.Sum(w => w.ServiceRevenue).ToString("N0", culture) kr |
+ @spec.Weeks.Sum(w => w.ProductRevenue).ToString("N0", culture) kr |
+ - |
+ @spec.Weeks.Sum(w => w.Commission).ToString("N2", culture) kr |
+
+
+
+
+
+
+
+
Detaljer
+
Lønspecifikation · @spec.Period
+
+
+
+
diff --git a/PlanTempus.Application/Features/Employees/Pages/SalarySpecification.cshtml.cs b/PlanTempus.Application/Features/Employees/Pages/SalarySpecification.cshtml.cs
new file mode 100644
index 0000000..a45d787
--- /dev/null
+++ b/PlanTempus.Application/Features/Employees/Pages/SalarySpecification.cshtml.cs
@@ -0,0 +1,46 @@
+using System.Text.Json;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+using PlanTempus.Application.Features.Employees.Components;
+
+namespace PlanTempus.Application.Features.Employees.Pages;
+
+public class SalarySpecificationModel : PageModel
+{
+ private readonly IWebHostEnvironment _environment;
+
+ public SalarySpecificationModel(IWebHostEnvironment environment)
+ {
+ _environment = environment;
+ }
+
+ public SalarySpecificationDto? Specification { get; private set; }
+ public string EmployeeName { get; private set; } = "Emma Larsen";
+ public string EmployeeNumber { get; private set; } = "EMP-001";
+ public string Department { get; private set; } = "Frisør";
+ public string EmploymentType { get; private set; } = "Fuldtid (37 t/uge)";
+
+ public IActionResult OnGet(string period)
+ {
+ var specs = LoadSalarySpecifications();
+ Specification = specs.FirstOrDefault(s => s.PeriodKey == period);
+
+ if (Specification == null)
+ {
+ return NotFound();
+ }
+
+ return Page();
+ }
+
+ private List LoadSalarySpecifications()
+ {
+ var jsonPath = Path.Combine(_environment.ContentRootPath, "Features", "Employees", "Data", "salarySpecificationMock.json");
+ var json = System.IO.File.ReadAllText(jsonPath);
+ var root = JsonSerializer.Deserialize(json, new JsonSerializerOptions
+ {
+ PropertyNameCaseInsensitive = true
+ });
+ return root?.Specifications ?? new List();
+ }
+}
diff --git a/PlanTempus.Application/Features/Localization/Translations/da.json b/PlanTempus.Application/Features/Localization/Translations/da.json
index b310564..6109947 100644
--- a/PlanTempus.Application/Features/Localization/Translations/da.json
+++ b/PlanTempus.Application/Features/Localization/Translations/da.json
@@ -494,7 +494,19 @@
"saturdaysupplementfull": "8-21 Lørdage (udenfor arbejdstid)",
"commission": "Provisionsberegning",
"productcommissionfull": "Provision på produktsalg",
- "servicecommissionfull": "Provision på servicesalg"
+ "servicecommissionfull": "Provision på servicesalg",
+ "specifications": "Lønspecifikationer",
+ "week": "Uge",
+ "normalhours": "Normtimer",
+ "overtimehours": "Overtid",
+ "vacationdays": "Feriedage",
+ "servicerevenue": "Services",
+ "productrevenue": "Produkter",
+ "minimumthreshold": "Minimum",
+ "total": "I alt",
+ "weeklynorm": "Normtid",
+ "overtimemultiplier": "Overtid",
+ "minimum": "Minimum"
},
"hr": {
"contractdocuments": "Kontrakt & Dokumenter",
diff --git a/PlanTempus.Application/Features/Localization/Translations/en.json b/PlanTempus.Application/Features/Localization/Translations/en.json
index a59a76d..b7adf82 100644
--- a/PlanTempus.Application/Features/Localization/Translations/en.json
+++ b/PlanTempus.Application/Features/Localization/Translations/en.json
@@ -494,7 +494,19 @@
"saturdaysupplementfull": "8-21 Saturdays (outside working hours)",
"commission": "Commission calculation",
"productcommissionfull": "Commission on product sales",
- "servicecommissionfull": "Commission on service sales"
+ "servicecommissionfull": "Commission on service sales",
+ "specifications": "Salary specifications",
+ "week": "Week",
+ "normalhours": "Normal hours",
+ "overtimehours": "Overtime",
+ "vacationdays": "Vacation days",
+ "servicerevenue": "Services",
+ "productrevenue": "Products",
+ "minimumthreshold": "Minimum",
+ "total": "Total",
+ "weeklynorm": "Weekly norm",
+ "overtimemultiplier": "Overtime",
+ "minimum": "Minimum"
},
"hr": {
"contractdocuments": "Contract & Documents",
diff --git a/PlanTempus.Application/Features/_Shared/Pages/_Layout.cshtml b/PlanTempus.Application/Features/_Shared/Pages/_Layout.cshtml
index e4bd330..9a6e870 100644
--- a/PlanTempus.Application/Features/_Shared/Pages/_Layout.cshtml
+++ b/PlanTempus.Application/Features/_Shared/Pages/_Layout.cshtml
@@ -28,6 +28,7 @@
+
diff --git a/PlanTempus.Application/wwwroot/css/COMPONENT-CATALOG.md b/PlanTempus.Application/wwwroot/css/COMPONENT-CATALOG.md
index 47a5727..6e1fd60 100644
--- a/PlanTempus.Application/wwwroot/css/COMPONENT-CATALOG.md
+++ b/PlanTempus.Application/wwwroot/css/COMPONENT-CATALOG.md
@@ -572,6 +572,50 @@ Dashed border knap til tilføjelse af elementer.
---
+## Accordion (accordion.css)
+
+Genbrugeligt accordion component med expand/collapse animation og single-open behavior.
+
+```html
+
+
+
+
+ Titel
+ Subtitle
+
+
+
+ 1.234 kr
+ Label
+
+
+
+
+
+
+
+
+
+
+
+```
+
+**Features:**
+- Single-open behavior (kun en udvidet ad gangen)
+- Smooth expand/collapse animation (250ms/200ms)
+- Caret icon roterer 180 ved expand
+- Config row (`swp-config-row`) til key-value info
+
+**TypeScript:**
+```typescript
+import { Accordion } from './modules/accordion';
+
+const accordion = new Accordion('#my-accordion', { singleOpen: true });
+```
+
+---
+
## Fil Reference
| Fil | Indhold |
@@ -582,7 +626,8 @@ Dashed border knap til tilføjelse af elementer.
| `components.css` | Buttons, badges, cards, section-label, add-button, avatars, icon-btn, **swp-data-table** |
| `stats.css` | Stat cards, stat rows |
| `tabs.css` | Tab bar, tab content |
-| `employees.css` | User info, edit forms, document lists, context styles (.employees-list, .salary-history, .stats-bookings) |
+| `accordion.css` | Accordion component (swp-accordion, swp-accordion-item, expand/collapse) |
+| `employees.css` | User info, edit forms, document lists, context styles (.employees-list, .salary-history, .salary-specifications, .stats-bookings) |
| `account.css` | Account/billing styles, context styles (.invoice-history) |
| `bookings.css` | Booking list items |
| `notifications.css` | Notification items |
diff --git a/PlanTempus.Application/wwwroot/css/accordion.css b/PlanTempus.Application/wwwroot/css/accordion.css
new file mode 100644
index 0000000..7391157
--- /dev/null
+++ b/PlanTempus.Application/wwwroot/css/accordion.css
@@ -0,0 +1,203 @@
+/**
+ * Accordion Component
+ *
+ * Generic reusable accordion with expand/collapse behavior.
+ * Based on POC employee-card pattern from poc-loen-provision.html.
+ *
+ * Usage:
+ *
+ *
+ *
+ *
+ * Title
+ * Subtitle
+ *
+ *
+ *
+ * Value
+ * Label
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ */
+
+/* ===========================================
+ ACCORDION CONTAINER
+ =========================================== */
+swp-accordion {
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-4);
+}
+
+/* ===========================================
+ ACCORDION ITEM
+ =========================================== */
+swp-accordion-item {
+ display: block;
+ background: var(--color-surface);
+ border-radius: var(--radius-md);
+ border: 1px solid var(--color-border);
+ overflow: hidden;
+}
+
+/* ===========================================
+ ACCORDION HEADER (clickable)
+ =========================================== */
+swp-accordion-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: var(--spacing-5) var(--spacing-6);
+ cursor: pointer;
+ transition: background var(--transition-fast);
+ user-select: none;
+}
+
+swp-accordion-header:hover {
+ background: var(--color-background-alt);
+}
+
+/* Info section (left side) */
+swp-accordion-info {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ min-width: 0;
+}
+
+swp-accordion-title {
+ font-size: var(--font-size-base);
+ font-weight: var(--font-weight-semibold);
+ color: var(--color-text);
+}
+
+swp-accordion-meta {
+ font-size: var(--font-size-sm);
+ color: var(--color-text-secondary);
+}
+
+/* Summary section (right side values) */
+swp-accordion-summary {
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-6);
+}
+
+swp-summary-item {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ gap: 2px;
+}
+
+swp-summary-value {
+ font-size: var(--font-size-lg);
+ font-weight: var(--font-weight-semibold);
+ font-family: var(--font-mono);
+ color: var(--color-text);
+}
+
+swp-summary-label {
+ font-size: 10px;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ color: var(--color-text-secondary);
+}
+
+/* Toggle icon */
+swp-accordion-toggle {
+ width: 24px;
+ height: 24px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--color-text-secondary);
+ transition: transform 200ms ease;
+ margin-left: var(--spacing-4);
+ flex-shrink: 0;
+}
+
+swp-accordion-toggle i {
+ font-size: 20px;
+}
+
+/* Expanded state - rotate toggle */
+swp-accordion-item.expanded swp-accordion-toggle {
+ transform: rotate(180deg);
+}
+
+/* ===========================================
+ ACCORDION CONTENT
+ =========================================== */
+swp-accordion-content {
+ display: none;
+ border-top: 1px solid var(--color-border);
+ overflow: hidden;
+}
+
+/* Expanded state - show content */
+swp-accordion-item.expanded swp-accordion-content {
+ display: block;
+}
+
+/* ===========================================
+ CONFIG ROW (inside accordion content)
+ =========================================== */
+swp-config-row {
+ display: flex;
+ gap: var(--spacing-6);
+ padding: var(--spacing-5) var(--spacing-6);
+ background: var(--color-background-alt);
+ border-bottom: 1px solid var(--color-border);
+ flex-wrap: wrap;
+}
+
+swp-config-item {
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-2);
+ font-size: var(--font-size-sm);
+}
+
+swp-config-label {
+ color: var(--color-text-secondary);
+}
+
+swp-config-value {
+ font-weight: var(--font-weight-medium);
+ color: var(--color-text);
+}
+
+swp-config-value.mono {
+ font-family: var(--font-mono);
+}
+
+/* ===========================================
+ ACCORDION TABLE WRAPPER
+ =========================================== */
+swp-accordion-table {
+ display: block;
+ padding: var(--spacing-6);
+}
+
+/* ===========================================
+ ACCORDION FOOTER (inside accordion content)
+ =========================================== */
+swp-accordion-footer {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: var(--spacing-3);
+ padding: var(--spacing-4) var(--spacing-6);
+ background: var(--color-background-alt);
+ border-top: 1px solid var(--color-border);
+}
diff --git a/PlanTempus.Application/wwwroot/css/employees.css b/PlanTempus.Application/wwwroot/css/employees.css
index b05ff19..d13aa53 100644
--- a/PlanTempus.Application/wwwroot/css/employees.css
+++ b/PlanTempus.Application/wwwroot/css/employees.css
@@ -954,3 +954,59 @@ swp-employee-display {
}
}
}
+
+/* ===========================================
+ SALARY SPECIFICATIONS ACCORDION
+ Reuses: swp-accordion (accordion.css), swp-data-table (components.css)
+ =========================================== */
+swp-card.salary-specifications {
+ margin-top: var(--spacing-8);
+}
+
+swp-card.salary-specifications swp-accordion {
+ padding: 0 var(--spacing-6) var(--spacing-6);
+}
+
+/* Table columns for weeks data (9 columns) */
+swp-card.salary-specifications swp-data-table.specification-weeks {
+ grid-template-columns: 70px repeat(8, 1fr);
+}
+
+/* Cell styling */
+swp-card.salary-specifications swp-data-table-cell {
+ padding: var(--spacing-3) var(--spacing-2);
+ font-size: var(--font-size-sm);
+}
+
+swp-card.salary-specifications swp-data-table-cell.mono {
+ font-family: var(--font-mono);
+}
+
+swp-card.salary-specifications swp-data-table-cell.warning {
+ color: var(--color-amber);
+}
+
+swp-card.salary-specifications swp-data-table-cell.highlight {
+ color: var(--color-teal);
+ font-weight: var(--font-weight-medium);
+}
+
+/* Footer row styling */
+swp-card.salary-specifications swp-data-table-footer {
+ display: grid;
+ grid-column: 1 / -1;
+ grid-template-columns: subgrid;
+ background: var(--color-background-alt);
+ border-top: 1px solid var(--color-border);
+}
+
+swp-card.salary-specifications swp-data-table-footer swp-data-table-cell {
+ font-weight: var(--font-weight-semibold);
+ padding: var(--spacing-4) var(--spacing-2);
+}
+
+swp-card.salary-specifications swp-data-table-footer swp-data-table-cell:first-child {
+ font-size: var(--font-size-xs);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
diff --git a/PlanTempus.Application/wwwroot/ts/modules/accordion.ts b/PlanTempus.Application/wwwroot/ts/modules/accordion.ts
new file mode 100644
index 0000000..543e495
--- /dev/null
+++ b/PlanTempus.Application/wwwroot/ts/modules/accordion.ts
@@ -0,0 +1,190 @@
+/**
+ * Accordion Controller
+ *
+ * Generic accordion component with smooth expand/collapse animations.
+ * Supports single-open behavior (only one item expanded at a time).
+ */
+
+export interface AccordionOptions {
+ /** Only allow one item to be expanded at a time (default: true) */
+ singleOpen?: boolean;
+ /** Animation duration for expand in ms (default: 250) */
+ expandDuration?: number;
+ /** Animation duration for collapse in ms (default: 200) */
+ collapseDuration?: number;
+}
+
+export class Accordion {
+ private container: HTMLElement;
+ private singleOpen: boolean;
+ private expandDuration: number;
+ private collapseDuration: number;
+
+ constructor(selector: string | HTMLElement, options: AccordionOptions = {}) {
+ // Get container element
+ if (typeof selector === 'string') {
+ const el = document.querySelector(selector);
+ if (!el) {
+ console.warn(`Accordion: Element not found for selector "${selector}"`);
+ return;
+ }
+ this.container = el;
+ } else {
+ this.container = selector;
+ }
+
+ // Set options with defaults
+ this.singleOpen = options.singleOpen ?? true;
+ this.expandDuration = options.expandDuration ?? 250;
+ this.collapseDuration = options.collapseDuration ?? 200;
+
+ this.setupEventListeners();
+ }
+
+ /**
+ * Setup click listeners on accordion headers
+ */
+ private setupEventListeners(): void {
+ const headers = this.container.querySelectorAll('swp-accordion-header');
+
+ headers.forEach(header => {
+ header.addEventListener('click', (e) => {
+ // Don't toggle if clicking on interactive elements
+ const target = e.target as HTMLElement;
+ if (target.closest('input, button, a, select')) return;
+
+ const item = header.closest('swp-accordion-item');
+ if (item) {
+ this.toggle(item);
+ }
+ });
+ });
+ }
+
+ /**
+ * Toggle an accordion item
+ */
+ toggle(item: HTMLElement): void {
+ const isExpanded = item.classList.contains('expanded');
+
+ if (isExpanded) {
+ this.collapse(item);
+ } else {
+ // Close other items first if single-open mode
+ if (this.singleOpen) {
+ this.container.querySelectorAll('swp-accordion-item.expanded').forEach(otherItem => {
+ if (otherItem !== item) {
+ this.collapse(otherItem);
+ }
+ });
+ }
+ this.expand(item);
+ }
+ }
+
+ /**
+ * Expand an accordion item with animation
+ */
+ expand(item: HTMLElement): void {
+ const content = item.querySelector('swp-accordion-content');
+ const toggle = item.querySelector('swp-accordion-toggle');
+
+ if (!content) return;
+
+ // Add expanded class immediately for CSS to show content
+ item.classList.add('expanded');
+
+ // Animate toggle icon rotation
+ toggle?.animate([
+ { transform: 'rotate(0deg)' },
+ { transform: 'rotate(180deg)' }
+ ], {
+ duration: this.expandDuration,
+ easing: 'ease-out',
+ fill: 'forwards'
+ });
+
+ // Animate content height
+ const height = content.scrollHeight;
+ content.animate([
+ { height: '0px', opacity: 0 },
+ { height: `${height}px`, opacity: 1 }
+ ], {
+ duration: this.expandDuration,
+ easing: 'ease-out',
+ fill: 'forwards'
+ });
+ }
+
+ /**
+ * Collapse an accordion item with animation
+ */
+ collapse(item: HTMLElement): void {
+ const content = item.querySelector('swp-accordion-content');
+ const toggle = item.querySelector('swp-accordion-toggle');
+
+ if (!content) return;
+
+ // Animate toggle icon rotation
+ toggle?.animate([
+ { transform: 'rotate(180deg)' },
+ { transform: 'rotate(0deg)' }
+ ], {
+ duration: this.collapseDuration,
+ easing: 'ease-out',
+ fill: 'forwards'
+ });
+
+ // Animate content height
+ const height = content.scrollHeight;
+ const animation = content.animate([
+ { height: `${height}px`, opacity: 1 },
+ { height: '0px', opacity: 0 }
+ ], {
+ duration: this.collapseDuration,
+ easing: 'ease-out',
+ fill: 'forwards'
+ });
+
+ // Remove expanded class after animation completes
+ animation.onfinish = () => {
+ item.classList.remove('expanded');
+ };
+ }
+
+ /**
+ * Expand all items (only useful when singleOpen is false)
+ */
+ expandAll(): void {
+ this.container.querySelectorAll('swp-accordion-item:not(.expanded)').forEach(item => {
+ this.expand(item);
+ });
+ }
+
+ /**
+ * Collapse all items
+ */
+ collapseAll(): void {
+ this.container.querySelectorAll('swp-accordion-item.expanded').forEach(item => {
+ this.collapse(item);
+ });
+ }
+
+ /**
+ * Get all expanded items
+ */
+ getExpanded(): HTMLElement[] {
+ return Array.from(this.container.querySelectorAll('swp-accordion-item.expanded'));
+ }
+}
+
+/**
+ * Initialize all accordions on the page
+ */
+export function initAccordions(options: AccordionOptions = {}): Accordion[] {
+ const accordions: Accordion[] = [];
+ document.querySelectorAll('swp-accordion').forEach(container => {
+ accordions.push(new Accordion(container, options));
+ });
+ return accordions;
+}
diff --git a/PlanTempus.Application/wwwroot/ts/modules/employees.ts b/PlanTempus.Application/wwwroot/ts/modules/employees.ts
index 75d025d..62642bb 100644
--- a/PlanTempus.Application/wwwroot/ts/modules/employees.ts
+++ b/PlanTempus.Application/wwwroot/ts/modules/employees.ts
@@ -1,4 +1,5 @@
import { createChart } from '@sevenweirdpeople/swp-charting';
+import { Accordion, initAccordions } from './accordion';
/**
* Employees Controller
@@ -72,6 +73,7 @@ export class EmployeesController {
private ratesSync: RatesSyncController | null = null;
private scheduleController: ScheduleController | null = null;
private statsController: EmployeeStatsController | null = null;
+ private salaryAccordions: Accordion[] = [];
private listView: HTMLElement | null = null;
private detailView: HTMLElement | null = null;
@@ -91,6 +93,20 @@ export class EmployeesController {
this.ratesSync = new RatesSyncController();
this.scheduleController = new ScheduleController();
this.statsController = new EmployeeStatsController();
+ this.initSalaryAccordions();
+ }
+
+ /**
+ * Initialize salary accordions when they exist
+ */
+ private initSalaryAccordions(): void {
+ // Initialize all accordions in the salary tab
+ const salaryTab = document.querySelector('swp-tab-content[data-tab="salary"]');
+ if (salaryTab) {
+ salaryTab.querySelectorAll('swp-accordion').forEach(accordion => {
+ this.salaryAccordions.push(new Accordion(accordion, { singleOpen: true }));
+ });
+ }
}
/**