forked from git.gladyson/openmonetis
refactor(inbox): remove process dialog e integra fluxo ao lancamento-dialog
- Remove process-dialog.tsx (componente não mais utilizado) - Simplifica inbox-page.tsx removendo estados e lógica do process dialog - Atualiza inbox-details-dialog para usar lancamento-dialog diretamente - Adiciona suporte a dados iniciais do inbox no lancamento-dialog - Move campos de metadata da inbox para o form de lançamento - Remove campo currency não utilizado do schema - Atualiza actions e data com melhor tratamento de erros
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { inboxItems, lancamentos } from "@/db/schema";
|
import { inboxItems } from "@/db/schema";
|
||||||
import { handleActionError } from "@/lib/actions/helpers";
|
import { handleActionError } from "@/lib/actions/helpers";
|
||||||
import type { ActionResult } from "@/lib/actions/types";
|
import type { ActionResult } from "@/lib/actions/types";
|
||||||
import { db } from "@/lib/db";
|
import { db } from "@/lib/db";
|
||||||
@@ -8,20 +8,9 @@ import { getUser } from "@/lib/auth/server";
|
|||||||
import { and, eq, inArray } from "drizzle-orm";
|
import { and, eq, inArray } from "drizzle-orm";
|
||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
import { z } from "zod";
|
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"),
|
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({
|
const discardInboxSchema = z.object({
|
||||||
@@ -39,12 +28,15 @@ function revalidateInbox() {
|
|||||||
revalidatePath("/dashboard");
|
revalidatePath("/dashboard");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function processInboxItemAction(
|
/**
|
||||||
input: z.infer<typeof processInboxSchema>
|
* Mark an inbox item as processed after a lancamento was created
|
||||||
|
*/
|
||||||
|
export async function markInboxAsProcessedAction(
|
||||||
|
input: z.infer<typeof markProcessedSchema>
|
||||||
): Promise<ActionResult> {
|
): Promise<ActionResult> {
|
||||||
try {
|
try {
|
||||||
const user = await getUser();
|
const user = await getUser();
|
||||||
const data = processInboxSchema.parse(input);
|
const data = markProcessedSchema.parse(input);
|
||||||
|
|
||||||
// Verificar se item existe e pertence ao usuário
|
// Verificar se item existe e pertence ao usuário
|
||||||
const [item] = await db
|
const [item] = await db
|
||||||
@@ -63,43 +55,19 @@ export async function processInboxItemAction(
|
|||||||
return { success: false, error: "Item não encontrado ou já processado." };
|
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
|
// Marcar item como processado
|
||||||
await db
|
await db
|
||||||
.update(inboxItems)
|
.update(inboxItems)
|
||||||
.set({
|
.set({
|
||||||
status: "processed",
|
status: "processed",
|
||||||
processedAt: new Date(),
|
processedAt: new Date(),
|
||||||
lancamentoId: newLancamento.id,
|
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
})
|
})
|
||||||
.where(eq(inboxItems.id, data.inboxItemId));
|
.where(eq(inboxItems.id, data.inboxItemId));
|
||||||
|
|
||||||
revalidateInbox();
|
revalidateInbox();
|
||||||
|
|
||||||
return { success: true, message: "Lançamento criado com sucesso!" };
|
return { success: true, message: "Item processado com sucesso!" };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return handleActionError(error);
|
return handleActionError(error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { db } from "@/lib/db";
|
import { db } from "@/lib/db";
|
||||||
import { inboxItems, categorias, contas, cartoes } from "@/db/schema";
|
import { inboxItems, categorias, contas, cartoes, lancamentos } from "@/db/schema";
|
||||||
import { eq, desc, and } from "drizzle-orm";
|
import { eq, desc, and, gte } from "drizzle-orm";
|
||||||
import type { InboxItem, SelectOption } from "@/components/caixa-de-entrada/types";
|
import type { InboxItem, SelectOption } from "@/components/caixa-de-entrada/types";
|
||||||
|
import {
|
||||||
|
fetchLancamentoFilterSources,
|
||||||
|
buildSluggedFilters,
|
||||||
|
buildOptionSets,
|
||||||
|
} from "@/lib/lancamentos/page-helpers";
|
||||||
|
|
||||||
export async function fetchInboxItems(
|
export async function fetchInboxItems(
|
||||||
userId: string,
|
userId: string,
|
||||||
@@ -80,3 +85,70 @@ export async function fetchPendingInboxCount(userId: string): Promise<number> {
|
|||||||
|
|
||||||
return items.length;
|
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<string>(new Set(filteredNames)).slice(
|
||||||
|
0,
|
||||||
|
100
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
pagadorOptions,
|
||||||
|
splitPagadorOptions,
|
||||||
|
defaultPagadorId,
|
||||||
|
contaOptions,
|
||||||
|
cartaoOptions,
|
||||||
|
categoriaOptions,
|
||||||
|
estabelecimentos,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,29 +1,26 @@
|
|||||||
import { InboxPage } from "@/components/caixa-de-entrada/inbox-page";
|
import { InboxPage } from "@/components/caixa-de-entrada/inbox-page";
|
||||||
import { getUserId } from "@/lib/auth/server";
|
import { getUserId } from "@/lib/auth/server";
|
||||||
import {
|
import { fetchInboxItems, fetchInboxDialogData } from "./data";
|
||||||
fetchInboxItems,
|
|
||||||
fetchCategoriasForSelect,
|
|
||||||
fetchContasForSelect,
|
|
||||||
fetchCartoesForSelect,
|
|
||||||
} from "./data";
|
|
||||||
|
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
const userId = await getUserId();
|
const userId = await getUserId();
|
||||||
|
|
||||||
const [items, categorias, contas, cartoes] = await Promise.all([
|
const [items, dialogData] = await Promise.all([
|
||||||
fetchInboxItems(userId, "pending"),
|
fetchInboxItems(userId, "pending"),
|
||||||
fetchCategoriasForSelect(userId),
|
fetchInboxDialogData(userId),
|
||||||
fetchContasForSelect(userId),
|
|
||||||
fetchCartoesForSelect(userId),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex flex-col items-start gap-6">
|
<main className="flex flex-col items-start gap-6">
|
||||||
<InboxPage
|
<InboxPage
|
||||||
items={items}
|
items={items}
|
||||||
categorias={categorias}
|
pagadorOptions={dialogData.pagadorOptions}
|
||||||
contas={contas}
|
splitPagadorOptions={dialogData.splitPagadorOptions}
|
||||||
cartoes={cartoes}
|
defaultPagadorId={dialogData.defaultPagadorId}
|
||||||
|
contaOptions={dialogData.contaOptions}
|
||||||
|
cartaoOptions={dialogData.cartaoOptions}
|
||||||
|
categoriaOptions={dialogData.categoriaOptions}
|
||||||
|
estabelecimentos={dialogData.estabelecimentos}
|
||||||
/>
|
/>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -110,7 +110,6 @@ export async function POST(request: Request) {
|
|||||||
parsedName: item.parsedName,
|
parsedName: item.parsedName,
|
||||||
parsedAmount: item.parsedAmount?.toString(),
|
parsedAmount: item.parsedAmount?.toString(),
|
||||||
parsedDate: item.parsedDate,
|
parsedDate: item.parsedDate,
|
||||||
parsedCardLastDigits: item.parsedCardLastDigits,
|
|
||||||
parsedTransactionType: item.parsedTransactionType,
|
parsedTransactionType: item.parsedTransactionType,
|
||||||
status: "pending",
|
status: "pending",
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -99,7 +99,6 @@ export async function POST(request: Request) {
|
|||||||
parsedName: data.parsedName,
|
parsedName: data.parsedName,
|
||||||
parsedAmount: data.parsedAmount?.toString(),
|
parsedAmount: data.parsedAmount?.toString(),
|
||||||
parsedDate: data.parsedDate,
|
parsedDate: data.parsedDate,
|
||||||
parsedCardLastDigits: data.parsedCardLastDigits,
|
|
||||||
parsedTransactionType: data.parsedTransactionType,
|
parsedTransactionType: data.parsedTransactionType,
|
||||||
status: "pending",
|
status: "pending",
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -95,19 +95,16 @@ export function InboxCard({
|
|||||||
|
|
||||||
<CardContent className="flex flex-1 flex-col gap-3">
|
<CardContent className="flex flex-1 flex-col gap-3">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
{item.parsedName && <p className="font-medium">{item.parsedName}</p>}
|
{item.originalTitle && (
|
||||||
<p className="line-clamp-2 text-sm text-muted-foreground">
|
<p className="font-medium">{item.originalTitle}</p>
|
||||||
|
)}
|
||||||
|
<p className="whitespace-pre-wrap text-sm text-muted-foreground">
|
||||||
{item.originalText}
|
{item.originalText}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-xs text-muted-foreground">{timeAgo}</span>
|
<span className="text-xs text-muted-foreground">{timeAgo}</span>
|
||||||
{item.parsedCardLastDigits && (
|
|
||||||
<span className="text-xs text-muted-foreground">
|
|
||||||
•••• {item.parsedCardLastDigits}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
|
|||||||
@@ -119,12 +119,6 @@ export function InboxDetailsDialog({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{item.parsedCardLastDigits && (
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-muted-foreground">Cartão</span>
|
|
||||||
<span>•••• {item.parsedCardLastDigits}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-muted-foreground">Tipo</span>
|
<span className="text-muted-foreground">Tipo</span>
|
||||||
<span>{item.parsedTransactionType || "Não identificado"}</span>
|
<span>{item.parsedTransactionType || "Não identificado"}</span>
|
||||||
|
|||||||
@@ -1,29 +1,40 @@
|
|||||||
"use client";
|
"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 { ConfirmActionDialog } from "@/components/confirm-action-dialog";
|
||||||
import { EmptyState } from "@/components/empty-state";
|
import { EmptyState } from "@/components/empty-state";
|
||||||
|
import { LancamentoDialog } from "@/components/lancamentos/dialogs/lancamento-dialog/lancamento-dialog";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { RiInboxLine } from "@remixicon/react";
|
import { RiInboxLine } from "@remixicon/react";
|
||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { InboxCard } from "./inbox-card";
|
import { InboxCard } from "./inbox-card";
|
||||||
import { InboxDetailsDialog } from "./inbox-details-dialog";
|
import { InboxDetailsDialog } from "./inbox-details-dialog";
|
||||||
import { ProcessDialog } from "./process-dialog";
|
|
||||||
import type { InboxItem, SelectOption } from "./types";
|
import type { InboxItem, SelectOption } from "./types";
|
||||||
|
|
||||||
interface InboxPageProps {
|
interface InboxPageProps {
|
||||||
items: InboxItem[];
|
items: InboxItem[];
|
||||||
categorias: SelectOption[];
|
pagadorOptions: SelectOption[];
|
||||||
contas: SelectOption[];
|
splitPagadorOptions: SelectOption[];
|
||||||
cartoes: SelectOption[];
|
defaultPagadorId: string | null;
|
||||||
|
contaOptions: SelectOption[];
|
||||||
|
cartaoOptions: SelectOption[];
|
||||||
|
categoriaOptions: SelectOption[];
|
||||||
|
estabelecimentos: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InboxPage({
|
export function InboxPage({
|
||||||
items,
|
items,
|
||||||
categorias,
|
pagadorOptions,
|
||||||
contas,
|
splitPagadorOptions,
|
||||||
cartoes,
|
defaultPagadorId,
|
||||||
|
contaOptions,
|
||||||
|
cartaoOptions,
|
||||||
|
categoriaOptions,
|
||||||
|
estabelecimentos,
|
||||||
}: InboxPageProps) {
|
}: InboxPageProps) {
|
||||||
const [processOpen, setProcessOpen] = useState(false);
|
const [processOpen, setProcessOpen] = useState(false);
|
||||||
const [itemToProcess, setItemToProcess] = useState<InboxItem | null>(null);
|
const [itemToProcess, setItemToProcess] = useState<InboxItem | null>(null);
|
||||||
@@ -96,6 +107,42 @@ export function InboxPage({
|
|||||||
throw new Error(result.error);
|
throw new Error(result.error);
|
||||||
}, [itemToDiscard]);
|
}, [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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full flex-col gap-6">
|
<div className="flex w-full flex-col gap-6">
|
||||||
@@ -122,13 +169,23 @@ export function InboxPage({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ProcessDialog
|
<LancamentoDialog
|
||||||
|
mode="create"
|
||||||
open={processOpen}
|
open={processOpen}
|
||||||
onOpenChange={handleProcessOpenChange}
|
onOpenChange={handleProcessOpenChange}
|
||||||
item={itemToProcess}
|
pagadorOptions={pagadorOptions}
|
||||||
categorias={categorias}
|
splitPagadorOptions={splitPagadorOptions}
|
||||||
contas={contas}
|
defaultPagadorId={defaultPagadorId}
|
||||||
cartoes={cartoes}
|
contaOptions={contaOptions}
|
||||||
|
cartaoOptions={cartaoOptions}
|
||||||
|
categoriaOptions={categoriaOptions}
|
||||||
|
estabelecimentos={estabelecimentos}
|
||||||
|
defaultPurchaseDate={defaultPurchaseDate}
|
||||||
|
defaultName={defaultName}
|
||||||
|
defaultAmount={defaultAmount}
|
||||||
|
defaultTransactionType={defaultTransactionType}
|
||||||
|
forceShowTransactionType
|
||||||
|
onSuccess={handleLancamentoSuccess}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<InboxDetailsDialog
|
<InboxDetailsDialog
|
||||||
|
|||||||
@@ -1,326 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { processInboxItemAction } from "@/app/(dashboard)/caixa-de-entrada/actions";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import { useCallback, useEffect, useState } from "react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import type { InboxItem, SelectOption } from "./types";
|
|
||||||
|
|
||||||
interface ProcessDialogProps {
|
|
||||||
open: boolean;
|
|
||||||
onOpenChange: (open: boolean) => 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 (
|
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
||||||
<DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-lg">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Processar Notificação</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Revise os dados extraídos e complete as informações para criar o
|
|
||||||
lançamento.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div className="grid gap-4 py-4">
|
|
||||||
{/* Texto original */}
|
|
||||||
<div className="rounded-md bg-muted p-3">
|
|
||||||
<p className="text-xs text-muted-foreground">Notificação original:</p>
|
|
||||||
<p className="mt-1 text-sm">{item.originalText}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Nome/Descrição */}
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label htmlFor="name">Descrição</Label>
|
|
||||||
<Input
|
|
||||||
id="name"
|
|
||||||
value={name}
|
|
||||||
onChange={(e) => setName(e.target.value)}
|
|
||||||
placeholder="Ex: Supermercado, Uber, etc."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Valor e Data */}
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label htmlFor="amount">Valor (R$)</Label>
|
|
||||||
<Input
|
|
||||||
id="amount"
|
|
||||||
type="number"
|
|
||||||
step="0.01"
|
|
||||||
value={amount}
|
|
||||||
onChange={(e) => setAmount(e.target.value)}
|
|
||||||
placeholder="0,00"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label htmlFor="purchaseDate">Data</Label>
|
|
||||||
<Input
|
|
||||||
id="purchaseDate"
|
|
||||||
type="date"
|
|
||||||
value={purchaseDate}
|
|
||||||
onChange={(e) => setPurchaseDate(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Tipo de transação */}
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label>Tipo</Label>
|
|
||||||
<Select
|
|
||||||
value={transactionType}
|
|
||||||
onValueChange={(v) => setTransactionType(v as "Despesa" | "Receita")}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="Despesa">Despesa</SelectItem>
|
|
||||||
<SelectItem value="Receita">Receita</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Forma de pagamento */}
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label>Forma de Pagamento</Label>
|
|
||||||
<Select value={paymentMethod} onValueChange={setPaymentMethod}>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="cartao-credito">Cartão de Crédito</SelectItem>
|
|
||||||
<SelectItem value="cartao-debito">Cartão de Débito</SelectItem>
|
|
||||||
<SelectItem value="pix">PIX</SelectItem>
|
|
||||||
<SelectItem value="dinheiro">Dinheiro</SelectItem>
|
|
||||||
<SelectItem value="transferencia">Transferência</SelectItem>
|
|
||||||
<SelectItem value="boleto">Boleto</SelectItem>
|
|
||||||
<SelectItem value="outros">Outros</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Cartão ou Conta */}
|
|
||||||
{paymentMethod === "cartao-credito" ? (
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label>Cartão</Label>
|
|
||||||
<Select value={cartaoId} onValueChange={setCartaoId}>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Selecione o cartão" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{cartoes.map((cartao) => (
|
|
||||||
<SelectItem key={cartao.id} value={cartao.id}>
|
|
||||||
{cartao.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label>Conta</Label>
|
|
||||||
<Select value={contaId} onValueChange={setContaId}>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Selecione a conta" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{contas.map((conta) => (
|
|
||||||
<SelectItem key={conta.id} value={conta.id}>
|
|
||||||
{conta.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Categoria */}
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label>Categoria</Label>
|
|
||||||
<Select value={categoriaId} onValueChange={setCategoriaId}>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Selecione a categoria" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{filteredCategorias.map((cat) => (
|
|
||||||
<SelectItem key={cat.id} value={cat.id}>
|
|
||||||
{cat.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Condição */}
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label>Condição</Label>
|
|
||||||
<Select value={condition} onValueChange={setCondition}>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="realizado">Realizado</SelectItem>
|
|
||||||
<SelectItem value="aberto">Aberto</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Notas */}
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label htmlFor="note">Observações (opcional)</Label>
|
|
||||||
<Textarea
|
|
||||||
id="note"
|
|
||||||
value={note}
|
|
||||||
onChange={(e) => setNote(e.target.value)}
|
|
||||||
placeholder="Observações adicionais..."
|
|
||||||
rows={2}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
||||||
Cancelar
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleSubmit} disabled={loading}>
|
|
||||||
{loading ? "Processando..." : "Criar Lançamento"}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
* Types for Caixa de Entrada (Inbox) feature
|
* Types for Caixa de Entrada (Inbox) feature
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { SelectOption as LancamentoSelectOption } from "@/components/lancamentos/types";
|
||||||
|
|
||||||
export interface InboxItem {
|
export interface InboxItem {
|
||||||
id: string;
|
id: string;
|
||||||
sourceApp: string;
|
sourceApp: string;
|
||||||
@@ -13,7 +15,6 @@ export interface InboxItem {
|
|||||||
parsedName: string | null;
|
parsedName: string | null;
|
||||||
parsedAmount: string | null;
|
parsedAmount: string | null;
|
||||||
parsedDate: Date | null;
|
parsedDate: Date | null;
|
||||||
parsedCardLastDigits: string | null;
|
|
||||||
parsedTransactionType: string | null;
|
parsedTransactionType: string | null;
|
||||||
status: string;
|
status: string;
|
||||||
lancamentoId: string | null;
|
lancamentoId: string | null;
|
||||||
@@ -43,7 +44,5 @@ export interface DiscardInboxInput {
|
|||||||
reason?: string;
|
reason?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelectOption {
|
// Re-export the lancamentos SelectOption for use in inbox components
|
||||||
id: string;
|
export type SelectOption = LancamentoSelectOption;
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -20,10 +20,16 @@ export interface LancamentoDialogProps {
|
|||||||
defaultCartaoId?: string | null;
|
defaultCartaoId?: string | null;
|
||||||
defaultPaymentMethod?: string | null;
|
defaultPaymentMethod?: string | null;
|
||||||
defaultPurchaseDate?: string | null;
|
defaultPurchaseDate?: string | null;
|
||||||
|
defaultName?: string | null;
|
||||||
|
defaultAmount?: string | null;
|
||||||
lockCartaoSelection?: boolean;
|
lockCartaoSelection?: boolean;
|
||||||
lockPaymentMethod?: boolean;
|
lockPaymentMethod?: boolean;
|
||||||
isImporting?: boolean;
|
isImporting?: boolean;
|
||||||
defaultTransactionType?: "Despesa" | "Receita";
|
defaultTransactionType?: "Despesa" | "Receita";
|
||||||
|
/** Force showing transaction type select even when defaultTransactionType is set */
|
||||||
|
forceShowTransactionType?: boolean;
|
||||||
|
/** Called after successful create/update. Receives the action result. */
|
||||||
|
onSuccess?: () => void;
|
||||||
onBulkEditRequest?: (data: {
|
onBulkEditRequest?: (data: {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -60,10 +60,14 @@ export function LancamentoDialog({
|
|||||||
defaultCartaoId,
|
defaultCartaoId,
|
||||||
defaultPaymentMethod,
|
defaultPaymentMethod,
|
||||||
defaultPurchaseDate,
|
defaultPurchaseDate,
|
||||||
|
defaultName,
|
||||||
|
defaultAmount,
|
||||||
lockCartaoSelection,
|
lockCartaoSelection,
|
||||||
lockPaymentMethod,
|
lockPaymentMethod,
|
||||||
isImporting = false,
|
isImporting = false,
|
||||||
defaultTransactionType,
|
defaultTransactionType,
|
||||||
|
forceShowTransactionType = false,
|
||||||
|
onSuccess,
|
||||||
onBulkEditRequest,
|
onBulkEditRequest,
|
||||||
}: LancamentoDialogProps) {
|
}: LancamentoDialogProps) {
|
||||||
const [dialogOpen, setDialogOpen] = useControlledState(
|
const [dialogOpen, setDialogOpen] = useControlledState(
|
||||||
@@ -77,6 +81,8 @@ export function LancamentoDialog({
|
|||||||
defaultCartaoId,
|
defaultCartaoId,
|
||||||
defaultPaymentMethod,
|
defaultPaymentMethod,
|
||||||
defaultPurchaseDate,
|
defaultPurchaseDate,
|
||||||
|
defaultName,
|
||||||
|
defaultAmount,
|
||||||
defaultTransactionType,
|
defaultTransactionType,
|
||||||
isImporting,
|
isImporting,
|
||||||
}),
|
}),
|
||||||
@@ -96,6 +102,8 @@ export function LancamentoDialog({
|
|||||||
defaultCartaoId,
|
defaultCartaoId,
|
||||||
defaultPaymentMethod,
|
defaultPaymentMethod,
|
||||||
defaultPurchaseDate,
|
defaultPurchaseDate,
|
||||||
|
defaultName,
|
||||||
|
defaultAmount,
|
||||||
defaultTransactionType,
|
defaultTransactionType,
|
||||||
isImporting,
|
isImporting,
|
||||||
},
|
},
|
||||||
@@ -112,6 +120,8 @@ export function LancamentoDialog({
|
|||||||
defaultCartaoId,
|
defaultCartaoId,
|
||||||
defaultPaymentMethod,
|
defaultPaymentMethod,
|
||||||
defaultPurchaseDate,
|
defaultPurchaseDate,
|
||||||
|
defaultName,
|
||||||
|
defaultAmount,
|
||||||
defaultTransactionType,
|
defaultTransactionType,
|
||||||
isImporting,
|
isImporting,
|
||||||
]);
|
]);
|
||||||
@@ -247,6 +257,7 @@ export function LancamentoDialog({
|
|||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
toast.success(result.message);
|
toast.success(result.message);
|
||||||
|
onSuccess?.();
|
||||||
setDialogOpen(false);
|
setDialogOpen(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -290,6 +301,7 @@ export function LancamentoDialog({
|
|||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
toast.success(result.message);
|
toast.success(result.message);
|
||||||
|
onSuccess?.();
|
||||||
setDialogOpen(false);
|
setDialogOpen(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -304,6 +316,7 @@ export function LancamentoDialog({
|
|||||||
lancamento?.id,
|
lancamento?.id,
|
||||||
lancamento?.seriesId,
|
lancamento?.seriesId,
|
||||||
setDialogOpen,
|
setDialogOpen,
|
||||||
|
onSuccess,
|
||||||
onBulkEditRequest,
|
onBulkEditRequest,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -371,7 +384,7 @@ export function LancamentoDialog({
|
|||||||
categoriaOptions={categoriaOptions}
|
categoriaOptions={categoriaOptions}
|
||||||
categoriaGroups={categoriaGroups}
|
categoriaGroups={categoriaGroups}
|
||||||
isUpdateMode={isUpdateMode}
|
isUpdateMode={isUpdateMode}
|
||||||
hideTransactionType={Boolean(isNewWithType)}
|
hideTransactionType={Boolean(isNewWithType) && !forceShowTransactionType}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!isUpdateMode ? (
|
{!isUpdateMode ? (
|
||||||
|
|||||||
@@ -478,7 +478,6 @@ export const inboxItems = pgTable(
|
|||||||
parsedName: text("parsed_name"), // Nome do estabelecimento
|
parsedName: text("parsed_name"), // Nome do estabelecimento
|
||||||
parsedAmount: numeric("parsed_amount", { precision: 12, scale: 2 }),
|
parsedAmount: numeric("parsed_amount", { precision: 12, scale: 2 }),
|
||||||
parsedDate: date("parsed_date", { mode: "date" }),
|
parsedDate: date("parsed_date", { mode: "date" }),
|
||||||
parsedCardLastDigits: text("parsed_card_last_digits"), // Ex: "1234"
|
|
||||||
parsedTransactionType: text("parsed_transaction_type"), // Despesa, Receita
|
parsedTransactionType: text("parsed_transaction_type"), // Despesa, Receita
|
||||||
|
|
||||||
// Status de processamento
|
// Status de processamento
|
||||||
|
|||||||
@@ -71,6 +71,13 @@
|
|||||||
"when": 1768925100873,
|
"when": 1768925100873,
|
||||||
"tag": "0009_add_dashboard_widgets",
|
"tag": "0009_add_dashboard_widgets",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 10,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1769369834242,
|
||||||
|
"tag": "0010_lame_psynapse",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -43,6 +43,8 @@ export type LancamentoFormOverrides = {
|
|||||||
defaultCartaoId?: string | null;
|
defaultCartaoId?: string | null;
|
||||||
defaultPaymentMethod?: string | null;
|
defaultPaymentMethod?: string | null;
|
||||||
defaultPurchaseDate?: string | null;
|
defaultPurchaseDate?: string | null;
|
||||||
|
defaultName?: string | null;
|
||||||
|
defaultAmount?: string | null;
|
||||||
defaultTransactionType?: "Despesa" | "Receita";
|
defaultTransactionType?: "Despesa" | "Receita";
|
||||||
isImporting?: boolean;
|
isImporting?: boolean;
|
||||||
};
|
};
|
||||||
@@ -84,8 +86,8 @@ export function buildLancamentoInitialState(
|
|||||||
: "");
|
: "");
|
||||||
|
|
||||||
// Calcular o valor correto para importação de parcelados
|
// Calcular o valor correto para importação de parcelados
|
||||||
let amountValue = "";
|
let amountValue = overrides?.defaultAmount ?? "";
|
||||||
if (typeof lancamento?.amount === "number") {
|
if (!amountValue && typeof lancamento?.amount === "number") {
|
||||||
let baseAmount = Math.abs(lancamento.amount);
|
let baseAmount = Math.abs(lancamento.amount);
|
||||||
|
|
||||||
// Se está importando e é parcelado, usar o valor total (parcela * quantidade)
|
// Se está importando e é parcelado, usar o valor total (parcela * quantidade)
|
||||||
@@ -106,7 +108,7 @@ export function buildLancamentoInitialState(
|
|||||||
lancamento?.period && /^\d{4}-\d{2}$/.test(lancamento.period)
|
lancamento?.period && /^\d{4}-\d{2}$/.test(lancamento.period)
|
||||||
? lancamento.period
|
? lancamento.period
|
||||||
: fallbackPeriod,
|
: fallbackPeriod,
|
||||||
name: lancamento?.name ?? "",
|
name: lancamento?.name ?? overrides?.defaultName ?? "",
|
||||||
transactionType:
|
transactionType:
|
||||||
lancamento?.transactionType ??
|
lancamento?.transactionType ??
|
||||||
overrides?.defaultTransactionType ??
|
overrides?.defaultTransactionType ??
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ export const inboxItemSchema = z.object({
|
|||||||
parsedName: z.string().optional(),
|
parsedName: z.string().optional(),
|
||||||
parsedAmount: z.coerce.number().optional(),
|
parsedAmount: z.coerce.number().optional(),
|
||||||
parsedDate: z.string().optional().transform((val) => (val ? new Date(val) : undefined)),
|
parsedDate: z.string().optional().transform((val) => (val ? new Date(val) : undefined)),
|
||||||
parsedCardLastDigits: z.string().length(4).optional(),
|
|
||||||
parsedTransactionType: z.enum(["Despesa", "Receita"]).optional(),
|
parsedTransactionType: z.enum(["Despesa", "Receita"]).optional(),
|
||||||
clientId: z.string().optional(), // ID local do app para rastreamento
|
clientId: z.string().optional(), // ID local do app para rastreamento
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user