From 1d36b121091a2d6cec2ba230240de1fbdd09c4ce Mon Sep 17 00:00:00 2001 From: Felipe Coutinho Date: Sat, 21 Mar 2026 19:32:38 +0000 Subject: [PATCH] style: normalizar formatacao de importacao e suporte --- .../(dashboard)/transactions/import/page.tsx | 17 +++++-- src/features/accounts/actions.ts | 29 ++++++------ .../actions/category-memory-action.ts | 6 ++- .../transactions/actions/import-action.ts | 33 ++++++++------ .../components/import/global-fields.tsx | 44 ++++++++++++------- .../components/import/import-page.tsx | 25 ++++++++--- .../components/import/import-steps.tsx | 4 +- .../components/import/review-table.tsx | 15 +++---- .../components/import/upload-zone.tsx | 10 ++--- .../components/table/transactions-table.tsx | 2 +- src/shared/lib/import/ofx-parser.ts | 2 +- src/shared/lib/import/xls-parser.ts | 5 ++- 12 files changed, 119 insertions(+), 73 deletions(-) diff --git a/src/app/(dashboard)/transactions/import/page.tsx b/src/app/(dashboard)/transactions/import/page.tsx index 27f0b9f..3fae5b1 100644 --- a/src/app/(dashboard)/transactions/import/page.tsx +++ b/src/app/(dashboard)/transactions/import/page.tsx @@ -1,14 +1,25 @@ import { ImportPage } from "@/features/transactions/components/import/import-page"; +import { + buildOptionSets, + buildSluggedFilters, +} from "@/features/transactions/page-helpers"; import { fetchTransactionFilterSources } from "@/features/transactions/queries"; -import { buildOptionSets, buildSluggedFilters } from "@/features/transactions/page-helpers"; import { getUserId } from "@/shared/lib/auth/server"; export default async function Page() { const userId = await getUserId(); const filterSources = await fetchTransactionFilterSources(userId); const sluggedFilters = buildSluggedFilters(filterSources); - const { payerOptions, accountOptions, cardOptions, categoryOptions, defaultPayerId } = - buildOptionSets({ ...sluggedFilters, payerRows: filterSources.payerRows }); + const { + payerOptions, + accountOptions, + cardOptions, + categoryOptions, + defaultPayerId, + } = buildOptionSets({ + ...sluggedFilters, + payerRows: filterSources.payerRows, + }); return (
diff --git a/src/features/accounts/actions.ts b/src/features/accounts/actions.ts index 97c7f68..0191494 100644 --- a/src/features/accounts/actions.ts +++ b/src/features/accounts/actions.ts @@ -48,21 +48,20 @@ const accountBaseSchema = z.object({ .string({ message: "Selecione um logo." }) .trim() .min(1, "Selecione um logo."), - initialBalance: z - .union([ - z.number(), - z - .string() - .trim() - .transform((value) => - value.length === 0 ? "0" : value.replace(",", "."), - ) - .refine( - (value) => !Number.isNaN(Number.parseFloat(value)), - "Informe um saldo inicial válido.", - ) - .transform((value) => Number.parseFloat(value)), - ]), + initialBalance: z.union([ + z.number(), + z + .string() + .trim() + .transform((value) => + value.length === 0 ? "0" : value.replace(",", "."), + ) + .refine( + (value) => !Number.isNaN(Number.parseFloat(value)), + "Informe um saldo inicial válido.", + ) + .transform((value) => Number.parseFloat(value)), + ]), excludeFromBalance: z .union([z.boolean(), z.string()]) .transform((value) => value === true || value === "true"), diff --git a/src/features/transactions/actions/category-memory-action.ts b/src/features/transactions/actions/category-memory-action.ts index 2070506..807975e 100644 --- a/src/features/transactions/actions/category-memory-action.ts +++ b/src/features/transactions/actions/category-memory-action.ts @@ -6,7 +6,6 @@ import { normalizeDescriptionKey } from "@/features/transactions/lib/import-util import { getUserId } from "@/shared/lib/auth/server"; import { db } from "@/shared/lib/db"; - // Retorna um map de descriptionKey → categoryId para as descrições fornecidas export async function fetchCategoryMappings( descriptions: string[], @@ -53,7 +52,10 @@ export async function saveCategoryMappings( .insert(importCategoryMappings) .values(toUpsert) .onConflictDoUpdate({ - target: [importCategoryMappings.userId, importCategoryMappings.descriptionKey], + target: [ + importCategoryMappings.userId, + importCategoryMappings.descriptionKey, + ], set: { categoryId: sql`excluded.category_id`, updatedAt: sql`excluded.updated_at`, diff --git a/src/features/transactions/actions/import-action.ts b/src/features/transactions/actions/import-action.ts index 28dfb75..2d6d624 100644 --- a/src/features/transactions/actions/import-action.ts +++ b/src/features/transactions/actions/import-action.ts @@ -29,7 +29,11 @@ const importSchema = z.object({ accountId: uuidSchema("FinancialAccount").nullable().optional(), cardId: uuidSchema("Cartão").nullable().optional(), paymentMethod: z.string().min(1), - invoicePeriod: z.string().regex(/^\d{4}-\d{2}$/, "Período inválido.").nullable().optional(), + invoicePeriod: z + .string() + .regex(/^\d{4}-\d{2}$/, "Período inválido.") + .nullable() + .optional(), }); export type ImportRow = z.infer; @@ -51,10 +55,7 @@ export async function checkDuplicateFitIds( .select({ ofxFitId: transactions.ofxFitId }) .from(transactions) .where( - and( - eq(transactions.userId, userId), - inArray(transactions.ofxFitId, ids), - ), + and(eq(transactions.userId, userId), inArray(transactions.ofxFitId, ids)), ); return rows.map((r) => r.ofxFitId).filter((id): id is string => id !== null); @@ -67,10 +68,14 @@ export async function importTransactionsAction( const parsed = importSchema.safeParse(input); if (!parsed.success) { - return { success: false, error: parsed.error.issues[0]?.message ?? "Dados inválidos." }; + return { + success: false, + error: parsed.error.issues[0]?.message ?? "Dados inválidos.", + }; } - const { rows, payerId, accountId, cardId, paymentMethod, invoicePeriod } = parsed.data; + const { rows, payerId, accountId, cardId, paymentMethod, invoicePeriod } = + parsed.data; // Valida ownership const [payerOk, accountOk, cardOk] = await Promise.all([ @@ -94,14 +99,19 @@ export async function importTransactionsAction( const records = rows.map((row) => { const purchaseDate = parseLocalDateString(row.date); - const period = invoicePeriod ?? `${purchaseDate.getFullYear()}-${String(purchaseDate.getMonth() + 1).padStart(2, "0")}`; + const period = + invoicePeriod ?? + `${purchaseDate.getFullYear()}-${String(purchaseDate.getMonth() + 1).padStart(2, "0")}`; return { name: row.description, transactionType: row.transactionType === "income" ? "Receita" : "Despesa", condition: "À vista" as const, paymentMethod, - amount: (row.transactionType === "expense" ? -row.amount : row.amount).toFixed(2), + amount: (row.transactionType === "expense" + ? -row.amount + : row.amount + ).toFixed(2), purchaseDate, period, isSettled, @@ -143,10 +153,7 @@ export async function deleteTransactionByFitId( await db .delete(transactions) .where( - and( - eq(transactions.userId, userId), - eq(transactions.ofxFitId, fitId), - ), + and(eq(transactions.userId, userId), eq(transactions.ofxFitId, fitId)), ); await revalidateForEntity("transactions", userId); diff --git a/src/features/transactions/components/import/global-fields.tsx b/src/features/transactions/components/import/global-fields.tsx index 33688e0..01631c0 100644 --- a/src/features/transactions/components/import/global-fields.tsx +++ b/src/features/transactions/components/import/global-fields.tsx @@ -33,7 +33,8 @@ export function decodeAccountCard(value: string): { id: string; } | null { if (value.startsWith("card:")) return { type: "card", id: value.slice(5) }; - if (value.startsWith("account:")) return { type: "account", id: value.slice(8) }; + if (value.startsWith("account:")) + return { type: "account", id: value.slice(8) }; return null; } @@ -65,7 +66,9 @@ export function GlobalFields({ onBulkCategoryChange, }: GlobalFieldsProps) { const isCard = accountCardValue?.startsWith("card:") ?? false; - const expenseCategories = categoryOptions.filter((o) => o.group === "despesa"); + const expenseCategories = categoryOptions.filter( + (o) => o.group === "despesa", + ); const incomeCategories = categoryOptions.filter((o) => o.group === "receita"); return ( @@ -131,7 +134,10 @@ export function GlobalFields({ {payerOptions.map((opt) => ( - + ))} @@ -150,7 +156,10 @@ export function GlobalFields({ Despesa {expenseCategories.map((opt) => ( - + ))} @@ -163,7 +172,10 @@ export function GlobalFields({ Receita {incomeCategories.map((opt) => ( - + ))} @@ -172,17 +184,17 @@ export function GlobalFields({ - {isCard && ( -
- - onInvoicePeriodChange(v || null)} - placeholder="Selecionar fatura…" - /> -
- )} + {isCard && ( +
+ + onInvoicePeriodChange(v || null)} + placeholder="Selecionar fatura…" + /> +
+ )} + - ); } diff --git a/src/features/transactions/components/import/import-page.tsx b/src/features/transactions/components/import/import-page.tsx index 704b594..b6956fc 100644 --- a/src/features/transactions/components/import/import-page.tsx +++ b/src/features/transactions/components/import/import-page.tsx @@ -1,13 +1,18 @@ "use client"; import { useRouter } from "next/navigation"; -import { useCallback, useEffect, useMemo, useState, useTransition } from "react"; +import { + useCallback, + useEffect, + useMemo, + useState, + useTransition, +} from "react"; import { toast } from "sonner"; import { fetchCategoryMappings, saveCategoryMappings, } from "@/features/transactions/actions/category-memory-action"; -import { normalizeDescriptionKey } from "@/features/transactions/lib/import-utils"; import { checkDuplicateFitIds, deleteTransactionByFitId, @@ -27,6 +32,7 @@ import { } 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, @@ -82,7 +88,8 @@ export function ImportPage({ ...t, isDuplicate: t.externalId ? duplicates.has(t.externalId) : false, selected: t.externalId ? !duplicates.has(t.externalId) : true, - categoryId: categoryMappings[normalizeDescriptionKey(t.description)] ?? null, + categoryId: + categoryMappings[normalizeDescriptionKey(t.description)] ?? null, })), ); } finally { @@ -167,7 +174,9 @@ export function ImportPage({ const handleImport = () => { if (!statement || !canImport) return; - const decoded = decodeAccountCard(accountCardValue!); + const decoded = accountCardValue + ? decodeAccountCard(accountCardValue) + : null; const cardId = decoded?.type === "card" ? decoded.id : null; const accountId = decoded?.type === "account" ? decoded.id : null; const paymentMethod = @@ -197,7 +206,10 @@ export function ImportPage({ // Salva mapeamentos description → category (fire-and-forget) saveCategoryMappings( - selectedRows.map((r) => ({ description: r.description, categoryId: r.categoryId })), + selectedRows.map((r) => ({ + description: r.description, + categoryId: r.categoryId, + })), ); const { importBatchId } = result; @@ -236,7 +248,8 @@ export function ImportPage({
Importar extrato - Importe transações a partir de um arquivo .ofx ou planilha .xlsx exportado pelo seu banco. + Importe transações a partir de um arquivo .ofx ou planilha .xlsx + exportado pelo seu banco.
diff --git a/src/features/transactions/components/import/import-steps.tsx b/src/features/transactions/components/import/import-steps.tsx index ec458f5..3d4a50b 100644 --- a/src/features/transactions/components/import/import-steps.tsx +++ b/src/features/transactions/components/import/import-steps.tsx @@ -34,7 +34,9 @@ export function ImportSteps({ current }: ImportStepsProps) { isCompleted && "border-primary bg-primary text-primary-foreground", isActive && "border-primary text-primary", - !isCompleted && !isActive && "border-muted-foreground/30 text-muted-foreground", + !isCompleted && + !isActive && + "border-muted-foreground/30 text-muted-foreground", )} > {isCompleted ? ( diff --git a/src/features/transactions/components/import/review-table.tsx b/src/features/transactions/components/import/review-table.tsx index 8029e70..f3d0bd5 100644 --- a/src/features/transactions/components/import/review-table.tsx +++ b/src/features/transactions/components/import/review-table.tsx @@ -1,7 +1,7 @@ "use client"; -import { useRef } from "react"; import { useVirtualizer } from "@tanstack/react-virtual"; +import { useRef } from "react"; import { CategorySelectContent } from "@/features/transactions/components/select-items"; import type { SelectOption } from "@/features/transactions/components/types"; import MoneyValues from "@/shared/components/money-values"; @@ -91,9 +91,7 @@ export function ReviewTable({ onCheckedChange={(v) => onToggleAll(!!v)} aria-label="Selecionar todas" data-state={ - !allSelected && someSelected - ? "indeterminate" - : undefined + !allSelected && someSelected ? "indeterminate" : undefined } /> @@ -114,7 +112,10 @@ export function ReviewTable({ )} {virtualRows.map((virtualRow) => { - const row = rows[virtualRow.index]!; + const row = rows[virtualRow.index]; + if (!row) { + return null; + } const index = virtualRow.index; return ( diff --git a/src/features/transactions/components/import/upload-zone.tsx b/src/features/transactions/components/import/upload-zone.tsx index 6bcbed2..e2cb7f3 100644 --- a/src/features/transactions/components/import/upload-zone.tsx +++ b/src/features/transactions/components/import/upload-zone.tsx @@ -37,7 +37,9 @@ export function UploadZone({ onParsed }: UploadZoneProps) { } onParsed(statement); } catch { - setError("Não foi possível ler o arquivo. Verifique se é um OFX válido."); + setError( + "Não foi possível ler o arquivo. Verifique se é um OFX válido.", + ); } }; reader.readAsText(file, "windows-1252"); @@ -119,11 +121,7 @@ export function UploadZone({ onParsed }: UploadZoneProps) { />
- {error ? ( -

{error}

- ) : ( - - )} + {error ?

{error}

: }