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"; } from "@/components/ui/table";
import { formatDistanceToNow } from 'date-fns'; import { formatDistanceToNow } from 'date-fns';
import { ptBR } from 'date-fns/locale'; import { ptBR } from 'date-fns/locale';
import {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination";
interface DespesasListProps { interface DespesasListProps {
despesas: DespesaCartao[]; despesas: DespesaCartao[];
@ -20,6 +29,8 @@ interface DespesasListProps {
export function DespesasList({ despesas, isLoading }: DespesasListProps) { export function DespesasList({ despesas, isLoading }: DespesasListProps) {
const { toast } = useToast(); const { toast } = useToast();
const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 5; // Number of expenses to display per page
const formatCurrency = (value: number) => { const formatCurrency = (value: number) => {
return new Intl.NumberFormat('pt-BR', { return new Intl.NumberFormat('pt-BR', {
@ -52,30 +63,118 @@ export function DespesasList({ despesas, isLoading }: DespesasListProps) {
</div>; </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 ( return (
<div className="rounded-md border"> <div className="space-y-4">
<Table> <div className="rounded-md border">
<TableHeader> <Table>
<TableRow> <TableHeader>
<TableHead>Data</TableHead> <TableRow>
<TableHead>Descrição</TableHead> <TableHead>Data</TableHead>
<TableHead className="text-right">Valor</TableHead> <TableHead>Descrição</TableHead>
</TableRow> <TableHead className="text-right">Valor</TableHead>
</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>
</TableRow> </TableRow>
))} </TableHeader>
</TableBody> <TableBody>
</Table> {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> </div>
); );
} }

View File

@ -1,3 +1,4 @@
import { supabase } from "@/integrations/supabase/client"; import { supabase } from "@/integrations/supabase/client";
import { CartaoCredito } from "@/types/cartaoTypes"; import { CartaoCredito } from "@/types/cartaoTypes";
import { gerarCartaoCodigo } from "./cartaoCodigoUtils"; import { gerarCartaoCodigo } from "./cartaoCodigoUtils";
@ -23,34 +24,27 @@ export async function getCartoes(): Promise<CartaoCredito[]> {
const { data, error } = await supabase const { data, error } = await supabase
.from('cartoes_credito') .from('cartoes_credito')
.select('*') .select('*')
.eq('login', normalizedEmail); .eq('login', normalizedEmail)
.order('nome', { ascending: true });
if (error) { if (error) {
console.error('Erro ao buscar cartões:', error); console.error('Erro ao buscar cartões:', error);
throw new Error('Não foi possível carregar os cartões de crédito'); 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 // Para cada cartão, obter o total de despesas
const cartoesCompletos = data.map(cartao => { const cartoesComDespesas = await Promise.all(data.map(async (cartao) => {
const cartaoCompleto = { const totalDespesas = await getTotalDespesasCartao(cartao.cartao_codigo || '');
return {
...cartao, ...cartao,
bandeira: cartao.bandeira || 'Não especificada', bandeira: cartao.bandeira || '',
banco: cartao.banco || 'Não especificado', banco: cartao.banco || '',
cartao_codigo: cartao.cartao_codigo || `cartao_${cartao.id.substring(0, 8)}` cartao_codigo: cartao.cartao_codigo || '',
total_despesas: totalDespesas
} as CartaoCredito; } as CartaoCredito;
}));
return cartaoCompleto; return cartoesComDespesas;
});
// 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;
} catch (error) { } catch (error) {
console.error('Erro ao buscar cartões:', error); console.error('Erro ao buscar cartões:', error);
return []; 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 * Gets a specific credit card by ID
* @param nome Card name * @param cartaoId Card ID
* @param banco Bank name * @returns Card details or null if not found
* @returns Boolean indicating if the card exists
*/ */
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 // Obter o email do usuário do localStorage
const userEmail = localStorage.getItem('userEmail'); const userEmail = localStorage.getItem('userEmail');
if (!userEmail) { if (!userEmail) {
console.error('Email do usuário não encontrado no localStorage'); 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 { try {
const { data, error, count } = await supabase const { data, error } = await supabase
.from('cartoes_credito') .from('cartoes_credito')
.select('*', { count: 'exact' }) .select('*')
.eq('login', normalizedEmail) .eq('id', cartaoId)
.eq('nome', nome) .single();
.eq('banco', banco);
if (error) { if (error) {
console.error('Erro ao verificar cartão existente:', error); console.error('Erro ao obter cartão:', error);
return false; 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) { } catch (error) {
console.error('Erro ao verificar cartão existente:', error); console.error('Erro ao obter cartão:', error);
return false; return null;
} }
} }
/** /**
* Creates a new credit card * Creates a new credit card for the user
* @param nome Card name * @param nome Card name
* @param bandeira Card brand
* @param banco Bank name * @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 // Obter o email do usuário do localStorage
const userEmail = localStorage.getItem('userEmail'); const userEmail = localStorage.getItem('userEmail');
// Obter o ID do usuário do localStorage (mantido por compatibilidade) // 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) // Normalizar o email (minúsculo e sem espaços)
const normalizedEmail = userEmail.trim().toLowerCase(); 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 { try {
// Gerar um código único para o cartão
const cartao_codigo = gerarCartaoCodigo(nome, bandeira);
const { data, error } = await supabase const { data, error } = await supabase
.from('cartoes_credito') .from('cartoes_credito')
.insert([{ .insert([{
nome: nome, nome: nome,
bandeira: bandeira,
banco: banco, banco: banco,
bandeira: bandeira,
cartao_codigo: cartao_codigo, cartao_codigo: cartao_codigo,
user_id: userId, login: normalizedEmail,
login: normalizedEmail user_id: userId
}]) }])
.select(); .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'); 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) { } catch (error) {
console.error('Erro ao criar cartão:', 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; return null;
} }
} }

View File

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