import { useMemo } from "react"; import { useTranslation } from "react-i18next"; import { useCategoryTaxonomy } from "../../hooks/useCategoryTaxonomy"; import type { MappingRow as MappingRowType, ConfidenceBadge, } from "../../services/categoryMappingService"; interface MappingRowProps { row: MappingRowType; /** When true, the row is highlighted (its preview panel is open). */ isSelected: boolean; /** Callback fired when the row is clicked — opens the preview panel. */ onSelect: (v2CategoryId: number) => void; /** * Called with the new v1 target id + name when the user resolves the row * via the inline dropdown. The dropdown is only rendered for unresolved * ("🟠 needs review") rows — resolved rows just show the target name. */ onResolve: (v2CategoryId: number, v1TargetId: number, v1TargetName: string) => void; /** Number of transactions currently attached to this v2 category. */ transactionCount: number; } function badgeClass(confidence: ConfidenceBadge): string { switch (confidence) { case "high": return "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300"; case "medium": return "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300"; case "low": return "bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300"; case "none": return "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300"; } } export default function MappingRow({ row, isSelected, onSelect, onResolve, transactionCount, }: MappingRowProps) { const { t } = useTranslation(); const { getLeaves } = useCategoryTaxonomy(); // For the resolve dropdown: all v1 leaves (terminal categories). We keep the // list flat because the simulate row is narrow; the search box in step 2 // already helps users find a target by keyword. const v1Leaves = useMemo(() => getLeaves(), [getLeaves]); const badgeLabel = t( `categoriesSeed.migration.simulate.confidence.${row.confidence}`, ); const reasonLabel = t( `categoriesSeed.migration.simulate.reason.${row.reason}`, ); const isUnresolved = row.v1TargetId === null || row.v1TargetId === undefined; const handleResolveChange = (ev: React.ChangeEvent) => { const v1TargetId = Number(ev.target.value); if (!Number.isFinite(v1TargetId) || v1TargetId <= 0) return; const leaf = v1Leaves.find((l) => l.id === v1TargetId); if (!leaf) return; const name = t(leaf.i18n_key, { defaultValue: leaf.name }); onResolve(row.v2CategoryId, v1TargetId, name); }; const rowClass = "grid grid-cols-12 gap-2 items-center px-3 py-2 rounded-md border text-sm cursor-pointer transition-colors " + (isSelected ? "bg-[var(--primary)]/10 border-[var(--primary)]/40" : "bg-[var(--card)] border-[var(--border)] hover:border-[var(--primary)]/30 hover:bg-[var(--muted)]"); const targetDisplayName = isUnresolved ? null : row.v1TargetName; return (
onSelect(row.v2CategoryId)} role="button" tabIndex={0} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onSelect(row.v2CategoryId); } }} aria-label={`${row.v2CategoryName} → ${targetDisplayName ?? t("categoriesSeed.migration.simulate.needsReview")}`} > {/* v2 category name + tx count */}
{row.v2CategoryName} {t("categoriesSeed.migration.simulate.txCount", { count: transactionCount, })}
{/* Confidence badge + reason */}
{badgeLabel} {reasonLabel}
{/* v1 target (or picker) */}
e.stopPropagation()} > {isUnresolved ? ( ) : ( {targetDisplayName} )}
); }