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:
Felipe Coutinho
2026-01-26 13:31:37 +00:00
parent 18471f2225
commit c0fb11f89c
16 changed files with 203 additions and 421 deletions

View File

@@ -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<typeof processInboxSchema>
/**
* Mark an inbox item as processed after a lancamento was created
*/
export async function markInboxAsProcessedAction(
input: z.infer<typeof markProcessedSchema>
): Promise<ActionResult> {
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);
}

View File

@@ -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<number> {
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,
};
}

View File

@@ -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 (
<main className="flex flex-col items-start gap-6">
<InboxPage
items={items}
categorias={categorias}
contas={contas}
cartoes={cartoes}
pagadorOptions={dialogData.pagadorOptions}
splitPagadorOptions={dialogData.splitPagadorOptions}
defaultPagadorId={dialogData.defaultPagadorId}
contaOptions={dialogData.contaOptions}
cartaoOptions={dialogData.cartaoOptions}
categoriaOptions={dialogData.categoriaOptions}
estabelecimentos={dialogData.estabelecimentos}
/>
</main>
);

View File

@@ -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",
})

View File

@@ -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",
})

View File

@@ -95,19 +95,16 @@ export function InboxCard({
<CardContent className="flex flex-1 flex-col gap-3">
<div className="flex-1">
{item.parsedName && <p className="font-medium">{item.parsedName}</p>}
<p className="line-clamp-2 text-sm text-muted-foreground">
{item.originalTitle && (
<p className="font-medium">{item.originalTitle}</p>
)}
<p className="whitespace-pre-wrap text-sm text-muted-foreground">
{item.originalText}
</p>
</div>
<div className="flex items-center justify-between">
<span className="text-xs text-muted-foreground">{timeAgo}</span>
{item.parsedCardLastDigits && (
<span className="text-xs text-muted-foreground">
{item.parsedCardLastDigits}
</span>
)}
</div>
<div className="flex gap-2">

View File

@@ -119,12 +119,6 @@ export function InboxDetailsDialog({
</span>
</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">
<span className="text-muted-foreground">Tipo</span>
<span>{item.parsedTransactionType || "Não identificado"}</span>

View File

@@ -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<InboxItem | null>(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 (
<>
<div className="flex w-full flex-col gap-6">
@@ -122,13 +169,23 @@ export function InboxPage({
)}
</div>
<ProcessDialog
<LancamentoDialog
mode="create"
open={processOpen}
onOpenChange={handleProcessOpenChange}
item={itemToProcess}
categorias={categorias}
contas={contas}
cartoes={cartoes}
pagadorOptions={pagadorOptions}
splitPagadorOptions={splitPagadorOptions}
defaultPagadorId={defaultPagadorId}
contaOptions={contaOptions}
cartaoOptions={cartaoOptions}
categoriaOptions={categoriaOptions}
estabelecimentos={estabelecimentos}
defaultPurchaseDate={defaultPurchaseDate}
defaultName={defaultName}
defaultAmount={defaultAmount}
defaultTransactionType={defaultTransactionType}
forceShowTransactionType
onSuccess={handleLancamentoSuccess}
/>
<InboxDetailsDialog

View File

@@ -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>
);
}

View File

@@ -2,6 +2,8 @@
* Types for Caixa de Entrada (Inbox) feature
*/
import type { SelectOption as LancamentoSelectOption } from "@/components/lancamentos/types";
export interface InboxItem {
id: string;
sourceApp: string;
@@ -13,7 +15,6 @@ export interface InboxItem {
parsedName: string | null;
parsedAmount: string | null;
parsedDate: Date | null;
parsedCardLastDigits: string | null;
parsedTransactionType: string | null;
status: string;
lancamentoId: string | null;
@@ -43,7 +44,5 @@ export interface DiscardInboxInput {
reason?: string;
}
export interface SelectOption {
id: string;
name: string;
}
// Re-export the lancamentos SelectOption for use in inbox components
export type SelectOption = LancamentoSelectOption;

View File

@@ -20,10 +20,16 @@ export interface LancamentoDialogProps {
defaultCartaoId?: string | null;
defaultPaymentMethod?: string | null;
defaultPurchaseDate?: string | null;
defaultName?: string | null;
defaultAmount?: string | null;
lockCartaoSelection?: boolean;
lockPaymentMethod?: boolean;
isImporting?: boolean;
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: {
id: string;
name: string;

View File

@@ -60,10 +60,14 @@ export function LancamentoDialog({
defaultCartaoId,
defaultPaymentMethod,
defaultPurchaseDate,
defaultName,
defaultAmount,
lockCartaoSelection,
lockPaymentMethod,
isImporting = false,
defaultTransactionType,
forceShowTransactionType = false,
onSuccess,
onBulkEditRequest,
}: LancamentoDialogProps) {
const [dialogOpen, setDialogOpen] = useControlledState(
@@ -77,6 +81,8 @@ export function LancamentoDialog({
defaultCartaoId,
defaultPaymentMethod,
defaultPurchaseDate,
defaultName,
defaultAmount,
defaultTransactionType,
isImporting,
}),
@@ -96,6 +102,8 @@ export function LancamentoDialog({
defaultCartaoId,
defaultPaymentMethod,
defaultPurchaseDate,
defaultName,
defaultAmount,
defaultTransactionType,
isImporting,
},
@@ -112,6 +120,8 @@ export function LancamentoDialog({
defaultCartaoId,
defaultPaymentMethod,
defaultPurchaseDate,
defaultName,
defaultAmount,
defaultTransactionType,
isImporting,
]);
@@ -247,6 +257,7 @@ export function LancamentoDialog({
if (result.success) {
toast.success(result.message);
onSuccess?.();
setDialogOpen(false);
return;
}
@@ -290,6 +301,7 @@ export function LancamentoDialog({
if (result.success) {
toast.success(result.message);
onSuccess?.();
setDialogOpen(false);
return;
}
@@ -304,6 +316,7 @@ export function LancamentoDialog({
lancamento?.id,
lancamento?.seriesId,
setDialogOpen,
onSuccess,
onBulkEditRequest,
],
);
@@ -371,7 +384,7 @@ export function LancamentoDialog({
categoriaOptions={categoriaOptions}
categoriaGroups={categoriaGroups}
isUpdateMode={isUpdateMode}
hideTransactionType={Boolean(isNewWithType)}
hideTransactionType={Boolean(isNewWithType) && !forceShowTransactionType}
/>
{!isUpdateMode ? (

View File

@@ -478,7 +478,6 @@ export const inboxItems = pgTable(
parsedName: text("parsed_name"), // Nome do estabelecimento
parsedAmount: numeric("parsed_amount", { precision: 12, scale: 2 }),
parsedDate: date("parsed_date", { mode: "date" }),
parsedCardLastDigits: text("parsed_card_last_digits"), // Ex: "1234"
parsedTransactionType: text("parsed_transaction_type"), // Despesa, Receita
// Status de processamento

View File

@@ -71,6 +71,13 @@
"when": 1768925100873,
"tag": "0009_add_dashboard_widgets",
"breakpoints": true
},
{
"idx": 10,
"version": "7",
"when": 1769369834242,
"tag": "0010_lame_psynapse",
"breakpoints": true
}
]
}

View File

@@ -43,6 +43,8 @@ export type LancamentoFormOverrides = {
defaultCartaoId?: string | null;
defaultPaymentMethod?: string | null;
defaultPurchaseDate?: string | null;
defaultName?: string | null;
defaultAmount?: string | null;
defaultTransactionType?: "Despesa" | "Receita";
isImporting?: boolean;
};
@@ -84,8 +86,8 @@ export function buildLancamentoInitialState(
: "");
// Calcular o valor correto para importação de parcelados
let amountValue = "";
if (typeof lancamento?.amount === "number") {
let amountValue = overrides?.defaultAmount ?? "";
if (!amountValue && typeof lancamento?.amount === "number") {
let baseAmount = Math.abs(lancamento.amount);
// 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
: fallbackPeriod,
name: lancamento?.name ?? "",
name: lancamento?.name ?? overrides?.defaultName ?? "",
transactionType:
lancamento?.transactionType ??
overrides?.defaultTransactionType ??

View File

@@ -14,7 +14,6 @@ export const inboxItemSchema = z.object({
parsedName: z.string().optional(),
parsedAmount: z.coerce.number().optional(),
parsedDate: z.string().optional().transform((val) => (val ? new Date(val) : undefined)),
parsedCardLastDigits: z.string().length(4).optional(),
parsedTransactionType: z.enum(["Despesa", "Receita"]).optional(),
clientId: z.string().optional(), // ID local do app para rastreamento
});