'use client' import { useRef } from 'react' import { useTranslations } from 'next-intl' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Checkbox } from '@/components/ui/checkbox' import { TD_CLASS, QUIET_LINK_CLASS, CHECKBOX_REVEAL_CLASS } from '@/components/ui/dry-table' import { cn, formatCurrency, formatDate } from '@/lib/utils' import { formatVoucher } from '@/lib/bookkeeping/voucher-series-resolver' import { AlertCircle, Landmark, Link2, Loader2 } from 'lucide-react' import type { SkattekontoBookingSuggestion, SkattekontoMatchSuggestion, StoredSkattekontoTransaction, } from '@/types/skatteverket' /** * Skattekonto-rad in the /transactions inbox, rendered as a dry-table row * (concept scene 10). The Skatteverket chip is the cue that this row is * fundamentally different from a bank tx: different counter-account (1630 vs * 1930), different categorization rules. No foldout: both actions fit inline. */ export default function SkattekontoInboxCard({ row, matchSuggestion, bookingSuggestion, isExiting = false, processing, selectable, isSelected, onToggleSelect, onBokfor, onMatch, onIgnore, }: { row: StoredSkattekontoTransaction matchSuggestion?: SkattekontoMatchSuggestion | null bookingSuggestion?: SkattekontoBookingSuggestion | null /** The row was just booked and is animating out during the page's 350ms * removal window (.row-exit collapse; instant under reduced motion). */ isExiting?: boolean processing: boolean selectable?: boolean isSelected?: boolean onToggleSelect?: (id: string, extend?: boolean) => void onBokfor: (row: StoredSkattekontoTransaction) => void onMatch: (row: StoredSkattekontoTransaction) => void /** Optional "Ignorera" affordance: hides the row from the work list without * booking it (skattekonto rows are never deleted). The parent owns the * confirm dialog and the PATCH. */ onIgnore?: (row: StoredSkattekontoTransaction) => void }) { const t = useTranslations('tx_skattekonto_card') // See TransactionInboxCard: onCheckedChange has no event, so shift is // captured from the preceding click. const shiftHeld = useRef(false) const amount = Number(row.belopp_skatteverket) const isIncome = amount > 0 const duplicateLabel = matchSuggestion?.voucher_series && matchSuggestion?.voucher_number ? t('duplicate_title_with_voucher', { label: formatVoucher({ voucher_series: matchSuggestion.voucher_series, voucher_number: matchSuggestion.voucher_number, }), }) : t('duplicate_title_draft') return ( {/* Always-visible selection checkbox (concept .cb) */} {/* Zero-width cell: the checkbox hangs in the left page margin so the date column can sit flush with the page edge. */} {selectable && ( { shiftHeld.current = e.shiftKey }} onCheckedChange={() => onToggleSelect?.(row.id, shiftHeld.current)} aria-label={t('select_row')} className={cn( 'absolute -left-5 top-1/2 -translate-y-1/2 border-foreground duration-150 md:-left-6', isSelected ? 'opacity-100' : CHECKBOX_REVEAL_CLASS, )} /> )} {formatDate(row.transaktionsdatum)} {row.transaktionstext} {t('skv_badge')} {matchSuggestion && ( {duplicateLabel} )} {/* What "Bokför" will do: the deterministic rule match, muted so it reads as information, not state. Suppressed on likely duplicates where linking (not booking) is the recommended action. */} {bookingSuggestion && !matchSuggestion && ( {t('suggestion_line', { account: bookingSuggestion.account_name ? `${bookingSuggestion.account} ${bookingSuggestion.account_name}` : bookingSuggestion.account, })} )} {isIncome ? '+' : ''} {formatCurrency(amount)} {matchSuggestion ? ( <> {/* Likely duplicate: linking beats re-booking, so it leads. */} ) : ( <> )} {onIgnore && ( )} ) }