Adds salary specifications with detailed accordion view
Introduces new salary specification feature with interactive accordion component Implements detailed salary breakdown including: - Salary specification JSON data model - Salary specification page with printable view - Accordion component for expanding/collapsing salary details - Localization support for new salary labels Enhances employee salary transparency and detail presentation
This commit is contained in:
parent
f3c54dde35
commit
a1059adf06
14 changed files with 1613 additions and 46 deletions
|
|
@ -52,7 +52,24 @@
|
|||
</swp-edit-section>
|
||||
</swp-card>
|
||||
|
||||
<!-- Tillæg -->
|
||||
<!-- Provision -->
|
||||
<swp-card>
|
||||
<swp-card-header>
|
||||
<swp-card-title>@Model.LabelCommission</swp-card-title>
|
||||
</swp-card-header>
|
||||
<swp-edit-section>
|
||||
<swp-edit-row id="card-productcommission">
|
||||
<swp-edit-label>@Model.LabelProductCommission</swp-edit-label>
|
||||
<input type="text" id="value-productcommission" data-type="number" value="@Model.ProductCommission" readonly>
|
||||
</swp-edit-row>
|
||||
<swp-edit-row id="card-servicecommission">
|
||||
<swp-edit-label>@Model.LabelServiceCommission</swp-edit-label>
|
||||
<input type="text" id="value-servicecommission" data-type="number" value="@Model.ServiceCommission" readonly>
|
||||
</swp-edit-row>
|
||||
</swp-edit-section>
|
||||
</swp-card>
|
||||
|
||||
<!-- Tillaeg -->
|
||||
<swp-card>
|
||||
<swp-card-header>
|
||||
<swp-card-title>@Model.LabelSupplements</swp-card-title>
|
||||
|
|
@ -72,47 +89,111 @@
|
|||
</swp-edit-row>
|
||||
</swp-edit-section>
|
||||
</swp-card>
|
||||
|
||||
<!-- Provision -->
|
||||
<swp-card>
|
||||
<swp-card-header>
|
||||
<swp-card-title>@Model.LabelCommission</swp-card-title>
|
||||
</swp-card-header>
|
||||
<swp-edit-section>
|
||||
<swp-edit-row id="card-productcommission">
|
||||
<swp-edit-label>@Model.LabelProductCommission</swp-edit-label>
|
||||
<input type="text" id="value-productcommission" data-type="number" value="@Model.ProductCommission" readonly>
|
||||
</swp-edit-row>
|
||||
<swp-edit-row id="card-servicecommission">
|
||||
<swp-edit-label>@Model.LabelServiceCommission</swp-edit-label>
|
||||
<input type="text" id="value-servicecommission" data-type="number" value="@Model.ServiceCommission" readonly>
|
||||
</swp-edit-row>
|
||||
</swp-edit-section>
|
||||
</swp-card>
|
||||
</swp-card-column>
|
||||
|
||||
<swp-card class="salary-history">
|
||||
<swp-card-header>
|
||||
<swp-card-title>@Model.LabelSalaryHistory</swp-card-title>
|
||||
</swp-card-header>
|
||||
<swp-data-table>
|
||||
<swp-data-table-header>
|
||||
<swp-data-table-cell>@Model.LabelPeriod</swp-data-table-cell>
|
||||
<swp-data-table-cell>@Model.LabelGrossSalary</swp-data-table-cell>
|
||||
<swp-data-table-cell></swp-data-table-cell>
|
||||
</swp-data-table-header>
|
||||
@foreach (var item in Model.SalaryHistory)
|
||||
{
|
||||
<swp-data-table-row>
|
||||
<swp-data-table-cell>@item.Period</swp-data-table-cell>
|
||||
<swp-data-table-cell class="mono">@item.GrossSalary</swp-data-table-cell>
|
||||
<swp-data-table-cell><i class="ph ph-caret-right"></i></swp-data-table-cell>
|
||||
</swp-data-table-row>
|
||||
}
|
||||
</swp-data-table>
|
||||
</swp-card>
|
||||
</swp-detail-grid>
|
||||
|
||||
<!-- Loenspecifikationer (full width accordion) -->
|
||||
<swp-card class="salary-specifications">
|
||||
<swp-card-header>
|
||||
<swp-card-title>@Model.LabelSpecifications</swp-card-title>
|
||||
</swp-card-header>
|
||||
<swp-accordion id="salary-specifications-accordion">
|
||||
@foreach (var spec in Model.Specifications)
|
||||
{
|
||||
<swp-accordion-item data-period="@spec.PeriodKey">
|
||||
<swp-accordion-header>
|
||||
<swp-accordion-info>
|
||||
<swp-accordion-title>@spec.Period</swp-accordion-title>
|
||||
<swp-accordion-meta>@spec.Weeks.Sum(w => w.NormalHours + w.OvertimeHours)t</swp-accordion-meta>
|
||||
</swp-accordion-info>
|
||||
<swp-accordion-summary>
|
||||
<swp-summary-item>
|
||||
<swp-summary-value>@spec.GrossSalaryFormatted</swp-summary-value>
|
||||
<swp-summary-label>@Model.LabelTotal</swp-summary-label>
|
||||
</swp-summary-item>
|
||||
<swp-accordion-toggle>
|
||||
<i class="ph ph-caret-down"></i>
|
||||
</swp-accordion-toggle>
|
||||
</swp-accordion-summary>
|
||||
</swp-accordion-header>
|
||||
<swp-accordion-content>
|
||||
<!-- Config row -->
|
||||
<swp-config-row>
|
||||
<swp-config-item>
|
||||
<swp-config-label>@Model.LabelNormalRate:</swp-config-label>
|
||||
<swp-config-value class="mono">@spec.Config.HourlyRateFormatted</swp-config-value>
|
||||
</swp-config-item>
|
||||
<swp-config-item>
|
||||
<swp-config-label>@Model.LabelWeeklyNorm:</swp-config-label>
|
||||
<swp-config-value>@spec.Config.WeeklyHoursFormatted</swp-config-value>
|
||||
</swp-config-item>
|
||||
<swp-config-item>
|
||||
<swp-config-label>@Model.LabelOvertimeMultiplier:</swp-config-label>
|
||||
<swp-config-value>@spec.Config.OvertimeFormatted</swp-config-value>
|
||||
</swp-config-item>
|
||||
<swp-config-item>
|
||||
<swp-config-label>@Model.LabelMinimum:</swp-config-label>
|
||||
<swp-config-value class="mono">@spec.Config.MinimumFormatted</swp-config-value>
|
||||
</swp-config-item>
|
||||
<swp-config-item>
|
||||
<swp-config-label>@Model.LabelProvision:</swp-config-label>
|
||||
<swp-config-value>@spec.Config.CommissionFormatted</swp-config-value>
|
||||
</swp-config-item>
|
||||
</swp-config-row>
|
||||
|
||||
<!-- Weeks table -->
|
||||
<swp-accordion-table>
|
||||
<swp-data-table class="specification-weeks">
|
||||
<swp-data-table-header>
|
||||
<swp-data-table-cell>@Model.LabelWeek</swp-data-table-cell>
|
||||
<swp-data-table-cell>Timer</swp-data-table-cell>
|
||||
<swp-data-table-cell>Overtid</swp-data-table-cell>
|
||||
<swp-data-table-cell>Ferie</swp-data-table-cell>
|
||||
<swp-data-table-cell>Services</swp-data-table-cell>
|
||||
<swp-data-table-cell>Produkter</swp-data-table-cell>
|
||||
<swp-data-table-cell>Minimum</swp-data-table-cell>
|
||||
<swp-data-table-cell>Provision</swp-data-table-cell>
|
||||
<swp-data-table-cell>I alt</swp-data-table-cell>
|
||||
</swp-data-table-header>
|
||||
@foreach (var week in spec.Weeks)
|
||||
{
|
||||
<swp-data-table-row>
|
||||
<swp-data-table-cell>Uge @week.WeekNumber</swp-data-table-cell>
|
||||
<swp-data-table-cell class="mono">@week.NormalHoursFormatted</swp-data-table-cell>
|
||||
<swp-data-table-cell class="mono @(week.HasOvertime ? "warning" : "")">@week.OvertimeHoursFormatted</swp-data-table-cell>
|
||||
<swp-data-table-cell class="mono">@week.VacationDaysFormatted</swp-data-table-cell>
|
||||
<swp-data-table-cell class="mono">@week.ServiceRevenueFormatted</swp-data-table-cell>
|
||||
<swp-data-table-cell class="mono">@week.ProductRevenueFormatted</swp-data-table-cell>
|
||||
<swp-data-table-cell class="mono">@week.MinimumThresholdFormatted</swp-data-table-cell>
|
||||
<swp-data-table-cell class="mono @(week.HasCommission ? "highlight" : "")">@week.CommissionFormatted</swp-data-table-cell>
|
||||
<swp-data-table-cell class="mono">@week.TotalPayFormatted</swp-data-table-cell>
|
||||
</swp-data-table-row>
|
||||
}
|
||||
<swp-data-table-footer>
|
||||
<swp-data-table-cell>TOTAL</swp-data-table-cell>
|
||||
<swp-data-table-cell class="mono">@(spec.Weeks.Sum(w => w.NormalHours))t</swp-data-table-cell>
|
||||
<swp-data-table-cell class="mono">@(spec.Weeks.Sum(w => w.OvertimeHours))t</swp-data-table-cell>
|
||||
<swp-data-table-cell class="mono">@(spec.Weeks.Sum(w => w.VacationDays)) dg</swp-data-table-cell>
|
||||
<swp-data-table-cell class="mono">@(spec.Weeks.Sum(w => w.ServiceRevenue).ToString("N0", System.Globalization.CultureInfo.GetCultureInfo("da-DK"))) kr</swp-data-table-cell>
|
||||
<swp-data-table-cell class="mono">@(spec.Weeks.Sum(w => w.ProductRevenue).ToString("N0", System.Globalization.CultureInfo.GetCultureInfo("da-DK"))) kr</swp-data-table-cell>
|
||||
<swp-data-table-cell class="mono">-</swp-data-table-cell>
|
||||
<swp-data-table-cell class="mono">@(spec.Weeks.Sum(w => w.Commission).ToString("N2", System.Globalization.CultureInfo.GetCultureInfo("da-DK"))) kr</swp-data-table-cell>
|
||||
<swp-data-table-cell class="mono">@(spec.Weeks.Sum(w => w.TotalPay).ToString("N2", System.Globalization.CultureInfo.GetCultureInfo("da-DK"))) kr</swp-data-table-cell>
|
||||
</swp-data-table-footer>
|
||||
</swp-data-table>
|
||||
</swp-accordion-table>
|
||||
<swp-accordion-footer>
|
||||
<a href="/medarbejdere/loenspecifikation/@spec.PeriodKey" target="_blank" class="swp-btn secondary">
|
||||
<i class="ph ph-file-text"></i>
|
||||
Se lønberegning
|
||||
</a>
|
||||
</swp-accordion-footer>
|
||||
</swp-accordion-content>
|
||||
</swp-accordion-item>
|
||||
}
|
||||
</swp-accordion>
|
||||
</swp-card>
|
||||
|
||||
<!-- Rates drawer -->
|
||||
<div id="rates-drawer" data-drawer="lg">
|
||||
<swp-drawer-header>
|
||||
|
|
@ -177,7 +258,7 @@
|
|||
</swp-data-row>
|
||||
</swp-data-table>
|
||||
|
||||
<!-- Tillæg -->
|
||||
<!-- Tillaeg -->
|
||||
<swp-data-section>
|
||||
<swp-section-label>@Model.LabelSupplements</swp-section-label>
|
||||
<swp-data-table>
|
||||
|
|
|
|||
|
|
@ -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<SalaryHistoryItem>
|
||||
{
|
||||
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<SalarySpecificationRoot>(json, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
})!;
|
||||
}
|
||||
}
|
||||
|
||||
public class EmployeeDetailSalaryViewModel
|
||||
{
|
||||
// Salary Specifications (from JSON)
|
||||
public List<SalarySpecificationDto> 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<SalarySpecificationDto> 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<SalaryWeekDto> 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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue