'use client' import { useEffect, useMemo, useState } from 'react' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Label } from '@/components/ui/label' import { Switch } from '@/components/ui/switch' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { ArrowLeft, ArrowRight, Columns3 } from 'lucide-react' import { formatCurrency } from '@/lib/utils' import { getCSVPreview } from '@/lib/import/bank-file/formats/generic-csv' import type { GenericCSVColumnMapping } from '@/lib/import/bank-file/types' interface BankFileColumnMappingStepProps { rawFileContent: string onConfirm: (mapping: GenericCSVColumnMapping) => void onBack: () => void } export default function BankFileColumnMappingStep({ rawFileContent, onConfirm, onBack, }: BankFileColumnMappingStepProps) { const [dateCol, setDateCol] = useState(-1) const [descCol, setDescCol] = useState(-1) const [amountCol, setAmountCol] = useState(-1) const [referenceCol, setReferenceCol] = useState(-1) const [counterpartyCol, setCounterpartyCol] = useState(-1) const [balanceCol, setBalanceCol] = useState(-1) // Auto-detect the most likely delimiter by counting field splits on the first line. // Runs once per file. Users can still override via the dropdown. const detectedDelimiter = useMemo(() => { const firstLine = rawFileContent.split(/\r?\n/).find((l) => l.trim() !== '') ?? '' const candidates: Array<{ d: string; count: number }> = [ { d: ',', count: getCSVPreview(firstLine, ',', 1)[0]?.length ?? 0 }, { d: ';', count: getCSVPreview(firstLine, ';', 1)[0]?.length ?? 0 }, { d: '\t', count: getCSVPreview(firstLine, '\t', 1)[0]?.length ?? 0 }, ] const best = candidates.reduce((a, b) => (b.count > a.count ? b : a)) return best.count > 1 ? best.d : ',' }, [rawFileContent]) const [delimiter, setDelimiter] = useState(detectedDelimiter) const [decimalSep, setDecimalSep] = useState<',' | '.'>(',') const [dateFormat, setDateFormat] = useState('YYYY-MM-DD') // Re-parse headers and preview whenever delimiter or file content changes const parsedRows = useMemo( () => getCSVPreview(rawFileContent, delimiter, 10), [rawFileContent, delimiter] ) // Auto-detect whether the first row is a header: if any cell on row 0 looks // like a date (YYYY-MM-DD, DD.MM.YYYY, DD/MM/YYYY, YYYYMMDD), it's data, not a header. // Users can still override via the switch. const DATE_PATTERNS = [/^\d{4}-\d{2}-\d{2}$/, /^\d{2}[./]\d{2}[./]\d{4}$/, /^\d{8}$/] const detectedHasHeader = useMemo(() => { const firstRow = parsedRows[0] if (!firstRow) return true const hasDateCell = firstRow.some((cell) => DATE_PATTERNS.some((re) => re.test(cell.trim())) ) return !hasDateCell }, [parsedRows]) const [hasHeaderOverride, setHasHeaderOverride] = useState(null) const hasHeader = hasHeaderOverride ?? detectedHasHeader const columnHeaders = useMemo(() => { if (hasHeader && parsedRows[0]) return parsedRows[0] const count = parsedRows[0]?.length ?? 0 return Array.from({ length: count }, (_, i) => `Kolumn ${i + 1}`) }, [parsedRows, hasHeader]) const dataRows = hasHeader ? parsedRows.slice(1) : parsedRows // Auto-guess date/description/amount columns from the first data row. // Only used as initial defaults — user can override any pick. const AMOUNT_RE = /^-?\d+([.,]\d+)?$/ useEffect(() => { if (dateCol !== -1 || descCol !== -1 || amountCol !== -1) return const sample = dataRows[0] if (!sample || sample.length === 0) return const dateIdx = sample.findIndex((cell) => DATE_PATTERNS.some((re) => re.test(cell.trim())) ) const amountIdx = sample .map((cell, i) => ({ i, cell: cell.trim().replace(/\s/g, '') })) .reverse() .find(({ cell, i }) => AMOUNT_RE.test(cell) && i !== dateIdx)?.i ?? -1 const descIdx = sample.findIndex((_, i) => i !== dateIdx && i !== amountIdx) if (dateIdx >= 0) setDateCol(dateIdx) if (descIdx >= 0) setDescCol(descIdx) if (amountIdx >= 0) setAmountCol(amountIdx) // eslint-disable-next-line react-hooks/exhaustive-deps }, [dataRows]) const isValid = dateCol >= 0 && descCol >= 0 && amountCol >= 0 const handleConfirm = () => { const mapping: GenericCSVColumnMapping = { date: dateCol, description: descCol, amount: amountCol, ...(referenceCol >= 0 && { reference: referenceCol }), ...(counterpartyCol >= 0 && { counterparty: counterpartyCol }), ...(balanceCol >= 0 && { balance: balanceCol }), delimiter, decimal_separator: decimalSep, skip_rows: hasHeader ? 1 : 0, date_format: dateFormat, } onConfirm(mapping) } const columnOptions = columnHeaders.map((h, i) => ({ label: `${i + 1}: ${h}`, value: i })) return (
Kolumnmappning Vi kunde inte identifiera bankformatet automatiskt. Mappa kolumnerna manuellt. {/* Header row toggle */}

Slå av om filen saknar rubrikrad och första raden redan innehåller transaktionsdata.

{/* Delimiter, decimal, and date format settings */}
{/* Required column mappings */}

Obligatoriska kolumner

{/* Optional column mappings */}

Valfria kolumner

{/* Live preview */} {isValid && dataRows.length > 0 && ( Förhandsgranskning Så tolkas dina data med den valda mappningen
Datum Beskrivning Belopp {dataRows.slice(0, 5).map((row, i) => { const amountStr = row[amountCol] || '0' const amount = decimalSep === ',' ? parseFloat(amountStr.replace(/\s/g, '').replace(',', '.')) : parseFloat(amountStr.replace(/\s/g, '')) return ( {row[dateCol] || '–'} {row[descCol] || '–'} = 0 ? 'text-success' : 'text-destructive' }`} > {!isNaN(amount) ? formatCurrency(amount) : amountStr} ) })}
)} {/* Navigation */}
) }