diff --git a/app/(dashboard)/page.tsx b/app/(dashboard)/page.tsx
index c17dd844..8407ac82 100644
--- a/app/(dashboard)/page.tsx
+++ b/app/(dashboard)/page.tsx
@@ -6,7 +6,8 @@ import WelcomeGate from '@/components/onboarding/WelcomeGate'
import { getActiveCompanyId } from '@/lib/company/context'
import { getDisplayTotal } from '@/lib/invoices/rounding'
import { ensureSandboxAgentProfile } from '@/lib/sandbox/ensure-agent'
-import type { Deadline, ReceiptQueueSummary, OnboardingProgress } from '@/types'
+import { getWorklistCounts, listSuggestedMatches } from '@/lib/worklist'
+import type { Deadline, OnboardingProgress } from '@/types'
export const dynamic = 'force-dynamic'
@@ -51,16 +52,6 @@ export default async function DashboardPage() {
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: settings },
@@ -69,22 +60,16 @@ export default async function DashboardPage() {
{ 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 },
{ count: sieImportCount },
{ count: staleUncategorizedCount },
- { count: uncategorizedCount },
{ count: skatteverketTokenCount },
{ data: agentProfile },
- { data: noDocRequiredEntries },
+ { count: postedEntriesCount },
+ worklist,
+ suggestedMatches,
] = await Promise.all([
supabase.from('company_settings').select('*').eq('company_id', companyId).single(),
supabase.from('customers').select('*', { count: 'exact', head: true }).eq('company_id', companyId),
@@ -96,26 +81,23 @@ export default async function DashboardPage() {
.eq('journal_entry.status', 'posted')
.eq('journal_entry.company_id', companyId)
.gte('journal_entry.entry_date', startOfYearStr),
- supabase.from('transactions').select('amount, amount_sek, is_business').eq('company_id', companyId).gte('date', startOfYearStr),
supabase.from('invoices').select('total, total_sek, vat_amount, vat_amount_sek, status').eq('company_id', companyId).in('status', ['sent', 'overdue']).is('credited_invoice_id', null),
supabase.from('bank_connections').select('id, accounts_data, status, consent_expires, bank_name').eq('company_id', companyId).eq('status', 'active'),
supabase.from('deadlines').select('*, customer:customers(id, name)').eq('company_id', companyId).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('company_id', companyId).eq('status', 'extracted'),
- supabase.from('receipts').select('*', { count: 'exact', head: true }).eq('company_id', companyId).eq('status', 'confirmed').is('matched_transaction_id', null),
- supabase.from('transactions').select('*', { count: 'exact', head: true }).eq('company_id', companyId).lt('amount', 0).is('receipt_id', null),
- supabase.from('journal_entries').select('*', { count: 'exact', head: true }).eq('company_id', companyId).eq('status', 'posted').in('source_type', needsDocSourceTypes),
- supabase.from('document_attachments').select('journal_entry_id').eq('company_id', companyId).eq('is_current_version', true).not('journal_entry_id', 'is', null),
- supabase.from('receipts').select('created_at').eq('company_id', companyId).eq('status', 'confirmed').order('created_at', { ascending: false }).limit(30),
supabase.from('sie_imports').select('*', { count: 'exact', head: true }).eq('company_id', companyId).eq('status', 'completed'),
supabase.from('transactions').select('*', { count: 'exact', head: true }).eq('company_id', companyId).is('journal_entry_id', null).eq('is_ignored', false).is('is_business', null).lt('date', new Date(now.getTime() - 14 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]),
- supabase.from('transactions').select('*', { count: 'exact', head: true }).eq('company_id', companyId).eq('is_ignored', false).is('is_business', null),
// Skatteverket tokens are user-scoped (one BankID identity per user) but
// carry the active company_id; either filter would work — we use user_id
// because that's what the token-store reads/writes against.
supabase.from('skatteverket_tokens').select('*', { count: 'exact', head: true }).eq('user_id', user.id),
supabase.from('agent_profiles').select('verified_at').eq('company_id', companyId).maybeSingle(),
- supabase.from('journal_entry_no_doc_required').select('journal_entry_id').eq('company_id', companyId),
+ // Any posted entry counts as "company has been used" for the hasData gate.
+ supabase.from('journal_entries').select('*', { count: 'exact', head: true }).eq('company_id', companyId).eq('status', 'posted'),
+ // Pending-work counts + suggested matches come from lib/worklist — the
+ // same source as the sidebar badges, so the numbers can never diverge.
+ getWorklistCounts(supabase, companyId),
+ listSuggestedMatches(supabase, companyId, 5),
])
// If onboarding is not complete, redirect to onboarding
@@ -204,16 +186,6 @@ export default async function DashboardPage() {
const ytdTotals = calculateTotals(journalLines, startOfYearStr)
const mtdTotals = calculateTotals(journalLines, startOfMonthStr)
- const uncategorizedTxns = (transactions || []).filter(
- (t) => t.is_business === null
- )
- const uncategorizedIncome = uncategorizedTxns
- .filter((t) => t.amount > 0)
- .reduce((sum, t) => sum + Number(t.amount_sek || t.amount), 0)
- const uncategorizedExpenses = uncategorizedTxns
- .filter((t) => t.amount < 0)
- .reduce((sum, t) => sum + Math.abs(Number(t.amount_sek || t.amount)), 0)
-
// Mirror the per-invoice öresavrundning rule used on the invoice list/detail
// pages: sum the displayed (rounded) SEK amount per invoice so the dashboard
// total matches what the user sees on the invoice list when the setting is on.
@@ -262,45 +234,6 @@ export default async function DashboardPage() {
),
}))
- const entriesWithDocsSet = new Set(
- (entriesWithDocs || []).map((d) => d.journal_entry_id)
- )
-
- // Exempted entries that *also* have a doc are already excluded by entriesWithDocsSet,
- // so subtracting only the exempt-without-doc set avoids double-counting.
- let exemptedWithoutDoc = 0
- for (const row of (noDocRequiredEntries || []) as { journal_entry_id: string }[]) {
- if (!entriesWithDocsSet.has(row.journal_entry_id)) exemptedWithoutDoc++
- }
-
- const missingUnderlagCount = Math.max(
- 0,
- (postedEntriesCount || 0) - entriesWithDocsSet.size - exemptedWithoutDoc
- )
-
- let streakCount = 0
- if (recentReceiptActivity && recentReceiptActivity.length > 0) {
- const todayDate = new Date()
- todayDate.setHours(0, 0, 0, 0)
-
- const activityDates = new Set(
- recentReceiptActivity.map((r) => new Date(r.created_at).toISOString().split('T')[0])
- )
-
- const checkDate = new Date(todayDate)
- while (activityDates.has(checkDate.toISOString().split('T')[0])) {
- streakCount++
- checkDate.setDate(checkDate.getDate() - 1)
- }
- }
-
- const receiptQueue: ReceiptQueueSummary = {
- unmatched_receipts_count: unmatchedReceiptsCount || 0,
- unmatched_transactions_count: unmatchedTransactionsCount || 0,
- pending_review_count: pendingReviewCount || 0,
- streak_count: streakCount,
- }
-
return (
)
diff --git a/components/dashboard/AttGoraSection.tsx b/components/dashboard/AttGoraSection.tsx
new file mode 100644
index 00000000..33e0d27b
--- /dev/null
+++ b/components/dashboard/AttGoraSection.tsx
@@ -0,0 +1,385 @@
+'use client'
+
+import { useState } from 'react'
+import Link from 'next/link'
+import { useTranslations } from 'next-intl'
+import { Card, CardContent } from '@/components/ui/card'
+import { Badge } from '@/components/ui/badge'
+import { Button } from '@/components/ui/button'
+import { EmptyState } from '@/components/ui/empty-state'
+import { useToast } from '@/components/ui/use-toast'
+import { cn, formatCurrency, formatDate } from '@/lib/utils'
+import { getErrorMessage } from '@/lib/errors/get-error-message'
+import {
+ ArrowLeftRight,
+ ArrowRight,
+ CalendarClock,
+ CheckCircle2,
+ ChevronRight,
+ Eye,
+ FileWarning,
+ Inbox,
+ Landmark,
+ Loader2,
+ Receipt,
+ ShieldCheck,
+ Stamp,
+} from 'lucide-react'
+import type { SuggestedMatch, WorklistCounts } from '@/lib/worklist/types'
+
+/**
+ * AttGoraSection — the dashboard's unified worklist ("Att göra").
+ *
+ * One flat ledger of everything actionable, grouped into three bands by
+ * session intent: Bokför (the daily loop), Granska & komplettera (close the
+ * gaps), Bevaka (time-driven). Every count comes from lib/worklist — the same
+ * source as the sidebar badges — so the numbers can never disagree.
+ *
+ * Suggested transaction↔invoice matches render inline with one-click confirm:
+ * the row posts to the existing match endpoints, fades out optimistically,
+ * and the counts refetch from /api/worklist/counts.
+ */
+
+interface ExpiringBankConnection {
+ id: string
+ bank_name: string
+ days_left: number
+}
+
+interface AttGoraSectionProps {
+ worklist: WorklistCounts
+ suggestedMatches: SuggestedMatch[]
+ expiringBankConnections?: ExpiringBankConnection[]
+ staleUncategorizedCount: number
+}
+
+interface WorklistRowProps {
+ href: string
+ icon: React.ComponentType<{ className?: string }>
+ label: string
+ detail?: string
+ count: number
+ badge?: React.ReactNode
+}
+
+function WorklistRow({ href, icon: Icon, label, detail, count, badge }: WorklistRowProps) {
+ return (
+
+
+
+
{label}
+ {detail &&
{detail}
}
+
+ {badge}
+ {count}
+
+
+ )
+}
+
+function BandHeader({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default function AttGoraSection({
+ worklist,
+ suggestedMatches,
+ expiringBankConnections = [],
+ staleUncategorizedCount,
+}: AttGoraSectionProps) {
+ const t = useTranslations('dashboard')
+ const { toast } = useToast()
+
+ const [counts, setCounts] = useState(worklist.counts)
+ const [total, setTotal] = useState(worklist.total)
+ const [matches, setMatches] = useState(suggestedMatches)
+ const [leavingIds, setLeavingIds] = useState>(new Set())
+ const [confirmingId, setConfirmingId] = useState(null)
+
+ async function refetchCounts() {
+ try {
+ const res = await fetch('/api/worklist/counts')
+ if (!res.ok) throw new Error(`worklist counts refetch failed: ${res.status}`)
+ const json = (await res.json().catch(() => ({}))) as { data?: WorklistCounts }
+ if (json.data) {
+ setCounts(json.data.counts)
+ setTotal(json.data.total)
+ }
+ } catch (err) {
+ // Stale counts self-correct on the next page load — never block the
+ // flow, but keep the failure observable (Sentry captures console.error)
+ // so a systematically broken counts endpoint doesn't hide behind
+ // silently frozen numbers.
+ console.error('[att-gora] worklist counts refetch failed', err)
+ }
+ }
+
+ async function handleConfirmMatch(match: SuggestedMatch) {
+ setConfirmingId(match.transaction_id)
+ try {
+ const url =
+ match.kind === 'invoice'
+ ? `/api/transactions/${match.transaction_id}/match-invoice`
+ : `/api/transactions/${match.transaction_id}/match-supplier-invoice`
+ const body =
+ match.kind === 'invoice'
+ ? { invoice_id: match.candidate_id }
+ : { supplier_invoice_id: match.candidate_id }
+ const res = await fetch(url, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(body),
+ })
+ const result = await res.json().catch(() => ({}))
+ if (!res.ok || result.error) {
+ toast({
+ title: t('suggested_failed_toast'),
+ description: getErrorMessage(result, { context: 'transaction', statusCode: res.status }),
+ variant: 'destructive',
+ })
+ return
+ }
+ toast({ title: t('suggested_confirmed_toast') })
+ // Fade the row out, drop it, then re-sync every count from the source
+ // of truth (the match also booked a transaction, so several numbers move).
+ setLeavingIds((prev) => new Set(prev).add(match.transaction_id))
+ setTimeout(() => {
+ setMatches((prev) => prev.filter((m) => m.transaction_id !== match.transaction_id))
+ setLeavingIds((prev) => {
+ const next = new Set(prev)
+ next.delete(match.transaction_id)
+ return next
+ })
+ }, 300)
+ void refetchCounts()
+ } catch {
+ toast({ title: t('suggested_failed_toast'), variant: 'destructive' })
+ } finally {
+ setConfirmingId(null)
+ }
+ }
+
+ const bokforRows = counts.book_transaction > 0 || counts.inbox_document > 0 || matches.length > 0
+ const granskaRows =
+ counts.supplier_invoice_approval > 0 ||
+ counts.verifikat_missing_document > 0 ||
+ counts.pending_operations > 0
+ const bevakaRows =
+ counts.overdue_invoice > 0 ||
+ counts.deadline_action > 0 ||
+ expiringBankConnections.length > 0
+ const allClear = !bokforRows && !granskaRows && !bevakaRows
+
+ // The header total must equal what the section actually shows: the worklist
+ // total plus expiring bank connections, which are dashboard-only (not a
+ // lib/worklist category). Every count that feeds this number has a row.
+ const displayTotal = total + expiringBankConnections.length
+
+ return (
+
+
+
{t('att_gora_title')}
+
+ {allClear ? t('all_done') : t('att_gora_left', { count: displayTotal })}
+
+
+
+
+
+ {allClear ? (
+
+ ) : (
+
+ {bokforRows && (
+
+
{t('band_bokfor')}
+
+ {counts.book_transaction > 0 && (
+
0 ? (
+
+ {t('row_book_transactions_stale', { count: staleUncategorizedCount })}
+
+ ) : undefined
+ }
+ />
+ )}
+ {matches.length > 0 && (
+
+
+ {t('suggested_title')}
+
+
+ {matches.map((match) => {
+ const isLeaving = leavingIds.has(match.transaction_id)
+ const isConfirming = confirmingId === match.transaction_id
+ return (
+
+
+
+ {match.transaction_description}
+
+ {' '}
+ · {formatCurrency(
+ Math.abs(match.transaction_amount),
+ match.transaction_currency,
+ )}
+
+
+
+
+ {match.kind === 'invoice'
+ ? t('suggested_kind_invoice')
+ : t('suggested_kind_supplier_invoice')}
+ {match.candidate_number ? ` ${match.candidate_number}` : ''}
+ {match.counterparty_name ? ` · ${match.counterparty_name}` : ''}
+ {' · '}
+ {formatDate(match.transaction_date)}
+
+
+
+
+
+
+
+ )
+ })}
+
+
+ )}
+ {counts.inbox_document > 0 && (
+
+ )}
+
+
+ )}
+
+ {granskaRows && (
+
+
{t('band_granska')}
+
+ {counts.supplier_invoice_approval > 0 && (
+
+ )}
+ {counts.verifikat_missing_document > 0 && (
+
+ )}
+ {counts.pending_operations > 0 && (
+
+ )}
+
+
+ )}
+
+ {bevakaRows && (
+
+
{t('band_bevaka')}
+
+ {counts.overdue_invoice > 0 && (
+
+ )}
+ {counts.deadline_action > 0 && (
+
+ )}
+ {expiringBankConnections.length > 0 && (
+
+ )}
+
+
+ )}
+
+ )}
+
+
+
+ )
+}
diff --git a/components/dashboard/DashboardContent.tsx b/components/dashboard/DashboardContent.tsx
index 2807a966..786988b3 100644
--- a/components/dashboard/DashboardContent.tsx
+++ b/components/dashboard/DashboardContent.tsx
@@ -9,19 +9,18 @@ import { cn, formatCurrency } from '@/lib/utils'
import { UpcomingDeadlinesWidget } from '@/components/deadlines/UpcomingDeadlinesWidget'
import { TaxTodoWidget } from '@/components/deadlines/TaxTodoWidget'
import NewUserChecklist from '@/components/onboarding/NewUserChecklist'
+import AttGoraSection from '@/components/dashboard/AttGoraSection'
import {
Receipt,
ArrowLeftRight,
- ChevronDown,
ChevronRight,
- Landmark,
CheckCircle2,
- FileWarning,
Clock,
ArrowRight,
MessageCircle,
} from 'lucide-react'
-import type { Deadline, ReceiptQueueSummary, OnboardingProgress } from '@/types'
+import type { Deadline, OnboardingProgress } from '@/types'
+import type { SuggestedMatch, WorklistCounts } from '@/lib/worklist/types'
import { getBranding } from '@/lib/branding/service'
const setupFreshStartKey = (companyId: string) => `erp_setup_fresh_start:${companyId}`
@@ -31,9 +30,6 @@ interface DashboardContentProps {
summary: {
ytd: { income: number; expenses: number; net: number }
mtd: { income: number; expenses: number; net: number }
- uncategorizedCount: number
- uncategorizedIncome: number
- uncategorizedExpenses: number
unpaidInvoicesCount: number
unpaidInvoicesTotal: number
unpaidVatTotal: number
@@ -41,10 +37,12 @@ interface DashboardContentProps {
bankBalance: number | null
expiringBankConnections?: { id: string; bank_name: string; days_left: number }[]
deadlines: Deadline[]
- receiptQueue: ReceiptQueueSummary | null
- missingUnderlagCount: number
staleUncategorizedCount: number
}
+ /** Unified pending-work counts from lib/worklist — same source as the sidebar badges. */
+ worklist: WorklistCounts
+ /** High-confidence transaction↔invoice matches for inline one-click confirm. */
+ suggestedMatches: SuggestedMatch[]
onboardingProgress?: OnboardingProgress
/**
* False until the company has a verified agent_profile. When false the hero
@@ -55,8 +53,7 @@ interface DashboardContentProps {
agentBuilt?: boolean
}
-export default function DashboardContent({ companyId, summary, onboardingProgress, agentBuilt = true }: DashboardContentProps) {
- const [showAllAlerts, setShowAllAlerts] = useState(false)
+export default function DashboardContent({ companyId, summary, worklist, suggestedMatches, onboardingProgress, agentBuilt = true }: DashboardContentProps) {
const t = useTranslations('dashboard')
// The setup gate exists to nudge brand-new users into a data-import step
@@ -113,143 +110,10 @@ export default function DashboardContent({ companyId, summary, onboardingProgres
}).format(amount)
}
- const alertItems: React.ReactNode[] = []
-
- if (summary.overdueInvoicesCount > 0) {
- alertItems.push(
-
-
-
-
-
-
-
{t('overdue_invoices')}
-
- {t('overdue_invoices_count', { count: summary.overdueInvoicesCount })}
-
-
-
-
-
-
- )
- }
-
- if (summary.unpaidInvoicesCount > 0 && summary.overdueInvoicesCount < summary.unpaidInvoicesCount) {
- alertItems.push(
-
-
-
-
-
-
-
{t('unpaid_invoices')}
-
- {t('unpaid_invoices_detail', {
- count: summary.unpaidInvoicesCount - summary.overdueInvoicesCount,
- amount: formatCurrency(summary.unpaidInvoicesTotal),
- })}
-
-
-
-
-
-
- )
- }
-
- if (summary.uncategorizedCount > 0) {
- alertItems.push(
-
-
-
-
-
-
-
{t('transactions')}
-
- {t('uncategorized_count', { count: summary.uncategorizedCount })}
-
-
-
-
-
-
- )
- }
-
- if (summary.missingUnderlagCount > 0) {
- alertItems.push(
-
-
-
-
-
-
-
{t('missing_underlag')}
-
- {t('missing_underlag_detail', { count: summary.missingUnderlagCount })}
-
-
-
-
-
-
- )
- }
-
- if (summary.staleUncategorizedCount > 0) {
- alertItems.push(
-
-
-
-
-
-
-
{t('stale_transactions')}
-
- {t('stale_transactions_detail', { count: summary.staleUncategorizedCount })}
-
-
-
-
-
-
- )
- }
-
- if (summary.expiringBankConnections && summary.expiringBankConnections.length > 0) {
- const conn = summary.expiringBankConnections[0]
- alertItems.push(
-
-
-
-
-
-
-
{t('bank_consent_expiring')}
-
- {conn.days_left === 1
- ? t('bank_consent_detail_one', { bank: conn.bank_name, days: conn.days_left })
- : t('bank_consent_detail_other', { bank: conn.bank_name, days: conn.days_left })}
-
-
-
-
-
-
- )
- }
-
- const MAX_VISIBLE_ALERTS = 3
- const visibleAlerts = showAllAlerts ? alertItems : alertItems.slice(0, MAX_VISIBLE_ALERTS)
- const hasMoreAlerts = alertItems.length > MAX_VISIBLE_ALERTS
-
- const passedDeadlinesCount = summary.deadlines.filter(d => !d.is_completed && new Date(d.due_date) <= new Date()).length
- const pendingReceiptsCount = summary.receiptQueue
- ? summary.receiptQueue.pending_review_count + summary.receiptQueue.unmatched_receipts_count
- : 0
- const todoCount = summary.uncategorizedCount + summary.overdueInvoicesCount + pendingReceiptsCount + passedDeadlinesCount
+ // One number, one source: the worklist total plus expiring bank connections
+ // (dashboard-only, not a lib/worklist category). Must match AttGoraSection's
+ // header so the tile and the section never disagree.
+ const todoCount = worklist.total + (summary.expiringBankConnections?.length ?? 0)
const slim = getBranding().navDensity === 'slim'
@@ -277,11 +141,12 @@ export default function DashboardContent({ companyId, summary, onboardingProgres
icon: Receipt,
}
}
- if (summary.uncategorizedCount > 0) {
+ if (worklist.counts.book_transaction > 0) {
+ const n = worklist.counts.book_transaction
return {
href: '/transactions',
title: 'Transaktioner att bokföra',
- body: `${summary.uncategorizedCount} obokförd${summary.uncategorizedCount === 1 ? '' : 'a'} transaktion${summary.uncategorizedCount === 1 ? '' : 'er'}.`,
+ body: `${n} obokförd${n === 1 ? '' : 'a'} transaktion${n === 1 ? '' : 'er'}.`,
cta: 'Bokför nu',
tone: 'primary' as const,
icon: ArrowLeftRight,
@@ -452,6 +317,15 @@ export default function DashboardContent({ companyId, summary, onboardingProgres
+ {/* Att göra — the unified worklist. One section, every actionable item,
+ same counts as the sidebar badges (lib/worklist). */}
+
+
{/* Result — revenue / expenses (always visible) */}
@@ -487,27 +361,6 @@ export default function DashboardContent({ companyId, summary, onboardingProgres
- {/* Att hantera — hidden in slim mode; the hero card already surfaces the top action */}
- {!slim && alertItems.length > 0 && (
-
- {t('alerts_title')}
-
- {visibleAlerts}
-
- {hasMoreAlerts && (
-
- )}
-
- )}
-
{/* Upcoming deadlines */}
{summary.deadlines && summary.deadlines.length > 0 && (
diff --git a/messages/en.json b/messages/en.json
index 46aedaa7..37e88c09 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -3704,7 +3704,30 @@
"anomalies_detail": "{count} to address",
"bank_consent_expiring": "Bank consent expiring",
"bank_consent_detail_one": "{bank} — {days} day left",
- "bank_consent_detail_other": "{bank} — {days} days left"
+ "bank_consent_detail_other": "{bank} — {days} days left",
+ "att_gora_title": "To do",
+ "att_gora_left": "{count} left",
+ "att_gora_empty_title": "All caught up!",
+ "att_gora_empty_body": "No transactions to record and no documents to handle.",
+ "band_bokfor": "Record",
+ "band_granska": "Review & complete",
+ "band_bevaka": "Monitor",
+ "row_book_transactions": "Transactions to record",
+ "row_book_transactions_stale": "{count} older than 14 days",
+ "row_inbox_documents": "Documents to handle",
+ "row_inbox_documents_detail": "Match to a transaction or record directly",
+ "row_supplier_approval": "Supplier invoices to approve",
+ "row_missing_underlag": "Vouchers missing documents",
+ "row_pending_ops": "To review",
+ "row_overdue_invoices": "Overdue customer invoices",
+ "suggested_title": "Suggested matches",
+ "suggested_kind_invoice": "Invoice",
+ "suggested_kind_supplier_invoice": "Supplier invoice",
+ "suggested_confirm": "Confirm",
+ "suggested_view": "View transaction",
+ "suggested_confirmed_toast": "Match recorded",
+ "suggested_failed_toast": "Match failed",
+ "row_deadlines": "VAT and tax deadlines"
},
"reports": {
"title": "Reports",
diff --git a/messages/sv.json b/messages/sv.json
index 3ea9dd34..4a9a0f00 100644
--- a/messages/sv.json
+++ b/messages/sv.json
@@ -3704,7 +3704,30 @@
"anomalies_detail": "{count} att åtgärda",
"bank_consent_expiring": "Banksamtycke löper ut",
"bank_consent_detail_one": "{bank} — {days} dag kvar",
- "bank_consent_detail_other": "{bank} — {days} dagar kvar"
+ "bank_consent_detail_other": "{bank} — {days} dagar kvar",
+ "att_gora_title": "Att göra",
+ "att_gora_left": "{count} kvar",
+ "att_gora_empty_title": "Allt klart!",
+ "att_gora_empty_body": "Inga transaktioner att bokföra och inga underlag att hantera.",
+ "band_bokfor": "Bokför",
+ "band_granska": "Granska & komplettera",
+ "band_bevaka": "Bevaka",
+ "row_book_transactions": "Bokföra transaktioner",
+ "row_book_transactions_stale": "{count} äldre än 14 dagar",
+ "row_inbox_documents": "Underlag att hantera",
+ "row_inbox_documents_detail": "Matcha mot transaktion eller bokför direkt",
+ "row_supplier_approval": "Leverantörsfakturor att attestera",
+ "row_missing_underlag": "Verifikat utan underlag",
+ "row_pending_ops": "Att granska",
+ "row_overdue_invoices": "Förfallna kundfakturor",
+ "suggested_title": "Föreslagna matchningar",
+ "suggested_kind_invoice": "Faktura",
+ "suggested_kind_supplier_invoice": "Leverantörsfaktura",
+ "suggested_confirm": "Bekräfta",
+ "suggested_view": "Visa transaktionen",
+ "suggested_confirmed_toast": "Matchning bokförd",
+ "suggested_failed_toast": "Matchningen misslyckades",
+ "row_deadlines": "Moms- och skattedeadlines"
},
"reports": {
"title": "Rapporter",