Refactor: Split TransactionsTable into components
Refactors the TransactionsTable component into smaller, more manageable components for improved code organization and readability.
This commit is contained in:
parent
47e301c9bf
commit
c2c12ae5b3
@ -1,29 +1,12 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { format } from 'date-fns';
|
||||
import { ChevronDown, Search, ChevronLeft, ChevronRight, Edit, Trash2 } from 'lucide-react';
|
||||
import { Transaction } from '@/types/financialTypes';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { deleteTransacao } from '@/services/transacaoService';
|
||||
import { Table } from '@/components/ui/table';
|
||||
import TransactionTableSearch from './table/TransactionTableSearch';
|
||||
import TransactionTableHeader from './table/TransactionTableHeader';
|
||||
import TransactionTableBody from './table/TransactionTableBody';
|
||||
import TransactionTablePagination from './table/TransactionTablePagination';
|
||||
import TransactionDeleteDialog from './table/TransactionDeleteDialog';
|
||||
import { useTransactionTableLogic } from './table/useTransactionTableLogic';
|
||||
|
||||
interface TransactionsTableProps {
|
||||
transactions: Transaction[];
|
||||
@ -40,304 +23,74 @@ const TransactionsTable = ({
|
||||
onEdit,
|
||||
onDelete
|
||||
}: TransactionsTableProps) => {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [sortColumn, setSortColumn] = useState<string>('quando');
|
||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
|
||||
const [transactionToDelete, setTransactionToDelete] = useState<Transaction | null>(null);
|
||||
const { toast } = useToast();
|
||||
const itemsPerPage = showPagination ? 10 : 5;
|
||||
|
||||
const handleSort = (column: string) => {
|
||||
if (sortColumn === column) {
|
||||
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
|
||||
} else {
|
||||
setSortColumn(column);
|
||||
setSortDirection('asc');
|
||||
}
|
||||
};
|
||||
const {
|
||||
searchQuery,
|
||||
setSearchQuery,
|
||||
sortColumn,
|
||||
sortDirection,
|
||||
currentPage,
|
||||
setCurrentPage,
|
||||
deleteConfirmOpen,
|
||||
setDeleteConfirmOpen,
|
||||
transactionToDelete,
|
||||
totalPages,
|
||||
paginatedTransactions,
|
||||
filteredTransactions,
|
||||
itemsPerPage,
|
||||
formatCurrency,
|
||||
handleSort,
|
||||
handleDeleteClick,
|
||||
handleConfirmDelete,
|
||||
} = useTransactionTableLogic({ transactions, showPagination, onDelete });
|
||||
|
||||
const handleEditTransaction = (transaction: Transaction) => {
|
||||
if (onEdit) {
|
||||
onEdit(transaction);
|
||||
} else {
|
||||
console.log('Editar transação:', transaction);
|
||||
// Implementar edição futura se não houver callback
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteClick = (transaction: Transaction) => {
|
||||
setTransactionToDelete(transaction);
|
||||
setDeleteConfirmOpen(true);
|
||||
};
|
||||
|
||||
const handleConfirmDelete = async () => {
|
||||
if (!transactionToDelete) return;
|
||||
|
||||
try {
|
||||
await deleteTransacao(transactionToDelete.id);
|
||||
toast({
|
||||
title: "Transação excluída",
|
||||
description: "A transação foi removida com sucesso",
|
||||
});
|
||||
|
||||
// Callback para recarregar os dados
|
||||
if (onDelete) {
|
||||
onDelete();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro ao excluir transação:', error);
|
||||
toast({
|
||||
title: "Erro",
|
||||
description: "Não foi possível excluir a transação",
|
||||
variant: "destructive"
|
||||
});
|
||||
} finally {
|
||||
setDeleteConfirmOpen(false);
|
||||
setTransactionToDelete(null);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredTransactions = transactions.filter((transaction) => {
|
||||
const query = searchQuery.toLowerCase();
|
||||
return (
|
||||
transaction.estabelecimento?.toLowerCase().includes(query) ||
|
||||
transaction.detalhes?.toLowerCase().includes(query) ||
|
||||
transaction.categoria?.toLowerCase().includes(query) ||
|
||||
transaction.tipo?.toLowerCase().includes(query)
|
||||
);
|
||||
});
|
||||
|
||||
const sortedTransactions = [...filteredTransactions].sort((a, b) => {
|
||||
if (sortColumn === 'valor') {
|
||||
return sortDirection === 'asc' ? a.valor - b.valor : b.valor - a.valor;
|
||||
}
|
||||
|
||||
if (sortColumn === 'quando') {
|
||||
return sortDirection === 'asc'
|
||||
? new Date(a.quando).getTime() - new Date(b.quando).getTime()
|
||||
: new Date(b.quando).getTime() - new Date(a.quando).getTime();
|
||||
}
|
||||
|
||||
const aValue = a[sortColumn as keyof Transaction]?.toString().toLowerCase() || '';
|
||||
const bValue = b[sortColumn as keyof Transaction]?.toString().toLowerCase() || '';
|
||||
|
||||
return sortDirection === 'asc'
|
||||
? aValue.localeCompare(bValue)
|
||||
: bValue.localeCompare(aValue);
|
||||
});
|
||||
|
||||
// Paginação
|
||||
const totalPages = Math.ceil(sortedTransactions.length / itemsPerPage);
|
||||
const paginatedTransactions = sortedTransactions.slice(
|
||||
(currentPage - 1) * itemsPerPage,
|
||||
currentPage * itemsPerPage
|
||||
);
|
||||
|
||||
const formatCurrency = (value: number) => {
|
||||
return new Intl.NumberFormat('pt-BR', {
|
||||
style: 'currency',
|
||||
currency: 'BRL',
|
||||
}).format(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="relative w-full max-w-sm">
|
||||
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Buscar transações..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-8"
|
||||
/>
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="ml-2">
|
||||
Filtrar <ChevronDown className="ml-1 h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => setSearchQuery('')}>
|
||||
Todas
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setSearchQuery('receita')}>
|
||||
Receitas
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setSearchQuery('despesa')}>
|
||||
Despesas
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<TransactionTableSearch
|
||||
searchQuery={searchQuery}
|
||||
onSearchChange={setSearchQuery}
|
||||
/>
|
||||
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead
|
||||
className="cursor-pointer w-[160px]"
|
||||
onClick={() => handleSort('quando')}
|
||||
>
|
||||
Data {sortColumn === 'quando' && (sortDirection === 'asc' ? '↑' : '↓')}
|
||||
</TableHead>
|
||||
<TableHead
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleSort('estabelecimento')}
|
||||
>
|
||||
Estabelecimento {sortColumn === 'estabelecimento' && (sortDirection === 'asc' ? '↑' : '↓')}
|
||||
</TableHead>
|
||||
<TableHead
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleSort('detalhes')}
|
||||
>
|
||||
Detalhes {sortColumn === 'detalhes' && (sortDirection === 'asc' ? '↑' : '↓')}
|
||||
</TableHead>
|
||||
<TableHead
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleSort('categoria')}
|
||||
>
|
||||
Categoria {sortColumn === 'categoria' && (sortDirection === 'asc' ? '↑' : '↓')}
|
||||
</TableHead>
|
||||
<TableHead
|
||||
className="text-right cursor-pointer"
|
||||
onClick={() => handleSort('valor')}
|
||||
>
|
||||
Valor {sortColumn === 'valor' && (sortDirection === 'asc' ? '↑' : '↓')}
|
||||
</TableHead>
|
||||
<TableHead className="w-[120px] text-right">Ações</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
Array(5).fill(0).map((_, i) => (
|
||||
<TableRow key={`skeleton-${i}`}>
|
||||
{Array(6).fill(0).map((_, j) => (
|
||||
<TableCell key={`cell-${i}-${j}`} className="p-2">
|
||||
<div className="h-4 bg-muted rounded animate-pulse-gentle" />
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : paginatedTransactions.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="text-center py-8 text-muted-foreground">
|
||||
{searchQuery ? 'Nenhuma transação encontrada' : 'Não há transações disponíveis'}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
paginatedTransactions.map((transaction) => (
|
||||
<TableRow key={transaction.id}>
|
||||
<TableCell className="font-medium">
|
||||
{format(new Date(transaction.quando), 'dd/MM/yyyy')}
|
||||
</TableCell>
|
||||
<TableCell>{transaction.estabelecimento}</TableCell>
|
||||
<TableCell>{transaction.detalhes}</TableCell>
|
||||
<TableCell>
|
||||
<span className="inline-block px-2 py-1 text-xs font-medium rounded-md bg-secondary">
|
||||
{transaction.categoria}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className={cn(
|
||||
"text-right font-medium",
|
||||
transaction.tipo === 'receita' ? "text-green-600" : "text-red-600"
|
||||
)}>
|
||||
{formatCurrency(Math.abs(transaction.valor))}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex justify-end space-x-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleEditTransaction(transaction)}
|
||||
title="Editar"
|
||||
className="hover:bg-blue-50 hover:text-blue-600"
|
||||
>
|
||||
<Edit className="h-4 w-4 text-blue-600" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleDeleteClick(transaction)}
|
||||
title="Excluir"
|
||||
className="hover:bg-red-50 hover:text-red-600"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-red-600" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
<TransactionTableHeader
|
||||
sortColumn={sortColumn}
|
||||
sortDirection={sortDirection}
|
||||
onSort={handleSort}
|
||||
/>
|
||||
<TransactionTableBody
|
||||
transactions={paginatedTransactions}
|
||||
isLoading={isLoading}
|
||||
onEdit={handleEditTransaction}
|
||||
onDelete={handleDeleteClick}
|
||||
formatCurrency={formatCurrency}
|
||||
/>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{showPagination && totalPages > 0 && (
|
||||
<div className="flex items-center justify-between pt-2">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Mostrando <span className="font-medium">{Math.min(paginatedTransactions.length, itemsPerPage)}</span> de{" "}
|
||||
<span className="font-medium">{filteredTransactions.length}</span> transações
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
Anterior
|
||||
</Button>
|
||||
<div className="text-sm">
|
||||
Página <span className="font-medium">{currentPage}</span> de{" "}
|
||||
<span className="font-medium">{totalPages}</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
Próxima
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!showPagination && filteredTransactions.length > itemsPerPage && (
|
||||
<div className="flex justify-center pt-2">
|
||||
<Link to="/transacoes">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
Ver todas as transações
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
<TransactionTablePagination
|
||||
showPagination={showPagination}
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
itemsPerPage={itemsPerPage}
|
||||
totalItems={filteredTransactions.length}
|
||||
displayedItems={paginatedTransactions.length}
|
||||
onPageChange={setCurrentPage}
|
||||
/>
|
||||
|
||||
<AlertDialog open={deleteConfirmOpen} onOpenChange={setDeleteConfirmOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Confirmar exclusão</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Tem certeza que deseja excluir esta transação? Esta ação não pode ser desfeita.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancelar</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleConfirmDelete} className="bg-red-600 hover:bg-red-700">
|
||||
Excluir
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<TransactionDeleteDialog
|
||||
isOpen={deleteConfirmOpen}
|
||||
transaction={transactionToDelete}
|
||||
onClose={() => setDeleteConfirmOpen(false)}
|
||||
onConfirm={handleConfirmDelete}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
33
src/components/dashboard/table/TransactionDeleteDialog.tsx
Normal file
33
src/components/dashboard/table/TransactionDeleteDialog.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
|
||||
import { Transaction } from '@/types/financialTypes';
|
||||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog';
|
||||
|
||||
interface TransactionDeleteDialogProps {
|
||||
isOpen: boolean;
|
||||
transaction: Transaction | null;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
}
|
||||
|
||||
const TransactionDeleteDialog = ({ isOpen, transaction, onClose, onConfirm }: TransactionDeleteDialogProps) => {
|
||||
return (
|
||||
<AlertDialog open={isOpen} onOpenChange={onClose}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Confirmar exclusão</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Tem certeza que deseja excluir esta transação? Esta ação não pode ser desfeita.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancelar</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={onConfirm} className="bg-red-600 hover:bg-red-700">
|
||||
Excluir
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default TransactionDeleteDialog;
|
||||
100
src/components/dashboard/table/TransactionTableBody.tsx
Normal file
100
src/components/dashboard/table/TransactionTableBody.tsx
Normal file
@ -0,0 +1,100 @@
|
||||
|
||||
import { format } from 'date-fns';
|
||||
import { Edit, Trash2 } from 'lucide-react';
|
||||
import { Transaction } from '@/types/financialTypes';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { TableBody, TableCell, TableRow } from '@/components/ui/table';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface TransactionTableBodyProps {
|
||||
transactions: Transaction[];
|
||||
isLoading: boolean;
|
||||
onEdit: (transaction: Transaction) => void;
|
||||
onDelete: (transaction: Transaction) => void;
|
||||
formatCurrency: (value: number) => string;
|
||||
}
|
||||
|
||||
const TransactionTableBody = ({
|
||||
transactions,
|
||||
isLoading,
|
||||
onEdit,
|
||||
onDelete,
|
||||
formatCurrency
|
||||
}: TransactionTableBodyProps) => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<TableBody>
|
||||
{Array(5).fill(0).map((_, i) => (
|
||||
<TableRow key={`skeleton-${i}`}>
|
||||
{Array(6).fill(0).map((_, j) => (
|
||||
<TableCell key={`cell-${i}-${j}`} className="p-2">
|
||||
<div className="h-4 bg-muted rounded animate-pulse-gentle" />
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
);
|
||||
}
|
||||
|
||||
if (transactions.length === 0) {
|
||||
return (
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="text-center py-8 text-muted-foreground">
|
||||
Nenhuma transação encontrada
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TableBody>
|
||||
{transactions.map((transaction) => (
|
||||
<TableRow key={transaction.id}>
|
||||
<TableCell className="font-medium">
|
||||
{format(new Date(transaction.quando), 'dd/MM/yyyy')}
|
||||
</TableCell>
|
||||
<TableCell>{transaction.estabelecimento}</TableCell>
|
||||
<TableCell>{transaction.detalhes}</TableCell>
|
||||
<TableCell>
|
||||
<span className="inline-block px-2 py-1 text-xs font-medium rounded-md bg-secondary">
|
||||
{transaction.categoria}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className={cn(
|
||||
"text-right font-medium",
|
||||
transaction.tipo === 'receita' ? "text-green-600" : "text-red-600"
|
||||
)}>
|
||||
{formatCurrency(Math.abs(transaction.valor))}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex justify-end space-x-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => onEdit(transaction)}
|
||||
title="Editar"
|
||||
className="hover:bg-blue-50 hover:text-blue-600"
|
||||
>
|
||||
<Edit className="h-4 w-4 text-blue-600" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => onDelete(transaction)}
|
||||
title="Excluir"
|
||||
className="hover:bg-red-50 hover:text-red-600"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-red-600" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
);
|
||||
};
|
||||
|
||||
export default TransactionTableBody;
|
||||
54
src/components/dashboard/table/TransactionTableHeader.tsx
Normal file
54
src/components/dashboard/table/TransactionTableHeader.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
|
||||
import { TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||
|
||||
interface TransactionTableHeaderProps {
|
||||
sortColumn: string;
|
||||
sortDirection: 'asc' | 'desc';
|
||||
onSort: (column: string) => void;
|
||||
}
|
||||
|
||||
const TransactionTableHeader = ({ sortColumn, sortDirection, onSort }: TransactionTableHeaderProps) => {
|
||||
const getSortIcon = (column: string) => {
|
||||
return sortColumn === column ? (sortDirection === 'asc' ? '↑' : '↓') : '';
|
||||
};
|
||||
|
||||
return (
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead
|
||||
className="cursor-pointer w-[160px]"
|
||||
onClick={() => onSort('quando')}
|
||||
>
|
||||
Data {getSortIcon('quando')}
|
||||
</TableHead>
|
||||
<TableHead
|
||||
className="cursor-pointer"
|
||||
onClick={() => onSort('estabelecimento')}
|
||||
>
|
||||
Estabelecimento {getSortIcon('estabelecimento')}
|
||||
</TableHead>
|
||||
<TableHead
|
||||
className="cursor-pointer"
|
||||
onClick={() => onSort('detalhes')}
|
||||
>
|
||||
Detalhes {getSortIcon('detalhes')}
|
||||
</TableHead>
|
||||
<TableHead
|
||||
className="cursor-pointer"
|
||||
onClick={() => onSort('categoria')}
|
||||
>
|
||||
Categoria {getSortIcon('categoria')}
|
||||
</TableHead>
|
||||
<TableHead
|
||||
className="text-right cursor-pointer"
|
||||
onClick={() => onSort('valor')}
|
||||
>
|
||||
Valor {getSortIcon('valor')}
|
||||
</TableHead>
|
||||
<TableHead className="w-[120px] text-right">Ações</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
);
|
||||
};
|
||||
|
||||
export default TransactionTableHeader;
|
||||
@ -0,0 +1,78 @@
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
interface TransactionTablePaginationProps {
|
||||
showPagination: boolean;
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
itemsPerPage: number;
|
||||
totalItems: number;
|
||||
displayedItems: number;
|
||||
onPageChange: (page: number) => void;
|
||||
}
|
||||
|
||||
const TransactionTablePagination = ({
|
||||
showPagination,
|
||||
currentPage,
|
||||
totalPages,
|
||||
itemsPerPage,
|
||||
totalItems,
|
||||
displayedItems,
|
||||
onPageChange
|
||||
}: TransactionTablePaginationProps) => {
|
||||
if (showPagination && totalPages > 0) {
|
||||
return (
|
||||
<div className="flex items-center justify-between pt-2">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Mostrando <span className="font-medium">{Math.min(displayedItems, itemsPerPage)}</span> de{" "}
|
||||
<span className="font-medium">{totalItems}</span> transações
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onPageChange(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
Anterior
|
||||
</Button>
|
||||
<div className="text-sm">
|
||||
Página <span className="font-medium">{currentPage}</span> de{" "}
|
||||
<span className="font-medium">{totalPages}</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onPageChange(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
Próxima
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!showPagination && totalItems > itemsPerPage) {
|
||||
return (
|
||||
<div className="flex justify-center pt-2">
|
||||
<Link to="/transacoes">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
Ver todas as transações
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default TransactionTablePagination;
|
||||
52
src/components/dashboard/table/TransactionTableSearch.tsx
Normal file
52
src/components/dashboard/table/TransactionTableSearch.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Search, ChevronDown } from 'lucide-react';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
|
||||
interface TransactionTableSearchProps {
|
||||
searchQuery: string;
|
||||
onSearchChange: (query: string) => void;
|
||||
}
|
||||
|
||||
const TransactionTableSearch = ({ searchQuery, onSearchChange }: TransactionTableSearchProps) => {
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="relative w-full max-w-sm">
|
||||
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Buscar transações..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
className="pl-8"
|
||||
/>
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="ml-2">
|
||||
Filtrar <ChevronDown className="ml-1 h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => onSearchChange('')}>
|
||||
Todas
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onSearchChange('receita')}>
|
||||
Receitas
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onSearchChange('despesa')}>
|
||||
Despesas
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TransactionTableSearch;
|
||||
134
src/components/dashboard/table/useTransactionTableLogic.ts
Normal file
134
src/components/dashboard/table/useTransactionTableLogic.ts
Normal file
@ -0,0 +1,134 @@
|
||||
|
||||
import { useState, useMemo } from 'react';
|
||||
import { Transaction } from '@/types/financialTypes';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { deleteTransacao } from '@/services/transacaoService';
|
||||
|
||||
interface UseTransactionTableLogicProps {
|
||||
transactions: Transaction[];
|
||||
showPagination?: boolean;
|
||||
onDelete?: () => void;
|
||||
}
|
||||
|
||||
export const useTransactionTableLogic = ({
|
||||
transactions,
|
||||
showPagination = false,
|
||||
onDelete
|
||||
}: UseTransactionTableLogicProps) => {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [sortColumn, setSortColumn] = useState<string>('quando');
|
||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
|
||||
const [transactionToDelete, setTransactionToDelete] = useState<Transaction | null>(null);
|
||||
const { toast } = useToast();
|
||||
const itemsPerPage = showPagination ? 10 : 5;
|
||||
|
||||
const handleSort = (column: string) => {
|
||||
if (sortColumn === column) {
|
||||
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
|
||||
} else {
|
||||
setSortColumn(column);
|
||||
setSortDirection('asc');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteClick = (transaction: Transaction) => {
|
||||
setTransactionToDelete(transaction);
|
||||
setDeleteConfirmOpen(true);
|
||||
};
|
||||
|
||||
const handleConfirmDelete = async () => {
|
||||
if (!transactionToDelete) return;
|
||||
|
||||
try {
|
||||
await deleteTransacao(transactionToDelete.id);
|
||||
toast({
|
||||
title: "Transação excluída",
|
||||
description: "A transação foi removida com sucesso",
|
||||
});
|
||||
|
||||
if (onDelete) {
|
||||
onDelete();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro ao excluir transação:', error);
|
||||
toast({
|
||||
title: "Erro",
|
||||
description: "Não foi possível excluir a transação",
|
||||
variant: "destructive"
|
||||
});
|
||||
} finally {
|
||||
setDeleteConfirmOpen(false);
|
||||
setTransactionToDelete(null);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredTransactions = useMemo(() => {
|
||||
return transactions.filter((transaction) => {
|
||||
const query = searchQuery.toLowerCase();
|
||||
return (
|
||||
transaction.estabelecimento?.toLowerCase().includes(query) ||
|
||||
transaction.detalhes?.toLowerCase().includes(query) ||
|
||||
transaction.categoria?.toLowerCase().includes(query) ||
|
||||
transaction.tipo?.toLowerCase().includes(query)
|
||||
);
|
||||
});
|
||||
}, [transactions, searchQuery]);
|
||||
|
||||
const sortedTransactions = useMemo(() => {
|
||||
return [...filteredTransactions].sort((a, b) => {
|
||||
if (sortColumn === 'valor') {
|
||||
return sortDirection === 'asc' ? a.valor - b.valor : b.valor - a.valor;
|
||||
}
|
||||
|
||||
if (sortColumn === 'quando') {
|
||||
return sortDirection === 'asc'
|
||||
? new Date(a.quando).getTime() - new Date(b.quando).getTime()
|
||||
: new Date(b.quando).getTime() - new Date(a.quando).getTime();
|
||||
}
|
||||
|
||||
const aValue = a[sortColumn as keyof Transaction]?.toString().toLowerCase() || '';
|
||||
const bValue = b[sortColumn as keyof Transaction]?.toString().toLowerCase() || '';
|
||||
|
||||
return sortDirection === 'asc'
|
||||
? aValue.localeCompare(bValue)
|
||||
: bValue.localeCompare(aValue);
|
||||
});
|
||||
}, [filteredTransactions, sortColumn, sortDirection]);
|
||||
|
||||
const totalPages = Math.ceil(sortedTransactions.length / itemsPerPage);
|
||||
const paginatedTransactions = useMemo(() => {
|
||||
return sortedTransactions.slice(
|
||||
(currentPage - 1) * itemsPerPage,
|
||||
currentPage * itemsPerPage
|
||||
);
|
||||
}, [sortedTransactions, currentPage, itemsPerPage]);
|
||||
|
||||
const formatCurrency = (value: number) => {
|
||||
return new Intl.NumberFormat('pt-BR', {
|
||||
style: 'currency',
|
||||
currency: 'BRL',
|
||||
}).format(value);
|
||||
};
|
||||
|
||||
return {
|
||||
searchQuery,
|
||||
setSearchQuery,
|
||||
sortColumn,
|
||||
sortDirection,
|
||||
currentPage,
|
||||
setCurrentPage,
|
||||
deleteConfirmOpen,
|
||||
setDeleteConfirmOpen,
|
||||
transactionToDelete,
|
||||
totalPages,
|
||||
paginatedTransactions,
|
||||
filteredTransactions,
|
||||
itemsPerPage,
|
||||
formatCurrency,
|
||||
handleSort,
|
||||
handleDeleteClick,
|
||||
handleConfirmDelete,
|
||||
};
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user