"use client";
import { useState, useRef, useEffect } from "react";
import { CATEGORIES, formatCategory } from "@/lib/categories";
import { useImportCSV } from "@/lib/hooks";
import {
parseCSVRows, detectHasHeaders, getColumnLabels, getDataRows, applyMapping,
saveBankPreset, loadBankPresets,
type DateFormat, type ColumnMapping, type ParsedTransaction, type BankPreset,
} from "@/lib/csv-parser";
const DATE_FORMATS: DateFormat[] = ["DD/MM/YYYY", "YYYY-MM-DD", "MM/DD/YYYY", "M/D/YYYY"];
const TX_TYPES = ["debit", "credit", "payment", "refund", "fee", "interest", "transfer"];
type Step = "upload" | "map" | "review" | "done";
function ColSelect({
label, value, onChange, options, required,
}: {
label: string; value: string; onChange: (v: string) => void;
options: string[]; required?: boolean;
}) {
return (
);
}
export function CsvImportModal({ onClose }: { onClose: () => void }) {
const importCSV = useImportCSV();
const fileRef = useRef(null);
const [step, setStep] = useState("upload");
const [rawRows, setRawRows] = useState([]);
const [hasHeaders, setHasHeaders] = useState(false);
const [columnLabels, setColumnLabels] = useState([]);
const [dataRows, setDataRows] = useState([]);
const [bankName, setBankName] = useState("");
const [dateFormat, setDateFormat] = useState("DD/MM/YYYY");
const [mapping, setMapping] = useState({
dateCol: "", descriptionCol: "", amountMode: "single", amountCol: "",
});
const [savePreset, setSavePreset] = useState(false);
const [editedRows, setEditedRows] = useState([]);
const [error, setError] = useState("");
const [presets, setPresets] = useState([]);
const [insertedCount, setInsertedCount] = useState(0);
useEffect(() => { setPresets(loadBankPresets()); }, []);
function handleFile(file: File) {
setError("");
const reader = new FileReader();
reader.onload = (e) => {
const text = e.target?.result as string;
const rows = parseCSVRows(text);
if (rows.length === 0) { setError("No data found in file"); return; }
setRawRows(rows);
const headers = detectHasHeaders(rows, dateFormat);
setHasHeaders(headers);
const labels = getColumnLabels(rows, headers);
setColumnLabels(labels);
setDataRows(getDataRows(rows, headers));
// auto-set first columns as defaults
setMapping((m) => ({
...m,
dateCol: labels[0] ?? "",
descriptionCol: labels[1] ?? "",
amountCol: labels[2] ?? "",
}));
setStep("map");
};
reader.readAsText(file);
}
function applyPreset(preset: BankPreset) {
setBankName(preset.bankName);
setDateFormat(preset.dateFormat);
setMapping(preset.mapping);
}
function refreshColumns() {
if (!rawRows.length) return;
const headers = detectHasHeaders(rawRows, dateFormat);
setHasHeaders(headers);
const labels = getColumnLabels(rawRows, headers);
setColumnLabels(labels);
setDataRows(getDataRows(rawRows, headers));
}
function handleNext() {
setError("");
if (!bankName.trim()) { setError("Bank name is required"); return; }
if (!mapping.dateCol) { setError("Date column is required"); return; }
if (!mapping.descriptionCol) { setError("Description column is required"); return; }
if (mapping.amountMode === "single" && !mapping.amountCol) { setError("Amount column is required"); return; }
if (mapping.amountMode === "debit_credit" && !mapping.debitCol && !mapping.creditCol) {
setError("At least one of debit/credit columns is required"); return;
}
const parsed = applyMapping(dataRows, columnLabels, mapping, dateFormat);
if (parsed.length === 0) { setError("No valid transactions could be parsed — check your column mapping and date format"); return; }
if (savePreset) {
saveBankPreset({ bankName: bankName.trim(), mapping, dateFormat });
}
setEditedRows(parsed);
setStep("review");
}
async function handleImport() {
setError("");
const valid = editedRows.filter((r) => r.date && r.amount > 0 && r.description);
if (!valid.length) { setError("No valid rows to import"); return; }
try {
const result = await importCSV.mutateAsync({ bank_name: bankName, transactions: valid });
setInsertedCount(result.inserted);
setStep("done");
} catch (e) {
setError(e instanceof Error ? e.message : "Import failed");
}
}
function updateRow(i: number, patch: Partial) {
setEditedRows((rows) => rows.map((r, idx) => idx === i ? { ...r, ...patch } : r));
}
function deleteRow(i: number) {
setEditedRows((rows) => rows.filter((_, idx) => idx !== i));
}
const modalWidth = step === "review" ? "max-w-5xl" : step === "map" ? "max-w-xl" : "max-w-md";
return (
e.stopPropagation()}
>
{/* Header */}
Import CSV
{(["upload", "map", "review", "done"] as Step[]).map((s, i) => (
{i + 1}. {s.charAt(0).toUpperCase() + s.slice(1)}
))}
{/* Body */}
{/* Step 1: Upload */}
{step === "upload" && (
{presets.length > 0 && (
)}
{ const f = e.target.files?.[0]; if (f) handleFile(f); }}
/>
{error &&
{error}
}
)}
{/* Step 2: Map */}
{step === "map" && (
{/* Raw preview */}
First 3 rows from file:
{columnLabels.map((h) => (
| {h} |
))}
{dataRows.slice(0, 3).map((row, i) => (
{columnLabels.map((_, ci) => (
| {row[ci] ?? ""} |
))}
))}
setMapping((m) => ({ ...m, dateCol: v }))} options={columnLabels} required />
setMapping((m) => ({ ...m, descriptionCol: v }))} options={columnLabels} required />
setMapping((m) => ({ ...m, merchantCol: v || undefined }))} options={columnLabels} />
setMapping((m) => ({ ...m, categoryCol: v || undefined }))} options={columnLabels} />
{error &&
{error}
}
)}
{/* Step 3: Review */}
{step === "review" && (
{editedRows.length} transactions parsed. Edit or remove rows before importing.
{["Date", "Description", "Amount", "Type", "Merchant", "Category", ""].map((h) => (
| {h} |
))}
{editedRows.map((row, i) => (
|
updateRow(i, { date: e.target.value })}
className="bg-transparent border-b border-zinc-700 text-zinc-300 text-xs w-28 focus:outline-none focus:border-indigo-500" />
|
updateRow(i, { description: e.target.value })}
className="bg-transparent border-b border-zinc-700 text-zinc-300 text-xs w-48 focus:outline-none focus:border-indigo-500" />
|
updateRow(i, { amount: parseFloat(e.target.value) || 0 })}
className="bg-transparent border-b border-zinc-700 text-zinc-300 text-xs w-20 focus:outline-none focus:border-indigo-500" />
|
|
updateRow(i, { merchant_name: e.target.value || undefined })}
className="bg-transparent border-b border-zinc-700 text-zinc-300 text-xs w-28 focus:outline-none focus:border-indigo-500" />
|
|
|
))}
{error &&
{error}
}
)}
{/* Step 4: Done */}
{step === "done" && (
✓
Imported {insertedCount} transactions
Tagged with csv-import
)}
{/* Footer */}
{step !== "done" && (
{step === "map" && (
)}
{step === "review" && (
)}
{step === "map" && (
)}
{step === "review" && (
)}
)}
);
}