Files
accounted/components/import/ImportResultStep.tsx
T
Jakob Wennberg 5cb8c3c1e4 feat: support contact links and SIE import UX polish (#138)
* feat: event log, pending operations, and MCP staging

- Event log system: persist bus events to event_log table for external
  automation platforms. Batch insert for transaction.synced. Daily
  cleanup cron at 02:00 UTC.
- Pending operations: MCP write tools (categorize, create customer,
  create invoice) now stage to pending_operations instead of executing
  directly. Users review and commit/reject from /pending in the web UI.
- Granskning page: card-based review UI with expandable previews,
  commit/reject dialogs. Only shown in nav when pending ops exist.
- Commit route re-executes using core lib functions (no extension
  imports). Guards against stale state (double-commit, deleted entities).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: stage new MCP write tools after main merge

Add staging for 4 new write tools from #133:
- mark_invoice_paid, send_invoice, mark_invoice_sent,
  match_transaction_invoice
- Expand pending_operations CHECK constraint
- Add commit executors with full execution logic
- Add UI labels and generic preview component
- Remove confirm parameter from categorize (single-call staging)
- Fix UUID in pending op title (fetch transaction description)
- Hide Granskning nav when no pending ops

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address PR review feedback

- Fix TS build error: use `select('*, customer:customers(*)')` for
  match_transaction_invoice to avoid array type inference
- Add status guard to commitSendInvoice (prevents duplicate sends)
- Replace auth.admin.getUserById with user email from session auth
- Restore optimistic lock check in commitMatchTransactionInvoice
- Fix tool description typo: expense_software → expense_office

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add support contact links and improve SIE import UX

Add a SupportLink component with a contact dialog throughout the app
(nav, help page, settings, MFA, error pages, empty states). Improve
SIE import flow with phased loading states, structured skip breakdowns,
and an elapsed-time counter. Fix MFA enroll stale factor cleanup and
URL encoding for settings return path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address PR review — open redirect, XSS, test cleanup, fallback email

- Validate returnTo is a relative path in MFA enroll (prevents open redirect)
- Add afterEach import to event-log-handler tests (fixes handler leak)
- HTML-escape user-supplied subject and message in support email body
- Replace hardcoded personal email with support@gnubok.se fallback

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:14:17 +01:00

267 lines
9.8 KiB
TypeScript

'use client'
import Link from 'next/link'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import {
CheckCircle,
XCircle,
AlertCircle,
FileText,
ExternalLink,
RotateCcw,
Info,
} from 'lucide-react'
import type { ImportResult } from '@/lib/import/types'
interface ImportResultStepProps {
result: ImportResult
onNewImport: () => void
}
export default function ImportResultStep({ result, onNewImport }: ImportResultStepProps) {
const hasErrors = result.errors.length > 0
const skipped = result.details?.skippedVouchers
// Filter out raw "hoppades över" warnings when we have structured data
const otherWarnings = skipped && skipped.total > 0
? result.warnings.filter((w) => !w.includes('hoppades över'))
: result.warnings
return (
<div className="space-y-6">
{/* Success/Failure header */}
<Card className={result.success ? 'border-success/50' : 'border-destructive/50'}>
<CardHeader>
<CardTitle className="flex items-center gap-2">
{result.success ? (
<>
<CheckCircle className="h-6 w-6 text-success" />
Import genomförd
</>
) : (
<>
<XCircle className="h-6 w-6 text-destructive" />
Import misslyckades
</>
)}
</CardTitle>
<CardDescription>
{result.success
? 'Din bokföring har importerats framgångsrikt.'
: 'Det uppstod fel under importen. Se detaljer nedan.'}
</CardDescription>
</CardHeader>
</Card>
{/* Statistics */}
{result.success && (
<div className="grid grid-cols-2 gap-4 md:grid-cols-3">
<Card>
<CardContent className="pt-6">
<div className="flex items-center gap-2 text-muted-foreground mb-1">
<FileText className="h-4 w-4" />
<span className="text-sm">Verifikationer skapade</span>
</div>
<p className="text-2xl font-display font-medium tabular-nums">{result.journalEntriesCreated}</p>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center gap-2 text-muted-foreground mb-1">
<span className="text-sm">Räkenskapsår</span>
</div>
<div className="text-2xl font-display font-medium">
{result.fiscalPeriodId ? (
<Badge variant="default" className="bg-success">Skapat</Badge>
) : (
<Badge variant="secondary">Befintligt</Badge>
)}
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center gap-2 text-muted-foreground mb-1">
<span className="text-sm">Ingående balanser</span>
</div>
<div className="text-2xl font-display font-medium">
{result.openingBalanceEntryId ? (
<Badge variant="default" className="bg-success">Importerade</Badge>
) : (
<Badge variant="secondary">Inga</Badge>
)}
</div>
</CardContent>
</Card>
</div>
)}
{/* Errors */}
{hasErrors && (
<Card className="border-destructive/50">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-destructive">
<XCircle className="h-5 w-5" />
Fel ({result.errors.length})
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2 max-h-48 overflow-y-auto">
{result.errors.map((error, i) => (
<div key={i} className="text-sm flex gap-2">
<XCircle className="h-4 w-4 text-destructive flex-shrink-0 mt-0.5" />
<span>{error}</span>
</div>
))}
</div>
</CardContent>
</Card>
)}
{/* Skipped vouchers — structured breakdown */}
{skipped && skipped.total > 0 && (
<Card className="border-muted-foreground/20">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-muted-foreground">
<Info className="h-5 w-5" />
Hoppade över {skipped.total} verifikationer
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
{skipped.empty > 0 && (
<div className="text-sm">
<p className="font-medium">{skipped.empty} tomma verifikationer</p>
<p className="text-muted-foreground">
Platshållare utan bokföringsrader vanligt i Fortnox och Visma. Påverkar inte din bokföring.
</p>
</div>
)}
{skipped.unbalanced > 0 && (
<div className="text-sm">
<p className="font-medium">{skipped.unbalanced} obalanserade verifikationer</p>
<p className="text-muted-foreground">
Debet och kredit stämmer inte överens i källsystemet. Saldon har justerats automatiskt.
</p>
</div>
)}
{skipped.singleLine > 0 && (
<div className="text-sm">
<p className="font-medium">{skipped.singleLine} enradsverifikationer</p>
<p className="text-muted-foreground">
Verifikationer med bara en rad (t.ex. periodiseringar). Kräver minst två rader för dubbelbokning.
</p>
</div>
)}
{skipped.unmapped > 0 && (
<div className="text-sm">
<p className="font-medium">{skipped.unmapped} verifikationer med ej kopplade konton</p>
<p className="text-muted-foreground">
Innehåller konton som inte kunde kopplas till din kontoplan.
</p>
</div>
)}
</div>
</CardContent>
</Card>
)}
{/* Other warnings (filtered) */}
{otherWarnings.length > 0 && (
<Card className="border-warning/50">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-warning">
<AlertCircle className="h-5 w-5" />
Varningar ({otherWarnings.length})
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2 max-h-48 overflow-y-auto">
{otherWarnings.map((warning, i) => (
<div key={i} className="text-sm flex gap-2">
<AlertCircle className="h-4 w-4 text-warning flex-shrink-0 mt-0.5" />
<span>{warning}</span>
</div>
))}
</div>
</CardContent>
</Card>
)}
{/* Next steps */}
{result.success && (
<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">
1
</div>
<div>
<p className="font-medium">Granska importerade verifikationer</p>
<p className="text-sm text-muted-foreground">
Kontrollera att allt ser korrekt ut i bokföringslistan
</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">
2
</div>
<div>
<p className="font-medium">Verifiera balanserna</p>
<p className="text-sm text-muted-foreground">
Jämför huvudboken med din tidigare bokföring
</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">
3
</div>
<div>
<p className="font-medium">Fortsätt med ny bokföring</p>
<p className="text-sm text-muted-foreground">
Nu kan du börja lägga till nya transaktioner
</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>
<div className="flex flex-col gap-2 sm:flex-row">
{result.success && (
<>
<Button variant="outline" className="min-h-11" asChild>
<Link href="/bookkeeping">
Visa bokföring
<ExternalLink className="ml-2 h-4 w-4" />
</Link>
</Button>
<Button className="min-h-11" asChild>
<Link href="/reports">
Visa rapporter
<ExternalLink className="ml-2 h-4 w-4" />
</Link>
</Button>
</>
)}
</div>
</div>
</div>
)
}