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:
parent
df6ee93e01
commit
4798390546
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
|
||||
import { supabase } from "@/integrations/supabase/client";
|
||||
import { DespesaCartao } from "@/types/cartaoTypes";
|
||||
import { getCartao } from "./cartoesService";
|
||||
|
||||
Loading…
Reference in New Issue
Block a user