'use client' import { useState } from 'react' import { Button } from '@/components/ui/button' import { Card, CardContent } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { Checkbox } from '@/components/ui/checkbox' import { Receipt as ReceiptIcon, Landmark } from 'lucide-react' import { formatCurrency, formatDate, cn } from '@/lib/utils' import type { AgentInboxItemView } from '@/app/(dashboard)/agent-inbox/page' import type { BookingProposalPayload, MatchProposalPayload } from '@/types' import ReceiptDetailDialog from './ReceiptDetailDialog' import TransactionDetailDialog from './TransactionDetailDialog' import { assessReceiptQuality } from './receipt-quality' interface ProposalCardProps { item: AgentInboxItemView isSelected: boolean isBusy: boolean onToggleSelect: () => void onAccept: () => void onReject: () => void onEdit: () => void onChangeMatch?: () => void } function confidenceLabel(c: number | null): string { if (c === null) return 'Ingen säkerhet' const pct = Math.round(c * 100) return `${pct}% säkerhet` } function confidenceColor(c: number | null): string { if (c === null) return 'bg-muted' if (c >= 0.9) return 'bg-success/15 text-success-foreground' if (c >= 0.6) return 'bg-warning/15 text-warning-foreground' return 'bg-destructive/15 text-destructive-foreground' } export default function ProposalCard({ item, isSelected, isBusy, onToggleSelect, onAccept, onReject, onEdit, onChangeMatch, }: ProposalCardProps) { const proposal = item.proposal! const inbox = item.inbox_item const tx = item.transaction const isMatch = proposal.step_type === 'match' const isUserEdited = Boolean(proposal.edit_diff) // BFL compliance: can't book without a source document. Block match-accept // when no receipt file is attached — the server-side validator enforces this // too, but disabling the button client-side avoids a round-trip error. const receiptMissing = isMatch && !inbox.document_id const matchPayload = isMatch ? (proposal.proposal_json as MatchProposalPayload) : null const bookingPayload = !isMatch ? (proposal.proposal_json as BookingProposalPayload) : null return (
{isMatch ? 'Match' : 'Bokföring'} {confidenceLabel(proposal.confidence)} {isUserEdited && ( Ändrad av användare )} {inbox.document && ( {inbox.document.file_name} )}
{isMatch && matchPayload && ( )} {!isMatch && bookingPayload && ( )} {receiptMissing && (

Kvittobild saknas — ladda upp i kvittodialogen innan du kan godkänna.

)}
{isMatch && onChangeMatch && ( )} {!isMatch && ( )}
) } function MatchProposalBody({ payload, reasoning, transaction, inbox, }: { payload: MatchProposalPayload reasoning: string | null transaction: AgentInboxItemView['transaction'] inbox: AgentInboxItemView['inbox_item'] }) { const proposedTx = transaction && transaction.id === payload.matched_transaction_id ? transaction : null return (
{proposedTx ? ( ) : (
Föreslagen transaktion: {payload.matched_transaction_id}
)}
) } function ReasoningDisclosure({ reasoning, alternatives, }: { reasoning: string | null alternatives?: MatchProposalPayload['alternatives'] }) { const [open, setOpen] = useState(false) const hasReasoning = Boolean(reasoning) const hasAlternatives = alternatives && alternatives.length > 0 if (!hasReasoning && !hasAlternatives) return null return (
{open && (
{reasoning && (

“{reasoning}”

)} {hasAlternatives && ( )}
)}
) } function ReceiptBox({ inbox, }: { inbox: AgentInboxItemView['inbox_item'] }) { const [open, setOpen] = useState(false) const data = (inbox.extracted_data as { merchant?: { name?: string | null } receipt?: { date?: string | null; currency?: string | null } totals?: { total?: number | null } } | null) ?? {} const merchant = data.merchant?.name ?? inbox.document?.file_name ?? 'Okänt kvitto' const total = data.totals?.total ?? null const currency = data.receipt?.currency ?? 'SEK' const date = data.receipt?.date ?? null const quality = assessReceiptQuality(inbox) const hasFile = Boolean(inbox.document_id) const needsAttention = !hasFile || !quality.ok return ( <> ) } function TransactionBox({ tx, }: { tx: NonNullable }) { const [open, setOpen] = useState(false) return ( <> ) } function BookingProposalBody({ payload, reasoning, }: { payload: BookingProposalPayload reasoning: string | null }) { const totalDebit = payload.lines.reduce((s, l) => s + l.debit_amount, 0) return (
{payload.lines.map((line, i) => ( ))}
Konto Debet Kredit
{line.account_number}{' '} {line.description} {line.debit_amount > 0 ? line.debit_amount.toFixed(2) : ''} {line.credit_amount > 0 ? line.credit_amount.toFixed(2) : ''}
{payload.vat_treatment && Moms: {payload.vat_treatment}} {payload.default_private && Privat uttag} {totalDebit.toFixed(2)} {totalDebit.toFixed(2)}
) }