feat: import system improvements, INK2 fix, and Swedish text corrections (#10)
- SIE parser: Windows-1252 and CP437 encoding detection and decoding - Bank file parser: add Nordea Business (Företag) CSV format - Bank file parser: improve format detection for SEB, Länsförsäkringar, generic CSV - INK2 engine: calculate årets resultat (7222) from income statement for open fiscal years - Dashboard: parallel Supabase queries, simplified dashboard page - Fix Swedish characters (å, ä, ö) in BAS data descriptions, validation messages, AI consent disclosures - Import wizard UI improvements across all steps - Migration: add 'bas_range' match type to sie_account_mappings constraint - Extensive new tests for SIE parser encoding and bank file parser Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
a1ca96453c
commit
e8fb84b4fd
@@ -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({
|
||||
|
||||
@@ -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<ImportMode>(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<boolean | null>(
|
||||
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 (
|
||||
<div className="space-y-6">
|
||||
@@ -755,11 +761,26 @@ export default function ImportPage() {
|
||||
</div>
|
||||
|
||||
{mode === null && (
|
||||
<div className={`grid gap-4 ${hasBankingExtension ? 'md:grid-cols-3' : 'md:grid-cols-2'}`}>
|
||||
{hasBankingExtension && (
|
||||
<div className={`grid gap-4 ${hasBankingExtension !== false ? 'md:grid-cols-3' : 'md:grid-cols-2'}`}>
|
||||
{hasBankingExtension === null && (
|
||||
<Card className="animate-pulse">
|
||||
<CardContent className="pt-6 pb-6 flex flex-col items-center text-center space-y-3">
|
||||
<div className="h-12 w-12 rounded-full bg-muted" />
|
||||
<div className="space-y-2 w-full flex flex-col items-center">
|
||||
<div className="h-5 bg-muted rounded w-24" />
|
||||
<div className="h-4 bg-muted rounded w-48" />
|
||||
<div className="h-4 bg-muted rounded w-40" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
{hasBankingExtension === true && (
|
||||
<Card
|
||||
className="cursor-pointer hover:border-primary/50 transition-colors border-primary/20 relative"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="cursor-pointer hover:border-primary/50 active:scale-[0.98] transition-all border-primary/20 relative"
|
||||
onClick={() => setMode('psd2')}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setMode('psd2') } }}
|
||||
>
|
||||
<CardContent className="pt-6 pb-6 flex flex-col items-center text-center space-y-3">
|
||||
<div className="h-12 w-12 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
@@ -779,8 +800,11 @@ export default function ImportPage() {
|
||||
)}
|
||||
|
||||
<Card
|
||||
className="cursor-pointer hover:border-primary/50 transition-colors"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="cursor-pointer hover:border-primary/50 active:scale-[0.98] transition-all"
|
||||
onClick={() => setMode('bank')}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setMode('bank') } }}
|
||||
>
|
||||
<CardContent className="pt-6 pb-6 flex flex-col items-center text-center space-y-3">
|
||||
<div className="h-12 w-12 rounded-full bg-muted flex items-center justify-center">
|
||||
@@ -799,8 +823,11 @@ export default function ImportPage() {
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
className="cursor-pointer hover:border-primary/50 transition-colors"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="cursor-pointer hover:border-primary/50 active:scale-[0.98] transition-all"
|
||||
onClick={() => setMode('sie')}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setMode('sie') } }}
|
||||
>
|
||||
<CardContent className="pt-6 pb-6 flex flex-col items-center text-center space-y-3">
|
||||
<div className="h-12 w-12 rounded-full bg-muted flex items-center justify-center">
|
||||
|
||||
+61
-144
@@ -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 (
|
||||
<DashboardContent
|
||||
firstName={firstName}
|
||||
|
||||
@@ -35,8 +35,10 @@ import type { CompanySettings } from '@/types'
|
||||
import { CalendarFeedSettings } from '@/components/settings/CalendarFeedSettings'
|
||||
import { getSettingsPanel } from '@/lib/extensions/settings-panel-registry'
|
||||
import { SecuritySettings } from '@/components/settings/SecuritySettings'
|
||||
import { ENABLED_EXTENSION_IDS } from '@/lib/extensions/_generated/enabled-extensions'
|
||||
|
||||
const BankingPanel = getSettingsPanel('enable-banking')
|
||||
const bankingCompiledIn = ENABLED_EXTENSION_IDS.has('enable-banking')
|
||||
|
||||
export default function SettingsPage() {
|
||||
const router = useRouter()
|
||||
@@ -47,7 +49,9 @@ export default function SettingsPage() {
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [settings, setSettings] = useState<CompanySettings | null>(null)
|
||||
const [hasBankingExtension, setHasBankingExtension] = useState(false)
|
||||
const [hasBankingExtension, setHasBankingExtension] = useState<boolean | null>(
|
||||
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 && (
|
||||
<TabsContent value="banking" className="space-y-6">
|
||||
{hasBankingExtension && BankingPanel ? (
|
||||
{hasBankingExtension === null ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : hasBankingExtension && BankingPanel ? (
|
||||
<BankingPanel />
|
||||
) : (
|
||||
<Card>
|
||||
|
||||
@@ -110,40 +110,36 @@ export default function TransactionsPage() {
|
||||
.filter((t) => t.potential_invoice_id)
|
||||
.map((t) => t.potential_invoice_id)
|
||||
|
||||
let invoiceMap: Record<string, Invoice & { customer?: Customer }> = {}
|
||||
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<string, Invoice & { customer?: Customer }>)
|
||||
}
|
||||
}
|
||||
|
||||
// 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<string, Invoice & { customer?: Customer }> = {}
|
||||
if (invoiceResult.data) {
|
||||
invoiceMap = invoiceResult.data.reduce((acc, inv) => {
|
||||
acc[inv.id] = inv
|
||||
return acc
|
||||
}, {} as Record<string, Invoice & { customer?: Customer }>)
|
||||
}
|
||||
|
||||
let inboxItemMap: Record<string, InvoiceInboxItem> = {}
|
||||
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<string, InvoiceInboxItem>)
|
||||
}
|
||||
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<string, InvoiceInboxItem>)
|
||||
}
|
||||
|
||||
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) => {
|
||||
|
||||
+52
-52
@@ -25,14 +25,14 @@ export default function DPAPage() {
|
||||
</CardHeader>
|
||||
<CardContent className="prose prose-sm max-w-none">
|
||||
<p>
|
||||
Detta personuppgiftsbitradesavtal ("DPA") ingar mellan:
|
||||
Detta personuppgiftsbitradesavtal ("DPA") ingår mellan:
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>Personuppgiftsansvarig ("den Ansvarige"):</strong> Du som anvandare av Gnubok,
|
||||
i egenskap av ansvarig for de personuppgifter du registrerar i tjansten
|
||||
(kunder, leverantorer, anstallda m.fl.).</li>
|
||||
<li><strong>Personuppgiftsbitrade ("Bitradet"):</strong> Arcim, som tillhandahaller
|
||||
Gnubok-tjansten och behandlar personuppgifter pa dina vagar.</li>
|
||||
<li><strong>Personuppgiftsansvarig ("den Ansvarige"):</strong> 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.).</li>
|
||||
<li><strong>Personuppgiftsbiträde ("Biträdet"):</strong> Arcim, som tillhandahåller
|
||||
Gnubok-tjänsten och behandlar personuppgifter på dina vägnar.</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -42,10 +42,10 @@ export default function DPAPage() {
|
||||
<CardTitle>2. Behandlingens syfte och omfattning</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="prose prose-sm max-w-none">
|
||||
<p>Bitradet behandlar personuppgifter for foljande andamal:</p>
|
||||
<p>Biträdet behandlar personuppgifter för följande ändamål:</p>
|
||||
<ul>
|
||||
<li>Tillhandahallande av bokforings- och redovisningstjanster</li>
|
||||
<li>Lagring och arkivering av bokforingsmaterial</li>
|
||||
<li>Tillhandahållande av bokförings- och redovisningstjänster</li>
|
||||
<li>Lagring och arkivering av bokföringsmaterial</li>
|
||||
<li>Fakturering och betalningshantering</li>
|
||||
<li>Bankkontosynkronisering (PSD2)</li>
|
||||
<li>AI-assisterad kategorisering och kvittohantering (efter separat samtycke)</li>
|
||||
@@ -53,92 +53,92 @@ export default function DPAPage() {
|
||||
<p>Kategorier av registrerade vars uppgifter behandlas:</p>
|
||||
<ul>
|
||||
<li>Den Ansvariges kunder (namn, kontaktuppgifter, organisationsnummer)</li>
|
||||
<li>Den Ansvariges leverantorer (namn, kontaktuppgifter, bankuppgifter)</li>
|
||||
<li>Den Ansvarige sjalv (kontouppgifter, foretagsinformation)</li>
|
||||
<li>Den Ansvariges leverantörer (namn, kontaktuppgifter, bankuppgifter)</li>
|
||||
<li>Den Ansvarige själv (kontouppgifter, företagsinformation)</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>3. Tekniska och organisatoriska atgarder</CardTitle>
|
||||
<CardTitle>3. Tekniska och organisatoriska åtgärder</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="prose prose-sm max-w-none">
|
||||
<p>Bitradet vidtar foljande atgarder for att skydda personuppgifterna:</p>
|
||||
<p>Biträdet vidtar följande åtgärder för att skydda personuppgifterna:</p>
|
||||
<ul>
|
||||
<li><strong>Kryptering:</strong> All data krypteras i transit (TLS 1.3) och i vila (AES-256)</li>
|
||||
<li><strong>Atkomstkontroll:</strong> Row Level Security (RLS) sakerstaller att varje anvandare
|
||||
enbart kan komma at sina egna uppgifter</li>
|
||||
<li><strong>Autentisering:</strong> Sakra inloggningsmetoder (magic link, inga losenord lagrade)</li>
|
||||
<li><strong>Integritetskontroll:</strong> SHA-256 checksummor for alla dokument, med
|
||||
<li><strong>Åtkomstkontroll:</strong> Row Level Security (RLS) säkerställer att varje användare
|
||||
enbart kan komma åt sina egna uppgifter</li>
|
||||
<li><strong>Autentisering:</strong> Säkra inloggningsmetoder (magic link, inga lösenord lagrade)</li>
|
||||
<li><strong>Integritetskontroll:</strong> SHA-256 checksummor för alla dokument, med
|
||||
regelbunden verifiering</li>
|
||||
<li><strong>Revisionslogg:</strong> Alla andringshandelser loggas automatiskt av databasen
|
||||
<li><strong>Revisionslogg:</strong> Alla ändringshandelser loggas automatiskt av databasen
|
||||
(ej redigerbara)</li>
|
||||
<li><strong>Oforanderlig bokforing:</strong> Bokforda verifikationer kan inte andras eller
|
||||
raderas (databasutlosare)</li>
|
||||
<li><strong>Sakerhetskopior:</strong> Kontinuerliga databaskopior med point-in-time-recovery</li>
|
||||
<li><strong>EU-lagring:</strong> All primar datalagring sker i EU (eu-central-1)</li>
|
||||
<li><strong>Oföränderlig bokföring:</strong> Bokförda verifikationer kan inte ändras eller
|
||||
raderas (databasutlösare)</li>
|
||||
<li><strong>Säkerhetskopior:</strong> Kontinuerliga databaskopior med point-in-time-recovery</li>
|
||||
<li><strong>EU-lagring:</strong> All primär datalagring sker i EU (eu-central-1)</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>4. Underbitraden</CardTitle>
|
||||
<CardTitle>4. Underbiträden</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="prose prose-sm max-w-none">
|
||||
<p>
|
||||
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{' '}
|
||||
<Link href="/privacy" className="text-primary underline underline-offset-4">
|
||||
integritetspolicy
|
||||
</Link>.
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>5. Dataintrangsnotifiering</CardTitle>
|
||||
<CardTitle>5. Dataintrångsnotifiering</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="prose prose-sm max-w-none">
|
||||
<p>
|
||||
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:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Typ av personuppgiftsincident</li>
|
||||
<li>Kategorier och ungefirligt antal registrerade som berorts</li>
|
||||
<li>Kategorier och ungefärligt antal registrerade som berörts</li>
|
||||
<li>Sannolika konsekvenser av incidenten</li>
|
||||
<li>Atgarder som vidtagits eller foreslas for att hantera incidenten</li>
|
||||
<li>Åtgärder som vidtagits eller föreslås för att hantera incidenten</li>
|
||||
</ul>
|
||||
<p>
|
||||
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).
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>6. Revisionsratt</CardTitle>
|
||||
<CardTitle>6. Revisionsrätt</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="prose prose-sm max-w-none">
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</CardContent>
|
||||
@@ -150,22 +150,22 @@ export default function DPAPage() {
|
||||
</CardHeader>
|
||||
<CardContent className="prose prose-sm max-w-none">
|
||||
<p>
|
||||
Vid uppsagning av tjansten ska Bitradet, enligt den Ansvariges val:
|
||||
Vid uppsägning av tjänsten ska Biträdet, enligt den Ansvariges val:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Aterlamna:</strong> Exportera alla personuppgifter i maskinlasbart format
|
||||
(SIE4, JSON, CSV) via tjansens exportfunktioner.
|
||||
<strong>Återlämna:</strong> Exportera alla personuppgifter i maskinläsbart format
|
||||
(SIE4, JSON, CSV) via tjänstens exportfunktioner.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Radera:</strong> Radera alla personuppgifter inom 30 dagar fran
|
||||
anvandarens begaran, med undantag for uppgifter som maste bevaras enligt lag.
|
||||
<strong>Radera:</strong> Radera alla personuppgifter inom 30 dagar från
|
||||
användarens begäran, med undantag för uppgifter som måste bevaras enligt lag.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
<strong>Undantag:</strong> 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.
|
||||
<strong>Undantag:</strong> 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.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -173,9 +173,9 @@ export default function DPAPage() {
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<p className="text-sm text-muted-foreground text-center">
|
||||
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.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -24,9 +24,9 @@ export default function PrivacyPolicyPage() {
|
||||
</CardHeader>
|
||||
<CardContent className="prose prose-sm max-w-none">
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -36,39 +36,39 @@ export default function PrivacyPolicyPage() {
|
||||
<CardTitle>2. Vilka uppgifter vi behandlar</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="prose prose-sm max-w-none">
|
||||
<p>Vi behandlar foljande kategorier av personuppgifter:</p>
|
||||
<p>Vi behandlar följande kategorier av personuppgifter:</p>
|
||||
<ul>
|
||||
<li><strong>Kontouppgifter:</strong> E-postadress (for inloggning via magic link)</li>
|
||||
<li><strong>Foretagsuppgifter:</strong> Foretagsnamn, organisationsnummer, adress, kontaktuppgifter</li>
|
||||
<li><strong>Bokforingsdata:</strong> Verifikationer, fakturor, kvitton, transaktioner, kontoplaner</li>
|
||||
<li><strong>Kontouppgifter:</strong> E-postadress (för inloggning via magic link)</li>
|
||||
<li><strong>Företagsuppgifter:</strong> Företagsnamn, organisationsnummer, adress, kontaktuppgifter</li>
|
||||
<li><strong>Bokföringsdata:</strong> Verifikationer, fakturor, kvitton, transaktioner, kontoplaner</li>
|
||||
<li><strong>Bankdata:</strong> Kontosaldon och transaktioner (via PSD2-koppling)</li>
|
||||
<li><strong>Dokument:</strong> Uppladdade kvitton, fakturor och andra bokforingsunderlag</li>
|
||||
<li><strong>Tekniska uppgifter:</strong> IP-adress, enhetstyp, anvandningsstatistik</li>
|
||||
<li><strong>Dokument:</strong> Uppladdade kvitton, fakturor och andra bokföringsunderlag</li>
|
||||
<li><strong>Tekniska uppgifter:</strong> IP-adress, enhetstyp, användningsstatistik</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>3. Rattslig grund (GDPR Art. 6)</CardTitle>
|
||||
<CardTitle>3. Rättslig grund (GDPR Art. 6)</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="prose prose-sm max-w-none">
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Avtal (Art. 6.1b):</strong> Behandling som ar nodvandig for att fullgora vara
|
||||
tjanster enligt anvandaravtalet.
|
||||
<strong>Avtal (Art. 6.1b):</strong> Behandling som är nödvändig för att fullgöra våra
|
||||
tjänster enligt användaravtalet.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Rattslig forpliktelse (Art. 6.1c):</strong> Bokforingslagens (BFL) krav pa
|
||||
7 ars arkivering av raknenskapsmaterial.
|
||||
<strong>Rättslig förpliktelse (Art. 6.1c):</strong> Bokföringslagens (BFL) krav på
|
||||
7 års arkivering av räkenskapsmaterial.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Berattigat intresse (Art. 6.1f):</strong> Produktforbattringar, sakerhet och
|
||||
bedrageriforbud.
|
||||
<strong>Berättigat intresse (Art. 6.1f):</strong> Produktförbättringar, säkerhet och
|
||||
bedrägeriforbud.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Samtycke (Art. 6.1a):</strong> For AI-baserade funktioner som skickar data
|
||||
till tredjepartstjanster (se separat samtycke vid aktivering).
|
||||
<strong>Samtycke (Art. 6.1a):</strong> För AI-baserade funktioner som skickar data
|
||||
till tredjepartstjänster (se separat samtycke vid aktivering).
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
@@ -76,12 +76,12 @@ export default function PrivacyPolicyPage() {
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>4. Underbitraden</CardTitle>
|
||||
<CardTitle>4. Underbiträden</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="prose prose-sm max-w-none">
|
||||
<p>
|
||||
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).
|
||||
</p>
|
||||
|
||||
@@ -89,7 +89,7 @@ export default function PrivacyPolicyPage() {
|
||||
<table className="min-w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b">
|
||||
<th className="text-left py-2 pr-4 font-semibold">Underbitrade</th>
|
||||
<th className="text-left py-2 pr-4 font-semibold">Underbiträde</th>
|
||||
<th className="text-left py-2 pr-4 font-semibold">Syfte</th>
|
||||
<th className="text-left py-2 pr-4 font-semibold">Plats</th>
|
||||
<th className="text-left py-2 font-semibold">Skyddsmekanism</th>
|
||||
@@ -105,7 +105,7 @@ export default function PrivacyPolicyPage() {
|
||||
<tr className="border-b">
|
||||
<td className="py-2 pr-4 font-medium">Vercel</td>
|
||||
<td className="py-2 pr-4">Applikationshosting</td>
|
||||
<td className="py-2 pr-4">Globalt CDN (EU-regioner tillgangliga)</td>
|
||||
<td className="py-2 pr-4">Globalt CDN (EU-regioner tillgängliga)</td>
|
||||
<td className="py-2">EU Data Residency</td>
|
||||
</tr>
|
||||
<tr className="border-b">
|
||||
@@ -120,7 +120,7 @@ export default function PrivacyPolicyPage() {
|
||||
<tr className="border-b">
|
||||
<td className="py-2 pr-4 font-medium">OpenAI</td>
|
||||
<td className="py-2 pr-4">
|
||||
Embedding-generering for likhetssokning (transaktionsmallar, kunskapsbas)
|
||||
Embedding-generering för likhetssökning (transaktionsmallar, kunskapsbas)
|
||||
</td>
|
||||
<td className="py-2 pr-4">USA</td>
|
||||
<td className="py-2">SCCs (standardavtalsklausuler)</td>
|
||||
@@ -139,7 +139,7 @@ export default function PrivacyPolicyPage() {
|
||||
</tr>
|
||||
<tr className="border-b">
|
||||
<td className="py-2 pr-4 font-medium">Recapt</td>
|
||||
<td className="py-2 pr-4">Produktanalys och anvanderfeedback</td>
|
||||
<td className="py-2 pr-4">Produktanalys och användarfeedback</td>
|
||||
<td className="py-2 pr-4">EU</td>
|
||||
<td className="py-2">EU-baserad</td>
|
||||
</tr>
|
||||
@@ -148,21 +148,21 @@ export default function PrivacyPolicyPage() {
|
||||
</div>
|
||||
|
||||
<p className="mt-4 text-sm text-muted-foreground">
|
||||
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.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>5. Tredjelandsoverforing</CardTitle>
|
||||
<CardTitle>5. Tredjelandsöverföring</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="prose prose-sm max-w-none">
|
||||
<p>
|
||||
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).
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -174,13 +174,13 @@ export default function PrivacyPolicyPage() {
|
||||
<CardContent className="prose prose-sm max-w-none">
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Bokforingsmaterial:</strong> 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.
|
||||
<strong>Bokföringsmaterial:</strong> 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.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Kontouppgifter:</strong> Sa lange kontot ar aktivt, plus 30 dagar efter
|
||||
begaran om radering (for att hantera pagaende bokforingsplikter).
|
||||
<strong>Kontouppgifter:</strong> Så länge kontot är aktivt, plus 30 dagar efter
|
||||
begäran om radering (för att hantera pågående bokföringsplikter).
|
||||
</li>
|
||||
<li>
|
||||
<strong>Tekniska loggar:</strong> Maximalt 90 dagar.
|
||||
@@ -191,24 +191,24 @@ export default function PrivacyPolicyPage() {
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>7. Dina rattigheter</CardTitle>
|
||||
<CardTitle>7. Dina rättigheter</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="prose prose-sm max-w-none">
|
||||
<p>Du har foljande rattigheter enligt GDPR:</p>
|
||||
<p>Du har följande rättigheter enligt GDPR:</p>
|
||||
<ul>
|
||||
<li><strong>Tillgang (Art. 15):</strong> Du kan begara en kopia av alla dina personuppgifter.</li>
|
||||
<li><strong>Rattelse (Art. 16):</strong> Du kan begara rattelse av felaktiga uppgifter.</li>
|
||||
<li><strong>Radering (Art. 17):</strong> Du kan begara radering, med undantag for uppgifter som
|
||||
omfattas av lagstadgade arkiveringskrav (BFL 7 ar).</li>
|
||||
<li><strong>Begransning (Art. 18):</strong> Du kan begara begransning av behandlingen.</li>
|
||||
<li><strong>Tillgång (Art. 15):</strong> Du kan begära en kopia av alla dina personuppgifter.</li>
|
||||
<li><strong>Rättelse (Art. 16):</strong> Du kan begära rättelse av felaktiga uppgifter.</li>
|
||||
<li><strong>Radering (Art. 17):</strong> Du kan begära radering, med undantag för uppgifter som
|
||||
omfattas av lagstadgade arkiveringskrav (BFL 7 år).</li>
|
||||
<li><strong>Begränsning (Art. 18):</strong> Du kan begära begränsning av behandlingen.</li>
|
||||
<li><strong>Dataportabilitet (Art. 20):</strong> Du kan exportera dina uppgifter i
|
||||
maskinlasbart format (SIE4, JSON, CSV) via exportfunktionerna i appen.</li>
|
||||
<li><strong>Invandning (Art. 21):</strong> Du kan invanda mot behandling baserad pa
|
||||
berattigat intresse.</li>
|
||||
maskinläsbart format (SIE4, JSON, CSV) via exportfunktionerna i appen.</li>
|
||||
<li><strong>Invändning (Art. 21):</strong> Du kan invända mot behandling baserad på
|
||||
berättigat intresse.</li>
|
||||
</ul>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -219,14 +219,14 @@ export default function PrivacyPolicyPage() {
|
||||
</CardHeader>
|
||||
<CardContent className="prose prose-sm max-w-none">
|
||||
<p>
|
||||
For fragor om behandlingen av dina personuppgifter, kontakta oss:
|
||||
För frågor om behandlingen av dina personuppgifter, kontakta oss:
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>Foretag:</strong> Arcim</li>
|
||||
<li><strong>Företag:</strong> Arcim</li>
|
||||
<li><strong>E-post:</strong> privacy@gnubok.se</li>
|
||||
</ul>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</CardContent>
|
||||
|
||||
Reference in New Issue
Block a user