feat: Add month filter to dashboard and transactions

Implement month filtering for the dashboard and transactions pages, allowing users to select a month and view data accordingly.
This commit is contained in:
gpt-engineer-app[bot] 2025-05-31 18:00:30 +00:00
parent e7b414724b
commit cb2d335aab
7 changed files with 249 additions and 43 deletions

View File

@ -0,0 +1,63 @@
import React from 'react';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Calendar } from 'lucide-react';
interface MonthFilterProps {
selectedMonth: string;
onMonthChange: (month: string) => void;
className?: string;
}
export const MonthFilter = ({ selectedMonth, onMonthChange, className }: MonthFilterProps) => {
const months = [
{ value: '2024-01', label: 'Janeiro 2024' },
{ value: '2024-02', label: 'Fevereiro 2024' },
{ value: '2024-03', label: 'Março 2024' },
{ value: '2024-04', label: 'Abril 2024' },
{ value: '2024-05', label: 'Maio 2024' },
{ value: '2024-06', label: 'Junho 2024' },
{ value: '2024-07', label: 'Julho 2024' },
{ value: '2024-08', label: 'Agosto 2024' },
{ value: '2024-09', label: 'Setembro 2024' },
{ value: '2024-10', label: 'Outubro 2024' },
{ value: '2024-11', label: 'Novembro 2024' },
{ value: '2024-12', label: 'Dezembro 2024' },
{ value: '2025-01', label: 'Janeiro 2025' },
{ value: '2025-02', label: 'Fevereiro 2025' },
{ value: '2025-03', label: 'Março 2025' },
{ value: '2025-04', label: 'Abril 2025' },
{ value: '2025-05', label: 'Maio 2025' },
{ value: '2025-06', label: 'Junho 2025' },
{ value: '2025-07', label: 'Julho 2025' },
{ value: '2025-08', label: 'Agosto 2025' },
{ value: '2025-09', label: 'Setembro 2025' },
{ value: '2025-10', label: 'Outubro 2025' },
{ value: '2025-11', label: 'Novembro 2025' },
{ value: '2025-12', label: 'Dezembro 2025' },
];
// 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')}`;
};
return (
<div className={`flex items-center space-x-2 ${className}`}>
<Calendar className="h-4 w-4 text-muted-foreground" />
<Select value={selectedMonth} onValueChange={onMonthChange}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Selecione o mês" />
</SelectTrigger>
<SelectContent>
{months.map((month) => (
<SelectItem key={month.value} value={month.value}>
{month.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
};

View File

@ -1,4 +1,3 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { Transaction } from '@/types/financialTypes'; import { Transaction } from '@/types/financialTypes';
import { CartaoCredito } from '@/types/cartaoTypes'; import { CartaoCredito } from '@/types/cartaoTypes';
@ -6,7 +5,11 @@ import { useToast } from '@/hooks/use-toast';
import { getTransacoes } from '@/services/transacao'; import { getTransacoes } from '@/services/transacao';
import { getCartoes } from '@/services/cartaoCreditoService'; import { getCartoes } from '@/services/cartaoCreditoService';
export const useTransactions = () => { interface UseTransactionsProps {
monthFilter?: string;
}
export const useTransactions = ({ monthFilter }: UseTransactionsProps = {}) => {
const [transactions, setTransactions] = useState<Transaction[]>([]); const [transactions, setTransactions] = useState<Transaction[]>([]);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [isDialogOpen, setIsDialogOpen] = useState(false); const [isDialogOpen, setIsDialogOpen] = useState(false);
@ -20,8 +23,8 @@ export const useTransactions = () => {
const loadTransactions = async () => { const loadTransactions = async () => {
try { try {
setIsLoading(true); setIsLoading(true);
console.log("Carregando todas as transações..."); console.log("Carregando transações com filtro:", monthFilter);
const data = await getTransacoes(); const data = await getTransacoes(monthFilter);
console.log(`${data.length} transações carregadas com sucesso`); console.log(`${data.length} transações carregadas com sucesso`);
setTransactions(data); setTransactions(data);
} catch (error) { } catch (error) {
@ -53,7 +56,7 @@ export const useTransactions = () => {
useEffect(() => { useEffect(() => {
loadTransactions(); loadTransactions();
loadCartoes(); loadCartoes();
}, []); }, [monthFilter]);
const handleTransactionSuccess = () => { const handleTransactionSuccess = () => {
setIsDialogOpen(false); setIsDialogOpen(false);

View File

@ -144,6 +144,39 @@ export type Database = {
} }
Relationships: [] Relationships: []
} }
conversas_zap: {
Row: {
created_at: string
date_time: string | null
id: number
id_transacao_gerada: number | null
login: string | null
msg_recebida: string | null
remote_jid: string | null
status_processamento: string | null
}
Insert: {
created_at?: string
date_time?: string | null
id?: number
id_transacao_gerada?: number | null
login?: string | null
msg_recebida?: string | null
remote_jid?: string | null
status_processamento?: string | null
}
Update: {
created_at?: string
date_time?: string | null
id?: number
id_transacao_gerada?: number | null
login?: string | null
msg_recebida?: string | null
remote_jid?: string | null
status_processamento?: string | null
}
Relationships: []
}
despesas_cartao: { despesas_cartao: {
Row: { Row: {
cartao_id: string | null cartao_id: string | null
@ -294,10 +327,11 @@ export type Database = {
empresa: string | null empresa: string | null
id: string id: string
instancia_zap: string | null instancia_zap: string | null
nome: string nome: string | null
remote_jid: string | null remote_jid: string | null
senha: string senha: string | null
status_instancia: string | null status_instancia: string | null
webhook: string | null
whatsapp: string whatsapp: string
} }
Insert: { Insert: {
@ -306,10 +340,11 @@ export type Database = {
empresa?: string | null empresa?: string | null
id?: string id?: string
instancia_zap?: string | null instancia_zap?: string | null
nome: string nome?: string | null
remote_jid?: string | null remote_jid?: string | null
senha: string senha?: string | null
status_instancia?: string | null status_instancia?: string | null
webhook?: string | null
whatsapp: string whatsapp: string
} }
Update: { Update: {
@ -318,10 +353,11 @@ export type Database = {
empresa?: string | null empresa?: string | null
id?: string id?: string
instancia_zap?: string | null instancia_zap?: string | null
nome?: string nome?: string | null
remote_jid?: string | null remote_jid?: string | null
senha?: string senha?: string | null
status_instancia?: string | null status_instancia?: string | null
webhook?: string | null
whatsapp?: string whatsapp?: string
} }
Relationships: [] Relationships: []

View File

@ -5,6 +5,7 @@ import SummaryCard from '@/components/dashboard/SummaryCard';
import TransactionsTable from '@/components/dashboard/TransactionsTable'; import TransactionsTable from '@/components/dashboard/TransactionsTable';
import CategoryChart from '@/components/dashboard/CategoryChart'; import CategoryChart from '@/components/dashboard/CategoryChart';
import MonthlyChart from '@/components/dashboard/MonthlyChart'; import MonthlyChart from '@/components/dashboard/MonthlyChart';
import { MonthFilter } from '@/components/filters/MonthFilter';
import { Wallet, ArrowUp, ArrowDown, PiggyBank, CreditCard } from 'lucide-react'; import { Wallet, ArrowUp, ArrowDown, PiggyBank, CreditCard } from 'lucide-react';
import { Transaction, CategorySummary, MonthlyData } from '@/types/financialTypes'; import { Transaction, CategorySummary, MonthlyData } from '@/types/financialTypes';
import { useToast } from "@/components/ui/use-toast"; import { useToast } from "@/components/ui/use-toast";
@ -24,6 +25,14 @@ const Dashboard = () => {
const [totals, setTotals] = useState({ receitas: 0, despesas: 0, saldo: 0, cartoes: 0 }); const [totals, setTotals] = useState({ receitas: 0, despesas: 0, saldo: 0, cartoes: 0 });
const { toast } = useToast(); const { toast } = useToast();
// 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());
// Use useCallback to ensure this function doesn't change on every render // Use useCallback to ensure this function doesn't change on every render
const setupUserId = useCallback(() => { const setupUserId = useCallback(() => {
const storedUserId = localStorage.getItem('userId'); const storedUserId = localStorage.getItem('userId');
@ -42,19 +51,19 @@ const Dashboard = () => {
setupUserId(); setupUserId();
}, [setupUserId]); }, [setupUserId]);
// Load data only once when component mounts // Load data when component mounts or month changes
useEffect(() => { useEffect(() => {
async function loadData() { async function loadData() {
try { try {
setIsLoading(true); setIsLoading(true);
console.log("Iniciando carregamento de dados..."); console.log("Iniciando carregamento de dados para o mês:", selectedMonth);
// Buscar todos os dados necessários // Buscar todos os dados necessários com filtro de mês
const [transacoesData, totalsData, categoriesData, monthlyDataResult, cartoesData] = await Promise.all([ const [transacoesData, totalsData, categoriesData, monthlyDataResult, cartoesData] = await Promise.all([
getTransacoes(), getTransacoes(selectedMonth),
getTransactionSummary(), getTransactionSummary(selectedMonth),
getCategorySummary(), getCategorySummary('despesa', selectedMonth),
getMonthlyData(), getMonthlyData(), // Monthly data não precisa de filtro pois já mostra todos os meses
getCartoes() getCartoes()
]); ]);
@ -67,7 +76,8 @@ const Dashboard = () => {
categories: categoriesData.length, categories: categoriesData.length,
monthlyData: monthlyDataResult.length, monthlyData: monthlyDataResult.length,
cartoes: cartoesData.length, cartoes: cartoesData.length,
totalCartoes: totalCartoes totalCartoes: totalCartoes,
mesEscolhido: selectedMonth
}); });
setTransactions(transacoesData); setTransactions(transacoesData);
@ -82,7 +92,7 @@ const Dashboard = () => {
toast({ toast({
title: "Dados carregados com sucesso", title: "Dados carregados com sucesso",
description: "Seus dados financeiros foram atualizados" description: `Dados financeiros de ${selectedMonth} foram atualizados`
}); });
} catch (error) { } catch (error) {
console.error("Erro ao carregar dados:", error); console.error("Erro ao carregar dados:", error);
@ -97,9 +107,8 @@ const Dashboard = () => {
} }
loadData(); loadData();
// Ensure toast doesn't cause re-renders by removing it from dependencies
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, [selectedMonth]);
const formatCurrency = (value: number) => { const formatCurrency = (value: number) => {
return new Intl.NumberFormat('pt-BR', { return new Intl.NumberFormat('pt-BR', {
@ -111,14 +120,30 @@ const Dashboard = () => {
// Calculate total of all expenses (regular + credit cards) // Calculate total of all expenses (regular + credit cards)
const totalDespesasGeral = totals.despesas + totals.cartoes; 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 ( return (
<Layout> <Layout>
<div className="space-y-6"> <div className="space-y-6">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<h1 className="text-2xl font-bold tracking-tight">Dashboard Financeiro</h1> <div className="space-y-1">
<p className="text-sm text-muted-foreground"> <h1 className="text-2xl font-bold tracking-tight">Dashboard Financeiro</h1>
Última atualização: {new Date().toLocaleDateString('pt-BR')} <p className="text-sm text-muted-foreground">
</p> Dados de: {formatMonthDisplay(selectedMonth)}
</p>
</div>
<MonthFilter
selectedMonth={selectedMonth}
onMonthChange={setSelectedMonth}
/>
</div> </div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4"> <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">

View File

@ -1,13 +1,22 @@
import React from 'react'; import React, { useState } from 'react';
import Layout from '@/components/layout/Layout'; import Layout from '@/components/layout/Layout';
import TransactionsTable from '@/components/dashboard/TransactionsTable'; import TransactionsTable from '@/components/dashboard/TransactionsTable';
import { useTransactions } from '@/hooks/useTransactions'; import { useTransactions } from '@/hooks/useTransactions';
import { TransactionHeader } from '@/components/transacoes/TransactionHeader'; import { TransactionHeader } from '@/components/transacoes/TransactionHeader';
import { TransactionSummaryCards } from '@/components/transacoes/TransactionSummaryCards'; import { TransactionSummaryCards } from '@/components/transacoes/TransactionSummaryCards';
import { TransactionDialogs } from '@/components/transacoes/TransactionDialogs'; import { TransactionDialogs } from '@/components/transacoes/TransactionDialogs';
import { MonthFilter } from '@/components/filters/MonthFilter';
const TransacoesPage = () => { const TransacoesPage = () => {
// 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 { const {
transactions, transactions,
isLoading, isLoading,
@ -29,11 +38,34 @@ const TransacoesPage = () => {
handleOpenDialog, handleOpenDialog,
handleOpenCartaoCreditoDialog, handleOpenCartaoCreditoDialog,
loadTransactions loadTransactions
} = useTransactions(); } = useTransactions({ monthFilter: selectedMonth });
// 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 ( return (
<Layout> <Layout>
<div className="space-y-6"> <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>
</div>
<MonthFilter
selectedMonth={selectedMonth}
onMonthChange={setSelectedMonth}
/>
</div>
<TransactionHeader <TransactionHeader
onOpenDialog={handleOpenDialog} onOpenDialog={handleOpenDialog}
onOpenCartaoCreditoDialog={handleOpenCartaoCreditoDialog} onOpenCartaoCreditoDialog={handleOpenCartaoCreditoDialog}

View File

@ -4,11 +4,12 @@ import { CategorySummary, MonthlyData } from "@/types/financialTypes";
import { getUserEmail, getUserGroups } from "./baseService"; import { getUserEmail, getUserGroups } from "./baseService";
/** /**
* Get transaction summary (totals for incomes and expenses) * Get transaction summary (totals for incomes and expenses) with optional month filter
* @param monthFilter - Optional month filter in format 'YYYY-MM'
* @returns Promise with summary data * @returns Promise with summary data
*/ */
export async function getTransactionSummary() { export async function getTransactionSummary(monthFilter?: string) {
console.log("Buscando resumo das transações..."); console.log("Buscando resumo das transações...", monthFilter ? `Filtro do mês: ${monthFilter}` : "Sem filtro de mês");
const normalizedEmail = getUserEmail(); const normalizedEmail = getUserEmail();
@ -20,12 +21,26 @@ export async function getTransactionSummary() {
// Get user groups by email // Get user groups by email
const groupIds = await getUserGroups(normalizedEmail); const groupIds = await getUserGroups(normalizedEmail);
// Fetch transaction summary with enhanced filter based on email (login) or group_id // Build the query with month filter if provided
const { data, error } = await supabase let query = supabase
.from('transacoes') .from('transacoes')
.select('tipo, valor') .select('tipo, valor')
.or(`login.eq.${normalizedEmail},${groupIds.length > 0 ? `grupo_id.in.(${groupIds.map(id => `"${id}"`).join(',')})` : ''}`); .or(`login.eq.${normalizedEmail},${groupIds.length > 0 ? `grupo_id.in.(${groupIds.map(id => `"${id}"`).join(',')})` : ''}`);
// Apply month filter if provided
if (monthFilter) {
const startDate = `${monthFilter}-01`;
const year = parseInt(monthFilter.split('-')[0]);
const month = parseInt(monthFilter.split('-')[1]);
const endDate = new Date(year, month, 0).toISOString().split('T')[0]; // Last day of the month
query = query
.gte('quando', startDate)
.lte('quando', `${endDate}T23:59:59.999Z`);
}
const { data, error } = await query;
if (error) { if (error) {
console.error('Erro ao buscar resumo das transações:', error); console.error('Erro ao buscar resumo das transações:', error);
throw new Error('Não foi possível carregar o resumo das transações'); throw new Error('Não foi possível carregar o resumo das transações');
@ -56,12 +71,13 @@ export async function getTransactionSummary() {
} }
/** /**
* Get category summary for transactions * Get category summary for transactions with optional month filter
* @param tipoFiltro Filter by transaction type ('receita', 'despesa', or 'all') * @param tipoFiltro Filter by transaction type ('receita', 'despesa', or 'all')
* @param monthFilter - Optional month filter in format 'YYYY-MM'
* @returns Promise with category summary data * @returns Promise with category summary data
*/ */
export async function getCategorySummary(tipoFiltro: string = 'despesa'): Promise<CategorySummary[]> { export async function getCategorySummary(tipoFiltro: string = 'despesa', monthFilter?: string): Promise<CategorySummary[]> {
console.log(`Buscando resumo de categorias para tipo: ${tipoFiltro}`); console.log(`Buscando resumo de categorias para tipo: ${tipoFiltro}`, monthFilter ? `Filtro do mês: ${monthFilter}` : "Sem filtro de mês");
const normalizedEmail = getUserEmail(); const normalizedEmail = getUserEmail();
@ -73,12 +89,26 @@ export async function getCategorySummary(tipoFiltro: string = 'despesa'): Promis
// Get user groups by email // Get user groups by email
const groupIds = await getUserGroups(normalizedEmail); const groupIds = await getUserGroups(normalizedEmail);
// Fetch category summary with enhanced filter based on email (login) or group_id // Build the query with month filter if provided
const { data, error } = await supabase let query = supabase
.from('transacoes') .from('transacoes')
.select('categoria, valor, tipo') .select('categoria, valor, tipo')
.or(`login.eq.${normalizedEmail},${groupIds.length > 0 ? `grupo_id.in.(${groupIds.map(id => `"${id}"`).join(',')})` : ''}`); .or(`login.eq.${normalizedEmail},${groupIds.length > 0 ? `grupo_id.in.(${groupIds.map(id => `"${id}"`).join(',')})` : ''}`);
// Apply month filter if provided
if (monthFilter) {
const startDate = `${monthFilter}-01`;
const year = parseInt(monthFilter.split('-')[0]);
const month = parseInt(monthFilter.split('-')[1]);
const endDate = new Date(year, month, 0).toISOString().split('T')[0]; // Last day of the month
query = query
.gte('quando', startDate)
.lte('quando', `${endDate}T23:59:59.999Z`);
}
const { data, error } = await query;
if (error) { if (error) {
console.error('Erro ao buscar resumo de categorias:', error); console.error('Erro ao buscar resumo de categorias:', error);
throw new Error('Não foi possível carregar o resumo por categoria'); throw new Error('Não foi possível carregar o resumo por categoria');

View File

@ -4,11 +4,12 @@ import { Transaction } from "@/types/financialTypes";
import { getUserEmail, getUserGroups } from "./baseService"; import { getUserEmail, getUserGroups } from "./baseService";
/** /**
* Fetch all transactions for the current user * Fetch all transactions for the current user with optional month filter
* @param monthFilter - Optional month filter in format 'YYYY-MM'
* @returns Promise with array of transactions * @returns Promise with array of transactions
*/ */
export async function getTransacoes(): Promise<Transaction[]> { export async function getTransacoes(monthFilter?: string): Promise<Transaction[]> {
console.log("Buscando transações do Supabase..."); console.log("Buscando transações do Supabase...", monthFilter ? `Filtro do mês: ${monthFilter}` : "Sem filtro de mês");
// Get the user's email from localStorage // Get the user's email from localStorage
const normalizedEmail = getUserEmail(); const normalizedEmail = getUserEmail();
@ -24,13 +25,29 @@ export async function getTransacoes(): Promise<Transaction[]> {
const groupIds = await getUserGroups(normalizedEmail); const groupIds = await getUserGroups(normalizedEmail);
console.log(`Encontrados ${groupIds.length} grupos vinculados ao usuário:`, groupIds); console.log(`Encontrados ${groupIds.length} grupos vinculados ao usuário:`, groupIds);
// Fetch transactions with filter based on email (login) or group_id // Build the query with month filter if provided
const { data, error } = await supabase let query = supabase
.from('transacoes') .from('transacoes')
.select('*') .select('*')
.or(`login.eq.${normalizedEmail},${groupIds.length > 0 ? `grupo_id.in.(${groupIds.map(id => `"${id}"`).join(',')})` : ''}`) .or(`login.eq.${normalizedEmail},${groupIds.length > 0 ? `grupo_id.in.(${groupIds.map(id => `"${id}"`).join(',')})` : ''}`)
.order('quando', { ascending: false }); .order('quando', { ascending: false });
// Apply month filter if provided
if (monthFilter) {
const startDate = `${monthFilter}-01`;
const year = parseInt(monthFilter.split('-')[0]);
const month = parseInt(monthFilter.split('-')[1]);
const endDate = new Date(year, month, 0).toISOString().split('T')[0]; // Last day of the month
query = query
.gte('quando', startDate)
.lte('quando', `${endDate}T23:59:59.999Z`);
console.log(`Aplicando filtro de mês: ${startDate} até ${endDate}`);
}
const { data, error } = await query;
if (error) { if (error) {
console.error('Erro ao buscar transações:', error); console.error('Erro ao buscar transações:', error);
throw new Error('Não foi possível carregar as transações'); throw new Error('Não foi possível carregar as transações');