feat: Add pagination to expenses list

Adds pagination to the DespesasList component to improve handling of large expense lists. Uses the Pagination component from ui/pagination.tsx.
This commit is contained in:
gpt-engineer-app[bot] 2025-05-21 00:56:16 +00:00
parent df6ee93e01
commit 4798390546
3 changed files with 178 additions and 111 deletions

View File

@ -12,6 +12,15 @@ import {
} from "@/components/ui/table";
import { formatDistanceToNow } from 'date-fns';
import { ptBR } from 'date-fns/locale';
import {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination";
interface DespesasListProps {
despesas: DespesaCartao[];
@ -20,7 +29,9 @@ interface DespesasListProps {
export function DespesasList({ despesas, isLoading }: DespesasListProps) {
const { toast } = useToast();
const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 5; // Number of expenses to display per page
const formatCurrency = (value: number) => {
return new Intl.NumberFormat('pt-BR', {
style: 'currency',
@ -52,30 +63,118 @@ export function DespesasList({ despesas, isLoading }: DespesasListProps) {
</div>;
}
// Calculate pagination values
const totalPages = Math.ceil(despesas.length / itemsPerPage);
const indexOfLastItem = currentPage * itemsPerPage;
const indexOfFirstItem = indexOfLastItem - itemsPerPage;
const currentItems = despesas.slice(indexOfFirstItem, indexOfLastItem);
// Generate page numbers for pagination
const pageNumbers = [];
for (let i = 1; i <= totalPages; i++) {
pageNumbers.push(i);
}
// Go to a specific page
const paginate = (pageNumber: number) => setCurrentPage(pageNumber);
// Go to previous page
const goToPreviousPage = () => {
if (currentPage > 1) {
setCurrentPage(currentPage - 1);
}
};
// Go to next page
const goToNextPage = () => {
if (currentPage < totalPages) {
setCurrentPage(currentPage + 1);
}
};
// Determine which page numbers to show
const getPageNumbers = () => {
const maxPagesToShow = 5;
let startPage = Math.max(1, currentPage - 2);
let endPage = Math.min(totalPages, startPage + maxPagesToShow - 1);
// Adjust if at the end of the range
if (endPage - startPage + 1 < maxPagesToShow && startPage > 1) {
startPage = Math.max(1, endPage - maxPagesToShow + 1);
}
return Array.from({ length: endPage - startPage + 1 }, (_, i) => startPage + i);
};
return (
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Data</TableHead>
<TableHead>Descrição</TableHead>
<TableHead className="text-right">Valor</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{despesas.map((despesa) => (
<TableRow key={despesa.id}>
<TableCell className="font-medium">
{formatDate(despesa.data_despesa)}
</TableCell>
<TableCell>{despesa.descricao}</TableCell>
<TableCell className="text-right">
{formatCurrency(despesa.valor)}
</TableCell>
<div className="space-y-4">
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Data</TableHead>
<TableHead>Descrição</TableHead>
<TableHead className="text-right">Valor</TableHead>
</TableRow>
))}
</TableBody>
</Table>
</TableHeader>
<TableBody>
{currentItems.map((despesa) => (
<TableRow key={despesa.id}>
<TableCell className="font-medium">
{formatDate(despesa.data_despesa)}
</TableCell>
<TableCell>{despesa.descricao}</TableCell>
<TableCell className="text-right">
{formatCurrency(despesa.valor)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
{totalPages > 1 && (
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
onClick={goToPreviousPage}
className={currentPage === 1 ? "pointer-events-none opacity-50" : "cursor-pointer"}
href="#"
/>
</PaginationItem>
{getPageNumbers().map((number) => (
<PaginationItem key={number}>
<PaginationLink
href="#"
isActive={number === currentPage}
onClick={(e) => {
e.preventDefault();
paginate(number);
}}
>
{number}
</PaginationLink>
</PaginationItem>
))}
{totalPages > 5 && currentPage < totalPages - 2 && (
<PaginationItem>
<PaginationEllipsis />
</PaginationItem>
)}
<PaginationItem>
<PaginationNext
onClick={goToNextPage}
className={currentPage === totalPages ? "pointer-events-none opacity-50" : "cursor-pointer"}
href="#"
/>
</PaginationItem>
</PaginationContent>
</Pagination>
)}
</div>
);
}

View File

@ -1,3 +1,4 @@
import { supabase } from "@/integrations/supabase/client";
import { CartaoCredito } from "@/types/cartaoTypes";
import { gerarCartaoCodigo } from "./cartaoCodigoUtils";
@ -23,34 +24,27 @@ export async function getCartoes(): Promise<CartaoCredito[]> {
const { data, error } = await supabase
.from('cartoes_credito')
.select('*')
.eq('login', normalizedEmail);
.eq('login', normalizedEmail)
.order('nome', { ascending: true });
if (error) {
console.error('Erro ao buscar cartões:', error);
throw new Error('Não foi possível carregar os cartões de crédito');
}
// Garantir que todos os dados têm os campos necessários da interface CartaoCredito
const cartoesCompletos = data.map(cartao => {
const cartaoCompleto = {
// Para cada cartão, obter o total de despesas
const cartoesComDespesas = await Promise.all(data.map(async (cartao) => {
const totalDespesas = await getTotalDespesasCartao(cartao.cartao_codigo || '');
return {
...cartao,
bandeira: cartao.bandeira || 'Não especificada',
banco: cartao.banco || 'Não especificado',
cartao_codigo: cartao.cartao_codigo || `cartao_${cartao.id.substring(0, 8)}`
bandeira: cartao.bandeira || '',
banco: cartao.banco || '',
cartao_codigo: cartao.cartao_codigo || '',
total_despesas: totalDespesas
} as CartaoCredito;
return cartaoCompleto;
});
}));
// Buscar o total de despesas para cada cartão - Promise.all já tipa corretamente o retorno
const cartoesComTotal = await Promise.all(
cartoesCompletos.map(async (cartao) => {
const total = await getTotalDespesasCartao(cartao.cartao_codigo);
return { ...cartao, total_despesas: total };
})
);
return cartoesComTotal;
return cartoesComDespesas;
} catch (error) {
console.error('Erro ao buscar cartões:', error);
return [];
@ -58,51 +52,59 @@ export async function getCartoes(): Promise<CartaoCredito[]> {
}
/**
* Checks if a card with the same name and bank already exists for the user
* @param nome Card name
* @param banco Bank name
* @returns Boolean indicating if the card exists
* Gets a specific credit card by ID
* @param cartaoId Card ID
* @returns Card details or null if not found
*/
export async function verificarCartaoExistente(nome: string, banco: string): Promise<boolean> {
export async function getCartao(cartaoId: string): Promise<CartaoCredito | null> {
// Obter o email do usuário do localStorage
const userEmail = localStorage.getItem('userEmail');
if (!userEmail) {
console.error('Email do usuário não encontrado no localStorage');
return false;
return null;
}
// Normalizar o email (minúsculo e sem espaços)
const normalizedEmail = userEmail.trim().toLowerCase();
try {
const { data, error, count } = await supabase
const { data, error } = await supabase
.from('cartoes_credito')
.select('*', { count: 'exact' })
.eq('login', normalizedEmail)
.eq('nome', nome)
.eq('banco', banco);
.select('*')
.eq('id', cartaoId)
.single();
if (error) {
console.error('Erro ao verificar cartão existente:', error);
return false;
console.error('Erro ao obter cartão:', error);
return null;
}
return count ? count > 0 : false;
// Obter o total de despesas para esse cartão
const totalDespesas = await getTotalDespesasCartao(data.cartao_codigo || '');
return {
...data,
bandeira: data.bandeira || '',
banco: data.banco || '',
cartao_codigo: data.cartao_codigo || '',
total_despesas: totalDespesas
} as CartaoCredito;
} catch (error) {
console.error('Erro ao verificar cartão existente:', error);
return false;
console.error('Erro ao obter cartão:', error);
return null;
}
}
/**
* Creates a new credit card
* Creates a new credit card for the user
* @param nome Card name
* @param bandeira Card brand
* @param banco Bank name
* @returns Created credit card or null if failed
* @param bandeira Card brand
* @returns Created card or null if failed
*/
export async function criarCartao(nome: string, bandeira: string, banco: string): Promise<CartaoCredito | null> {
export async function criarCartao(
nome: string,
banco: string,
bandeira: string
): Promise<CartaoCredito | null> {
// Obter o email do usuário do localStorage
const userEmail = localStorage.getItem('userEmail');
// Obter o ID do usuário do localStorage (mantido por compatibilidade)
@ -116,25 +118,19 @@ export async function criarCartao(nome: string, bandeira: string, banco: string)
// Normalizar o email (minúsculo e sem espaços)
const normalizedEmail = userEmail.trim().toLowerCase();
// Verificar se já existe um cartão com o mesmo nome e banco
const cartaoExistente = await verificarCartaoExistente(nome, banco);
if (cartaoExistente) {
throw new Error(`Você já possui um cartão ${nome} do banco ${banco}.`);
}
// Gerar um código único para o cartão
const cartao_codigo = gerarCartaoCodigo(nome, banco);
try {
// Gerar um código único para o cartão
const cartao_codigo = gerarCartaoCodigo(nome, bandeira);
const { data, error } = await supabase
.from('cartoes_credito')
.insert([{
nome: nome,
bandeira: bandeira,
banco: banco,
bandeira: bandeira,
cartao_codigo: cartao_codigo,
user_id: userId,
login: normalizedEmail
login: normalizedEmail,
user_id: userId
}])
.select();
@ -143,42 +139,13 @@ export async function criarCartao(nome: string, bandeira: string, banco: string)
throw new Error('Não foi possível criar o cartão de crédito');
}
return data[0] as CartaoCredito;
// Retornar o cartão criado
return {
...data[0],
total_despesas: 0
} as CartaoCredito;
} catch (error) {
console.error('Erro ao criar cartão:', error);
throw error;
}
}
/**
* Fetches details of a specific credit card
* @param cartaoId Card ID
* @returns Credit card or null if not found
*/
export async function getCartao(cartaoId: string): Promise<CartaoCredito | null> {
try {
const { data, error } = await supabase
.from('cartoes_credito')
.select('*')
.eq('id', cartaoId)
.single();
if (error) {
console.error('Erro ao buscar cartão:', error);
throw new Error('Não foi possível carregar os detalhes do cartão');
}
// Garantir que o objeto retornado tem todos os campos necessários
const cartaoCompleto = { ...data } as CartaoCredito;
// Garantir que cartao_codigo existe
if (!cartaoCompleto.cartao_codigo) {
cartaoCompleto.cartao_codigo = `cartao_${cartaoCompleto.id.substring(0, 8)}`;
}
return cartaoCompleto;
} catch (error) {
console.error('Erro ao buscar cartão:', error);
return null;
}
}

View File

@ -1,3 +1,4 @@
import { supabase } from "@/integrations/supabase/client";
import { DespesaCartao } from "@/types/cartaoTypes";
import { getCartao } from "./cartoesService";