'use client' import { useState, useEffect, useCallback } from 'react' import Link from 'next/link' import { useRouter } from 'next/navigation' import { useTranslations } from 'next-intl' import { Badge } from '@/components/ui/badge' import { Skeleton } from '@/components/ui/skeleton' import { Button } from '@/components/ui/button' import { EmptyState } from '@/components/ui/empty-state' import { TH_CLASS, TD_CLASS, QUIET_LINK_CLASS } from '@/components/ui/dry-table' import { HandCoins, Loader2, Plus, Users } 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 { cn, formatCurrency, formatDate } from '@/lib/utils' import type { EmployeeMasked, SalaryRun } from '@/types' const STATUS_LABEL_KEYS: Record = { draft: 'status_draft', review: 'status_review', approved: 'status_approved', paid: 'status_paid', booked: 'status_booked', corrected: 'status_corrected', } // In-flight states all wear the quiet beige chip (concept scene 22's Utkast // look): the payout-date note beside the chip carries the urgency, not an // ochre border. Booked renders as muted text, corrected as the outline // exception. const STATUS_VARIANTS: Record = { draft: 'secondary', review: 'secondary', approved: 'secondary', paid: 'success', booked: 'success', corrected: 'outline', } /** * Löner landing (concept scene 22): header + the lönekörningar dry-table, * nothing else. The open run's row is the way into the flow (chip + payout * date); AGI, skatt, blockers and semester live on the run detail and the * employee register. */ export default function SalaryPage() { const [runs, setRuns] = useState([]) const [employees, setEmployees] = useState([]) const [loading, setLoading] = useState(true) const [starting, setStarting] = useState(false) const { canWrite } = useCanWrite() const { toast } = useToast() const router = useRouter() const t = useTranslations('salary') const load = useCallback(async () => { const [runsRes, empRes] = await Promise.all([ fetch('/api/salary/runs').catch(() => null), fetch('/api/salary/employees').catch(() => null), ]) if (runsRes?.ok) { const { data } = await runsRes.json() setRuns(data || []) } if (empRes?.ok) { const { data } = await empRes.json() setEmployees(data || []) } setLoading(false) }, []) useEffect(() => { load() }, [load]) // One-click run creation: the API seeds all active employees, calculates, // and resolves period/pay-date/series defaults from settings. async function startRun() { setStarting(true) try { const res = await fetch('/api/salary/runs', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}', }) const json = await res.json().catch(() => null) if (res.status === 201 && json?.data?.id) { router.push(`/salary/runs/${json.data.id}`) return } const existingId = json?.error?.details?.existingId if (res.status === 409 && existingId) { toast({ title: t('run_exists_opening') }) router.push(`/salary/runs/${existingId}`) return } toast({ title: t('start_run_failed'), description: getErrorMessage(json, { context: 'salary', statusCode: res.status }), variant: 'destructive', }) } finally { setStarting(false) } } const periodOf = (r: SalaryRun) => `${r.period_year}-${String(r.period_month).padStart(2, '0')}` const header = (

{t('title')}

{t('employees')} {canWrite && ( )}
) if (loading) { return (
{header}
{[1, 2, 3].map(i => ( ))}
) } return (
{header} {runs.length === 0 && employees.length === 0 ? ( ) : (
{runs.length === 0 ? ( ) : (
{runs.slice(0, 12).map(run => { // PostgREST count embed: employee_count is [{ count: n }]. const employeeCount = ( run as SalaryRun & { employee_count?: { count: number }[] } ).employee_count?.[0]?.count const inFlight = run.status !== 'booked' && run.status !== 'corrected' return ( router.push(`/salary/runs/${run.id}`)} > ) })}
{t('th_period')} {t('th_status')} {t('th_employees')} {t('th_gross')} {t('th_net')}
e.stopPropagation()} > {periodOf(run)} {run.status === 'booked' ? ( {t('status_booked')} ) : ( {STATUS_LABEL_KEYS[run.status] ? t(STATUS_LABEL_KEYS[run.status]) : run.status} )} {inFlight && ( {t('run_payout_note', { date: formatDate(run.payment_date) })} )} {employeeCount ?? ''} {formatCurrency(run.total_gross)} {formatCurrency(run.total_net)}
)}
)}
) }