`.
+
+**Primitives — always use these, don't hand-roll.**
+
+| Need | Component | Notes |
+|---|---|---|
+| Page title + action | `components/ui/page-header.tsx` `PageHeader` | Use this, not bespoke `
` + ` ` blocks. Drop the `description` prop when it just paraphrases the title. |
+| Data table | `components/ui/table.tsx` `Table / TableHeader / TableHead / TableRow / TableCell` | Header style is baked in: `text-[11px] font-medium uppercase tracking-wider text-muted-foreground`. Wrap in `` when the table is a card's primary content. Add `tabular-nums` to numeric cells. |
+| Status indicator | `components/ui/badge.tsx` `` | Variants: `default / secondary / success / warning / destructive / outline`. **Never** use raw Tailwind colors (`bg-blue-100`, `bg-emerald-500/10`, etc.) for status. Map status → variant via a small `Record` per feature. |
+| No-data state | `components/ui/empty-state.tsx` `EmptyState` | Don't hand-roll `…
`. Preset variants exist (`EmptyInvoices`, `EmptyCustomers`, `EmptyTransactions`, etc.). |
+| Loading placeholder | `components/ui/skeleton.tsx` `` | Don't hand-roll `bg-muted rounded animate-pulse` divs. |
+| Inline help / formulas | `components/ui/info-tooltip.tsx` `InfoTooltip` | Hover-revealed; don't use always-visible info buttons. |
+| Fiscal year picker | `components/common/FiscalYearSelector.tsx` | Don't use raw `` for fiscal periods. |
+
+**Tabular display rules.**
+- All financial values get `tabular-nums`.
+- Dates in tables: `tabular-nums` for fixed width.
+- Right-align numeric columns (`text-right`).
+- For group bands inside tables (Resultatrapport-style): `{label} `.
+
+**Date formatting.** Two helpers in `lib/utils.ts`:
+- `formatDate(x)` → `2026-05-11` (ISO `yyyy-MM-dd`). Use for accounting data — transaction dates, invoice dates, payment dates, voucher dates. Aligns in tables, matches SIE/BFL convention.
+- `formatDateLong(x)` → `11 maj 2026` (Swedish long form). Use for metadata — when something was created, linked, verified, expires. Settings panels and audit displays.
+
+Never render raw `{x.invoice_date}` directly — always route through `formatDate()` for code consistency.
+
+**Currency.** `formatCurrency(n, currency?)` from `lib/utils.ts`. Default SEK.
+
+**Typography.**
+- Page title: `` (or use `PageHeader`).
+- Card title: `` for sections, default for primary cards.
+- Section divider header inside a page: ``.
+- Headline number: `font-display text-xl font-medium tabular-nums`.
+- Display font (`font-display`, Fraunces) reserved for h1/h2/h3 and primary financial numbers.
+
+**Forbidden / dead patterns.**
+- Page descriptions that paraphrase the page title (e.g. ``) → drop the description.
+- Two different status indicators on the same element (e.g. colored card border *and* Badge for status) → pick one (prefer Badge).
+- Mobile-specific `` duplicating desktop tabs in code — use a single Tabs primitive or a single grouped `Select`.
+- Hand-rolled icon buttons smaller than `h-10 w-10`. Use shadcn `Button size="icon"`.
+- Color-coded status using full-rainbow Tailwind palette (`bg-amber-100`, `bg-emerald-500/10`, etc.). Use Badge variants tied to the brand palette.
diff --git a/app/(dashboard)/bookkeeping/[id]/page.tsx b/app/(dashboard)/bookkeeping/[id]/page.tsx
index ad7248bf..e910d819 100644
--- a/app/(dashboard)/bookkeeping/[id]/page.tsx
+++ b/app/(dashboard)/bookkeeping/[id]/page.tsx
@@ -9,6 +9,7 @@ import { AccountNumber } from '@/components/ui/account-number'
import { Textarea } from '@/components/ui/textarea'
import { Loader2, ArrowLeft, Paperclip, AlertTriangle, Lock, MessageSquare, Pencil, Check, X, Copy } from 'lucide-react'
import { useCanWrite } from '@/lib/hooks/use-can-write'
+import { formatDate } from '@/lib/utils'
import JournalEntryAttachments from '@/components/bookkeeping/JournalEntryAttachments'
import JournalEntryStatusBadge, { sourceTypeLabels } from '@/components/bookkeeping/JournalEntryStatusBadge'
import CorrectionEntryDialog from '@/components/bookkeeping/CorrectionEntryDialog'
@@ -84,9 +85,12 @@ export default function JournalEntryDetailPage({ params }: { params: Promise<{ i
const res = await fetch(`/api/bookkeeping/journal-entries/${id}`, { method: 'DELETE' })
const result = await res.json()
if (res.ok) {
+ const wasDraft = result.data?.was_draft === true
toast({
- title: 'Verifikat raderat',
- description: `Verifikat ${result.data?.voucher_series ?? ''}${result.data?.voucher_number ?? ''} har raderats.`,
+ title: wasDraft ? 'Utkast raderat' : 'Verifikat raderat',
+ description: wasDraft
+ ? 'Utkastet har tagits bort.'
+ : `Verifikat ${result.data?.voucher_series ?? ''}${result.data?.voucher_number ?? ''} har raderats.`,
})
router.push('/bookkeeping')
} else {
@@ -156,7 +160,7 @@ export default function JournalEntryDetailPage({ params }: { params: Promise<{ i
const fullChain = [entry, ...chain]
return (
-
+
{/* Back link */}
{entry.description}
- {entry.status === 'posted' && (
+ {(entry.status === 'posted' || entry.status === 'draft') && (
- {isLastInSeries && (
+ {(entry.status === 'draft' || isLastInSeries) && (
{!canWrite && }
- Radera verifikat
+ {entry.status === 'draft' ? 'Radera utkast' : 'Radera verifikat'}
)}
{canCorrect && (
@@ -206,17 +210,19 @@ export default function JournalEntryDetailPage({ params }: { params: Promise<{ i
Skapa ändringsverifikation
)}
- router.push(`/bookkeeping?copy_from=${entry.id}`)}
- disabled={!canWrite}
- title={!canWrite ? 'Du har endast läsbehörighet i detta företag' : undefined}
- >
- {!canWrite ? : }
- Kopiera verifikat
-
+ {entry.status === 'posted' && (
+ router.push(`/bookkeeping?copy_from=${entry.id}`)}
+ disabled={!canWrite}
+ title={!canWrite ? 'Du har endast läsbehörighet i detta företag' : undefined}
+ >
+ {!canWrite ? : }
+ Kopiera verifikat
+
+ )}
)}
@@ -230,7 +236,7 @@ export default function JournalEntryDetailPage({ params }: { params: Promise<{ i
Datum
- {entry.entry_date}
+ {formatDate(entry.entry_date)}
{entry.committed_at && (
@@ -260,11 +266,11 @@ export default function JournalEntryDetailPage({ params }: { params: Promise<{ i
{!editingNotes && canWrite && (
{ setNotesValue(entry.notes || ''); setEditingNotes(true) }}
+ aria-label="Redigera anteckning"
>
-
+
)}
@@ -381,8 +387,8 @@ export default function JournalEntryDetailPage({ params }: { params: Promise<{ i
{/* Desktop table */}
-
-
+
+
Konto
Beskrivning
Debet
@@ -530,8 +536,12 @@ export default function JournalEntryDetailPage({ params }: { params: Promise<{ i
onOpenChange={setShowDeleteConfirm}
onConfirm={handleDelete}
isSubmitting={isDeleting}
- title="Radera verifikat"
- warningText={`Verifikat ${entry?.voucher_series ?? ''}${entry?.voucher_number ?? ''} raderas permanent. Eventuella kopplade underlag behålls men avlänkas. Denna åtgärd kan inte ångras.`}
+ title={entry?.status === 'draft' ? 'Radera utkast' : 'Radera verifikat'}
+ warningText={
+ entry?.status === 'draft'
+ ? 'Utkastet har aldrig bokförts och kan tas bort utan att påverka verifikationsserien. Eventuella kopplade underlag behålls men avlänkas. Denna åtgärd kan inte ångras.'
+ : `Verifikat ${entry?.voucher_series ?? ''}${entry?.voucher_number ?? ''} raderas permanent. Eventuella kopplade underlag behålls men avlänkas. Denna åtgärd kan inte ångras.`
+ }
confirmLabel="Radera permanent"
>
@@ -539,9 +549,9 @@ export default function JournalEntryDetailPage({ params }: { params: Promise<{ i
Permanent radering
- Verifikatet och dess kontorader tas bort. Kopplade transaktioner och
- fakturor behåller sina uppgifter men markeras som ej bokförda.
- Underlag (kvitton, dokument) behålls men avlänkas.
+ {entry?.status === 'draft'
+ ? 'Utkastet och dess kontorader tas bort. Kopplade fakturor och transaktioner påverkas inte — de stannar kvar som obokförda. Underlag (kvitton, dokument) behålls men avlänkas.'
+ : 'Verifikatet och dess kontorader tas bort. Kopplade transaktioner och fakturor behåller sina uppgifter men markeras som ej bokförda. Underlag (kvitton, dokument) behålls men avlänkas.'}
diff --git a/app/(dashboard)/bookkeeping/page.tsx b/app/(dashboard)/bookkeeping/page.tsx
index 6f1736f8..52e591b7 100644
--- a/app/(dashboard)/bookkeeping/page.tsx
+++ b/app/(dashboard)/bookkeeping/page.tsx
@@ -11,6 +11,7 @@ import ChartOfAccountsManager from '@/components/bookkeeping/ChartOfAccountsMana
import { FiscalYearSelector } from '@/components/common/FiscalYearSelector'
import { useToast } from '@/components/ui/use-toast'
import { Lock, Loader2, Copy } from 'lucide-react'
+import { PageHeader } from '@/components/ui/page-header'
import type { JournalEntry, JournalEntryLine } from '@/types'
interface CopyPrefill {
@@ -129,25 +130,18 @@ export default function BookkeepingPage() {
}, [refreshKey])
return (
-
-
-
-
Bokföring
-
- Skapa verifikationer, hantera kontoplanen och bifoga underlag
-
-
-
-
-
- Årsbokslut
-
-
-
-
- {activeTab === 'journal' && (
-
- )}
+
+
+
+
+ Årsbokslut
+
+
+ }
+ />
setActiveTab(v as TabValue)}>
@@ -163,7 +157,8 @@ export default function BookkeepingPage() {
Kontoplan
-
+
+
diff --git a/app/(dashboard)/bookkeeping/year-end/page.tsx b/app/(dashboard)/bookkeeping/year-end/page.tsx
index ea9557b4..0bbca857 100644
--- a/app/(dashboard)/bookkeeping/year-end/page.tsx
+++ b/app/(dashboard)/bookkeeping/year-end/page.tsx
@@ -6,7 +6,7 @@ import { SupportLink } from '@/components/ui/support-link'
export default function YearEndPage() {
return (
-
+
Årsbokslut
diff --git a/app/(dashboard)/customers/[id]/page.tsx b/app/(dashboard)/customers/[id]/page.tsx
index 79141e38..53930636 100644
--- a/app/(dashboard)/customers/[id]/page.tsx
+++ b/app/(dashboard)/customers/[id]/page.tsx
@@ -26,7 +26,7 @@ import {
Lock,
} from 'lucide-react'
import { useCanWrite } from '@/lib/hooks/use-can-write'
-import { cn } from '@/lib/utils'
+import { cn, formatDate } from '@/lib/utils'
import { invoiceNumberDisplay } from '@/lib/invoices/display'
import type { Customer, CustomerType, CreateCustomerInput } from '@/types'
@@ -185,7 +185,7 @@ export default function CustomerDetailPage({
const Icon = customerTypeIcons[customer.customer_type]
return (
-
+
{/* Header */}
@@ -352,7 +352,7 @@ export default function CustomerDetailPage({
>
{invoiceNumberDisplay(invoice.invoice_number)}
-
{invoice.invoice_date}
+
{formatDate(invoice.invoice_date)}
diff --git a/app/(dashboard)/customers/page.tsx b/app/(dashboard)/customers/page.tsx
index bb9ad14a..78d337d5 100644
--- a/app/(dashboard)/customers/page.tsx
+++ b/app/(dashboard)/customers/page.tsx
@@ -11,7 +11,8 @@ import { useToast } from '@/components/ui/use-toast'
import { getErrorMessage } from '@/lib/errors/get-error-message'
import { Plus, Search, Users, Lock } from 'lucide-react'
import CustomerForm from '@/components/customers/CustomerForm'
-import { EmptyCustomers } from '@/components/ui/empty-state'
+import { EmptyCustomers, EmptyState } from '@/components/ui/empty-state'
+import { PageHeader } from '@/components/ui/page-header'
import Link from 'next/link'
import { useCompany } from '@/contexts/CompanyContext'
import { useCanWrite } from '@/lib/hooks/use-can-write'
@@ -105,45 +106,42 @@ export default function CustomersPage() {
)
return (
-
-
-
-
Kunder
-
- Hantera dina kunder och deras faktureringsuppgifter
-
-
-
-
-
- {canWrite ? (
-
- ) : (
-
- )}
- Ny kund
-
-
-
-
- Lägg till kund
-
-
-
-
-
+
+
+
+
+ {canWrite ? (
+
+ ) : (
+
+ )}
+ Ny kund
+
+
+
+
+ Lägg till kund
+
+
+
+
+ }
+ />
{/* Search */}
setSearchTerm(e.target.value)}
className="pl-10"
@@ -167,15 +165,13 @@ export default function CustomersPage() {
) : filteredCustomers.length === 0 ? (
-
+
{searchTerm ? (
-
-
-
Inga träffar
-
- Inga kunder matchar "{searchTerm}"
-
-
+
) : (
setIsDialogOpen(true)} />
)}
diff --git a/app/(dashboard)/deadlines/page.tsx b/app/(dashboard)/deadlines/page.tsx
index 6230c340..6a5b8773 100644
--- a/app/(dashboard)/deadlines/page.tsx
+++ b/app/(dashboard)/deadlines/page.tsx
@@ -197,9 +197,9 @@ export default function DeadlinesPage() {
if (isLoading) {
return (
-
+
-
+
{[1, 2, 3, 4].map((i) => (
diff --git a/app/(dashboard)/extensions/page.tsx b/app/(dashboard)/extensions/page.tsx
index 79f7fb19..5b21b959 100644
--- a/app/(dashboard)/extensions/page.tsx
+++ b/app/(dashboard)/extensions/page.tsx
@@ -17,7 +17,7 @@ export default function ExtensionsPage() {
{/* General extensions */}
{generalSector && (
-
+
{generalSector.name}
diff --git a/app/(dashboard)/help/page.tsx b/app/(dashboard)/help/page.tsx
index 64b05525..8f2e9aaf 100644
--- a/app/(dashboard)/help/page.tsx
+++ b/app/(dashboard)/help/page.tsx
@@ -5,6 +5,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'
import { HelpLink } from '@/components/ui/info-tooltip'
+import { PageHeader } from '@/components/ui/page-header'
import {
Search,
BookOpen,
@@ -319,13 +320,10 @@ export default function HelpPage() {
return (
- {/* Header */}
-
+
{/* Search */}
@@ -369,7 +367,7 @@ export default function HelpPage() {
{/* Terms list */}
-
+
{filteredTerms.length === 0 ? (
@@ -433,7 +431,6 @@ export default function HelpPage() {
Externa resurser
- Mer hjälp från officiella källor
diff --git a/app/(dashboard)/invoices/[id]/credit/page.tsx b/app/(dashboard)/invoices/[id]/credit/page.tsx
index 2193f76c..5cac3af2 100644
--- a/app/(dashboard)/invoices/[id]/credit/page.tsx
+++ b/app/(dashboard)/invoices/[id]/credit/page.tsx
@@ -149,7 +149,7 @@ export default function CreateCreditNotePage({ params }: { params: Promise<{ id:
{/* Header */}
-
router.back()}>
+ router.back()} aria-label="Tillbaka">
diff --git a/app/(dashboard)/invoices/[id]/page.tsx b/app/(dashboard)/invoices/[id]/page.tsx
index 2b5cdb11..eef0da73 100644
--- a/app/(dashboard)/invoices/[id]/page.tsx
+++ b/app/(dashboard)/invoices/[id]/page.tsx
@@ -369,11 +369,11 @@ export default function InvoiceDetailPage({ params }: { params: Promise<{ id: st
const isDeliveryNote = docType === 'delivery_note'
const isRealInvoice = docType === 'invoice'
return (
-
+
{/* Header */}
-
router.back()}>
+ router.back()} aria-label="Tillbaka">
diff --git a/app/(dashboard)/invoices/new/page.tsx b/app/(dashboard)/invoices/new/page.tsx
index b881e945..40b872cc 100644
--- a/app/(dashboard)/invoices/new/page.tsx
+++ b/app/(dashboard)/invoices/new/page.tsx
@@ -56,6 +56,10 @@ type FormData = z.infer
const currencies: Currency[] = ['SEK', 'EUR', 'USD', 'GBP', 'NOK', 'DKK']
const units = ['st', 'tim', 'dag', 'månad', 'km', 'kg']
+function RequiredMark() {
+ return *
+}
+
export default function NewInvoicePage() {
const router = useRouter()
const { toast } = useToast()
@@ -438,9 +442,9 @@ export default function NewInvoicePage() {
}
return (
-
+
-
router.back()}>
+ router.back()} aria-label="Tillbaka">
@@ -475,7 +479,7 @@ export default function NewInvoicePage() {
{/* Customer selection */}
- Kund
+ Kund
Välj vilken kund fakturan ska skickas till
@@ -564,6 +568,7 @@ export default function NewInvoicePage() {
type="number"
step="0.01"
inputMode="decimal"
+ className="text-right tabular-nums"
{...register(`items.${index}.quantity`, { valueAsNumber: true })}
/>
@@ -594,6 +599,7 @@ export default function NewInvoicePage() {
type="number"
step="any"
inputMode="decimal"
+ className="text-right tabular-nums"
{...register(`items.${index}.unit_price`, { valueAsNumber: true })}
/>
@@ -729,13 +735,13 @@ export default function NewInvoicePage() {
- Fakturadatum
-
+ Fakturadatum
+
- Förfallodatum
-
+ Förfallodatum
+
{watchDocumentType === 'invoice' && (
diff --git a/app/(dashboard)/invoices/page.tsx b/app/(dashboard)/invoices/page.tsx
index a420c876..dc10fc46 100644
--- a/app/(dashboard)/invoices/page.tsx
+++ b/app/(dashboard)/invoices/page.tsx
@@ -16,19 +16,19 @@ import { cn } from '@/lib/utils'
import { invoiceNumberDisplay } from '@/lib/invoices/display'
import { getDisplayTotal } from '@/lib/invoices/rounding'
import { Plus, Search, Receipt, Lock } from 'lucide-react'
-import { EmptyInvoices } from '@/components/ui/empty-state'
+import { EmptyInvoices, EmptyState } from '@/components/ui/empty-state'
import { useCompany } from '@/contexts/CompanyContext'
import { useCanWrite } from '@/lib/hooks/use-can-write'
import type { Invoice, InvoiceStatus } from '@/types'
-const statusConfig: Record
= {
- draft: { label: 'Utkast', variant: 'secondary', borderColor: 'border-muted-foreground/30' },
- sent: { label: 'Skickad', variant: 'default', borderColor: 'border-warning/50' },
- paid: { label: 'Betald', variant: 'success', borderColor: 'border-success/50' },
- partially_paid: { label: 'Delbetalad', variant: 'warning', borderColor: 'border-warning/50' },
- overdue: { label: 'Förfallen', variant: 'destructive', borderColor: 'border-destructive/50' },
- cancelled: { label: 'Makulerad', variant: 'secondary', borderColor: 'border-muted-foreground/30' },
- credited: { label: 'Krediterad', variant: 'secondary', borderColor: 'border-muted-foreground/30' },
+const statusConfig: Record = {
+ draft: { label: 'Utkast', variant: 'secondary' },
+ sent: { label: 'Skickad', variant: 'default' },
+ paid: { label: 'Betald', variant: 'success' },
+ partially_paid: { label: 'Delbetalad', variant: 'warning' },
+ overdue: { label: 'Förfallen', variant: 'destructive' },
+ cancelled: { label: 'Makulerad', variant: 'secondary' },
+ credited: { label: 'Krediterad', variant: 'secondary' },
}
function getRelativeTimeLabel(dueDateStr: string, status: InvoiceStatus): { text: string; color: string } | null {
@@ -134,10 +134,9 @@ export default function InvoicesPage() {
}
return (
-
+
@@ -158,58 +157,33 @@ export default function InvoicesPage() {
}
/>
- {/* Stats */}
-
- {isLoading ? (
- <>
- {[1, 2, 3].map((i) => (
-
-
-
-
-
- ))}
- >
- ) : (
- <>
-
-
- Totalt antal
- {invoices.length}
-
-
-
-
- Obetalda
-
-
{stats.unpaid}
- {stats.overdue > 0 && (
-
- {stats.overdue} förfallna
-
- )}
-
-
-
-
-
- Att få in
- {formatCurrency(stats.unpaidAmount)}
-
-
- >
- )}
-
+ {/* Inline summary */}
+ {!isLoading && invoices.length > 0 && (
+
+ {invoices.length} {invoices.length === 1 ? 'faktura' : 'fakturor'}
+ {stats.unpaid > 0 && (
+ <>
+ {' · '}
+ {stats.unpaid} obetalda
+ {' · '}
+ {formatCurrency(stats.unpaidAmount)} att få in
+ {stats.overdue > 0 && (
+ <>
+ {' · '}
+ {stats.overdue} förfallna
+ >
+ )}
+ >
+ )}
+
+ )}
{/* Search and tabs */}
setSearchTerm(e.target.value)}
className="pl-10"
@@ -265,30 +239,26 @@ export default function InvoicesPage() {
) : filteredInvoices.length === 0 ? (
-
+
{searchTerm ? (
-
-
-
Inga träffar
-
- Inga fakturor matchar "{searchTerm}"
-
-
+
) : invoices.length === 0 ? (
) : (
-
-
-
Inga fakturor i denna kategori
-
- Prova att byta flik för att se fler fakturor
-
-
+
)}
) : (
-
+
{filteredInvoices.map((invoice) => {
const status = statusConfig[invoice.status]
const isCreditNote = !!invoice.credited_invoice_id
@@ -299,9 +269,7 @@ export default function InvoicesPage() {
return (
diff --git a/app/(dashboard)/kpi/page.tsx b/app/(dashboard)/kpi/page.tsx
index e6214247..7e55fb05 100644
--- a/app/(dashboard)/kpi/page.tsx
+++ b/app/(dashboard)/kpi/page.tsx
@@ -1,48 +1,35 @@
'use client'
import { useState, useEffect, useCallback } from 'react'
-import { Label } from '@/components/ui/label'
import { Card, CardContent } from '@/components/ui/card'
+import { Skeleton } from "@/components/ui/skeleton"
+import { FiscalYearSelector } from '@/components/common/FiscalYearSelector'
import { KPIHeroCards } from '@/components/kpi/KPIHeroCards'
import { KPITrendChart } from '@/components/kpi/KPITrendChart'
import { KPISettingsDialog } from '@/components/kpi/KPISettingsDialog'
import { getDefaultPreferences } from '@/lib/reports/kpi-definitions'
-import type { FiscalPeriod, KPIReport, KPIPreferences } from '@/types'
+import type { KPIReport, KPIPreferences } from '@/types'
export default function KpiPage() {
- const [periods, setPeriods] = useState
([])
- const [selectedPeriod, setSelectedPeriod] = useState('')
+ const [selectedPeriod, setSelectedPeriod] = useState('')
const [report, setReport] = useState(null)
const [preferences, setPreferences] = useState(getDefaultPreferences())
- const [isLoadingInit, setIsLoadingInit] = useState(true)
const [isLoadingReport, setIsLoadingReport] = useState(false)
const [isSavingPrefs, setIsSavingPrefs] = useState(false)
const [error, setError] = useState(null)
useEffect(() => {
- async function init() {
+ let cancelled = false
+ ;(async () => {
try {
- const [periodsRes, prefsRes] = await Promise.all([
- fetch('/api/bookkeeping/fiscal-periods'),
- fetch('/api/kpi/preferences'),
- ])
- const { data: periodsData } = await periodsRes.json()
- const { data: prefsData } = await prefsRes.json()
-
- const today = new Date().toISOString().split('T')[0]
- const activePeriods = (periodsData || []).filter((p: FiscalPeriod) => p.period_start <= today)
- setPeriods(activePeriods)
- if (prefsData) setPreferences(prefsData)
- if (activePeriods.length > 0) {
- setSelectedPeriod(activePeriods[0].id)
- }
+ const res = await fetch('/api/kpi/preferences')
+ const { data } = await res.json()
+ if (!cancelled && data) setPreferences(data)
} catch {
- setError('Kunde inte hämta data')
- } finally {
- setIsLoadingInit(false)
+ // Silently fall back to defaults
}
- }
- init()
+ })()
+ return () => { cancelled = true }
}, [])
const fetchReport = useCallback(async (periodId: string) => {
@@ -63,7 +50,6 @@ export default function KpiPage() {
useEffect(() => {
if (!selectedPeriod) return
let cancelled = false
-
fetchReport(selectedPeriod).then(() => {
if (cancelled) setReport(null)
})
@@ -81,11 +67,7 @@ export default function KpiPage() {
if (!res.ok) throw new Error()
const { data } = await res.json()
setPreferences(data)
-
- // Re-fetch report if account overrides changed (calculations may differ)
- if (selectedPeriod) {
- await fetchReport(selectedPeriod)
- }
+ if (selectedPeriod) await fetchReport(selectedPeriod)
} catch {
// Silently fail — user can retry
} finally {
@@ -93,25 +75,10 @@ export default function KpiPage() {
}
}
- if (isLoadingInit) {
- return (
-
-
-
Nyckeltal
-
Översikt av företagets ekonomiska hälsa
-
-
-
- )
- }
-
return (
-
+
-
-
Nyckeltal
-
Översikt av företagets ekonomiska hälsa
-
+
Nyckeltal
- {/* Period selector */}
- {periods.length > 0 && (
-
- Räkenskapsår
- setSelectedPeriod(e.target.value)}
- className="w-full mt-1 max-w-xs rounded-md border border-input bg-background px-3 py-2 text-sm"
- >
- {periods.map((p) => (
-
- {p.name} ({p.period_start} — {p.period_end})
-
- ))}
-
-
- )}
+
setSelectedPeriod(id || '')}
+ includeAllOption={false}
+ hideFuturePeriods
+ />
{error && (
@@ -153,36 +109,28 @@ export default function KpiPage() {
{report.months.length > 0 && }
>
)}
-
- {!isLoadingReport && !error && !report && periods.length === 0 && (
-
-
- Inget räkenskapsår hittades. Skapa ett räkenskapsår för att se nyckeltal.
-
-
- )}
)
}
function LoadingSkeleton() {
return (
-
+
{[1, 2, 3, 4].map((i) => (
-
-
-
-
+
+
+
+
))}
-
-
-
+
+
+
diff --git a/app/(dashboard)/layout.tsx b/app/(dashboard)/layout.tsx
index 829ee5c7..0576584b 100644
--- a/app/(dashboard)/layout.tsx
+++ b/app/(dashboard)/layout.tsx
@@ -97,7 +97,7 @@ export default async function DashboardLayout({
/>
@@ -147,7 +147,7 @@ export default async function DashboardLayout({
isSandbox={false}
extensionNavItems={getExtensionNavItems()}
/>
-
+
{children}
@@ -222,7 +222,7 @@ export default async function DashboardLayout({
isSandbox={isSandbox}
extensionNavItems={getExtensionNavItems()}
/>
-
+
{children}
{!isSandbox && (
diff --git a/app/(dashboard)/loading.tsx b/app/(dashboard)/loading.tsx
index 05c14e18..71a54ae5 100644
--- a/app/(dashboard)/loading.tsx
+++ b/app/(dashboard)/loading.tsx
@@ -2,15 +2,9 @@ import { Skeleton } from '@/components/ui/skeleton'
export default function DashboardLoading() {
return (
-
- {/* Header */}
-
-
-
-
-
- {/* Summary cards */}
-
+
+ {/* Metrics row */}
+
{[1, 2, 3, 4].map((i) => (
@@ -20,23 +14,24 @@ export default function DashboardLoading() {
))}
- {/* Quick actions */}
-
- {[1, 2, 3, 4].map((i) => (
-
+ {/* Income / Expenses */}
+
+ {[1, 2].map((i) => (
+
+
+
+
+
))}
- {/* Alerts + Deadlines */}
-
- {/* Alerts */}
+ {/* Alerts + deadlines */}
+
-
- {/* Deadlines */}
{[1, 2, 3].map((i) => (
diff --git a/app/(dashboard)/page.tsx b/app/(dashboard)/page.tsx
index 55091bbd..919dc621 100644
--- a/app/(dashboard)/page.tsx
+++ b/app/(dashboard)/page.tsx
@@ -55,7 +55,6 @@ export default async function DashboardPage() {
// Fetch all data in parallel
const [
- { data: profile },
{ data: settings },
{ count: customerCount },
{ count: invoiceCount },
@@ -77,7 +76,6 @@ export default async function DashboardPage() {
{ count: uncategorizedCount },
{ count: skatteverketTokenCount },
] = await Promise.all([
- supabase.from('profiles').select('full_name').eq('id', user.id).single(),
supabase.from('company_settings').select('*').eq('company_id', companyId).single(),
supabase.from('customers').select('*', { count: 'exact', head: true }).eq('company_id', companyId),
supabase.from('invoices').select('*', { count: 'exact', head: true }).eq('company_id', companyId),
@@ -108,8 +106,6 @@ export default async function DashboardPage() {
supabase.from('skatteverket_tokens').select('*', { count: 'exact', head: true }).eq('user_id', user.id),
])
- const firstName = profile?.full_name?.split(' ')[0] || null
-
// If onboarding is not complete, redirect to onboarding
if (!settings?.onboarding_complete) {
redirect('/onboarding')
@@ -236,9 +232,7 @@ export default async function DashboardPage() {
return (
{showBulkControls && bulkEligible.length > 0 && (
-
+
)}
{op.status === 'committed' && (
-
+
Godkänd
)}
{op.status === 'rejected' && (
-
+
Avvisad
diff --git a/app/(dashboard)/reports/page.tsx b/app/(dashboard)/reports/page.tsx
index 07113398..dba8909b 100644
--- a/app/(dashboard)/reports/page.tsx
+++ b/app/(dashboard)/reports/page.tsx
@@ -3,14 +3,16 @@
import React, { useState, useEffect, useCallback } from 'react'
import Link from 'next/link'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
+import { Skeleton } from "@/components/ui/skeleton"
import { Button } from '@/components/ui/button'
import { Label } from '@/components/ui/label'
import { Badge } from '@/components/ui/badge'
-import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'
import { Download, AlertCircle, ChevronDown, ChevronRight, ArrowRight } from 'lucide-react'
+import { formatDate } from '@/lib/utils'
import { AccountNumber } from '@/components/ui/account-number'
import { useCompany } from '@/contexts/CompanyContext'
import { FiscalYearSelector } from '@/components/common/FiscalYearSelector'
+import { ReportsNav } from '@/components/reports/ReportsNav'
import { NEDeclarationView } from '@/components/reports/NEDeclarationView'
import { INK2DeclarationView } from '@/components/reports/INK2DeclarationView'
import { BankReconciliationView } from '@/components/reports/BankReconciliationView'
@@ -89,14 +91,9 @@ export default function ReportsPage() {
const isAktiebolag = company?.entity_type === 'aktiebolag'
return (
-
+
-
-
Rapporter
-
- Generera skattedeklarationer, resultaträkningar och exportera till Skatteverket
-
-
+
Rapporter
@@ -125,19 +122,19 @@ export default function ReportsPage() {
{[1, 2, 3, 4].map((i) => (
))}
-
-
+
+
@@ -166,173 +163,44 @@ export default function ReportsPage() {
)}
-
- {/* Mobile: compact select dropdown */}
-
- handleTabChange(e.target.value)}
- className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
- >
-
- Resultatrapport
- Balansrapport
- Saldobalans
-
-
- Resultaträkning
- Balansräkning
-
-
- Momsdeklaration
- {isEnskildFirma && NE-bilaga }
- {isAktiebolag && INK2 }
-
-
- Huvudbok
- Grundbok
- Kundreskontra
- Leverantörsreskontra
-
-
- Bankavstämning
-
-
-
-
- {/* Desktop: inline grouped tab navigation */}
-
- {/* Löpande rapporter */}
-
- Löpande rapporter
-
-
- Resultatrapport
-
-
- Balansrapport
-
-
- Saldobalans
-
-
-
-
-
-
- {/* Bokslut */}
-
- Bokslut
-
-
- Resultaträkning
-
-
- Balansräkning
-
-
-
-
-
-
- {/* Skatt & moms */}
-
- Skatt & moms
-
-
- Momsdeklaration
-
- {isEnskildFirma && (
-
- NE-bilaga
-
- )}
- {isAktiebolag && (
-
- INK2
-
- )}
-
-
-
-
-
- {/* Huvudböcker */}
-
- Huvudböcker
-
-
- Huvudbok
-
-
- Grundbok
-
-
- Kundreskontra
-
-
- Leverantörsreskontra
-
-
-
-
-
-
- {/* Avstämning */}
-
- Avstämning
-
-
- Bankavstämning
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {isEnskildFirma && (
-
+
+
+
+ {activeTab === 'resultatrapport' && (
+
+ )}
+ {activeTab === 'balansrapport' && (
+
+ )}
+ {activeTab === 'trial-balance' && (
+
+ )}
+ {activeTab === 'income-statement' && (
+
+ )}
+ {activeTab === 'balance-sheet' && (
+
+ )}
+ {activeTab === 'vat-declaration' &&
}
+ {isEnskildFirma && activeTab === 'ne-declaration' && (
-
- )}
- {isAktiebolag && (
-
+ )}
+ {isAktiebolag && activeTab === 'ink2-declaration' && (
-
- )}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ )}
+ {activeTab === 'huvudbok' && (
+
+ )}
+ {activeTab === 'grundbok' &&
}
+ {activeTab === 'kundreskontra' &&
}
+ {activeTab === 'supplier-ledger' &&
}
+ {activeTab === 'bank-reconciliation' &&
}
+
+
>
) : (
@@ -472,8 +340,8 @@ function TrialBalanceView({ periodId, onNavigateToAccount }: { periodId: string;
{viewMode === 'simplified' ? (
-
-
+
+
Konto
Namn
Ingående saldo
@@ -512,8 +380,8 @@ function TrialBalanceView({ periodId, onNavigateToAccount }: { periodId: string;
) : (
-
-
+
+
Konto
Namn
Period debet
@@ -1350,12 +1218,12 @@ function VatDeclarationView() {
Momsdeklaration - {data.period.start} till {data.period.end}
0
- ? 'bg-orange-100 text-orange-800'
+ ? 'warning'
: data.rutor.ruta49 < 0
- ? 'bg-success/10 text-success'
- : 'bg-gray-100 text-gray-800'
+ ? 'success'
+ : 'secondary'
}
>
{data.rutor.ruta49 > 0
@@ -1704,8 +1572,8 @@ function SupplierLedgerView({ periodId }: { periodId: string }) {
-
-
+
+
Leverantör
Ej förfallet
1-30 dagar
@@ -1936,8 +1804,8 @@ function GeneralLedgerView({ periodId, initialAccountFilter }: { periodId: strin
-
-
+
+
Ver.nr
Datum
Beskrivning
@@ -2096,8 +1964,8 @@ function JournalRegisterView({ periodId }: { periodId: string }) {
-
-
+
+
Ver.nr
Datum
@@ -2330,8 +2198,8 @@ function ARLedgerView({ periodId }: { periodId: string }) {
-
-
+
+
Kund
Ej förfallet
@@ -2371,8 +2239,8 @@ function ARLedgerView({ periodId }: { periodId: string }) {
{inv.invoice_number}
- {inv.invoice_date}
- förfaller {inv.due_date}
+ {formatDate(inv.invoice_date)}
+ förfaller {formatDate(inv.due_date)}
{inv.days_overdue > 0 ? `${inv.days_overdue} dagar förfallen` : 'Ej förfallen'}
diff --git a/app/(dashboard)/salary/employees/[id]/page.tsx b/app/(dashboard)/salary/employees/[id]/page.tsx
index 19e5f235..fce79eb8 100644
--- a/app/(dashboard)/salary/employees/[id]/page.tsx
+++ b/app/(dashboard)/salary/employees/[id]/page.tsx
@@ -4,6 +4,7 @@ import { useState, useEffect, use } from 'react'
import { useRouter } from 'next/navigation'
import Link from 'next/link'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
+import { Skeleton } from "@/components/ui/skeleton"
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
@@ -111,7 +112,7 @@ export default function EmployeeDetailPage({ params }: { params: Promise<{ id: s
} else {
const result = await res.json()
toast({
- title: 'Fel',
+ title: 'Kunde inte uppdatera anställd',
description: getErrorMessage(result, { context: 'salary', statusCode: res.status }),
variant: 'destructive',
})
@@ -133,8 +134,8 @@ export default function EmployeeDetailPage({ params }: { params: Promise<{ id: s
if (loading) {
return (
)
}
@@ -144,11 +145,11 @@ export default function EmployeeDetailPage({ params }: { params: Promise<{ id: s
}
return (
-
+
-
-
+
+
diff --git a/app/(dashboard)/salary/employees/new/page.tsx b/app/(dashboard)/salary/employees/new/page.tsx
index 18b71ef6..74affe20 100644
--- a/app/(dashboard)/salary/employees/new/page.tsx
+++ b/app/(dashboard)/salary/employees/new/page.tsx
@@ -68,7 +68,7 @@ export default function NewEmployeePage() {
} else {
const result = await res.json()
toast({
- title: 'Fel',
+ title: 'Kunde inte skapa anställd',
description: getErrorMessage(result, { context: 'salary', statusCode: res.status }),
variant: 'destructive',
})
@@ -78,10 +78,10 @@ export default function NewEmployeePage() {
}
return (
-
+
diff --git a/app/(dashboard)/salary/employees/page.tsx b/app/(dashboard)/salary/employees/page.tsx
index 4c3df846..c1e7ea9d 100644
--- a/app/(dashboard)/salary/employees/page.tsx
+++ b/app/(dashboard)/salary/employees/page.tsx
@@ -3,7 +3,10 @@
import { useState, useEffect } from 'react'
import Link from 'next/link'
import { Card, CardContent } from '@/components/ui/card'
+import { Skeleton } from "@/components/ui/skeleton"
import { Button } from '@/components/ui/button'
+import { EmptyState } from '@/components/ui/empty-state'
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { Plus, ArrowLeft, UserCircle } from 'lucide-react'
import { useCanWrite } from '@/lib/hooks/use-can-write'
import { formatCurrency } from '@/lib/utils'
@@ -33,11 +36,11 @@ export default function EmployeesPage() {
}, [])
return (
-
+
-
-
+
+
Anställda
@@ -57,65 +60,62 @@ export default function EmployeesPage() {
{loading ? (
{[1, 2, 3].map(i => (
-
+
))}
) : employees.length === 0 ? (
-
-
- Inga anställda registrerade
- {canWrite && (
-
-
-
- Lägg till anställd
-
-
- )}
+
+
) : (
-
-
-
- Namn
- Personnummer
- Typ
- Månadslön
- Sysselsättningsgrad
- Skattetabell
-
-
-
+
+
+
+ Namn
+ Personnummer
+ Typ
+ Månadslön
+ Sysselsättningsgrad
+ Skattetabell
+
+
+
{employees.map(emp => (
-
-
-
+
+
+
{emp.first_name} {emp.last_name}
-
-
+
+
{emp.personnummer}
-
-
+
+
{EMPLOYMENT_LABELS[emp.employment_type] || emp.employment_type}
-
-
+
+
{emp.monthly_salary ? formatCurrency(emp.monthly_salary) : '—'}
-
-
+
+
{emp.employment_degree}%
-
-
+
+
{emp.tax_table_number ? `Tabell ${emp.tax_table_number}, kol ${emp.tax_column}` : '—'}
-
-
+
+
))}
-
-
+
+
)}
diff --git a/app/(dashboard)/salary/page.tsx b/app/(dashboard)/salary/page.tsx
index a4bd11cf..b3416572 100644
--- a/app/(dashboard)/salary/page.tsx
+++ b/app/(dashboard)/salary/page.tsx
@@ -2,11 +2,16 @@
import { useState, useEffect } from 'react'
import Link from 'next/link'
+import { Badge } from '@/components/ui/badge'
+import { Skeleton } from "@/components/ui/skeleton"
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
+import { EmptyState } from '@/components/ui/empty-state'
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { Plus, Users, HandCoins, CalendarDays, ArrowRight } from 'lucide-react'
+import { PageHeader } from '@/components/ui/page-header'
import { useCanWrite } from '@/lib/hooks/use-can-write'
-import { formatCurrency } from '@/lib/utils'
+import { formatCurrency, formatDate } from '@/lib/utils'
import type { SalaryRun } from '@/types'
const STATUS_LABELS: Record
= {
@@ -17,12 +22,12 @@ const STATUS_LABELS: Record = {
booked: 'Bokförd',
}
-const STATUS_COLORS: Record = {
- draft: 'bg-muted text-muted-foreground',
- review: 'bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-400',
- approved: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400',
- paid: 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900/30 dark:text-emerald-400',
- booked: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400',
+const STATUS_VARIANTS: Record = {
+ draft: 'secondary',
+ review: 'warning',
+ approved: 'default',
+ paid: 'success',
+ booked: 'success',
}
export default function SalaryPage() {
@@ -61,12 +66,12 @@ export default function SalaryPage() {
return (
{[1, 2, 3].map(i => (
-
+
))}
@@ -74,35 +79,33 @@ export default function SalaryPage() {
}
return (
-
- {/* Header */}
-
-
-
Löner
-
Hantera anställda och lönekörningar
-
-
-
-
-
- Anställda
-
-
- {canWrite && (
-
-
-
- Ny lönekörning
+
+
+
+
+
+ Anställda
- )}
-
-
+ {canWrite && (
+
+
+
+ Ny lönekörning
+
+
+ )}
+
+ }
+ />
{/* Summary cards */}
-
+
@@ -113,7 +116,7 @@ export default function SalaryPage() {
-
+
@@ -124,7 +127,7 @@ export default function SalaryPage() {
-
+
@@ -143,63 +146,58 @@ export default function SalaryPage() {
{runs.length === 0 ? (
-
-
-
Inga lönekörningar ännu
- {canWrite && (
-
-
-
- Skapa första lönekörningen
-
-
- )}
-
+
) : (
-
-
-
- Period
- Utbetalningsdag
- Brutto
- Netto
- Avgifter
- Status
-
-
-
-
+
+
+
+ Period
+ Utbetalningsdag
+ Brutto
+ Netto
+ Avgifter
+ Status
+
+
+
+
{runs.slice(0, 12).map(run => (
-
-
+
+
{run.period_year}-{String(run.period_month).padStart(2, '0')}
-
-
- {run.payment_date}
-
-
+
+
+ {formatDate(run.payment_date)}
+
+
{formatCurrency(run.total_gross)}
-
-
+
+
{formatCurrency(run.total_net)}
-
-
+
+
{formatCurrency(run.total_avgifter)}
-
-
-
+
+
+
{STATUS_LABELS[run.status]}
-
-
-
+
+
+
-
-
+
+
))}
-
-
+
+
)}
diff --git a/app/(dashboard)/salary/runs/[id]/employees/[employeeId]/page.tsx b/app/(dashboard)/salary/runs/[id]/employees/[employeeId]/page.tsx
index 34034c75..31d35a2d 100644
--- a/app/(dashboard)/salary/runs/[id]/employees/[employeeId]/page.tsx
+++ b/app/(dashboard)/salary/runs/[id]/employees/[employeeId]/page.tsx
@@ -7,7 +7,38 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { AbsenceCalendar } from '@/components/salary/AbsenceCalendar'
import { formatCurrency } from '@/lib/utils'
-import type { SalaryRun, SalaryRunEmployee, SalaryLineItem, Employee } from '@/types'
+import type { SalaryRun, SalaryRunEmployee, SalaryLineItem, SalaryLineItemType, Employee } from '@/types'
+
+const LINE_ITEM_TYPE_LABELS: Record
= {
+ monthly_salary: 'Månadslön',
+ hourly_salary: 'Timlön',
+ overtime: 'Övertid',
+ bonus: 'Bonus',
+ commission: 'Provision',
+ gross_deduction_pension: 'Bruttoavdrag — pension',
+ gross_deduction_other: 'Bruttoavdrag — övrigt',
+ benefit_car: 'Bilförmån',
+ benefit_housing: 'Bostadsförmån',
+ benefit_meals: 'Kostförmån',
+ benefit_wellness: 'Friskvård',
+ benefit_other: 'Övrig förmån',
+ sick_karens: 'Karensavdrag',
+ sick_day2_14: 'Sjuklön (dag 2–14, 80 %)',
+ sick_day15_plus: 'Sjuklön (dag 15+, Försäkringskassan)',
+ vab: 'VAB (vård av sjukt barn)',
+ parental_leave: 'Föräldraledighet',
+ vacation: 'Semester',
+ traktamente_taxfree: 'Traktamente (skattefritt)',
+ traktamente_taxable: 'Traktamente (skattepliktigt)',
+ mileage_taxfree: 'Milersättning (skattefritt)',
+ mileage_taxable: 'Milersättning (skattepliktigt)',
+ net_deduction_advance: 'Nettoavdrag — förskott',
+ net_deduction_union: 'Nettoavdrag — fackavgift',
+ net_deduction_benefit_payment: 'Nettoavdrag — förmånsbetalning',
+ net_deduction_other: 'Nettoavdrag — övrigt',
+ correction: 'Korrigering',
+ other: 'Övrigt',
+}
interface DetailResponse {
run: SalaryRun
@@ -95,7 +126,7 @@ export default function SalaryRunEmployeeDetailPage({
const readOnly = run.status !== 'draft' && run.status !== 'review'
return (
-
+
{/* Header */}
) : (
-
-
- Typ
- Beskrivning
- Antal
- Belopp
+
+
+ Typ
+ Beskrivning
+ Antal
+ Belopp
{lineItems.map(li => (
- {li.item_type}
+ {LINE_ITEM_TYPE_LABELS[li.item_type] ?? li.item_type}
{li.description}
{li.quantity ?? '—'}
{formatCurrency(li.amount)}
diff --git a/app/(dashboard)/salary/runs/[id]/page.tsx b/app/(dashboard)/salary/runs/[id]/page.tsx
index 637f58fd..38c6098c 100644
--- a/app/(dashboard)/salary/runs/[id]/page.tsx
+++ b/app/(dashboard)/salary/runs/[id]/page.tsx
@@ -3,16 +3,19 @@
import { useState, useEffect, use } from 'react'
import { useRouter } from 'next/navigation'
import Link from 'next/link'
+import { Badge } from '@/components/ui/badge'
+import { Skeleton } from "@/components/ui/skeleton"
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import {
ArrowLeft, Calculator, Eye, Check, CreditCard, BookOpen,
ArrowLeftCircle, Loader2, Download,
} from 'lucide-react'
import { useToast } from '@/components/ui/use-toast'
import { useCanWrite } from '@/lib/hooks/use-can-write'
-import { formatCurrency } from '@/lib/utils'
+import { formatCurrency, formatDate } from '@/lib/utils'
import { getErrorMessage } from '@/lib/errors/get-error-message'
import type { SalaryRun, SalaryRunEmployee, Employee, CreateJournalEntryLineInput } from '@/types'
import { AGIPanel } from '@/components/salary/AGIPanel'
@@ -30,13 +33,13 @@ const STATUS_LABELS: Record = {
corrected: 'Korrigerad',
}
-const STATUS_COLORS: Record = {
- draft: 'bg-muted text-muted-foreground',
- review: 'bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-400',
- approved: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400',
- paid: 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900/30 dark:text-emerald-400',
- booked: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400',
- corrected: 'bg-muted text-muted-foreground',
+const STATUS_VARIANTS: Record = {
+ draft: 'secondary',
+ review: 'warning',
+ approved: 'default',
+ paid: 'success',
+ booked: 'success',
+ corrected: 'secondary',
}
interface EntryPreview {
@@ -113,7 +116,7 @@ export default function SalaryRunDetailPage({ params }: { params: Promise<{ id:
} else {
const result = await res.json()
toast({
- title: 'Fel',
+ title: 'Kunde inte uppdatera status',
description: getErrorMessage(result, { context: 'salary', statusCode: res.status }),
variant: 'destructive',
})
@@ -134,7 +137,7 @@ export default function SalaryRunDetailPage({ params }: { params: Promise<{ id:
} else {
const result = await res.json()
toast({
- title: 'Fel',
+ title: 'Kunde inte lägga till anställd',
description: getErrorMessage(result, { context: 'salary', statusCode: res.status }),
variant: 'destructive',
})
@@ -208,8 +211,8 @@ export default function SalaryRunDetailPage({ params }: { params: Promise<{ id:
if (loading) {
return (
)
}
@@ -224,40 +227,42 @@ export default function SalaryRunDetailPage({ params }: { params: Promise<{ id:
const notAdded = availableEmployees.filter(e => !addedEmployeeIds.has(e.id))
return (
-
+
{/* Header */}
-
-
+
+
Lönekörning {periodLabel}
- Utbetalning: {run.payment_date}
+ Utbetalning: {formatDate(run.payment_date)}
-
+
{STATUS_LABELS[run.status]}
-
+
{/* Summary cards */}
-
+
{[
{ label: 'Brutto', value: run.total_gross },
{ label: 'Skatt', value: run.total_tax },
- { label: 'Netto', value: run.total_net },
+ { label: 'Netto', value: run.total_net, accent: true },
{ label: 'Avgifter', value: run.total_avgifter },
{ label: 'Total kostnad', value: run.total_employer_cost },
- ].map(({ label, value }) => (
+ ].map(({ label, value, accent }) => (
-
- {label}
- {formatCurrency(value)}
+
+ {label}
+
+ {formatCurrency(value)}
+
))}
@@ -292,30 +297,30 @@ export default function SalaryRunDetailPage({ params }: { params: Promise<{ id:
Inga anställda tillagda ännu
) : (
-
-
-
- Anställd
- Brutto
- Skatt
- Netto
- Avgifter
- Semester
-
-
-
+
+
+
+ Anställd
+ Brutto
+ Skatt
+ Netto
+ Avgifter
+ Semester
+
+
+
{employees.map(sre => {
const employee = (sre as SalaryRunEmployee & { employee?: { first_name: string; last_name: string; personnummer: string } }).employee
const name = employee
? `${employee.first_name} ${employee.last_name}`
: `Anställd ${sre.employee_id.slice(0, 8)}...`
return (
- router.push(`/salary/runs/${id}/employees/${sre.employee_id}`)}
>
-
+
{name}
-
- {formatCurrency(sre.gross_salary)}
- {formatCurrency(sre.tax_withheld)}
- {formatCurrency(sre.net_salary)}
- {formatCurrency(sre.avgifter_amount)}
- {formatCurrency(sre.vacation_accrual)}
-
+
+ Brutto {formatCurrency(sre.gross_salary)}
+
+
+ {formatCurrency(sre.gross_salary)}
+ {formatCurrency(sre.tax_withheld)}
+ {formatCurrency(sre.net_salary)}
+ {formatCurrency(sre.avgifter_amount)}
+ {formatCurrency(sre.vacation_accrual)}
+
)
})}
-
-
+
+
)}
@@ -384,8 +392,8 @@ export default function SalaryRunDetailPage({ params }: { params: Promise<{ id:
{entry!.description}
-
-
+
+
Konto
Beskrivning
Debet
diff --git a/app/(dashboard)/salary/runs/new/page.tsx b/app/(dashboard)/salary/runs/new/page.tsx
index 00c1b546..ae2b4f0d 100644
--- a/app/(dashboard)/salary/runs/new/page.tsx
+++ b/app/(dashboard)/salary/runs/new/page.tsx
@@ -46,7 +46,7 @@ export default function NewSalaryRunPage() {
} else {
const result = await res.json()
toast({
- title: 'Fel',
+ title: 'Kunde inte skapa lönekörning',
description: getErrorMessage(result, { context: 'salary', statusCode: res.status }),
variant: 'destructive',
})
@@ -56,10 +56,10 @@ export default function NewSalaryRunPage() {
}
return (
-
+
diff --git a/app/(dashboard)/settings/banking/page.tsx b/app/(dashboard)/settings/banking/page.tsx
index 21243a49..976da378 100644
--- a/app/(dashboard)/settings/banking/page.tsx
+++ b/app/(dashboard)/settings/banking/page.tsx
@@ -112,7 +112,7 @@ export default function BankingSettingsPage() {
}, [searchParams, router, toast])
return (
-
+
{bankConnectionError && (
diff --git a/app/(dashboard)/settings/salary/page.tsx b/app/(dashboard)/settings/salary/page.tsx
index e79ab54c..6b10415a 100644
--- a/app/(dashboard)/settings/salary/page.tsx
+++ b/app/(dashboard)/settings/salary/page.tsx
@@ -1,16 +1,12 @@
'use client'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
+import { PageHeader } from '@/components/ui/page-header'
export default function SalarySettingsPage() {
return (
-
-
-
Löneinställningar
-
- Konfiguration för lönemodulen
-
-
+
+
diff --git a/app/(dashboard)/settings/skatteverket/page.tsx b/app/(dashboard)/settings/skatteverket/page.tsx
index 8ba6f21a..ddf425d9 100644
--- a/app/(dashboard)/settings/skatteverket/page.tsx
+++ b/app/(dashboard)/settings/skatteverket/page.tsx
@@ -33,7 +33,7 @@ export default function SkatteverketSettingsPage() {
}, [searchParams, router, toast])
return (
-
+
)
diff --git a/app/(dashboard)/settings/templates/page.tsx b/app/(dashboard)/settings/templates/page.tsx
index 3ea33122..c80a8b6d 100644
--- a/app/(dashboard)/settings/templates/page.tsx
+++ b/app/(dashboard)/settings/templates/page.tsx
@@ -5,7 +5,7 @@ import { CounterpartyTemplatesPanel } from '@/components/settings/CounterpartyTe
export default function TemplatesSettingsPage() {
return (
-
+
diff --git a/app/(dashboard)/skattekonto/page.tsx b/app/(dashboard)/skattekonto/page.tsx
index 3a39faaf..0666d167 100644
--- a/app/(dashboard)/skattekonto/page.tsx
+++ b/app/(dashboard)/skattekonto/page.tsx
@@ -6,6 +6,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { useToast } from '@/components/ui/use-toast'
import { formatCurrency } from '@/lib/utils'
import {
@@ -365,69 +366,65 @@ function TransactionTable({
}
return (
-
-
-
-
- Datum
- {showForfallodatum && Förfallodatum }
- Beskrivning
- Belopp
- Status
-
-
-
-
- {rows.map(row => {
- const negative = Number(row.belopp_skatteverket) < 0
- const isBooked = !!row.journal_entry_id
- return (
-
- {row.transaktionsdatum}
- {showForfallodatum && (
- {row.forfallodatum ?? '–'}
+
+
+
+ Datum
+ {showForfallodatum && Förfallodatum }
+ Beskrivning
+ Belopp
+ Status
+
+
+
+
+ {rows.map(row => {
+ const negative = Number(row.belopp_skatteverket) < 0
+ const isBooked = !!row.journal_entry_id
+ return (
+
+ {row.transaktionsdatum}
+ {showForfallodatum && (
+ {row.forfallodatum ?? '–'}
+ )}
+ {row.transaktionstext}
+
+ {formatCurrency(Number(row.belopp_skatteverket))}
+
+
+ {isBooked ? (
+
+
+ Bokförd
+
+ ) : (
+ Ej bokförd
)}
- {row.transaktionstext}
-
- {formatCurrency(Number(row.belopp_skatteverket))}
-
-
- {isBooked ? (
-
-
- Bokförd
-
- ) : (
- Ej bokförd
- )}
-
-
- {isBooked ? (
-
-
- Visa verifikat
-
-
- ) : (
- onBokfor(row.id)}
- disabled={bookingId === row.id}
- >
- {bookingId === row.id ? 'Bokför…' : 'Bokför'}
-
- )}
-
-
- )
- })}
-
-
-
+
+
+ {isBooked ? (
+
+
+ Visa verifikat
+
+
+ ) : (
+ onBokfor(row.id)}
+ disabled={bookingId === row.id}
+ >
+ {bookingId === row.id ? 'Bokför…' : 'Bokför'}
+
+ )}
+
+
+ )
+ })}
+
+
)
}
diff --git a/app/(dashboard)/supplier-invoices/[id]/page.tsx b/app/(dashboard)/supplier-invoices/[id]/page.tsx
index 9c7adef2..d085bcda 100644
--- a/app/(dashboard)/supplier-invoices/[id]/page.tsx
+++ b/app/(dashboard)/supplier-invoices/[id]/page.tsx
@@ -3,6 +3,7 @@
import { useState, useEffect } from 'react'
import { useParams, useRouter } from 'next/navigation'
import { Button } from '@/components/ui/button'
+import { Skeleton } from "@/components/ui/skeleton"
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Input } from '@/components/ui/input'
@@ -12,6 +13,7 @@ import { useToast } from '@/components/ui/use-toast'
import { getErrorMessage } from '@/lib/errors/get-error-message'
import { ArrowLeft, CheckCircle, CreditCard, FileText, Trash2, Lock, Undo2, Info } from 'lucide-react'
import { useCanWrite } from '@/lib/hooks/use-can-write'
+import { formatDate } from '@/lib/utils'
import Link from 'next/link'
import { AccountNumber } from '@/components/ui/account-number'
import { DestructiveConfirmDialog, useDestructiveConfirm } from '@/components/ui/destructive-confirm-dialog'
@@ -21,15 +23,15 @@ function formatAmount(amount: number): string {
return amount.toLocaleString('sv-SE', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
}
-const statusColors: Record
= {
- registered: 'bg-blue-100 text-blue-800',
- approved: 'bg-yellow-100 text-yellow-800',
- paid: 'bg-success/10 text-success',
- partially_paid: 'bg-orange-100 text-orange-800',
- overdue: 'bg-destructive/10 text-destructive',
- disputed: 'bg-purple-100 text-purple-800',
- credited: 'bg-gray-100 text-gray-800',
- reversed: 'bg-gray-100 text-gray-500',
+const statusVariants: Record = {
+ registered: 'secondary',
+ approved: 'default',
+ paid: 'success',
+ partially_paid: 'warning',
+ overdue: 'destructive',
+ disputed: 'destructive',
+ credited: 'secondary',
+ reversed: 'secondary',
}
const statusLabels: Record = {
@@ -177,7 +179,7 @@ export default function SupplierInvoiceDetailPage() {
if (isLoading) {
return (
)
@@ -202,7 +204,7 @@ export default function SupplierInvoiceDetailPage() {
{/* Header */}
-
router.push('/supplier-invoices')}>
+ router.push('/supplier-invoices')} aria-label="Tillbaka till leverantörsfakturor">
@@ -210,7 +212,7 @@ export default function SupplierInvoiceDetailPage() {
Ankomst #{invoice.arrival_number}
-
+
{statusLabels[invoice.status] || invoice.status}
@@ -321,11 +323,11 @@ export default function SupplierInvoiceDetailPage() {
Fakturadatum
- {invoice.invoice_date}
+ {formatDate(invoice.invoice_date)}
Förfallodatum
- {invoice.due_date}
+ {formatDate(invoice.due_date)}
{invoice.delivery_date && (
@@ -341,7 +343,7 @@ export default function SupplierInvoiceDetailPage() {
)}
{invoice.reverse_charge && (
- Omvänd skattskyldighet
+ Omvänd skattskyldighet
)}
@@ -403,8 +405,8 @@ export default function SupplierInvoiceDetailPage() {
{/* Desktop table */}
-
-
+
+
Beskrivning
Antal
Enhet
@@ -460,8 +462,8 @@ export default function SupplierInvoiceDetailPage() {
{/* Desktop table */}
-
-
+
+
Datum
Belopp
Verifikation
@@ -471,7 +473,7 @@ export default function SupplierInvoiceDetailPage() {
{payments.map((p) => (
- {p.payment_date}
+ {formatDate(p.payment_date)}
{formatAmount(p.amount)} {p.currency}
{p.journal_entry_id ? (
@@ -491,7 +493,7 @@ export default function SupplierInvoiceDetailPage() {
{payments.map((p) => (
- {p.payment_date}
+ {formatDate(p.payment_date)}
{formatAmount(p.amount)} {p.currency}
diff --git a/app/(dashboard)/supplier-invoices/new/page.tsx b/app/(dashboard)/supplier-invoices/new/page.tsx
index 1fd37960..730b4676 100644
--- a/app/(dashboard)/supplier-invoices/new/page.tsx
+++ b/app/(dashboard)/supplier-invoices/new/page.tsx
@@ -55,6 +55,10 @@ interface NewSupplierForm {
default_expense_account: string
}
+function RequiredMark() {
+ return
*
+}
+
function formatAmount(amount: number): string {
return amount.toLocaleString('sv-SE', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
}
@@ -741,7 +745,7 @@ export default function NewSupplierInvoicePage() {
return (
-
router.push('/supplier-invoices')}>
+ router.push('/supplier-invoices')} aria-label="Tillbaka till leverantörsfakturor">
@@ -794,7 +798,7 @@ export default function NewSupplierInvoicePage() {
- Leverantör *
+ Leverantör
- Leverantörens fakturanummer *
+ Leverantörens fakturanummer
{(() => {
const { ref: rhfRef, ...rest } = register('supplier_invoice_number')
return (
@@ -843,11 +847,11 @@ export default function NewSupplierInvoicePage() {