Fix: Remove duplicate menu and category page effect
Removed the duplicate menu from the Dashboard, Transações, Cartões de Credito, Metas, Calendário, Conectar WhatsApp, and Grupos pages. Also removed the background effect from the Categorias page.
This commit is contained in:
parent
6121502853
commit
583136ca75
@ -1,6 +1,5 @@
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import Layout from '@/components/layout/Layout';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import { getTransacoes } from '@/services/transacaoService';
|
||||
@ -66,85 +65,83 @@ const CalendarioPage = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className="text-2xl font-bold tracking-tight">Calendário de Transações</h1>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-5 gap-6">
|
||||
<Card className="md:col-span-2">
|
||||
<CardContent className="pt-6">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={selectedDate}
|
||||
onSelect={setSelectedDate}
|
||||
locale={ptBR}
|
||||
className="max-w-full pointer-events-auto"
|
||||
modifiers={{
|
||||
hasTransaction: (date) => hasDayTransaction(date),
|
||||
}}
|
||||
modifiersStyles={{
|
||||
hasTransaction: {
|
||||
fontWeight: "bold",
|
||||
textDecoration: "underline",
|
||||
backgroundColor: "rgba(16, 185, 129, 0.1)"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="mt-4 text-sm text-muted-foreground text-center">
|
||||
Dias com transações estão destacados
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="md:col-span-3">
|
||||
<CardContent className="p-6">
|
||||
<h2 className="text-lg font-semibold mb-4">
|
||||
{selectedDate ? format(selectedDate, "dd 'de' MMMM 'de' yyyy", { locale: ptBR }) : "Selecione uma data"}
|
||||
</h2>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="space-y-3">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="p-4 border rounded-md animate-pulse">
|
||||
<div className="h-4 bg-muted rounded w-1/4 mb-2"></div>
|
||||
<div className="h-6 bg-muted rounded w-1/2"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : filteredTransactions.length === 0 ? (
|
||||
<div className="text-center py-10 text-muted-foreground">
|
||||
Nenhuma transação nesta data
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{filteredTransactions.map((transaction) => (
|
||||
<div key={transaction.id} className="p-4 border rounded-md hover:bg-muted/50 transition-colors">
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<div className="font-medium">{transaction.estabelecimento}</div>
|
||||
<Badge variant={transaction.tipo.toLowerCase() === 'receita' || transaction.tipo.toLowerCase() === 'entrada' ? "success" : "destructive"}>
|
||||
{transaction.tipo.toLowerCase() === 'receita' || transaction.tipo.toLowerCase() === 'entrada' ? 'Receita' : 'Despesa'}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground mb-2">{transaction.detalhes}</div>
|
||||
<div className="flex justify-between">
|
||||
<Badge variant="outline">{transaction.categoria}</Badge>
|
||||
<span className={transaction.tipo.toLowerCase() === 'receita' || transaction.tipo.toLowerCase() === 'entrada' ? "text-finance-green font-medium" : "text-finance-red font-medium"}>
|
||||
{transaction.tipo.toLowerCase() === 'receita' || transaction.tipo.toLowerCase() === 'entrada'
|
||||
? formatCurrency(Math.abs(transaction.valor))
|
||||
: `-${formatCurrency(Math.abs(transaction.valor))}`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className="text-2xl font-bold tracking-tight">Calendário de Transações</h1>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<div className="grid md:grid-cols-5 gap-6">
|
||||
<Card className="md:col-span-2">
|
||||
<CardContent className="pt-6">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={selectedDate}
|
||||
onSelect={setSelectedDate}
|
||||
locale={ptBR}
|
||||
className="max-w-full pointer-events-auto"
|
||||
modifiers={{
|
||||
hasTransaction: (date) => hasDayTransaction(date),
|
||||
}}
|
||||
modifiersStyles={{
|
||||
hasTransaction: {
|
||||
fontWeight: "bold",
|
||||
textDecoration: "underline",
|
||||
backgroundColor: "rgba(16, 185, 129, 0.1)"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="mt-4 text-sm text-muted-foreground text-center">
|
||||
Dias com transações estão destacados
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="md:col-span-3">
|
||||
<CardContent className="p-6">
|
||||
<h2 className="text-lg font-semibold mb-4">
|
||||
{selectedDate ? format(selectedDate, "dd 'de' MMMM 'de' yyyy", { locale: ptBR }) : "Selecione uma data"}
|
||||
</h2>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="space-y-3">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="p-4 border rounded-md animate-pulse">
|
||||
<div className="h-4 bg-muted rounded w-1/4 mb-2"></div>
|
||||
<div className="h-6 bg-muted rounded w-1/2"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : filteredTransactions.length === 0 ? (
|
||||
<div className="text-center py-10 text-muted-foreground">
|
||||
Nenhuma transação nesta data
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{filteredTransactions.map((transaction) => (
|
||||
<div key={transaction.id} className="p-4 border rounded-md hover:bg-muted/50 transition-colors">
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<div className="font-medium">{transaction.estabelecimento}</div>
|
||||
<Badge variant={transaction.tipo.toLowerCase() === 'receita' || transaction.tipo.toLowerCase() === 'entrada' ? "success" : "destructive"}>
|
||||
{transaction.tipo.toLowerCase() === 'receita' || transaction.tipo.toLowerCase() === 'entrada' ? 'Receita' : 'Despesa'}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground mb-2">{transaction.detalhes}</div>
|
||||
<div className="flex justify-between">
|
||||
<Badge variant="outline">{transaction.categoria}</Badge>
|
||||
<span className={transaction.tipo.toLowerCase() === 'receita' || transaction.tipo.toLowerCase() === 'entrada' ? "text-finance-green font-medium" : "text-finance-red font-medium"}>
|
||||
{transaction.tipo.toLowerCase() === 'receita' || transaction.tipo.toLowerCase() === 'entrada'
|
||||
? formatCurrency(Math.abs(transaction.valor))
|
||||
: `-${formatCurrency(Math.abs(transaction.valor))}`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import Layout from '@/components/layout/Layout';
|
||||
import { CartaoCredito, DespesaCartao } from '@/types/cartaoTypes';
|
||||
import { getCartoes, getDespesasCartao, getTotalDespesasCartao } from '@/services/cartaoCreditoService';
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
@ -107,32 +106,30 @@ const CartoesCreditoPage = () => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="space-y-6">
|
||||
<CartaoActions
|
||||
isDetalhes={detalheCartaoAberto}
|
||||
cartaoSelecionado={cartaoSelecionado}
|
||||
onVoltarParaLista={handleVoltarParaLista}
|
||||
onCartaoSuccess={handleCartaoSuccess}
|
||||
<div className="space-y-6">
|
||||
<CartaoActions
|
||||
isDetalhes={detalheCartaoAberto}
|
||||
cartaoSelecionado={cartaoSelecionado}
|
||||
onVoltarParaLista={handleVoltarParaLista}
|
||||
onCartaoSuccess={handleCartaoSuccess}
|
||||
onDespesaSuccess={handleDespesaSuccess}
|
||||
/>
|
||||
|
||||
{!detalheCartaoAberto ? (
|
||||
<CartaoListView
|
||||
cartoes={cartoes}
|
||||
isLoading={isLoading}
|
||||
onCartaoClick={handleCartaoClick}
|
||||
/>
|
||||
) : cartaoSelecionado && (
|
||||
<CartaoDetalhesAvancado
|
||||
cartao={cartaoSelecionado}
|
||||
despesas={despesas}
|
||||
isLoading={isLoading}
|
||||
onDespesaSuccess={handleDespesaSuccess}
|
||||
/>
|
||||
|
||||
{!detalheCartaoAberto ? (
|
||||
<CartaoListView
|
||||
cartoes={cartoes}
|
||||
isLoading={isLoading}
|
||||
onCartaoClick={handleCartaoClick}
|
||||
/>
|
||||
) : cartaoSelecionado && (
|
||||
<CartaoDetalhesAvancado
|
||||
cartao={cartaoSelecionado}
|
||||
despesas={despesas}
|
||||
isLoading={isLoading}
|
||||
onDespesaSuccess={handleDespesaSuccess}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Layout>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import Layout from '@/components/layout/Layout';
|
||||
import { CardSpotlight } from '@/components/ui/card-spotlight';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { getCategorySummary } from '@/services/transacao';
|
||||
import { CategorySummary } from '@/types/financialTypes';
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
@ -98,102 +98,110 @@ const CategoriasPage = () => {
|
||||
{isLoading ? (
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{[1, 2, 3, 4, 5, 6].map((i) => (
|
||||
<CardSpotlight key={i} className="animate-pulse">
|
||||
<div className="pb-2">
|
||||
<div className="h-5 bg-muted rounded w-1/2"></div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="h-4 bg-muted rounded w-1/4 mb-3"></div>
|
||||
<div className="h-2 bg-muted rounded"></div>
|
||||
</div>
|
||||
</CardSpotlight>
|
||||
<Card key={i} className="animate-pulse">
|
||||
<CardContent className="p-6">
|
||||
<div className="pb-2">
|
||||
<div className="h-5 bg-muted rounded w-1/2"></div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="h-4 bg-muted rounded w-1/4 mb-3"></div>
|
||||
<div className="h-2 bg-muted rounded"></div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
) : categories.length === 0 ? (
|
||||
<CardSpotlight>
|
||||
<div className="py-10 text-center">
|
||||
<p className="text-muted-foreground">Nenhuma categoria encontrada</p>
|
||||
<p className="mt-2 text-sm">
|
||||
{tipoFiltro === 'despesa'
|
||||
? 'Verifique se existem transações do tipo "despesa" cadastradas'
|
||||
: tipoFiltro === 'receita'
|
||||
? 'Verifique se existem transações do tipo "receita" cadastradas'
|
||||
: 'Verifique se existem transações cadastradas'}
|
||||
</p>
|
||||
</div>
|
||||
</CardSpotlight>
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="py-10 text-center">
|
||||
<p className="text-muted-foreground">Nenhuma categoria encontrada</p>
|
||||
<p className="mt-2 text-sm">
|
||||
{tipoFiltro === 'despesa'
|
||||
? 'Verifique se existem transações do tipo "despesa" cadastradas'
|
||||
: tipoFiltro === 'receita'
|
||||
? 'Verifique se existem transações do tipo "receita" cadastradas'
|
||||
: 'Verifique se existem transações cadastradas'}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{categories.map((category) => (
|
||||
<CardSpotlight key={category.categoria} className="relative z-10">
|
||||
<div className="pb-2">
|
||||
<div className="text-base flex items-center font-semibold relative z-20">
|
||||
<span
|
||||
className="w-3 h-3 rounded-full mr-2"
|
||||
style={{ backgroundColor: category.color }}
|
||||
/>
|
||||
{category.categoria}
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative z-20">
|
||||
<div className="text-2xl font-bold mb-2">
|
||||
{formatCurrency(category.valor)}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Progress
|
||||
value={category.percentage * 100}
|
||||
className="h-2"
|
||||
style={{
|
||||
backgroundColor: 'rgba(0,0,0,0.1)',
|
||||
'--progress-background': category.color
|
||||
} as React.CSSProperties}
|
||||
/>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{(category.percentage * 100).toFixed(1)}% do total
|
||||
<Card key={category.categoria}>
|
||||
<CardContent className="p-6">
|
||||
<div className="pb-2">
|
||||
<div className="text-base flex items-center font-semibold">
|
||||
<span
|
||||
className="w-3 h-3 rounded-full mr-2"
|
||||
style={{ backgroundColor: category.color }}
|
||||
/>
|
||||
{category.categoria}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardSpotlight>
|
||||
<div>
|
||||
<div className="text-2xl font-bold mb-2">
|
||||
{formatCurrency(category.valor)}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Progress
|
||||
value={category.percentage * 100}
|
||||
className="h-2"
|
||||
style={{
|
||||
backgroundColor: 'rgba(0,0,0,0.1)',
|
||||
'--progress-background': category.color
|
||||
} as React.CSSProperties}
|
||||
/>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{(category.percentage * 100).toFixed(1)}% do total
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<CardSpotlight className="mt-6">
|
||||
<div>
|
||||
<h3 className="text-xl font-bold mb-4 relative z-20">Resumo de Categorias {tipoFiltro !== 'all' ? (tipoFiltro === 'despesa' ? '(Despesas)' : '(Receitas)') : ''}</h3>
|
||||
</div>
|
||||
<div className="relative z-20">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Categoria</TableHead>
|
||||
<TableHead>Valor</TableHead>
|
||||
<TableHead>Percentual</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{categories.map((category) => (
|
||||
<TableRow key={category.categoria}>
|
||||
<TableCell className="font-medium flex items-center">
|
||||
<span
|
||||
className="w-3 h-3 rounded-full mr-2"
|
||||
style={{ backgroundColor: category.color }}
|
||||
/>
|
||||
{category.categoria}
|
||||
</TableCell>
|
||||
<TableCell>{formatCurrency(category.valor)}</TableCell>
|
||||
<TableCell>{(category.percentage * 100).toFixed(1)}%</TableCell>
|
||||
<Card className="mt-6">
|
||||
<CardContent className="p-6">
|
||||
<div>
|
||||
<h3 className="text-xl font-bold mb-4">Resumo de Categorias {tipoFiltro !== 'all' ? (tipoFiltro === 'despesa' ? '(Despesas)' : '(Receitas)') : ''}</h3>
|
||||
</div>
|
||||
<div>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Categoria</TableHead>
|
||||
<TableHead>Valor</TableHead>
|
||||
<TableHead>Percentual</TableHead>
|
||||
</TableRow>
|
||||
))}
|
||||
<TableRow className="bg-muted/50">
|
||||
<TableCell className="font-bold">Total</TableCell>
|
||||
<TableCell className="font-bold">{formatCurrency(totalGastos)}</TableCell>
|
||||
<TableCell>100%</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</CardSpotlight>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{categories.map((category) => (
|
||||
<TableRow key={category.categoria}>
|
||||
<TableCell className="font-medium flex items-center">
|
||||
<span
|
||||
className="w-3 h-3 rounded-full mr-2"
|
||||
style={{ backgroundColor: category.color }}
|
||||
/>
|
||||
{category.categoria}
|
||||
</TableCell>
|
||||
<TableCell>{formatCurrency(category.valor)}</TableCell>
|
||||
<TableCell>{(category.percentage * 100).toFixed(1)}%</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
<TableRow className="bg-muted/50">
|
||||
<TableCell className="font-bold">Total</TableCell>
|
||||
<TableCell className="font-bold">{formatCurrency(totalGastos)}</TableCell>
|
||||
<TableCell>100%</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import Layout from '@/components/layout/Layout';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { RefreshCw } from 'lucide-react';
|
||||
import { listarGruposWhatsApp } from '@/services/gruposWhatsAppService';
|
||||
@ -65,31 +64,29 @@ const GruposWhatsApp = () => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className="text-2xl font-bold tracking-tight">Grupos do WhatsApp</h1>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={buscarGrupos}
|
||||
disabled={carregando}
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 mr-2 ${carregando ? 'animate-spin' : ''}`} />
|
||||
{carregando ? 'Atualizando...' : 'Atualizar'}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className="text-2xl font-bold tracking-tight">Grupos do WhatsApp</h1>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={buscarGrupos}
|
||||
disabled={carregando}
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 mr-2 ${carregando ? 'animate-spin' : ''}`} />
|
||||
{carregando ? 'Atualizando...' : 'Atualizar'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<CreateGroupForm userEmail={userEmail} onSuccess={buscarGrupos} />
|
||||
<GroupsList
|
||||
grupos={grupos}
|
||||
carregando={carregando}
|
||||
onDeleteGroup={handleDeleteGroup}
|
||||
/>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<CreateGroupForm userEmail={userEmail} onSuccess={buscarGrupos} />
|
||||
<GroupsList
|
||||
grupos={grupos}
|
||||
carregando={carregando}
|
||||
onDeleteGroup={handleDeleteGroup}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -1,228 +1,178 @@
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import Layout from '@/components/layout/Layout';
|
||||
import SummaryCard from '@/components/dashboard/SummaryCard';
|
||||
import TransactionsTable from '@/components/dashboard/TransactionsTable';
|
||||
import CategoryChart from '@/components/dashboard/CategoryChart';
|
||||
import MonthlyChart from '@/components/dashboard/MonthlyChart';
|
||||
import { MonthFilter } from '@/components/filters/MonthFilter';
|
||||
import { OnboardingTour, useOnboardingTour } from '@/components/onboarding';
|
||||
import { Wallet, ArrowUp, ArrowDown, PiggyBank } from 'lucide-react';
|
||||
import { Transaction, CategorySummary, MonthlyData } from '@/types/financialTypes';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import {
|
||||
getTransacoes,
|
||||
getTransactionSummary,
|
||||
getCategorySummary,
|
||||
getMonthlyData
|
||||
} from '@/services/transacao';
|
||||
import { getCartoes } from '@/services/cartaoCreditoService';
|
||||
import { ArrowUpIcon, ArrowDownIcon, CreditCardIcon, Target } from "lucide-react";
|
||||
import { getResumoFinanceiro } from "@/services/transacao";
|
||||
import { ResumoFinanceiro } from "@/types/financialTypes";
|
||||
import TransactionsTable from "@/components/dashboard/TransactionsTable";
|
||||
import { useTransactions } from "@/hooks/useTransactions";
|
||||
import CategoryChart from "@/components/dashboard/CategoryChart";
|
||||
|
||||
const Dashboard = () => {
|
||||
const [resumo, setResumo] = useState<ResumoFinanceiro | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [transactions, setTransactions] = useState<Transaction[]>([]);
|
||||
const [categories, setCategories] = useState<CategorySummary[]>([]);
|
||||
const [monthlyData, setMonthlyData] = useState<MonthlyData[]>([]);
|
||||
const [totals, setTotals] = useState({ receitas: 0, despesas: 0, saldo: 0, cartoes: 0 });
|
||||
const { toast } = useToast();
|
||||
|
||||
// Hook do tour de onboarding
|
||||
const {
|
||||
isOpen: tourIsOpen,
|
||||
currentStep: tourCurrentStep,
|
||||
nextStep: tourNextStep,
|
||||
skipTour,
|
||||
closeTour
|
||||
} = useOnboardingTour();
|
||||
|
||||
// Função para obter o mês atual no formato YYYY-MM
|
||||
const getCurrentMonth = () => {
|
||||
const now = new Date();
|
||||
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const [selectedMonth, setSelectedMonth] = useState<string>(getCurrentMonth());
|
||||
const {
|
||||
transactions,
|
||||
isLoading: transactionsLoading,
|
||||
formatCurrency
|
||||
} = useTransactions({ monthFilter: getCurrentMonth() });
|
||||
|
||||
// Load data when component mounts or month changes
|
||||
useEffect(() => {
|
||||
async function loadData() {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
console.log("🚀 [Dashboard] Iniciando carregamento de dados para o mês:", selectedMonth);
|
||||
const storedUserId = localStorage.getItem('userId');
|
||||
const finDashUser = localStorage.getItem('finDashUser');
|
||||
|
||||
// Verificar se existe email no localStorage
|
||||
const userEmail = localStorage.getItem('userEmail');
|
||||
console.log("📧 [Dashboard] Email no localStorage:", userEmail);
|
||||
|
||||
if (!userEmail) {
|
||||
console.error("❌ [Dashboard] Email não encontrado no localStorage!");
|
||||
toast({
|
||||
title: "Erro de autenticação",
|
||||
description: "Email do usuário não encontrado. Faça login novamente.",
|
||||
variant: "destructive"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Buscar todos os dados necessários com filtro de mês
|
||||
console.log("📊 [Dashboard] Buscando dados do Supabase...");
|
||||
const [transacoesData, totalsData, categoriesData, monthlyDataResult, cartoesData] = await Promise.all([
|
||||
getTransacoes(selectedMonth),
|
||||
getTransactionSummary(selectedMonth),
|
||||
getCategorySummary('despesa', selectedMonth),
|
||||
getMonthlyData(), // Monthly data não precisa de filtro pois já mostra todos os meses
|
||||
getCartoes()
|
||||
]);
|
||||
|
||||
// Calcular total dos cartões
|
||||
const totalCartoes = cartoesData.reduce((sum, cartao) => sum + (cartao.total_despesas || 0), 0);
|
||||
|
||||
console.log("✅ [Dashboard] Dados obtidos com sucesso:", {
|
||||
transacoes: transacoesData.length,
|
||||
totals: totalsData,
|
||||
categories: categoriesData.length,
|
||||
monthlyData: monthlyDataResult.length,
|
||||
cartoes: cartoesData.length,
|
||||
totalCartoes: totalCartoes,
|
||||
mesEscolhido: selectedMonth
|
||||
});
|
||||
|
||||
setTransactions(transacoesData);
|
||||
setTotals({
|
||||
...totalsData,
|
||||
cartoes: totalCartoes,
|
||||
// Atualiza o saldo incluindo despesas de cartão
|
||||
saldo: totalsData.receitas - totalsData.despesas - totalCartoes
|
||||
});
|
||||
setCategories(categoriesData);
|
||||
setMonthlyData(monthlyDataResult);
|
||||
|
||||
if (transacoesData.length > 0 || categoriesData.length > 0 || monthlyDataResult.length > 0) {
|
||||
toast({
|
||||
title: "Dados carregados com sucesso",
|
||||
description: `${transacoesData.length} transações encontradas para ${selectedMonth}`
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: "Nenhum dado encontrado",
|
||||
description: `Não foram encontradas transações para ${selectedMonth}`,
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("💥 [Dashboard] Erro ao carregar dados:", error);
|
||||
toast({
|
||||
title: "Erro ao carregar dados",
|
||||
description: "Verifique a conexão com o Supabase",
|
||||
variant: "destructive"
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
if (!storedUserId && finDashUser) {
|
||||
localStorage.setItem('userId', finDashUser);
|
||||
} else if (!storedUserId && !finDashUser) {
|
||||
const defaultId = '9f267008-9128-4a2f-b730-de0a0b5602a9';
|
||||
localStorage.setItem('userId', defaultId);
|
||||
}
|
||||
}, []);
|
||||
|
||||
loadData();
|
||||
}, [selectedMonth, toast]);
|
||||
|
||||
const formatCurrency = (value: number) => {
|
||||
return new Intl.NumberFormat('pt-BR', {
|
||||
style: 'currency',
|
||||
currency: 'BRL',
|
||||
}).format(value);
|
||||
const loadResumo = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
console.log("Carregando resumo financeiro...");
|
||||
const data = await getResumoFinanceiro();
|
||||
console.log("Resumo carregado:", data);
|
||||
setResumo(data);
|
||||
} catch (error) {
|
||||
console.error("Erro ao carregar resumo:", error);
|
||||
toast({
|
||||
title: "Erro ao carregar resumo",
|
||||
description: "Não foi possível obter os dados do Supabase",
|
||||
variant: "destructive"
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Calculate total of all expenses (regular + credit cards)
|
||||
const totalDespesasGeral = totals.despesas + totals.cartoes;
|
||||
useEffect(() => {
|
||||
loadResumo();
|
||||
}, [toast]);
|
||||
|
||||
// Função para formatar o mês para exibição
|
||||
const formatMonthDisplay = (month: string) => {
|
||||
const [year, monthNum] = month.split('-');
|
||||
const months = [
|
||||
'Janeiro', 'Fevereiro', 'Março', 'Abril', 'Maio', 'Junho',
|
||||
'Julho', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Dezembro'
|
||||
];
|
||||
return `${months[parseInt(monthNum) - 1]} ${year}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-2xl font-bold tracking-tight">Dashboard Financeiro</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Dados de: {formatMonthDisplay(selectedMonth)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<MonthFilter
|
||||
selectedMonth={selectedMonth}
|
||||
onMonthChange={setSelectedMonth}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Resumo em cards */}
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<SummaryCard
|
||||
title="Receitas"
|
||||
value={formatCurrency(totals.receitas)}
|
||||
icon={<ArrowUp size={24} className="text-green-600" />}
|
||||
trend={5}
|
||||
iconClass="bg-green-100"
|
||||
valueClass="text-green-700"
|
||||
className="border-green-200 shadow-lg"
|
||||
/>
|
||||
<SummaryCard
|
||||
title="Despesas"
|
||||
value={formatCurrency(totalDespesasGeral)}
|
||||
secondaryValue={`Cartões: ${formatCurrency(totals.cartoes)}`}
|
||||
icon={<ArrowDown size={24} className="text-red-600" />}
|
||||
trend={-2}
|
||||
iconClass="bg-red-100"
|
||||
valueClass="text-red-700"
|
||||
className="border-red-200 shadow-lg"
|
||||
/>
|
||||
<SummaryCard
|
||||
title="Saldo"
|
||||
value={formatCurrency(totals.saldo)}
|
||||
icon={<Wallet size={24} className="text-blue-600" />}
|
||||
iconClass="bg-blue-100"
|
||||
valueClass="text-blue-700"
|
||||
className="border-blue-200 shadow-lg"
|
||||
/>
|
||||
<SummaryCard
|
||||
title="Economia"
|
||||
value={`${totals.receitas > 0 ? ((totals.saldo / totals.receitas) * 100).toFixed(1) : 0}%`}
|
||||
secondaryValue={formatCurrency(totals.saldo)}
|
||||
icon={<PiggyBank size={24} className="text-purple-600" />}
|
||||
iconClass="bg-purple-100"
|
||||
valueClass="text-purple-700"
|
||||
className="border-purple-200 shadow-lg"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* ---- GRÁFICOS EM COLUNA ---- */}
|
||||
<div className="flex flex-col gap-6 w-full">
|
||||
<MonthlyChart data={monthlyData} isLoading={isLoading} />
|
||||
<CategoryChart categories={categories} isLoading={isLoading} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-xl font-semibold">Transações Recentes</h2>
|
||||
<TransactionsTable transactions={transactions} isLoading={isLoading} />
|
||||
<div className="grid gap-4 md:grid-cols-4">
|
||||
{[1, 2, 3, 4].map((i) => (
|
||||
<Card key={i}>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<div className="h-4 bg-muted rounded w-1/2 animate-pulse"></div>
|
||||
<div className="h-4 w-4 bg-muted rounded animate-pulse"></div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-8 bg-muted rounded w-3/4 mb-2 animate-pulse"></div>
|
||||
<div className="h-3 bg-muted rounded w-1/2 animate-pulse"></div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
{/* Tour de Onboarding */}
|
||||
<OnboardingTour
|
||||
isOpen={tourIsOpen}
|
||||
currentStep={tourCurrentStep}
|
||||
onNext={tourNextStep}
|
||||
onSkip={skipTour}
|
||||
onClose={closeTour}
|
||||
/>
|
||||
</Layout>
|
||||
return (
|
||||
<div className="space-y-6" data-tour="dashboard-content">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-2xl font-bold tracking-tight">Dashboard</h1>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-4">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Total Receitas</CardTitle>
|
||||
<ArrowUpIcon className="h-4 w-4 text-green-600" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-green-600">
|
||||
{resumo ? formatCurrency(resumo.totalReceitas) : 'R$ 0,00'}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">Entradas do mês</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Total Despesas</CardTitle>
|
||||
<ArrowDownIcon className="h-4 w-4 text-red-600" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-red-600">
|
||||
{resumo ? formatCurrency(resumo.totalDespesas) : 'R$ 0,00'}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">Saídas do mês</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Cartões de Crédito</CardTitle>
|
||||
<CreditCardIcon className="h-4 w-4 text-blue-600" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-blue-600">
|
||||
{resumo ? formatCurrency(resumo.totalCartoes || 0) : 'R$ 0,00'}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">Gastos nos cartões</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</CardTitle>
|
||||
<Target className="h-4 w-4 text-purple-600" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className={`text-2xl font-bold ${
|
||||
resumo && (resumo.totalReceitas - resumo.totalDespesas - (resumo.totalCartoes || 0)) >= 0
|
||||
? 'text-green-600'
|
||||
: 'text-red-600'
|
||||
}`}>
|
||||
{resumo ? formatCurrency(resumo.totalReceitas - resumo.totalDespesas - (resumo.totalCartoes || 0)) : 'R$ 0,00'}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">Resultado do mês</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<Card className="md:col-span-1">
|
||||
<CardHeader>
|
||||
<CardTitle>Despesas por Categoria</CardTitle>
|
||||
<CardDescription>Distribuição dos seus gastos</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CategoryChart />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="md:col-span-1">
|
||||
<CardHeader>
|
||||
<CardTitle>Últimas Transações</CardTitle>
|
||||
<CardDescription>Suas transações mais recentes</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<TransactionsTable
|
||||
transactions={transactions.slice(0, 5)}
|
||||
isLoading={transactionsLoading}
|
||||
showPagination={false}
|
||||
onEdit={() => {}}
|
||||
onDelete={() => {}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -1,78 +1,31 @@
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { format } from 'date-fns';
|
||||
import { ptBR } from 'date-fns/locale';
|
||||
import Layout from '@/components/layout/Layout';
|
||||
import { MetasForm } from '@/components/dashboard/MetasForm';
|
||||
import { MetaProgressCard } from '@/components/dashboard/MetaProgressCard';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { ResultadoMeta } from '@/types/financialTypes';
|
||||
import { useToast } from '@/components/ui/use-toast';
|
||||
import { calcularResultadosMetas, getMeta, deletarMeta } from '@/services/metasService';
|
||||
import { PlusCircle } from 'lucide-react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { MetaFinanceira } from '@/types/financialTypes';
|
||||
import { getMetas } from '@/services/metasService';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import MetaForm from '@/components/metas/MetaForm';
|
||||
import MetaCard from '@/components/metas/MetaCard';
|
||||
|
||||
const MetasPage = () => {
|
||||
const { toast } = useToast();
|
||||
const [userId, setUserId] = useState<string>('');
|
||||
const [resultados, setResultados] = useState<ResultadoMeta[]>([]);
|
||||
const [metas, setMetas] = useState<MetaFinanceira[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||
const [metaParaEditar, setMetaParaEditar] = useState<{ mes: number, ano: number, id?: string } | null>(null);
|
||||
const [metaParaDeletar, setMetaParaDeletar] = useState<{ mes: number, ano: number, id: string } | null>(null);
|
||||
const [valorMetaAtual, setValorMetaAtual] = useState(0);
|
||||
const [isDeletingMeta, setIsDeletingMeta] = useState(false);
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [editingMeta, setEditingMeta] = useState<MetaFinanceira | null>(null);
|
||||
const { toast } = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
// Obter userId armazenado no localStorage
|
||||
const storedUserId = localStorage.getItem('userId');
|
||||
if (storedUserId) {
|
||||
setUserId(storedUserId);
|
||||
loadData(storedUserId);
|
||||
} else {
|
||||
// Tentar usar finDashUser como fallback
|
||||
const finDashUser = localStorage.getItem('finDashUser');
|
||||
if (finDashUser) {
|
||||
setUserId(finDashUser);
|
||||
loadData(finDashUser);
|
||||
} else {
|
||||
// Caso não encontre nenhum ID de usuário, usar um ID padrão temporário
|
||||
const defaultId = '9f267008-9128-4a2f-b730-de0a0b5602a9';
|
||||
localStorage.setItem('userId', defaultId);
|
||||
setUserId(defaultId);
|
||||
loadData(defaultId);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadData = async (user: string) => {
|
||||
const loadMetas = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const resultado = await calcularResultadosMetas(user);
|
||||
setResultados(resultado);
|
||||
const data = await getMetas();
|
||||
setMetas(data);
|
||||
} catch (error) {
|
||||
console.error('Erro ao carregar metas:', error);
|
||||
toast({
|
||||
title: 'Erro ao carregar metas',
|
||||
description: 'Não foi possível obter os dados de metas',
|
||||
title: 'Erro',
|
||||
description: 'Não foi possível carregar suas metas',
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
@ -80,207 +33,107 @@ const MetasPage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditarMeta = async (mes: number, ano: number) => {
|
||||
try {
|
||||
const meta = await getMeta(userId, mes, ano);
|
||||
if (meta) {
|
||||
setValorMetaAtual(meta.valor_meta);
|
||||
setMetaParaEditar({ mes, ano, id: meta.id });
|
||||
setIsDialogOpen(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar meta para edição:', error);
|
||||
}
|
||||
useEffect(() => {
|
||||
loadMetas();
|
||||
}, []);
|
||||
|
||||
const handleMetaSuccess = () => {
|
||||
loadMetas();
|
||||
setShowForm(false);
|
||||
setEditingMeta(null);
|
||||
toast({
|
||||
title: 'Sucesso',
|
||||
description: editingMeta ? 'Meta atualizada com sucesso' : 'Meta criada com sucesso',
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeletarMeta = (mes: number, ano: number, id: string) => {
|
||||
setMetaParaDeletar({ mes, ano, id });
|
||||
setIsDeleteDialogOpen(true);
|
||||
const handleEditMeta = (meta: MetaFinanceira) => {
|
||||
setEditingMeta(meta);
|
||||
setShowForm(true);
|
||||
};
|
||||
|
||||
const confirmarDeletarMeta = async () => {
|
||||
if (!metaParaDeletar?.id) return;
|
||||
|
||||
try {
|
||||
setIsDeletingMeta(true);
|
||||
await deletarMeta(metaParaDeletar.id);
|
||||
|
||||
// Atualizar lista de metas após deleção
|
||||
await loadData(userId);
|
||||
|
||||
toast({
|
||||
title: 'Meta excluída',
|
||||
description: `A meta de ${format(new Date(metaParaDeletar.ano, metaParaDeletar.mes - 1, 1), 'MMMM', { locale: ptBR })} ${metaParaDeletar.ano} foi excluída com sucesso.`,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Erro ao deletar meta:', error);
|
||||
toast({
|
||||
title: 'Erro ao deletar meta',
|
||||
description: 'Não foi possível excluir a meta',
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
setIsDeletingMeta(false);
|
||||
setIsDeleteDialogOpen(false);
|
||||
setMetaParaDeletar(null);
|
||||
}
|
||||
const handleDeleteSuccess = () => {
|
||||
loadMetas();
|
||||
toast({
|
||||
title: 'Sucesso',
|
||||
description: 'Meta excluída com sucesso',
|
||||
});
|
||||
};
|
||||
|
||||
const handleNovaMeta = () => {
|
||||
setMetaParaEditar(null);
|
||||
setValorMetaAtual(0);
|
||||
setIsDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleSuccess = () => {
|
||||
setIsDialogOpen(false);
|
||||
loadData(userId);
|
||||
};
|
||||
|
||||
const getMesAtual = () => {
|
||||
const now = new Date();
|
||||
return {
|
||||
mes: now.getMonth() + 1,
|
||||
ano: now.getFullYear(),
|
||||
nome: format(now, 'MMMM', { locale: ptBR }),
|
||||
};
|
||||
};
|
||||
|
||||
const mesAtual = getMesAtual();
|
||||
|
||||
// Agrupar por ano (objeto com chave = ano e valor = array de metas daquele ano)
|
||||
const resultadosPorAno = resultados.reduce((acc, meta) => {
|
||||
if (!acc[meta.ano]) {
|
||||
acc[meta.ano] = [];
|
||||
}
|
||||
acc[meta.ano].push(meta);
|
||||
return acc;
|
||||
}, {} as Record<number, ResultadoMeta[]>);
|
||||
|
||||
// Ordenar os anos de forma decrescente
|
||||
const anosOrdenados = Object.keys(resultadosPorAno)
|
||||
.map(Number)
|
||||
.sort((a, b) => b - a);
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className="text-2xl font-bold tracking-tight">Metas de Economia</h1>
|
||||
<Button
|
||||
className="flex items-center gap-2 bg-finance-blue hover:bg-finance-blue/90"
|
||||
onClick={handleNovaMeta}
|
||||
>
|
||||
<PlusCircle className="h-4 w-4" />
|
||||
Nova Meta
|
||||
</Button>
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Metas Financeiras</h1>
|
||||
<p className="text-gray-600 mt-2">Defina e acompanhe seus objetivos financeiros</p>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center py-10">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin h-8 w-8 border-4 border-primary border-t-transparent rounded-full mx-auto"></div>
|
||||
<p className="mt-4 text-muted-foreground">Carregando suas metas...</p>
|
||||
</div>
|
||||
</div>
|
||||
) : resultados.length === 0 ? (
|
||||
<Card className="flex flex-col items-center justify-center py-10 px-4 text-center">
|
||||
<h3 className="text-xl font-semibold mb-2">Nenhuma meta definida</h3>
|
||||
<p className="text-muted-foreground mb-6">
|
||||
Defina sua primeira meta de economia para {mesAtual.nome} {mesAtual.ano}
|
||||
</p>
|
||||
<Button onClick={handleNovaMeta}>
|
||||
<PlusCircle className="h-4 w-4 mr-2" />
|
||||
Definir Meta
|
||||
</Button>
|
||||
</Card>
|
||||
) : (
|
||||
<>
|
||||
{anosOrdenados.map(ano => (
|
||||
<div key={ano} className="space-y-3">
|
||||
<h2 className="text-xl font-semibold">{ano}</h2>
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{resultadosPorAno[ano]
|
||||
.sort((a, b) => b.mes - a.mes)
|
||||
.map(meta => (
|
||||
<MetaProgressCard
|
||||
key={`${meta.ano}-${meta.mes}`}
|
||||
meta={meta}
|
||||
onEditClick={() => handleEditarMeta(meta.mes, meta.ano)}
|
||||
onDeleteClick={() => {
|
||||
// Aqui precisamos buscar o ID primeiro pois resultadoMeta não tem o ID
|
||||
// Vamos usar o metaParaEditar.id que já foi carregado se o usuário clicou em editar antes
|
||||
if (metaParaEditar && metaParaEditar.mes === meta.mes && metaParaEditar.ano === meta.ano && metaParaEditar.id) {
|
||||
handleDeletarMeta(meta.mes, meta.ano, metaParaEditar.id);
|
||||
} else {
|
||||
// Buscar o ID antes de deletar
|
||||
getMeta(userId, meta.mes, meta.ano).then(metaCompleta => {
|
||||
if (metaCompleta && metaCompleta.id) {
|
||||
handleDeletarMeta(meta.mes, meta.ano, metaCompleta.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{metaParaEditar ? 'Editar Meta' : 'Nova Meta'}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{metaParaEditar
|
||||
? 'Atualize os valores da sua meta de economia.'
|
||||
: 'Defina uma nova meta de economia mensal.'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<MetasForm
|
||||
onSuccess={handleSuccess}
|
||||
onCancel={() => setIsDialogOpen(false)}
|
||||
userId={userId}
|
||||
valorInicial={metaParaEditar ? valorMetaAtual : 0}
|
||||
mesInicial={metaParaEditar?.mes}
|
||||
anoInicial={metaParaEditar?.ano}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<AlertDialog
|
||||
open={isDeleteDialogOpen}
|
||||
onOpenChange={setIsDeleteDialogOpen}
|
||||
<Button
|
||||
onClick={() => setShowForm(true)}
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white"
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
Confirmar exclusão
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{metaParaDeletar && (
|
||||
`Tem certeza que deseja excluir a meta de ${format(new Date(metaParaDeletar.ano, metaParaDeletar.mes - 1, 1), 'MMMM', { locale: ptBR })} de ${metaParaDeletar.ano}? Esta ação não pode ser desfeita.`
|
||||
)}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isDeletingMeta}>Cancelar</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={confirmarDeletarMeta}
|
||||
disabled={isDeletingMeta}
|
||||
className="bg-destructive hover:bg-destructive/90"
|
||||
>
|
||||
{isDeletingMeta ? 'Deletando...' : 'Deletar Meta'}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Nova Meta
|
||||
</Button>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
{showForm && (
|
||||
<MetaForm
|
||||
meta={editingMeta}
|
||||
onSuccess={handleMetaSuccess}
|
||||
onCancel={() => {
|
||||
setShowForm(false);
|
||||
setEditingMeta(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isLoading ? (
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<Card key={i} className="animate-pulse">
|
||||
<CardHeader>
|
||||
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
|
||||
<div className="h-3 bg-gray-200 rounded w-1/2"></div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
<div className="h-3 bg-gray-200 rounded"></div>
|
||||
<div className="h-3 bg-gray-200 rounded w-5/6"></div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
) : metas.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent className="py-10 text-center">
|
||||
<div className="text-gray-400 mb-4">
|
||||
<Plus className="h-16 w-16 mx-auto" />
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">Nenhuma meta cadastrada</h3>
|
||||
<p className="text-gray-600 mb-4">Comece criando sua primeira meta financeira</p>
|
||||
<Button
|
||||
onClick={() => setShowForm(true)}
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Criar primeira meta
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{metas.map((meta) => (
|
||||
<MetaCard
|
||||
key={meta.id}
|
||||
meta={meta}
|
||||
onEdit={handleEditMeta}
|
||||
onDeleteSuccess={handleDeleteSuccess}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import Layout from '@/components/layout/Layout';
|
||||
import TransactionsTable from '@/components/dashboard/TransactionsTable';
|
||||
import { useTransactions } from '@/hooks/useTransactions';
|
||||
import { TransactionHeader } from '@/components/transacoes/TransactionHeader';
|
||||
@ -54,82 +53,80 @@ const TransacoesPage = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-2xl font-bold tracking-tight">Transações</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Dados de: {formatMonthDisplay(selectedMonth)}
|
||||
</p>
|
||||
{/* Exibe mensagem para trial OU para admin-liberou */}
|
||||
{!access.loading && access.podeAdicionarTransacao && (
|
||||
access.adminLiberou ? (
|
||||
<div className="mt-1 text-xs text-green-600 font-bold">
|
||||
{access.motivo}
|
||||
</div>
|
||||
) : (access.diasRestantesTrial > 0 && (
|
||||
<div className="mt-1 text-xs text-blue-600 font-medium">
|
||||
{`Você está em período gratuito. ${access.diasRestantesTrial} dia(s) restante(s) de teste.`}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
<MonthFilter
|
||||
selectedMonth={selectedMonth}
|
||||
onMonthChange={setSelectedMonth}
|
||||
/>
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-2xl font-bold tracking-tight">Transações</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Dados de: {formatMonthDisplay(selectedMonth)}
|
||||
</p>
|
||||
{/* Exibe mensagem para trial OU para admin-liberou */}
|
||||
{!access.loading && access.podeAdicionarTransacao && (
|
||||
access.adminLiberou ? (
|
||||
<div className="mt-1 text-xs text-green-600 font-bold">
|
||||
{access.motivo}
|
||||
</div>
|
||||
) : (access.diasRestantesTrial > 0 && (
|
||||
<div className="mt-1 text-xs text-blue-600 font-medium">
|
||||
{`Você está em período gratuito. ${access.diasRestantesTrial} dia(s) restante(s) de teste.`}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Mensagem de bloqueio */}
|
||||
{!access.loading && !access.podeAdicionarTransacao && (
|
||||
<div className="bg-yellow-100 border border-yellow-300 text-yellow-800 px-4 py-3 rounded relative mb-4">
|
||||
<span className="font-medium">Atenção:</span> {access.motivo}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Só passa as funções de adicionar se permitido */}
|
||||
<TransactionHeader
|
||||
onOpenDialog={access.podeAdicionarTransacao ? handleOpenDialog : () => {}}
|
||||
onOpenCartaoCreditoDialog={access.podeAdicionarTransacao ? handleOpenCartaoCreditoDialog : () => {}}
|
||||
disableAdicionar={!access.podeAdicionarTransacao}
|
||||
/>
|
||||
|
||||
{/* Resumo em Cards */}
|
||||
<TransactionSummaryCards
|
||||
totalReceitas={totalReceitas}
|
||||
totalDespesas={totalDespesas}
|
||||
totalCartoes={totalCartoes}
|
||||
formatCurrency={formatCurrency}
|
||||
/>
|
||||
|
||||
{/* Tabela completa de transações */}
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-xl font-semibold">Todas as Transações</h2>
|
||||
<TransactionsTable
|
||||
transactions={transactions}
|
||||
isLoading={isLoading}
|
||||
showPagination={true}
|
||||
onEdit={handleEditTransaction}
|
||||
onDelete={loadTransactions}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Diálogos */}
|
||||
<TransactionDialogs
|
||||
isDialogOpen={isDialogOpen}
|
||||
isCartaoCreditoDialogOpen={isCartaoCreditoDialogOpen}
|
||||
tipoForm={tipoForm}
|
||||
selectedTransaction={selectedTransaction}
|
||||
isEditing={isEditing}
|
||||
cartoes={cartoes}
|
||||
onTransactionSuccess={handleTransactionSuccess}
|
||||
onDespesaCartaoSuccess={handleDespesaCartaoSuccess}
|
||||
onCloseDialog={handleCloseDialog}
|
||||
onCloseCartaoCreditoDialog={handleCloseCartaoCreditoDialog}
|
||||
<MonthFilter
|
||||
selectedMonth={selectedMonth}
|
||||
onMonthChange={setSelectedMonth}
|
||||
/>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
{/* Mensagem de bloqueio */}
|
||||
{!access.loading && !access.podeAdicionarTransacao && (
|
||||
<div className="bg-yellow-100 border border-yellow-300 text-yellow-800 px-4 py-3 rounded relative mb-4">
|
||||
<span className="font-medium">Atenção:</span> {access.motivo}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Só passa as funções de adicionar se permitido */}
|
||||
<TransactionHeader
|
||||
onOpenDialog={access.podeAdicionarTransacao ? handleOpenDialog : () => {}}
|
||||
onOpenCartaoCreditoDialog={access.podeAdicionarTransacao ? handleOpenCartaoCreditoDialog : () => {}}
|
||||
disableAdicionar={!access.podeAdicionarTransacao}
|
||||
/>
|
||||
|
||||
{/* Resumo em Cards */}
|
||||
<TransactionSummaryCards
|
||||
totalReceitas={totalReceitas}
|
||||
totalDespesas={totalDespesas}
|
||||
totalCartoes={totalCartoes}
|
||||
formatCurrency={formatCurrency}
|
||||
/>
|
||||
|
||||
{/* Tabela completa de transações */}
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-xl font-semibold">Todas as Transações</h2>
|
||||
<TransactionsTable
|
||||
transactions={transactions}
|
||||
isLoading={isLoading}
|
||||
showPagination={true}
|
||||
onEdit={handleEditTransaction}
|
||||
onDelete={loadTransactions}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Diálogos */}
|
||||
<TransactionDialogs
|
||||
isDialogOpen={isDialogOpen}
|
||||
isCartaoCreditoDialogOpen={isCartaoCreditoDialogOpen}
|
||||
tipoForm={tipoForm}
|
||||
selectedTransaction={selectedTransaction}
|
||||
isEditing={isEditing}
|
||||
cartoes={cartoes}
|
||||
onTransactionSuccess={handleTransactionSuccess}
|
||||
onDespesaCartaoSuccess={handleDespesaCartaoSuccess}
|
||||
onCloseDialog={handleCloseDialog}
|
||||
onCloseCartaoCreditoDialog={handleCloseCartaoCreditoDialog}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
|
||||
import { WhatsAppInstance } from '@/types/whatsAppTypes';
|
||||
import Layout from '@/components/layout/Layout';
|
||||
import LoadingState from '@/components/whatsapp/LoadingState';
|
||||
import WhatsAppHeader from '@/components/whatsapp/WhatsAppHeader';
|
||||
import WhatsAppManager from '@/components/whatsapp/WhatsAppManager';
|
||||
@ -85,41 +84,37 @@ const WhatsApp = () => {
|
||||
|
||||
if (checkingExistingInstance || isLoading) {
|
||||
return (
|
||||
<Layout>
|
||||
<LoadingState message="Verificando suas instâncias..." />
|
||||
</Layout>
|
||||
<LoadingState message="Verificando suas instâncias..." />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="space-y-6">
|
||||
<WhatsAppHeader
|
||||
userEmail={userEmail}
|
||||
onRefresh={recheckInstance}
|
||||
isRefreshing={checkingExistingInstance}
|
||||
/>
|
||||
<div className="space-y-6">
|
||||
<WhatsAppHeader
|
||||
userEmail={userEmail}
|
||||
onRefresh={recheckInstance}
|
||||
isRefreshing={checkingExistingInstance}
|
||||
/>
|
||||
|
||||
<WhatsAppManager
|
||||
hasExistingInstance={hasExistingInstance}
|
||||
existingInstanceData={existingInstanceData}
|
||||
instances={instances}
|
||||
isRefreshing={isRefreshing}
|
||||
activeInstance={activeInstance}
|
||||
qrDialogOpen={qrDialogOpen}
|
||||
onInstanceCreated={handleInstanceCreated}
|
||||
onViewQrCode={handleViewQrCode}
|
||||
onDelete={handleDeleteInstanceWrapper}
|
||||
onRestart={handleRestartInstance}
|
||||
onLogout={handleLogoutInstance}
|
||||
onDisconnect={handleDisconnectInstance}
|
||||
onSetPresence={handleSetPresence}
|
||||
onRefreshInstances={refreshInstances}
|
||||
onStatusCheck={checkAllInstancesStatus}
|
||||
setQrDialogOpen={setQrDialogOpen}
|
||||
/>
|
||||
</div>
|
||||
</Layout>
|
||||
<WhatsAppManager
|
||||
hasExistingInstance={hasExistingInstance}
|
||||
existingInstanceData={existingInstanceData}
|
||||
instances={instances}
|
||||
isRefreshing={isRefreshing}
|
||||
activeInstance={activeInstance}
|
||||
qrDialogOpen={qrDialogOpen}
|
||||
onInstanceCreated={handleInstanceCreated}
|
||||
onViewQrCode={handleViewQrCode}
|
||||
onDelete={handleDeleteInstanceWrapper}
|
||||
onRestart={handleRestartInstance}
|
||||
onLogout={handleLogoutInstance}
|
||||
onDisconnect={handleDisconnectInstance}
|
||||
onSetPresence={handleSetPresence}
|
||||
onRefreshInstances={refreshInstances}
|
||||
onStatusCheck={checkAllInstancesStatus}
|
||||
setQrDialogOpen={setQrDialogOpen}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user