From d0b3f21bde20a5338d3160e80da64e01aa945949 Mon Sep 17 00:00:00 2001 From: Jakob Wennberg <149234542+jakobwennberg@users.noreply.github.com> Date: Wed, 1 Apr 2026 17:08:00 +0200 Subject: [PATCH] feat: remove AI extensions, restructure settings, and add atomic voucher commits (#157) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove AI-dependent extensions (ai-chat, ai-categorization, receipt-ocr, invoice-inbox) and their infrastructure (lib/ai/*, ai-consent, LangChain/ Anthropic/OpenAI deps) to simplify core and reduce bundle size. Restructure monolithic settings page into dedicated sub-pages (company, bookkeeping, invoicing, tax, banking, api, account, team, templates) with shared layout and sidebar navigation. Add atomic commit_journal_entry RPC so voucher number increment and status update happen in a single transaction — prevents burned numbers on constraint failures. Add continuity check report and voucher gap explanation tracking. Co-authored-by: Claude Opus 4.6 (1M context) --- app/(dashboard)/import/page.tsx | 2 +- app/(dashboard)/kpi/page.tsx | 8 +- app/(dashboard)/receipts/page.tsx | 29 +- app/(dashboard)/receipts/scan/page.tsx | 20 +- app/(dashboard)/reports/page.tsx | 8 +- app/(dashboard)/settings/account/page.tsx | 100 ++ app/(dashboard)/settings/api/page.tsx | 7 + app/(dashboard)/settings/banking/page.tsx | 108 ++ app/(dashboard)/settings/bookkeeping/page.tsx | 94 ++ app/(dashboard)/settings/company/page.tsx | 49 + app/(dashboard)/settings/invoicing/page.tsx | 58 + app/(dashboard)/settings/layout.tsx | 63 + app/(dashboard)/settings/page.tsx | 861 +---------- app/(dashboard)/settings/tax/page.tsx | 27 + app/(dashboard)/settings/team/page.tsx | 7 + app/(dashboard)/settings/templates/page.tsx | 7 + app/(dashboard)/transactions/page.tsx | 3 +- app/(public)/privacy/page.tsx | 17 - .../admin/seed-template-embeddings/route.ts | 36 - app/api/ai-consent/__tests__/route.test.ts | 141 -- app/api/ai-consent/route.ts | 76 - app/api/bookkeeping/fiscal-periods/route.ts | 37 +- .../voucher-gaps/__tests__/route.test.ts | 241 ++++ app/api/bookkeeping/voucher-gaps/route.ts | 149 ++ .../enable-banking/sync/cron/route.ts | 2 +- app/api/extensions/ext/[...path]/route.ts | 14 +- .../webhook/__tests__/route.test.ts | 158 --- .../extensions/invoice-inbox/webhook/route.ts | 290 ---- app/api/import/sie/parse/route.ts | 25 +- app/api/reports/continuity-check/route.ts | 36 + app/api/settings/logo/route.ts | 96 ++ .../[id]/describe/__tests__/route.test.ts | 43 +- app/api/transactions/[id]/describe/route.ts | 87 +- .../transactions/suggest-categories/route.ts | 36 +- components/chat/ChatInput.tsx | 83 -- components/chat/ChatMessage.tsx | 157 -- components/chat/ChatPanel.tsx | 173 --- components/chat/ChatWidget.tsx | 67 - .../chat/artifacts/ArtifactRenderer.tsx | 45 - .../chat/artifacts/ChatAgingBuckets.tsx | 71 - components/chat/artifacts/ChatChart.tsx | 130 -- components/chat/artifacts/ChatDataTable.tsx | 80 -- components/chat/artifacts/ChatKpiCards.tsx | 47 - components/chat/index.ts | 5 - components/chat/useChatStream.ts | 233 --- components/dashboard/DashboardContent.tsx | 2 +- components/extensions/AiConsentDialog.tsx | 102 -- .../extensions/general/AiChatWorkspace.tsx | 12 - .../general/DocumentInboxWorkspace.tsx | 371 ----- .../general/EnableBankingWorkspace.tsx | 2 +- .../general/InvoiceInboxWorkspace.tsx | 328 ----- .../document-inbox/DocumentInboxCard.tsx | 150 -- .../document-inbox/ReceiptInboxDetail.tsx | 311 ---- .../invoice-inbox/InboxDetailDialog.tsx | 454 ------ .../general/invoice-inbox/InboxItemCard.tsx | 101 -- .../invoice-inbox/InboxSettingsDialog.tsx | 159 --- .../general/invoice-inbox/InboxUploadZone.tsx | 226 --- components/import/SIEPreviewStep.tsx | 8 +- components/settings/AccountDangerZone.tsx | 140 ++ components/settings/BankDetailsForm.tsx | 121 ++ components/settings/CompanyInfoForm.tsx | 105 ++ components/settings/InvoiceSettingsForm.tsx | 66 + components/settings/LogoUpload.tsx | 149 ++ components/settings/PdfPrintSettings.tsx | 127 ++ components/settings/PeriodLockingSettings.tsx | 56 + components/settings/SecuritySettings.tsx | 2 +- components/settings/SettingsFormWrapper.tsx | 88 ++ .../settings/SettingsLoadingSkeleton.tsx | 25 + components/settings/SettingsSidebar.tsx | 87 ++ components/settings/TaxSettingsForm.tsx | 34 + components/settings/VoucherSeriesManager.tsx | 74 + components/settings/useSettings.ts | 41 + .../transactions/SwipeCategorizationView.tsx | 23 +- .../general/ai-categorization/api-routes.ts | 161 --- .../general/ai-categorization/categorizer.ts | 465 ------ extensions/general/ai-categorization/index.ts | 400 ------ .../__tests__/description-analyzer.test.ts | 297 ---- .../lib/__tests__/template-embeddings.test.ts | 233 --- .../lib/description-analyzer.ts | 277 ---- .../lib/template-embeddings.ts | 265 ---- .../general/ai-categorization/manifest.json | 19 - extensions/general/ai-chat/api-routes.ts | 634 --------- extensions/general/ai-chat/chatbot/agent.ts | 133 -- .../general/ai-chat/chatbot/artifacts.ts | 237 ---- extensions/general/ai-chat/chatbot/chain.ts | 250 ---- extensions/general/ai-chat/chatbot/config.ts | 36 - .../general/ai-chat/chatbot/embeddings.ts | 25 - extensions/general/ai-chat/chatbot/prompts.ts | 134 -- .../general/ai-chat/chatbot/retriever.ts | 51 - extensions/general/ai-chat/chatbot/router.ts | 156 -- extensions/general/ai-chat/chatbot/tools.ts | 586 -------- extensions/general/ai-chat/chatbot/tracing.ts | 49 - extensions/general/ai-chat/index.ts | 25 - .../general/ai-chat/ingestion/ingest.ts | 353 ----- extensions/general/ai-chat/manifest.json | 26 - extensions/general/enable-banking/index.ts | 2 +- .../invoice-inbox/__tests__/index.test.ts | 103 -- .../general/invoice-inbox/api-routes.ts | 1257 ----------------- extensions/general/invoice-inbox/index.ts | 227 --- .../lib/__tests__/email-handler.test.ts | 182 --- .../lib/__tests__/invoice-analyzer.test.ts | 113 -- .../lib/__tests__/supplier-matcher.test.ts | 240 ---- .../invoice-inbox/lib/email-handler.ts | 83 -- .../invoice-inbox/lib/invoice-analyzer.ts | 23 - .../invoice-inbox/lib/supplier-matcher.ts | 197 --- .../general/invoice-inbox/manifest.json | 25 - extensions/general/invoice-inbox/types.ts | 41 - extensions/general/mcp-server/index.ts | 2 +- .../receipt-ocr/__tests__/index.test.ts | 261 ---- extensions/general/receipt-ocr/api-routes.ts | 875 ------------ .../receipt-ocr/components/ReceiptCamera.tsx | 336 ----- .../components/ReceiptDashboard.tsx | 169 --- .../components/ReceiptLineItemRow.tsx | 99 -- .../components/ReceiptReviewView.tsx | 418 ------ .../components/TransactionMatcher.tsx | 229 --- .../general/receipt-ocr/components/index.ts | 5 - extensions/general/receipt-ocr/index.ts | 341 ----- .../lib/__tests__/receipt-categorizer.test.ts | 154 -- .../lib/__tests__/receipt-matcher.test.ts | 286 ---- .../lib/__tests__/receipt-pipeline.test.ts | 184 --- .../receipt-ocr/lib/receipt-analyzer.ts | 95 -- .../receipt-ocr/lib/receipt-categorizer.ts | 322 ----- .../receipt-ocr/lib/receipt-matcher.ts | 186 --- .../receipt-ocr/lib/receipt-pipeline.ts | 172 --- .../general/receipt-ocr/lib/receipt-utils.ts | 94 -- extensions/general/receipt-ocr/manifest.json | 25 - .../receipt-ocr/pages/ReceiptsPage.tsx | 344 ----- .../pages/scan/ScanReceiptPage.tsx | 157 -- extensions/general/receipt-ocr/types.ts | 15 - extensions/general/skatteverket/index.ts | 2 +- lib/ai/__tests__/document-analyzer.test.ts | 378 ----- lib/ai/__tests__/preprocess-image.test.ts | 68 - lib/ai/__tests__/validation-helpers.test.ts | 131 -- lib/ai/__tests__/vision-client.test.ts | 256 ---- lib/ai/document-analyzer.ts | 673 --------- lib/ai/preprocess-image.ts | 43 - lib/ai/usage-tracker.ts | 37 - lib/ai/validation-helpers.ts | 117 -- lib/ai/vision-client.ts | 140 -- lib/api/schemas.ts | 27 + .../__tests__/template-embeddings.test.ts | 103 +- .../__tests__/voucher-atomicity.test.ts | 131 ++ lib/bookkeeping/engine.ts | 44 +- lib/bookkeeping/template-embeddings.ts | 46 +- .../__tests__/year-end-service.test.ts | 250 +++- lib/core/bookkeeping/year-end-service.ts | 145 +- lib/documents/__tests__/classifier.test.ts | 119 -- lib/documents/classifier.ts | 27 - lib/extensions/__tests__/ai-consent.test.ts | 120 -- .../__tests__/context-factory.test.ts | 2 +- .../__tests__/invoice-inbox-utils.test.ts | 132 -- lib/extensions/__tests__/sectors.test.ts | 10 +- lib/extensions/ai-consent.ts | 134 -- lib/extensions/invoice-inbox-utils.ts | 94 -- lib/import/__tests__/sie-import.test.ts | 8 +- lib/import/__tests__/sie-parser.test.ts | 10 +- lib/import/sie-import.ts | 95 +- lib/import/sie-parser.ts | 40 +- lib/import/types.ts | 14 +- lib/invoices/pdf-template.tsx | 43 +- .../__tests__/continuity-check.test.ts | 294 ++++ lib/reports/continuity-check.ts | 124 ++ lib/transactions/category-suggestions.ts | 99 +- package-lock.json | 506 +------ package.json | 8 - .../20260401200000_settings_restructure.sql | 40 + ...20260401300000_add_continuity_verified.sql | 4 + ...0402100000_voucher_number_unique_index.sql | 6 + ...0260402100100_voucher_gap_explanations.sql | 55 + .../20260402100200_atomic_commit_entry.sql | 54 + tests/helpers.ts | 9 + types/index.ts | 57 + 172 files changed, 3621 insertions(+), 20538 deletions(-) create mode 100644 app/(dashboard)/settings/account/page.tsx create mode 100644 app/(dashboard)/settings/api/page.tsx create mode 100644 app/(dashboard)/settings/banking/page.tsx create mode 100644 app/(dashboard)/settings/bookkeeping/page.tsx create mode 100644 app/(dashboard)/settings/company/page.tsx create mode 100644 app/(dashboard)/settings/invoicing/page.tsx create mode 100644 app/(dashboard)/settings/layout.tsx create mode 100644 app/(dashboard)/settings/tax/page.tsx create mode 100644 app/(dashboard)/settings/team/page.tsx create mode 100644 app/(dashboard)/settings/templates/page.tsx delete mode 100644 app/api/admin/seed-template-embeddings/route.ts delete mode 100644 app/api/ai-consent/__tests__/route.test.ts delete mode 100644 app/api/ai-consent/route.ts create mode 100644 app/api/bookkeeping/voucher-gaps/__tests__/route.test.ts create mode 100644 app/api/bookkeeping/voucher-gaps/route.ts delete mode 100644 app/api/extensions/invoice-inbox/webhook/__tests__/route.test.ts delete mode 100644 app/api/extensions/invoice-inbox/webhook/route.ts create mode 100644 app/api/reports/continuity-check/route.ts create mode 100644 app/api/settings/logo/route.ts delete mode 100644 components/chat/ChatInput.tsx delete mode 100644 components/chat/ChatMessage.tsx delete mode 100644 components/chat/ChatPanel.tsx delete mode 100644 components/chat/ChatWidget.tsx delete mode 100644 components/chat/artifacts/ArtifactRenderer.tsx delete mode 100644 components/chat/artifacts/ChatAgingBuckets.tsx delete mode 100644 components/chat/artifacts/ChatChart.tsx delete mode 100644 components/chat/artifacts/ChatDataTable.tsx delete mode 100644 components/chat/artifacts/ChatKpiCards.tsx delete mode 100644 components/chat/index.ts delete mode 100644 components/chat/useChatStream.ts delete mode 100644 components/extensions/AiConsentDialog.tsx delete mode 100644 components/extensions/general/AiChatWorkspace.tsx delete mode 100644 components/extensions/general/DocumentInboxWorkspace.tsx delete mode 100644 components/extensions/general/InvoiceInboxWorkspace.tsx delete mode 100644 components/extensions/general/document-inbox/DocumentInboxCard.tsx delete mode 100644 components/extensions/general/document-inbox/ReceiptInboxDetail.tsx delete mode 100644 components/extensions/general/invoice-inbox/InboxDetailDialog.tsx delete mode 100644 components/extensions/general/invoice-inbox/InboxItemCard.tsx delete mode 100644 components/extensions/general/invoice-inbox/InboxSettingsDialog.tsx delete mode 100644 components/extensions/general/invoice-inbox/InboxUploadZone.tsx create mode 100644 components/settings/AccountDangerZone.tsx create mode 100644 components/settings/BankDetailsForm.tsx create mode 100644 components/settings/CompanyInfoForm.tsx create mode 100644 components/settings/InvoiceSettingsForm.tsx create mode 100644 components/settings/LogoUpload.tsx create mode 100644 components/settings/PdfPrintSettings.tsx create mode 100644 components/settings/PeriodLockingSettings.tsx create mode 100644 components/settings/SettingsFormWrapper.tsx create mode 100644 components/settings/SettingsLoadingSkeleton.tsx create mode 100644 components/settings/SettingsSidebar.tsx create mode 100644 components/settings/TaxSettingsForm.tsx create mode 100644 components/settings/VoucherSeriesManager.tsx create mode 100644 components/settings/useSettings.ts delete mode 100644 extensions/general/ai-categorization/api-routes.ts delete mode 100644 extensions/general/ai-categorization/categorizer.ts delete mode 100644 extensions/general/ai-categorization/index.ts delete mode 100644 extensions/general/ai-categorization/lib/__tests__/description-analyzer.test.ts delete mode 100644 extensions/general/ai-categorization/lib/__tests__/template-embeddings.test.ts delete mode 100644 extensions/general/ai-categorization/lib/description-analyzer.ts delete mode 100644 extensions/general/ai-categorization/lib/template-embeddings.ts delete mode 100644 extensions/general/ai-categorization/manifest.json delete mode 100644 extensions/general/ai-chat/api-routes.ts delete mode 100644 extensions/general/ai-chat/chatbot/agent.ts delete mode 100644 extensions/general/ai-chat/chatbot/artifacts.ts delete mode 100644 extensions/general/ai-chat/chatbot/chain.ts delete mode 100644 extensions/general/ai-chat/chatbot/config.ts delete mode 100644 extensions/general/ai-chat/chatbot/embeddings.ts delete mode 100644 extensions/general/ai-chat/chatbot/prompts.ts delete mode 100644 extensions/general/ai-chat/chatbot/retriever.ts delete mode 100644 extensions/general/ai-chat/chatbot/router.ts delete mode 100644 extensions/general/ai-chat/chatbot/tools.ts delete mode 100644 extensions/general/ai-chat/chatbot/tracing.ts delete mode 100644 extensions/general/ai-chat/index.ts delete mode 100644 extensions/general/ai-chat/ingestion/ingest.ts delete mode 100644 extensions/general/ai-chat/manifest.json delete mode 100644 extensions/general/invoice-inbox/__tests__/index.test.ts delete mode 100644 extensions/general/invoice-inbox/api-routes.ts delete mode 100644 extensions/general/invoice-inbox/index.ts delete mode 100644 extensions/general/invoice-inbox/lib/__tests__/email-handler.test.ts delete mode 100644 extensions/general/invoice-inbox/lib/__tests__/invoice-analyzer.test.ts delete mode 100644 extensions/general/invoice-inbox/lib/__tests__/supplier-matcher.test.ts delete mode 100644 extensions/general/invoice-inbox/lib/email-handler.ts delete mode 100644 extensions/general/invoice-inbox/lib/invoice-analyzer.ts delete mode 100644 extensions/general/invoice-inbox/lib/supplier-matcher.ts delete mode 100644 extensions/general/invoice-inbox/manifest.json delete mode 100644 extensions/general/invoice-inbox/types.ts delete mode 100644 extensions/general/receipt-ocr/__tests__/index.test.ts delete mode 100644 extensions/general/receipt-ocr/api-routes.ts delete mode 100644 extensions/general/receipt-ocr/components/ReceiptCamera.tsx delete mode 100644 extensions/general/receipt-ocr/components/ReceiptDashboard.tsx delete mode 100644 extensions/general/receipt-ocr/components/ReceiptLineItemRow.tsx delete mode 100644 extensions/general/receipt-ocr/components/ReceiptReviewView.tsx delete mode 100644 extensions/general/receipt-ocr/components/TransactionMatcher.tsx delete mode 100644 extensions/general/receipt-ocr/components/index.ts delete mode 100644 extensions/general/receipt-ocr/index.ts delete mode 100644 extensions/general/receipt-ocr/lib/__tests__/receipt-categorizer.test.ts delete mode 100644 extensions/general/receipt-ocr/lib/__tests__/receipt-matcher.test.ts delete mode 100644 extensions/general/receipt-ocr/lib/__tests__/receipt-pipeline.test.ts delete mode 100644 extensions/general/receipt-ocr/lib/receipt-analyzer.ts delete mode 100644 extensions/general/receipt-ocr/lib/receipt-categorizer.ts delete mode 100644 extensions/general/receipt-ocr/lib/receipt-matcher.ts delete mode 100644 extensions/general/receipt-ocr/lib/receipt-pipeline.ts delete mode 100644 extensions/general/receipt-ocr/lib/receipt-utils.ts delete mode 100644 extensions/general/receipt-ocr/manifest.json delete mode 100644 extensions/general/receipt-ocr/pages/ReceiptsPage.tsx delete mode 100644 extensions/general/receipt-ocr/pages/scan/ScanReceiptPage.tsx delete mode 100644 extensions/general/receipt-ocr/types.ts delete mode 100644 lib/ai/__tests__/document-analyzer.test.ts delete mode 100644 lib/ai/__tests__/preprocess-image.test.ts delete mode 100644 lib/ai/__tests__/validation-helpers.test.ts delete mode 100644 lib/ai/__tests__/vision-client.test.ts delete mode 100644 lib/ai/document-analyzer.ts delete mode 100644 lib/ai/preprocess-image.ts delete mode 100644 lib/ai/usage-tracker.ts delete mode 100644 lib/ai/validation-helpers.ts delete mode 100644 lib/ai/vision-client.ts create mode 100644 lib/bookkeeping/__tests__/voucher-atomicity.test.ts delete mode 100644 lib/documents/__tests__/classifier.test.ts delete mode 100644 lib/documents/classifier.ts delete mode 100644 lib/extensions/__tests__/ai-consent.test.ts delete mode 100644 lib/extensions/__tests__/invoice-inbox-utils.test.ts delete mode 100644 lib/extensions/ai-consent.ts delete mode 100644 lib/extensions/invoice-inbox-utils.ts create mode 100644 lib/reports/__tests__/continuity-check.test.ts create mode 100644 lib/reports/continuity-check.ts create mode 100644 supabase/migrations/20260401200000_settings_restructure.sql create mode 100644 supabase/migrations/20260401300000_add_continuity_verified.sql create mode 100644 supabase/migrations/20260402100000_voucher_number_unique_index.sql create mode 100644 supabase/migrations/20260402100100_voucher_gap_explanations.sql create mode 100644 supabase/migrations/20260402100200_atomic_commit_entry.sql 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.'} -

-
- -
- -