diff --git a/app/(dashboard)/deadlines/page.tsx b/app/(dashboard)/deadlines/page.tsx index c1ac98f7..19149859 100644 --- a/app/(dashboard)/deadlines/page.tsx +++ b/app/(dashboard)/deadlines/page.tsx @@ -23,40 +23,27 @@ export default function DeadlinesPage() { setIsLoading(true) try { - // Fetch deadlines with customer names - const { data: deadlinesData, error: deadlinesError } = await supabase - .from('deadlines') - .select('*, customer:customers(name)') - .order('due_date', { ascending: true }) - - if (deadlinesError) throw deadlinesError - - // Fetch customers for the form - const { data: customersData, error: customersError } = await supabase - .from('customers') - .select('id, name') - .order('name', { ascending: true }) - - if (customersError) throw customersError - - // Fetch overdue invoices summary const today = new Date().toISOString().split('T')[0] - const { data: overdueData, error: overdueError } = await supabase - .from('invoices') - .select('total_sek, total') - .in('status', ['sent', 'unpaid']) - .lt('due_date', today) - if (overdueError) throw overdueError + // Fetch all data in parallel + const [deadlinesRes, customersRes, overdueRes] = await Promise.all([ + supabase.from('deadlines').select('*, customer:customers(name)').order('due_date', { ascending: true }), + supabase.from('customers').select('id, name').order('name', { ascending: true }), + supabase.from('invoices').select('total_sek, total').in('status', ['sent', 'unpaid']).lt('due_date', today), + ]) - const overdueCount = overdueData?.length || 0 - const overdueTotal = (overdueData || []).reduce( + if (deadlinesRes.error) throw deadlinesRes.error + if (customersRes.error) throw customersRes.error + if (overdueRes.error) throw overdueRes.error + + const overdueCount = overdueRes.data?.length || 0 + const overdueTotal = (overdueRes.data || []).reduce( (sum, inv) => sum + (inv.total_sek || inv.total || 0), 0 ) - setDeadlines(deadlinesData || []) - setCustomers(customersData || []) + setDeadlines(deadlinesRes.data || []) + setCustomers(customersRes.data || []) setOverdueInvoices({ count: overdueCount, total: overdueTotal }) } catch { toast({ diff --git a/app/(dashboard)/import/page.tsx b/app/(dashboard)/import/page.tsx index efcc3b72..2f993c01 100644 --- a/app/(dashboard)/import/page.tsx +++ b/app/(dashboard)/import/page.tsx @@ -38,6 +38,7 @@ import type { ParseIssue, } from '@/lib/import/types' import type { BASAccount } from '@/types' +import { ENABLED_EXTENSION_IDS } from '@/lib/extensions/_generated/enabled-extensions' // ============================================================ // Bank File Import Wizard Steps @@ -733,16 +734,21 @@ type ImportMode = null | 'psd2' | 'bank' | 'sie' export default function ImportPage() { const [mode, setMode] = useState(null) - const [hasBankingExtension, setHasBankingExtension] = useState(false) + // If extension isn't compiled in, we know synchronously it's unavailable + const bankingCompiledIn = ENABLED_EXTENSION_IDS.has('enable-banking') + const [hasBankingExtension, setHasBankingExtension] = useState( + bankingCompiledIn ? null : false + ) useEffect(() => { + if (!bankingCompiledIn) return fetch('/api/extensions/toggles/general/enable-banking') .then(res => res.ok ? res.json() : null) .then(json => { - if (json?.data?.enabled) setHasBankingExtension(true) + setHasBankingExtension(json?.data?.enabled || false) }) - .catch(() => {}) - }, []) + .catch(() => setHasBankingExtension(false)) + }, [bankingCompiledIn]) return (
@@ -755,11 +761,26 @@ export default function ImportPage() {
{mode === null && ( -
- {hasBankingExtension && ( +
+ {hasBankingExtension === null && ( + + +
+
+
+
+
+
+ + + )} + {hasBankingExtension === true && ( setMode('psd2')} + onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setMode('psd2') } }} >
@@ -779,8 +800,11 @@ export default function ImportPage() { )} setMode('bank')} + onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setMode('bank') } }} >
@@ -799,8 +823,11 @@ export default function ImportPage() { setMode('sie')} + onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setMode('sie') } }} >
diff --git a/app/(dashboard)/page.tsx b/app/(dashboard)/page.tsx index a2977742..7f603edd 100644 --- a/app/(dashboard)/page.tsx +++ b/app/(dashboard)/page.tsx @@ -14,44 +14,70 @@ export default async function DashboardPage() { redirect('/login') } - // Fetch profile for name - const { data: profile } = await supabase - .from('profiles') - .select('full_name') - .eq('id', user.id) - .single() + // Fetch current year date boundaries + const startOfYearStr = new Date(new Date().getFullYear(), 0, 1).toISOString().split('T')[0] + const startOfMonthStr = new Date(new Date().getFullYear(), new Date().getMonth(), 1).toISOString().split('T')[0] + const now = new Date() + const today = now.toISOString().split('T')[0] + const nextWeek = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0] + + // Source types that require supporting documents + const needsDocSourceTypes = [ + 'manual', + 'bank_transaction', + 'supplier_invoice_registered', + 'supplier_invoice_paid', + 'supplier_invoice_cash_payment', + 'import', + ] + + // Fetch all data in parallel + const [ + { data: profile }, + { data: settings }, + { count: customerCount }, + { count: invoiceCount }, + { count: receiptCount }, + { count: transactionCount }, + { data: journalLines }, + { data: transactions }, + { data: unpaidInvoices }, + { data: bankConnections }, + { data: deadlines }, + { count: pendingReviewCount }, + { count: unmatchedReceiptsCount }, + { count: unmatchedTransactionsCount }, + { count: postedEntriesCount }, + { data: entriesWithDocs }, + { data: recentReceiptActivity }, + { data: enabledToggles }, + ] = await Promise.all([ + supabase.from('profiles').select('full_name').eq('id', user.id).single(), + supabase.from('company_settings').select('*').eq('user_id', user.id).single(), + supabase.from('customers').select('*', { count: 'exact', head: true }).eq('user_id', user.id), + supabase.from('invoices').select('*', { count: 'exact', head: true }).eq('user_id', user.id), + supabase.from('receipts').select('*', { count: 'exact', head: true }).eq('user_id', user.id), + supabase.from('transactions').select('*', { count: 'exact', head: true }).eq('user_id', user.id), + supabase.from('journal_entry_lines') + .select('account_number, debit_amount, credit_amount, journal_entry:journal_entries!inner(entry_date, status)') + .eq('journal_entry.status', 'posted') + .gte('journal_entry.entry_date', startOfYearStr), + supabase.from('transactions').select('amount, amount_sek, is_business').eq('user_id', user.id).gte('date', startOfYearStr), + supabase.from('invoices').select('total, total_sek, vat_amount, vat_amount_sek, status').eq('user_id', user.id).in('status', ['sent', 'overdue']), + supabase.from('bank_connections').select('id, accounts_data, status, consent_expires, bank_name').eq('user_id', user.id).eq('status', 'active'), + supabase.from('deadlines').select('*, customer:customers(id, name)').eq('user_id', user.id).eq('is_completed', false) + .or(`due_date.lt.${today},due_date.lte.${nextWeek}`).order('due_date', { ascending: true }), + supabase.from('receipts').select('*', { count: 'exact', head: true }).eq('user_id', user.id).eq('status', 'extracted'), + supabase.from('receipts').select('*', { count: 'exact', head: true }).eq('user_id', user.id).eq('status', 'confirmed').is('matched_transaction_id', null), + supabase.from('transactions').select('*', { count: 'exact', head: true }).eq('user_id', user.id).lt('amount', 0).is('receipt_id', null), + supabase.from('journal_entries').select('*', { count: 'exact', head: true }).eq('user_id', user.id).eq('status', 'posted').in('source_type', needsDocSourceTypes), + supabase.from('document_attachments').select('journal_entry_id').eq('user_id', user.id).eq('is_current_version', true).not('journal_entry_id', 'is', null), + supabase.from('receipts').select('created_at').eq('user_id', user.id).eq('status', 'confirmed').order('created_at', { ascending: false }).limit(30), + supabase.from('extension_toggles').select('sector_slug, extension_slug').eq('user_id', user.id).eq('enabled', true), + ]) const firstName = profile?.full_name?.split(' ')[0] || null - // Fetch company settings - const { data: settings } = await supabase - .from('company_settings') - .select('*') - .eq('user_id', user.id) - .single() - - // ── EF / AB dashboard ────────────────────────────────────────────── - // Fetch onboarding progress for new user checklist - const { count: customerCount } = await supabase - .from('customers') - .select('*', { count: 'exact', head: true }) - .eq('user_id', user.id) - - const { count: invoiceCount } = await supabase - .from('invoices') - .select('*', { count: 'exact', head: true }) - .eq('user_id', user.id) - - const { count: receiptCount } = await supabase - .from('receipts') - .select('*', { count: 'exact', head: true }) - .eq('user_id', user.id) - - const { count: transactionCount } = await supabase - .from('transactions') - .select('*', { count: 'exact', head: true }) - .eq('user_id', user.id) - const onboardingProgress: OnboardingProgress = { hasCustomers: (customerCount || 0) > 0, hasInvoices: (invoiceCount || 0) > 0, @@ -59,17 +85,6 @@ export default async function DashboardPage() { hasBankConnected: (transactionCount || 0) > 0, } - // Fetch current year date boundaries - const startOfYearStr = new Date(new Date().getFullYear(), 0, 1).toISOString().split('T')[0] - const startOfMonthStr = new Date(new Date().getFullYear(), new Date().getMonth(), 1).toISOString().split('T')[0] - - // Fetch posted journal entry lines for revenue/expense accounts (classes 3-7) - const { data: journalLines } = await supabase - .from('journal_entry_lines') - .select('account_number, debit_amount, credit_amount, journal_entry:journal_entries!inner(entry_date, status)') - .eq('journal_entry.status', 'posted') - .gte('journal_entry.entry_date', startOfYearStr) - // Calculate totals from journal entry lines using account classes const calculateTotals = (lines: typeof journalLines, fromDate: string) => { const filtered = (lines || []).filter((l) => { @@ -100,13 +115,6 @@ export default async function DashboardPage() { const ytdTotals = calculateTotals(journalLines, startOfYearStr) const mtdTotals = calculateTotals(journalLines, startOfMonthStr) - // Fetch uncategorized transaction counts (still useful for the alert) - const { data: transactions } = await supabase - .from('transactions') - .select('amount, amount_sek, is_business') - .eq('user_id', user.id) - .gte('date', startOfYearStr) - const uncategorizedTxns = (transactions || []).filter( (t) => t.is_business === null ) @@ -118,19 +126,11 @@ export default async function DashboardPage() { .filter((t) => t.amount < 0) .reduce((sum, t) => sum + Math.abs(Number(t.amount_sek || t.amount)), 0) - // Fetch unpaid invoices with VAT amounts - const { data: unpaidInvoices } = await supabase - .from('invoices') - .select('total, total_sek, vat_amount, vat_amount_sek, status') - .eq('user_id', user.id) - .in('status', ['sent', 'overdue']) - const unpaidTotal = (unpaidInvoices || []).reduce( (sum, inv) => sum + Number(inv.total_sek || inv.total), 0 ) - // Calculate VAT from unpaid invoices (this is VAT we've invoiced but not yet received) const unpaidVatTotal = (unpaidInvoices || []).reduce( (sum, inv) => sum + Number(inv.vat_amount_sek || inv.vat_amount || 0), 0 @@ -140,13 +140,6 @@ export default async function DashboardPage() { (inv) => inv.status === 'overdue' ).length - // Fetch bank balance and consent info (if connected) - const { data: bankConnections } = await supabase - .from('bank_connections') - .select('id, accounts_data, status, consent_expires, bank_name') - .eq('user_id', user.id) - .eq('status', 'active') - let bankBalance: number | null = null if (bankConnections && bankConnections.length > 0) { const allBalances = bankConnections.flatMap(conn => { @@ -158,7 +151,6 @@ export default async function DashboardPage() { } } - // Compute expiring bank connections (consent expires within 14 days) const nowMs = new Date().getTime() const expiringBankConnections = (bankConnections || []) .filter(conn => { @@ -176,80 +168,12 @@ export default async function DashboardPage() { ), })) - // Fetch upcoming deadlines (next 7 days + overdue) - const now = new Date() - const today = now.toISOString().split('T')[0] - const nextWeek = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0] - - const { data: deadlines } = await supabase - .from('deadlines') - .select('*, customer:customers(id, name)') - .eq('user_id', user.id) - .eq('is_completed', false) - .or(`due_date.lt.${today},due_date.lte.${nextWeek}`) - .order('due_date', { ascending: true }) - - // Fetch receipt queue summary - const { count: pendingReviewCount } = await supabase - .from('receipts') - .select('*', { count: 'exact', head: true }) - .eq('user_id', user.id) - .eq('status', 'extracted') - - const { count: unmatchedReceiptsCount } = await supabase - .from('receipts') - .select('*', { count: 'exact', head: true }) - .eq('user_id', user.id) - .eq('status', 'confirmed') - .is('matched_transaction_id', null) - - const { count: unmatchedTransactionsCount } = await supabase - .from('transactions') - .select('*', { count: 'exact', head: true }) - .eq('user_id', user.id) - .lt('amount', 0) - .is('receipt_id', null) - - // Count journal entries missing underlag (documents) - // Source types that require supporting documents - const needsDocSourceTypes = [ - 'manual', - 'bank_transaction', - 'supplier_invoice_registered', - 'supplier_invoice_paid', - 'supplier_invoice_cash_payment', - 'import', - ] - - const { count: postedEntriesCount } = await supabase - .from('journal_entries') - .select('*', { count: 'exact', head: true }) - .eq('user_id', user.id) - .eq('status', 'posted') - .in('source_type', needsDocSourceTypes) - - const { data: entriesWithDocs } = await supabase - .from('document_attachments') - .select('journal_entry_id') - .eq('user_id', user.id) - .eq('is_current_version', true) - .not('journal_entry_id', 'is', null) - const uniqueEntriesWithDocs = new Set( (entriesWithDocs || []).map((d) => d.journal_entry_id) ).size const missingUnderlagCount = Math.max(0, (postedEntriesCount || 0) - uniqueEntriesWithDocs) - // Calculate receipt streak - const { data: recentReceiptActivity } = await supabase - .from('receipts') - .select('created_at') - .eq('user_id', user.id) - .eq('status', 'confirmed') - .order('created_at', { ascending: false }) - .limit(30) - let streakCount = 0 if (recentReceiptActivity && recentReceiptActivity.length > 0) { const todayDate = new Date() @@ -273,13 +197,6 @@ export default async function DashboardPage() { streak_count: streakCount, } - // Fetch enabled extension toggles - const { data: enabledToggles } = await supabase - .from('extension_toggles') - .select('sector_slug, extension_slug') - .eq('user_id', user.id) - .eq('enabled', true) - return ( (null) - const [hasBankingExtension, setHasBankingExtension] = useState(false) + const [hasBankingExtension, setHasBankingExtension] = useState( + bankingCompiledIn ? null : false + ) const [hasCalendarExtension, setHasCalendarExtension] = useState(false) const [showDeleteDialog, setShowDeleteDialog] = useState(false) const [deleteConfirmText, setDeleteConfirmText] = useState('') @@ -70,21 +74,15 @@ export default function SettingsPage() { return } - // Fetch settings - const { data: settingsData } = await supabase - .from('company_settings') - .select('*') - .eq('user_id', user.id) - .single() - - setSettings(settingsData) - - // Check extension toggles in parallel - const [bankingToggleRes, calendarToggleRes] = await Promise.all([ + // Fetch settings and extension toggles in parallel + const [settingsRes, bankingToggleRes, calendarToggleRes] = await Promise.all([ + supabase.from('company_settings').select('*').eq('user_id', user.id).single(), fetch('/api/extensions/toggles/general/enable-banking').catch(() => null), fetch('/api/extensions/toggles/general/calendar').catch(() => null), ]) + setSettings(settingsRes.data) + if (bankingToggleRes?.ok) { const { data } = await bankingToggleRes.json() setHasBankingExtension(data?.enabled || false) @@ -541,7 +539,11 @@ export default function SettingsPage() { {/* Banking settings — loaded dynamically from extension, hidden for sandbox */} {!settings?.is_sandbox && ( - {hasBankingExtension && BankingPanel ? ( + {hasBankingExtension === null ? ( +
+ +
+ ) : hasBankingExtension && BankingPanel ? ( ) : ( diff --git a/app/(dashboard)/transactions/page.tsx b/app/(dashboard)/transactions/page.tsx index 1f067182..b8fc63b2 100644 --- a/app/(dashboard)/transactions/page.tsx +++ b/app/(dashboard)/transactions/page.tsx @@ -110,40 +110,36 @@ export default function TransactionsPage() { .filter((t) => t.potential_invoice_id) .map((t) => t.potential_invoice_id) - let invoiceMap: Record = {} - if (potentialInvoiceIds.length > 0) { - const { data: invoices } = await supabase - .from('invoices') - .select('*, customer:customers(*)') - .in('id', potentialInvoiceIds) - if (invoices) { - invoiceMap = invoices.reduce((acc, inv) => { - acc[inv.id] = inv - return acc - }, {} as Record) - } - } - - // Fetch matched inbox items for unbooked transactions const unbookedTxIds = (txData || []) .filter((t) => !t.journal_entry_id && t.is_business === null) .map((t) => t.id) + // Fetch invoices and inbox items in parallel + const [invoiceResult, inboxResult] = await Promise.all([ + potentialInvoiceIds.length > 0 + ? supabase.from('invoices').select('*, customer:customers(*)').in('id', potentialInvoiceIds) + : Promise.resolve({ data: null }), + unbookedTxIds.length > 0 + ? supabase.from('invoice_inbox_items').select('*').in('matched_transaction_id', unbookedTxIds).in('status', ['ready', 'processing']) + : Promise.resolve({ data: null }), + ]) + + let invoiceMap: Record = {} + if (invoiceResult.data) { + invoiceMap = invoiceResult.data.reduce((acc, inv) => { + acc[inv.id] = inv + return acc + }, {} as Record) + } + let inboxItemMap: Record = {} - if (unbookedTxIds.length > 0) { - const { data: inboxItems } = await supabase - .from('invoice_inbox_items') - .select('*') - .in('matched_transaction_id', unbookedTxIds) - .in('status', ['ready', 'processing']) - if (inboxItems) { - inboxItemMap = inboxItems.reduce((acc, item) => { - if (item.matched_transaction_id) { - acc[item.matched_transaction_id] = item as InvoiceInboxItem - } - return acc - }, {} as Record) - } + if (inboxResult.data) { + inboxItemMap = inboxResult.data.reduce((acc, item) => { + if (item.matched_transaction_id) { + acc[item.matched_transaction_id] = item as InvoiceInboxItem + } + return acc + }, {} as Record) } const transactionsWithInvoices: TransactionWithInvoice[] = (txData || []).map((t) => ({ @@ -178,24 +174,27 @@ export default function TransactionsPage() { setIsLoadingSuggestions(false) } - // Fetch transactions on mount - // eslint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => { fetchTransactions() }, []) - - // Fetch entity type for tooltip context + // Fetch transactions and entity type in parallel on mount, then suggestions useEffect(() => { - async function fetchEntityType() { - try { - const res = await fetch('/api/settings') - const data = await res.json() - if (data?.entity_type) { - setEntityType(data.entity_type) - } - } catch { - // Non-critical, defaults to enskild_firma + let cancelled = false + + async function loadAll() { + // Fetch transactions and entity type in parallel + const [, entityRes] = await Promise.all([ + fetchTransactions(), + fetch('/api/settings').then(r => r.json()).catch(() => null), + ]) + + if (cancelled) return + + if (entityRes?.entity_type) { + setEntityType(entityRes.entity_type) } } - fetchEntityType() + + loadAll() + + return () => { cancelled = true } }, []) // Auto-fetch suggestions when transactions load @@ -207,6 +206,7 @@ export default function TransactionsPage() { if (uncatIds.length > 0) { fetchCategorySuggestions(uncatIds) } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [transactions.length]) const handleCategorize: CategorizeHandler = async (id, isBusiness, category, vatTreatment, accountOverride, templateId, inboxItemId) => { diff --git a/app/(public)/dpa/page.tsx b/app/(public)/dpa/page.tsx index a7d1a17a..64832654 100644 --- a/app/(public)/dpa/page.tsx +++ b/app/(public)/dpa/page.tsx @@ -25,14 +25,14 @@ export default function DPAPage() {

- Detta personuppgiftsbitradesavtal ("DPA") ingar mellan: + Detta personuppgiftsbitradesavtal ("DPA") ingår mellan:

    -
  • Personuppgiftsansvarig ("den Ansvarige"): Du som anvandare av Gnubok, - i egenskap av ansvarig for de personuppgifter du registrerar i tjansten - (kunder, leverantorer, anstallda m.fl.).
  • -
  • Personuppgiftsbitrade ("Bitradet"): Arcim, som tillhandahaller - Gnubok-tjansten och behandlar personuppgifter pa dina vagar.
  • +
  • Personuppgiftsansvarig ("den Ansvarige"): Du som användare av Gnubok, + i egenskap av ansvarig för de personuppgifter du registrerar i tjänsten + (kunder, leverantörer, anställda m.fl.).
  • +
  • Personuppgiftsbiträde ("Biträdet"): Arcim, som tillhandahåller + Gnubok-tjänsten och behandlar personuppgifter på dina vägnar.
@@ -42,10 +42,10 @@ export default function DPAPage() { 2. Behandlingens syfte och omfattning -

Bitradet behandlar personuppgifter for foljande andamal:

+

Biträdet behandlar personuppgifter för följande ändamål:

    -
  • Tillhandahallande av bokforings- och redovisningstjanster
  • -
  • Lagring och arkivering av bokforingsmaterial
  • +
  • Tillhandahållande av bokförings- och redovisningstjänster
  • +
  • Lagring och arkivering av bokföringsmaterial
  • Fakturering och betalningshantering
  • Bankkontosynkronisering (PSD2)
  • AI-assisterad kategorisering och kvittohantering (efter separat samtycke)
  • @@ -53,92 +53,92 @@ export default function DPAPage() {

    Kategorier av registrerade vars uppgifter behandlas:

    • Den Ansvariges kunder (namn, kontaktuppgifter, organisationsnummer)
    • -
    • Den Ansvariges leverantorer (namn, kontaktuppgifter, bankuppgifter)
    • -
    • Den Ansvarige sjalv (kontouppgifter, foretagsinformation)
    • +
    • Den Ansvariges leverantörer (namn, kontaktuppgifter, bankuppgifter)
    • +
    • Den Ansvarige själv (kontouppgifter, företagsinformation)
    - 3. Tekniska och organisatoriska atgarder + 3. Tekniska och organisatoriska åtgärder -

    Bitradet vidtar foljande atgarder for att skydda personuppgifterna:

    +

    Biträdet vidtar följande åtgärder för att skydda personuppgifterna:

    • Kryptering: All data krypteras i transit (TLS 1.3) och i vila (AES-256)
    • -
    • Atkomstkontroll: Row Level Security (RLS) sakerstaller att varje anvandare - enbart kan komma at sina egna uppgifter
    • -
    • Autentisering: Sakra inloggningsmetoder (magic link, inga losenord lagrade)
    • -
    • Integritetskontroll: SHA-256 checksummor for alla dokument, med +
    • Åtkomstkontroll: Row Level Security (RLS) säkerställer att varje användare + enbart kan komma åt sina egna uppgifter
    • +
    • Autentisering: Säkra inloggningsmetoder (magic link, inga lösenord lagrade)
    • +
    • Integritetskontroll: SHA-256 checksummor för alla dokument, med regelbunden verifiering
    • -
    • Revisionslogg: Alla andringshandelser loggas automatiskt av databasen +
    • Revisionslogg: Alla ändringshandelser loggas automatiskt av databasen (ej redigerbara)
    • -
    • Oforanderlig bokforing: Bokforda verifikationer kan inte andras eller - raderas (databasutlosare)
    • -
    • Sakerhetskopior: Kontinuerliga databaskopior med point-in-time-recovery
    • -
    • EU-lagring: All primar datalagring sker i EU (eu-central-1)
    • +
    • Oföränderlig bokföring: Bokförda verifikationer kan inte ändras eller + raderas (databasutlösare)
    • +
    • Säkerhetskopior: Kontinuerliga databaskopior med point-in-time-recovery
    • +
    • EU-lagring: All primär datalagring sker i EU (eu-central-1)
    - 4. Underbitraden + 4. Underbiträden

    - Bitradet anvander underbitraden for att tillhandahalla tjansten. En fullstandig - forteckning over underbitraden, inklusive syfte och geografisk plats, finns i - var{' '} + Biträdet använder underbiträden för att tillhandahålla tjänsten. En fullständig + förteckning över underbiträden, inklusive syfte och geografisk plats, finns i + vår{' '} integritetspolicy .

    - Bitradet kommer att informera den Ansvarige minst 30 dagar i forvag innan - en ny underbitrade anlitas, sa att den Ansvarige har mojlighet att invanda. + Biträdet kommer att informera den Ansvarige minst 30 dagar i förväg innan + en ny underbiträde anlitas, så att den Ansvarige har möjlighet att invända.

    - 5. Dataintrangsnotifiering + 5. Dataintrångsnotifiering

    - Vid en personuppgiftsincident ska Bitradet utan ondodigt drojsmal, och senast - inom 72 timmar fran det att incidenten upptacktes, meddela den Ansvarige. - Meddelandet ska innehalla: + Vid en personuppgiftsincident ska Biträdet utan onödigt dröjsmål, och senast + inom 72 timmar från det att incidenten upptäcktes, meddela den Ansvarige. + Meddelandet ska innehålla:

    • Typ av personuppgiftsincident
    • -
    • Kategorier och ungefirligt antal registrerade som berorts
    • +
    • Kategorier och ungefärligt antal registrerade som berörts
    • Sannolika konsekvenser av incidenten
    • -
    • Atgarder som vidtagits eller foreslas for att hantera incidenten
    • +
    • Åtgärder som vidtagits eller föreslås för att hantera incidenten

    - Bitradet ska bistå den Ansvarige med den information som behovs for att den - Ansvarige ska kunna uppfylla sin anmalningsplikt till IMY (Integritetsskyddsmyndigheten). + Biträdet ska bistå den Ansvarige med den information som behövs för att den + Ansvarige ska kunna uppfylla sin anmälningsplikt till IMY (Integritetsskyddsmyndigheten).

    - 6. Revisionsratt + 6. Revisionsrätt

    - Den Ansvarige har ratt att, direkt eller genom en oberoende revisor, utfora - revisioner och inspektioner for att sakerst alla att Bitradet uppfyller sina - atagarder enligt detta avtal. Bitradet ska tillhandahalla all nodvandig + Den Ansvarige har rätt att, direkt eller genom en oberoende revisor, utföra + revisioner och inspektioner för att säkerställa att Biträdet uppfyller sina + åtaganden enligt detta avtal. Biträdet ska tillhandahålla all nödvändig information och medverka till revisioner.

    Revisioner ska ske med rimligt varsel (minst 30 dagar) och under ordinarie - kontorstider. Bitradet kan erbjuda alternativ i form av tredjepartsgranskningar + kontorstider. Biträdet kan erbjuda alternativ i form av tredjepartsgranskningar eller certifieringar.

    @@ -150,22 +150,22 @@ export default function DPAPage() {

    - Vid uppsagning av tjansten ska Bitradet, enligt den Ansvariges val: + Vid uppsägning av tjänsten ska Biträdet, enligt den Ansvariges val:

    • - Aterlamna: Exportera alla personuppgifter i maskinlasbart format - (SIE4, JSON, CSV) via tjansens exportfunktioner. + Återlämna: Exportera alla personuppgifter i maskinläsbart format + (SIE4, JSON, CSV) via tjänstens exportfunktioner.
    • - Radera: Radera alla personuppgifter inom 30 dagar fran - anvandarens begaran, med undantag for uppgifter som maste bevaras enligt lag. + Radera: Radera alla personuppgifter inom 30 dagar från + användarens begäran, med undantag för uppgifter som måste bevaras enligt lag.

    - Undantag: Bokforingsmaterial som omfattas av Bokforingslagen (BFL) - 7 kap. 2 § (7 ars arkiveringskrav) raderas forst nar lagringsfristen lopt ut. - Under denna period ar materialet skyddat mot obehorig atkomst och andring. + Undantag: Bokföringsmaterial som omfattas av Bokföringslagen (BFL) + 7 kap. 2 § (7 års arkiveringskrav) raderas först när lagringsfristen löpt ut. + Under denna period är materialet skyddat mot obehörig åtkomst och ändring.

    @@ -173,9 +173,9 @@ export default function DPAPage() {

    - Detta personuppgiftsbitradesavtal trader i kraft nar du skapar ett konto pa - Gnubok och galler sa lange du anvander tjansten. For fragor, kontakta oss - pa privacy@gnubok.se. + Detta personuppgiftsbitradesavtal träder i kraft när du skapar ett konto på + Gnubok och gäller så länge du använder tjänsten. För frågor, kontakta oss + på privacy@gnubok.se.

    diff --git a/app/(public)/privacy/page.tsx b/app/(public)/privacy/page.tsx index efc856f6..660f10e4 100644 --- a/app/(public)/privacy/page.tsx +++ b/app/(public)/privacy/page.tsx @@ -24,9 +24,9 @@ export default function PrivacyPolicyPage() {

    - Arcim ("vi", "oss") ar personuppgiftsansvarig for behandlingen av dina - personuppgifter i samband med anvandningen av Gnubok. Vi behandlar dina uppgifter i - enlighet med EU:s dataskyddsforordning (GDPR) och svensk dataskyddslagstiftning. + Arcim ("vi", "oss") är personuppgiftsansvarig för behandlingen av dina + personuppgifter i samband med användningen av Gnubok. Vi behandlar dina uppgifter i + enlighet med EU:s dataskyddsförordning (GDPR) och svensk dataskyddslagstiftning.

    @@ -36,39 +36,39 @@ export default function PrivacyPolicyPage() { 2. Vilka uppgifter vi behandlar -

    Vi behandlar foljande kategorier av personuppgifter:

    +

    Vi behandlar följande kategorier av personuppgifter:

      -
    • Kontouppgifter: E-postadress (for inloggning via magic link)
    • -
    • Foretagsuppgifter: Foretagsnamn, organisationsnummer, adress, kontaktuppgifter
    • -
    • Bokforingsdata: Verifikationer, fakturor, kvitton, transaktioner, kontoplaner
    • +
    • Kontouppgifter: E-postadress (för inloggning via magic link)
    • +
    • Företagsuppgifter: Företagsnamn, organisationsnummer, adress, kontaktuppgifter
    • +
    • Bokföringsdata: Verifikationer, fakturor, kvitton, transaktioner, kontoplaner
    • Bankdata: Kontosaldon och transaktioner (via PSD2-koppling)
    • -
    • Dokument: Uppladdade kvitton, fakturor och andra bokforingsunderlag
    • -
    • Tekniska uppgifter: IP-adress, enhetstyp, anvandningsstatistik
    • +
    • Dokument: Uppladdade kvitton, fakturor och andra bokföringsunderlag
    • +
    • Tekniska uppgifter: IP-adress, enhetstyp, användningsstatistik
    - 3. Rattslig grund (GDPR Art. 6) + 3. Rättslig grund (GDPR Art. 6)
    • - Avtal (Art. 6.1b): Behandling som ar nodvandig for att fullgora vara - tjanster enligt anvandaravtalet. + Avtal (Art. 6.1b): Behandling som är nödvändig för att fullgöra våra + tjänster enligt användaravtalet.
    • - Rattslig forpliktelse (Art. 6.1c): Bokforingslagens (BFL) krav pa - 7 ars arkivering av raknenskapsmaterial. + Rättslig förpliktelse (Art. 6.1c): Bokföringslagens (BFL) krav på + 7 års arkivering av räkenskapsmaterial.
    • - Berattigat intresse (Art. 6.1f): Produktforbattringar, sakerhet och - bedrageriforbud. + Berättigat intresse (Art. 6.1f): Produktförbättringar, säkerhet och + bedrägeriforbud.
    • - Samtycke (Art. 6.1a): For AI-baserade funktioner som skickar data - till tredjepartstjanster (se separat samtycke vid aktivering). + Samtycke (Art. 6.1a): För AI-baserade funktioner som skickar data + till tredjepartstjänster (se separat samtycke vid aktivering).
    @@ -76,12 +76,12 @@ export default function PrivacyPolicyPage() { - 4. Underbitraden + 4. Underbiträden

    - Vi anvander foljande underbitraden for att tillhandahalla tjansten. Uppgifterna nedan anger - vilka uppgifter som delas med respektive underbitrade, syftet samt var behandlingen sker + Vi använder följande underbiträden för att tillhandahålla tjänsten. Uppgifterna nedan anger + vilka uppgifter som delas med respektive underbiträde, syftet samt var behandlingen sker (GDPR Art. 13).

    @@ -89,7 +89,7 @@ export default function PrivacyPolicyPage() { - + @@ -105,7 +105,7 @@ export default function PrivacyPolicyPage() { - + @@ -120,7 +120,7 @@ export default function PrivacyPolicyPage() { @@ -139,7 +139,7 @@ export default function PrivacyPolicyPage() { - + @@ -148,21 +148,21 @@ export default function PrivacyPolicyPage() {

    - AI-funktioner (Anthropic, OpenAI) kraver separat samtycke fore aktivering. - Data skickas forst nar du aktivt godkanner anvandningen. + AI-funktioner (Anthropic, OpenAI) kräver separat samtycke före aktivering. + Data skickas först när du aktivt godkänner användningen.

    - 5. Tredjelandsoverforing + 5. Tredjelandsöverföring

    - Vissa underbitraden ar baserade i USA. For dessa overforing anvands EU-kommissionens + Vissa underbiträden är baserade i USA. För dessa överföringar används EU-kommissionens standardavtalsklausuler (SCCs) som skyddsmekanism i enlighet med GDPR kapitel V. - All primaer datalagring (databas, filer) sker inom EU via Supabase (eu-central-1). + All primär datalagring (databas, filer) sker inom EU via Supabase (eu-central-1).

    @@ -174,13 +174,13 @@ export default function PrivacyPolicyPage() {
    • - Bokforingsmaterial: 7 ar fran rakenskapsarets slut, i enlighet - med Bokforingslagen (BFL) 7 kap. 2 §. Systemet hindrar radering av material - kopplat till bokforda verifikationer under denna period. + Bokföringsmaterial: 7 år från räkenskapsårets slut, i enlighet + med Bokföringslagen (BFL) 7 kap. 2 §. Systemet hindrar radering av material + kopplat till bokförda verifikationer under denna period.
    • - Kontouppgifter: Sa lange kontot ar aktivt, plus 30 dagar efter - begaran om radering (for att hantera pagaende bokforingsplikter). + Kontouppgifter: Så länge kontot är aktivt, plus 30 dagar efter + begäran om radering (för att hantera pågående bokföringsplikter).
    • Tekniska loggar: Maximalt 90 dagar. @@ -191,24 +191,24 @@ export default function PrivacyPolicyPage() { - 7. Dina rattigheter + 7. Dina rättigheter -

      Du har foljande rattigheter enligt GDPR:

      +

      Du har följande rättigheter enligt GDPR:

        -
      • Tillgang (Art. 15): Du kan begara en kopia av alla dina personuppgifter.
      • -
      • Rattelse (Art. 16): Du kan begara rattelse av felaktiga uppgifter.
      • -
      • Radering (Art. 17): Du kan begara radering, med undantag for uppgifter som - omfattas av lagstadgade arkiveringskrav (BFL 7 ar).
      • -
      • Begransning (Art. 18): Du kan begara begransning av behandlingen.
      • +
      • Tillgång (Art. 15): Du kan begära en kopia av alla dina personuppgifter.
      • +
      • Rättelse (Art. 16): Du kan begära rättelse av felaktiga uppgifter.
      • +
      • Radering (Art. 17): Du kan begära radering, med undantag för uppgifter som + omfattas av lagstadgade arkiveringskrav (BFL 7 år).
      • +
      • Begränsning (Art. 18): Du kan begära begränsning av behandlingen.
      • Dataportabilitet (Art. 20): Du kan exportera dina uppgifter i - maskinlasbart format (SIE4, JSON, CSV) via exportfunktionerna i appen.
      • -
      • Invandning (Art. 21): Du kan invanda mot behandling baserad pa - berattigat intresse.
      • + maskinläsbart format (SIE4, JSON, CSV) via exportfunktionerna i appen. +
      • Invändning (Art. 21): Du kan invända mot behandling baserad på + berättigat intresse.

      - For att utova dina rattigheter, kontakta oss pa adressen nedan. Vi besvarar alla - forfragar inom 30 dagar. + För att utöva dina rättigheter, kontakta oss på adressen nedan. Vi besvarar alla + förfrågningar inom 30 dagar.

      @@ -219,14 +219,14 @@ export default function PrivacyPolicyPage() {

      - For fragor om behandlingen av dina personuppgifter, kontakta oss: + För frågor om behandlingen av dina personuppgifter, kontakta oss:

        -
      • Foretag: Arcim
      • +
      • Företag: Arcim
      • E-post: privacy@gnubok.se

      - Du har aven ratt att lamna klagomal till Integritetsskyddsmyndigheten (IMY), + Du har även rätt att lämna klagomål till Integritetsskyddsmyndigheten (IMY), www.imy.se.

      diff --git a/components/extensions/AiConsentDialog.tsx b/components/extensions/AiConsentDialog.tsx index da77a9c3..8922b963 100644 --- a/components/extensions/AiConsentDialog.tsx +++ b/components/extensions/AiConsentDialog.tsx @@ -52,16 +52,16 @@ export function AiConsentDialog({ - AI-samtycke kravs + AI-samtycke krävs - Denna funktion anvander AI-tjanster fran externa leverantorer. - Granska informationen nedan innan du fortsatter. + Denna funktion använder AI-tjänster från externa leverantörer. + Granska informationen nedan innan du fortsätter.
      -

      Leverantor

      +

      Leverantör

      {disclosure.provider}

      @@ -80,11 +80,11 @@ export function AiConsentDialog({

      - Las mer i var{' '} + Läs mer i vår{' '} integritetspolicy - . Du kan nar som helst aterkalla ditt samtycke i installningarna. + . Du kan när som helst återkalla ditt samtycke i inställningarna.

      diff --git a/components/extensions/shared/ConfirmDeleteDialog.tsx b/components/extensions/shared/ConfirmDeleteDialog.tsx index e5bbd851..50a77e3d 100644 --- a/components/extensions/shared/ConfirmDeleteDialog.tsx +++ b/components/extensions/shared/ConfirmDeleteDialog.tsx @@ -18,8 +18,8 @@ interface ConfirmDeleteDialogProps { export default function ConfirmDeleteDialog({ open, onOpenChange, - title = 'Bekrafta borttagning', - description = 'Ar du saker pa att du vill ta bort detta? Atgarden kan inte angras.', + title = 'Bekräfta borttagning', + description = 'Är du säker på att du vill ta bort detta? Åtgärden kan inte ångras.', onConfirm, isDeleting = false, }: ConfirmDeleteDialogProps) { diff --git a/components/import/AccountMappingStep.tsx b/components/import/AccountMappingStep.tsx index dea50c2f..c73fe61b 100644 --- a/components/import/AccountMappingStep.tsx +++ b/components/import/AccountMappingStep.tsx @@ -204,7 +204,7 @@ export default function AccountMappingStep({ {/* Mapping table */} -
      +
    UnderbitradeUnderbiträde Syfte Plats Skyddsmekanism
    Vercel ApplikationshostingGlobalt CDN (EU-regioner tillgangliga)Globalt CDN (EU-regioner tillgängliga) EU Data Residency
    OpenAI - Embedding-generering for likhetssokning (transaktionsmallar, kunskapsbas) + Embedding-generering för likhetssökning (transaktionsmallar, kunskapsbas) USA SCCs (standardavtalsklausuler)
    RecaptProduktanalys och anvanderfeedbackProduktanalys och användarfeedback EU EU-baserad
    @@ -317,11 +317,11 @@ export default function AccountMappingStep({ {/* Actions */} -
    - - diff --git a/components/import/BankFileColumnMappingStep.tsx b/components/import/BankFileColumnMappingStep.tsx index bd651a3c..6ef0e75b 100644 --- a/components/import/BankFileColumnMappingStep.tsx +++ b/components/import/BankFileColumnMappingStep.tsx @@ -248,7 +248,7 @@ export default function BankFileColumnMappingStep({ -
    +
    @@ -286,12 +286,12 @@ export default function BankFileColumnMappingStep({ )} {/* Navigation */} -
    - - diff --git a/components/import/BankFileConfirmStep.tsx b/components/import/BankFileConfirmStep.tsx index 34274916..86545810 100644 --- a/components/import/BankFileConfirmStep.tsx +++ b/components/import/BankFileConfirmStep.tsx @@ -112,12 +112,13 @@ export default function BankFileConfirmStep({ {/* Actions */} -
    -
    @@ -201,12 +201,12 @@ export default function BankFilePreviewStep({ )} {/* Navigation */} -
    - - diff --git a/components/import/BankFileResultStep.tsx b/components/import/BankFileResultStep.tsx index 28e50818..96aeb405 100644 --- a/components/import/BankFileResultStep.tsx +++ b/components/import/BankFileResultStep.tsx @@ -30,7 +30,7 @@ export default function BankFileResultStep({ {isSuccess ? ( <> - + Import genomförd ) : ( @@ -97,21 +97,19 @@ export default function BankFileResultStep({ )} {/* Actions */} -
    - -
    - {isSuccess && ( - - )} -
    + {isSuccess && ( + + )}
    ) diff --git a/components/import/BankFileUploadStep.tsx b/components/import/BankFileUploadStep.tsx index 067e6551..8a4459b7 100644 --- a/components/import/BankFileUploadStep.tsx +++ b/components/import/BankFileUploadStep.tsx @@ -23,6 +23,7 @@ import type { BankFileFormatId } from '@/lib/import/bank-file/types' const FORMAT_NAMES: Record = { nordea: 'Nordea', + nordea_business: 'Nordea Företag', seb: 'SEB', swedbank: 'Swedbank', handelsbanken: 'Handelsbanken', @@ -110,18 +111,19 @@ export default function BankFileUploadStep({ {/* Format override */} -
    +