feat: unified document inbox, full BAS 2026, and document-transaction matching

- Expand BAS reference from ~180 to ~1,276 accounts (full BAS Kontoplan 2026)
  with K2 exclusion flags, per-class data files, and computed SRU codes
- Evolve invoice inbox into unified document inbox handling invoices, receipts,
  and government letters with AI-powered classification (Claude Haiku Vision)
- Add multi-pass document-to-transaction matching engine with greedy assignment
  for both supplier invoices (reference/amount/date/name) and receipts
  (weighted amount/merchant/date scoring)
- Add supplier invoice matching in transaction ingest pipeline
- Inject booking template suggestions into AI extraction prompts
- Surface matched documents in swipe categorization UI with one-tap booking
- Auto-activate missing BAS accounts during SIE import against full reference
- Add K2 filter toggle in Chart of Accounts manager
- Add receipt confirmation route with BFNAR representation fields
- Add database migrations for K2 support and document matching columns
- Remove obsolete extension migration scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-02-25 16:59:02 +01:00
co-authored by Claude Opus 4.6
parent 6956a757f3
commit 39e407644d
68 changed files with 19234 additions and 2412 deletions
@@ -56,6 +56,7 @@ const TYPE_LABELS: Record<string, string> = {
equity: 'EK',
revenue: 'Intakt',
expense: 'Kostnad',
untaxed_reserves: 'Ob. reserver',
}
// ---------------------------------------------------------------------------
@@ -69,6 +70,7 @@ export default function ChartOfAccountsManager() {
const [view, setView] = useState<'my-accounts' | 'bas-catalog'>('my-accounts')
const [searchQuery, setSearchQuery] = useState('')
const [expandedClasses, setExpandedClasses] = useState<Set<number>>(new Set())
const [hideK2Excluded, setHideK2Excluded] = useState<boolean | null>(null)
// Data state
const [accounts, setAccounts] = useState<BASAccount[]>([])
@@ -104,10 +106,25 @@ export default function ChartOfAccountsManager() {
async function load() {
setLoading(true)
await Promise.all([fetchAccounts(), fetchReference()])
// Set K2 filter default based on company settings (plan_type)
if (hideK2Excluded === null) {
try {
const res = await fetch('/api/settings')
if (res.ok) {
const { data } = await res.json()
// Default to hiding K2-excluded accounts if the company uses K2 (plan_type === 'k1')
setHideK2Excluded(data?.plan_type === 'k1')
} else {
setHideK2Excluded(false)
}
} catch {
setHideK2Excluded(false)
}
}
setLoading(false)
}
load()
}, [fetchAccounts, fetchReference])
}, [fetchAccounts, fetchReference, hideK2Excluded])
const refreshAll = useCallback(async () => {
await Promise.all([fetchAccounts(), fetchReference()])
@@ -221,12 +238,18 @@ export default function ChartOfAccountsManager() {
}, [filteredAccounts])
const filteredReference = useMemo(() => {
if (!searchQuery) return referenceAccounts
const q = searchQuery.toLowerCase()
return referenceAccounts.filter(
(a) => a.account_number.includes(q) || a.account_name.toLowerCase().includes(q)
)
}, [referenceAccounts, searchQuery])
let filtered = referenceAccounts
if (hideK2Excluded) {
filtered = filtered.filter((a) => !a.k2_excluded)
}
if (searchQuery) {
const q = searchQuery.toLowerCase()
filtered = filtered.filter(
(a) => a.account_number.includes(q) || a.account_name.toLowerCase().includes(q)
)
}
return filtered
}, [referenceAccounts, searchQuery, hideK2Excluded])
const groupedReference = useMemo(() => {
const grouped: Record<number, ReferenceAccount[]> = {}
@@ -284,6 +307,17 @@ export default function ChartOfAccountsManager() {
Eget konto
</Button>
)}
{view === 'bas-catalog' && (
<label className="flex items-center gap-2 text-sm">
<Switch
checked={hideK2Excluded ?? false}
onCheckedChange={setHideK2Excluded}
className="scale-75"
/>
<span className="text-muted-foreground">Dolj K2-undantagna</span>
</label>
)}
</div>
{/* Search */}