feat: remove AI extensions, restructure settings, and add atomic voucher commits (#157)

Remove AI-dependent extensions (ai-chat, ai-categorization, receipt-ocr,
invoice-inbox) and their infrastructure (lib/ai/*, ai-consent, LangChain/
Anthropic/OpenAI deps) to simplify core and reduce bundle size.

Restructure monolithic settings page into dedicated sub-pages (company,
bookkeeping, invoicing, tax, banking, api, account, team, templates) with
shared layout and sidebar navigation.

Add atomic commit_journal_entry RPC so voucher number increment and status
update happen in a single transaction — prevents burned numbers on constraint
failures. Add continuity check report and voucher gap explanation tracking.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-04-01 17:08:00 +02:00
committed by GitHub
co-authored by Claude Opus 4.6
parent e89f2c402d
commit d0b3f21bde
172 changed files with 3621 additions and 20538 deletions
+36 -4
View File
@@ -183,7 +183,8 @@ export function decodeBuffer(buffer: ArrayBuffer, encoding: SIEEncoding): string
}
/**
* Parse a date from SIE format (YYYYMMDD)
* Parse a date from SIE format (YYYYMMDD) into a Date object.
* Used for voucher dates where Date arithmetic is needed.
*/
function parseSIEDate(dateStr: string): Date | null {
if (!dateStr || dateStr.length !== 8) {
@@ -208,6 +209,37 @@ function parseSIEDate(dateStr: string): Date | null {
return date
}
/**
* Parse a date from SIE format (YYYYMMDD) into an ISO date string "YYYY-MM-DD".
* Used for fiscal year dates and generated dates to avoid timezone issues
* during JSON serialization (Date objects shift when crossing UTC boundaries).
*/
function parseSIEDateString(dateStr: string): string | null {
if (!dateStr || dateStr.length !== 8) {
return null
}
const year = dateStr.substring(0, 4)
const month = dateStr.substring(4, 6)
const day = dateStr.substring(6, 8)
const y = parseInt(year, 10)
const m = parseInt(month, 10)
const d = parseInt(day, 10)
if (isNaN(y) || isNaN(m) || isNaN(d)) {
return null
}
// Validate by round-tripping through Date (rejects Feb 30, etc.)
const date = new Date(y, m - 1, d)
if (date.getFullYear() !== y || date.getMonth() !== m - 1 || date.getDate() !== d) {
return null
}
return `${year}-${month}-${day}`
}
/**
* Parse a quoted string field from SIE
* Handles: "value" or value
@@ -405,7 +437,7 @@ export function parseSIEFile(content: string): ParsedSIEFile {
case 'GEN':
if (fields[1]) {
header.generatedDate = parseSIEDate(fields[1])
header.generatedDate = parseSIEDateString(fields[1])
}
break
@@ -435,8 +467,8 @@ export function parseSIEFile(content: string): ParsedSIEFile {
case 'RAR': {
// #RAR yearIndex start end
const yearIndex = parseInt(fields[1], 10)
const start = parseSIEDate(fields[2])
const end = parseSIEDate(fields[3])
const start = parseSIEDateString(fields[2])
const end = parseSIEDateString(fields[3])
if (start && end) {
header.fiscalYears.push({ yearIndex, start, end })