|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>TanStack Table with TailwindCSS</title> |
|
|
<script src="https://cdn.tailwindcss.com"></script> |
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
|
|
<script src="https://unpkg.com/@tanstack/table-core@8.9.3/build/umd/index.production.js"></script> |
|
|
<style> |
|
|
|
|
|
.table-container::-webkit-scrollbar { |
|
|
height: 8px; |
|
|
width: 8px; |
|
|
} |
|
|
.table-container::-webkit-scrollbar-track { |
|
|
background: #f1f1f1; |
|
|
border-radius: 4px; |
|
|
} |
|
|
.table-container::-webkit-scrollbar-thumb { |
|
|
background: #888; |
|
|
border-radius: 4px; |
|
|
} |
|
|
.table-container::-webkit-scrollbar-thumb:hover { |
|
|
background: #555; |
|
|
} |
|
|
|
|
|
|
|
|
@keyframes rowHover { |
|
|
0% { background-color: inherit; } |
|
|
100% { background-color: #f8fafc; } |
|
|
} |
|
|
|
|
|
.animate-row-hover:hover { |
|
|
animation: rowHover 0.2s ease-out forwards; |
|
|
} |
|
|
|
|
|
|
|
|
@keyframes spin { |
|
|
0% { transform: rotate(0deg); } |
|
|
100% { transform: rotate(360deg); } |
|
|
} |
|
|
|
|
|
.loading-spinner { |
|
|
animation: spin 1s linear infinite; |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body class="bg-gray-50 min-h-screen"> |
|
|
<div class="container mx-auto px-4 py-8"> |
|
|
<div class="bg-white rounded-xl shadow-md overflow-hidden p-6 mb-8"> |
|
|
<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-6 gap-4"> |
|
|
<div> |
|
|
<h1 class="text-2xl font-bold text-gray-800">Employee Directory</h1> |
|
|
<p class="text-gray-600">Manage your organization's employees</p> |
|
|
</div> |
|
|
<div class="flex flex-col sm:flex-row gap-3 w-full md:w-auto"> |
|
|
<div class="relative flex-grow"> |
|
|
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"> |
|
|
<i class="fas fa-search text-gray-400"></i> |
|
|
</div> |
|
|
<input |
|
|
type="text" |
|
|
id="searchInput" |
|
|
placeholder="Search employees..." |
|
|
class="pl-10 pr-4 py-2 border border-gray-300 rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" |
|
|
> |
|
|
</div> |
|
|
<button |
|
|
id="addEmployeeBtn" |
|
|
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center justify-center gap-2 transition-colors" |
|
|
> |
|
|
<i class="fas fa-plus"></i> |
|
|
<span>Add Employee</span> |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="flex flex-wrap items-center justify-between gap-3 mb-4"> |
|
|
<div class="flex items-center gap-2"> |
|
|
<label for="statusFilter" class="text-sm font-medium text-gray-700">Status:</label> |
|
|
<select |
|
|
id="statusFilter" |
|
|
class="border border-gray-300 rounded-md px-3 py-1 text-sm focus:outline-none focus:ring-1 focus:ring-blue-500" |
|
|
> |
|
|
<option value="all">All</option> |
|
|
<option value="active">Active</option> |
|
|
<option value="inactive">Inactive</option> |
|
|
<option value="on-leave">On Leave</option> |
|
|
</select> |
|
|
</div> |
|
|
|
|
|
<div class="flex items-center gap-2"> |
|
|
<label class="text-sm font-medium text-gray-700">Department:</label> |
|
|
<div class="flex flex-wrap gap-2"> |
|
|
<button data-dept="all" class="department-pill px-3 py-1 rounded-full text-xs bg-blue-100 text-blue-800 border border-blue-200 hover:bg-blue-200 active:bg-blue-300"> |
|
|
All |
|
|
</button> |
|
|
<button data-dept="Engineering" class="department-pill px-3 py-1 rounded-full text-xs bg-blue-100 text-blue-800 border border-blue-200 hover:bg-blue-200 active:bg-blue-300"> |
|
|
Engineering |
|
|
</button> |
|
|
<button data-dept="Marketing" class="department-pill px-3 py-1 rounded-full text-xs bg-purple-100 text-purple-800 border border-purple-200 hover:bg-purple-200 active:bg-purple-300"> |
|
|
Marketing |
|
|
</button> |
|
|
<button data-dept="Sales" class="department-pill px-3 py-1 rounded-full text-xs bg-green-100 text-green-800 border border-green-200 hover:bg-green-200 active:bg-green-300"> |
|
|
Sales |
|
|
</button> |
|
|
<button data-dept="HR" class="department-pill px-3 py-1 rounded-full text-xs bg-pink-100 text-pink-800 border border-pink-200 hover:bg-pink-200 active:bg-pink-300"> |
|
|
HR |
|
|
</button> |
|
|
<button data-dept="Finance" class="department-pill px-3 py-1 rounded-full text-xs bg-yellow-100 text-yellow-800 border border-yellow-200 hover:bg-yellow-200 active:bg-yellow-300"> |
|
|
Finance |
|
|
</button> |
|
|
<button data-dept="Product" class="department-pill px-3 py-1 rounded-full text-xs bg-indigo-100 text-indigo-800 border border-indigo-200 hover:bg-indigo-200 active:bg-indigo-300"> |
|
|
Product |
|
|
</button> |
|
|
<button data-dept="Design" class="department-pill px-3 py-1 rounded-full text-xs bg-red-100 text-red-800 border border-red-200 hover:bg-red-200 active:bg-red-300"> |
|
|
Design |
|
|
</button> |
|
|
<button data-dept="Operations" class="department-pill px-3 py-1 rounded-full text-xs bg-gray-100 text-gray-800 border border-gray-200 hover:bg-gray-200 active:bg-gray-300"> |
|
|
Operations |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="flex items-center gap-2"> |
|
|
<label for="rowsPerPage" class="text-sm font-medium text-gray-700">Rows:</label> |
|
|
<select |
|
|
id="rowsPerPage" |
|
|
class="border border-gray-300 rounded-md px-3 py-1 text-sm focus:outline-none focus:ring-1 focus:ring-blue-500" |
|
|
> |
|
|
<option value="5">5</option> |
|
|
<option value="10" selected>10</option> |
|
|
<option value="20">20</option> |
|
|
<option value="50">50</option> |
|
|
</select> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="table-container overflow-x-auto rounded-lg border border-gray-200"> |
|
|
<div id="loadingIndicator" class="hidden p-8 flex items-center justify-center"> |
|
|
<div class="loading-spinner h-8 w-8 border-4 border-blue-500 border-t-transparent rounded-full"></div> |
|
|
</div> |
|
|
<table id="dataTable" class="min-w-full divide-y divide-gray-200"> |
|
|
<thead class="bg-gray-50"> |
|
|
<tr id="tableHeader"></tr> |
|
|
</thead> |
|
|
<tbody id="tableBody" class="bg-white divide-y divide-gray-200"></tbody> |
|
|
</table> |
|
|
</div> |
|
|
|
|
|
<div class="flex flex-col sm:flex-row items-center justify-between mt-4 gap-4"> |
|
|
<div class="text-sm text-gray-600"> |
|
|
Showing <span id="showingFrom">1</span> to <span id="showingTo">10</span> of <span id="totalItems">50</span> entries |
|
|
</div> |
|
|
|
|
|
<div class="flex items-center gap-2"> |
|
|
<button id="firstPage" class="px-3 py-1 border rounded-md disabled:opacity-50"> |
|
|
<i class="fas fa-angle-double-left"></i> |
|
|
</button> |
|
|
<button id="prevPage" class="px-3 py-1 border rounded-md disabled:opacity-50"> |
|
|
<i class="fas fa-angle-left"></i> |
|
|
</button> |
|
|
<div id="pageNumbers" class="flex gap-1"></div> |
|
|
<button id="nextPage" class="px-3 py-1 border rounded-md disabled:opacity-50"> |
|
|
<i class="fas fa-angle-right"></i> |
|
|
</button> |
|
|
<button id="lastPage" class="px-3 py-1 border rounded-md disabled:opacity-50"> |
|
|
<i class="fas fa-angle-double-right"></i> |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div id="employeeModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden"> |
|
|
<div class="bg-white rounded-xl shadow-xl w-full max-w-md mx-4"> |
|
|
<div class="flex justify-between items-center border-b px-6 py-4"> |
|
|
<h3 class="text-lg font-semibold" id="modalTitle">Add Employee</h3> |
|
|
<button id="closeModal" class="text-gray-500 hover:text-gray-700"> |
|
|
<i class="fas fa-times"></i> |
|
|
</button> |
|
|
</div> |
|
|
<div class="p-6"> |
|
|
<form id="employeeForm"> |
|
|
<div class="mb-4"> |
|
|
<label for="name" class="block text-sm font-medium text-gray-700 mb-1">Full Name</label> |
|
|
<input type="text" id="name" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-1 focus:ring-blue-500"> |
|
|
</div> |
|
|
<div class="mb-4"> |
|
|
<label for="position" class="block text-sm font-medium text-gray-700 mb-1">Position</label> |
|
|
<input type="text" id="position" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-1 focus:ring-blue-500"> |
|
|
</div> |
|
|
<div class="mb-4"> |
|
|
<label for="department" class="block text-sm font-medium text-gray-700 mb-1">Department</label> |
|
|
<select id="department" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-1 focus:ring-blue-500"> |
|
|
<option value="Engineering">Engineering</option> |
|
|
<option value="Marketing">Marketing</option> |
|
|
<option value="Sales">Sales</option> |
|
|
<option value="HR">Human Resources</option> |
|
|
<option value="Finance">Finance</option> |
|
|
</select> |
|
|
</div> |
|
|
<div class="mb-4"> |
|
|
<label for="status" class="block text-sm font-medium text-gray-700 mb-1">Status</label> |
|
|
<select id="status" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-1 focus:ring-blue-500"> |
|
|
<option value="active">Active</option> |
|
|
<option value="inactive">Inactive</option> |
|
|
<option value="on-leave">On Leave</option> |
|
|
</select> |
|
|
</div> |
|
|
<div class="flex justify-end gap-3 mt-6"> |
|
|
<button type="button" id="cancelBtn" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50">Cancel</button> |
|
|
<button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700">Save</button> |
|
|
</div> |
|
|
</form> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
|
|
|
const departments = ['Engineering', 'Marketing', 'Sales', 'HR', 'Finance', 'Product', 'Design', 'Operations']; |
|
|
const positions = { |
|
|
Engineering: ['Software Engineer', 'Frontend Developer', 'Backend Developer', 'DevOps Engineer', 'QA Engineer', 'Data Engineer'], |
|
|
Marketing: ['Marketing Specialist', 'Content Writer', 'SEO Specialist', 'Social Media Manager'], |
|
|
Sales: ['Sales Executive', 'Sales Manager', 'Account Executive', 'Business Development'], |
|
|
HR: ['HR Coordinator', 'Recruiter', 'HR Manager', 'Talent Acquisition'], |
|
|
Finance: ['Finance Manager', 'Accountant', 'Financial Analyst', 'Controller'], |
|
|
Product: ['Product Manager', 'Product Owner', 'Product Designer'], |
|
|
Design: ['UX Designer', 'UI Designer', 'Graphic Designer'], |
|
|
Operations: ['Operations Manager', 'Office Administrator'] |
|
|
}; |
|
|
const statuses = ['active', 'inactive', 'on-leave']; |
|
|
|
|
|
const firstNames = ['John', 'Jane', 'Robert', 'Emily', 'Michael', 'Sarah', 'David', 'Jessica', 'Thomas', 'Lisa', 'William', 'Karen', 'James', 'Nancy', 'Daniel', 'Jennifer', 'Richard', 'Susan', 'Joseph', 'Margaret']; |
|
|
const lastNames = ['Doe', 'Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Miller', 'Davis', 'Garcia', 'Rodriguez', 'Wilson', 'Martinez', 'Anderson', 'Taylor', 'Thomas', 'Lee', 'Perez', 'Thompson', 'White', 'Harris']; |
|
|
|
|
|
const data = []; |
|
|
for (let i = 1; i <= 100; i++) { |
|
|
const dept = departments[Math.floor(Math.random() * departments.length)]; |
|
|
const pos = positions[dept][Math.floor(Math.random() * positions[dept].length)]; |
|
|
const status = statuses[Math.floor(Math.random() * statuses.length)]; |
|
|
const firstName = firstNames[Math.floor(Math.random() * firstNames.length)]; |
|
|
const lastName = lastNames[Math.floor(Math.random() * lastNames.length)]; |
|
|
const joinDate = new Date(2018 + Math.floor(Math.random() * 5), Math.floor(Math.random() * 12), Math.floor(Math.random() * 28) + 1); |
|
|
|
|
|
data.push({ |
|
|
id: i, |
|
|
name: `${firstName} ${lastName}`, |
|
|
position: pos, |
|
|
department: dept, |
|
|
status: status, |
|
|
joinDate: joinDate.toISOString().split('T')[0] |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
const table = window.TanStackTableCore; |
|
|
|
|
|
|
|
|
const columns = [ |
|
|
{ |
|
|
accessorKey: 'id', |
|
|
header: 'ID', |
|
|
size: 80, |
|
|
cell: info => info.getValue(), |
|
|
}, |
|
|
{ |
|
|
accessorKey: 'name', |
|
|
header: 'Name', |
|
|
cell: info => { |
|
|
const name = info.getValue(); |
|
|
return `<div class="flex items-center gap-3"> |
|
|
<div class="h-8 w-8 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 font-medium"> |
|
|
${name.charAt(0)} |
|
|
</div> |
|
|
<span class="font-medium">${name}</span> |
|
|
</div>`; |
|
|
}, |
|
|
}, |
|
|
{ |
|
|
accessorKey: 'position', |
|
|
header: 'Position', |
|
|
cell: info => `<span class="text-gray-700">${info.getValue()}</span>`, |
|
|
}, |
|
|
{ |
|
|
accessorKey: 'department', |
|
|
header: 'Department', |
|
|
cell: info => { |
|
|
const dept = info.getValue(); |
|
|
let bgColor = 'bg-gray-100 text-gray-800'; |
|
|
if (dept === 'Engineering') bgColor = 'bg-blue-100 text-blue-800'; |
|
|
if (dept === 'Marketing') bgColor = 'bg-purple-100 text-purple-800'; |
|
|
if (dept === 'Sales') bgColor = 'bg-green-100 text-green-800'; |
|
|
if (dept === 'HR') bgColor = 'bg-pink-100 text-pink-800'; |
|
|
if (dept === 'Finance') bgColor = 'bg-yellow-100 text-yellow-800'; |
|
|
|
|
|
return `<span class="px-2 py-1 text-xs rounded-full ${bgColor}">${dept}</span>`; |
|
|
}, |
|
|
}, |
|
|
{ |
|
|
accessorKey: 'status', |
|
|
header: 'Status', |
|
|
cell: info => { |
|
|
const status = info.getValue(); |
|
|
let bgColor = 'bg-gray-100 text-gray-800'; |
|
|
let icon = 'fa-circle'; |
|
|
if (status === 'active') { |
|
|
bgColor = 'bg-green-100 text-green-800'; |
|
|
icon = 'fa-check-circle'; |
|
|
} |
|
|
if (status === 'inactive') { |
|
|
bgColor = 'bg-red-100 text-red-800'; |
|
|
icon = 'fa-times-circle'; |
|
|
} |
|
|
if (status === 'on-leave') { |
|
|
bgColor = 'bg-yellow-100 text-yellow-800'; |
|
|
icon = 'fa-umbrella-beach'; |
|
|
} |
|
|
|
|
|
return `<div class="flex items-center gap-2"> |
|
|
<i class="fas ${icon} text-xs"></i> |
|
|
<span class="capitalize">${status}</span> |
|
|
</div>`; |
|
|
}, |
|
|
}, |
|
|
{ |
|
|
accessorKey: 'joinDate', |
|
|
header: 'Join Date', |
|
|
cell: info => { |
|
|
const date = new Date(info.getValue()); |
|
|
return date.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }); |
|
|
}, |
|
|
}, |
|
|
{ |
|
|
id: 'actions', |
|
|
header: 'Actions', |
|
|
cell: info => { |
|
|
return `<div class="flex gap-2"> |
|
|
<button class="edit-btn px-2 py-1 text-blue-600 hover:text-blue-800" data-id="${info.row.original.id}"> |
|
|
<i class="fas fa-edit"></i> |
|
|
</button> |
|
|
<button class="delete-btn px-2 py-1 text-red-600 hover:text-red-800" data-id="${info.row.original.id}"> |
|
|
<i class="fas fa-trash"></i> |
|
|
</button> |
|
|
</div>`; |
|
|
}, |
|
|
}, |
|
|
]; |
|
|
|
|
|
|
|
|
let tableState = { |
|
|
data: [...data], |
|
|
columns, |
|
|
pagination: { |
|
|
pageIndex: 0, |
|
|
pageSize: 10, |
|
|
}, |
|
|
sorting: [], |
|
|
globalFilter: '', |
|
|
columnFilters: [], |
|
|
}; |
|
|
|
|
|
|
|
|
const tableInstance = table.createTable({ |
|
|
data: [...data], |
|
|
columns: tableState.columns, |
|
|
state: { |
|
|
pagination: tableState.pagination, |
|
|
sorting: tableState.sorting, |
|
|
globalFilter: tableState.globalFilter, |
|
|
columnFilters: tableState.columnFilters, |
|
|
}, |
|
|
initialState: { |
|
|
pagination: { |
|
|
pageSize: 10 |
|
|
} |
|
|
}, |
|
|
onPaginationChange: updater => { |
|
|
tableState.pagination = typeof updater === 'function' ? updater(tableState.pagination) : updater; |
|
|
renderTable(); |
|
|
}, |
|
|
onSortingChange: updater => { |
|
|
tableState.sorting = typeof updater === 'function' ? updater(tableState.sorting) : updater; |
|
|
renderTable(); |
|
|
}, |
|
|
onGlobalFilterChange: updater => { |
|
|
tableState.globalFilter = typeof updater === 'function' ? updater(tableState.globalFilter) : updater; |
|
|
tableState.pagination.pageIndex = 0; |
|
|
renderTable(); |
|
|
}, |
|
|
onColumnFiltersChange: updater => { |
|
|
tableState.columnFilters = typeof updater === 'function' ? updater(tableState.columnFilters) : updater; |
|
|
tableState.pagination.pageIndex = 0; |
|
|
renderTable(); |
|
|
}, |
|
|
getCoreRowModel: table.getCoreRowModel(), |
|
|
getSortedRowModel: table.getSortedRowModel(), |
|
|
getFilteredRowModel: table.getFilteredRowModel(), |
|
|
getPaginationRowModel: table.getPaginationRowModel(), |
|
|
}); |
|
|
|
|
|
|
|
|
function renderTable() { |
|
|
|
|
|
document.getElementById('loadingIndicator').classList.remove('hidden'); |
|
|
document.getElementById('dataTable').classList.add('opacity-50'); |
|
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
|
|
|
const headerGroups = tableInstance.getHeaderGroups(); |
|
|
const rowModel = tableInstance.getRowModel(); |
|
|
|
|
|
console.log('Row model rows:', rowModel.rows); |
|
|
|
|
|
|
|
|
const headerRow = document.getElementById('tableHeader'); |
|
|
headerRow.innerHTML = ''; |
|
|
|
|
|
headerGroups[0].headers.forEach(header => { |
|
|
const th = document.createElement('th'); |
|
|
th.className = 'px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider'; |
|
|
th.style.width = `${header.getSize()}px`; |
|
|
|
|
|
const canSort = header.column.getCanSort(); |
|
|
|
|
|
if (canSort) { |
|
|
th.className += ' cursor-pointer hover:bg-gray-100'; |
|
|
th.onclick = header.column.getToggleSortingHandler(); |
|
|
} |
|
|
|
|
|
const sortDirection = header.column.getIsSorted(); |
|
|
let sortIcon = ''; |
|
|
|
|
|
if (sortDirection === 'asc') { |
|
|
sortIcon = '<i class="fas fa-sort-up ml-1"></i>'; |
|
|
} else if (sortDirection === 'desc') { |
|
|
sortIcon = '<i class="fas fa-sort-down ml-1"></i>'; |
|
|
} else if (canSort) { |
|
|
sortIcon = '<i class="fas fa-sort ml-1 text-gray-300"></i>'; |
|
|
} |
|
|
|
|
|
th.innerHTML = `<div class="flex items-center">${header.column.columnDef.header}${sortIcon}</div>`; |
|
|
headerRow.appendChild(th); |
|
|
}); |
|
|
|
|
|
|
|
|
const tableBody = document.getElementById('tableBody'); |
|
|
tableBody.innerHTML = ''; |
|
|
|
|
|
rowModel.rows.forEach(row => { |
|
|
const tr = document.createElement('tr'); |
|
|
tr.className = 'animate-row-hover'; |
|
|
|
|
|
row.getVisibleCells().forEach(cell => { |
|
|
const td = document.createElement('td'); |
|
|
td.className = 'px-6 py-4 whitespace-nowrap'; |
|
|
td.innerHTML = cell.column.columnDef.cell(cell); |
|
|
tr.appendChild(td); |
|
|
}); |
|
|
|
|
|
tableBody.appendChild(tr); |
|
|
}); |
|
|
|
|
|
|
|
|
const pageCount = tableInstance.getPageCount(); |
|
|
const currentPage = tableInstance.getState().pagination.pageIndex; |
|
|
const pageSize = tableInstance.getState().pagination.pageSize; |
|
|
const totalItems = tableInstance.getFilteredRowModel().rows.length; |
|
|
|
|
|
document.getElementById('showingFrom').textContent = (currentPage * pageSize) + 1; |
|
|
document.getElementById('showingTo').textContent = Math.min((currentPage + 1) * pageSize, totalItems); |
|
|
document.getElementById('totalItems').textContent = totalItems; |
|
|
|
|
|
|
|
|
const pageNumbers = document.getElementById('pageNumbers'); |
|
|
pageNumbers.innerHTML = ''; |
|
|
|
|
|
const maxVisiblePages = 5; |
|
|
let startPage = Math.max(0, currentPage - Math.floor(maxVisiblePages / 2)); |
|
|
let endPage = Math.min(pageCount - 1, startPage + maxVisiblePages - 1); |
|
|
|
|
|
if (endPage - startPage + 1 < maxVisiblePages) { |
|
|
startPage = Math.max(0, endPage - maxVisiblePages + 1); |
|
|
} |
|
|
|
|
|
|
|
|
if (startPage > 0) { |
|
|
const firstPageBtn = document.createElement('button'); |
|
|
firstPageBtn.className = `px-3 py-1 border rounded-md ${currentPage === 0 ? 'bg-blue-100 border-blue-300' : ''}`; |
|
|
firstPageBtn.textContent = '1'; |
|
|
firstPageBtn.onclick = () => tableInstance.setPageIndex(0); |
|
|
pageNumbers.appendChild(firstPageBtn); |
|
|
|
|
|
if (startPage > 1) { |
|
|
const ellipsis = document.createElement('span'); |
|
|
ellipsis.className = 'px-2'; |
|
|
ellipsis.textContent = '...'; |
|
|
pageNumbers.appendChild(ellipsis); |
|
|
} |
|
|
} |
|
|
|
|
|
for (let i = startPage; i <= endPage; i++) { |
|
|
const pageBtn = document.createElement('button'); |
|
|
pageBtn.className = `px-3 py-1 border rounded-md ${currentPage === i ? 'bg-blue-100 border-blue-300' : ''}`; |
|
|
pageBtn.textContent = i + 1; |
|
|
pageBtn.onclick = () => tableInstance.setPageIndex(i); |
|
|
pageNumbers.appendChild(pageBtn); |
|
|
} |
|
|
|
|
|
|
|
|
if (endPage < pageCount - 1) { |
|
|
if (endPage < pageCount - 2) { |
|
|
const ellipsis = document.createElement('span'); |
|
|
ellipsis.className = 'px-2'; |
|
|
ellipsis.textContent = '...'; |
|
|
pageNumbers.appendChild(ellipsis); |
|
|
} |
|
|
|
|
|
const lastPageBtn = document.createElement('button'); |
|
|
lastPageBtn.className = `px-3 py-1 border rounded-md ${currentPage === pageCount - 1 ? 'bg-blue-100 border-blue-300' : ''}`; |
|
|
lastPageBtn.textContent = pageCount; |
|
|
lastPageBtn.onclick = () => tableInstance.setPageIndex(pageCount - 1); |
|
|
pageNumbers.appendChild(lastPageBtn); |
|
|
} |
|
|
|
|
|
|
|
|
document.getElementById('firstPage').disabled = currentPage === 0; |
|
|
document.getElementById('prevPage').disabled = currentPage === 0; |
|
|
document.getElementById('nextPage').disabled = currentPage >= pageCount - 1; |
|
|
document.getElementById('lastPage').disabled = currentPage >= pageCount - 1; |
|
|
|
|
|
|
|
|
document.getElementById('loadingIndicator').classList.add('hidden'); |
|
|
document.getElementById('dataTable').classList.remove('opacity-50'); |
|
|
}, 300); |
|
|
} |
|
|
|
|
|
|
|
|
console.log('Initial data:', data); |
|
|
console.log('Table instance:', tableInstance); |
|
|
renderTable(); |
|
|
|
|
|
|
|
|
document.getElementById('searchInput').addEventListener('input', (e) => { |
|
|
tableInstance.setGlobalFilter(e.target.value); |
|
|
}); |
|
|
|
|
|
document.getElementById('statusFilter').addEventListener('change', (e) => { |
|
|
if (e.target.value === 'all') { |
|
|
tableInstance.setColumnFilters(tableState.columnFilters.filter(f => f.id !== 'status')); |
|
|
} else { |
|
|
tableInstance.setColumnFilters([ |
|
|
...tableState.columnFilters.filter(f => f.id !== 'status'), |
|
|
{ id: 'status', value: e.target.value } |
|
|
]); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
document.addEventListener('click', (e) => { |
|
|
if (e.target.closest('.department-pill')) { |
|
|
const dept = e.target.closest('.department-pill').getAttribute('data-dept'); |
|
|
|
|
|
|
|
|
document.querySelectorAll('.department-pill').forEach(btn => { |
|
|
btn.classList.remove('bg-blue-600', 'text-white'); |
|
|
btn.classList.add(btn.classList.contains('bg-blue-100') ? 'bg-blue-100' : |
|
|
btn.classList.contains('bg-purple-100') ? 'bg-purple-100' : |
|
|
btn.classList.contains('bg-green-100') ? 'bg-green-100' : |
|
|
btn.classList.contains('bg-pink-100') ? 'bg-pink-100' : |
|
|
btn.classList.contains('bg-yellow-100') ? 'bg-yellow-100' : |
|
|
btn.classList.contains('bg-indigo-100') ? 'bg-indigo-100' : |
|
|
btn.classList.contains('bg-red-100') ? 'bg-red-100' : 'bg-gray-100'); |
|
|
}); |
|
|
|
|
|
if (dept === 'all') { |
|
|
tableInstance.setColumnFilters(tableState.columnFilters.filter(f => f.id !== 'department')); |
|
|
} else { |
|
|
e.target.closest('.department-pill').classList.remove('bg-blue-100', 'bg-purple-100', 'bg-green-100', 'bg-pink-100', 'bg-yellow-100', 'bg-indigo-100', 'bg-red-100', 'bg-gray-100'); |
|
|
e.target.closest('.department-pill').classList.add('bg-blue-600', 'text-white'); |
|
|
tableInstance.setColumnFilters([ |
|
|
...tableState.columnFilters.filter(f => f.id !== 'department'), |
|
|
{ id: 'department', value: dept } |
|
|
]); |
|
|
} |
|
|
} |
|
|
}); |
|
|
|
|
|
document.getElementById('rowsPerPage').addEventListener('change', (e) => { |
|
|
tableInstance.setPageSize(Number(e.target.value)); |
|
|
}); |
|
|
|
|
|
document.getElementById('firstPage').addEventListener('click', () => { |
|
|
tableInstance.setPageIndex(0); |
|
|
}); |
|
|
|
|
|
document.getElementById('prevPage').addEventListener('click', () => { |
|
|
tableInstance.previousPage(); |
|
|
}); |
|
|
|
|
|
document.getElementById('nextPage').addEventListener('click', () => { |
|
|
tableInstance.nextPage(); |
|
|
}); |
|
|
|
|
|
document.getElementById('lastPage').addEventListener('click', () => { |
|
|
tableInstance.setPageIndex(tableInstance.getPageCount() - 1); |
|
|
}); |
|
|
|
|
|
|
|
|
const modal = document.getElementById('employeeModal'); |
|
|
const addEmployeeBtn = document.getElementById('addEmployeeBtn'); |
|
|
const closeModal = document.getElementById('closeModal'); |
|
|
const cancelBtn = document.getElementById('cancelBtn'); |
|
|
const employeeForm = document.getElementById('employeeForm'); |
|
|
|
|
|
let currentEditId = null; |
|
|
|
|
|
function openModal(employee = null) { |
|
|
if (employee) { |
|
|
document.getElementById('modalTitle').textContent = 'Edit Employee'; |
|
|
document.getElementById('name').value = employee.name; |
|
|
document.getElementById('position').value = employee.position; |
|
|
document.getElementById('department').value = employee.department; |
|
|
document.getElementById('status').value = employee.status; |
|
|
currentEditId = employee.id; |
|
|
} else { |
|
|
document.getElementById('modalTitle').textContent = 'Add Employee'; |
|
|
employeeForm.reset(); |
|
|
currentEditId = null; |
|
|
} |
|
|
modal.classList.remove('hidden'); |
|
|
} |
|
|
|
|
|
function closeModalHandler() { |
|
|
modal.classList.add('hidden'); |
|
|
} |
|
|
|
|
|
addEmployeeBtn.addEventListener('click', () => openModal()); |
|
|
closeModal.addEventListener('click', closeModalHandler); |
|
|
cancelBtn.addEventListener('click', closeModalHandler); |
|
|
|
|
|
|
|
|
employeeForm.addEventListener('submit', (e) => { |
|
|
e.preventDefault(); |
|
|
|
|
|
const newEmployee = { |
|
|
id: currentEditId || Math.max(...tableState.data.map(e => e.id)) + 1, |
|
|
name: document.getElementById('name').value, |
|
|
position: document.getElementById('position').value, |
|
|
department: document.getElementById('department').value, |
|
|
status: document.getElementById('status').value, |
|
|
joinDate: new Date().toISOString().split('T')[0], |
|
|
}; |
|
|
|
|
|
if (currentEditId) { |
|
|
|
|
|
const index = tableState.data.findIndex(e => e.id === currentEditId); |
|
|
if (index !== -1) { |
|
|
tableState.data[index] = newEmployee; |
|
|
} |
|
|
} else { |
|
|
|
|
|
tableState.data.unshift(newEmployee); |
|
|
} |
|
|
|
|
|
|
|
|
tableState.pagination.pageIndex = 0; |
|
|
tableInstance.setData([...tableState.data]); |
|
|
renderTable(); |
|
|
|
|
|
closeModalHandler(); |
|
|
}); |
|
|
|
|
|
|
|
|
document.addEventListener('click', (e) => { |
|
|
|
|
|
if (e.target.closest('.edit-btn')) { |
|
|
const id = parseInt(e.target.closest('.edit-btn').getAttribute('data-id')); |
|
|
const employee = tableState.data.find(e => e.id === id); |
|
|
if (employee) { |
|
|
openModal(employee); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (e.target.closest('.delete-btn')) { |
|
|
const id = parseInt(e.target.closest('.delete-btn').getAttribute('data-id')); |
|
|
if (confirm('Are you sure you want to delete this employee?')) { |
|
|
tableState.data = tableState.data.filter(e => e.id !== id); |
|
|
tableInstance.setData([...tableState.data]); |
|
|
renderTable(); |
|
|
} |
|
|
} |
|
|
}); |
|
|
</script> |
|
|
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=etrano/zz" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
|
|
</html> |