Files
accounted/components/import/BankFileConfirmStep.tsx
T
Jakob Wennberg 91e2c1705a feat: per-line VAT, invoice document types, ledger-based VAT declaration, bank reconciliation, and pagination
Per-line VAT rates:
- Add generatePerRateLines() to group invoice items by vat_rate with separate
  revenue + VAT lines per rate group (invoice-entries.ts)
- Add getAvailableVatRates() and getVatTreatmentForRate() (vat-rules.ts)
- PDF template shows per-line VAT column and per-rate totals for mixed-rate invoices
- Invoice create/review UI supports per-line rate selection
- Types: add vat_rate/vat_amount to InvoiceItem, vat_rate to CreateInvoiceItemInput

Invoice document types (proforma, delivery note):
- Add InvoiceDocumentType, document_type and converted_from_id to Invoice type
- PDF hides prices for delivery notes, adds proforma notice
- Email templates support all document types
- mark-paid skips journal entries for non-invoice document types
- Migration 031: invoice_document_type

Accounting method support:
- Add AccountingMethod type (accrual/cash)
- Migration 032: add_accounting_method column to company_settings

VAT declaration rewrite:
- Rewrite to read directly from general ledger (26xx/3xxx account lines)
  instead of aggregating invoices/transactions/receipts
- ACCOUNT_RUTA mapping drives momsdeklaration boxes from GL balances

Bank reconciliation:
- Transaction ingest now pre-fetches unlinked GL lines and attempts
  auto-reconciliation during import
- Add transaction.reconciled event type
- Add ReconciliationMethod type and reconciliation_method on Transaction
- Migration 030: bank_reconciliation
- New reconciliation engine, API routes, and BankReconciliationView component

Pagination (fetchAllRows):
- New lib/supabase/fetch-all.ts overcomes PostgREST 1000-row limit
- Adopted in all report generators, SIE/SRU export, account list APIs

Fiscal period validation:
- New validate-period-duration.ts enforces max 18 months per BFL 3 kap.
- Applied in period-service.ts and fiscal-periods API

Account mapper simplification:
- Remove Levenshtein/fuzzy matching, use exact account number match only

Swedbank parser improvements:
- Support abbreviated headers (Clnr, Bokfdag, Radnr)
- Use Referens column as counterparty

Chart of accounts management:
- Add DELETE endpoint with system account and usage protection
- PUT uses partial updates
- New AccountCombobox, AddAccountDialog, EditAccountDialog, ChartOfAccountsManager

Tax deadline corrections:
- Rewrite inkomstdeklaration_ab using Skatteverket lookup table
- Rewrite arsredovisning deadline to 7 months after FY end per ÅRL 8:3

Onboarding first fiscal year:
- Add first fiscal year toggle with date pickers and 18-month validation

UI terminology:
- Change "okategoriserad/kategorisera" to "obokförd/bokföra" throughout

Report column fix:
- Fix start_date/end_date to period_start/period_end in report queries

Supplier invoice input:
- CreateSupplierInvoiceItemInput uses amount field (legacy quantity/unit_price kept)

Misc:
- SIE import uses upsert for idempotent account creation
- account-descriptions.ts falls back to BAS reference data
- Add invoice_default_notes to CompanySettings
- Update CLAUDE.md to reflect current project state

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 14:57:15 +01:00

179 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { useState } from 'react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Checkbox } from '@/components/ui/checkbox'
import { Label } from '@/components/ui/label'
import {
ArrowLeft,
Loader2,
Play,
FileText,
AlertTriangle,
Link2,
Calendar,
} from 'lucide-react'
import { formatCurrency } from '@/lib/utils'
import type { BankFileParseResult } from '@/lib/import/bank-file/types'
interface BankFileConfirmStepProps {
parseResult: BankFileParseResult
onExecute: (options: { skip_duplicates: boolean; auto_categorize: boolean }) => void
onBack: () => void
isLoading: boolean
}
export default function BankFileConfirmStep({
parseResult,
onExecute,
onBack,
isLoading,
}: BankFileConfirmStepProps) {
const [skipDuplicates, setSkipDuplicates] = useState(true)
const [autoCategorize, setAutoCategorize] = useState(true)
const { transactions, stats, date_from, date_to, format_name } = parseResult
const refsCount = transactions.filter((t) => t.reference).length
return (
<div className="space-y-6">
{/* Summary */}
<Card>
<CardHeader>
<CardTitle>Bekräfta import</CardTitle>
<CardDescription>
Granska sammanfattningen och importera transaktionerna.
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* Stats grid */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="p-4 bg-muted/50 rounded-lg">
<div className="flex items-center gap-2 text-muted-foreground mb-1">
<FileText className="h-4 w-4" />
<span className="text-xs">Transaktioner</span>
</div>
<p className="text-xl font-bold">{stats.parsed_rows}</p>
</div>
<div className="p-4 bg-muted/50 rounded-lg">
<div className="flex items-center gap-2 text-muted-foreground mb-1">
<Calendar className="h-4 w-4" />
<span className="text-xs">Period</span>
</div>
<p className="text-sm font-medium">
{date_from} {date_to}
</p>
</div>
<div className="p-4 bg-muted/50 rounded-lg">
<div className="flex items-center gap-2 text-green-600 mb-1">
<span className="text-xs">Inkomster</span>
</div>
<p className="text-xl font-bold text-green-600">
{formatCurrency(stats.total_income)}
</p>
</div>
<div className="p-4 bg-muted/50 rounded-lg">
<div className="flex items-center gap-2 text-red-600 mb-1">
<span className="text-xs">Utgifter</span>
</div>
<p className="text-xl font-bold text-red-600">
{formatCurrency(stats.total_expenses)}
</p>
</div>
</div>
{/* Additional info */}
<div className="flex flex-wrap gap-2">
<Badge variant="secondary">Format: {format_name}</Badge>
{refsCount > 0 && (
<Badge variant="outline" className="text-blue-600 border-blue-300">
<Link2 className="mr-1 h-3 w-3" />
{refsCount} med OCR/referens
</Badge>
)}
</div>
{/* Options */}
<div className="border rounded-lg p-4 space-y-4">
<h3 className="text-sm font-medium">Importinställningar</h3>
<div className="flex items-center space-x-3">
<Checkbox
id="skip-duplicates"
checked={skipDuplicates}
onCheckedChange={(checked) => setSkipDuplicates(checked === true)}
/>
<div>
<Label htmlFor="skip-duplicates" className="text-sm font-medium cursor-pointer">
Hoppa över dubletter
</Label>
<p className="text-xs text-muted-foreground">
Transaktioner som redan finns i systemet importeras inte igen
</p>
</div>
</div>
<div className="flex items-center space-x-3">
<Checkbox
id="auto-categorize"
checked={autoCategorize}
onCheckedChange={(checked) => setAutoCategorize(checked === true)}
/>
<div>
<Label htmlFor="auto-categorize" className="text-sm font-medium cursor-pointer">
Auto-bokför kända transaktioner
</Label>
<p className="text-xs text-muted-foreground">
Skapar automatiskt bokföringsposter för transaktioner med hög konfidens
</p>
</div>
</div>
</div>
{/* Warning note */}
<div className="flex gap-3 p-3 bg-yellow-50 dark:bg-yellow-950/20 border border-yellow-200 dark:border-yellow-800 rounded-lg">
<AlertTriangle className="h-4 w-4 text-yellow-600 flex-shrink-0 mt-0.5" />
<p className="text-sm text-muted-foreground">
Importerade transaktioner som inte automatiskt bokförs visas som
&quot;obokförda&quot; transaktionssidan. Du kan bokföra dem manuellt
efteråt.
</p>
</div>
</CardContent>
</Card>
{/* Actions */}
<div className="flex justify-between">
<Button variant="outline" onClick={onBack} disabled={isLoading}>
<ArrowLeft className="mr-2 h-4 w-4" />
Tillbaka
</Button>
<Button
onClick={() => onExecute({
skip_duplicates: skipDuplicates,
auto_categorize: autoCategorize,
})}
disabled={isLoading}
>
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Importerar...
</>
) : (
<>
<Play className="mr-2 h-4 w-4" />
Importera {stats.parsed_rows} transaktioner
</>
)}
</Button>
</div>
</div>
)
}