add time elapsed instead of running

This commit is contained in:
chris 2025-08-02 09:03:36 -04:00
parent b005af58a2
commit a4d737111c

View File

@ -53,7 +53,8 @@
const navUserControls = document.getElementById('nav-user-controls'), welcomeMessage = document.getElementById('welcome-message'), signOutBtn = document.getElementById('sign-out-btn');
const messageBox = document.getElementById('message-box'), loadingSpinner = document.getElementById('loading-spinner'), modalContainer = document.getElementById('modal-container');
let authToken, user, allTimeEntries = [], allUsers = [], employeeTimerInterval = null;
let adminTimerIntervals = [];
// --- Helper Functions ---
const showLoading = (show) => { loadingSpinner.innerHTML = show ? `<div class="inline-block animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-blue-500"></div>` : ''; };
const showMessage = (message, type = 'success') => { messageBox.innerHTML = `<div class="p-4 mb-4 text-sm rounded-lg flex items-center justify-between ${type === 'error' ? 'bg-red-100 text-red-700' : 'bg-green-100 text-green-700'}" role="alert"><span>${message}</span><button onclick="messageBox.classList.add('hidden')" class="font-bold text-lg">&times;</button></div>`; messageBox.classList.remove('hidden'); };
@ -104,10 +105,15 @@
}
// --- View Management ---
const showView = (viewName) => {
clearInterval(employeeTimerInterval);
Object.keys(mainViews).forEach(v => mainViews[v].classList.toggle('hidden', v !== viewName));
}
// --- View Management ---
const showView = (viewName) => {
// Clear all timers when the view changes
clearInterval(employeeTimerInterval);
adminTimerIntervals.forEach(clearInterval);
adminTimerIntervals = [];
Object.keys(mainViews).forEach(v => mainViews[v].classList.toggle('hidden', v !== viewName));
}
// --- UI Rendering ---
function updateUI() {
@ -219,75 +225,88 @@
}
async function renderAdminDashboard() {
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;
allTimeEntries = logsRes.data;
allUsers = usersRes.data;
const pendingRequests = requestsRes.data;
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');
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="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>
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;
allTimeEntries = logsRes.data;
allUsers = usersRes.data;
const pendingRequests = requestsRes.data;
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');
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="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">
<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: ${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>
</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: ${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}">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">${formatDate(r.start_date)} - ${formatDate(r.end_date)}</td><td class="p-2">${r.reason||''}</td><td class="p-2 space-x-1"><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></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">${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">${formatDateTime(e.punch_in_time)}</td><td class="p-2">${formatDateTime(e.punch_out_time)}</td><td class="p-2">${e.punch_out_time ? formatDecimal(new Date(e.punch_out_time) - new Date(e.punch_in_time)) : 'Running'}</td><td class="p-2 space-x-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></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 Punch</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:</label><input type="datetime-local" id="add-punch-in" class="w-full p-2 border rounded" required><label class="text-sm">Out:</label><input type="datetime-local" id="add-punch-out" class="w-full p-2 border rounded" required><button type="submit" class="w-full bg-purple-600 text-white p-2 rounded hover:bg-purple-700">Add Punch</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 space-x-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>` : ''}`}</td></tr>`).join('')}</tbody></table></div></div>
</div>
</div>`;
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('create-user-form').addEventListener('submit', handleCreateUser);
document.getElementById('add-punch-form').addEventListener('submit', handleAddPunch);
document.getElementById('admin-dashboard').addEventListener('click', handleAdminDashboardClick);
</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">${formatDate(r.start_date)} - ${formatDate(r.end_date)}</td><td class="p-2">${r.reason||''}</td><td class="p-2 space-x-1"><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></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">${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">${formatDateTime(e.punch_in_time)}</td><td class="p-2">${formatDateTime(e.punch_out_time)}</td><td class="p-2" id="admin-duration-${e.id}">${e.punch_out_time ? formatDecimal(new Date(e.punch_out_time) - new Date(e.punch_in_time)) + ' hrs' : '...'}</td><td class="p-2 space-x-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></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 Punch</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:</label><input type="datetime-local" id="add-punch-in" class="w-full p-2 border rounded" required><label class="text-sm">Out:</label><input type="datetime-local" id="add-punch-out" class="w-full p-2 border rounded" required><button type="submit" class="w-full bg-purple-600 text-white p-2 rounded hover:bg-purple-700">Add Punch</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 space-x-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>` : ''}`}</td></tr>`).join('')}</tbody></table></div></div>
</div>
</div>`;
// ** NEW: Start timers for all currently punched-in users **
punchedInEntries.forEach(entry => {
const durationCell = document.getElementById(`admin-duration-${entry.id}`);
if (durationCell) {
const punchInTime = new Date(entry.punch_in_time);
const intervalId = setInterval(() => {
durationCell.textContent = formatDuration(Date.now() - punchInTime.getTime());
}, 1000);
adminTimerIntervals.push(intervalId); // Store the timer to clear it later
}
});
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('create-user-form').addEventListener('submit', handleCreateUser);
document.getElementById('add-punch-form').addEventListener('submit', handleAddPunch);
document.getElementById('admin-dashboard').addEventListener('click', handleAdminDashboardClick);
}
function renderArchiveView() {
apiCall('/admin/archives').then(res => {