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

View File

@ -144,6 +144,39 @@ export type Database = {
}
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: {
Row: {
cartao_id: string | null
@ -294,10 +327,11 @@ export type Database = {
empresa: string | null
id: string
instancia_zap: string | null
nome: string
nome: string | null
remote_jid: string | null
senha: string
senha: string | null
status_instancia: string | null
webhook: string | null
whatsapp: string
}
Insert: {
@ -306,10 +340,11 @@ export type Database = {
empresa?: string | null
id?: string
instancia_zap?: string | null
nome: string
nome?: string | null
remote_jid?: string | null
senha: string
senha?: string | null
status_instancia?: string | null
webhook?: string | null
whatsapp: string
}
Update: {
@ -318,10 +353,11 @@ export type Database = {
empresa?: string | null
id?: string
instancia_zap?: string | null
nome?: string
nome?: string | null
remote_jid?: string | null
senha?: string
senha?: string | null
status_instancia?: string | null
webhook?: string | null
whatsapp?: string
}
Relationships: []

View File

@ -5,6 +5,7 @@ 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 { Wallet, ArrowUp, ArrowDown, PiggyBank, CreditCard } from 'lucide-react';
import { Transaction, CategorySummary, MonthlyData } from '@/types/financialTypes';
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 { 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
const setupUserId = useCallback(() => {
const storedUserId = localStorage.getItem('userId');
@ -42,19 +51,19 @@ const Dashboard = () => {
setupUserId();
}, [setupUserId]);
// Load data only once when component mounts
// Load data when component mounts or month changes
useEffect(() => {
async function loadData() {
try {
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([
getTransacoes(),
getTransactionSummary(),
getCategorySummary(),
getMonthlyData(),
getTransacoes(selectedMonth),
getTransactionSummary(selectedMonth),
getCategorySummary('despesa', selectedMonth),
getMonthlyData(), // Monthly data não precisa de filtro pois já mostra todos os meses
getCartoes()
]);
@ -67,7 +76,8 @@ const Dashboard = () => {
categories: categoriesData.length,
monthlyData: monthlyDataResult.length,
cartoes: cartoesData.length,
totalCartoes: totalCartoes
totalCartoes: totalCartoes,
mesEscolhido: selectedMonth
});
setTransactions(transacoesData);
@ -82,7 +92,7 @@ const Dashboard = () => {
toast({
title: "Dados carregados com sucesso",
description: "Seus dados financeiros foram atualizados"
description: `Dados financeiros de ${selectedMonth} foram atualizados`
});
} catch (error) {
console.error("Erro ao carregar dados:", error);
@ -97,9 +107,8 @@ const Dashboard = () => {
}
loadData();
// Ensure toast doesn't cause re-renders by removing it from dependencies
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [selectedMonth]);
const formatCurrency = (value: number) => {
return new Intl.NumberFormat('pt-BR', {
@ -111,14 +120,30 @@ const Dashboard = () => {
// 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">
<h1 className="text-2xl font-bold tracking-tight">Dashboard Financeiro</h1>
<p className="text-sm text-muted-foreground">
Última atualização: {new Date().toLocaleDateString('pt-BR')}
</p>
<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>
<MonthFilter
selectedMonth={selectedMonth}
onMonthChange={setSelectedMonth}
/>
</div>
<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 TransactionsTable from '@/components/dashboard/TransactionsTable';
import { useTransactions } from '@/hooks/useTransactions';
import { TransactionHeader } from '@/components/transacoes/TransactionHeader';
import { TransactionSummaryCards } from '@/components/transacoes/TransactionSummaryCards';
import { TransactionDialogs } from '@/components/transacoes/TransactionDialogs';
import { MonthFilter } from '@/components/filters/MonthFilter';
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 {
transactions,
isLoading,
@ -29,11 +38,34 @@ const TransacoesPage = () => {
handleOpenDialog,
handleOpenCartaoCreditoDialog,
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 (
<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>
</div>
<MonthFilter
selectedMonth={selectedMonth}
onMonthChange={setSelectedMonth}
/>
</div>
<TransactionHeader
onOpenDialog={handleOpenDialog}
onOpenCartaoCreditoDialog={handleOpenCartaoCreditoDialog}

View File

@ -4,11 +4,12 @@ import { CategorySummary, MonthlyData } from "@/types/financialTypes";
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
*/
export async function getTransactionSummary() {
console.log("Buscando resumo das transações...");
export async function getTransactionSummary(monthFilter?: string) {
console.log("Buscando resumo das transações...", monthFilter ? `Filtro do mês: ${monthFilter}` : "Sem filtro de mês");
const normalizedEmail = getUserEmail();
@ -20,12 +21,26 @@ export async function getTransactionSummary() {
// Get user groups by email
const groupIds = await getUserGroups(normalizedEmail);
// Fetch transaction summary with enhanced filter based on email (login) or group_id
const { data, error } = await supabase
// Build the query with month filter if provided
let query = supabase
.from('transacoes')
.select('tipo, valor')
.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) {
console.error('Erro ao buscar resumo das transações:', error);
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 monthFilter - Optional month filter in format 'YYYY-MM'
* @returns Promise with category summary data
*/
export async function getCategorySummary(tipoFiltro: string = 'despesa'): Promise<CategorySummary[]> {
console.log(`Buscando resumo de categorias para tipo: ${tipoFiltro}`);
export async function getCategorySummary(tipoFiltro: string = 'despesa', monthFilter?: string): Promise<CategorySummary[]> {
console.log(`Buscando resumo de categorias para tipo: ${tipoFiltro}`, monthFilter ? `Filtro do mês: ${monthFilter}` : "Sem filtro de mês");
const normalizedEmail = getUserEmail();
@ -73,12 +89,26 @@ export async function getCategorySummary(tipoFiltro: string = 'despesa'): Promis
// Get user groups by email
const groupIds = await getUserGroups(normalizedEmail);
// Fetch category summary with enhanced filter based on email (login) or group_id
const { data, error } = await supabase
// Build the query with month filter if provided
let query = supabase
.from('transacoes')
.select('categoria, valor, tipo')
.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) {
console.error('Erro ao buscar resumo de categorias:', error);
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";
/**
* 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
*/
export async function getTransacoes(): Promise<Transaction[]> {
console.log("Buscando transações do Supabase...");
export async function getTransacoes(monthFilter?: string): Promise<Transaction[]> {
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
const normalizedEmail = getUserEmail();
@ -24,13 +25,29 @@ export async function getTransacoes(): Promise<Transaction[]> {
const groupIds = await getUserGroups(normalizedEmail);
console.log(`Encontrados ${groupIds.length} grupos vinculados ao usuário:`, groupIds);
// Fetch transactions with filter based on email (login) or group_id
const { data, error } = await supabase
// Build the query with month filter if provided
let query = supabase
.from('transacoes')
.select('*')
.or(`login.eq.${normalizedEmail},${groupIds.length > 0 ? `grupo_id.in.(${groupIds.map(id => `"${id}"`).join(',')})` : ''}`)
.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) {
console.error('Erro ao buscar transações:', error);
throw new Error('Não foi possível carregar as transações');