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
This commit is contained in:
Janus C. H. Knudsen 2026-01-13 22:37:29 +01:00
parent 2e6207bb0b
commit f71f00099a
15 changed files with 1589 additions and 137 deletions

View file

@ -7,7 +7,8 @@
"Bash(find:*)",
"Bash(tree:*)",
"Bash(npm run analyze-css:*)",
"Bash(node:*)"
"Bash(node:*)",
"Bash(npx esbuild:*)"
]
}
}

View file

@ -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<EmployeeTag> 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);

View file

@ -9,23 +9,23 @@
<swp-edit-section>
<swp-edit-row>
<swp-edit-label>@Model.LabelFullName</swp-edit-label>
<swp-edit-value contenteditable="true">@Model.Name</swp-edit-value>
<input type="text" id="fullname" value="@Model.Name">
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>@Model.LabelEmail</swp-edit-label>
<swp-edit-value contenteditable="true">@Model.Email</swp-edit-value>
<input type="text" id="email" value="@Model.Email">
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>@Model.LabelPhone</swp-edit-label>
<swp-edit-value contenteditable="true">@Model.Phone</swp-edit-value>
<input type="text" id="phone" value="@Model.Phone">
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>@Model.LabelAddress</swp-edit-label>
<swp-edit-value contenteditable="true">@Model.Address</swp-edit-value>
<input type="text" id="address" value="@Model.Address">
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>@Model.LabelPostalCity</swp-edit-label>
<swp-edit-value contenteditable="true">@Model.PostalCity</swp-edit-value>
<input type="text" id="postalcity" value="@Model.PostalCity">
</swp-edit-row>
</swp-edit-section>
</swp-card>
@ -36,15 +36,15 @@
<swp-edit-section>
<swp-edit-row>
<swp-edit-label>@Model.LabelBirthDate</swp-edit-label>
<swp-edit-value contenteditable="true">@Model.BirthDate</swp-edit-value>
<input type="text" id="birthdate" value="@Model.BirthDate">
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>@Model.LabelEmergencyContact</swp-edit-label>
<swp-edit-value contenteditable="true">@Model.EmergencyContact</swp-edit-value>
<input type="text" id="emergencycontact" value="@Model.EmergencyContact">
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>@Model.LabelEmergencyPhone</swp-edit-label>
<swp-edit-value contenteditable="true">@Model.EmergencyPhone</swp-edit-value>
<input type="text" id="emergencyphone" value="@Model.EmergencyPhone">
</swp-edit-row>
</swp-edit-section>
</swp-card>
@ -58,19 +58,19 @@
<swp-edit-section>
<swp-edit-row>
<swp-edit-label>@Model.LabelEmploymentDate</swp-edit-label>
<swp-edit-value contenteditable="true">@Model.EmploymentDate</swp-edit-value>
<input type="text" id="employmentdate" value="@Model.EmploymentDate">
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>@Model.LabelPosition</swp-edit-label>
<swp-edit-value contenteditable="true">@Model.Position</swp-edit-value>
<input type="text" id="position" value="@Model.Position">
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>@Model.LabelEmploymentType</swp-edit-label>
<swp-edit-value contenteditable="true">@Model.EmploymentType</swp-edit-value>
<input type="text" id="employmenttype" value="@Model.EmploymentType">
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>@Model.LabelHoursPerWeek</swp-edit-label>
<swp-edit-value contenteditable="true">@Model.HoursPerWeek</swp-edit-value>
<input type="text" id="hoursperweek" value="@Model.HoursPerWeek">
</swp-edit-row>
</swp-edit-section>
</swp-card>

View file

@ -1,44 +1,183 @@
@model PlanTempus.Application.Features.Employees.Components.EmployeeDetailHRViewModel
<swp-detail-grid>
<!-- Left column -->
<div>
<!-- Contract & Documents -->
<swp-card>
<swp-section-label>@Model.LabelDocuments</swp-section-label>
<swp-document-list>
<swp-document-item>
<i class="ph ph-file-pdf"></i>
<swp-document-name>@Model.LabelContract</swp-document-name>
<swp-document-date>15. aug 2019</swp-document-date>
</swp-document-item>
<swp-document-item>
<i class="ph ph-file-pdf"></i>
<swp-document-name>Lønaftale 2024</swp-document-name>
<swp-document-date>1. jan 2024</swp-document-date>
</swp-document-item>
</swp-document-list>
</swp-card>
<swp-section-label>@Model.LabelContractDocuments</swp-section-label>
<swp-card>
<swp-section-label>@Model.LabelVacation</swp-section-label>
<swp-edit-section>
<swp-edit-row>
<swp-edit-label>Optjent ferie</swp-edit-label>
<swp-edit-value>25 dage</swp-edit-value>
<swp-edit-label>@Model.LabelContractType</swp-edit-label>
<swp-edit-select>
<select>
@foreach (var option in Model.ContractTypeOptions)
{
<option selected="@(option == Model.ContractType)">@option</option>
}
</select>
</swp-edit-select>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>Afholdt ferie</swp-edit-label>
<swp-edit-value>12 dage</swp-edit-value>
<swp-edit-label>@Model.LabelTerminationNotice</swp-edit-label>
<swp-edit-select>
<select>
@foreach (var option in Model.TerminationNoticeOptions)
{
<option selected="@(option == Model.TerminationNotice)">@option</option>
}
</select>
</swp-edit-select>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>Resterende</swp-edit-label>
<swp-edit-value>13 dage</swp-edit-value>
<swp-edit-label>@Model.LabelContractExpiry</swp-edit-label>
<swp-edit-value>@Model.ContractExpiry</swp-edit-value>
</swp-edit-row>
</swp-edit-section>
<swp-document-list style="margin-top: var(--spacing-6);">
@foreach (var doc in Model.Documents)
{
<swp-document-item>
<i class="ph ph-file-pdf"></i>
<swp-document-info>
<swp-document-name>@doc.Name</swp-document-name>
<swp-document-meta>@doc.UploadDate</swp-document-meta>
</swp-document-info>
<swp-document-actions>
<swp-btn class="secondary sm">Vis</swp-btn>
</swp-document-actions>
</swp-document-item>
}
</swp-document-list>
<swp-add-button>+ @Model.LabelUploadDocument</swp-add-button>
</swp-card>
<!-- Planned absence -->
<swp-card>
<swp-section-label>@Model.LabelPlannedAbsence</swp-section-label>
<swp-simple-list>
@foreach (var absence in Model.PlannedAbsences)
{
<swp-simple-item>
<swp-simple-item-text>@absence.Dates</swp-simple-item-text>
<swp-status-badge class="@absence.TypeClass">@absence.Type</swp-status-badge>
</swp-simple-item>
}
</swp-simple-list>
<swp-add-button>+ @Model.LabelAddAbsence</swp-add-button>
</swp-card>
</div>
<!-- Right column -->
<div>
<!-- Certifications -->
<swp-card>
<swp-section-label>@Model.LabelCertifications</swp-section-label>
<swp-document-list>
@foreach (var cert in Model.Certifications)
{
<swp-document-item>
<i class="ph ph-certificate"></i>
<swp-document-info>
<swp-document-name>@cert.Name</swp-document-name>
<swp-document-meta>@cert.ExpiryDate</swp-document-meta>
</swp-document-info>
<swp-document-actions>
<swp-status-badge class="@cert.StatusClass">@cert.Status</swp-status-badge>
</swp-document-actions>
</swp-document-item>
}
</swp-document-list>
<swp-add-button>+ @Model.LabelAddCertification</swp-add-button>
</swp-card>
<!-- Courses -->
<swp-card>
<swp-section-label>@Model.LabelCourses</swp-section-label>
<swp-subsection>
<swp-subsection-title>@Model.LabelCompletedCourses</swp-subsection-title>
<swp-document-list>
@foreach (var course in Model.CompletedCourses)
{
<swp-document-item>
<swp-document-info>
<swp-document-name>@course.Name</swp-document-name>
<swp-document-meta>@course.Provider · @course.Date</swp-document-meta>
</swp-document-info>
</swp-document-item>
}
</swp-document-list>
</swp-subsection>
<swp-subsection>
<swp-subsection-title>@Model.LabelPlannedCourses</swp-subsection-title>
<swp-document-list>
@foreach (var course in Model.PlannedCourses)
{
<swp-document-item>
<swp-document-info>
<swp-document-name>@course.Name</swp-document-name>
<swp-document-meta>@course.Provider · @course.Date</swp-document-meta>
</swp-document-info>
@if (!string.IsNullOrEmpty(course.Status))
{
<swp-document-actions>
<swp-status-badge class="enrolled">@course.Status</swp-status-badge>
</swp-document-actions>
}
</swp-document-item>
}
</swp-document-list>
</swp-subsection>
<swp-add-button>+ @Model.LabelAddCourse</swp-add-button>
</swp-card>
</div>
</swp-detail-grid>
<!-- Vacation & Absence section -->
<swp-detail-grid style="margin-top: var(--spacing-8);">
<swp-card>
<swp-section-label>@Model.LabelVacationBalance</swp-section-label>
<swp-edit-section>
<swp-edit-row>
<swp-edit-label>@Model.LabelVacationEarned</swp-edit-label>
<swp-edit-value>@Model.VacationEarned</swp-edit-value>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>@Model.LabelVacationUsed</swp-edit-label>
<swp-edit-value>@Model.VacationUsed</swp-edit-value>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>@Model.LabelVacationRemaining</swp-edit-label>
<swp-edit-value style="font-weight: 600; color: var(--color-teal);">@Model.VacationRemaining</swp-edit-value>
</swp-edit-row>
</swp-edit-section>
</swp-card>
<swp-card>
<swp-section-label>@Model.LabelNotes</swp-section-label>
<swp-notes-area contenteditable="true">
Ingen noter tilføjet endnu...
</swp-notes-area>
<swp-section-label>@Model.LabelAbsenceSickness</swp-section-label>
<swp-edit-section>
<swp-edit-row>
<swp-edit-label>@Model.LabelSickDays2025</swp-edit-label>
<swp-edit-value>@Model.SickDays2025</swp-edit-value>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>@Model.LabelSickDays2024</swp-edit-label>
<swp-edit-value>@Model.SickDays2024</swp-edit-value>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>@Model.LabelChildSickDays2025</swp-edit-label>
<swp-edit-value>@Model.ChildSickDays2025</swp-edit-value>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>@Model.LabelMaternityLeave</swp-edit-label>
<swp-edit-value>@Model.MaternityLeave</swp-edit-value>
</swp-edit-row>
</swp-edit-section>
</swp-card>
</swp-detail-grid>

View file

@ -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<string> { "Fastansættelse", "Tidsbegrænset", "Freelance", "Elev/Lærling" },
TerminationNoticeOptions = new List<string> { "14 dage", "1 måned", "3 måneder" },
// Mock data - Documents
Documents = new List<DocumentRecord>
{
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<CertificationRecord>
{
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<CourseRecord>
{
new("Avanceret balayage teknikker", "Wella Academy", "Marts 2024"),
new("Kundeservice & mersalg", "SalonUp", "November 2023")
},
PlannedCourses = new List<CourseRecord>
{
new("Olaplex certificering", "Olaplex DK", "15. februar 2026", "Tilmeldt")
},
// Mock data - Planned absence
PlannedAbsences = new List<PlannedAbsenceRecord>
{
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<string> ContractTypeOptions { get; init; } = new();
public List<string> TerminationNoticeOptions { get; init; } = new();
// Data collections
public List<DocumentRecord> Documents { get; init; } = new();
public List<CertificationRecord> Certifications { get; init; } = new();
public List<CourseRecord> CompletedCourses { get; init; } = new();
public List<CourseRecord> PlannedCourses { get; init; } = new();
public List<PlannedAbsenceRecord> PlannedAbsences { get; init; } = new();
}

View file

@ -1,39 +1,215 @@
@model PlanTempus.Application.Features.Employees.Components.EmployeeDetailSalaryViewModel
<swp-detail-grid>
<div>
<!-- Satser (Grundsatser) -->
<swp-card>
<swp-section-label>@Model.LabelPaymentInfo</swp-section-label>
<swp-section-header>
<swp-section-label>@Model.LabelRates</swp-section-label>
<swp-section-action data-drawer-trigger="rates-drawer">@Model.LabelEdit</swp-section-action>
</swp-section-header>
<swp-edit-section>
<swp-edit-row>
<swp-edit-label>@Model.LabelBankAccount</swp-edit-label>
<swp-edit-value contenteditable="true">@Model.BankAccount</swp-edit-value>
<swp-edit-row id="card-normal">
<swp-edit-label>@Model.LabelNormalRate</swp-edit-label>
<input type="text" id="value-normal" data-type="number" value="@Model.NormalRate" readonly>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>@Model.LabelTaxCard</swp-edit-label>
<swp-edit-value contenteditable="true">@Model.TaxCard</swp-edit-value>
<swp-edit-row id="card-overtime">
<swp-edit-label>@Model.LabelOvertimeRate</swp-edit-label>
<input type="text" id="value-overtime" data-type="number" value="@Model.OvertimeRate" readonly>
</swp-edit-row>
<swp-edit-row id="card-course" style="display: none;">
<swp-edit-label>@Model.LabelCourseRate</swp-edit-label>
<input type="text" id="value-course" data-type="number" value="" readonly>
</swp-edit-row>
<swp-edit-row id="card-timeoff" style="display: none;">
<swp-edit-label>@Model.LabelTimeOffRate</swp-edit-label>
<input type="text" id="value-timeoff" data-type="number" value="" readonly>
</swp-edit-row>
<swp-edit-row id="card-paidleave" style="display: none;">
<swp-edit-label>@Model.LabelPaidLeaveRate</swp-edit-label>
<input type="text" id="value-paidleave" data-type="number" value="" readonly>
</swp-edit-row>
<swp-edit-row id="card-vacation">
<swp-edit-label>@Model.LabelVacationRate</swp-edit-label>
<input type="text" id="value-vacation" data-type="number" value="@Model.VacationRateValue kr" readonly>
</swp-edit-row>
<swp-edit-row id="card-office" style="display: none;">
<swp-edit-label>@Model.LabelOfficeRate</swp-edit-label>
<input type="text" id="value-office" data-type="number" value="" readonly>
</swp-edit-row>
<swp-edit-row id="card-childsick" style="display: none;">
<swp-edit-label>@Model.LabelChildSickRate</swp-edit-label>
<input type="text" id="value-childsick" data-type="number" value="" readonly>
</swp-edit-row>
<swp-edit-row id="card-childhospital" style="display: none;">
<swp-edit-label>@Model.LabelChildHospitalRate</swp-edit-label>
<input type="text" id="value-childhospital" data-type="number" value="" readonly>
</swp-edit-row>
<swp-edit-row id="card-maternity" style="display: none;">
<swp-edit-label>@Model.LabelMaternityRate</swp-edit-label>
<input type="text" id="value-maternity" data-type="number" value="" readonly>
</swp-edit-row>
</swp-edit-section>
</swp-card>
<!-- Tillæg -->
<swp-card>
<swp-section-label>@Model.LabelSalarySettings</swp-section-label>
<swp-section-label>@Model.LabelSupplements</swp-section-label>
<swp-edit-section>
<swp-edit-row>
<swp-edit-label>@Model.LabelHourlyRate</swp-edit-label>
<swp-edit-value contenteditable="true">@Model.HourlyRate</swp-edit-value>
<swp-edit-row id="card-weekday">
<swp-edit-label>@Model.LabelWeekdaySupplement</swp-edit-label>
<input type="text" id="value-weekday" data-type="number" value="@Model.WeekdaySupplement" readonly>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>@Model.LabelMonthlyFixed</swp-edit-label>
<swp-edit-value contenteditable="true">@Model.MonthlyFixedSalary</swp-edit-value>
<swp-edit-row id="card-saturday">
<swp-edit-label>@Model.LabelSaturdaySupplement</swp-edit-label>
<input type="text" id="value-saturday" data-type="number" value="@Model.SaturdaySupplement" readonly>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>@Model.LabelCommission</swp-edit-label>
<swp-edit-value contenteditable="true">10%</swp-edit-value>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>@Model.LabelProductCommission</swp-edit-label>
<swp-edit-value contenteditable="true">5%</swp-edit-value>
<swp-edit-row id="card-sunday">
<swp-edit-label>@Model.LabelSundaySupplement</swp-edit-label>
<input type="text" id="value-sunday" data-type="number" value="@Model.SundaySupplement" readonly>
</swp-edit-row>
</swp-edit-section>
</swp-card>
<!-- Provision -->
<swp-card>
<swp-section-label>@Model.LabelCommission</swp-section-label>
<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>
</div>
<swp-card>
<swp-section-label>@Model.LabelSalaryHistory</swp-section-label>
<swp-salary-table>
<swp-salary-table-header>
<swp-salary-table-cell>@Model.LabelPeriod</swp-salary-table-cell>
<swp-salary-table-cell>@Model.LabelGrossSalary</swp-salary-table-cell>
<swp-salary-table-cell></swp-salary-table-cell>
</swp-salary-table-header>
<swp-salary-table-body>
@foreach (var item in Model.SalaryHistory)
{
<swp-salary-table-row>
<swp-salary-table-cell>@item.Period</swp-salary-table-cell>
<swp-salary-table-cell class="mono">@item.GrossSalary</swp-salary-table-cell>
<swp-salary-table-cell><i class="ph ph-caret-right"></i></swp-salary-table-cell>
</swp-salary-table-row>
}
</swp-salary-table-body>
</swp-salary-table>
</swp-card>
</swp-detail-grid>
<!-- Rates drawer -->
<div id="rates-drawer" data-drawer="lg">
<swp-drawer-header>
<swp-drawer-title>@Model.LabelRatesDrawerTitle</swp-drawer-title>
<swp-drawer-close data-drawer-close>
<i class="ph ph-x"></i>
</swp-drawer-close>
</swp-drawer-header>
<swp-drawer-body class="rates-content">
<!-- Grundsatser -->
<swp-section-label>@Model.LabelBaseRates</swp-section-label>
<swp-data-table>
<swp-data-row>
<input type="checkbox" id="rate-normal-enabled" checked>
<swp-data-label>@Model.LabelNormalRate</swp-data-label>
<swp-data-input><input type="text" id="rate-normal" value="@Model.NormalRateValue"> kr</swp-data-input>
</swp-data-row>
<swp-data-row>
<input type="checkbox" id="rate-overtime-enabled" checked>
<swp-data-label>@Model.LabelOvertimeRate</swp-data-label>
<swp-data-input><input type="text" id="rate-overtime" value="@Model.OvertimeRateValue"> kr</swp-data-input>
</swp-data-row>
<swp-data-row>
<input type="checkbox" id="rate-course-enabled">
<swp-data-label class="disabled">@Model.LabelCourseRate</swp-data-label>
<swp-data-input class="disabled"><input type="text" id="rate-course" value="@Model.CourseRateValue"> kr</swp-data-input>
</swp-data-row>
<swp-data-row>
<input type="checkbox" id="rate-timeoff-enabled">
<swp-data-label class="disabled">@Model.LabelTimeOffRate</swp-data-label>
<swp-data-input class="disabled"><input type="text" id="rate-timeoff" value="@Model.TimeOffRateValue"> kr</swp-data-input>
</swp-data-row>
<swp-data-row>
<input type="checkbox" id="rate-paidleave-enabled">
<swp-data-label class="disabled">@Model.LabelPaidLeaveRate</swp-data-label>
<swp-data-input class="disabled"><input type="text" id="rate-paidleave" value="@Model.PaidLeaveRateValue"> kr</swp-data-input>
</swp-data-row>
<swp-data-row>
<input type="checkbox" id="rate-vacation-enabled" checked>
<swp-data-label>@Model.LabelVacationRate</swp-data-label>
<swp-data-input><input type="text" id="rate-vacation" value="@Model.VacationRateValue"> kr</swp-data-input>
</swp-data-row>
<swp-data-row>
<input type="checkbox" id="rate-office-enabled">
<swp-data-label class="disabled">@Model.LabelOfficeRate</swp-data-label>
<swp-data-input class="disabled"><input type="text" id="rate-office" value="@Model.OfficeRateValue"> kr</swp-data-input>
</swp-data-row>
<swp-data-row>
<input type="checkbox" id="rate-childsick-enabled">
<swp-data-label class="disabled">@Model.LabelChildSickRate</swp-data-label>
<swp-data-input class="disabled"><input type="text" id="rate-childsick" value="@Model.ChildSickRateValue"> kr</swp-data-input>
</swp-data-row>
<swp-data-row>
<input type="checkbox" id="rate-childhospital-enabled">
<swp-data-label class="disabled">@Model.LabelChildHospitalRate</swp-data-label>
<swp-data-input class="disabled"><input type="text" id="rate-childhospital" value="@Model.ChildHospitalRateValue"> kr</swp-data-input>
</swp-data-row>
<swp-data-row>
<input type="checkbox" id="rate-maternity-enabled">
<swp-data-label class="disabled">@Model.LabelMaternityRate</swp-data-label>
<swp-data-input class="disabled"><input type="text" id="rate-maternity" value="@Model.MaternityRateValue"> kr</swp-data-input>
</swp-data-row>
</swp-data-table>
<!-- Tillæg -->
<swp-data-section>
<swp-section-label>@Model.LabelSupplements</swp-section-label>
<swp-data-table>
<swp-data-row>
<input type="checkbox" id="rate-weekday-enabled" checked>
<swp-data-label>@Model.LabelWeekdaySupplementFull</swp-data-label>
<swp-data-input><input type="text" id="rate-weekday" value="@Model.WeekdaySupplementValue"> kr</swp-data-input>
</swp-data-row>
<swp-data-row>
<input type="checkbox" id="rate-saturday-enabled" checked>
<swp-data-label>@Model.LabelSaturdaySupplementFull</swp-data-label>
<swp-data-input><input type="text" id="rate-saturday" value="@Model.SaturdaySupplementValue"> kr</swp-data-input>
</swp-data-row>
<swp-data-row>
<input type="checkbox" id="rate-sunday-enabled" checked>
<swp-data-label>@Model.LabelSundaySupplement</swp-data-label>
<swp-data-input><input type="text" id="rate-sunday" value="@Model.SundaySupplementValue"> kr</swp-data-input>
</swp-data-row>
</swp-data-table>
</swp-data-section>
<!-- Provision -->
<swp-data-section>
<swp-section-label>@Model.LabelCommission</swp-section-label>
<swp-data-table>
<swp-data-row>
<input type="checkbox" id="rate-productcommission-enabled" checked>
<swp-data-label>@Model.LabelProductCommissionFull</swp-data-label>
<swp-data-input><input type="text" id="rate-productcommission" value="@Model.ProductCommissionValue"> %</swp-data-input>
</swp-data-row>
<swp-data-row>
<input type="checkbox" id="rate-servicecommission-enabled" checked>
<swp-data-label>@Model.LabelServiceCommissionFull</swp-data-label>
<swp-data-input><input type="text" id="rate-servicecommission" value="@Model.ServiceCommissionValue"> %</swp-data-input>
</swp-data-row>
</swp-data-table>
</swp-data-section>
</swp-drawer-body>
</div>

View file

@ -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<SalaryHistoryItem>
{
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<SalaryHistoryItem> SalaryHistory { get; init; } = new();
}
public class SalaryHistoryItem
{
public required string Period { get; init; }
public required string GrossSalary { get; init; }
}

View file

@ -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",

View file

@ -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": {

View file

@ -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
<swp-edit-section>
<swp-edit-row>
<swp-edit-label>Label tekst</swp-edit-label>
<swp-edit-value>Værdi</swp-edit-value>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>Med redigering</swp-edit-label>
<swp-edit-value contenteditable="true">Redigerbar værdi</swp-edit-value>
</swp-edit-row>
</swp-edit-section>
```
### Med select dropdown
```html
<swp-edit-section>
<swp-edit-row>
<swp-edit-label>Vælg type</swp-edit-label>
<swp-edit-select>
<select>
<option>Option 1</option>
<option>Option 2</option>
</select>
</swp-edit-select>
</swp-edit-row>
</swp-edit-section>
```
### Mono-font for tal
```html
<swp-edit-value class="mono">131,49 kr</swp-edit-value>
```
---
## 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
<swp-document-list>
<swp-document-item>
<i class="ph ph-file-pdf"></i>
<swp-document-info>
<swp-document-name>Ansættelseskontrakt.pdf</swp-document-name>
<swp-document-meta>Uploadet 1. aug 2019</swp-document-meta>
</swp-document-info>
<swp-document-actions>
<swp-btn class="secondary sm">Vis</swp-btn>
</swp-document-actions>
</swp-document-item>
</swp-document-list>
```
**Med badge i stedet for knap:**
```html
<swp-document-item>
<i class="ph ph-certificate"></i>
<swp-document-info>
<swp-document-name>Balayage Specialist</swp-document-name>
<swp-document-meta>Udløber: 15. juni 2026</swp-document-meta>
</swp-document-info>
<swp-document-actions>
<swp-status-badge class="valid">Gyldig</swp-status-badge>
</swp-document-actions>
</swp-document-item>
```
---
## Subsection (employees.css)
Til gruppering af lister (f.eks. "Gennemførte kurser" / "Planlagte kurser").
```html
<swp-subsection>
<swp-subsection-title>Gennemførte kurser</swp-subsection-title>
<swp-document-list>
<!-- items -->
</swp-document-list>
</swp-subsection>
```
---
## Simple List (employees.css)
Simpel liste med tekst + badge (f.eks. planlagt fravær).
```html
<swp-simple-list>
<swp-simple-item>
<swp-simple-item-text>23. dec 2. jan 2026</swp-simple-item-text>
<swp-status-badge class="ferie">Ferie</swp-status-badge>
</swp-simple-item>
</swp-simple-list>
```
---
## Salary Table (employees.css)
Bruger Grid + Subgrid mønsteret.
```html
<swp-salary-table>
<swp-salary-table-header>
<swp-salary-table-cell>Periode</swp-salary-table-cell>
<swp-salary-table-cell>Bruttoløn</swp-salary-table-cell>
<swp-salary-table-cell></swp-salary-table-cell>
</swp-salary-table-header>
<swp-salary-table-body>
<swp-salary-table-row>
<swp-salary-table-cell>Januar 2026</swp-salary-table-cell>
<swp-salary-table-cell class="mono">34.063,50 kr</swp-salary-table-cell>
<swp-salary-table-cell><i class="ph ph-caret-right"></i></swp-salary-table-cell>
</swp-salary-table-row>
</swp-salary-table-body>
</swp-salary-table>
```
Rækker har hover-effekt og chevron bliver teal ved hover.
---
## Add Button (components.css)
Dashed border knap til tilføjelse af elementer.
```html
<swp-add-button>+ Upload dokument</swp-add-button>
<swp-add-button>+ Tilføj certificering</swp-add-button>
```
**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 |

View file

@ -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);
}

View file

@ -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
=========================================== */

View file

@ -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 {

File diff suppressed because one or more lines are too long

View file

@ -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<HTMLElement>('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}`;
}
}