style: normalizar formatacao de importacao e suporte

This commit is contained in:
Felipe Coutinho
2026-03-21 19:32:38 +00:00
parent 19a1b1e943
commit 1d36b12109
12 changed files with 119 additions and 73 deletions

View File

@@ -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({
<SelectContent>
{payerOptions.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
<PayerSelectContent label={opt.label} avatarUrl={opt.avatarUrl} />
<PayerSelectContent
label={opt.label}
avatarUrl={opt.avatarUrl}
/>
</SelectItem>
))}
</SelectContent>
@@ -150,7 +156,10 @@ export function GlobalFields({
<SelectLabel>Despesa</SelectLabel>
{expenseCategories.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
<CategorySelectContent label={opt.label} icon={opt.icon} />
<CategorySelectContent
label={opt.label}
icon={opt.icon}
/>
</SelectItem>
))}
</SelectGroup>
@@ -163,7 +172,10 @@ export function GlobalFields({
<SelectLabel>Receita</SelectLabel>
{incomeCategories.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
<CategorySelectContent label={opt.label} icon={opt.icon} />
<CategorySelectContent
label={opt.label}
icon={opt.icon}
/>
</SelectItem>
))}
</SelectGroup>
@@ -172,17 +184,17 @@ export function GlobalFields({
</Select>
</div>
{isCard && (
<div className="flex min-w-44 flex-col gap-1.5">
<Label>Fatura</Label>
<PeriodPicker
value={invoicePeriod ?? ""}
onChange={(v) => onInvoicePeriodChange(v || null)}
placeholder="Selecionar fatura…"
/>
</div>
)}
{isCard && (
<div className="flex min-w-44 flex-col gap-1.5">
<Label>Fatura</Label>
<PeriodPicker
value={invoicePeriod ?? ""}
onChange={(v) => onInvoicePeriodChange(v || null)}
placeholder="Selecionar fatura…"
/>
</div>
)}
</div>
</div>
</div>
);
}

View File

@@ -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({
<div>
<CardTitle>Importar extrato</CardTitle>
<CardDescription>
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.
</CardDescription>
</div>
<ImportSteps current={currentStep} />

View File

@@ -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 ? (

View File

@@ -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
}
/>
</TableHead>
@@ -114,7 +112,10 @@ export function ReviewTable({
</TableRow>
)}
{virtualRows.map((virtualRow) => {
const row = rows[virtualRow.index]!;
const row = rows[virtualRow.index];
if (!row) {
return null;
}
const index = virtualRow.index;
return (
<TableRow
@@ -199,9 +200,7 @@ export function ReviewTable({
<TableCell>
<TransactionTypeBadge
kind={
row.transactionType === "income"
? "Receita"
: "Despesa"
row.transactionType === "income" ? "Receita" : "Despesa"
}
/>
</TableCell>

View File

@@ -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) {
/>
<div className="flex items-center justify-between">
{error ? (
<p className="text-destructive text-sm">{error}</p>
) : (
<span />
)}
{error ? <p className="text-destructive text-sm">{error}</p> : <span />}
<button
type="button"
onClick={handleDownloadTemplate}