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:
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user