460 lines
24 KiB
JavaScript
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;
|
|
|
|
|
|
|