feat: BAS data overhaul, currency revaluation, expenses, UI polish, and cleanup

- Update BAS account catalog with comprehensive SRU codes and K2 flags
- Add currency revaluation service with tests and API route
- Add expenses page and account deletion API
- Enhance booking templates with new patterns and improved tests
- Improve transaction categorization with template picker and description matching
- Polish dashboard, onboarding, import, and transaction UIs
- Refactor year-end service for multi-step closing
- Move SRU generator to ne-bilaga, remove standalone SRU export
- Remove unused dev docs, mock data, and extension hooks
- Add invoice delivery note sequences migration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-03-04 14:19:56 +01:00
co-authored by Claude Opus 4.6
parent a9bf24ce0b
commit 66a4027f1e
104 changed files with 4964 additions and 5501 deletions
+7 -27
View File
@@ -8,13 +8,8 @@ import { Progress } from '@/components/ui/progress'
import {
Check,
Circle,
Users,
Receipt,
Building2,
Camera,
Sparkles,
X,
ChevronRight,
} from 'lucide-react'
import { cn } from '@/lib/utils'
@@ -23,7 +18,6 @@ interface ChecklistItem {
label: string
description: string
href: string
icon: React.ComponentType<{ className?: string }>
completed: boolean
}
@@ -61,7 +55,6 @@ export default function NewUserChecklist({
label: 'Skapa konto',
description: 'Du har ett konto!',
href: '#',
icon: Check,
completed: true, // Always completed if they're seeing this
},
{
@@ -69,7 +62,6 @@ export default function NewUserChecklist({
label: 'Lägg till din första kund',
description: 'Spara kunduppgifter för enkel fakturering',
href: '/customers/new',
icon: Users,
completed: hasCustomers,
},
{
@@ -77,7 +69,6 @@ export default function NewUserChecklist({
label: 'Skicka din första faktura',
description: 'Skapa en professionell faktura på 60 sekunder',
href: '/invoices/new',
icon: Receipt,
completed: hasInvoices,
},
{
@@ -85,7 +76,6 @@ export default function NewUserChecklist({
label: 'Importera transaktioner',
description: 'Importera kontoutdrag från din bank',
href: '/import',
icon: Building2,
completed: hasBankConnected,
},
{
@@ -93,7 +83,6 @@ export default function NewUserChecklist({
label: 'Skanna ditt första kvitto',
description: 'Fotografera för automatisk bokföring',
href: '/receipts/scan',
icon: Camera,
completed: hasReceipts,
},
]
@@ -150,16 +139,11 @@ export default function NewUserChecklist({
>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-primary/10">
<Sparkles className="h-4 w-4 text-primary" />
</div>
<div>
<CardTitle className="text-base">Kom igång</CardTitle>
<p className="text-sm text-muted-foreground">
{completedCount} av {items.length} steg klara
</p>
</div>
<div>
<CardTitle className="text-base">Kom igång</CardTitle>
<p className="text-sm text-muted-foreground">
{completedCount} av {items.length} steg klara
</p>
</div>
<Button
variant="ghost"
@@ -175,9 +159,7 @@ export default function NewUserChecklist({
</CardHeader>
<CardContent className="pt-0">
<ul className="space-y-1">
{items.map((item) => {
const Icon = item.icon
return (
{items.map((item) => (
<li key={item.id}>
{item.completed ? (
<div className="flex items-center gap-3 p-2 rounded-lg">
@@ -204,12 +186,10 @@ export default function NewUserChecklist({
{item.description}
</p>
</div>
<ChevronRight className="h-4 w-4 text-muted-foreground group-hover:text-primary group-hover:translate-x-0.5 transition-all" />
</Link>
)}
</li>
)
})}
))}
</ul>
</CardContent>
</Card>
+86 -11
View File
@@ -1,6 +1,6 @@
'use client'
import { useState, useMemo } from 'react'
import { useState, useMemo, useEffect } from 'react'
import { useForm, Controller } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
@@ -49,6 +49,7 @@ interface Step3Output {
interface Step3Props {
initialData: Partial<Step3Output>
entityType?: EntityType
orgNumber?: string
onNext: (data: Step3Output) => void
onBack: () => void
isSaving: boolean
@@ -138,6 +139,7 @@ function getABFirstYearEndDates(
export default function Step3TaxRegistration({
initialData,
entityType,
orgNumber,
onNext,
onBack,
isSaving,
@@ -149,6 +151,7 @@ export default function Step3TaxRegistration({
handleSubmit,
watch,
control,
setValue,
formState: { errors },
} = useForm<FormData>({
resolver: zodResolver(schema),
@@ -174,6 +177,17 @@ export default function Step3TaxRegistration({
const fiscalYearEndMonth = watch('fiscal_year_end_month')
const accountingMethod = watch('accounting_method')
// Auto-fill VAT number when vat_registered toggles on
const vatNumber = watch('vat_number')
useEffect(() => {
if (vatRegistered && !vatNumber && orgNumber) {
const cleaned = orgNumber.replace(/[-\s]/g, '')
if (cleaned.length >= 10) {
setValue('vat_number', `SE${cleaned}01`)
}
}
}, [vatRegistered, vatNumber, orgNumber, setValue])
// State for AB first-year end month selector
const [abEndMonth, setAbEndMonth] = useState<number>(
initialData.first_year_end
@@ -361,18 +375,79 @@ export default function Step3TaxRegistration({
{isFirstYear && (
<div className="space-y-4 rounded-lg bg-muted/50 p-4">
<div className="space-y-2">
<Label htmlFor="first_year_start">Startdatum</Label>
<Label>Startdatum</Label>
<Controller
name="first_year_start"
control={control}
render={({ field }) => (
<Input
id="first_year_start"
type="date"
value={field.value || ''}
onChange={(e) => field.onChange(e.target.value)}
/>
)}
render={({ field }) => {
const parsed = field.value ? (() => {
const d = new Date(field.value)
if (isNaN(d.getTime())) return null
return { day: d.getDate(), month: d.getMonth() + 1, year: d.getFullYear() }
})() : null
const selectedDay = parsed?.day ?? 0
const selectedMonth = parsed?.month ?? 0
const selectedYear = parsed?.year ?? 0
const currentYear = new Date().getFullYear()
const years = Array.from({ length: 7 }, (_, i) => currentYear - 5 + i)
const maxDays = selectedMonth && selectedYear
? lastDayOfMonth(selectedYear, selectedMonth)
: 31
const compose = (day: number, month: number, year: number) => {
if (day && month && year) {
const clamped = Math.min(day, lastDayOfMonth(year, month))
field.onChange(`${year}-${String(month).padStart(2, '0')}-${String(clamped).padStart(2, '0')}`)
}
}
return (
<div className="grid grid-cols-3 gap-2">
<Select
value={selectedDay ? selectedDay.toString() : ''}
onValueChange={(v) => compose(parseInt(v), selectedMonth, selectedYear)}
>
<SelectTrigger>
<SelectValue placeholder="Dag" />
</SelectTrigger>
<SelectContent>
{Array.from({ length: maxDays }, (_, i) => i + 1).map((d) => (
<SelectItem key={d} value={d.toString()}>{d}</SelectItem>
))}
</SelectContent>
</Select>
<Select
value={selectedMonth ? selectedMonth.toString() : ''}
onValueChange={(v) => compose(selectedDay, parseInt(v), selectedYear)}
>
<SelectTrigger>
<SelectValue placeholder="Månad" />
</SelectTrigger>
<SelectContent>
{monthNames.map((name, i) => (
<SelectItem key={i + 1} value={(i + 1).toString()}>{name}</SelectItem>
))}
</SelectContent>
</Select>
<Select
value={selectedYear ? selectedYear.toString() : ''}
onValueChange={(v) => compose(selectedDay, selectedMonth, parseInt(v))}
>
<SelectTrigger>
<SelectValue placeholder="År" />
</SelectTrigger>
<SelectContent>
{years.map((y) => (
<SelectItem key={y} value={y.toString()}>{y}</SelectItem>
))}
</SelectContent>
</Select>
</div>
)
}}
/>
<p className="text-xs text-muted-foreground">
Dagen verksamheten startade (bör vara den 1:a i en månad).
@@ -593,7 +668,7 @@ export default function Step3TaxRegistration({
</SelectTrigger>
<SelectContent>
<SelectItem value="monthly">Månad</SelectItem>
<SelectItem value="quarterly">Kvartal (rekommenderas)</SelectItem>
<SelectItem value="quarterly">Kvartal</SelectItem>
<SelectItem value="yearly">År</SelectItem>
</SelectContent>
</Select>