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 { Transaction } from '@/types/financialTypes';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Table } from '@/components/ui/table';
|
||||||
import { Input } from '@/components/ui/input';
|
import TransactionTableSearch from './table/TransactionTableSearch';
|
||||||
import {
|
import TransactionTableHeader from './table/TransactionTableHeader';
|
||||||
DropdownMenu,
|
import TransactionTableBody from './table/TransactionTableBody';
|
||||||
DropdownMenuContent,
|
import TransactionTablePagination from './table/TransactionTablePagination';
|
||||||
DropdownMenuItem,
|
import TransactionDeleteDialog from './table/TransactionDeleteDialog';
|
||||||
DropdownMenuTrigger,
|
import { useTransactionTableLogic } from './table/useTransactionTableLogic';
|
||||||
} 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';
|
|
||||||
|
|
||||||
interface TransactionsTableProps {
|
interface TransactionsTableProps {
|
||||||
transactions: Transaction[];
|
transactions: Transaction[];
|
||||||
@ -40,304 +23,74 @@ const TransactionsTable = ({
|
|||||||
onEdit,
|
onEdit,
|
||||||
onDelete
|
onDelete
|
||||||
}: TransactionsTableProps) => {
|
}: TransactionsTableProps) => {
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const {
|
||||||
const [sortColumn, setSortColumn] = useState<string>('quando');
|
searchQuery,
|
||||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
|
setSearchQuery,
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
sortColumn,
|
||||||
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
|
sortDirection,
|
||||||
const [transactionToDelete, setTransactionToDelete] = useState<Transaction | null>(null);
|
currentPage,
|
||||||
const { toast } = useToast();
|
setCurrentPage,
|
||||||
const itemsPerPage = showPagination ? 10 : 5;
|
deleteConfirmOpen,
|
||||||
|
setDeleteConfirmOpen,
|
||||||
const handleSort = (column: string) => {
|
transactionToDelete,
|
||||||
if (sortColumn === column) {
|
totalPages,
|
||||||
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
|
paginatedTransactions,
|
||||||
} else {
|
filteredTransactions,
|
||||||
setSortColumn(column);
|
itemsPerPage,
|
||||||
setSortDirection('asc');
|
formatCurrency,
|
||||||
}
|
handleSort,
|
||||||
};
|
handleDeleteClick,
|
||||||
|
handleConfirmDelete,
|
||||||
|
} = useTransactionTableLogic({ transactions, showPagination, onDelete });
|
||||||
|
|
||||||
const handleEditTransaction = (transaction: Transaction) => {
|
const handleEditTransaction = (transaction: Transaction) => {
|
||||||
if (onEdit) {
|
if (onEdit) {
|
||||||
onEdit(transaction);
|
onEdit(transaction);
|
||||||
} else {
|
} else {
|
||||||
console.log('Editar transação:', transaction);
|
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 (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<TransactionTableSearch
|
||||||
<div className="relative w-full max-w-sm">
|
searchQuery={searchQuery}
|
||||||
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
onSearchChange={setSearchQuery}
|
||||||
<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>
|
|
||||||
|
|
||||||
<div className="rounded-md border">
|
<div className="rounded-md border">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TransactionTableHeader
|
||||||
<TableRow>
|
sortColumn={sortColumn}
|
||||||
<TableHead
|
sortDirection={sortDirection}
|
||||||
className="cursor-pointer w-[160px]"
|
onSort={handleSort}
|
||||||
onClick={() => handleSort('quando')}
|
/>
|
||||||
>
|
<TransactionTableBody
|
||||||
Data {sortColumn === 'quando' && (sortDirection === 'asc' ? '↑' : '↓')}
|
transactions={paginatedTransactions}
|
||||||
</TableHead>
|
isLoading={isLoading}
|
||||||
<TableHead
|
onEdit={handleEditTransaction}
|
||||||
className="cursor-pointer"
|
onDelete={handleDeleteClick}
|
||||||
onClick={() => handleSort('estabelecimento')}
|
formatCurrency={formatCurrency}
|
||||||
>
|
/>
|
||||||
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>
|
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showPagination && totalPages > 0 && (
|
<TransactionTablePagination
|
||||||
<div className="flex items-center justify-between pt-2">
|
showPagination={showPagination}
|
||||||
<div className="text-sm text-muted-foreground">
|
currentPage={currentPage}
|
||||||
Mostrando <span className="font-medium">{Math.min(paginatedTransactions.length, itemsPerPage)}</span> de{" "}
|
totalPages={totalPages}
|
||||||
<span className="font-medium">{filteredTransactions.length}</span> transações
|
itemsPerPage={itemsPerPage}
|
||||||
</div>
|
totalItems={filteredTransactions.length}
|
||||||
<div className="flex items-center space-x-2">
|
displayedItems={paginatedTransactions.length}
|
||||||
<Button
|
onPageChange={setCurrentPage}
|
||||||
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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<AlertDialog open={deleteConfirmOpen} onOpenChange={setDeleteConfirmOpen}>
|
<TransactionDeleteDialog
|
||||||
<AlertDialogContent>
|
isOpen={deleteConfirmOpen}
|
||||||
<AlertDialogHeader>
|
transaction={transactionToDelete}
|
||||||
<AlertDialogTitle>Confirmar exclusão</AlertDialogTitle>
|
onClose={() => setDeleteConfirmOpen(false)}
|
||||||
<AlertDialogDescription>
|
onConfirm={handleConfirmDelete}
|
||||||
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>
|
|
||||||
</div>
|
</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