mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-05-09 11:01:45 +00:00
feat: importação de extratos OFX/XLS com memória de categorias
Adiciona fluxo completo de importação de extratos bancários: - Upload e parsing de arquivos OFX e XLS/XLSX - Tela de revisão com virtualização (@tanstack/react-virtual) - Detecção automática de categoria por histórico de uso - Deduplicação por FITID (OFX) e importBatchId - Tabela `import_category_mappings` para persistir mapeamentos - Botão de acesso ao fluxo na tabela de transações Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
62
src/features/transactions/actions/category-memory-action.ts
Normal file
62
src/features/transactions/actions/category-memory-action.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
"use server";
|
||||
|
||||
import { and, eq, inArray, sql } from "drizzle-orm";
|
||||
import { importCategoryMappings } from "@/db/schema";
|
||||
import { normalizeDescriptionKey } from "@/features/transactions/lib/import-utils";
|
||||
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[],
|
||||
): Promise<Record<string, string>> {
|
||||
const userId = await getUserId();
|
||||
const keys = descriptions.map(normalizeDescriptionKey).filter(Boolean);
|
||||
if (keys.length === 0) return {};
|
||||
|
||||
const rows = await db
|
||||
.select({
|
||||
descriptionKey: importCategoryMappings.descriptionKey,
|
||||
categoryId: importCategoryMappings.categoryId,
|
||||
})
|
||||
.from(importCategoryMappings)
|
||||
.where(
|
||||
and(
|
||||
eq(importCategoryMappings.userId, userId),
|
||||
inArray(importCategoryMappings.descriptionKey, keys),
|
||||
),
|
||||
);
|
||||
|
||||
return Object.fromEntries(rows.map((r) => [r.descriptionKey, r.categoryId]));
|
||||
}
|
||||
|
||||
// Salva/atualiza mapeamentos description → category após uma importação
|
||||
export async function saveCategoryMappings(
|
||||
rows: { description: string; categoryId: string | null }[],
|
||||
): Promise<void> {
|
||||
const userId = await getUserId();
|
||||
|
||||
const toUpsert = rows
|
||||
.filter((r) => r.categoryId !== null)
|
||||
.map((r) => ({
|
||||
userId,
|
||||
descriptionKey: normalizeDescriptionKey(r.description),
|
||||
categoryId: r.categoryId as string,
|
||||
updatedAt: new Date(),
|
||||
}))
|
||||
.filter((r) => r.descriptionKey.length > 0);
|
||||
|
||||
if (toUpsert.length === 0) return;
|
||||
|
||||
await db
|
||||
.insert(importCategoryMappings)
|
||||
.values(toUpsert)
|
||||
.onConflictDoUpdate({
|
||||
target: [importCategoryMappings.userId, importCategoryMappings.descriptionKey],
|
||||
set: {
|
||||
categoryId: sql`excluded.category_id`,
|
||||
updatedAt: sql`excluded.updated_at`,
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user