Salary module improvements (#250)
* feat: implement salary module with personnummer encryption, salary entries, tax tables, and AGI tracking - Added personnummer encryption and decryption functions for secure storage. - Created salary entries handling for journal entries including gross salary, tax withholding, and employer contributions. - Implemented tax table lookup functionality for calculating tax amounts based on monthly income. - Developed SQL migration for salary module including tables for payroll configuration, tax rates, employees, salary runs, salary run employees, salary line items, and AGI declarations. - Established row-level security policies for all new tables to ensure company-scoped access. * feat: add salary calculation modules for 2026 - Implemented engångsskatt calculation for one-time payments with tax brackets. - Added löneväxling functionality for salary sacrifice to pension, including employer savings and warnings. - Created pain.001 generator for salary batch payments in compliance with Swedish banking standards. - Developed PDF template for payslips, including detailed breakdowns and employer costs. - Generated seed data for Swedish tax tables for 2026, including SQL insert statements. - Implemented traktamente calculations for per diem and mileage allowances, adhering to Skatteverket regulations. - Added seed script for populating tax tables in the database. * feat: Update meal reduction percentages in traktamente calculation fix: Remove obsolete seed script for 2026 tax tables feat: Extend SalaryRunStatus type to include 'corrected' status feat: Implement KU10 XML generation endpoint for annual employee income statements feat: Add endpoint for creating corrections to booked salary runs feat: Implement endpoint for sending payslip PDFs to employees feat: Create KU10 XML generator for annual reporting feat: Add salary transaction matcher for auto-linking bank transactions to salary entries chore: Add database migration for salary correction support * feat: replace select elements with custom Select component for employment and salary types * feat: enhance salary calculations with pension entry and avgifter category support * feat: enhance employee management with salary type, tax status, and validation improvements * feat: Implement AGI submission flow to Skatteverket - Added AGI submission route to handle the submission process. - Created AGI client for interacting with Skatteverket's API. - Introduced AGI mappers to convert salary run data into the required AGI JSON payload format. - Enhanced API client to support custom base URLs for Skatteverket API requests. - Added types for AGI submission payload and validation results. - Implemented tests for AGI mappers to ensure correct payload structure and data handling. * feat: enhance salary module with Skatteverket integration and update dashboard navigation * Update app/api/salary/runs/[id]/agi/submit/route.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * Update app/api/salary/runs/[id]/approve/route.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * feat: integrate write permission check and remove Skatteverket extension --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
greptile-apps[bot]
parent
24466c6e94
commit
bb0db7a588
@@ -12,7 +12,6 @@ import { ArrowLeft, Save, Trash2 } from 'lucide-react'
|
||||
import { useToast } from '@/components/ui/use-toast'
|
||||
import { useCanWrite } from '@/lib/hooks/use-can-write'
|
||||
import { getErrorMessage } from '@/lib/errors/get-error-message'
|
||||
import { formatCurrency } from '@/lib/utils'
|
||||
import type { Employee } from '@/types'
|
||||
|
||||
const EMPLOYMENT_LABELS: Record<string, string> = {
|
||||
@@ -21,6 +20,17 @@ const EMPLOYMENT_LABELS: Record<string, string> = {
|
||||
board_member: 'Styrelseledamot',
|
||||
}
|
||||
|
||||
const F_SKATT_LABELS: Record<string, string> = {
|
||||
a_skatt: 'A-skatt',
|
||||
f_skatt: 'F-skatt',
|
||||
fa_skatt: 'FA-skatt',
|
||||
not_verified: 'Ej verifierad',
|
||||
}
|
||||
|
||||
function RequiredMark() {
|
||||
return <span className="text-destructive ml-0.5">*</span>
|
||||
}
|
||||
|
||||
export default function EmployeeDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = use(params)
|
||||
const router = useRouter()
|
||||
@@ -30,6 +40,12 @@ export default function EmployeeDetailPage({ params }: { params: Promise<{ id: s
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [employmentType, setEmploymentType] = useState('employee')
|
||||
const [salaryType, setSalaryType] = useState('monthly')
|
||||
const [fSkattStatus, setFSkattStatus] = useState('a_skatt')
|
||||
const [isSidoinkomst, setIsSidoinkomst] = useState(false)
|
||||
const [vacationRule, setVacationRule] = useState('procentregeln')
|
||||
|
||||
const requiresTaxTable = fSkattStatus === 'a_skatt' && !isSidoinkomst
|
||||
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
@@ -38,6 +54,10 @@ export default function EmployeeDetailPage({ params }: { params: Promise<{ id: s
|
||||
const { data } = await res.json()
|
||||
setEmployee(data)
|
||||
setEmploymentType(data.employment_type)
|
||||
setSalaryType(data.salary_type || 'monthly')
|
||||
setFSkattStatus(data.f_skatt_status || 'a_skatt')
|
||||
setIsSidoinkomst(data.is_sidoinkomst || false)
|
||||
setVacationRule(data.vacation_rule || 'procentregeln')
|
||||
}
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -49,19 +69,33 @@ export default function EmployeeDetailPage({ params }: { params: Promise<{ id: s
|
||||
setSaving(true)
|
||||
|
||||
const form = new FormData(e.currentTarget)
|
||||
const body = {
|
||||
const body: Record<string, unknown> = {
|
||||
first_name: form.get('first_name') as string,
|
||||
last_name: form.get('last_name') as string,
|
||||
employment_type: employmentType,
|
||||
employment_degree: parseFloat(form.get('employment_degree') as string) || 100,
|
||||
monthly_salary: parseFloat(form.get('monthly_salary') as string) || undefined,
|
||||
hourly_rate: parseFloat(form.get('hourly_rate') as string) || undefined,
|
||||
salary_type: salaryType,
|
||||
f_skatt_status: fSkattStatus,
|
||||
is_sidoinkomst: isSidoinkomst,
|
||||
tax_table_number: parseInt(form.get('tax_table_number') as string) || undefined,
|
||||
tax_column: parseInt(form.get('tax_column') as string) || 1,
|
||||
tax_municipality: form.get('tax_municipality') as string || 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,
|
||||
}
|
||||
|
||||
// Include salary field matching the current salary_type
|
||||
if (salaryType === 'monthly') {
|
||||
body.monthly_salary = parseFloat(form.get('monthly_salary') as string) || undefined
|
||||
} else {
|
||||
body.hourly_rate = parseFloat(form.get('hourly_rate') as string) || undefined
|
||||
}
|
||||
|
||||
const res = await fetch(`/api/salary/employees/${id}`, {
|
||||
@@ -134,22 +168,66 @@ export default function EmployeeDetailPage({ params }: { params: Promise<{ id: s
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSave} className="space-y-6">
|
||||
{/* Personal info */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Uppgifter</CardTitle>
|
||||
<CardTitle className="text-base">Personuppgifter</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="first_name">Förnamn</Label>
|
||||
<Label htmlFor="first_name">Förnamn<RequiredMark /></Label>
|
||||
<Input id="first_name" name="first_name" defaultValue={employee.first_name} required disabled={!canWrite} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="last_name">Efternamn</Label>
|
||||
<Label htmlFor="last_name">Efternamn<RequiredMark /></Label>
|
||||
<Input id="last_name" name="last_name" defaultValue={employee.last_name} required disabled={!canWrite} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">E-post</Label>
|
||||
<Input id="email" name="email" type="email" defaultValue={employee.email || ''} disabled={!canWrite} />
|
||||
<p className="text-xs text-muted-foreground">Krävs för att skicka lönebesked</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="phone">Telefon</Label>
|
||||
<Input id="phone" name="phone" defaultValue={employee.phone || ''} disabled={!canWrite} />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Address */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Adress</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="address_line1">Gatuadress</Label>
|
||||
<Input id="address_line1" name="address_line1" defaultValue={employee.address_line1 || ''} disabled={!canWrite} />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="postal_code">Postnummer</Label>
|
||||
<Input id="postal_code" name="postal_code" defaultValue={employee.postal_code || ''} className="max-w-[160px]" disabled={!canWrite} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="city">Ort</Label>
|
||||
<Input id="city" name="city" defaultValue={employee.city || ''} disabled={!canWrite} />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Employment */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Anställning</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="employment_type">Typ</Label>
|
||||
<Select value={employmentType} onValueChange={setEmploymentType} disabled={!canWrite}>
|
||||
@@ -165,37 +243,165 @@ export default function EmployeeDetailPage({ params }: { params: Promise<{ id: s
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="employment_degree">Sysselsättningsgrad (%)</Label>
|
||||
<Input id="employment_degree" name="employment_degree" type="number" defaultValue={employee.employment_degree} disabled={!canWrite} />
|
||||
<Input id="employment_degree" name="employment_degree" type="number" defaultValue={employee.employment_degree} min="1" max="100" disabled={!canWrite} />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Salary */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Lön</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="monthly_salary">Månadslön</Label>
|
||||
<Input id="monthly_salary" name="monthly_salary" type="number" defaultValue={employee.monthly_salary || ''} disabled={!canWrite} />
|
||||
<Label htmlFor="salary_type">Löneform<RequiredMark /></Label>
|
||||
<Select value={salaryType} onValueChange={setSalaryType} disabled={!canWrite}>
|
||||
<SelectTrigger id="salary_type">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="monthly">Månadslön</SelectItem>
|
||||
<SelectItem value="hourly">Timlön</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{salaryType === 'monthly' ? (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="monthly_salary">Månadslön (brutto, SEK)<RequiredMark /></Label>
|
||||
<Input id="monthly_salary" name="monthly_salary" type="number" step="1" min="1" defaultValue={employee.monthly_salary || ''} required disabled={!canWrite} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="hourly_rate">Timlön (SEK)<RequiredMark /></Label>
|
||||
<Input id="hourly_rate" name="hourly_rate" type="number" step="0.01" min="0.01" defaultValue={employee.hourly_rate || ''} required disabled={!canWrite} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Tax */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Skatt</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="f_skatt_status">Skatteform</Label>
|
||||
<Select value={fSkattStatus} onValueChange={setFSkattStatus} disabled={!canWrite}>
|
||||
<SelectTrigger id="f_skatt_status">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="a_skatt">A-skatt</SelectItem>
|
||||
<SelectItem value="f_skatt">F-skatt</SelectItem>
|
||||
<SelectItem value="fa_skatt">FA-skatt</SelectItem>
|
||||
<SelectItem value="not_verified">Ej verifierad</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{employee.f_skatt_verified_at && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Verifierad: {new Date(employee.f_skatt_verified_at).toLocaleDateString('sv-SE')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-end pb-2">
|
||||
<label className="flex items-center gap-2 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isSidoinkomst}
|
||||
onChange={(e) => setIsSidoinkomst(e.target.checked)}
|
||||
disabled={!canWrite}
|
||||
className="rounded border-border"
|
||||
/>
|
||||
Sidoinkomst (30% skatteavdrag)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="hourly_rate">Timlön</Label>
|
||||
<Input id="hourly_rate" name="hourly_rate" type="number" step="0.01" defaultValue={employee.hourly_rate || ''} disabled={!canWrite} />
|
||||
<Label htmlFor="tax_table_number">
|
||||
Skattetabell (29-42){requiresTaxTable && <RequiredMark />}
|
||||
</Label>
|
||||
<Input
|
||||
id="tax_table_number"
|
||||
name="tax_table_number"
|
||||
type="number"
|
||||
min="29"
|
||||
max="42"
|
||||
defaultValue={employee.tax_table_number || ''}
|
||||
required={requiresTaxTable}
|
||||
disabled={!canWrite}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">Baseras på folkbokföringskommun</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="tax_table_number">Skattetabell</Label>
|
||||
<Input id="tax_table_number" name="tax_table_number" type="number" defaultValue={employee.tax_table_number || ''} disabled={!canWrite} />
|
||||
<Label htmlFor="tax_column">Kolumn (1-6)</Label>
|
||||
<Input id="tax_column" name="tax_column" type="number" defaultValue={employee.tax_column} min="1" max="6" disabled={!canWrite} />
|
||||
<p className="text-xs text-muted-foreground">1 = standard under 66 år</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="tax_column">Kolumn</Label>
|
||||
<Input id="tax_column" name="tax_column" type="number" defaultValue={employee.tax_column} disabled={!canWrite} />
|
||||
<Label htmlFor="tax_municipality">
|
||||
Folkbokföringskommun{requiresTaxTable && <RequiredMark />}
|
||||
</Label>
|
||||
<Input
|
||||
id="tax_municipality"
|
||||
name="tax_municipality"
|
||||
defaultValue={employee.tax_municipality || ''}
|
||||
required={requiresTaxTable}
|
||||
disabled={!canWrite}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Vacation */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Semester</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">E-post</Label>
|
||||
<Input id="email" name="email" type="email" defaultValue={employee.email || ''} disabled={!canWrite} />
|
||||
<Label htmlFor="vacation_rule">Semesterregel</Label>
|
||||
<Select value={vacationRule} onValueChange={setVacationRule} disabled={!canWrite}>
|
||||
<SelectTrigger id="vacation_rule">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="procentregeln">Procentregeln (12%)</SelectItem>
|
||||
<SelectItem value="sammaloneregeln">Sammalöneregeln</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="phone">Telefon</Label>
|
||||
<Input id="phone" name="phone" defaultValue={employee.phone || ''} disabled={!canWrite} />
|
||||
<Label htmlFor="vacation_days_per_year">Semesterdagar per år</Label>
|
||||
<Input
|
||||
id="vacation_days_per_year"
|
||||
name="vacation_days_per_year"
|
||||
type="number"
|
||||
min="25"
|
||||
max="40"
|
||||
defaultValue={employee.vacation_days_per_year}
|
||||
disabled={!canWrite}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">Lagstadgat minimum: 25 dagar</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Bank */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Bankkonto</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="clearing_number">Clearingnummer</Label>
|
||||
@@ -206,6 +412,7 @@ export default function EmployeeDetailPage({ params }: { params: Promise<{ id: s
|
||||
<Input id="bank_account_number" name="bank_account_number" defaultValue={employee.bank_account_number || ''} disabled={!canWrite} />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-2">Krävs innan lönekörning kan godkännas</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
@@ -12,12 +12,20 @@ import { ArrowLeft, Save } from 'lucide-react'
|
||||
import { useToast } from '@/components/ui/use-toast'
|
||||
import { getErrorMessage } from '@/lib/errors/get-error-message'
|
||||
|
||||
function RequiredMark() {
|
||||
return <span className="text-destructive ml-0.5">*</span>
|
||||
}
|
||||
|
||||
export default function NewEmployeePage() {
|
||||
const router = useRouter()
|
||||
const { toast } = useToast()
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [employmentType, setEmploymentType] = useState('employee')
|
||||
const [salaryType, setSalaryType] = useState('monthly')
|
||||
const [fSkattStatus, setFSkattStatus] = useState('a_skatt')
|
||||
const [isSidoinkomst, setIsSidoinkomst] = useState(false)
|
||||
|
||||
const requiresTaxTable = fSkattStatus === 'a_skatt' && !isSidoinkomst
|
||||
|
||||
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault()
|
||||
@@ -32,13 +40,18 @@ export default function NewEmployeePage() {
|
||||
employment_start: form.get('employment_start') as string,
|
||||
employment_degree: parseFloat(form.get('employment_degree') as string) || 100,
|
||||
salary_type: salaryType,
|
||||
monthly_salary: parseFloat(form.get('monthly_salary') as string) || undefined,
|
||||
hourly_rate: parseFloat(form.get('hourly_rate') as string) || undefined,
|
||||
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: fSkattStatus,
|
||||
is_sidoinkomst: isSidoinkomst,
|
||||
tax_table_number: parseInt(form.get('tax_table_number') as string) || undefined,
|
||||
tax_column: parseInt(form.get('tax_column') as string) || 1,
|
||||
tax_municipality: form.get('tax_municipality') as string || 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,
|
||||
}
|
||||
@@ -82,23 +95,24 @@ export default function NewEmployeePage() {
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="first_name">Förnamn</Label>
|
||||
<Label htmlFor="first_name">Förnamn<RequiredMark /></Label>
|
||||
<Input id="first_name" name="first_name" required />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="last_name">Efternamn</Label>
|
||||
<Label htmlFor="last_name">Efternamn<RequiredMark /></Label>
|
||||
<Input id="last_name" name="last_name" required />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="personnummer">Personnummer (12 siffror)</Label>
|
||||
<Label htmlFor="personnummer">Personnummer (12 siffror)<RequiredMark /></Label>
|
||||
<Input id="personnummer" name="personnummer" placeholder="ÅÅÅÅMMDDNNNN" required maxLength={13} />
|
||||
<p className="text-xs text-muted-foreground">Krypteras vid lagring</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">E-post</Label>
|
||||
<Input id="email" name="email" type="email" />
|
||||
<p className="text-xs text-muted-foreground">Krävs för att skicka lönebesked</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
@@ -108,6 +122,29 @@ export default function NewEmployeePage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Address */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Adress</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="address_line1">Gatuadress</Label>
|
||||
<Input id="address_line1" name="address_line1" />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="postal_code">Postnummer</Label>
|
||||
<Input id="postal_code" name="postal_code" className="max-w-[160px]" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="city">Ort</Label>
|
||||
<Input id="city" name="city" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Employment */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -129,7 +166,7 @@ export default function NewEmployeePage() {
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="employment_start">Anställningsdatum</Label>
|
||||
<Label htmlFor="employment_start">Anställningsdatum<RequiredMark /></Label>
|
||||
<Input id="employment_start" name="employment_start" type="date" required />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
@@ -146,9 +183,9 @@ export default function NewEmployeePage() {
|
||||
<CardTitle className="text-base">Lön</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="salary_type">Löneform</Label>
|
||||
<Label htmlFor="salary_type">Löneform<RequiredMark /></Label>
|
||||
<Select value={salaryType} onValueChange={setSalaryType}>
|
||||
<SelectTrigger id="salary_type">
|
||||
<SelectValue />
|
||||
@@ -159,14 +196,17 @@ export default function NewEmployeePage() {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="monthly_salary">Månadslön (brutto, SEK)</Label>
|
||||
<Input id="monthly_salary" name="monthly_salary" type="number" step="1" min="0" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="hourly_rate">Timlön (SEK)</Label>
|
||||
<Input id="hourly_rate" name="hourly_rate" type="number" step="0.01" min="0" />
|
||||
</div>
|
||||
{salaryType === 'monthly' ? (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="monthly_salary">Månadslön (brutto, SEK)<RequiredMark /></Label>
|
||||
<Input id="monthly_salary" name="monthly_salary" type="number" step="1" min="1" required />
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="hourly_rate">Timlön (SEK)<RequiredMark /></Label>
|
||||
<Input id="hourly_rate" name="hourly_rate" type="number" step="0.01" min="0.01" required />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -177,10 +217,46 @@ export default function NewEmployeePage() {
|
||||
<CardTitle className="text-base">Skatt</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="f_skatt_status">Skatteform</Label>
|
||||
<Select value={fSkattStatus} onValueChange={setFSkattStatus}>
|
||||
<SelectTrigger id="f_skatt_status">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="a_skatt">A-skatt</SelectItem>
|
||||
<SelectItem value="f_skatt">F-skatt</SelectItem>
|
||||
<SelectItem value="fa_skatt">FA-skatt</SelectItem>
|
||||
<SelectItem value="not_verified">Ej verifierad</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-end pb-2">
|
||||
<label className="flex items-center gap-2 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isSidoinkomst}
|
||||
onChange={(e) => setIsSidoinkomst(e.target.checked)}
|
||||
className="rounded border-border"
|
||||
/>
|
||||
Sidoinkomst (30% skatteavdrag)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="tax_table_number">Skattetabell (29-42)</Label>
|
||||
<Input id="tax_table_number" name="tax_table_number" type="number" min="29" max="42" />
|
||||
<Label htmlFor="tax_table_number">
|
||||
Skattetabell (29-42){requiresTaxTable && <RequiredMark />}
|
||||
</Label>
|
||||
<Input
|
||||
id="tax_table_number"
|
||||
name="tax_table_number"
|
||||
type="number"
|
||||
min="29"
|
||||
max="42"
|
||||
required={requiresTaxTable}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">Baseras på folkbokföringskommun</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
@@ -189,8 +265,10 @@ export default function NewEmployeePage() {
|
||||
<p className="text-xs text-muted-foreground">1 = standard under 66 år</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="tax_municipality">Folkbokföringskommun</Label>
|
||||
<Input id="tax_municipality" name="tax_municipality" />
|
||||
<Label htmlFor="tax_municipality">
|
||||
Folkbokföringskommun{requiresTaxTable && <RequiredMark />}
|
||||
</Label>
|
||||
<Input id="tax_municipality" name="tax_municipality" required={requiresTaxTable} />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -212,6 +290,7 @@ export default function NewEmployeePage() {
|
||||
<Input id="bank_account_number" name="bank_account_number" />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-2">Krävs innan lönekörning kan godkännas</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user