From c2c12ae5b35f40b72119a91eff9a0b5013c51073 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Tue, 24 Jun 2025 18:44:40 +0000 Subject: [PATCH] Refactor: Split TransactionsTable into components Refactors the TransactionsTable component into smaller, more manageable components for improved code organization and readability. --- .../dashboard/TransactionsTable.tsx | 361 +++--------------- .../table/TransactionDeleteDialog.tsx | 33 ++ .../dashboard/table/TransactionTableBody.tsx | 100 +++++ .../table/TransactionTableHeader.tsx | 54 +++ .../table/TransactionTablePagination.tsx | 78 ++++ .../table/TransactionTableSearch.tsx | 52 +++ .../table/useTransactionTableLogic.ts | 134 +++++++ 7 files changed, 508 insertions(+), 304 deletions(-) create mode 100644 src/components/dashboard/table/TransactionDeleteDialog.tsx create mode 100644 src/components/dashboard/table/TransactionTableBody.tsx create mode 100644 src/components/dashboard/table/TransactionTableHeader.tsx create mode 100644 src/components/dashboard/table/TransactionTablePagination.tsx create mode 100644 src/components/dashboard/table/TransactionTableSearch.tsx create mode 100644 src/components/dashboard/table/useTransactionTableLogic.ts diff --git a/src/components/dashboard/TransactionsTable.tsx b/src/components/dashboard/TransactionsTable.tsx index e2fce59..dc68964 100644 --- a/src/components/dashboard/TransactionsTable.tsx +++ b/src/components/dashboard/TransactionsTable.tsx @@ -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('quando'); - const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc'); - const [currentPage, setCurrentPage] = useState(1); - const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false); - const [transactionToDelete, setTransactionToDelete] = useState(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 (
-
-
- - setSearchQuery(e.target.value)} - className="pl-8" - /> -
- - - - - - setSearchQuery('')}> - Todas - - setSearchQuery('receita')}> - Receitas - - setSearchQuery('despesa')}> - Despesas - - - -
+
- - - handleSort('quando')} - > - Data {sortColumn === 'quando' && (sortDirection === 'asc' ? '↑' : '↓')} - - handleSort('estabelecimento')} - > - Estabelecimento {sortColumn === 'estabelecimento' && (sortDirection === 'asc' ? '↑' : '↓')} - - handleSort('detalhes')} - > - Detalhes {sortColumn === 'detalhes' && (sortDirection === 'asc' ? '↑' : '↓')} - - handleSort('categoria')} - > - Categoria {sortColumn === 'categoria' && (sortDirection === 'asc' ? '↑' : '↓')} - - handleSort('valor')} - > - Valor {sortColumn === 'valor' && (sortDirection === 'asc' ? '↑' : '↓')} - - Ações - - - - {isLoading ? ( - Array(5).fill(0).map((_, i) => ( - - {Array(6).fill(0).map((_, j) => ( - -
- - ))} - - )) - ) : paginatedTransactions.length === 0 ? ( - - - {searchQuery ? 'Nenhuma transação encontrada' : 'Não há transações disponíveis'} - - - ) : ( - paginatedTransactions.map((transaction) => ( - - - {format(new Date(transaction.quando), 'dd/MM/yyyy')} - - {transaction.estabelecimento} - {transaction.detalhes} - - - {transaction.categoria} - - - - {formatCurrency(Math.abs(transaction.valor))} - - -
- - -
-
-
- )) - )} - + +
- {showPagination && totalPages > 0 && ( -
-
- Mostrando {Math.min(paginatedTransactions.length, itemsPerPage)} de{" "} - {filteredTransactions.length} transações -
-
- -
- Página {currentPage} de{" "} - {totalPages} -
- -
-
- )} - - {!showPagination && filteredTransactions.length > itemsPerPage && ( -
- - - -
- )} + - - - - Confirmar exclusão - - Tem certeza que deseja excluir esta transação? Esta ação não pode ser desfeita. - - - - Cancelar - - Excluir - - - - + setDeleteConfirmOpen(false)} + onConfirm={handleConfirmDelete} + />
); }; diff --git a/src/components/dashboard/table/TransactionDeleteDialog.tsx b/src/components/dashboard/table/TransactionDeleteDialog.tsx new file mode 100644 index 0000000..e409125 --- /dev/null +++ b/src/components/dashboard/table/TransactionDeleteDialog.tsx @@ -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 ( + + + + Confirmar exclusão + + Tem certeza que deseja excluir esta transação? Esta ação não pode ser desfeita. + + + + Cancelar + + Excluir + + + + + ); +}; + +export default TransactionDeleteDialog; diff --git a/src/components/dashboard/table/TransactionTableBody.tsx b/src/components/dashboard/table/TransactionTableBody.tsx new file mode 100644 index 0000000..b9d883d --- /dev/null +++ b/src/components/dashboard/table/TransactionTableBody.tsx @@ -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 ( + + {Array(5).fill(0).map((_, i) => ( + + {Array(6).fill(0).map((_, j) => ( + +
+ + ))} + + ))} + + ); + } + + if (transactions.length === 0) { + return ( + + + + Nenhuma transação encontrada + + + + ); + } + + return ( + + {transactions.map((transaction) => ( + + + {format(new Date(transaction.quando), 'dd/MM/yyyy')} + + {transaction.estabelecimento} + {transaction.detalhes} + + + {transaction.categoria} + + + + {formatCurrency(Math.abs(transaction.valor))} + + +
+ + +
+
+
+ ))} +
+ ); +}; + +export default TransactionTableBody; diff --git a/src/components/dashboard/table/TransactionTableHeader.tsx b/src/components/dashboard/table/TransactionTableHeader.tsx new file mode 100644 index 0000000..32df333 --- /dev/null +++ b/src/components/dashboard/table/TransactionTableHeader.tsx @@ -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 ( + + + onSort('quando')} + > + Data {getSortIcon('quando')} + + onSort('estabelecimento')} + > + Estabelecimento {getSortIcon('estabelecimento')} + + onSort('detalhes')} + > + Detalhes {getSortIcon('detalhes')} + + onSort('categoria')} + > + Categoria {getSortIcon('categoria')} + + onSort('valor')} + > + Valor {getSortIcon('valor')} + + Ações + + + ); +}; + +export default TransactionTableHeader; diff --git a/src/components/dashboard/table/TransactionTablePagination.tsx b/src/components/dashboard/table/TransactionTablePagination.tsx new file mode 100644 index 0000000..672e502 --- /dev/null +++ b/src/components/dashboard/table/TransactionTablePagination.tsx @@ -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 ( +
+
+ Mostrando {Math.min(displayedItems, itemsPerPage)} de{" "} + {totalItems} transações +
+
+ +
+ Página {currentPage} de{" "} + {totalPages} +
+ +
+
+ ); + } + + if (!showPagination && totalItems > itemsPerPage) { + return ( +
+ + + +
+ ); + } + + return null; +}; + +export default TransactionTablePagination; diff --git a/src/components/dashboard/table/TransactionTableSearch.tsx b/src/components/dashboard/table/TransactionTableSearch.tsx new file mode 100644 index 0000000..4bae580 --- /dev/null +++ b/src/components/dashboard/table/TransactionTableSearch.tsx @@ -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 ( +
+
+ + onSearchChange(e.target.value)} + className="pl-8" + /> +
+ + + + + + onSearchChange('')}> + Todas + + onSearchChange('receita')}> + Receitas + + onSearchChange('despesa')}> + Despesas + + + +
+ ); +}; + +export default TransactionTableSearch; diff --git a/src/components/dashboard/table/useTransactionTableLogic.ts b/src/components/dashboard/table/useTransactionTableLogic.ts new file mode 100644 index 0000000..5d7ebde --- /dev/null +++ b/src/components/dashboard/table/useTransactionTableLogic.ts @@ -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('quando'); + const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc'); + const [currentPage, setCurrentPage] = useState(1); + const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false); + const [transactionToDelete, setTransactionToDelete] = useState(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, + }; +};