"use client"; import { useRouter } from "next/navigation"; import { useCallback, useEffect, useMemo, useState, useTransition, } from "react"; import { toast } from "sonner"; import { fetchCategoryMappings, saveCategoryMappings, } from "@/features/transactions/actions/category-memory-action"; import { checkDuplicateFitIds, deleteTransactionByFitId, importTransactionsAction, undoImportAction, } from "@/features/transactions/actions/import-action"; import { decodeAccountCard, encodeAccountCard, GlobalFields, } from "@/features/transactions/components/import/global-fields"; import { ImportSteps } from "@/features/transactions/components/import/import-steps"; import { ImportSummary } from "@/features/transactions/components/import/import-summary"; import { type ReviewRow, ReviewTable, } from "@/features/transactions/components/import/review-table"; import { UploadZone } from "@/features/transactions/components/import/upload-zone"; import type { SelectOption } from "@/features/transactions/components/types"; import { normalizeDescriptionKey } from "@/features/transactions/lib/import-utils"; import { Button } from "@/shared/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/shared/components/ui/card"; import { Skeleton } from "@/shared/components/ui/skeleton"; import type { ImportStatement } from "@/shared/lib/import/types"; interface ImportPageProps { payerOptions: SelectOption[]; accountOptions: SelectOption[]; cardOptions: SelectOption[]; categoryOptions: SelectOption[]; defaultPayerId: string | null; } export function ImportPage({ payerOptions, accountOptions, cardOptions, categoryOptions, defaultPayerId, }: ImportPageProps) { const router = useRouter(); const [isPending, startTransition] = useTransition(); const [isChecking, setIsChecking] = useState(false); const [statement, setStatement] = useState(null); const [rows, setRows] = useState([]); const [payerId, setPayerId] = useState(defaultPayerId); const [accountCardValue, setAccountCardValue] = useState(null); const [invoicePeriod, setInvoicePeriod] = useState(null); const handleParsed = useCallback(async (stmt: ImportStatement) => { setStatement(stmt); setIsChecking(true); try { const fitIds = stmt.transactions .map((t) => t.externalId) .filter((id): id is string => id !== null); const [duplicates, categoryMappings] = await Promise.all([ checkDuplicateFitIds(fitIds).then((ids) => new Set(ids)), fetchCategoryMappings(stmt.transactions.map((t) => t.description)), ]); setRows( stmt.transactions.map((t) => ({ ...t, isDuplicate: t.externalId ? duplicates.has(t.externalId) : false, selected: t.externalId ? !duplicates.has(t.externalId) : true, categoryId: categoryMappings[normalizeDescriptionKey(t.description)] ?? null, })), ); } finally { setIsChecking(false); } }, []); // Pré-seleciona cartão ou conta com base no tipo detectado no OFX useEffect(() => { if (!statement || accountCardValue) return; if (statement.isCreditCard && cardOptions[0]) { setAccountCardValue(encodeAccountCard("card", cardOptions[0].value)); } else if (!statement.isCreditCard && accountOptions[0]) { setAccountCardValue( encodeAccountCard("account", accountOptions[0].value), ); } }, [statement, cardOptions, accountOptions, accountCardValue]); const toggleRow = (index: number) => { setRows((prev) => prev.map((r, i) => (i === index ? { ...r, selected: !r.selected } : r)), ); }; const toggleAll = (selected: boolean) => { setRows((prev) => prev.map((r) => ({ ...r, selected }))); }; const handleCategoryChange = (index: number, categoryId: string | null) => { setRows((prev) => prev.map((r, i) => (i === index ? { ...r, categoryId } : r)), ); }; const handleUndoDuplicate = async (index: number) => { const row = rows[index]; if (!row?.externalId) return; const result = await deleteTransactionByFitId(row.externalId); if (!result.success) { toast.error("Não foi possível desfazer a importação anterior."); return; } setRows((prev) => prev.map((r, i) => i === index ? { ...r, isDuplicate: false, selected: true } : r, ), ); toast.success("Importação anterior removida."); }; const handleDescriptionChange = (index: number, description: string) => { setRows((prev) => prev.map((r, i) => (i === index ? { ...r, description } : r)), ); }; const handleBulkCategoryChange = (categoryId: string) => { setRows((prev) => prev.map((r) => (r.selected ? { ...r, categoryId } : r))); }; const isCard = accountCardValue?.startsWith("card:") ?? false; const { selectedRows, duplicateCount, uncategorizedCount } = useMemo(() => { const selected = rows.filter((r) => r.selected); return { selectedRows: selected, duplicateCount: rows.filter((r) => r.isDuplicate).length, uncategorizedCount: selected.filter((r) => !r.categoryId).length, }; }, [rows]); const canImport = selectedRows.length > 0 && !!accountCardValue && uncategorizedCount === 0 && (!isCard || !!invoicePeriod) && !isPending; const handleImport = () => { if (!statement || !canImport) return; const decoded = accountCardValue ? decodeAccountCard(accountCardValue) : null; const cardId = decoded?.type === "card" ? decoded.id : null; const accountId = decoded?.type === "account" ? decoded.id : null; const paymentMethod = decoded?.type === "card" ? "Cartão de crédito" : "Pix"; startTransition(async () => { const result = await importTransactionsAction({ rows: selectedRows.map((r) => ({ externalId: r.externalId, date: r.date, amount: r.amount, description: r.description, transactionType: r.transactionType, categoryId: r.categoryId, })), payerId, accountId, cardId, paymentMethod, invoicePeriod, }); if (!result.success) { toast.error(result.error); return; } // Salva mapeamentos description → category (fire-and-forget) saveCategoryMappings( selectedRows.map((r) => ({ description: r.description, categoryId: r.categoryId, })), ); const { importBatchId } = result; const msg = result.skipped > 0 ? `${result.imported} importados, ${result.skipped} duplicatas ignoradas.` : `${result.imported} lançamentos importados.`; router.push("/transactions"); toast.success(msg, { duration: 8000, action: importBatchId ? { label: "Desfazer", onClick: async () => { const undo = await undoImportAction(importBatchId); if (undo.success) { toast.success("Importação desfeita."); } else { toast.error("Não foi possível desfazer."); } }, } : undefined, }); }); }; const currentStep = !statement ? "upload" : isPending ? "done" : "review"; return (
Importar extrato Importe transações a partir de um arquivo .ofx ou planilha .xlsx exportado pelo seu banco.
{!statement || isChecking ? ( <> {!statement && } {isChecking && (
{Array.from({ length: 6 }).map((_, i) => ( ))}
)} ) : ( <> {/* Sticky footer */}
{!accountCardValue ? (

Selecione uma conta ou cartão para continuar.

) : uncategorizedCount > 0 ? (

{uncategorizedCount} lançamento {uncategorizedCount !== 1 ? "s" : ""} sem categoria.

) : isCard && !invoicePeriod ? (

Selecione a fatura para continuar.

) : null}
)}
); }