diff --git a/app/(dashboard)/caixa-de-entrada/actions.ts b/app/(dashboard)/caixa-de-entrada/actions.ts index fc7b25b..c2dc5bd 100644 --- a/app/(dashboard)/caixa-de-entrada/actions.ts +++ b/app/(dashboard)/caixa-de-entrada/actions.ts @@ -1,6 +1,6 @@ "use server"; -import { inboxItems, lancamentos } from "@/db/schema"; +import { inboxItems } from "@/db/schema"; import { handleActionError } from "@/lib/actions/helpers"; import type { ActionResult } from "@/lib/actions/types"; import { db } from "@/lib/db"; @@ -8,20 +8,9 @@ import { getUser } from "@/lib/auth/server"; import { and, eq, inArray } from "drizzle-orm"; import { revalidatePath } from "next/cache"; import { z } from "zod"; -import { getCurrentPeriod } from "@/lib/utils/period"; -const processInboxSchema = z.object({ +const markProcessedSchema = z.object({ inboxItemId: z.string().uuid("ID do item inválido"), - name: z.string().min(1, "Nome é obrigatório"), - amount: z.coerce.number().positive("Valor deve ser positivo"), - purchaseDate: z.string().min(1, "Data é obrigatória"), - transactionType: z.enum(["Despesa", "Receita"]), - condition: z.string().min(1, "Condição é obrigatória"), - paymentMethod: z.string().min(1, "Forma de pagamento é obrigatória"), - categoriaId: z.string().uuid("Categoria inválida"), - contaId: z.string().uuid("Conta inválida").optional(), - cartaoId: z.string().uuid("Cartão inválido").optional(), - note: z.string().optional(), }); const discardInboxSchema = z.object({ @@ -39,12 +28,15 @@ function revalidateInbox() { revalidatePath("/dashboard"); } -export async function processInboxItemAction( - input: z.infer +/** + * Mark an inbox item as processed after a lancamento was created + */ +export async function markInboxAsProcessedAction( + input: z.infer ): Promise { try { const user = await getUser(); - const data = processInboxSchema.parse(input); + const data = markProcessedSchema.parse(input); // Verificar se item existe e pertence ao usuário const [item] = await db @@ -63,43 +55,19 @@ export async function processInboxItemAction( return { success: false, error: "Item não encontrado ou já processado." }; } - // Determinar período baseado na data de compra - const purchaseDate = new Date(data.purchaseDate); - const period = getCurrentPeriod(purchaseDate); - - // Criar lançamento - const [newLancamento] = await db - .insert(lancamentos) - .values({ - userId: user.id, - name: data.name, - amount: data.amount.toString(), - purchaseDate: purchaseDate, - transactionType: data.transactionType, - condition: data.condition, - paymentMethod: data.paymentMethod, - categoriaId: data.categoriaId, - contaId: data.contaId, - cartaoId: data.cartaoId, - note: data.note, - period, - }) - .returning({ id: lancamentos.id }); - // Marcar item como processado await db .update(inboxItems) .set({ status: "processed", processedAt: new Date(), - lancamentoId: newLancamento.id, updatedAt: new Date(), }) .where(eq(inboxItems.id, data.inboxItemId)); revalidateInbox(); - return { success: true, message: "Lançamento criado com sucesso!" }; + return { success: true, message: "Item processado com sucesso!" }; } catch (error) { return handleActionError(error); } diff --git a/app/(dashboard)/caixa-de-entrada/data.ts b/app/(dashboard)/caixa-de-entrada/data.ts index b9f7be4..0df1a70 100644 --- a/app/(dashboard)/caixa-de-entrada/data.ts +++ b/app/(dashboard)/caixa-de-entrada/data.ts @@ -3,9 +3,14 @@ */ import { db } from "@/lib/db"; -import { inboxItems, categorias, contas, cartoes } from "@/db/schema"; -import { eq, desc, and } from "drizzle-orm"; +import { inboxItems, categorias, contas, cartoes, lancamentos } from "@/db/schema"; +import { eq, desc, and, gte } from "drizzle-orm"; import type { InboxItem, SelectOption } from "@/components/caixa-de-entrada/types"; +import { + fetchLancamentoFilterSources, + buildSluggedFilters, + buildOptionSets, +} from "@/lib/lancamentos/page-helpers"; export async function fetchInboxItems( userId: string, @@ -80,3 +85,70 @@ export async function fetchPendingInboxCount(userId: string): Promise { return items.length; } + +/** + * Fetch all data needed for the LancamentoDialog in inbox context + */ +export async function fetchInboxDialogData(userId: string): Promise<{ + pagadorOptions: SelectOption[]; + splitPagadorOptions: SelectOption[]; + defaultPagadorId: string | null; + contaOptions: SelectOption[]; + cartaoOptions: SelectOption[]; + categoriaOptions: SelectOption[]; + estabelecimentos: string[]; +}> { + const filterSources = await fetchLancamentoFilterSources(userId); + const sluggedFilters = buildSluggedFilters(filterSources); + + const { + pagadorOptions, + splitPagadorOptions, + defaultPagadorId, + contaOptions, + cartaoOptions, + categoriaOptions, + } = buildOptionSets({ + ...sluggedFilters, + pagadorRows: filterSources.pagadorRows, + }); + + // Fetch recent establishments (same approach as getRecentEstablishmentsAction) + const threeMonthsAgo = new Date(); + threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3); + + const recentEstablishments = await db + .select({ name: lancamentos.name }) + .from(lancamentos) + .where( + and( + eq(lancamentos.userId, userId), + gte(lancamentos.purchaseDate, threeMonthsAgo) + ) + ) + .orderBy(desc(lancamentos.purchaseDate)); + + // Remove duplicates and filter empty names + const filteredNames: string[] = recentEstablishments + .map((r: { name: string }) => r.name) + .filter( + (name: string | null): name is string => + name != null && + name.trim().length > 0 && + !name.toLowerCase().startsWith("pagamento fatura") + ); + const estabelecimentos = Array.from(new Set(filteredNames)).slice( + 0, + 100 + ); + + return { + pagadorOptions, + splitPagadorOptions, + defaultPagadorId, + contaOptions, + cartaoOptions, + categoriaOptions, + estabelecimentos, + }; +} diff --git a/app/(dashboard)/caixa-de-entrada/page.tsx b/app/(dashboard)/caixa-de-entrada/page.tsx index 15b70ab..8bc683a 100644 --- a/app/(dashboard)/caixa-de-entrada/page.tsx +++ b/app/(dashboard)/caixa-de-entrada/page.tsx @@ -1,29 +1,26 @@ import { InboxPage } from "@/components/caixa-de-entrada/inbox-page"; import { getUserId } from "@/lib/auth/server"; -import { - fetchInboxItems, - fetchCategoriasForSelect, - fetchContasForSelect, - fetchCartoesForSelect, -} from "./data"; +import { fetchInboxItems, fetchInboxDialogData } from "./data"; export default async function Page() { const userId = await getUserId(); - const [items, categorias, contas, cartoes] = await Promise.all([ + const [items, dialogData] = await Promise.all([ fetchInboxItems(userId, "pending"), - fetchCategoriasForSelect(userId), - fetchContasForSelect(userId), - fetchCartoesForSelect(userId), + fetchInboxDialogData(userId), ]); return (
); diff --git a/app/api/inbox/batch/route.ts b/app/api/inbox/batch/route.ts index 57ed8d4..235263d 100644 --- a/app/api/inbox/batch/route.ts +++ b/app/api/inbox/batch/route.ts @@ -110,7 +110,6 @@ export async function POST(request: Request) { parsedName: item.parsedName, parsedAmount: item.parsedAmount?.toString(), parsedDate: item.parsedDate, - parsedCardLastDigits: item.parsedCardLastDigits, parsedTransactionType: item.parsedTransactionType, status: "pending", }) diff --git a/app/api/inbox/route.ts b/app/api/inbox/route.ts index 32dff0a..46938e4 100644 --- a/app/api/inbox/route.ts +++ b/app/api/inbox/route.ts @@ -99,7 +99,6 @@ export async function POST(request: Request) { parsedName: data.parsedName, parsedAmount: data.parsedAmount?.toString(), parsedDate: data.parsedDate, - parsedCardLastDigits: data.parsedCardLastDigits, parsedTransactionType: data.parsedTransactionType, status: "pending", }) diff --git a/components/caixa-de-entrada/inbox-card.tsx b/components/caixa-de-entrada/inbox-card.tsx index 411b850..8493a88 100644 --- a/components/caixa-de-entrada/inbox-card.tsx +++ b/components/caixa-de-entrada/inbox-card.tsx @@ -95,19 +95,16 @@ export function InboxCard({
- {item.parsedName &&

{item.parsedName}

} -

+ {item.originalTitle && ( +

{item.originalTitle}

+ )} +

{item.originalText}

{timeAgo} - {item.parsedCardLastDigits && ( - - •••• {item.parsedCardLastDigits} - - )}
diff --git a/components/caixa-de-entrada/inbox-details-dialog.tsx b/components/caixa-de-entrada/inbox-details-dialog.tsx index 9e10549..a9eaf6a 100644 --- a/components/caixa-de-entrada/inbox-details-dialog.tsx +++ b/components/caixa-de-entrada/inbox-details-dialog.tsx @@ -119,12 +119,6 @@ export function InboxDetailsDialog({
)} - {item.parsedCardLastDigits && ( -
- Cartão - •••• {item.parsedCardLastDigits} -
- )}
Tipo {item.parsedTransactionType || "Não identificado"} diff --git a/components/caixa-de-entrada/inbox-page.tsx b/components/caixa-de-entrada/inbox-page.tsx index 38bb45a..04e8945 100644 --- a/components/caixa-de-entrada/inbox-page.tsx +++ b/components/caixa-de-entrada/inbox-page.tsx @@ -1,29 +1,40 @@ "use client"; -import { discardInboxItemAction } from "@/app/(dashboard)/caixa-de-entrada/actions"; +import { + discardInboxItemAction, + markInboxAsProcessedAction, +} from "@/app/(dashboard)/caixa-de-entrada/actions"; import { ConfirmActionDialog } from "@/components/confirm-action-dialog"; import { EmptyState } from "@/components/empty-state"; +import { LancamentoDialog } from "@/components/lancamentos/dialogs/lancamento-dialog/lancamento-dialog"; import { Card } from "@/components/ui/card"; import { RiInboxLine } from "@remixicon/react"; import { useCallback, useMemo, useState } from "react"; import { toast } from "sonner"; import { InboxCard } from "./inbox-card"; import { InboxDetailsDialog } from "./inbox-details-dialog"; -import { ProcessDialog } from "./process-dialog"; import type { InboxItem, SelectOption } from "./types"; interface InboxPageProps { items: InboxItem[]; - categorias: SelectOption[]; - contas: SelectOption[]; - cartoes: SelectOption[]; + pagadorOptions: SelectOption[]; + splitPagadorOptions: SelectOption[]; + defaultPagadorId: string | null; + contaOptions: SelectOption[]; + cartaoOptions: SelectOption[]; + categoriaOptions: SelectOption[]; + estabelecimentos: string[]; } export function InboxPage({ items, - categorias, - contas, - cartoes, + pagadorOptions, + splitPagadorOptions, + defaultPagadorId, + contaOptions, + cartaoOptions, + categoriaOptions, + estabelecimentos, }: InboxPageProps) { const [processOpen, setProcessOpen] = useState(false); const [itemToProcess, setItemToProcess] = useState(null); @@ -96,6 +107,42 @@ export function InboxPage({ throw new Error(result.error); }, [itemToDiscard]); + const handleLancamentoSuccess = useCallback(async () => { + if (!itemToProcess) return; + + const result = await markInboxAsProcessedAction({ + inboxItemId: itemToProcess.id, + }); + + if (result.success) { + toast.success("Notificação processada!"); + } else { + toast.error(result.error); + } + }, [itemToProcess]); + + // Prepare default values from inbox item + // Use parsedDate if available, otherwise fall back to notificationTimestamp + const getDateString = (date: Date | string | null | undefined): string | null => { + if (!date) return null; + if (typeof date === "string") return date.slice(0, 10); + return date.toISOString().slice(0, 10); + }; + + const defaultPurchaseDate = + getDateString(itemToProcess?.parsedDate) ?? + getDateString(itemToProcess?.notificationTimestamp) ?? + null; + + const defaultName = itemToProcess?.parsedName ?? null; + + const defaultAmount = itemToProcess?.parsedAmount + ? String(Math.abs(Number(itemToProcess.parsedAmount))) + : null; + + const defaultTransactionType = + itemToProcess?.parsedTransactionType === "Receita" ? "Receita" : "Despesa"; + return ( <>
@@ -122,13 +169,23 @@ export function InboxPage({ )}
- void; - item: InboxItem | null; - categorias: SelectOption[]; - contas: SelectOption[]; - cartoes: SelectOption[]; -} - -export function ProcessDialog({ - open, - onOpenChange, - item, - categorias, - contas, - cartoes, -}: ProcessDialogProps) { - const [loading, setLoading] = useState(false); - - // Form state - const [name, setName] = useState(""); - const [amount, setAmount] = useState(""); - const [purchaseDate, setPurchaseDate] = useState(""); - const [transactionType, setTransactionType] = useState<"Despesa" | "Receita">( - "Despesa" - ); - const [condition, setCondition] = useState("realizado"); - const [paymentMethod, setPaymentMethod] = useState("cartao-credito"); - const [categoriaId, setCategoriaId] = useState(""); - const [contaId, setContaId] = useState(""); - const [cartaoId, setCartaoId] = useState(""); - const [note, setNote] = useState(""); - - // Pré-preencher com dados parseados - useEffect(() => { - if (item) { - setName(item.parsedName || ""); - setAmount(item.parsedAmount || ""); - setPurchaseDate( - item.parsedDate - ? new Date(item.parsedDate).toISOString().split("T")[0] - : new Date(item.notificationTimestamp).toISOString().split("T")[0] - ); - setTransactionType( - (item.parsedTransactionType as "Despesa" | "Receita") || "Despesa" - ); - setCondition("realizado"); - setPaymentMethod(item.parsedCardLastDigits ? "cartao-credito" : "outros"); - setCategoriaId(""); - setContaId(""); - setCartaoId(""); - setNote(""); - } - }, [item]); - - // Por enquanto, mostrar todas as categorias - // Em produção, seria melhor filtrar pelo tipo (Despesa/Receita) - const filteredCategorias = categorias; - - const handleSubmit = useCallback(async () => { - if (!item) return; - - if (!categoriaId) { - toast.error("Selecione uma categoria."); - return; - } - - if (paymentMethod === "cartao-credito" && !cartaoId) { - toast.error("Selecione um cartão."); - return; - } - - if (paymentMethod !== "cartao-credito" && !contaId) { - toast.error("Selecione uma conta."); - return; - } - - setLoading(true); - - try { - const result = await processInboxItemAction({ - inboxItemId: item.id, - name, - amount: parseFloat(amount), - purchaseDate, - transactionType, - condition, - paymentMethod, - categoriaId, - contaId: paymentMethod !== "cartao-credito" ? contaId : undefined, - cartaoId: paymentMethod === "cartao-credito" ? cartaoId : undefined, - note: note || undefined, - }); - - if (result.success) { - toast.success(result.message); - onOpenChange(false); - } else { - toast.error(result.error); - } - } finally { - setLoading(false); - } - }, [ - item, - name, - amount, - purchaseDate, - transactionType, - condition, - paymentMethod, - categoriaId, - contaId, - cartaoId, - note, - onOpenChange, - ]); - - if (!item) return null; - - return ( - - - - Processar Notificação - - Revise os dados extraídos e complete as informações para criar o - lançamento. - - - -
- {/* Texto original */} -
-

Notificação original:

-

{item.originalText}

-
- - {/* Nome/Descrição */} -
- - setName(e.target.value)} - placeholder="Ex: Supermercado, Uber, etc." - /> -
- - {/* Valor e Data */} -
-
- - setAmount(e.target.value)} - placeholder="0,00" - /> -
-
- - setPurchaseDate(e.target.value)} - /> -
-
- - {/* Tipo de transação */} -
- - -
- - {/* Forma de pagamento */} -
- - -
- - {/* Cartão ou Conta */} - {paymentMethod === "cartao-credito" ? ( -
- - -
- ) : ( -
- - -
- )} - - {/* Categoria */} -
- - -
- - {/* Condição */} -
- - -
- - {/* Notas */} -
- -