arcadiasuite/client/src/pages/Financeiro.tsx

1463 lines
59 KiB
TypeScript

import { BrowserFrame } from "@/components/Browser/BrowserFrame";
import { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { ScrollArea } from "@/components/ui/scroll-area";
import { useToast } from "@/hooks/use-toast";
import { useAuth } from "@/hooks/use-auth";
import {
Wallet,
Building2,
ArrowUpCircle,
ArrowDownCircle,
TrendingUp,
Plus,
Edit,
Trash2,
DollarSign,
CreditCard,
Banknote,
CalendarDays,
Receipt,
Clock,
CheckCircle,
XCircle,
AlertTriangle,
ArrowRightLeft,
Settings,
PiggyBank,
ChevronRight
} from "lucide-react";
import { format } from "date-fns";
import { ptBR } from "date-fns/locale";
type TabType = "dashboard" | "payables" | "receivables" | "accounts" | "transactions" | "settings";
interface BankAccount {
id: number;
code: string;
name: string;
bankCode: string;
bankName: string;
agency: string;
accountNumber: string;
accountType: string;
initialBalance: string;
currentBalance: string;
isActive: boolean;
}
interface PaymentMethod {
id: number;
code: string;
name: string;
type: string;
fee: string;
daysToReceive: number;
isActive: boolean;
}
interface PaymentPlan {
id: number;
code: string;
name: string;
installments: number;
intervalDays: number;
firstDueDays: number;
discountPercent: string;
interestPercent: string;
isActive: boolean;
}
interface Category {
id: number;
code: string;
name: string;
type: string;
isActive: boolean;
}
interface Payable {
id: number;
documentNumber: string;
supplierName: string;
description: string;
issueDate: string;
dueDate: string;
originalAmount: string;
paidAmount: string;
remainingAmount: string;
status: string;
}
interface Receivable {
id: number;
documentNumber: string;
customerName: string;
description: string;
issueDate: string;
dueDate: string;
originalAmount: string;
receivedAmount: string;
remainingAmount: string;
status: string;
}
interface Transaction {
id: number;
bankAccountId: number;
type: string;
amount: string;
balanceAfter: string;
transactionDate: string;
description: string;
}
interface Dashboard {
totalBalance: number;
bankAccounts: number;
totalPayables: number;
payablesCount: number;
overduePayables: number;
totalReceivables: number;
receivablesCount: number;
overdueReceivables: number;
projectedBalance: number;
}
const api = {
get: async (url: string) => {
const res = await fetch(url, { credentials: "include" });
if (!res.ok) throw new Error("Request failed");
return res.json();
},
post: async (url: string, data: any) => {
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
credentials: "include",
});
if (!res.ok) throw new Error("Request failed");
return res.json();
},
put: async (url: string, data: any) => {
const res = await fetch(url, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
credentials: "include",
});
if (!res.ok) throw new Error("Request failed");
return res.json();
},
delete: async (url: string) => {
const res = await fetch(url, { method: "DELETE", credentials: "include" });
if (!res.ok) throw new Error("Request failed");
},
};
const formatCurrency = (value: string | number) => {
const num = typeof value === "string" ? parseFloat(value) : value;
return new Intl.NumberFormat("pt-BR", { style: "currency", currency: "BRL" }).format(num || 0);
};
const formatDate = (date: string) => {
if (!date) return "-";
return format(new Date(date), "dd/MM/yyyy", { locale: ptBR });
};
const getStatusBadge = (status: string) => {
const styles: Record<string, { variant: "default" | "secondary" | "destructive" | "outline"; label: string }> = {
pending: { variant: "outline", label: "Pendente" },
partial: { variant: "secondary", label: "Parcial" },
paid: { variant: "default", label: "Pago" },
received: { variant: "default", label: "Recebido" },
overdue: { variant: "destructive", label: "Vencido" },
cancelled: { variant: "secondary", label: "Cancelado" },
};
const style = styles[status] || styles.pending;
return <Badge variant={style.variant}>{style.label}</Badge>;
};
export default function Financeiro() {
const { user } = useAuth();
const { toast } = useToast();
const queryClient = useQueryClient();
const [activeTab, setActiveTab] = useState<TabType>("dashboard");
const [showAccountDialog, setShowAccountDialog] = useState(false);
const [showPayableDialog, setShowPayableDialog] = useState(false);
const [showReceivableDialog, setShowReceivableDialog] = useState(false);
const [showPaymentMethodDialog, setShowPaymentMethodDialog] = useState(false);
const [showPaymentPlanDialog, setShowPaymentPlanDialog] = useState(false);
const [showCategoryDialog, setShowCategoryDialog] = useState(false);
const [showTransferDialog, setShowTransferDialog] = useState(false);
const [showPayDialog, setShowPayDialog] = useState(false);
const [showReceiveDialog, setShowReceiveDialog] = useState(false);
const [editingItem, setEditingItem] = useState<any>(null);
const [selectedItem, setSelectedItem] = useState<any>(null);
const { data: dashboard } = useQuery<Dashboard>({
queryKey: ["/api/financeiro/dashboard"],
queryFn: () => api.get("/api/financeiro/dashboard"),
});
const { data: bankAccounts = [] } = useQuery<BankAccount[]>({
queryKey: ["/api/financeiro/bank-accounts"],
queryFn: () => api.get("/api/financeiro/bank-accounts"),
});
const { data: paymentMethods = [] } = useQuery<PaymentMethod[]>({
queryKey: ["/api/financeiro/payment-methods"],
queryFn: () => api.get("/api/financeiro/payment-methods"),
});
const { data: paymentPlans = [] } = useQuery<PaymentPlan[]>({
queryKey: ["/api/financeiro/payment-plans"],
queryFn: () => api.get("/api/financeiro/payment-plans"),
});
const { data: categories = [] } = useQuery<Category[]>({
queryKey: ["/api/financeiro/categories"],
queryFn: () => api.get("/api/financeiro/categories"),
});
const { data: payables = [] } = useQuery<Payable[]>({
queryKey: ["/api/financeiro/payables"],
queryFn: () => api.get("/api/financeiro/payables"),
});
const { data: receivables = [] } = useQuery<Receivable[]>({
queryKey: ["/api/financeiro/receivables"],
queryFn: () => api.get("/api/financeiro/receivables"),
});
const { data: transactions = [] } = useQuery<Transaction[]>({
queryKey: ["/api/financeiro/transactions"],
queryFn: () => api.get("/api/financeiro/transactions"),
});
const createAccountMutation = useMutation({
mutationFn: (data: any) => api.post("/api/financeiro/bank-accounts", data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/financeiro/bank-accounts"] });
queryClient.invalidateQueries({ queryKey: ["/api/financeiro/dashboard"] });
setShowAccountDialog(false);
setEditingItem(null);
toast({ title: "Conta bancária criada!" });
},
onError: () => toast({ title: "Erro ao criar conta", variant: "destructive" }),
});
const updateAccountMutation = useMutation({
mutationFn: ({ id, data }: { id: number; data: any }) => api.put(`/api/financeiro/bank-accounts/${id}`, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/financeiro/bank-accounts"] });
setShowAccountDialog(false);
setEditingItem(null);
toast({ title: "Conta atualizada!" });
},
});
const deleteAccountMutation = useMutation({
mutationFn: (id: number) => api.delete(`/api/financeiro/bank-accounts/${id}`),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/financeiro/bank-accounts"] });
queryClient.invalidateQueries({ queryKey: ["/api/financeiro/dashboard"] });
toast({ title: "Conta excluída!" });
},
});
const createPayableMutation = useMutation({
mutationFn: (data: any) => api.post("/api/financeiro/payables", data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/financeiro/payables"] });
queryClient.invalidateQueries({ queryKey: ["/api/financeiro/dashboard"] });
setShowPayableDialog(false);
setEditingItem(null);
toast({ title: "Conta a pagar criada!" });
},
onError: () => toast({ title: "Erro ao criar conta a pagar", variant: "destructive" }),
});
const createReceivableMutation = useMutation({
mutationFn: (data: any) => api.post("/api/financeiro/receivables", data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/financeiro/receivables"] });
queryClient.invalidateQueries({ queryKey: ["/api/financeiro/dashboard"] });
setShowReceivableDialog(false);
setEditingItem(null);
toast({ title: "Conta a receber criada!" });
},
onError: () => toast({ title: "Erro ao criar conta a receber", variant: "destructive" }),
});
const payMutation = useMutation({
mutationFn: ({ id, data }: { id: number; data: any }) => api.post(`/api/financeiro/payables/${id}/pay`, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/financeiro/payables"] });
queryClient.invalidateQueries({ queryKey: ["/api/financeiro/bank-accounts"] });
queryClient.invalidateQueries({ queryKey: ["/api/financeiro/transactions"] });
queryClient.invalidateQueries({ queryKey: ["/api/financeiro/dashboard"] });
setShowPayDialog(false);
setSelectedItem(null);
toast({ title: "Pagamento registrado!" });
},
onError: () => toast({ title: "Erro ao registrar pagamento", variant: "destructive" }),
});
const receiveMutation = useMutation({
mutationFn: ({ id, data }: { id: number; data: any }) => api.post(`/api/financeiro/receivables/${id}/receive`, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/financeiro/receivables"] });
queryClient.invalidateQueries({ queryKey: ["/api/financeiro/bank-accounts"] });
queryClient.invalidateQueries({ queryKey: ["/api/financeiro/transactions"] });
queryClient.invalidateQueries({ queryKey: ["/api/financeiro/dashboard"] });
setShowReceiveDialog(false);
setSelectedItem(null);
toast({ title: "Recebimento registrado!" });
},
onError: () => toast({ title: "Erro ao registrar recebimento", variant: "destructive" }),
});
const transferMutation = useMutation({
mutationFn: (data: any) => api.post("/api/financeiro/transfers", data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/financeiro/bank-accounts"] });
queryClient.invalidateQueries({ queryKey: ["/api/financeiro/transactions"] });
queryClient.invalidateQueries({ queryKey: ["/api/financeiro/dashboard"] });
setShowTransferDialog(false);
toast({ title: "Transferência realizada!" });
},
onError: () => toast({ title: "Erro na transferência", variant: "destructive" }),
});
const createPaymentMethodMutation = useMutation({
mutationFn: (data: any) => api.post("/api/financeiro/payment-methods", data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/financeiro/payment-methods"] });
setShowPaymentMethodDialog(false);
toast({ title: "Meio de pagamento criado!" });
},
});
const createPaymentPlanMutation = useMutation({
mutationFn: (data: any) => api.post("/api/financeiro/payment-plans", data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/financeiro/payment-plans"] });
setShowPaymentPlanDialog(false);
toast({ title: "Plano de pagamento criado!" });
},
});
const createCategoryMutation = useMutation({
mutationFn: (data: any) => api.post("/api/financeiro/categories", data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/financeiro/categories"] });
setShowCategoryDialog(false);
toast({ title: "Categoria criada!" });
},
});
const handleSaveAccount = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const form = e.currentTarget;
const formData = new FormData(form);
const data = {
code: formData.get("code"),
name: formData.get("name"),
bankCode: formData.get("bankCode"),
bankName: formData.get("bankName"),
agency: formData.get("agency"),
accountNumber: formData.get("accountNumber"),
accountType: formData.get("accountType"),
initialBalance: formData.get("initialBalance") || "0",
};
if (editingItem) {
updateAccountMutation.mutate({ id: editingItem.id, data });
} else {
createAccountMutation.mutate(data);
}
};
const handleSavePayable = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const form = e.currentTarget;
const formData = new FormData(form);
const data = {
documentNumber: formData.get("documentNumber"),
supplierName: formData.get("supplierName"),
description: formData.get("description"),
issueDate: formData.get("issueDate"),
dueDate: formData.get("dueDate"),
originalAmount: formData.get("originalAmount"),
categoryId: formData.get("categoryId") ? parseInt(formData.get("categoryId") as string) : null,
};
createPayableMutation.mutate(data);
};
const handleSaveReceivable = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const form = e.currentTarget;
const formData = new FormData(form);
const data = {
documentNumber: formData.get("documentNumber"),
customerName: formData.get("customerName"),
description: formData.get("description"),
issueDate: formData.get("issueDate"),
dueDate: formData.get("dueDate"),
originalAmount: formData.get("originalAmount"),
categoryId: formData.get("categoryId") ? parseInt(formData.get("categoryId") as string) : null,
};
createReceivableMutation.mutate(data);
};
const handlePay = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const form = e.currentTarget;
const formData = new FormData(form);
const data = {
amount: formData.get("amount"),
bankAccountId: parseInt(formData.get("bankAccountId") as string),
paymentDate: formData.get("paymentDate"),
};
payMutation.mutate({ id: selectedItem.id, data });
};
const handleReceive = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const form = e.currentTarget;
const formData = new FormData(form);
const data = {
amount: formData.get("amount"),
bankAccountId: parseInt(formData.get("bankAccountId") as string),
receiveDate: formData.get("receiveDate"),
};
receiveMutation.mutate({ id: selectedItem.id, data });
};
const handleTransfer = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const form = e.currentTarget;
const formData = new FormData(form);
const data = {
fromAccountId: parseInt(formData.get("fromAccountId") as string),
toAccountId: parseInt(formData.get("toAccountId") as string),
amount: formData.get("amount"),
description: formData.get("description"),
transferDate: formData.get("transferDate"),
};
transferMutation.mutate(data);
};
const renderDashboard = () => (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Saldo Total</CardTitle>
<Wallet className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-600">{formatCurrency(dashboard?.totalBalance || 0)}</div>
<p className="text-xs text-muted-foreground">{dashboard?.bankAccounts || 0} contas ativas</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">A Pagar</CardTitle>
<ArrowDownCircle className="h-4 w-4 text-red-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-red-600">{formatCurrency(dashboard?.totalPayables || 0)}</div>
<p className="text-xs text-muted-foreground">
{dashboard?.payablesCount || 0} títulos pendentes
{(dashboard?.overduePayables || 0) > 0 && (
<span className="text-red-500 ml-1">({dashboard?.overduePayables} vencidos)</span>
)}
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">A Receber</CardTitle>
<ArrowUpCircle className="h-4 w-4 text-green-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-blue-600">{formatCurrency(dashboard?.totalReceivables || 0)}</div>
<p className="text-xs text-muted-foreground">
{dashboard?.receivablesCount || 0} títulos pendentes
{(dashboard?.overdueReceivables || 0) > 0 && (
<span className="text-orange-500 ml-1">({dashboard?.overdueReceivables} vencidos)</span>
)}
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Saldo Projetado</CardTitle>
<TrendingUp className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className={`text-2xl font-bold ${(dashboard?.projectedBalance || 0) >= 0 ? "text-green-600" : "text-red-600"}`}>
{formatCurrency(dashboard?.projectedBalance || 0)}
</div>
<p className="text-xs text-muted-foreground">Saldo + Receber - Pagar</p>
</CardContent>
</Card>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Building2 className="h-5 w-5" />
Contas Bancárias
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
{bankAccounts.slice(0, 5).map((account) => (
<div key={account.id} className="flex items-center justify-between p-3 bg-muted/50 rounded-lg">
<div>
<p className="font-medium">{account.name}</p>
<p className="text-sm text-muted-foreground">{account.bankName}</p>
</div>
<div className="text-right">
<p className={`font-bold ${parseFloat(account.currentBalance) >= 0 ? "text-green-600" : "text-red-600"}`}>
{formatCurrency(account.currentBalance)}
</p>
</div>
</div>
))}
{bankAccounts.length === 0 && (
<p className="text-center text-muted-foreground py-4">Nenhuma conta cadastrada</p>
)}
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Clock className="h-5 w-5" />
Próximos Vencimentos
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
{[...payables.filter(p => p.status === "pending").slice(0, 3).map(p => ({ ...p, tipo: "pagar" })),
...receivables.filter(r => r.status === "pending").slice(0, 3).map(r => ({ ...r, tipo: "receber" }))]
.sort((a, b) => new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime())
.slice(0, 5)
.map((item: any, idx) => (
<div key={idx} className="flex items-center justify-between p-3 bg-muted/50 rounded-lg">
<div className="flex items-center gap-3">
{item.tipo === "pagar" ? (
<ArrowDownCircle className="h-4 w-4 text-red-500" />
) : (
<ArrowUpCircle className="h-4 w-4 text-green-500" />
)}
<div>
<p className="font-medium text-sm">{item.supplierName || item.customerName}</p>
<p className="text-xs text-muted-foreground">{formatDate(item.dueDate)}</p>
</div>
</div>
<p className={`font-bold ${item.tipo === "pagar" ? "text-red-600" : "text-green-600"}`}>
{formatCurrency(item.remainingAmount)}
</p>
</div>
))}
{payables.length === 0 && receivables.length === 0 && (
<p className="text-center text-muted-foreground py-4">Sem vencimentos pendentes</p>
)}
</div>
</CardContent>
</Card>
</div>
</div>
);
const renderPayables = () => (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="text-lg font-semibold">Contas a Pagar</h3>
<Button onClick={() => setShowPayableDialog(true)} data-testid="button-add-payable">
<Plus className="h-4 w-4 mr-2" /> Nova Conta
</Button>
</div>
<Card>
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead>Documento</TableHead>
<TableHead>Fornecedor</TableHead>
<TableHead>Vencimento</TableHead>
<TableHead className="text-right">Valor</TableHead>
<TableHead className="text-right">Pago</TableHead>
<TableHead className="text-right">Saldo</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Ações</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{payables.map((item) => (
<TableRow key={item.id} data-testid={`row-payable-${item.id}`}>
<TableCell className="font-medium">{item.documentNumber || "-"}</TableCell>
<TableCell>{item.supplierName}</TableCell>
<TableCell>{formatDate(item.dueDate)}</TableCell>
<TableCell className="text-right">{formatCurrency(item.originalAmount)}</TableCell>
<TableCell className="text-right">{formatCurrency(item.paidAmount)}</TableCell>
<TableCell className="text-right font-bold">{formatCurrency(item.remainingAmount)}</TableCell>
<TableCell>{getStatusBadge(item.status)}</TableCell>
<TableCell className="text-right">
{item.status !== "paid" && item.status !== "cancelled" && (
<Button
variant="outline"
size="sm"
onClick={() => { setSelectedItem(item); setShowPayDialog(true); }}
data-testid={`button-pay-${item.id}`}
>
<DollarSign className="h-4 w-4 mr-1" /> Pagar
</Button>
)}
</TableCell>
</TableRow>
))}
{payables.length === 0 && (
<TableRow>
<TableCell colSpan={8} className="text-center py-8 text-muted-foreground">
Nenhuma conta a pagar cadastrada
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</CardContent>
</Card>
</div>
);
const renderReceivables = () => (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="text-lg font-semibold">Contas a Receber</h3>
<Button onClick={() => setShowReceivableDialog(true)} data-testid="button-add-receivable">
<Plus className="h-4 w-4 mr-2" /> Nova Conta
</Button>
</div>
<Card>
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead>Documento</TableHead>
<TableHead>Cliente</TableHead>
<TableHead>Vencimento</TableHead>
<TableHead className="text-right">Valor</TableHead>
<TableHead className="text-right">Recebido</TableHead>
<TableHead className="text-right">Saldo</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Ações</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{receivables.map((item) => (
<TableRow key={item.id} data-testid={`row-receivable-${item.id}`}>
<TableCell className="font-medium">{item.documentNumber || "-"}</TableCell>
<TableCell>{item.customerName}</TableCell>
<TableCell>{formatDate(item.dueDate)}</TableCell>
<TableCell className="text-right">{formatCurrency(item.originalAmount)}</TableCell>
<TableCell className="text-right">{formatCurrency(item.receivedAmount)}</TableCell>
<TableCell className="text-right font-bold">{formatCurrency(item.remainingAmount)}</TableCell>
<TableCell>{getStatusBadge(item.status)}</TableCell>
<TableCell className="text-right">
{item.status !== "received" && item.status !== "cancelled" && (
<Button
variant="outline"
size="sm"
onClick={() => { setSelectedItem(item); setShowReceiveDialog(true); }}
data-testid={`button-receive-${item.id}`}
>
<DollarSign className="h-4 w-4 mr-1" /> Receber
</Button>
)}
</TableCell>
</TableRow>
))}
{receivables.length === 0 && (
<TableRow>
<TableCell colSpan={8} className="text-center py-8 text-muted-foreground">
Nenhuma conta a receber cadastrada
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</CardContent>
</Card>
</div>
);
const renderAccounts = () => (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="text-lg font-semibold">Contas Bancárias</h3>
<div className="flex gap-2">
<Button variant="outline" onClick={() => setShowTransferDialog(true)} data-testid="button-transfer">
<ArrowRightLeft className="h-4 w-4 mr-2" /> Transferência
</Button>
<Button onClick={() => { setEditingItem(null); setShowAccountDialog(true); }} data-testid="button-add-account">
<Plus className="h-4 w-4 mr-2" /> Nova Conta
</Button>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{bankAccounts.map((account) => (
<Card key={account.id} data-testid={`card-account-${account.id}`}>
<CardHeader className="pb-2">
<div className="flex justify-between items-start">
<div>
<CardTitle className="text-lg">{account.name}</CardTitle>
<CardDescription>{account.bankName}</CardDescription>
</div>
<div className="flex gap-1">
<Button variant="ghost" size="icon" onClick={() => { setEditingItem(account); setShowAccountDialog(true); }}>
<Edit className="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon" onClick={() => deleteAccountMutation.mutate(account.id)}>
<Trash2 className="h-4 w-4 text-red-500" />
</Button>
</div>
</div>
</CardHeader>
<CardContent>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Agência:</span>
<span>{account.agency}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Conta:</span>
<span>{account.accountNumber}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Tipo:</span>
<span>{account.accountType === "checking" ? "Corrente" : account.accountType === "savings" ? "Poupança" : "Investimento"}</span>
</div>
<div className="pt-2 border-t">
<div className="flex justify-between items-center">
<span className="font-medium">Saldo Atual:</span>
<span className={`text-xl font-bold ${parseFloat(account.currentBalance) >= 0 ? "text-green-600" : "text-red-600"}`}>
{formatCurrency(account.currentBalance)}
</span>
</div>
</div>
</div>
</CardContent>
</Card>
))}
{bankAccounts.length === 0 && (
<Card className="col-span-full">
<CardContent className="py-8 text-center text-muted-foreground">
Nenhuma conta bancária cadastrada
</CardContent>
</Card>
)}
</div>
</div>
);
const renderTransactions = () => (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="text-lg font-semibold">Extrato de Movimentações</h3>
</div>
<Card>
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead>Data</TableHead>
<TableHead>Descrição</TableHead>
<TableHead>Tipo</TableHead>
<TableHead className="text-right">Valor</TableHead>
<TableHead className="text-right">Saldo</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{transactions.map((tx) => (
<TableRow key={tx.id}>
<TableCell>{formatDate(tx.transactionDate)}</TableCell>
<TableCell>{tx.description || "-"}</TableCell>
<TableCell>
{tx.type === "credit" ? (
<Badge variant="default" className="bg-green-600">Entrada</Badge>
) : (
<Badge variant="destructive">Saída</Badge>
)}
</TableCell>
<TableCell className={`text-right font-medium ${tx.type === "credit" ? "text-green-600" : "text-red-600"}`}>
{tx.type === "credit" ? "+" : "-"}{formatCurrency(tx.amount)}
</TableCell>
<TableCell className="text-right">{formatCurrency(tx.balanceAfter)}</TableCell>
</TableRow>
))}
{transactions.length === 0 && (
<TableRow>
<TableCell colSpan={5} className="text-center py-8 text-muted-foreground">
Nenhuma movimentação registrada
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</CardContent>
</Card>
</div>
);
const renderSettings = () => (
<div className="space-y-6">
<div>
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-semibold">Meios de Pagamento</h3>
<Button variant="outline" onClick={() => setShowPaymentMethodDialog(true)} data-testid="button-add-payment-method">
<Plus className="h-4 w-4 mr-2" /> Novo
</Button>
</div>
<Card>
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead>Código</TableHead>
<TableHead>Nome</TableHead>
<TableHead>Tipo</TableHead>
<TableHead>Taxa</TableHead>
<TableHead>Dias p/ Receber</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{paymentMethods.map((method) => (
<TableRow key={method.id}>
<TableCell>{method.code}</TableCell>
<TableCell>{method.name}</TableCell>
<TableCell>{method.type}</TableCell>
<TableCell>{method.fee}%</TableCell>
<TableCell>{method.daysToReceive}</TableCell>
<TableCell>
<Badge variant={method.isActive ? "default" : "secondary"}>
{method.isActive ? "Ativo" : "Inativo"}
</Badge>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</div>
<div>
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-semibold">Planos de Pagamento</h3>
<Button variant="outline" onClick={() => setShowPaymentPlanDialog(true)} data-testid="button-add-payment-plan">
<Plus className="h-4 w-4 mr-2" /> Novo
</Button>
</div>
<Card>
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead>Código</TableHead>
<TableHead>Nome</TableHead>
<TableHead>Parcelas</TableHead>
<TableHead>Intervalo</TableHead>
<TableHead>1ª Parcela</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{paymentPlans.map((plan) => (
<TableRow key={plan.id}>
<TableCell>{plan.code}</TableCell>
<TableCell>{plan.name}</TableCell>
<TableCell>{plan.installments}x</TableCell>
<TableCell>{plan.intervalDays} dias</TableCell>
<TableCell>{plan.firstDueDays} dias</TableCell>
<TableCell>
<Badge variant={plan.isActive ? "default" : "secondary"}>
{plan.isActive ? "Ativo" : "Inativo"}
</Badge>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</div>
<div>
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-semibold">Categorias de Fluxo de Caixa</h3>
<Button variant="outline" onClick={() => setShowCategoryDialog(true)} data-testid="button-add-category">
<Plus className="h-4 w-4 mr-2" /> Nova
</Button>
</div>
<Card>
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead>Código</TableHead>
<TableHead>Nome</TableHead>
<TableHead>Tipo</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{categories.map((cat) => (
<TableRow key={cat.id}>
<TableCell>{cat.code}</TableCell>
<TableCell>{cat.name}</TableCell>
<TableCell>
<Badge variant={cat.type === "income" ? "default" : "destructive"}>
{cat.type === "income" ? "Receita" : "Despesa"}
</Badge>
</TableCell>
<TableCell>
<Badge variant={cat.isActive ? "default" : "secondary"}>
{cat.isActive ? "Ativo" : "Inativo"}
</Badge>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</div>
</div>
);
return (
<BrowserFrame title="Arcádia Financeiro" currentPath="/financeiro">
<div className="p-6">
<div className="mb-6">
<h1 className="text-3xl font-bold flex items-center gap-3">
<PiggyBank className="h-8 w-8 text-green-600" />
Arcádia Financeiro
</h1>
<p className="text-muted-foreground mt-1">
Gestão financeira completa: contas a pagar/receber, fluxo de caixa e controle bancário
</p>
</div>
<Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as TabType)} className="space-y-4">
<TabsList className="grid grid-cols-6 w-full max-w-4xl">
<TabsTrigger value="dashboard" data-testid="tab-dashboard">Dashboard</TabsTrigger>
<TabsTrigger value="payables" data-testid="tab-payables">A Pagar</TabsTrigger>
<TabsTrigger value="receivables" data-testid="tab-receivables">A Receber</TabsTrigger>
<TabsTrigger value="accounts" data-testid="tab-accounts">Contas</TabsTrigger>
<TabsTrigger value="transactions" data-testid="tab-transactions">Extrato</TabsTrigger>
<TabsTrigger value="settings" data-testid="tab-settings">Configurações</TabsTrigger>
</TabsList>
<TabsContent value="dashboard">{renderDashboard()}</TabsContent>
<TabsContent value="payables">{renderPayables()}</TabsContent>
<TabsContent value="receivables">{renderReceivables()}</TabsContent>
<TabsContent value="accounts">{renderAccounts()}</TabsContent>
<TabsContent value="transactions">{renderTransactions()}</TabsContent>
<TabsContent value="settings">{renderSettings()}</TabsContent>
</Tabs>
<Dialog open={showAccountDialog} onOpenChange={setShowAccountDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>{editingItem ? "Editar Conta Bancária" : "Nova Conta Bancária"}</DialogTitle>
</DialogHeader>
<form onSubmit={handleSaveAccount} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label>Código</Label>
<Input name="code" defaultValue={editingItem?.code} required />
</div>
<div>
<Label>Nome</Label>
<Input name="name" defaultValue={editingItem?.name} required />
</div>
<div>
<Label>Código do Banco</Label>
<Input name="bankCode" defaultValue={editingItem?.bankCode} />
</div>
<div>
<Label>Nome do Banco</Label>
<Input name="bankName" defaultValue={editingItem?.bankName} />
</div>
<div>
<Label>Agência</Label>
<Input name="agency" defaultValue={editingItem?.agency} />
</div>
<div>
<Label>Número da Conta</Label>
<Input name="accountNumber" defaultValue={editingItem?.accountNumber} />
</div>
<div>
<Label>Tipo de Conta</Label>
<Select name="accountType" defaultValue={editingItem?.accountType || "checking"}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="checking">Corrente</SelectItem>
<SelectItem value="savings">Poupança</SelectItem>
<SelectItem value="investment">Investimento</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label>Saldo Inicial</Label>
<Input name="initialBalance" type="number" step="0.01" defaultValue={editingItem?.initialBalance || "0"} />
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setShowAccountDialog(false)}>Cancelar</Button>
<Button type="submit">Salvar</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
<Dialog open={showPayableDialog} onOpenChange={setShowPayableDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>Nova Conta a Pagar</DialogTitle>
</DialogHeader>
<form onSubmit={handleSavePayable} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label>Documento</Label>
<Input name="documentNumber" />
</div>
<div>
<Label>Fornecedor</Label>
<Input name="supplierName" required />
</div>
<div className="col-span-2">
<Label>Descrição</Label>
<Input name="description" />
</div>
<div>
<Label>Data de Emissão</Label>
<Input name="issueDate" type="date" required />
</div>
<div>
<Label>Vencimento</Label>
<Input name="dueDate" type="date" required />
</div>
<div>
<Label>Valor</Label>
<Input name="originalAmount" type="number" step="0.01" required />
</div>
<div>
<Label>Categoria</Label>
<Select name="categoryId">
<SelectTrigger>
<SelectValue placeholder="Selecione" />
</SelectTrigger>
<SelectContent>
{categories.filter(c => c.type === "expense").map(c => (
<SelectItem key={c.id} value={c.id.toString()}>{c.name}</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setShowPayableDialog(false)}>Cancelar</Button>
<Button type="submit">Salvar</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
<Dialog open={showReceivableDialog} onOpenChange={setShowReceivableDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>Nova Conta a Receber</DialogTitle>
</DialogHeader>
<form onSubmit={handleSaveReceivable} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label>Documento</Label>
<Input name="documentNumber" />
</div>
<div>
<Label>Cliente</Label>
<Input name="customerName" required />
</div>
<div className="col-span-2">
<Label>Descrição</Label>
<Input name="description" />
</div>
<div>
<Label>Data de Emissão</Label>
<Input name="issueDate" type="date" required />
</div>
<div>
<Label>Vencimento</Label>
<Input name="dueDate" type="date" required />
</div>
<div>
<Label>Valor</Label>
<Input name="originalAmount" type="number" step="0.01" required />
</div>
<div>
<Label>Categoria</Label>
<Select name="categoryId">
<SelectTrigger>
<SelectValue placeholder="Selecione" />
</SelectTrigger>
<SelectContent>
{categories.filter(c => c.type === "income").map(c => (
<SelectItem key={c.id} value={c.id.toString()}>{c.name}</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setShowReceivableDialog(false)}>Cancelar</Button>
<Button type="submit">Salvar</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
<Dialog open={showPayDialog} onOpenChange={setShowPayDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>Registrar Pagamento</DialogTitle>
</DialogHeader>
{selectedItem && (
<form onSubmit={handlePay} className="space-y-4">
<div className="p-4 bg-muted rounded-lg">
<p className="font-medium">{selectedItem.supplierName}</p>
<p className="text-sm text-muted-foreground">{selectedItem.description}</p>
<p className="text-lg font-bold mt-2">Saldo: {formatCurrency(selectedItem.remainingAmount)}</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label>Valor a Pagar</Label>
<Input name="amount" type="number" step="0.01" defaultValue={selectedItem.remainingAmount} required />
</div>
<div>
<Label>Data do Pagamento</Label>
<Input name="paymentDate" type="date" defaultValue={new Date().toISOString().split('T')[0]} required />
</div>
<div className="col-span-2">
<Label>Conta Bancária</Label>
<Select name="bankAccountId" required>
<SelectTrigger>
<SelectValue placeholder="Selecione a conta" />
</SelectTrigger>
<SelectContent>
{bankAccounts.map(acc => (
<SelectItem key={acc.id} value={acc.id.toString()}>
{acc.name} ({formatCurrency(acc.currentBalance)})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setShowPayDialog(false)}>Cancelar</Button>
<Button type="submit">Confirmar Pagamento</Button>
</DialogFooter>
</form>
)}
</DialogContent>
</Dialog>
<Dialog open={showReceiveDialog} onOpenChange={setShowReceiveDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>Registrar Recebimento</DialogTitle>
</DialogHeader>
{selectedItem && (
<form onSubmit={handleReceive} className="space-y-4">
<div className="p-4 bg-muted rounded-lg">
<p className="font-medium">{selectedItem.customerName}</p>
<p className="text-sm text-muted-foreground">{selectedItem.description}</p>
<p className="text-lg font-bold mt-2">Saldo: {formatCurrency(selectedItem.remainingAmount)}</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label>Valor Recebido</Label>
<Input name="amount" type="number" step="0.01" defaultValue={selectedItem.remainingAmount} required />
</div>
<div>
<Label>Data do Recebimento</Label>
<Input name="receiveDate" type="date" defaultValue={new Date().toISOString().split('T')[0]} required />
</div>
<div className="col-span-2">
<Label>Conta Bancária</Label>
<Select name="bankAccountId" required>
<SelectTrigger>
<SelectValue placeholder="Selecione a conta" />
</SelectTrigger>
<SelectContent>
{bankAccounts.map(acc => (
<SelectItem key={acc.id} value={acc.id.toString()}>
{acc.name} ({formatCurrency(acc.currentBalance)})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setShowReceiveDialog(false)}>Cancelar</Button>
<Button type="submit">Confirmar Recebimento</Button>
</DialogFooter>
</form>
)}
</DialogContent>
</Dialog>
<Dialog open={showTransferDialog} onOpenChange={setShowTransferDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>Transferência entre Contas</DialogTitle>
</DialogHeader>
<form onSubmit={handleTransfer} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label>Conta de Origem</Label>
<Select name="fromAccountId" required>
<SelectTrigger>
<SelectValue placeholder="Selecione" />
</SelectTrigger>
<SelectContent>
{bankAccounts.map(acc => (
<SelectItem key={acc.id} value={acc.id.toString()}>
{acc.name} ({formatCurrency(acc.currentBalance)})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label>Conta de Destino</Label>
<Select name="toAccountId" required>
<SelectTrigger>
<SelectValue placeholder="Selecione" />
</SelectTrigger>
<SelectContent>
{bankAccounts.map(acc => (
<SelectItem key={acc.id} value={acc.id.toString()}>
{acc.name} ({formatCurrency(acc.currentBalance)})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label>Valor</Label>
<Input name="amount" type="number" step="0.01" required />
</div>
<div>
<Label>Data</Label>
<Input name="transferDate" type="date" defaultValue={new Date().toISOString().split('T')[0]} required />
</div>
<div className="col-span-2">
<Label>Descrição</Label>
<Input name="description" />
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setShowTransferDialog(false)}>Cancelar</Button>
<Button type="submit">Transferir</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
<Dialog open={showPaymentMethodDialog} onOpenChange={setShowPaymentMethodDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>Novo Meio de Pagamento</DialogTitle>
</DialogHeader>
<form onSubmit={(e) => {
e.preventDefault();
const form = e.currentTarget;
const formData = new FormData(form);
createPaymentMethodMutation.mutate({
code: formData.get("code"),
name: formData.get("name"),
type: formData.get("type"),
fee: formData.get("fee") || "0",
daysToReceive: parseInt(formData.get("daysToReceive") as string) || 0,
});
}} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label>Código</Label>
<Input name="code" required />
</div>
<div>
<Label>Nome</Label>
<Input name="name" required />
</div>
<div>
<Label>Tipo</Label>
<Select name="type" required>
<SelectTrigger>
<SelectValue placeholder="Selecione" />
</SelectTrigger>
<SelectContent>
<SelectItem value="cash">Dinheiro</SelectItem>
<SelectItem value="credit_card">Cartão de Crédito</SelectItem>
<SelectItem value="debit_card">Cartão de Débito</SelectItem>
<SelectItem value="pix">PIX</SelectItem>
<SelectItem value="bank_transfer">Transferência</SelectItem>
<SelectItem value="boleto">Boleto</SelectItem>
<SelectItem value="check">Cheque</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label>Taxa (%)</Label>
<Input name="fee" type="number" step="0.01" defaultValue="0" />
</div>
<div>
<Label>Dias para Receber</Label>
<Input name="daysToReceive" type="number" defaultValue="0" />
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setShowPaymentMethodDialog(false)}>Cancelar</Button>
<Button type="submit">Salvar</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
<Dialog open={showPaymentPlanDialog} onOpenChange={setShowPaymentPlanDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>Novo Plano de Pagamento</DialogTitle>
</DialogHeader>
<form onSubmit={(e) => {
e.preventDefault();
const form = e.currentTarget;
const formData = new FormData(form);
createPaymentPlanMutation.mutate({
code: formData.get("code"),
name: formData.get("name"),
installments: parseInt(formData.get("installments") as string) || 1,
intervalDays: parseInt(formData.get("intervalDays") as string) || 30,
firstDueDays: parseInt(formData.get("firstDueDays") as string) || 30,
});
}} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label>Código</Label>
<Input name="code" required />
</div>
<div>
<Label>Nome</Label>
<Input name="name" required />
</div>
<div>
<Label> de Parcelas</Label>
<Input name="installments" type="number" defaultValue="1" required />
</div>
<div>
<Label>Intervalo (dias)</Label>
<Input name="intervalDays" type="number" defaultValue="30" required />
</div>
<div>
<Label>1ª Parcela (dias)</Label>
<Input name="firstDueDays" type="number" defaultValue="30" required />
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setShowPaymentPlanDialog(false)}>Cancelar</Button>
<Button type="submit">Salvar</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
<Dialog open={showCategoryDialog} onOpenChange={setShowCategoryDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>Nova Categoria</DialogTitle>
</DialogHeader>
<form onSubmit={(e) => {
e.preventDefault();
const form = e.currentTarget;
const formData = new FormData(form);
createCategoryMutation.mutate({
code: formData.get("code"),
name: formData.get("name"),
type: formData.get("type"),
});
}} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label>Código</Label>
<Input name="code" required />
</div>
<div>
<Label>Nome</Label>
<Input name="name" required />
</div>
<div className="col-span-2">
<Label>Tipo</Label>
<Select name="type" required>
<SelectTrigger>
<SelectValue placeholder="Selecione" />
</SelectTrigger>
<SelectContent>
<SelectItem value="income">Receita</SelectItem>
<SelectItem value="expense">Despesa</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setShowCategoryDialog(false)}>Cancelar</Button>
<Button type="submit">Salvar</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
</BrowserFrame>
);
}