'use client' import { useState, useMemo } from 'react' import { useTranslations } from 'next-intl' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Badge } from '@/components/ui/badge' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { ArrowRight, Search, Check, CheckCircle, AlertCircle, XCircle, Filter, } from 'lucide-react' import type { AccountMapping } from '@/lib/import/types' import type { BASAccount } from '@/types' import { getAccountClassName } from '@/lib/bookkeeping/account-descriptions' import { defaultRateForVatTreatment, vatTreatmentsForAccountClass, type AccountVatTreatment, } from '@/lib/vat/account-vat-treatment' import { InfoTooltip, Tooltip, TooltipContent, TooltipTrigger, } from '@/components/ui/info-tooltip' import { cn } from '@/lib/utils' interface AccountMappingStepProps { mappings: AccountMapping[] basAccounts: BASAccount[] onMappingChange: (sourceAccount: string, targetAccount: string, targetName: string) => void onVatTreatmentChange: ( sourceAccount: string, treatment: AccountVatTreatment | null, rate: number | null, ) => void /** Accept the suggested VAT treatment for every unreviewed row at once. */ onConfirmAllVatTreatments: () => void onContinue: () => void onBack: () => void } type FilterType = 'all' | 'unmapped' | 'vat_review' | 'low_confidence' | 'manual' const PAGE_SIZE = 50 export default function AccountMappingStep({ mappings, basAccounts, onMappingChange, onVatTreatmentChange, onConfirmAllVatTreatments, onContinue, onBack, }: AccountMappingStepProps) { const t = useTranslations('chart_of_accounts') const [searchTerm, setSearchTerm] = useState('') // Default to showing unmapped accounts first (most actionable) const [filter, setFilter] = useState(() => { const hasUnmapped = mappings.some((m) => !m.targetAccount) const hasVatReview = mappings.some((m) => m.requiresVatTreatmentReview && !m.vatTreatmentReviewed) return hasUnmapped ? 'unmapped' : hasVatReview ? 'vat_review' : 'all' }) const [currentPage, setCurrentPage] = useState(1) // Filter and search mappings const filteredMappings = useMemo(() => { let result = mappings // Apply filter switch (filter) { case 'unmapped': result = result.filter((m) => !m.targetAccount) break case 'low_confidence': result = result.filter((m) => m.targetAccount && m.confidence < 0.7) break case 'vat_review': result = result.filter((m) => m.requiresVatTreatmentReview && !m.vatTreatmentReviewed) break case 'manual': result = result.filter((m) => m.isOverride) break } // Apply search if (searchTerm) { const term = searchTerm.toLowerCase() result = result.filter( (m) => m.sourceAccount.includes(term) || m.sourceName.toLowerCase().includes(term) || m.targetAccount?.includes(term) || m.targetName?.toLowerCase().includes(term) ) } return result }, [mappings, filter, searchTerm]) // Pagination const totalPages = Math.ceil(filteredMappings.length / PAGE_SIZE) const paginatedMappings = useMemo(() => { const start = (currentPage - 1) * PAGE_SIZE return filteredMappings.slice(start, start + PAGE_SIZE) }, [filteredMappings, currentPage]) // Reset page when filter or search changes const handleFilterChange = (newFilter: FilterType) => { setFilter(newFilter) setCurrentPage(1) } const handleSearchChange = (term: string) => { setSearchTerm(term) setCurrentPage(1) } // Calculate stats const stats = useMemo(() => { const unmapped = mappings.filter((m) => !m.targetAccount).length const lowConfidence = mappings.filter((m) => m.targetAccount && m.confidence < 0.7).length const manual = mappings.filter((m) => m.isOverride).length const vatReview = mappings.filter((m) => m.requiresVatTreatmentReview && !m.vatTreatmentReviewed).length return { unmapped, lowConfidence, manual, vatReview } }, [mappings]) const canContinue = stats.unmapped === 0 && stats.vatReview === 0 // Group BAS accounts by class for the dropdown const accountsByClass = useMemo(() => { const groups: { [key: string]: BASAccount[] } = {} for (const account of basAccounts) { const className = getAccountClassName(account.account_class) if (!groups[className]) { groups[className] = [] } groups[className].push(account) } return groups }, [basAccounts]) return (
Kontomappning Varje konto i SIE-filen kopplas till ett konto i din kontoplan. De flesta matchas automatiskt: granska de osäkra nedan. {/* Stats */}
0 ? 'secondary' : 'outline'} className="cursor-pointer" onClick={() => handleFilterChange('vat_review')} > {t('vat_review_filter', { count: stats.vatReview })} 0 ? 'destructive' : 'secondary'} className="cursor-pointer" onClick={() => handleFilterChange('unmapped')} > {stats.unmapped} ej mappade 0 ? 'secondary' : 'outline'} className="cursor-pointer" onClick={() => handleFilterChange('low_confidence')} > {stats.lowConfidence} osäkra handleFilterChange('manual')} > {stats.manual} manuellt satta handleFilterChange('all')} > Visa alla ({mappings.length})
{/* Search and filter */}
handleSearchChange(e.target.value)} className="pl-9" />
{/* Mapping table */}
{/* Under table-fixed the header widths ARE the layout: cells never grow them. The budget is ~990px so the table fits a laptop content column at 100 % zoom (#2125: 1216px overflowed, with 144px spent on a four-digit source account); beyond that the wrapper scrolls and the confirm column stays sticky (#1668). 13px text and px-3 cells match the page-level list density. */} Källkonto Källnamn Målkonto {t('vat_treatment_column')} Konfidens {t('vat_treatment_confirm')} {paginatedMappings.map((mapping) => ( {mapping.sourceAccount} {mapping.sourceAccount === mapping.targetAccount && ['3', '4', '5', '6'].includes(mapping.sourceAccount.charAt(0)) ? (
) : ( - )}
{mapping.targetAccount && ( )} {mapping.requiresVatTreatmentReview && !mapping.vatTreatmentReviewed && ( /* Icon-only: the column header carries the label and the tooltip repeats it on hover; the accessible name also says which row. */ {t('vat_treatment_confirm')} )}
))} {paginatedMappings.length === 0 && ( Inga konton matchar filtret )}
{/* Pagination */} {totalPages > 1 && (

Visar {((currentPage - 1) * PAGE_SIZE) + 1}-{Math.min(currentPage * PAGE_SIZE, filteredMappings.length)} av {filteredMappings.length}

Sida {currentPage} av {totalPages}
)}
{/* Actions */}
{stats.vatReview > 0 && stats.unmapped === 0 && ( )}
) } function TruncatedSourceName({ sourceName }: { sourceName: string }) { const [open, setOpen] = useState(false) return ( {sourceName} ) } function ConfidenceBadge({ confidence, isOverride, }: { confidence: number matchType: string // Keep for potential future use isOverride: boolean }) { if (isOverride) { return Manuell } if (confidence >= 0.9) { return Exakt } if (confidence >= 0.7) { return Trolig } return Osäker }