budget-view-finance/src/components/dashboard/TransactionsTable.tsx
gpt-engineer-app[bot] 035484ff38 Fix: Apply color adjustments and UI improvements
- Apply color adjustments to transaction values, icons, and dashboard cards.
- Fix the visibility of the "Nova Receita" and "Nova Despesa" buttons.
- Address the issue of the site adopting a black color scheme.
- Add an indicator to the menu toggle button.
2025-06-23 03:07:38 +00:00

346 lines
12 KiB
TypeScript

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';
interface TransactionsTableProps {
transactions: Transaction[];
isLoading?: boolean;
showPagination?: boolean;
onEdit?: (transaction: Transaction) => void;
onDelete?: () => void;
}
const TransactionsTable = ({
transactions,
isLoading = false,
showPagination = false,
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 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>
<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>
</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>
)}
<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>
</div>
);
};
export default TransactionsTable;