'use client'
import { useState, useEffect } from 'react'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Save } from 'lucide-react'
import { cn } from '@/lib/utils'
import { useToast } from '@/components/ui/use-toast'
import { getErrorMessage } from '@/lib/errors/get-error-message'
import {
validateEmployeeBankAccount,
isValidClearing,
isValidAccount,
normalizeBankNumber,
lookupBankByClearing,
checkEmployeeAccountChecksum,
BANK_ISSUE_MESSAGES_SV,
BANK_CHECKSUM_WARNING_SV,
} from '@/lib/salary/payment/bank-account'
import EmployeeTaxCard, { type EmployeeTaxValue } from '@/components/salary/EmployeeTaxCard'
import LineDimensionFields from '@/components/dimensions/LineDimensionFields'
function RequiredMark() {
return *
}
// Small caps section label; sections are separated by a hairline divider
// instead of being boxed in their own cards (compact, Linear-style layout).
const SECTION_HEADER = 'text-xs font-semibold uppercase tracking-wider text-muted-foreground'
/** Compact label + control stack shared by every field in the dialog. */
function Field({
label,
htmlFor,
required,
className,
children,
}: {
label: string
htmlFor?: string
required?: boolean
className?: string
children: React.ReactNode
}) {
return (
{children}
)
}
interface Props {
open: boolean
onOpenChange: (open: boolean) => void
/** Fired after a successful create. Hosts close the dialog and refresh their list. */
onCreated: () => void
}
/**
* "Ny anställd" as a modal: mirrors NewSupplierInvoiceDialog. The last
* register entity (after customers/suppliers/articles) to move off a full
* page. Compact layout: borderless sections split by hairline dividers
* (no per-section cards) with a sticky Spara bar, to keep scrolling short.
*/
export default function NewEmployeeDialog({ open, onOpenChange, onCreated }: Props) {
return (
)
}
// Inner component so form state resets whenever the dialog reopens (Radix
// unmounts DialogContent children on close).
function NewEmployeeForm({ onCreated, onCancel }: { onCreated: () => void; onCancel: () => void }) {
const { toast } = useToast()
const [saving, setSaving] = useState(false)
const [employmentType, setEmploymentType] = useState('employee')
const [salaryType, setSalaryType] = useState('monthly')
const [personnummer, setPersonnummer] = useState('')
const [vacationRule, setVacationRule] = useState('procentregeln')
const [clearing, setClearing] = useState('')
const [account, setAccount] = useState('')
// Suppress the (soft) check-digit warning while the user is still typing in
// the bank fields; only surface it once they move focus away.
const [bankFocused, setBankFocused] = useState(false)
// Default dimensions bag ({sie_dim_no: object_code}) proposed on the
// employee's salary-cost lines at booking. The fields render only when
// company_settings.dimensions_enabled: same UI gate as the voucher form.
const [dimensionsEnabled, setDimensionsEnabled] = useState(false)
const [dimensions, setDimensions] = useState>({})
const [tax, setTax] = useState({
f_skatt_status: 'a_skatt',
is_sidoinkomst: false,
tax_table_number: null,
tax_column: 1,
tax_municipality: '',
})
useEffect(() => {
fetch('/api/settings')
.then((r) => r.json())
.then(({ data }) => setDimensionsEnabled(data?.dimensions_enabled === true))
.catch(() => {/* keep the dimension fields hidden */})
}, [])
function setDimension(dimNo: string, code: string | null) {
setDimensions((prev) => {
const next = { ...prev }
const value = code?.trim()
if (value) next[dimNo] = value
else delete next[dimNo]
return next
})
}
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
// Block on structurally invalid bank details before hitting the server.
const bankIssues = validateEmployeeBankAccount(clearing, account)
if (bankIssues.length > 0) {
toast({
title: 'Kontrollera bankuppgifterna',
description: bankIssues.map((i) => i.message).join('. '),
variant: 'destructive',
})
return
}
setSaving(true)
const form = new FormData(e.currentTarget)
const body = {
first_name: form.get('first_name') as string,
last_name: form.get('last_name') as string,
personnummer: personnummer.replace(/\D/g, ''),
employment_type: employmentType,
employment_start: form.get('employment_start') as string,
employment_end: form.get('employment_end') as string || undefined,
employment_degree: parseFloat(form.get('employment_degree') as string) || 100,
hours_per_week: parseFloat(form.get('hours_per_week') as string) || 40,
workdays_per_week: parseFloat(form.get('workdays_per_week') as string) || 5,
salary_type: salaryType,
monthly_salary: salaryType === 'monthly' ? (parseFloat(form.get('monthly_salary') as string) || undefined) : undefined,
hourly_rate: salaryType === 'hourly' ? (parseFloat(form.get('hourly_rate') as string) || undefined) : undefined,
f_skatt_status: tax.f_skatt_status,
is_sidoinkomst: tax.is_sidoinkomst,
tax_table_number: tax.tax_table_number ?? undefined,
tax_column: tax.tax_column,
tax_municipality: tax.tax_municipality || undefined,
email: form.get('email') as string || undefined,
phone: form.get('phone') as string || undefined,
address_line1: form.get('address_line1') as string || undefined,
postal_code: form.get('postal_code') as string || undefined,
city: form.get('city') as string || undefined,
clearing_number: normalizeBankNumber(clearing) || undefined,
bank_account_number: normalizeBankNumber(account) || undefined,
vacation_rule: vacationRule,
vacation_days_per_year: parseInt(form.get('vacation_days_per_year') as string) || 25,
// Always sent: {} means no default dimensions.
default_dimensions: dimensions,
}
const res = await fetch('/api/salary/employees', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
if (res.ok) {
toast({ title: 'Anställd skapad' })
onCreated()
} else {
const result = await res.json()
toast({
title: 'Kunde inte skapa anställd',
description: getErrorMessage(result, { context: 'salary', statusCode: res.status }),
variant: 'destructive',
})
}
setSaving(false)
}
const bankName = lookupBankByClearing(clearing)
const showChecksumWarning =
!bankFocused &&
validateEmployeeBankAccount(clearing, account).length === 0 &&
checkEmployeeAccountChecksum(clearing, account) === 'invalid'
return (
)
}