'use client' import { useState, useEffect } from 'react' import { Dialog, DialogContent, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' 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 { useToast } from '@/components/ui/use-toast' import { getErrorMessage } from '@/lib/errors/get-error-message' import EmployeeTaxCard, { type EmployeeTaxValue } from '@/components/salary/EmployeeTaxCard' import LineDimensionFields from '@/components/dimensions/LineDimensionFields' function RequiredMark() { return * } 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. Card sections carry over from the old /salary/employees/new page. */ export default function NewEmployeeDialog({ open, onOpenChange, onCreated }: Props) { return ( e.preventDefault()} onPointerDownOutside={(e) => e.preventDefault()} onInteractOutside={(e) => e.preventDefault()} > Ny anställd onOpenChange(false)} /> ) } // 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') // 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() 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, 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: form.get('clearing_number') as string || undefined, bank_account_number: form.get('bank_account_number') as string || 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) } return (
{/* Personal info */} Personuppgifter
setPersonnummer(e.target.value)} />

Krypteras vid lagring

Krävs för att skicka lönebesked

{/* Address */} Adress
{/* Employment */} Anställning
{/* Salary */} Lön
{salaryType === 'monthly' ? (
) : (
)}
{/* Default dimensions (kostnadsställe/projekt) */} {dimensionsEnabled && ( Kostnadsställe / Projekt (standard)

Föreslås på lönekostnadsrader vid bokföring av lönekörningar.

)} {/* Tax */} {/* Vacation */} Semester
{vacationRule === 'none' && (

Ingen avsättning till 2920 bokas. Vanligt för ägare som är enda anställd.

)} {vacationRule === 'semesterersattning' && (

12 % läggs på varje lönekörning och bokas mot 7285. Ingen semesterlöneskuld byggs upp. Vanligt för tim- och visstidsanställda.

)}

Lagstadgat minimum: 25 dagar

{/* Bank */} Bankkonto

Krävs innan lönekörning kan godkännas

{/* Actions */}
) }