diff --git a/app/(dashboard)/import/page.tsx b/app/(dashboard)/import/page.tsx index aba8a654..b707be73 100644 --- a/app/(dashboard)/import/page.tsx +++ b/app/(dashboard)/import/page.tsx @@ -359,7 +359,7 @@ function SIEImportWizard() { const data = await res.json() if (!res.ok) { - if (data.error === 'duplicate') { + if (data.error === 'duplicate' || data.error === 'duplicate_period') { setError(data.message) } else if (data.error === 'validation') { setError(`${data.message}: ${data.errors?.join(', ') || 'Unknown validation error'}`) diff --git a/app/(dashboard)/kpi/page.tsx b/app/(dashboard)/kpi/page.tsx index f692f2cd..e6214247 100644 --- a/app/(dashboard)/kpi/page.tsx +++ b/app/(dashboard)/kpi/page.tsx @@ -29,10 +29,12 @@ export default function KpiPage() { const { data: periodsData } = await periodsRes.json() const { data: prefsData } = await prefsRes.json() - setPeriods(periodsData || []) + 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 (periodsData && periodsData.length > 0) { - setSelectedPeriod(periodsData[0].id) + if (activePeriods.length > 0) { + setSelectedPeriod(activePeriods[0].id) } } catch { setError('Kunde inte hämta data') diff --git a/app/(dashboard)/receipts/page.tsx b/app/(dashboard)/receipts/page.tsx index 6e4502d2..81f7c7db 100644 --- a/app/(dashboard)/receipts/page.tsx +++ b/app/(dashboard)/receipts/page.tsx @@ -1,24 +1,9 @@ 'use client' -import dynamic from 'next/dynamic' -import { ENABLED_EXTENSION_IDS } from '@/lib/extensions/_generated/enabled-extensions' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' -import { Button } from '@/components/ui/button' -import { Receipt, Loader2 } from 'lucide-react' -import Link from 'next/link' - -const ReceiptsPageOCR = dynamic( - () => import('@/extensions/general/receipt-ocr/pages/ReceiptsPage'), - { loading: () =>
} -) - -const ocrEnabled = ENABLED_EXTENSION_IDS.has('receipt-ocr') +import { Receipt } from 'lucide-react' export default function ReceiptsPage() { - if (ocrEnabled) { - return - } - return (
@@ -32,19 +17,13 @@ export default function ReceiptsPage() { - Kvittoscanning + Kvitton -

- Aktivera tillägget "Kvittoscanning" för att skanna kvitton med AI, - extrahera data automatiskt och matcha mot transaktioner. +

+ Kvittoscanning är inte tillgängligt just nu.

-
diff --git a/app/(dashboard)/receipts/scan/page.tsx b/app/(dashboard)/receipts/scan/page.tsx index 77147b62..8700be0e 100644 --- a/app/(dashboard)/receipts/scan/page.tsx +++ b/app/(dashboard)/receipts/scan/page.tsx @@ -1,30 +1,14 @@ 'use client' -import dynamic from 'next/dynamic' import { useEffect } from 'react' import { useRouter } from 'next/navigation' -import { ENABLED_EXTENSION_IDS } from '@/lib/extensions/_generated/enabled-extensions' -import { Loader2 } from 'lucide-react' - -const ScanReceiptPageOCR = dynamic( - () => import('@/extensions/general/receipt-ocr/pages/scan/ScanReceiptPage'), - { loading: () =>
} -) - -const ocrEnabled = ENABLED_EXTENSION_IDS.has('receipt-ocr') export default function ScanReceiptPage() { const router = useRouter() useEffect(() => { - if (!ocrEnabled) { - router.replace('/receipts') - } + router.replace('/receipts') }, [router]) - if (!ocrEnabled) { - return null - } - - return + return null } diff --git a/app/(dashboard)/reports/page.tsx b/app/(dashboard)/reports/page.tsx index de71e3b2..ffe23d72 100644 --- a/app/(dashboard)/reports/page.tsx +++ b/app/(dashboard)/reports/page.tsx @@ -81,9 +81,11 @@ export default function ReportsPage() { async function fetchPeriods() { const res = await fetch('/api/bookkeeping/fiscal-periods') const { data } = await res.json() - setPeriods(data || []) - if (data && data.length > 0) { - setSelectedPeriod(data[0].id) + const today = new Date().toISOString().split('T')[0] + const activePeriods = (data || []).filter((p: FiscalPeriod) => p.period_start <= today) + setPeriods(activePeriods) + if (activePeriods.length > 0) { + setSelectedPeriod(activePeriods[0].id) } } diff --git a/app/(dashboard)/settings/account/page.tsx b/app/(dashboard)/settings/account/page.tsx new file mode 100644 index 00000000..5904e019 --- /dev/null +++ b/app/(dashboard)/settings/account/page.tsx @@ -0,0 +1,100 @@ +'use client' + +import { useState, useEffect } from 'react' +import { useRouter } from 'next/navigation' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { Sun, Moon, Monitor, LogOut } from 'lucide-react' +import { useTheme } from 'next-themes' +import { createClient } from '@/lib/supabase/client' +import { SecuritySettings } from '@/components/settings/SecuritySettings' +import { CalendarFeedSettings } from '@/components/settings/CalendarFeedSettings' +import { AccountDangerZone } from '@/components/settings/AccountDangerZone' +import { ENABLED_EXTENSION_IDS } from '@/lib/extensions/_generated/enabled-extensions' +import { useSettings } from '@/components/settings/useSettings' + +export default function AccountSettingsPage() { + const router = useRouter() + const supabase = createClient() + const { theme, setTheme } = useTheme() + const [mounted, setMounted] = useState(false) + const hasCalendarExtension = ENABLED_EXTENSION_IDS.has('calendar') + const { settings } = useSettings() + + useEffect(() => { setMounted(true) }, []) + + async function handleLogout() { + await supabase.auth.signOut() + router.push('/login') + } + + return ( +
+ {/* Appearance */} +
+

+ Utseende +

+ {mounted && ( +
+ {([ + { value: 'light', label: 'Ljust', icon: Sun }, + { value: 'dark', label: 'Mörkt', icon: Moon }, + { value: 'system', label: 'System', icon: Monitor }, + ] as const).map(({ value, label, icon: Icon }) => ( + + ))} +
+ )} +
+ + {/* Security */} +
+ +
+ + {/* Calendar feed */} + {hasCalendarExtension && ( +
+ +
+ )} + + {/* Logout */} +
+ + + Kontoinställningar + + +
+
+

Logga ut

+

Logga ut från ditt konto

+
+ +
+
+
+
+ + {/* Delete account — only for non-sandbox */} + {!settings?.is_sandbox && } +
+ ) +} diff --git a/app/(dashboard)/settings/api/page.tsx b/app/(dashboard)/settings/api/page.tsx new file mode 100644 index 00000000..351bf38c --- /dev/null +++ b/app/(dashboard)/settings/api/page.tsx @@ -0,0 +1,7 @@ +'use client' + +import { ApiKeysPanel } from '@/components/settings/ApiKeysPanel' + +export default function ApiSettingsPage() { + return +} diff --git a/app/(dashboard)/settings/banking/page.tsx b/app/(dashboard)/settings/banking/page.tsx new file mode 100644 index 00000000..4e560754 --- /dev/null +++ b/app/(dashboard)/settings/banking/page.tsx @@ -0,0 +1,108 @@ +'use client' + +import { useState, useEffect } from 'react' +import { useSearchParams, useRouter } from 'next/navigation' +import Link from 'next/link' +import { Card, CardContent } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { useToast } from '@/components/ui/use-toast' +import { AlertTriangle, CreditCard, ExternalLink } from 'lucide-react' +import { getSettingsPanel } from '@/lib/extensions/settings-panel-registry' +import { ENABLED_EXTENSION_IDS } from '@/lib/extensions/_generated/enabled-extensions' + +const BankingPanel = getSettingsPanel('enable-banking') + +export default function BankingSettingsPage() { + const searchParams = useSearchParams() + const router = useRouter() + const { toast } = useToast() + const [bankConnectionError, setBankConnectionError] = useState(null) + const hasBankingExtension = ENABLED_EXTENSION_IDS.has('enable-banking') + + useEffect(() => { + const bankConnected = searchParams.get('bank_connected') + const bankError = searchParams.get('bank_error') + + if (bankConnected === 'true') { + toast({ + title: 'Bank ansluten!', + description: 'Din bank är nu kopplad. Transaktioner hämtas...', + }) + + const connectionId = searchParams.get('connection_id') + if (connectionId) { + fetch('/api/extensions/ext/enable-banking/sync', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ connection_id: connectionId, days_back: 90 }), + }) + .then(res => res.json()) + .then(data => { + if (data.imported > 0) { + toast({ + title: 'Transaktioner hämtade', + description: `${data.imported} transaktioner importerade`, + }) + } + }) + .catch(() => {}) + } + + router.replace('/settings/banking') + } + + if (bankError) { + const errorMsg = decodeURIComponent(bankError) + toast({ + title: 'Anslutning misslyckades', + description: errorMsg, + variant: 'destructive', + }) + setBankConnectionError(errorMsg) + router.replace('/settings/banking') + } + }, [searchParams, router, toast]) + + return ( +
+ {bankConnectionError && ( +
+ +
+

{bankConnectionError}

+

+ Du kan också importera transaktioner via bankfil istället. +

+
+ +
+ )} + + {hasBankingExtension && BankingPanel ? ( + + ) : ( + + + +

Bankintegration (PSD2) är inte aktiverad

+

+ Aktivera tillägget Enable Banking för att koppla ditt bankkonto och automatiskt hämta transaktioner. +

+ +
+
+ )} +
+ ) +} diff --git a/app/(dashboard)/settings/bookkeeping/page.tsx b/app/(dashboard)/settings/bookkeeping/page.tsx new file mode 100644 index 00000000..26d9625f --- /dev/null +++ b/app/(dashboard)/settings/bookkeeping/page.tsx @@ -0,0 +1,94 @@ +'use client' + +import Link from 'next/link' +import { SettingsFormWrapper } from '@/components/settings/SettingsFormWrapper' +import { SettingsLoadingSkeleton } from '@/components/settings/SettingsLoadingSkeleton' +import { PeriodLockingSettings } from '@/components/settings/PeriodLockingSettings' +import { VoucherSeriesManager } from '@/components/settings/VoucherSeriesManager' +import { useSettings } from '@/components/settings/useSettings' +import { Label } from '@/components/ui/label' +import { ExternalLink } from 'lucide-react' +import type { CompanySettings } from '@/types' + +export default function BookkeepingSettingsPage() { + const { settings, isLoading, updateSettings } = useSettings() + + if (isLoading || !settings) return + + function handleSave(formData: FormData) { + const autoLockValue = formData.get('auto_lock_period_days') as string + const lockedThrough = (formData.get('bookkeeping_locked_through') as string) || null + const accountingMethod = (formData.get('accounting_method') as string) || 'accrual' + + const updates: Record = { + bookkeeping_locked_through: lockedThrough, + auto_lock_period_days: autoLockValue === 'none' ? null : parseInt(autoLockValue), + accounting_method: accountingMethod, + } + updateSettings(updates as Partial) + return updates + } + + return ( +
+ + {/* Accounting method */} +
+

+ Bokföringsmetod +

+
+ + +

+ {settings.entity_type === 'aktiebolag' + ? 'Aktiebolag med omsättning över 3 MSEK måste använda faktureringsmetoden.' + : 'Kontantmetoden är tillgänglig för enskild firma med omsättning under 3 MSEK.'} +

+
+
+ + {/* Period locking */} +
+ +
+
+ + {/* Voucher series — read-only, no form submit needed */} +
+ +
+ + {/* Cross-links */} +
+

+ Relaterat +

+
+ + + Räkenskapsår och ingående balanser + + + + Kontoplan (BAS) + +
+
+
+ ) +} diff --git a/app/(dashboard)/settings/company/page.tsx b/app/(dashboard)/settings/company/page.tsx new file mode 100644 index 00000000..dafff5e1 --- /dev/null +++ b/app/(dashboard)/settings/company/page.tsx @@ -0,0 +1,49 @@ +'use client' + +import { CompanyInfoForm } from '@/components/settings/CompanyInfoForm' +import { CompanyMembersSection } from '@/components/settings/CompanyMembersSection' +import { LogoUpload } from '@/components/settings/LogoUpload' +import { SettingsFormWrapper } from '@/components/settings/SettingsFormWrapper' +import { SettingsLoadingSkeleton } from '@/components/settings/SettingsLoadingSkeleton' +import { useSettings } from '@/components/settings/useSettings' +import type { CompanySettings } from '@/types' + +export default function CompanySettingsPage() { + const { settings, isLoading, updateSettings } = useSettings() + + if (isLoading || !settings) return + + function handleSave(formData: FormData) { + const updates: Record = { + ...(formData.has('company_name') && { company_name: formData.get('company_name') as string }), + ...(formData.has('org_number') && { org_number: formData.get('org_number') as string }), + address_line1: formData.get('address_line1') as string, + postal_code: formData.get('postal_code') as string, + city: formData.get('city') as string, + phone: (formData.get('phone') as string) || '', + email: (formData.get('email') as string) || '', + website: (formData.get('website') as string) || '', + } + updateSettings(updates as Partial) + return updates + } + + return ( +
+ + + + +
+ updateSettings({ logo_url: url })} + /> +
+ +
+ +
+
+ ) +} diff --git a/app/(dashboard)/settings/invoicing/page.tsx b/app/(dashboard)/settings/invoicing/page.tsx new file mode 100644 index 00000000..31dc98b6 --- /dev/null +++ b/app/(dashboard)/settings/invoicing/page.tsx @@ -0,0 +1,58 @@ +'use client' + +import { BankDetailsForm, validateBankFields } from '@/components/settings/BankDetailsForm' +import { InvoiceSettingsForm } from '@/components/settings/InvoiceSettingsForm' +import { PdfPrintSettings } from '@/components/settings/PdfPrintSettings' +import { SettingsFormWrapper } from '@/components/settings/SettingsFormWrapper' +import { SettingsLoadingSkeleton } from '@/components/settings/SettingsLoadingSkeleton' +import { useSettings } from '@/components/settings/useSettings' +import { useToast } from '@/components/ui/use-toast' +import type { CompanySettings } from '@/types' + +export default function InvoicingSettingsPage() { + const { settings, isLoading, updateSettings } = useSettings() + const { toast } = useToast() + + if (isLoading || !settings) return + + function handleSave(formData: FormData) { + const bankErrors = validateBankFields(formData) + if (bankErrors.length > 0) { + toast({ + title: 'Kontrollera bankuppgifter', + description: bankErrors.map(e => e.message).join(', '), + variant: 'destructive', + }) + return {} + } + + const updates: Record = { + bank_name: formData.get('bank_name') as string, + clearing_number: formData.get('clearing_number') as string, + account_number: formData.get('account_number') as string, + bankgiro: (formData.get('bankgiro') as string) || null, + invoice_prefix: (formData.get('invoice_prefix') as string) || null, + next_invoice_number: parseInt(formData.get('next_invoice_number') as string) || 1, + invoice_default_days: parseInt(formData.get('invoice_default_days') as string) || 30, + invoice_default_notes: (formData.get('invoice_default_notes') as string) || null, + } + updateSettings(updates as Partial) + return updates + } + + return ( +
+ + +
+ +
+
+ + {/* PDF settings — saves individually via toggle switches */} +
+ +
+
+ ) +} diff --git a/app/(dashboard)/settings/layout.tsx b/app/(dashboard)/settings/layout.tsx new file mode 100644 index 00000000..a145e22f --- /dev/null +++ b/app/(dashboard)/settings/layout.tsx @@ -0,0 +1,63 @@ +'use client' + +import { useEffect, useState } from 'react' +import { useSearchParams, useRouter } from 'next/navigation' +import { SettingsNav } from '@/components/settings/SettingsSidebar' +import { useCompany } from '@/contexts/CompanyContext' +import { createClient } from '@/lib/supabase/client' + +const TAB_TO_ROUTE: Record = { + company: '/settings/company', + invoicing: '/settings/invoicing', + bookkeeping: '/settings/bookkeeping', + tax: '/settings/tax', + team: '/settings/team', + banking: '/settings/banking', + templates: '/settings/templates', + account: '/settings/account', + api: '/settings/api', +} + +export default function SettingsLayout({ children }: { children: React.ReactNode }) { + const searchParams = useSearchParams() + const router = useRouter() + const { company } = useCompany() + const [isSandbox, setIsSandbox] = useState(false) + + // Fetch sandbox status + useEffect(() => { + if (!company?.id) return + const supabase = createClient() + supabase + .from('company_settings') + .select('is_sandbox') + .eq('company_id', company.id) + .single() + .then(({ data }) => { + if (data?.is_sandbox) setIsSandbox(true) + }) + }, [company?.id]) + + // Handle legacy ?tab= URLs + useEffect(() => { + const tab = searchParams.get('tab') + if (tab && TAB_TO_ROUTE[tab]) { + router.replace(TAB_TO_ROUTE[tab]) + } + }, [searchParams, router]) + + return ( +
+
+

Inställningar

+

+ Hantera ditt företag och konto +

+
+ + + +
{children}
+
+ ) +} diff --git a/app/(dashboard)/settings/page.tsx b/app/(dashboard)/settings/page.tsx index b8bafa7a..58041da8 100644 --- a/app/(dashboard)/settings/page.tsx +++ b/app/(dashboard)/settings/page.tsx @@ -1,862 +1,5 @@ -'use client' - -import { useState, useEffect } from 'react' -import { useRouter, useSearchParams } from 'next/navigation' -import Link from 'next/link' -import { createClient } from '@/lib/supabase/client' -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' -import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' -import { Textarea } from '@/components/ui/textarea' -import { Label } from '@/components/ui/label' -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' -import { useToast } from '@/components/ui/use-toast' -import { Separator } from '@/components/ui/separator' -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog' -import { - Loader2, - CreditCard, - LogOut, - Sun, - Moon, - Monitor, - ExternalLink, - AlertTriangle, -} from 'lucide-react' -import { useTheme } from 'next-themes' -import { useCompany } from '@/contexts/CompanyContext' -import type { CompanySettings } from '@/types' -import { validateBankgiroNumber, formatBankgiroNumber } from '@/lib/bankgiro/luhn' -import { BankNameCombobox } from '@/components/settings/BankNameCombobox' -import { CalendarFeedSettings } from '@/components/settings/CalendarFeedSettings' -import { SupportLink } from '@/components/ui/support-link' -import { getSettingsPanel } from '@/lib/extensions/settings-panel-registry' -import { SecuritySettings } from '@/components/settings/SecuritySettings' -import { ApiKeysPanel } from '@/components/settings/ApiKeysPanel' -import { CounterpartyTemplatesPanel } from '@/components/settings/CounterpartyTemplatesPanel' -import { TeamPanel } from '@/components/settings/TeamPanel' -import { CompanyMembersSection } from '@/components/settings/CompanyMembersSection' -import { ENABLED_EXTENSION_IDS } from '@/lib/extensions/_generated/enabled-extensions' - -const BankingPanel = getSettingsPanel('enable-banking') +import { redirect } from 'next/navigation' export default function SettingsPage() { - const router = useRouter() - const searchParams = useSearchParams() - const { toast } = useToast() - const supabase = createClient() - const { company, isTeamMember } = useCompany() - - const [isLoading, setIsLoading] = useState(true) - const [isSaving, setIsSaving] = useState(false) - const [settings, setSettings] = useState(null) - const hasBankingExtension = ENABLED_EXTENSION_IDS.has('enable-banking') - const hasCalendarExtension = ENABLED_EXTENSION_IDS.has('calendar') - const hasMcpExtension = ENABLED_EXTENSION_IDS.has('mcp-server') - const [bankConnectionError, setBankConnectionError] = useState(null) - const [bankgiroError, setBankgiroError] = useState(null) - const [clearingError, setClearingError] = useState(null) - const [accountNumberError, setAccountNumberError] = useState(null) - const [showDeleteDialog, setShowDeleteDialog] = useState(false) - const [deleteConfirmText, setDeleteConfirmText] = useState('') - const [isDeleting, setIsDeleting] = useState(false) - const { theme, setTheme } = useTheme() - const [mounted, setMounted] = useState(false) - - const hasCompany = !!company - const defaultTab = hasCompany ? 'company' : (isTeamMember ? 'team' : 'account') - const initialTab = searchParams.get('tab') || defaultTab - const [activeTab, setActiveTab] = useState(initialTab) - - const settingsTabs = [ - { value: 'company', label: 'Företag', show: hasCompany }, - { value: 'team', label: 'Lag', show: isTeamMember }, - { value: 'banking', label: 'Bank (PSD2)', show: hasCompany && !settings?.is_sandbox && hasBankingExtension }, - { value: 'templates', label: 'Mallar', show: hasCompany }, - { value: 'account', label: 'Konto', show: true }, - { value: 'api', label: 'API', show: hasCompany && hasMcpExtension }, - ].filter(t => t.show) - - useEffect(() => { - setMounted(true) - }, []) - - async function fetchData() { - setIsLoading(true) - - const { data: { user } } = await supabase.auth.getUser() - if (!user) { - router.push('/login') - return - } - - if (company?.id) { - const settingsRes = await supabase.from('company_settings').select('*').eq('company_id', company.id).single() - setSettings(settingsRes.data) - } - - setIsLoading(false) - } - - useEffect(() => { - fetchData() - - // Handle callback messages - const bankConnected = searchParams.get('bank_connected') - const bankError = searchParams.get('bank_error') - - if (bankConnected === 'true') { - toast({ - title: 'Bank ansluten!', - description: 'Din bank är nu kopplad. Transaktioner hämtas...', - }) - - // Auto-sync transactions after connection - const connectionId = searchParams.get('connection_id') - if (connectionId) { - fetch('/api/extensions/ext/enable-banking/sync', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ connection_id: connectionId, days_back: 90 }), - }) - .then(res => res.json()) - .then(data => { - if (data.imported > 0) { - toast({ - title: 'Transaktioner hämtade', - description: `${data.imported} transaktioner importerade`, - }) - } - }) - .catch(() => {}) - } - - router.replace('/settings?tab=banking') - } - - if (bankError) { - const errorMsg = decodeURIComponent(bankError) - toast({ - title: 'Anslutning misslyckades', - description: errorMsg, - variant: 'destructive', - }) - setBankConnectionError(errorMsg) - setActiveTab('banking') - router.replace('/settings?tab=banking') - } - }, [searchParams]) - - async function handleSaveSettings(e: React.FormEvent) { - e.preventDefault() - if (!settings) return - - const formData = new FormData(e.currentTarget) - - // Validate bank fields before saving - const clearingVal = (formData.get('clearing_number') as string || '').trim() - const accountVal = (formData.get('account_number') as string || '').trim() - const bankgiroVal = (formData.get('bankgiro') as string || '').trim() - - let hasErrors = false - - if (clearingVal && !/^\d{4,5}$/.test(clearingVal)) { - setClearingError('Clearingnummer måste vara 4-5 siffror') - hasErrors = true - } else { - setClearingError(null) - } - - if (accountVal && !/^\d{6,12}$/.test(accountVal)) { - setAccountNumberError('Kontonummer måste vara 6-12 siffror') - hasErrors = true - } else { - setAccountNumberError(null) - } - - if (bankgiroVal && !validateBankgiroNumber(bankgiroVal)) { - setBankgiroError('Ogiltigt bankgironummer (7-8 siffror med kontrollsiffra)') - hasErrors = true - } else { - setBankgiroError(null) - } - - if (hasErrors) return - - setIsSaving(true) - - // Disabled inputs are excluded from FormData by the browser, - // so only include company_name/org_number when not locked - const updates: Record = { - ...(formData.has('company_name') && { company_name: formData.get('company_name') as string }), - ...(formData.has('org_number') && { org_number: formData.get('org_number') as string }), - address_line1: formData.get('address_line1') as string, - postal_code: formData.get('postal_code') as string, - city: formData.get('city') as string, - bank_name: formData.get('bank_name') as string, - clearing_number: formData.get('clearing_number') as string, - account_number: formData.get('account_number') as string, - bankgiro: (formData.get('bankgiro') as string) || null, - preliminary_tax_monthly: parseFloat(formData.get('preliminary_tax_monthly') as string) || null, - invoice_prefix: formData.get('invoice_prefix') as string || null, - next_invoice_number: parseInt(formData.get('next_invoice_number') as string) || 1, - invoice_default_days: parseInt(formData.get('invoice_default_days') as string) || 30, - accounting_method: formData.get('accounting_method') as string || 'accrual', - invoice_default_notes: (formData.get('invoice_default_notes') as string) || null, - } - - try { - const response = await fetch('/api/settings', { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(updates), - }) - - const result = await response.json() - - if (!response.ok) { - throw new Error(result.error || 'Kunde inte spara inställningar') - } - - toast({ - title: 'Sparat', - description: 'Dina inställningar har uppdaterats', - }) - setSettings({ ...settings, ...updates } as typeof settings) - } catch (error) { - toast({ - title: 'Kunde inte spara inställningar', - description: error instanceof Error ? error.message : 'Försök igen.', - variant: 'destructive', - }) - } - - setIsSaving(false) - } - - async function handleLogout() { - await supabase.auth.signOut() - router.push('/login') - } - - async function handleDeleteAccount() { - if (deleteConfirmText !== 'RADERA') return - setIsDeleting(true) - - try { - const response = await fetch('/api/account/delete', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ confirm: 'RADERA' }), - }) - - if (!response.ok) { - const result = await response.json() - throw new Error(result.error || 'Kunde inte radera kontot') - } - - router.push('/login') - } catch (error) { - toast({ - title: 'Kunde inte radera kontot', - description: error instanceof Error ? error.message : 'Försök igen.', - variant: 'destructive', - }) - setIsDeleting(false) - } - } - - if (isLoading) { - return ( -
-
-
-
-
-
- - -
-
- - -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- - -
- ) - } - - return ( -
-
-

Inställningar

-

- Hantera dina företags- och kontoinställningar -

-
- - - {/* Mobile: dropdown selector */} -
- -
- - {/* Desktop: tab pills */} - - {settingsTabs.map(t => ( - {t.label} - ))} - - - {/* Company settings */} - -
- {/* Företagsuppgifter */} - - - Företagsuppgifter - - Namn, organisationsnummer och adress - - - -
-
- - - {settings?.onboarding_complete && ( -

Kan inte ändras efter att kontot skapats

- )} -
-
- - - {settings?.onboarding_complete && ( -

Kan inte ändras efter att kontot skapats

- )} -
-
- -
- - -
- -
-
- - -
-
- - -
-
-
-
- - {/* Bankuppgifter */} - - - Bankuppgifter - - Betalningsuppgifter som visas på dina fakturor - - - -
-
- - -
-
- - { - e.target.value = e.target.value.replace(/\D/g, '') - }} - onBlur={(e) => { - const val = e.target.value.trim() - if (!val) { - setClearingError(null) - return - } - if (!/^\d{4,5}$/.test(val)) { - setClearingError('Clearingnummer måste vara 4-5 siffror') - } else { - setClearingError(null) - } - }} - /> - {clearingError && ( -

{clearingError}

- )} -
-
- - { - e.target.value = e.target.value.replace(/\D/g, '') - }} - onBlur={(e) => { - const val = e.target.value.trim() - if (!val) { - setAccountNumberError(null) - return - } - if (!/^\d{6,12}$/.test(val)) { - setAccountNumberError('Kontonummer måste vara 6-12 siffror') - } else { - setAccountNumberError(null) - } - }} - /> - {accountNumberError && ( -

{accountNumberError}

- )} -
-
-
- - { - const val = e.target.value.trim() - if (!val) { - setBankgiroError(null) - return - } - if (validateBankgiroNumber(val)) { - e.target.value = formatBankgiroNumber(val) - setBankgiroError(null) - } else { - setBankgiroError('Ogiltigt bankgironummer (7-8 siffror med kontrollsiffra)') - } - }} - /> - {bankgiroError && ( -

{bankgiroError}

- )} -
-
-
- - {/* Fakturainställningar */} - - - Fakturainställningar - - Numrering, betalningsvillkor och bokföringsmetod - - - -
-
- - -
-
- - -
-
- - -
-
- -
- - -

- {settings?.entity_type === 'aktiebolag' - ? 'Aktiebolag med omsättning över 3 MSEK måste använda faktureringsmetoden enligt BFL. Mindre aktiebolag kan välja kontantmetoden.' - : 'Kontantmetoden är tillgänglig för enskild firma med omsättning under 3 MSEK.'} -

-
- -
- -