From 8f15f986879796b2981316ceb6605fb6065dfe7a Mon Sep 17 00:00:00 2001
From: Mattsson <111893710+mattssonn@users.noreply.github.com>
Date: Tue, 12 May 2026 01:10:01 +0200
Subject: [PATCH] Bug/employees creation (#440)
* Fix/employees page salary display logic and labels
* feat: add salary_worked_days table and related functionality
- Implemented the salary_worked_days table to track per-day worked hours for hourly employees.
- Established row-level security (RLS) policies to ensure tenant isolation for salary_worked_days.
- Created unique index on (employee_id, work_date) to enforce uniqueness.
- Added trigger to enforce a 24-hour cap across worked and absence days for the same employee and date.
- Developed tests to validate RLS, uniqueness, and 24-hour cap logic.
* Fix: update hourly_salary calculation and refresh logic in salary run processing
---
app/(dashboard)/salary/employees/page.tsx | 6 +-
.../runs/[id]/employees/[employeeId]/page.tsx | 58 +-
.../salary/employees/[id]/absence/route.ts | 5 +
.../[id]/worked-hours/batch/route.ts | 97 ++
.../employees/[id]/worked-hours/route.ts | 190 ++++
app/api/salary/runs/[id]/calculate/route.ts | 75 +-
components/salary/AbsenceCalendar.tsx | 445 --------
components/salary/SalaryCalendar.tsx | 1004 +++++++++++++++++
lib/api/schemas.ts | 31 +
lib/import/shared/encoding.ts | 51 +-
.../__tests__/salary-worked-days.pg.test.ts | 163 +++
.../20260512120000_salary_worked_days.sql | 74 ++
...0_worked_days_absence_conflict_trigger.sql | 83 ++
13 files changed, 1802 insertions(+), 480 deletions(-)
create mode 100644 app/api/salary/employees/[id]/worked-hours/batch/route.ts
create mode 100644 app/api/salary/employees/[id]/worked-hours/route.ts
delete mode 100644 components/salary/AbsenceCalendar.tsx
create mode 100644 components/salary/SalaryCalendar.tsx
create mode 100644 lib/salary/__tests__/salary-worked-days.pg.test.ts
create mode 100644 supabase/migrations/20260512120000_salary_worked_days.sql
create mode 100644 supabase/migrations/20260512120100_worked_days_absence_conflict_trigger.sql
diff --git a/app/(dashboard)/salary/employees/page.tsx b/app/(dashboard)/salary/employees/page.tsx
index c1e7ea9d..fa73969c 100644
--- a/app/(dashboard)/salary/employees/page.tsx
+++ b/app/(dashboard)/salary/employees/page.tsx
@@ -84,7 +84,7 @@ export default function EmployeesPage() {
Namn
Personnummer
Typ
- Månadslön
+ Lön
Sysselsättningsgrad
Skattetabell
@@ -104,7 +104,9 @@ export default function EmployeesPage() {
{EMPLOYMENT_LABELS[emp.employment_type] || emp.employment_type}
- {emp.monthly_salary ? formatCurrency(emp.monthly_salary) : '—'}
+ {emp.salary_type === 'hourly'
+ ? emp.hourly_rate ? `${formatCurrency(emp.hourly_rate)}/tim` : '—'
+ : emp.monthly_salary ? formatCurrency(emp.monthly_salary) : '—'}
{emp.employment_degree}%
diff --git a/app/(dashboard)/salary/runs/[id]/employees/[employeeId]/page.tsx b/app/(dashboard)/salary/runs/[id]/employees/[employeeId]/page.tsx
index 31d35a2d..05c4b6bb 100644
--- a/app/(dashboard)/salary/runs/[id]/employees/[employeeId]/page.tsx
+++ b/app/(dashboard)/salary/runs/[id]/employees/[employeeId]/page.tsx
@@ -2,10 +2,10 @@
import { use, useEffect, useMemo, useState } from 'react'
import Link from 'next/link'
-import { ArrowLeft, Loader2 } from 'lucide-react'
+import { ArrowLeft, Calculator, Loader2 } from 'lucide-react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
-import { AbsenceCalendar } from '@/components/salary/AbsenceCalendar'
+import { SalaryCalendar } from '@/components/salary/SalaryCalendar'
import { formatCurrency } from '@/lib/utils'
import type { SalaryRun, SalaryRunEmployee, SalaryLineItem, SalaryLineItemType, Employee } from '@/types'
@@ -54,6 +54,10 @@ export default function SalaryRunEmployeeDetailPage({
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
+ const [calculating, setCalculating] = useState(false)
+ // Live counts pushed from the calendar — overrides the stale snapshot from
+ // the last calculation so badges update immediately on absence save.
+ const [liveCounts, setLiveCounts] = useState<{ sick: number; vab: number; parental: number } | null>(null)
const load = async () => {
setLoading(true)
@@ -80,6 +84,23 @@ export default function SalaryRunEmployeeDetailPage({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [runId, employeeId])
+ const handleCalculate = async () => {
+ setCalculating(true)
+ setError(null)
+ try {
+ const res = await fetch(`/api/salary/runs/${runId}/calculate`, { method: 'POST' })
+ const json = await res.json().catch(() => ({}))
+ if (!res.ok) {
+ throw new Error(json.error || 'Beräkning misslyckades')
+ }
+ await load()
+ } catch (e) {
+ setError(e instanceof Error ? e.message : 'Okänt fel')
+ } finally {
+ setCalculating(false)
+ }
+ }
+
const periodStart = useMemo(() => {
if (!data) return ''
const y = data.run.period_year
@@ -144,8 +165,18 @@ export default function SalaryRunEmployeeDetailPage({
{employee.personnummer} · Lönespecifikation {periodLabel}
-