updated add punch admin

This commit is contained in:
chris 2025-08-02 09:22:29 -04:00
parent d6c98f93bb
commit 901efb6bf6
2 changed files with 85 additions and 19 deletions

View File

@ -226,12 +226,22 @@
<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></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">${formatDate(r.start_date)} - ${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">${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"><div class="flex flex-col sm:flex-row items-start sm:items-center gap-2">${/* UPDATED */''}<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 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"><div class="flex flex-col sm:flex-row items-start sm:items-center gap-2">${/* UPDATED */''}${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 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"><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>`;
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); } });
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 => {
@ -298,8 +308,24 @@
async function handleCreateUser(e) { e.preventDefault(); const username = e.target.elements['new-username'].value; const password = e.target.elements['new-password'].value; const role = e.target.elements['new-user-role'].value; const res = await apiCall('/admin/create-user', 'POST', { username, password, role }); if (res.success) { showMessage(res.data.message, 'success'); e.target.reset(); renderAdminDashboard(); } }
async function handleChangePassword(e) { e.preventDefault(); const currentPassword = e.target.elements['modal-current-pw'].value; const newPassword = e.target.elements['modal-new-pw'].value; const res = await apiCall('/user/change-password', 'POST', { currentPassword, newPassword }); if (res.success) { showMessage(res.data.message, 'success'); modalContainer.innerHTML = ''; } }
async function handleResetPassword(e) { e.preventDefault(); const username = e.target.elements['reset-username'].value; const newPassword = e.target.elements['reset-new-pw'].value; const res = await apiCall('/admin/reset-password', 'POST', { username, newPassword }); if (res.success) { showMessage(res.data.message, 'success'); modalContainer.innerHTML = ''; } }
async function handleAddPunch(e) { e.preventDefault(); const selected = e.target.elements['add-punch-user']; const userId = selected.value; const username = selected.options[selected.selectedIndex].dataset.username; const punchInTime = new Date(e.target.elements['add-punch-in'].value).toISOString(); const punchOutTime = new Date(e.target.elements['add-punch-out'].value).toISOString(); const res = await apiCall('/admin/add-punch', 'POST', { userId, username, punchInTime, punchOutTime }); if (res.success) { showMessage(res.data.message, 'success'); e.target.reset(); renderAdminDashboard(); } }
async function handleTimeOffRequest(e) { e.preventDefault(); const startDate = e.target.elements['start-date'].value; const endDate = e.target.elements['end-date'].value; const reason = e.target.elements['reason'].value; if (new Date(endDate) < new Date(startDate)) { return showMessage('End date cannot be before start date.', 'error'); } const res = await apiCall('/user/request-time-off', 'POST', { startDate, endDate, reason }); if (res.success) { showMessage(res.data.message, 'success'); e.target.reset(); renderEmployeeDashboard(); } }
async function handleAddPunch(e) {
e.preventDefault();
const selected = e.target.elements['add-punch-user'];
const userId = selected.value;
const username = selected.options[selected.selectedIndex].dataset.username;
const punchInTime = new Date(e.target.elements['add-punch-in'].value).toISOString();
// Handle the optional punch-out time
const punchOutValue = e.target.elements['add-punch-out'].value;
const punchOutTime = punchOutValue ? new Date(punchOutValue).toISOString() : null;
const res = await apiCall('/admin/add-punch', 'POST', { userId, username, punchInTime, punchOutTime });
if (res.success) {
showMessage(res.data.message, 'success');
e.target.reset();
renderAdminDashboard();
}
} async function handleTimeOffRequest(e) { e.preventDefault(); const startDate = e.target.elements['start-date'].value; const endDate = e.target.elements['end-date'].value; const reason = e.target.elements['reason'].value; if (new Date(endDate) < new Date(startDate)) { return showMessage('End date cannot be before start date.', 'error'); } const res = await apiCall('/user/request-time-off', 'POST', { startDate, endDate, reason }); if (res.success) { showMessage(res.data.message, 'success'); e.target.reset(); renderEmployeeDashboard(); } }
// --- Initializer ---
signOutBtn.addEventListener('click', () => handleSignOut('You have been signed out.'));

View File

@ -225,6 +225,46 @@ app.get('/api/admin/users', authenticateToken, requireRole('admin'), async (req,
}
});
app.post('/api/admin/add-punch', authenticateToken, requireRole('admin'), async (req, res) => {
try {
const { userId, username, punchInTime, punchOutTime } = req.body;
if (!userId || !punchInTime) {
return res.status(400).json({ message: "User and Punch In time are required." });
}
// SCENARIO 1: A "punch-in only" was submitted
if (!punchOutTime) {
// First, ensure this user doesn't already have an active punch
const existingPunch = await db.get("SELECT id FROM time_entries WHERE user_id = ? AND status = 'in'", [userId]);
if (existingPunch) {
return res.status(409).json({ message: `${username} is already punched in. Please edit their existing entry.` });
}
// If clear, insert the new active punch
await db.run(
'INSERT INTO time_entries (user_id, username, punch_in_time, status) VALUES (?, ?, ?, ?)',
[userId, username, punchInTime, 'in']
);
return res.status(201).json({ message: `Active punch successfully started for ${username}.` });
}
// SCENARIO 2: A complete entry (in and out) was submitted
if (new Date(punchOutTime) < new Date(punchInTime)) {
return res.status(400).json({ message: "Punch out time cannot be before punch in time." });
}
await db.run(
'INSERT INTO time_entries (user_id, username, punch_in_time, punch_out_time, status) VALUES (?, ?, ?, ?, ?)',
[userId, username, punchInTime, punchOutTime, 'out']
);
res.status(201).json({ message: `Completed entry added successfully for ${username}.` });
} catch (err) {
console.error("Error adding manual punch:", err);
res.status(500).json({ message: 'Failed to add manual punch.' });
}
});
// Gets all time entries for the detailed log view
app.get('/api/admin/logs', authenticateToken, requireRole('admin'), async (req, res) => {
try {