'use client' import { useState } from 'react' import Link from 'next/link' import { useRouter } from 'next/navigation' import { useTranslations } from 'next-intl' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { ChevronRight, FileDown, Loader2, Trash2 } from 'lucide-react' import { formatCurrency } from '@/lib/utils' import { roundOre } from '@/lib/money' import type { EmployeeMasked, SalaryRunEmployee } from '@/types' import type { RunDetail } from './types' type SreWithEmployee = SalaryRunEmployee & { employee?: { first_name: string last_name: string // The run detail endpoint returns the masked display form under this key; // the encrypted `personnummer` column never reaches the client. personnummer_masked: string default_dimensions?: Record } } interface RunEmployeesTableProps { run: RunDetail runId: string employees: SalaryRunEmployee[] availableEmployees: EmployeeMasked[] canWrite: boolean actionLoading: string | null dimensionsEnabled: boolean isCalculated: boolean onAddEmployee: (employeeId: string) => void onRemoveEmployee: (employeeId: string, name: string) => void onSalaryEdit: (employeeId: string, raw: string, previous: number) => void } export function RunEmployeesTable({ run, runId, employees, availableEmployees, canWrite, actionLoading, dimensionsEnabled, isCalculated, onAddEmployee, onRemoveEmployee, onSalaryEdit, }: RunEmployeesTableProps) { const t = useTranslations('salary_run') const router = useRouter() const [addEmployeeKey, setAddEmployeeKey] = useState(0) const addedEmployeeIds = new Set(employees.map(e => e.employee_id)) const notAdded = availableEmployees.filter(e => !addedEmployeeIds.has(e.id)) const canRemoveEmployee = run.status === 'draft' && canWrite const isDraft = run.status === 'draft' // Δ vs the latest booked run — hidden on the first-ever run and before // calculation (gross is 0 until then, the diff would be noise). const previous = run.previous_run ?? null const showDiff = previous != null && isCalculated function diffNode(sre: SalaryRunEmployee) { if (!previous) return null const prev = previous.by_employee[sre.employee_id] if (!prev) { return ( {t('diff_new_employee')} ) } const delta = roundOre(sre.gross_salary - prev.gross) if (delta === 0) return null const sign = delta > 0 ? '+' : '−' return ( {sign} {formatCurrency(Math.abs(delta))} ) } return (

{t('employees_title', { count: employees.length })}

{isDraft && canWrite && notAdded.length > 0 && ( )}
{employees.length === 0 ? (
{t('no_employees_yet')}
) : (
{employees.map(sre => { const employee = (sre as SreWithEmployee).employee const name = employee ? `${employee.first_name} ${employee.last_name}` : `${t('employee_fallback')} ${sre.employee_id.slice(0, 8)}...` const dims = employee?.default_dimensions ?? {} const dimLabel = Object.keys(dims) .sort((a, b) => Number(a) - Number(b)) .map(k => dims[k]) .join(' · ') const taxValue = sre.tax_withheld_override ?? sre.tax_withheld const avgifterValue = sre.avgifter_amount_override ?? sre.avgifter_amount 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 return (
router.push(`/salary/runs/${runId}/employees/${sre.employee_id}`)} > {/* Name + inline meta + (once calculated) a breakdown sub-line */}
e.stopPropagation()} > {name} {dimensionsEnabled && dimLabel && ( {dimLabel} )} {showDiff && diffNode(sre)}
{isCalculated && (

{t('th_tax')} {formatCurrency(taxValue)} · {t('th_net')}{' '} {formatCurrency(netValue)} · {t('th_avgifter')}{' '} {formatCurrency(avgifterValue)} · {t('th_vacation')}{' '} {formatCurrency(sre.vacation_accrual)}

)}
{/* Right side: editable monthly salary (draft) or the number */} {editableSalary ? ( 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" /> ) : ( {formatCurrency(primaryNumber)} )} {/* Payslip PDF */} e.stopPropagation()} className="shrink-0 text-muted-foreground hover:text-foreground transition-colors" title={t('view_payslip_title')} aria-label={t('view_payslip_title')} > {canRemoveEmployee && ( )}
) })}
)}
) }