e1f13f870a
* fix(transactions): paginate the ingest dedup maps past the 1000-row cap
buildExistingTransactionMaps issued un-paginated selects for the booked and
unbooked dedup maps, so PostgREST silently truncated each at 1000 rows: a
re-import over a wide date range in an active company deduped against a
partial map and inserted everything past the cap as duplicates. Both queries
now go through fetchAllRows with a stable .order('id') for range paging.
Also exports the function and its types for the upcoming read-only duplicate
preview, which must share the exact stored-row universe execute-side ingest
dedups against.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(import): add read-only duplicate preview endpoint for bank files
New POST /api/import/bank-file/check-duplicates (withRouteContext + Zod,
transactions capped at 20000) computes external_ids with the exact
generateExternalId(tx, format, index) derivation execute uses and runs
previewDuplicates: Layer-1 id collisions plus the Layer-2 text bridge with
counting semantics and the currency guard, against the same stored-row maps
ingest builds (buildExistingTransactionMaps). The result is advisory; execute
stays authoritative and mirrors/settlement-account guards are documented
preview/execute differences.
A dedicated endpoint because the generic_csv path re-parses client-side and
never re-hits /parse. Also removes the dead existing_transaction_count field
from the parse response (a raw date-range count consumed by nothing).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(import): surface duplicate rows in the bank-file import wizard
Overlapping bank imports used to dedup silently: the wizard promised
'Importera N transaktioner', ingest skipped the twins, and the user saw fewer
rows than parsed with zero explanation. The wizard now calls check-duplicates
after a successful parse AND inside handleColumnMappingConfirm (the
generic_csv path never re-hits parse), and:
- BankFilePreviewStep: warning card in the AlertTriangle pattern ('{count}
rader finns redan', skipped automatically) plus a 'Finns redan' badge on
flagged rows in the 50-row table
- BankFileConfirmStep: repeats the summary card (generic path skips preview)
and the CTA counts 'Importera {parsed - duplicates} transaktioner'
- BankFileResultStep: renders result.duplicates when > 0, closing the loop
ingest.ts documents as unrendered
Execute semantics unchanged: all rows are sent, ingest skips; the preview is
advisory and never promises an exact final number. New strings in both
messages/sv.json and messages/en.json next to the import_psd2 anchors.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
139 lines
5.2 KiB
TypeScript
139 lines
5.2 KiB
TypeScript
'use client'
|
|
|
|
import Link from 'next/link'
|
|
import { useTranslations } from 'next-intl'
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
import { Button } from '@/components/ui/button'
|
|
import {
|
|
CheckCircle,
|
|
XCircle,
|
|
ArrowRight,
|
|
RotateCcw,
|
|
} from 'lucide-react'
|
|
import type { IngestResult } from '@/lib/transactions/ingest'
|
|
|
|
interface BankFileResultStepProps {
|
|
result: IngestResult
|
|
onNewImport: () => void
|
|
}
|
|
|
|
export default function BankFileResultStep({
|
|
result,
|
|
onNewImport,
|
|
}: BankFileResultStepProps) {
|
|
const t = useTranslations('transactions')
|
|
const isSuccess = result.imported > 0 || result.duplicates > 0
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Status header */}
|
|
<Card className={isSuccess ? 'border-border' : 'border-destructive/50'}>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
{isSuccess ? (
|
|
<>
|
|
<CheckCircle className="h-6 w-6 text-success" />
|
|
Import genomförd
|
|
</>
|
|
) : (
|
|
<>
|
|
<XCircle className="h-6 w-6 text-destructive" />
|
|
Import misslyckades
|
|
</>
|
|
)}
|
|
</CardTitle>
|
|
<CardDescription>
|
|
{isSuccess
|
|
? `${result.imported} transaktioner importerades framgångsrikt.`
|
|
: `${result.errors} fel uppstod under importen.`}
|
|
</CardDescription>
|
|
{/* Close the silent-dedup loop: without this line, skipped rows just
|
|
look like they vanished (fewer imported than parsed, no
|
|
explanation). */}
|
|
{result.duplicates > 0 && (
|
|
<CardDescription>
|
|
{t('import_duplicate_result', { count: result.duplicates })}
|
|
</CardDescription>
|
|
)}
|
|
</CardHeader>
|
|
{!isSuccess && result.first_error && (
|
|
<CardContent>
|
|
<div className="rounded-md border border-destructive/30 bg-destructive/5 p-3 text-sm">
|
|
<p className="font-medium text-destructive">Databasfel</p>
|
|
<p className="mt-1 font-mono text-xs text-muted-foreground break-all">
|
|
{result.first_error.message}
|
|
{result.first_error.details ? `: ${result.first_error.details}` : ''}
|
|
{result.first_error.code ? ` (${result.first_error.code})` : ''}
|
|
</p>
|
|
</div>
|
|
</CardContent>
|
|
)}
|
|
</Card>
|
|
|
|
{/* Next steps */}
|
|
{isSuccess && (
|
|
<Card className="bg-muted/50">
|
|
<CardHeader>
|
|
<CardTitle className="text-base">Nästa steg</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="flex items-start gap-3">
|
|
<div className="w-6 h-6 rounded-full bg-primary text-primary-foreground flex items-center justify-center text-sm font-medium flex-shrink-0">
|
|
1
|
|
</div>
|
|
<div>
|
|
<p className="font-medium">Granska obokförda transaktioner</p>
|
|
<p className="text-sm text-muted-foreground">
|
|
{result.imported - result.auto_categorized > 0
|
|
? `${result.imported - result.auto_categorized} transaktioner behöver bokföras manuellt.`
|
|
: 'Alla transaktioner bokfördes automatiskt.'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-start gap-3">
|
|
<div className="w-6 h-6 rounded-full bg-primary text-primary-foreground flex items-center justify-center text-sm font-medium flex-shrink-0">
|
|
2
|
|
</div>
|
|
<div>
|
|
<p className="font-medium">Bekräfta fakturamatchningar</p>
|
|
<p className="text-sm text-muted-foreground">
|
|
{result.auto_matched_invoices > 0
|
|
? `${result.auto_matched_invoices} transaktioner matchades mot fakturor. Bekräfta dessa på transaktionssidan.`
|
|
: 'Inga automatiska fakturamatchningar hittades.'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-start gap-3">
|
|
<div className="w-6 h-6 rounded-full bg-primary text-primary-foreground flex items-center justify-center text-sm font-medium flex-shrink-0">
|
|
3
|
|
</div>
|
|
<div>
|
|
<p className="font-medium">Importera fler kontoutdrag</p>
|
|
<p className="text-sm text-muted-foreground">
|
|
Importera löpande kontoutdrag för att hålla bokföringen uppdaterad.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Actions */}
|
|
<div className="flex flex-col-reverse gap-3 sm:flex-row sm:justify-between">
|
|
<Button variant="outline" className="min-h-11" onClick={onNewImport}>
|
|
<RotateCcw className="mr-2 h-4 w-4" />
|
|
Ny import
|
|
</Button>
|
|
{isSuccess && (
|
|
<Button className="min-h-11" asChild>
|
|
<Link href="/transactions">
|
|
Visa transaktioner
|
|
<ArrowRight className="ml-2 h-4 w-4" />
|
|
</Link>
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|