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:
parent
6381038c90
commit
eca6f4ece8
@ -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()); }
|
||||
|
||||
@ -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">
|
||||
|
||||
29
server.js
29
server.js
@ -452,6 +452,35 @@ app.get('/api/user/notes', authenticateToken, async (req, res) => {
|
||||
res.status(500).json({ message: 'Failed to delete time entry.' });
|
||||
}
|
||||
});
|
||||
|
||||
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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user