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:
co-authored by
Claude Opus 4.6
parent
a9bf24ce0b
commit
66a4027f1e
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user