PlanTempusApp/PlanTempus.Application/Features/Employees/Pages/SalarySpecification.cshtml
Janus C. H. Knudsen 7aaa475a14 Enhances localization and UI for customer and employee views
Updates customer detail tab with localized activity log label
Refactors salary specification page with improved button styles and actions
Adds localization support for customer activity log in Danish and English translations

Improves print and interaction experience for salary specification page
2026-01-23 23:19:57 +01:00

481 lines
17 KiB
Text
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@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");
}
<!DOCTYPE html>
<html lang="da">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lønspecifikation @spec.Period</title>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
/* Print setup */
@@page { size: A4; margin: 14mm; }
@@media print {
.no-print { display: none !important; }
.page-break { break-after: page; page-break-after: always; }
a { color: inherit; text-decoration: none; }
}
/* Base */
:root {
--ink: #0f172a;
--muted: #475569;
--line: #e2e8f0;
--soft: #f8fafc;
--accent: #0ea5e9;
--font-mono: 'JetBrains Mono', monospace;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
color: var(--ink);
background: #fff;
}
.sheet {
max-width: 210mm;
margin: 0 auto;
padding: 20px;
}
/* Header */
.hdr {
display: flex;
justify-content: space-between;
gap: 16px;
padding: 14px 0 10px;
border-bottom: 2px solid var(--line);
margin-bottom: 12px;
}
.brand {
display: flex;
flex-direction: column;
gap: 6px;
}
.title {
font-size: 20px;
font-weight: 800;
letter-spacing: .2px;
margin: 0;
line-height: 1.1;
}
.subtitle {
margin: 0;
color: var(--muted);
font-size: 12px;
}
.meta {
text-align: right;
min-width: 260px;
}
.meta .pill {
display: inline-block;
font-size: 12px;
padding: 6px 10px;
border: 1px solid var(--line);
border-radius: 999px;
background: var(--soft);
margin-bottom: 8px;
}
.meta .kv {
display: grid;
grid-template-columns: auto auto;
gap: 4px 12px;
justify-content: end;
font-size: 12px;
color: var(--muted);
}
.meta .kv b { color: var(--ink); font-weight: 600; }
/* Cards */
.card {
border: 1px solid var(--line);
border-radius: 14px;
overflow: hidden;
background: #fff;
margin-bottom: 12px;
}
.card .hd {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
background: linear-gradient(0deg, var(--soft), #fff);
border-bottom: 1px solid var(--line);
}
.card .hd h2 {
font-size: 13px;
margin: 0;
letter-spacing: .2px;
text-transform: uppercase;
color: var(--muted);
}
.card .bd { padding: 12px; }
.grid {
display: grid;
grid-template-columns: 1.2fr .8fr;
gap: 12px;
}
/* Total */
.total {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
padding: 14px;
border-radius: 14px;
border: 1px solid var(--line);
background: var(--soft);
margin-bottom: 12px;
}
.total .label {
color: var(--muted);
font-size: 12px;
text-transform: uppercase;
letter-spacing: .25px;
margin-bottom: 6px;
}
.total .big {
font-size: 28px;
font-weight: 900;
font-family: var(--font-mono);
margin: 0;
line-height: 1.05;
}
.total .big small {
font-size: 14px;
font-weight: 700;
color: var(--muted);
}
/* Tables */
table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
}
th, td {
padding: 8px;
border-bottom: 1px solid var(--line);
vertical-align: top;
}
th {
text-align: left;
color: var(--muted);
font-weight: 700;
font-size: 11px;
text-transform: uppercase;
letter-spacing: .2px;
background: var(--soft);
}
td.num, th.num { text-align: right; }
td.num { font-family: var(--font-mono); }
tr:last-child td { border-bottom: none; }
.note {
margin: 8px 0 0;
color: var(--muted);
font-size: 11px;
line-height: 1.35;
}
/* Footer */
.ftr {
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid var(--line);
display: flex;
justify-content: space-between;
gap: 12px;
font-size: 10.5px;
color: var(--muted);
}
.avoid-break { break-inside: avoid; page-break-inside: avoid; }
/* Action buttons */
.actions {
position: fixed;
top: 20px;
right: 20px;
display: flex;
gap: 8px;
z-index: 100;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.15s ease;
}
.btn.primary {
background: #00897b;
color: white;
}
.btn.primary:hover { background: #00796b; }
.btn.secondary {
background: #f1f5f9;
color: #334155;
border: 1px solid #e2e8f0;
}
.btn.secondary:hover { background: #e2e8f0; }
</style>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@@phosphor-icons/web@@2.1.2/src/regular/style.css" />
</head>
<body>
<div class="actions no-print">
<button class="btn primary" onclick="window.print()">
<i class="ph ph-printer"></i>
Print / Gem som PDF
</button>
<button class="btn secondary" onclick="window.close()">
<i class="ph ph-x"></i>
Luk
</button>
</div>
<div class="sheet">
<!-- Header -->
<div class="hdr">
<div class="brand">
<p class="title">Lønspecifikation</p>
<p class="subtitle">Periode: <b>@spec.Period</b></p>
</div>
<div class="meta">
<div class="pill">Medarbejdernr.: <b>@Model.EmployeeNumber</b></div>
<div class="kv">
<span>Medarbejder:</span><b>@Model.EmployeeName</b>
<span>Afdeling:</span><b>@Model.Department</b>
<span>Ansættelse:</span><b>@Model.EmploymentType</b>
</div>
</div>
</div>
<!-- Total -->
<section class="total avoid-break">
<div>
<div class="label">Bruttoløn (@spec.Period)</div>
<p class="big">@spec.GrossSalary.ToString("N2", culture) <small>kr</small></p>
</div>
</section>
<!-- Grid: Summary + Balances -->
<section class="grid">
<div class="card avoid-break">
<div class="hd">
<h2>Samlet lønopgørelse</h2>
</div>
<div class="bd">
<table>
<thead>
<tr>
<th>Løndel</th>
<th class="num">Beløb</th>
</tr>
</thead>
<tbody>
<tr>
<td>Grundløn inkl. overarbejde</td>
<td class="num">@((spec.BasePay + spec.OvertimePay).ToString("N2", culture)) kr</td>
</tr>
<tr>
<td>Provision i alt</td>
<td class="num">@((spec.ServiceCommission + spec.ProductCommission).ToString("N2", culture)) kr</td>
</tr>
<tr>
<td><b>Bruttoløn</b></td>
<td class="num"><b>@spec.GrossSalary.ToString("N2", culture) kr</b></td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="card avoid-break">
<div class="hd">
<h2>Satser</h2>
</div>
<div class="bd">
<table>
<thead>
<tr>
<th>Type</th>
<th class="num">Værdi</th>
</tr>
</thead>
<tbody>
<tr>
<td>Timeløn</td>
<td class="num">@spec.Config.HourlyRate.ToString("N0", culture) kr</td>
</tr>
<tr>
<td>Normtid</td>
<td class="num">@spec.Config.WeeklyHours t/uge</td>
</tr>
<tr>
<td>Overtidstillæg</td>
<td class="num">@spec.Config.OvertimeFormatted</td>
</tr>
<tr>
<td>Minimum pr. time</td>
<td class="num">@spec.Config.MinimumPerHour.ToString("N0", culture) kr</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
<!-- Quick Summary -->
<section class="card avoid-break">
<div class="hd">
<h2>Hurtigt resumé</h2>
</div>
<div class="bd">
<table>
<thead>
<tr>
<th>Nøglepunkt</th>
<th class="num">Værdi</th>
</tr>
</thead>
<tbody>
<tr>
<td>Normaltimer</td>
<td class="num">@spec.Weeks.Sum(w => w.NormalHours) t</td>
</tr>
<tr>
<td>Overarbejde</td>
<td class="num">@spec.Weeks.Sum(w => w.OvertimeHours) t</td>
</tr>
<tr>
<td>Provision (services + produkter)</td>
<td class="num">@((spec.ServiceCommission + spec.ProductCommission).ToString("N2", culture)) kr</td>
</tr>
<tr>
<td>Feriedage</td>
<td class="num">@spec.Weeks.Sum(w => w.VacationDays) dage</td>
</tr>
</tbody>
</table>
</div>
</section>
<div class="ftr">
<div>Overblik</div>
<div style="text-align:right">Lønspecifikation · @spec.Period</div>
</div>
<div class="page-break"></div>
<!-- Page 2: Details -->
<section class="card">
<div class="hd">
<h2>Arbejdstid pr. uge</h2>
</div>
<div class="bd">
<table>
<thead>
<tr>
<th>Uge</th>
<th class="num">Normaltimer</th>
<th class="num">Overtid</th>
<th class="num">Services</th>
<th class="num">Produkter</th>
<th class="num">Provision</th>
<th class="num">I alt</th>
</tr>
</thead>
<tbody>
@foreach (var week in spec.Weeks)
{
<tr>
<td>Uge @week.WeekNumber</td>
<td class="num">@week.NormalHours t</td>
<td class="num">@week.OvertimeHours t</td>
<td class="num">@week.ServiceRevenue.ToString("N0", culture) kr</td>
<td class="num">@week.ProductRevenue.ToString("N0", culture) kr</td>
<td class="num">@week.Commission.ToString("N2", culture) kr</td>
<td class="num">@week.TotalPay.ToString("N2", culture) kr</td>
</tr>
}
<tr>
<td><b>I alt</b></td>
<td class="num"><b>@spec.Weeks.Sum(w => w.NormalHours) t</b></td>
<td class="num"><b>@spec.Weeks.Sum(w => w.OvertimeHours) t</b></td>
<td class="num"><b>@spec.Weeks.Sum(w => w.ServiceRevenue).ToString("N0", culture) kr</b></td>
<td class="num"><b>@spec.Weeks.Sum(w => w.ProductRevenue).ToString("N0", culture) kr</b></td>
<td class="num"><b>@spec.Weeks.Sum(w => w.Commission).ToString("N2", culture) kr</b></td>
<td class="num"><b>@spec.Weeks.Sum(w => w.TotalPay).ToString("N2", culture) kr</b></td>
</tr>
</tbody>
</table>
<p class="note">
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.
</p>
</div>
</section>
<section class="card">
<div class="hd">
<h2>Provision</h2>
</div>
<div class="bd">
<p class="note" style="margin-top:0">
<b>Services:</b> @spec.Config.ServiceCommissionPct% af omsætning over minimum (@spec.Config.MinimumPerHour.ToString("N0", culture) kr/time).<br/>
<b>Produkter:</b> @spec.Config.ProductCommissionPct% af salg.
</p>
<table>
<thead>
<tr>
<th>Uge</th>
<th class="num">Service oms.</th>
<th class="num">Produkt oms.</th>
<th class="num">Minimum</th>
<th class="num">Provision</th>
</tr>
</thead>
<tbody>
@foreach (var week in spec.Weeks)
{
<tr>
<td>Uge @week.WeekNumber</td>
<td class="num">@week.ServiceRevenue.ToString("N0", culture) kr</td>
<td class="num">@week.ProductRevenue.ToString("N0", culture) kr</td>
<td class="num">@week.MinimumThreshold.ToString("N0", culture) kr</td>
<td class="num">@week.Commission.ToString("N2", culture) kr</td>
</tr>
}
<tr>
<td><b>I alt</b></td>
<td class="num"><b>@spec.Weeks.Sum(w => w.ServiceRevenue).ToString("N0", culture) kr</b></td>
<td class="num"><b>@spec.Weeks.Sum(w => w.ProductRevenue).ToString("N0", culture) kr</b></td>
<td class="num">-</td>
<td class="num"><b>@spec.Weeks.Sum(w => w.Commission).ToString("N2", culture) kr</b></td>
</tr>
</tbody>
</table>
</div>
</section>
<div class="ftr">
<div>Detaljer</div>
<div style="text-align:right">Lønspecifikation · @spec.Period</div>
</div>
</div>
</body>
</html>