testes/src/features/prafrot/components/ExcelTable.jsx

460 lines
24 KiB
JavaScript

import React, { useMemo, useState, useEffect } from 'react';
import { Edit2, Trash2, Filter, Columns, Layers, Download, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react';
import AdvancedFiltersModal from './AdvancedFiltersModal';
const ExcelTable = ({
data,
columns,
filterDefs = [],
onEdit,
onDelete,
onRowClick,
loading = false,
pageSize: initialPageSize = 50,
selectedIds = [],
onSelectionChange,
rowKey = 'id'
}) => {
const [showFilters, setShowFilters] = useState(false);
const [filters, setFilters] = useState({});
const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' });
// Pagination State
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(initialPageSize);
// ... (existing useMemo logic) ...
// Helper para lidar com seleção
// 1. Extract Unique Values...
const options = React.useMemo(() => {
const opts = {};
const getUnique = (key) => [...new Set(data.map(item => item[key]).filter(Boolean))].sort();
filterDefs.forEach(def => {
if (def.type === 'select') {
opts[def.field] = getUnique(def.field);
}
});
return opts;
}, [data, filterDefs]);
// 2. Process Data...
const processedData = React.useMemo(() => {
let result = [...data];
// Filter
Object.keys(filters).forEach(key => {
if (filters[key]) {
result = result.filter(item => {
const itemValue = String(item[key] || '').toLowerCase();
const filterValue = String(filters[key]).toLowerCase();
return itemValue.includes(filterValue);
});
}
});
// Sort
if (sortConfig.key) {
result.sort((a, b) => {
const valA = a[sortConfig.key] || '';
const valB = b[sortConfig.key] || '';
if (valA < valB) return sortConfig.direction === 'asc' ? -1 : 1;
if (valA > valB) return sortConfig.direction === 'asc' ? 1 : -1;
return 0;
});
}
return result;
}, [data, filters, sortConfig]);
// 3. Paginate...
const paginatedData = React.useMemo(() => {
const startIndex = (currentPage - 1) * pageSize;
return processedData.slice(startIndex, startIndex + pageSize);
}, [processedData, currentPage, pageSize]);
// Reset page...
useEffect(() => {
setCurrentPage(1);
}, [filters, sortConfig, pageSize]);
// Helper para lidar com seleção (Movido para cá para ter acesso a processedData)
const handleSelectAll = (e) => {
if (!onSelectionChange) return;
if (e.target.checked) {
const allIds = processedData.map(item => item[rowKey]);
onSelectionChange(allIds);
} else {
onSelectionChange([]);
}
};
const handleSelectRow = (id) => {
if (!onSelectionChange) return;
const newSelected = selectedIds.includes(id)
? selectedIds.filter(x => x !== id)
: [...selectedIds, id];
onSelectionChange(newSelected);
};
const isAllSelected = processedData && processedData.length > 0 && processedData.every(item => selectedIds.includes(item[rowKey]));
const isIndeterminate = processedData && processedData.some(item => selectedIds.includes(item[rowKey])) && !isAllSelected;
const totalPages = Math.ceil(processedData.length / pageSize);
const handleSort = (key) => {
setSortConfig(current => ({
key,
direction: current.key === key && current.direction === 'asc' ? 'desc' : 'asc'
}));
};
const handleApplyFilters = (newFilters) => {
setFilters(newFilters);
setShowFilters(false);
};
return (
<>
<div className="bg-white dark:bg-[#1b1b1b] border border-slate-200 dark:border-[#2a2a2a] rounded-xl flex flex-col h-full w-full max-w-full min-w-0 text-xs font-sans antialiased text-slate-700 dark:text-[#e0e0e0] relative overflow-hidden transition-colors">
{/* Loading Overlay */}
{loading && (
<div className="absolute inset-0 z-50 bg-white/80 dark:bg-[#1b1b1b]/80 backdrop-blur-sm flex items-center justify-center">
<div className="flex flex-col items-center gap-4">
<div className="relative">
<div className="w-12 h-12 border-4 border-slate-200 dark:border-[#252525] border-t-orange-500 rounded-full animate-spin"></div>
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-2 h-2 bg-orange-500 rounded-full animate-pulse"></div>
</div>
</div>
<span className="text-orange-500 font-bold uppercase tracking-widest text-[10px] animate-pulse">Carregando Dados...</span>
</div>
</div>
)}
{/* 1. Top Toolbar */}
<div className="flex items-center justify-between px-3 py-2 bg-white dark:bg-[#1b1b1b] border-b border-slate-200 dark:border-[#2a2a2a]">
{/* Left Actions */}
<div className="flex items-center gap-3">
{/* Filter Button */}
<button
onClick={() => setShowFilters(true)}
className={`flex items-center gap-1.5 px-3 py-1.5 border rounded font-semibold transition-colors ${
Object.keys(filters).length > 0
? 'bg-blue-50 dark:bg-blue-900/30 border-blue-200 dark:border-blue-800 text-blue-600 dark:text-blue-400 hover:bg-blue-100 dark:hover:bg-blue-900/50'
: 'bg-slate-50 dark:bg-[#252525] hover:bg-slate-100 dark:hover:bg-[#333] border-slate-200 dark:border-[#333] text-slate-600 dark:text-stone-300'
}`}
>
<Filter size={14} strokeWidth={2.5} />
<span className="uppercase tracking-wide text-[11px]">
{Object.keys(filters).length > 0 ? `${Object.keys(filters).length} Filtros` : 'Filtros Avançados'}
</span>
</button>
{/* Actions Button (Green Highlight) - Commented as requested
<button className="flex items-center gap-1.5 px-3 py-1.5 bg-orange-50 dark:bg-[#1f2824] hover:bg-orange-100 dark:hover:bg-[#25302b] border border-orange-200 dark:border-[#2b3832] rounded text-orange-600 dark:text-orange-400 font-semibold transition-colors">
<span className="uppercase tracking-wide text-[11px]">Ações</span>
<span className="bg-orange-100 dark:bg-[#15201b] text-orange-600 px-1 rounded text-[9px]">
<span className="w-2 h-2 rounded-full bg-orange-500 inline-block"></span>
</span>
</button>
*/}
</div>
{/* Right Actions - Commented out as requested
<div className="flex items-center gap-2">
<button className="flex items-center gap-1.5 px-3 py-1.5 hover:bg-[#252525] border border-transparent hover:border-[#333] rounded text-stone-400 transition-colors">
<Columns size={12} />
<span className="text-[10px] font-semibold">Colunas</span>
</button>
<button className="flex items-center gap-1.5 px-3 py-1.5 hover:bg-[#252525] border border-transparent hover:border-[#333] rounded text-stone-400 transition-colors">
<Layers size={12} />
<span className="text-[10px] font-semibold">Agrupar</span>
</button>
<button className="flex items-center gap-1.5 px-3 py-1.5 hover:bg-[#252525] border border-transparent hover:border-[#333] rounded text-stone-400 transition-colors">
<Download size={12} />
<span className="text-[10px] font-semibold">Exportar</span>
</button>
</div>
*/}
</div>
{/* 2. Table Header (The Complex Part) */}
<div className="overflow-x-auto overflow-y-auto flex-1 relative custom-scrollbar w-full min-w-0">
<style>{`
.custom-scrollbar {
scrollbar-width: thin;
scrollbar-color: #cbd5e1 #f8fafc;
}
.dark .custom-scrollbar {
scrollbar-color: #334155 #0f172a;
}
.custom-scrollbar::-webkit-scrollbar {
width: 10px;
height: 10px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: #f8fafc;
}
.dark .custom-scrollbar::-webkit-scrollbar-track {
background: #0f172a;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #cbd5e1;
border: 2px solid #f8fafc;
border-radius: 6px;
}
.dark .custom-scrollbar::-webkit-scrollbar-thumb {
background: #334155;
border: 2px solid #0f172a;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
.dark .custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: #475569;
}
.custom-scrollbar::-webkit-scrollbar-corner {
background: #f8fafc;
}
.dark .custom-scrollbar::-webkit-scrollbar-corner {
background: #0f172a;
}
`}</style>
<table className="border-collapse table-fixed custom-scrollbar w-full min-w-full">
<thead className="sticky top-0 z-20 bg-slate-50 dark:bg-[#1b1b1b] shadow-lg shadow-black/5 dark:shadow-black/20">
<tr className="border-b border-slate-200 dark:border-[#333]">
{/* Checkbox Header */}
<th className="sticky left-0 z-30 bg-slate-50 dark:bg-[#1b1b1b] w-[40px] min-w-[40px] h-[40px] border-r border-[#e2e8f0] dark:border-[#333]">
<div className="flex items-center justify-center h-full w-full">
<input
type="checkbox"
checked={isAllSelected}
ref={input => { if (input) input.indeterminate = isIndeterminate; }}
onChange={handleSelectAll}
disabled={!onSelectionChange}
className="appearance-none w-3.5 h-3.5 border-2 border-slate-300 dark:border-slate-600 rounded-sm bg-white dark:bg-[#2a2a2a] checked:bg-orange-500 checked:border-orange-500 transition-all cursor-pointer relative checked:bg-[url('data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2214%22%20height%3D%2214%22%20viewBox%3D%220%200%2014%2014%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M2.5%207L5.5%2010L11.5%204%22%20stroke%3D%22%23FFF%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%2F%3E%3C%2Fsvg%3E')] bg-center bg-no-repeat bg-[length:70%]"
/>
</div>
</th>
{onEdit || onDelete ? (
<th className="sticky left-[40px] z-30 bg-slate-50 dark:bg-[#1b1b1b] w-[60px] min-w-[60px] h-[40px] border-r border-slate-200 dark:border-[#333] px-2 text-left">
<div className="flex items-center h-full w-full uppercase font-bold text-slate-500 dark:text-[#e0e0e0] tracking-wider text-[11px]">
Ações
</div>
</th>
) : null}
{/* Dynamic Columns */}
{columns.map((col, idx) => (
<th
key={idx}
onClick={() => handleSort(col.field)}
className="h-[40px] border-r border-slate-200 dark:border-[#333] bg-slate-50 dark:bg-[#1b1b1b] px-3 relative group hover:bg-slate-100 dark:hover:bg-[#222] transition-colors cursor-pointer select-none"
style={{ width: col.width, minWidth: col.width }}
>
<div className="flex items-center justify-between h-full w-full">
<div className="flex items-center gap-1.5">
<span className={`uppercase font-bold tracking-wider text-[11px] ${sortConfig.key === col.field ? 'text-orange-600 dark:text-orange-500' : 'text-slate-500 dark:text-[#e0e0e0]'}`}>
{col.header}
</span>
</div>
{/* Sort/Menu Icons */}
<div className={`flex flex-col gap-0.5 ${sortConfig.key === col.field ? 'opacity-100' : 'opacity-40 group-hover:opacity-100'}`}>
<svg width="8" height="4" viewBox="0 0 8 4" fill="currentColor" className={`${sortConfig.key === col.field && sortConfig.direction === 'asc' ? 'text-orange-600 dark:text-orange-500' : 'text-slate-400 dark:text-stone-400'} rotate-180`}>
<path d="M4 4L0 0H8L4 4Z" />
</svg>
<svg width="8" height="4" viewBox="0 0 8 4" fill="currentColor" className={`${sortConfig.key === col.field && sortConfig.direction === 'desc' ? 'text-orange-600 dark:text-orange-500' : 'text-slate-400 dark:text-stone-400'}`}>
<path d="M4 4L0 0H8L4 4Z" />
</svg>
</div>
</div>
{/* Column Resizer Handle */}
<div className="absolute right-0 top-0 bottom-0 w-[4px] cursor-col-resize hover:bg-orange-500/50 z-10 translate-x-1/2" onClick={(e) => e.stopPropagation()} />
</th>
))}
{/* Spacer for scrollbar */}
<th className="bg-slate-50 dark:bg-[#1b1b1b] border-b border-slate-200 dark:border-[#333]"></th>
</tr>
</thead>
{/* 3. Table Body - Displaying Paginated Data */}
<tbody className="bg-white dark:bg-[#151515]">
{paginatedData.map((row, idx) => (
<tr
key={idx}
onClick={(e) => {
// Não disparar onRowClick se clicar em checkbox, botões ou links
if (e.target.closest('input[type="checkbox"]') ||
e.target.closest('button') ||
e.target.closest('a')) {
return;
}
onRowClick && onRowClick(row);
}}
className={`h-[44px] border-b border-slate-100 dark:border-[#252525] hover:bg-slate-50 dark:hover:bg-[#202020] transition-colors group text-sm ${idx % 2 === 0 ? 'bg-white dark:bg-[#151515]' : 'bg-slate-50/50 dark:bg-[#181818]'} ${idx === 0 ? '!bg-slate-100 dark:!bg-[#2b2b2b]' : ''} ${onRowClick ? 'cursor-pointer' : ''}`}>
{/* Checkbox Cell */}
<td className={`sticky left-0 z-10 border-r border-slate-100 dark:border-[#252525] px-0 text-center ${idx % 2 === 0 ? 'bg-white dark:bg-[#151515]' : 'bg-slate-50/50 dark:bg-[#181818]'} ${idx === 0 ? '!bg-slate-100 dark:!bg-[#2b2b2b]' : ''} group-hover:bg-slate-50 dark:group-hover:bg-[#202020]`}>
<div className="flex items-center justify-center h-full w-full">
<input
type="checkbox"
checked={selectedIds.includes(row[rowKey])}
onChange={() => handleSelectRow(row[rowKey])}
disabled={!onSelectionChange}
className="appearance-none w-3.5 h-3.5 border-2 border-slate-300 dark:border-slate-600 rounded-sm bg-white dark:bg-[#2a2a2a] checked:bg-orange-500 checked:border-orange-500 transition-all cursor-pointer relative checked:bg-[url('data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2214%22%20height%3D%2214%22%20viewBox%3D%220%200%2014%2014%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M2.5%207L5.5%2010L11.5%204%22%20stroke%3D%22%23FFF%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%2F%3E%3C%2Fsvg%3E')] bg-center bg-no-repeat bg-[length:70%]"
/>
</div>
</td>
{/* Actions Cell */}
{(onEdit || onDelete) ? (
<td className={`sticky left-[40px] z-10 border-r border-slate-100 dark:border-[#252525] px-0 text-center ${idx % 2 === 0 ? 'bg-white dark:bg-[#151515]' : 'bg-slate-50/50 dark:bg-[#181818]'} ${idx === 0 ? '!bg-slate-100 dark:!bg-[#2b2b2b]' : ''} group-hover:bg-slate-50 dark:group-hover:bg-[#202020]`}>
<div className="flex items-center justify-center h-full w-full gap-2">
<button
onClick={() => onEdit && onEdit(row)}
className="text-slate-500 dark:text-stone-500 hover:text-orange-600 dark:hover:text-stone-300 transition-colors p-1"
>
<Edit2 size={16} strokeWidth={2} />
</button>
{onDelete && (
<button
onClick={() => onDelete(row)}
className="text-slate-500 dark:text-stone-500 hover:text-rose-600 dark:hover:text-rose-400 transition-colors p-1"
>
<Trash2 size={16} strokeWidth={2} />
</button>
)}
</div>
</td>
) : null}
{/* Data Cells */}
{columns.map((col, cIdx) => (
<td key={cIdx} className="border-r border-slate-100 dark:border-[#252525] px-3 whitespace-nowrap overflow-hidden text-ellipsis text-slate-700 dark:text-stone-300">
{col.render ? col.render(row) : (
<span className={col.className}>{row[col.field] || '-'}</span>
)}
</td>
))}
<td className=""></td>
</tr>
))}
{/* Empty rows filler if needed */}
{!loading && paginatedData.length === 0 && (
<tr>
<td colSpan={columns.length + 3} className="h-24 text-center text-stone-500 italic">
{filters && Object.keys(filters).length > 0 ? 'Nenhum registro encontrado para os filtros selecionados.' : 'Nenhum registro disponível.'}
</td>
</tr>
)}
</tbody>
</table>
</div>
{/* 4. Footer Steps (Pagination Controls) */}
<div className="bg-slate-50 dark:bg-[#1b1b1b] border-t border-slate-200 dark:border-[#333] h-[40px] flex items-center justify-between px-2 overflow-hidden select-none">
{/* Left: Totals */}
<div className="flex h-full items-center gap-4">
<div className="flex items-center gap-2 px-4 h-full relative group min-w-[120px]">
<div className="absolute left-0 top-2 bottom-2 w-[3px] bg-orange-500 rounded-r-sm"></div>
<span className="text-[10px] uppercase font-bold text-slate-500 tracking-wider">Total:</span>
<span className="text-sm font-bold text-slate-800 dark:text-stone-200">{processedData.length}</span>
<div className="absolute right-0 top-2 bottom-2 w-[1px] bg-slate-200 dark:bg-[#333]"></div>
</div>
<div className="flex items-center gap-2 px-4 h-full relative group min-w-[100px]">
<div className="absolute left-0 top-2 bottom-2 w-[3px] bg-orange-500 rounded-r-sm opacity-0 group-hover:opacity-100 transition-opacity"></div>
<span className="text-[10px] uppercase font-bold text-slate-500 tracking-wider">Página:</span>
<span className="text-sm font-bold text-slate-800 dark:text-stone-200">{currentPage} / {totalPages || 1}</span>
</div>
</div>
{/* Right: Pagination Controls */}
<div className="flex items-center gap-1 pr-2">
<div className="text-[11px] text-slate-500 mr-3 hidden md:block">
Exibindo <span className="font-bold text-slate-700 dark:text-stone-300">{Math.min((currentPage - 1) * pageSize + 1, processedData.length)} a {Math.min(currentPage * pageSize, processedData.length)}</span>
</div>
{/* First Page */}
<button
onClick={() => setCurrentPage(1)}
disabled={currentPage === 1}
className="w-7 h-7 flex items-center justify-center rounded bg-white dark:bg-[#151515] border border-slate-200 dark:border-[#333] hover:border-orange-500 dark:hover:border-[#555] text-slate-500 dark:text-stone-400 group disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
>
<ChevronsLeft size={14} />
</button>
{/* Prev Page */}
<button
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
disabled={currentPage === 1}
className="w-7 h-7 flex items-center justify-center rounded bg-white dark:bg-[#151515] border border-slate-200 dark:border-[#333] hover:border-orange-500 dark:hover:border-[#555] text-slate-500 dark:text-stone-400 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
>
<ChevronLeft size={14} />
</button>
{/* Manual Page Buttons (Simple logic) */}
<div className="flex gap-1 mx-1">
<button className="w-7 h-7 flex items-center justify-center rounded bg-orange-500 text-white dark:text-black font-bold text-[11px] transition-colors shadow-lg shadow-orange-500/10">
{currentPage}
</button>
</div>
{/* Next Page */}
<button
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
disabled={currentPage === totalPages || totalPages === 0}
className="w-7 h-7 flex items-center justify-center rounded bg-white dark:bg-[#151515] border border-slate-200 dark:border-[#333] hover:border-orange-500 dark:hover:border-[#555] text-slate-500 dark:text-stone-400 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
>
<ChevronRight size={14} />
</button>
{/* Last Page */}
<button
onClick={() => setCurrentPage(totalPages)}
disabled={currentPage === totalPages || totalPages === 0}
className="w-7 h-7 flex items-center justify-center rounded bg-white dark:bg-[#151515] border border-slate-200 dark:border-[#333] hover:border-orange-500 dark:hover:border-[#555] text-slate-500 dark:text-stone-400 group disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
>
<ChevronsRight size={14} />
</button>
</div>
</div>
</div>
<AdvancedFiltersModal
isOpen={showFilters}
onClose={() => setShowFilters(false)}
onApply={handleApplyFilters}
options={options}
initialFilters={filters}
config={filterDefs}
/>
</>
);
};
export default ExcelTable;