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,7 +65,6 @@ 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>
|
||||
@ -144,7 +142,6 @@ const CalendarioPage = () => {
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -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,7 +106,6 @@ const CartoesCreditoPage = () => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="space-y-6">
|
||||
<CartaoActions
|
||||
isDetalhes={detalheCartaoAberto}
|
||||
@ -132,7 +130,6 @@ const CartoesCreditoPage = () => {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -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,7 +98,8 @@ 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">
|
||||
<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>
|
||||
@ -106,11 +107,13 @@ const CategoriasPage = () => {
|
||||
<div className="h-4 bg-muted rounded w-1/4 mb-3"></div>
|
||||
<div className="h-2 bg-muted rounded"></div>
|
||||
</div>
|
||||
</CardSpotlight>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
) : categories.length === 0 ? (
|
||||
<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">
|
||||
@ -121,14 +124,16 @@ const CategoriasPage = () => {
|
||||
: 'Verifique se existem transações cadastradas'}
|
||||
</p>
|
||||
</div>
|
||||
</CardSpotlight>
|
||||
</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">
|
||||
<Card key={category.categoria}>
|
||||
<CardContent className="p-6">
|
||||
<div className="pb-2">
|
||||
<div className="text-base flex items-center font-semibold relative z-20">
|
||||
<div className="text-base flex items-center font-semibold">
|
||||
<span
|
||||
className="w-3 h-3 rounded-full mr-2"
|
||||
style={{ backgroundColor: category.color }}
|
||||
@ -136,7 +141,7 @@ const CategoriasPage = () => {
|
||||
{category.categoria}
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative z-20">
|
||||
<div>
|
||||
<div className="text-2xl font-bold mb-2">
|
||||
{formatCurrency(category.valor)}
|
||||
</div>
|
||||
@ -154,15 +159,17 @@ const CategoriasPage = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardSpotlight>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<CardSpotlight className="mt-6">
|
||||
<Card className="mt-6">
|
||||
<CardContent className="p-6">
|
||||
<div>
|
||||
<h3 className="text-xl font-bold mb-4 relative z-20">Resumo de Categorias {tipoFiltro !== 'all' ? (tipoFiltro === 'despesa' ? '(Despesas)' : '(Receitas)') : ''}</h3>
|
||||
<h3 className="text-xl font-bold mb-4">Resumo de Categorias {tipoFiltro !== 'all' ? (tipoFiltro === 'despesa' ? '(Despesas)' : '(Receitas)') : ''}</h3>
|
||||
</div>
|
||||
<div className="relative z-20">
|
||||
<div>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
@ -193,7 +200,8 @@ const CategoriasPage = () => {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</CardSpotlight>
|
||||
</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,7 +64,6 @@ 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>
|
||||
@ -89,7 +87,6 @@ const GruposWhatsApp = () => {
|
||||
onDeleteGroup={handleDeleteGroup}
|
||||
/>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -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() {
|
||||
const storedUserId = localStorage.getItem('userId');
|
||||
const finDashUser = localStorage.getItem('finDashUser');
|
||||
|
||||
if (!storedUserId && finDashUser) {
|
||||
localStorage.setItem('userId', finDashUser);
|
||||
} else if (!storedUserId && !finDashUser) {
|
||||
const defaultId = '9f267008-9128-4a2f-b730-de0a0b5602a9';
|
||||
localStorage.setItem('userId', defaultId);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadResumo = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
console.log("🚀 [Dashboard] Iniciando carregamento de dados para o mês:", selectedMonth);
|
||||
|
||||
// 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"
|
||||
});
|
||||
}
|
||||
console.log("Carregando resumo financeiro...");
|
||||
const data = await getResumoFinanceiro();
|
||||
console.log("Resumo carregado:", data);
|
||||
setResumo(data);
|
||||
} catch (error) {
|
||||
console.error("💥 [Dashboard] Erro ao carregar dados:", error);
|
||||
console.error("Erro ao carregar resumo:", error);
|
||||
toast({
|
||||
title: "Erro ao carregar dados",
|
||||
description: "Verifique a conexão com o Supabase",
|
||||
title: "Erro ao carregar resumo",
|
||||
description: "Não foi possível obter os dados do Supabase",
|
||||
variant: "destructive"
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadResumo();
|
||||
}, [toast]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
loadData();
|
||||
}, [selectedMonth, toast]);
|
||||
|
||||
const formatCurrency = (value: number) => {
|
||||
return new Intl.NumberFormat('pt-BR', {
|
||||
style: 'currency',
|
||||
currency: 'BRL',
|
||||
}).format(value);
|
||||
};
|
||||
|
||||
// Calculate total of all expenses (regular + credit cards)
|
||||
const totalDespesasGeral = totals.despesas + totals.cartoes;
|
||||
|
||||
// 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>
|
||||
<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 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>
|
||||
|
||||
{/* 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 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>
|
||||
|
||||
{/* ---- 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="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>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-xl font-semibold">Transações Recentes</h2>
|
||||
<TransactionsTable transactions={transactions} isLoading={isLoading} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tour de Onboarding */}
|
||||
<OnboardingTour
|
||||
isOpen={tourIsOpen}
|
||||
currentStep={tourCurrentStep}
|
||||
onNext={tourNextStep}
|
||||
onSkip={skipTour}
|
||||
onClose={closeTour}
|
||||
<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={() => {}}
|
||||
/>
|
||||
</Layout>
|
||||
</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);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeletarMeta = (mes: number, ano: number, id: string) => {
|
||||
setMetaParaDeletar({ mes, ano, id });
|
||||
setIsDeleteDialogOpen(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);
|
||||
useEffect(() => {
|
||||
loadMetas();
|
||||
}, []);
|
||||
|
||||
const handleMetaSuccess = () => {
|
||||
loadMetas();
|
||||
setShowForm(false);
|
||||
setEditingMeta(null);
|
||||
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.`,
|
||||
title: 'Sucesso',
|
||||
description: editingMeta ? 'Meta atualizada com sucesso' : 'Meta criada com sucesso',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Erro ao deletar meta:', error);
|
||||
};
|
||||
|
||||
const handleEditMeta = (meta: MetaFinanceira) => {
|
||||
setEditingMeta(meta);
|
||||
setShowForm(true);
|
||||
};
|
||||
|
||||
const handleDeleteSuccess = () => {
|
||||
loadMetas();
|
||||
toast({
|
||||
title: 'Erro ao deletar meta',
|
||||
description: 'Não foi possível excluir a meta',
|
||||
variant: 'destructive',
|
||||
title: 'Sucesso',
|
||||
description: 'Meta excluída com sucesso',
|
||||
});
|
||||
} finally {
|
||||
setIsDeletingMeta(false);
|
||||
setIsDeleteDialogOpen(false);
|
||||
setMetaParaDeletar(null);
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
<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>
|
||||
<Button
|
||||
className="flex items-center gap-2 bg-finance-blue hover:bg-finance-blue/90"
|
||||
onClick={handleNovaMeta}
|
||||
onClick={() => setShowForm(true)}
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white"
|
||||
>
|
||||
<PlusCircle className="h-4 w-4" />
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Nova Meta
|
||||
</Button>
|
||||
</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);
|
||||
}
|
||||
});
|
||||
}
|
||||
{showForm && (
|
||||
<MetaForm
|
||||
meta={editingMeta}
|
||||
onSuccess={handleMetaSuccess}
|
||||
onCancel={() => {
|
||||
setShowForm(false);
|
||||
setEditingMeta(null);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</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}
|
||||
{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}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<AlertDialog
|
||||
open={isDeleteDialogOpen}
|
||||
onOpenChange={setIsDeleteDialogOpen}
|
||||
>
|
||||
<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>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -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,7 +53,6 @@ const TransacoesPage = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="space-y-1">
|
||||
@ -129,7 +127,6 @@ const TransacoesPage = () => {
|
||||
onCloseCartaoCreditoDialog={handleCloseCartaoCreditoDialog}
|
||||
/>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -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,14 +84,11 @@ const WhatsApp = () => {
|
||||
|
||||
if (checkingExistingInstance || isLoading) {
|
||||
return (
|
||||
<Layout>
|
||||
<LoadingState message="Verificando suas instâncias..." />
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="space-y-6">
|
||||
<WhatsAppHeader
|
||||
userEmail={userEmail}
|
||||
@ -119,7 +115,6 @@ const WhatsApp = () => {
|
||||
setQrDialogOpen={setQrDialogOpen}
|
||||
/>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user