feat: Allow individual log archiving

Adds the ability for admins to archive individual time log entries.

- Adds an 'Archive' button to the detailed logs table in the admin UI.
- Adds a new API endpoint  to handle the
  archiving of a single log entry.
- Updates the frontend to call the new endpoint when the 'Archive'
  button is clicked.
This commit is contained in:
chris 2025-11-20 12:50:25 -05:00
parent 6381038c90
commit eca6f4ece8
3 changed files with 32 additions and 1 deletions

View File

@ -301,6 +301,7 @@ function handleAdminDashboardClick(e) {
if (target.classList.contains('edit-btn')) renderEditModal(id, handleEditSubmit);
if (target.classList.contains('delete-btn') && confirm('Delete this time entry?')) apiCall(`/admin/logs/${id}`, 'DELETE').then(res => res.success && renderAdminDashboard());
if (target.classList.contains('archive-log-btn') && confirm('Archive this time entry?')) apiCall(`/admin/logs/archive/${id}`, 'POST').then(res => res.success && renderAdminDashboard());
if (target.classList.contains('force-clock-out-btn') && confirm(`Force clock out ${username}?`)) apiCall('/admin/force-clock-out', 'POST', { userId: userid }).then(res => res.success && renderAdminDashboard());
if (target.classList.contains('reset-pw-btn')) renderResetPasswordModal(username, handleResetPassword);
if (target.classList.contains('change-role-btn')) { const newRole = role === 'admin' ? 'employee' : 'admin'; if (confirm(`Change ${username} to ${newRole}?`)) apiCall('/admin/update-role', 'POST', { username, newRole }).then(res => res.success && renderAdminDashboard()); }

View File

@ -242,7 +242,8 @@ export async function renderAdminDashboard() {
<button id="archive-btn" class="px-4 py-2 bg-amber-500 text-white rounded-lg hover:bg-amber-600">Archive Time 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><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>
${e.status === 'out' ? `<button class="archive-log-btn font-medium text-amber-600 hover:underline" data-id="${e.id}">Archive</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">

View File

@ -453,6 +453,35 @@ app.get('/api/user/notes', authenticateToken, async (req, res) => {
}
});
app.post('/api/admin/logs/archive/:id', authenticateToken, requireRole('admin'), async (req, res) => {
try {
const { id } = req.params;
const entry = await db.get("SELECT * FROM time_entries WHERE id = ?", [id]);
if (!entry) {
return res.status(404).json({ message: "Time entry not found." });
}
if (entry.status !== 'out') {
return res.status(400).json({ message: "Only completed (punched-out) entries can be archived." });
}
const archivedAt = new Date().toISOString();
await db.exec('BEGIN TRANSACTION');
await db.run('INSERT INTO archived_time_entries (id, user_id, username, punch_in_time, punch_out_time, status, archived_at) VALUES (?, ?, ?, ?, ?, ?, ?)',
[entry.id, entry.user_id, entry.username, entry.punch_in_time, entry.punch_out_time, entry.status, archivedAt]
);
await db.run('DELETE FROM time_entries WHERE id = ?', [id]);
await db.exec('COMMIT');
res.json({ message: 'Time entry archived successfully.' });
} catch (err) {
await db.exec('ROLLBACK');
console.error("Archiving error:", err)
res.status(500).json({ message: 'Failed to archive time entry.' });
}
});
app.post('/api/admin/force-clock-out', authenticateToken, requireRole('admin'), async (req, res) => {
try {
const { userId } = req.body;