test tabbed interface
This commit is contained in:
parent
faee2459bc
commit
c7379f5af3
@ -204,20 +204,19 @@ export function attachEmployeeDashboardListeners() {
|
||||
document.getElementById('view-request-history-btn').addEventListener('click', handleViewRequestHistoryClick);
|
||||
}
|
||||
|
||||
// In js/main.js
|
||||
|
||||
export function attachAdminDashboardListeners() {
|
||||
// Event delegation for all buttons
|
||||
// This master listener handles most buttons in the admin view via event delegation
|
||||
document.getElementById('admin-dashboard').addEventListener('click', handleAdminDashboardClick);
|
||||
|
||||
// Specific form handlers
|
||||
// Listeners for specific forms that need to prevent default submission behavior
|
||||
document.getElementById('create-user-form').addEventListener('submit', handleCreateUser);
|
||||
document.getElementById('add-punch-form').addEventListener('submit', handleAddPunch);
|
||||
document.getElementById('add-note-form').addEventListener('submit', handleAddNote);
|
||||
|
||||
// Other top-level buttons
|
||||
document.getElementById('archive-btn').addEventListener('click', handleArchive);
|
||||
document.getElementById('view-archives-btn').addEventListener('click', renderArchiveView);
|
||||
document.getElementById('view-time-off-history-btn').addEventListener('click', renderTimeOffHistoryView);
|
||||
document.getElementById('view-notes-btn').addEventListener('click', handleViewNotesClick);
|
||||
// Call the function to make the new tabs work
|
||||
setupTabbedInterface();
|
||||
}
|
||||
|
||||
// --- APP INITIALIZER ---
|
||||
@ -241,6 +240,36 @@ function initializeApp() {
|
||||
}
|
||||
}
|
||||
|
||||
// This function handles the logic for switching between tabs
|
||||
function setupTabbedInterface() {
|
||||
const tabsContainer = document.getElementById('admin-tabs-nav');
|
||||
const contentContainer = document.getElementById('admin-tabs-content');
|
||||
|
||||
// Exit if the tab elements aren't on the page
|
||||
if (!tabsContainer || !contentContainer) return;
|
||||
|
||||
// Use event delegation on the tab navigation container
|
||||
tabsContainer.addEventListener('click', (e) => {
|
||||
const clickedTab = e.target.closest('.tab-btn');
|
||||
// Ignore clicks that aren't on a tab button
|
||||
if (!clickedTab) return;
|
||||
|
||||
const tabTarget = clickedTab.dataset.tab;
|
||||
|
||||
// Update the active state on tab buttons
|
||||
tabsContainer.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
btn.classList.remove('active-tab');
|
||||
});
|
||||
clickedTab.classList.add('active-tab');
|
||||
|
||||
// Show the correct content panel and hide the others
|
||||
contentContainer.querySelectorAll('[id^="tab-content-"]').forEach(panel => {
|
||||
panel.classList.add('hidden');
|
||||
});
|
||||
document.getElementById(`tab-content-${tabTarget}`).classList.remove('hidden');
|
||||
});
|
||||
}
|
||||
|
||||
// --- START THE APP ---
|
||||
// Attach global listeners that are always present.
|
||||
document.getElementById('sign-out-btn').addEventListener('click', () => handleSignOut('You have been signed out.'));
|
||||
|
||||
@ -147,25 +147,41 @@ export async function renderEmployeeDashboard() {
|
||||
}
|
||||
}
|
||||
|
||||
// In js/ui.js
|
||||
|
||||
export async function renderAdminDashboard() {
|
||||
showView('admin');
|
||||
const [logsRes, usersRes, requestsRes] = await Promise.all([apiCall('/admin/logs'), apiCall('/admin/users'), apiCall('/admin/time-off-requests/pending')]);
|
||||
if (!logsRes.success || !usersRes.success || !requestsRes.success) return;
|
||||
|
||||
// Update module-level state
|
||||
// --- Existing data processing logic (no changes here) ---
|
||||
allTimeEntries = logsRes.data;
|
||||
allUsers = usersRes.data;
|
||||
const pendingRequests = requestsRes.data;
|
||||
const user = JSON.parse(localStorage.getItem('user'));
|
||||
|
||||
const employeeTotals = allTimeEntries.reduce((acc, entry) => { const dur = entry.punch_out_time ? (new Date(entry.punch_out_time) - new Date(entry.punch_in_time)) : (Date.now() - new Date(entry.punch_in_time)); acc[entry.username] = (acc[entry.username] || 0) + dur; return acc; }, {});
|
||||
const punchedInEntries = allTimeEntries.filter(e => e.status === 'in');
|
||||
const employeesOnly = allUsers.filter(u => u.role === 'employee');
|
||||
|
||||
// --- New Tabbed HTML Structure ---
|
||||
mainViews.admin.innerHTML = `
|
||||
<div class="max-w-6xl mx-auto space-y-8">
|
||||
<div class="bg-white rounded-xl shadow-md p-6"><div class="flex flex-wrap justify-between items-center gap-4"><h2 class="text-2xl font-bold">Admin Dashboard</h2><div class="flex-shrink-0 space-x-2"><button id="view-archives-btn" class="px-4 py-2 bg-gray-500 text-white rounded-lg hover:bg-gray-600">View Archives</button><button id="archive-btn" class="px-4 py-2 bg-amber-500 text-white rounded-lg hover:bg-amber-600">Archive Records</button></div></div></div>
|
||||
<div class="bg-white rounded-xl shadow-md p-6">
|
||||
<div class="max-w-6xl mx-auto space-y-4">
|
||||
<div class="bg-white rounded-xl shadow-md p-4">
|
||||
<h2 class="text-2xl font-bold">Admin Dashboard</h2>
|
||||
</div>
|
||||
|
||||
<div id="admin-tabs-nav" class="flex border-b border-gray-200 bg-white rounded-t-lg px-4">
|
||||
<button data-tab="overview" class="tab-btn active-tab py-3 px-4">Overview</button>
|
||||
<button data-tab="logs" class="tab-btn py-3 px-4">Time Logs</button>
|
||||
<button data-tab="users" class="tab-btn py-3 px-4">User Management</button>
|
||||
</div>
|
||||
|
||||
<div id="admin-tabs-content" class="bg-white rounded-b-lg shadow-md p-6">
|
||||
|
||||
<div id="tab-content-overview" class="space-y-8">
|
||||
<div><h3 class="text-xl font-bold text-gray-700 mb-2">Currently Punched In</h3><ul class="border rounded-lg divide-y">${punchedInEntries.map(e => `<li class="flex flex-col items-start space-y-2 p-3 sm:flex-row sm:items-center sm:justify-between sm:space-y-0"><span class="font-medium text-gray-800">${e.username}</span><div class="flex items-center space-x-4"><span class="text-sm text-gray-500">Since: ${utils.formatDateTime(e.punch_in_time)}</span><button class="force-clock-out-btn px-3 py-1 text-xs bg-red-500 text-white rounded whitespace-nowrap" data-userid="${e.user_id}" data-username="${e.username}">Force Clock Out</button></div></li>`).join('') || '<li class="p-4 text-center text-gray-500">None</li>'}</ul></div>
|
||||
<div><div class="flex justify-between items-center mb-4"><h3 class="text-xl font-bold text-gray-700">Pending Time Off Requests</h3><button id="view-time-off-history-btn" class="px-4 py-2 text-sm bg-gray-200 rounded-lg hover:bg-gray-300">View History</button></div><div class="overflow-x-auto border rounded-lg"><table class="min-w-full text-sm text-left"><thead class="bg-gray-50"><tr><th class="p-2">Employee</th><th class="p-2">Dates</th><th class="p-2">Reason</th><th class="p-2">Actions</th></tr></thead><tbody>${pendingRequests.map(r => `<tr class="border-t"><td class="p-2">${r.username}</td><td class="p-2 whitespace-nowrap">${utils.formatDate(r.start_date)} - ${utils.formatDate(r.end_date)}</td><td class="p-2">${r.reason||''}</td><td class="p-2"><div class="flex flex-col sm:flex-row gap-2"><button class="approve-request-btn font-medium text-green-600 hover:underline" data-id="${r.id}">Approve</button><button class="deny-request-btn font-medium text-red-600 hover:underline" data-id="${r.id}">Deny</button></div></td></tr>`).join('') || '<tr><td colspan="4" class="text-center p-4">No pending requests.</td></tr>'}</tbody></table></div></div>
|
||||
<div>
|
||||
<h3 class="text-xl font-bold text-gray-700 mb-4">Employee Notes</h3>
|
||||
<form id="add-note-form" class="space-y-3 bg-gray-50 p-4 rounded-lg">
|
||||
<select id="note-user-select" class="w-full p-2 border rounded" required><option value="">-- Select an Employee --</option>${employeesOnly.map(u => `<option value="${u.id}">${u.username}</option>`).join('')}</select>
|
||||
@ -174,15 +190,27 @@ export async function renderAdminDashboard() {
|
||||
</form>
|
||||
<div id="employee-notes-container" class="mt-4"></div>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl shadow-md p-6"><h3 class="text-xl font-bold text-gray-700 mb-2">Currently Punched In</h3><ul class="border rounded-lg divide-y">${punchedInEntries.map(e => `<li class="flex flex-col items-start space-y-2 p-3 sm:flex-row sm:items-center sm:justify-between sm:space-y-0"><span class="font-medium text-gray-800">${e.username}</span><div class="flex items-center space-x-4"><span class="text-sm text-gray-500">Since: ${utils.formatDateTime(e.punch_in_time)}</span><button class="force-clock-out-btn px-3 py-1 text-xs bg-red-500 text-white rounded whitespace-nowrap" data-userid="${e.user_id}" data-username="${e.username}">Force Clock Out</button></div></li>`).join('') || '<li class="p-4 text-center text-gray-500">None</li>'}</ul></div>
|
||||
<div class="bg-white rounded-xl shadow-md p-6"><div class="flex justify-between items-center mb-4"><h3 class="text-xl font-bold text-gray-700">Pending Time Off Requests</h3><button id="view-time-off-history-btn" class="px-4 py-2 text-sm bg-gray-200 rounded-lg hover:bg-gray-300">View History</button></div><div class="overflow-x-auto border rounded-lg"><table class="min-w-full text-sm text-left"><thead class="bg-gray-50"><tr><th class="p-2">Employee</th><th class="p-2">Dates</th><th class="p-2">Reason</th><th class="p-2">Actions</th></tr></thead><tbody>${pendingRequests.map(r => `<tr class="border-t"><td class="p-2">${r.username}</td><td class="p-2 whitespace-nowrap">${utils.formatDate(r.start_date)} - ${utils.formatDate(r.end_date)}</td><td class="p-2">${r.reason||''}</td><td class="p-2"><div class="flex flex-col sm:flex-row gap-2"><button class="approve-request-btn font-medium text-green-600 hover:underline" data-id="${r.id}">Approve</button><button class="deny-request-btn font-medium text-red-600 hover:underline" data-id="${r.id}">Deny</button></div></td></tr>`).join('') || '<tr><td colspan="4" class="text-center p-4">No pending requests.</td></tr>'}</tbody></table></div></div>
|
||||
<div class="bg-white rounded-xl shadow-md p-6"><h3 class="text-xl font-bold text-gray-700 mb-2">Hours by Employee</h3><div class="overflow-x-auto border rounded-lg"><table class="min-w-full text-sm text-left"><thead class="bg-gray-50"><tr><th class="p-2">Employee</th><th class="p-2">Total Hours</th></tr></thead><tbody>${Object.entries(employeeTotals).map(([username, totalMs]) => `<tr class="border-t"><td class="p-2 font-medium">${username}</td><td class="p-2">${utils.formatDecimal(totalMs)}</td></tr>`).join('') || '<tr><td colspan="2" class="text-center p-4">No data.</td></tr>'}</tbody></table></div></div>
|
||||
<div class="bg-white rounded-xl shadow-md p-6"><h3 class="text-xl font-bold text-gray-700 mb-4">Detailed Logs</h3><div class="overflow-x-auto border rounded-lg"><table class="min-w-full text-sm text-left"><thead class="bg-gray-50"><tr><th class="p-2">Employee</th><th class="p-2">In</th><th class="p-2">Out</th><th class="p-2">Duration</th><th class="p-2">Actions</th></tr></thead><tbody>${allTimeEntries.map(e => `<tr class="border-t"><td class="p-2">${e.username||'N/A'}</td><td class="p-2">${utils.formatDateTime(e.punch_in_time)}</td><td class="p-2">${utils.formatDateTime(e.punch_out_time)}</td><td class="p-2" id="admin-duration-${e.id}">${e.punch_out_time ? utils.formatDecimal(new Date(e.punch_out_time) - new Date(e.punch_in_time)) + ' hrs' : '...'}</td><td class="p-2"><div class="flex flex-col sm:flex-row items-start sm:items-center gap-2"><button class="edit-btn font-medium text-blue-600 hover:underline" data-id="${e.id}">Edit</button><button class="delete-btn font-medium text-red-600 hover:underline" data-id="${e.id}">Delete</button></div></td></tr>`).join('')}</tbody></table></div></div>
|
||||
<div class="bg-white rounded-xl shadow-md p-6"><h3 class="text-xl font-bold text-gray-700 mb-4">User & Payroll Management</h3><div class="grid md:grid-cols-2 gap-6"><form id="create-user-form" class="space-y-3 bg-gray-50 p-4 rounded-lg"><h4 class="font-semibold">Create User</h4><input type="text" id="new-username" placeholder="Username" class="w-full p-2 border rounded" required><input type="password" id="new-password" placeholder="Password" class="w-full p-2 border rounded" required><select id="new-user-role" class="w-full p-2 border rounded"><option value="employee">Employee</option><option value="admin">Admin</option></select><button type="submit" class="w-full bg-green-600 text-white p-2 rounded hover:bg-green-700">Create User</button></form><form id="add-punch-form" class="space-y-3 bg-gray-50 p-4 rounded-lg"><h4 class="font-semibold">Add Manual Entry</h4><select id="add-punch-user" class="w-full p-2 border rounded" required>${allUsers.map(u => `<option value="${u.id}" data-username="${u.username}">${u.username}</option>`).join('')}</select><label class="text-sm">In (Required):</label><input type="datetime-local" id="add-punch-in" class="w-full p-2 border rounded" required><label class="text-sm">Out (Optional):</label><input type="datetime-local" id="add-punch-out" class="w-full p-2 border rounded"><button type="submit" class="w-full bg-purple-600 text-white p-2 rounded hover:bg-purple-700">Add Entry</button></form></div><div class="mt-6"><h4 class="font-semibold mb-2">Manage Users</h4><div class="overflow-x-auto border rounded-lg"><table class="min-w-full text-sm text-left"><thead class="bg-gray-50"><tr><th class="p-2">Username</th><th class="p-2">Role</th><th class="p-2">Actions</th></tr></thead><tbody>${allUsers.map(u => `<tr class="border-t"><td class="p-2 font-medium">${u.username}</td><td class="p-2 capitalize">${u.role}</td><td class="p-2"><div class="flex flex-col sm:flex-row items-start sm:items-center gap-2">${u.isPrimary ? `<span class="text-sm text-gray-500">Primary Admin</span>` : `<button class="reset-pw-btn font-medium text-blue-600 hover:underline" data-username="${u.username}">Reset PW</button><button class="change-role-btn font-medium text-purple-600 hover:underline" data-username="${u.username}" data-role="${u.role}">${u.role === 'admin' ? 'Demote' : 'Promote'}</button>${u.username !== user.username ? `<button class="delete-user-btn font-medium text-red-600 hover:underline" data-username="${u.username}">Delete</button>` : ''}`}</div></td></tr>`).join('')}</tbody></table></div></div></div>
|
||||
</div>`;
|
||||
</div>
|
||||
|
||||
attachAdminDashboardListeners(); // Attach all listeners for this view
|
||||
<div id="tab-content-logs" class="space-y-8 hidden">
|
||||
<div class="flex-shrink-0 space-x-2"><button id="view-archives-btn" class="px-4 py-2 bg-gray-500 text-white rounded-lg hover:bg-gray-600">View Archives</button><button id="archive-btn" class="px-4 py-2 bg-amber-500 text-white rounded-lg hover:bg-amber-600">Archive All Completed Records</button></div>
|
||||
<div><h3 class="text-xl font-bold text-gray-700 mb-2">Hours by Employee</h3><div class="overflow-x-auto border rounded-lg"><table class="min-w-full text-sm text-left"><thead class="bg-gray-50"><tr><th class="p-2">Employee</th><th class="p-2">Total Hours</th></tr></thead><tbody>${Object.entries(employeeTotals).map(([username, totalMs]) => `<tr class="border-t"><td class="p-2 font-medium">${username}</td><td class="p-2">${utils.formatDecimal(totalMs)}</td></tr>`).join('') || '<tr><td colspan="2" class="text-center p-4">No data.</td></tr>'}</tbody></table></div></div>
|
||||
<div><h3 class="text-xl font-bold text-gray-700 mb-4">Detailed Logs</h3><div class="overflow-x-auto border rounded-lg"><table class="min-w-full text-sm text-left"><thead class="bg-gray-50"><tr><th class="p-2">Employee</th><th class="p-2">In</th><th class="p-2">Out</th><th class="p-2">Duration</th><th class="p-2">Actions</th></tr></thead><tbody>${allTimeEntries.map(e => `<tr class="border-t"><td class="p-2">${e.username||'N/A'}</td><td class="p-2">${utils.formatDateTime(e.punch_in_time)}</td><td class="p-2">${utils.formatDateTime(e.punch_out_time)}</td><td class="p-2" id="admin-duration-${e.id}">${e.punch_out_time ? utils.formatDecimal(new Date(e.punch_out_time) - new Date(e.punch_in_time)) + ' hrs' : '...'}</td><td class="p-2"><div class="flex flex-col sm:flex-row items-start sm:items-center gap-2"><button class="edit-btn font-medium text-blue-600 hover:underline" data-id="${e.id}">Edit</button><button class="delete-btn font-medium text-red-600 hover:underline" data-id="${e.id}">Delete</button></div></td></tr>`).join('')}</tbody></table></div></div>
|
||||
</div>
|
||||
|
||||
<div id="tab-content-users" class="space-y-8 hidden">
|
||||
<div class="grid md:grid-cols-2 gap-6">
|
||||
<form id="create-user-form" class="space-y-3 bg-gray-50 p-4 rounded-lg"><h4 class="font-semibold">Create User</h4><input type="text" id="new-username" placeholder="Username" class="w-full p-2 border rounded" required><input type="password" id="new-password" placeholder="Password" class="w-full p-2 border rounded" required><select id="new-user-role" class="w-full p-2 border rounded"><option value="employee">Employee</option><option value="admin">Admin</option></select><button type="submit" class="w-full bg-green-600 text-white p-2 rounded hover:bg-green-700">Create User</button></form>
|
||||
<form id="add-punch-form" class="space-y-3 bg-gray-50 p-4 rounded-lg"><h4 class="font-semibold">Add Manual Entry</h4><select id="add-punch-user" class="w-full p-2 border rounded" required>${allUsers.map(u => `<option value="${u.id}" data-username="${u.username}">${u.username}</option>`).join('')}</select><label class="text-sm">In (Required):</label><input type="datetime-local" id="add-punch-in" class="w-full p-2 border rounded" required><label class="text-sm">Out (Optional):</label><input type="datetime-local" id="add-punch-out" class="w-full p-2 border rounded"><button type="submit" class="w-full bg-purple-600 text-white p-2 rounded hover:bg-purple-700">Add Entry</button></form>
|
||||
</div>
|
||||
<div><h4 class="font-semibold mb-2">Manage Users</h4><div class="overflow-x-auto border rounded-lg"><table class="min-w-full text-sm text-left"><thead class="bg-gray-50"><tr><th class="p-2">Username</th><th class="p-2">Role</th><th class="p-2">Actions</th></tr></thead><tbody>${allUsers.map(u => `<tr class="border-t"><td class="p-2 font-medium">${u.username}</td><td class="p-2 capitalize">${u.role}</td><td class="p-2"><div class="flex flex-col sm:flex-row items-start sm:items-center gap-2">${u.isPrimary ? `<span class="text-sm text-gray-500">Primary Admin</span>` : `<button class="reset-pw-btn font-medium text-blue-600 hover:underline" data-username="${u.username}">Reset PW</button><button class="change-role-btn font-medium text-purple-600 hover:underline" data-username="${u.username}" data-role="${u.role}">${u.role === 'admin' ? 'Demote' : 'Promote'}</button>${u.username !== user.username ? `<button class="delete-user-btn font-medium text-red-600 hover:underline" data-username="${u.username}">Delete</button>` : ''}`}</div></td></tr>`).join('')}</tbody></table></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// The rest of the function remains the same
|
||||
attachAdminDashboardListeners();
|
||||
punchedInEntries.forEach(entry => {
|
||||
const durationCell = document.getElementById(`admin-duration-${entry.id}`);
|
||||
if (durationCell) {
|
||||
|
||||
@ -29,3 +29,19 @@ body {
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
/* Tab Styles */
|
||||
.tab-btn {
|
||||
/* Smooth transition for color and border changes */
|
||||
transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease;
|
||||
/* A transparent bottom border on all tabs to prevent layout shift */
|
||||
border-bottom: 3px solid transparent;
|
||||
}
|
||||
|
||||
.active-tab {
|
||||
/* The blue bottom border for the active tab */
|
||||
border-color: #2563EB; /* Tailwind's blue-600 */
|
||||
/* A darker text color for the active tab to make it stand out */
|
||||
color: #1E40AF; /* Tailwind's blue-800 */
|
||||
font-weight: 500;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user