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";
|
} 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,7 +63,51 @@ 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="space-y-4">
|
||||||
<div className="rounded-md border">
|
<div className="rounded-md border">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
@ -63,7 +118,7 @@ export function DespesasList({ despesas, isLoading }: DespesasListProps) {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{despesas.map((despesa) => (
|
{currentItems.map((despesa) => (
|
||||||
<TableRow key={despesa.id}>
|
<TableRow key={despesa.id}>
|
||||||
<TableCell className="font-medium">
|
<TableCell className="font-medium">
|
||||||
{formatDate(despesa.data_despesa)}
|
{formatDate(despesa.data_despesa)}
|
||||||
@ -77,5 +132,49 @@ export function DespesasList({ despesas, isLoading }: DespesasListProps) {
|
|||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</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 { 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user