fix(salary): show the sysselsättningsgrad product next to the run salary input (#1702)
A Discord report had a 10 % employee: typing 4 531 in the run gave a 453,10 kr gross, so the user typed 45 310 to get it right. The engine was correct (grundlön = månadslön × sysselsättningsgrad / 100) but nothing on the row said so; the formula only lived in Beräkningsdetaljer. Below 100 % the row now prints "× 10 % = 4 531 kr" under the monthly salary (input and read-only shapes), and both employee forms explain under Sysselsättningsgrad that the base salary is monthly salary × degree. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Jakob Wennberg
Claude Fable 5
parent
f101bde6a8
commit
eb0df1722b
@@ -317,6 +317,7 @@ export default function EmployeeDetailPage({ params }: { params: Promise<{ id: s
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="employment_degree">{t('form_employment_degree')}</Label>
|
||||
<Input id="employment_degree" name="employment_degree" type="number" defaultValue={employee.employment_degree} min="1" max="100" disabled={!canWrite} />
|
||||
<p className="text-xs text-muted-foreground">{t('form_employment_degree_hint')}</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="hours_per_week">{t('form_hours_per_week')}</Label>
|
||||
|
||||
@@ -294,6 +294,7 @@ function NewEmployeeForm({ onCreated, onCancel }: { onCreated: () => void; onCan
|
||||
</Field>
|
||||
<Field label="Sysselsättningsgrad (%)" htmlFor="employment_degree">
|
||||
<Input id="employment_degree" name="employment_degree" type="number" defaultValue="100" min="1" max="100" />
|
||||
<p className="text-xs text-muted-foreground">Under 100 % räknas grundlönen som månadslön × sysselsättningsgrad.</p>
|
||||
</Field>
|
||||
<Field label="Timmar per vecka" htmlFor="hours_per_week">
|
||||
<Input id="hours_per_week" name="hours_per_week" type="number" defaultValue="40" min="1" max="80" step="0.5" />
|
||||
|
||||
@@ -142,6 +142,22 @@ export function RunEmployeesTable({
|
||||
const netValue = sre.net_salary + (sre.tax_withheld - taxValue)
|
||||
const editableSalary = isDraft && canWrite && sre.salary_type === 'monthly'
|
||||
const primaryNumber = isDraft ? sre.monthly_salary : sre.gross_salary
|
||||
// In a draft the number on the row is the full-time monthly salary
|
||||
// the engine multiplies by the employee's sysselsättningsgrad. Below
|
||||
// 100 % that product is not obvious from the input alone (a 10 %
|
||||
// employee typed 45 310 to get a 4 531 kr gross), so spell it out
|
||||
// right under the number instead of only in Beräkningsdetaljer.
|
||||
const degree = Number(sre.employment_degree)
|
||||
const showDegreeHint =
|
||||
isDraft && sre.salary_type === 'monthly' && Number.isFinite(degree) && degree < 100
|
||||
const degreeHint = showDegreeHint ? (
|
||||
<span className="text-[11px] leading-tight text-muted-foreground tabular-nums whitespace-nowrap">
|
||||
{t('degree_hint', {
|
||||
degree: degree.toLocaleString('sv-SE'),
|
||||
amount: formatCurrency(roundOre((sre.monthly_salary || 0) * (degree / 100))),
|
||||
})}
|
||||
</span>
|
||||
) : null
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -176,21 +192,27 @@ export function RunEmployeesTable({
|
||||
|
||||
{/* Right side: editable monthly salary (draft) or the number */}
|
||||
{editableSalary ? (
|
||||
<Input
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
defaultValue={sre.monthly_salary}
|
||||
onClick={e => e.stopPropagation()}
|
||||
onBlur={e => onSalaryEdit(sre.employee_id, e.target.value, sre.monthly_salary)}
|
||||
disabled={actionLoading === `salary-${sre.employee_id}`}
|
||||
aria-label={t('salary_input_aria', { name })}
|
||||
className="h-8 w-28 text-right tabular-nums shrink-0"
|
||||
/>
|
||||
<div className="flex flex-col items-end gap-0.5 shrink-0">
|
||||
<Input
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
defaultValue={sre.monthly_salary}
|
||||
onClick={e => e.stopPropagation()}
|
||||
onBlur={e => onSalaryEdit(sre.employee_id, e.target.value, sre.monthly_salary)}
|
||||
disabled={actionLoading === `salary-${sre.employee_id}`}
|
||||
aria-label={t('salary_input_aria', { name })}
|
||||
className="h-8 w-28 text-right tabular-nums"
|
||||
/>
|
||||
{degreeHint}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-right tabular-nums font-medium shrink-0">
|
||||
{formatCurrency(primaryNumber)}
|
||||
</span>
|
||||
<div className="flex flex-col items-end gap-0.5 shrink-0">
|
||||
<span className="text-right tabular-nums font-medium">
|
||||
{formatCurrency(primaryNumber)}
|
||||
</span>
|
||||
{degreeHint}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Payslip PDF */}
|
||||
|
||||
@@ -6703,6 +6703,7 @@
|
||||
"view_pdf": "View PDF",
|
||||
"view_payslip_title": "View payslip",
|
||||
"salary_input_aria": "Monthly salary for {name}",
|
||||
"degree_hint": "× {degree} % = {amount}",
|
||||
"remove_sr": "Remove",
|
||||
"remove_employee_aria": "Remove {name} from the payroll run",
|
||||
"remove_employee_title": "Remove from the payroll run",
|
||||
@@ -6937,6 +6938,7 @@
|
||||
"form_employment_start": "Employment start date",
|
||||
"form_employment_end": "End date",
|
||||
"form_employment_degree": "Degree of employment (%)",
|
||||
"form_employment_degree_hint": "Below 100 % the base salary is monthly salary × degree of employment.",
|
||||
"form_hours_per_week": "Hours per week",
|
||||
"form_workdays_per_week": "Workdays per week",
|
||||
"form_work_schedule_hint": "The work schedule drives daily rates for absence and hourly-rate derivation (40 h / 5 days = default).",
|
||||
|
||||
@@ -6703,6 +6703,7 @@
|
||||
"view_pdf": "Visa PDF",
|
||||
"view_payslip_title": "Visa lönespecifikation",
|
||||
"salary_input_aria": "Månadslön för {name}",
|
||||
"degree_hint": "× {degree} % = {amount}",
|
||||
"remove_sr": "Ta bort",
|
||||
"remove_employee_aria": "Ta bort {name} från lönekörningen",
|
||||
"remove_employee_title": "Ta bort från lönekörningen",
|
||||
@@ -6937,6 +6938,7 @@
|
||||
"form_employment_start": "Anställningsdatum",
|
||||
"form_employment_end": "Slutdatum",
|
||||
"form_employment_degree": "Sysselsättningsgrad (%)",
|
||||
"form_employment_degree_hint": "Under 100 % räknas grundlönen som månadslön × sysselsättningsgrad.",
|
||||
"form_hours_per_week": "Timmar per vecka",
|
||||
"form_workdays_per_week": "Arbetsdagar per vecka",
|
||||
"form_work_schedule_hint": "Arbetsschemat styr dagslön vid frånvaro och timlöneberäkning (40 h/5 dagar = standard).",
|
||||
|
||||
Reference in New Issue
Block a user