add delete noted

This commit is contained in:
chris 2025-08-02 09:55:30 -04:00
parent 36c350437b
commit bf3bbc379a
2 changed files with 97 additions and 20 deletions

View File

@ -231,14 +231,12 @@
}
}
async function renderAdminDashboard() {
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');
// Filter out admins from the "leave a note" dropdown
const employeesOnly = allUsers.filter(u => u.role === 'employee');
mainViews.admin.innerHTML = `
@ -246,15 +244,19 @@
<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">
<h3 class="text-xl font-bold text-gray-700 mb-4">Leave a Note for Employee</h3>
<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>
<textarea id="note-text" placeholder="Write your note here..." class="w-full p-2 border rounded" rows="3" required></textarea>
<button type="submit" class="w-full bg-cyan-600 text-white p-2 rounded hover:bg-cyan-700">Submit Note</button>
<textarea id="note-text" placeholder="Write a new note here..." class="w-full p-2 border rounded" rows="3" required></textarea>
<div class="flex gap-2">
<button type="submit" class="w-full bg-cyan-600 text-white p-2 rounded hover:bg-cyan-700">Submit Note</button>
<button type="button" id="view-notes-btn" class="w-full bg-gray-600 text-white p-2 rounded hover:bg-gray-700">View Notes</button>
</div>
</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: ${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>
@ -264,11 +266,10 @@
<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('add-note-form').addEventListener('submit', handleAddNote); // Add listener for new form
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('add-note-form').addEventListener('submit', handleAddNote);
document.getElementById('view-notes-btn').addEventListener('click', handleViewNotesClick); // Add listener for new button
document.getElementById('admin-dashboard').addEventListener('click', handleAdminDashboardClick);
}
}
function renderArchiveView() {
apiCall('/admin/archives').then(res => {
if (!res.success) return;
@ -313,22 +314,68 @@
renderModal(`Reset Password for ${username}`, formHTML, handleResetPassword);
}
async function handleViewNotesClick() {
const userId = document.getElementById('note-user-select').value;
const container = document.getElementById('employee-notes-container');
if (!userId) {
return showMessage('Please select an employee to view their notes.', 'error');
}
container.innerHTML = 'Loading notes...';
const res = await apiCall(`/admin/notes/${userId}`);
if (res.success) {
if (res.data.length > 0) {
container.innerHTML = `
<h4 class="font-semibold mb-2 text-gray-600">Showing Notes for ${document.getElementById('note-user-select').options[document.getElementById('note-user-select').selectedIndex].text}</h4>
<ul class="space-y-3 max-h-72 overflow-y-auto border rounded-lg p-2 bg-gray-50">
${res.data.map(note => `
<li class="bg-white p-3 rounded-lg shadow-sm flex justify-between items-start">
<div>
<p class="text-gray-800 break-words">"${note.note_text}"</p>
<p class="text-xs text-gray-500 mt-2">- ${note.admin_username} on ${formatDate(note.created_at)}</p>
</div>
<button class="delete-note-btn text-red-500 hover:text-red-700 flex-shrink-0 ml-4" data-note-id="${note.id}">&times;</button>
</li>
`).join('')}
</ul>`;
} else {
container.innerHTML = '<p class="text-gray-500 text-center">No notes found for this employee.</p>';
}
} else {
container.innerHTML = '<p class="text-red-500 text-center">Could not load notes.</p>';
}
}
// --- Event Handlers (no changes needed below this line, but included for completeness) ---
async function handleAuthSubmit(e) { e.preventDefault(); const username = e.target.elements.username.value; const password = e.target.elements.password.value; const res = await apiCall('/login', 'POST', { username, password }); if (res.success) { authToken = res.data.token; user = res.data.user; localStorage.setItem('authToken', authToken); localStorage.setItem('user', JSON.stringify(user)); updateUI(); } }
const handleSignOut = (message) => { localStorage.clear(); authToken = null; user = null; updateUI(); if (message) showMessage(message, 'success'); };
const handlePunch = () => apiCall('/punch', 'POST').then(res => res.success && renderEmployeeDashboard());
function handleAdminDashboardClick(e) {
const target = e.target;
const { id, userid, username, role } = target.dataset;
if (target.classList.contains('edit-btn')) renderEditModal(id);
if (target.classList.contains('delete-btn') && confirm('Are you sure you want to delete this time entry?')) apiCall(`/admin/logs/${id}`, 'DELETE').then(res => res.success && renderAdminDashboard());
if (target.classList.contains('force-clock-out-btn') && confirm(`Are you sure you want to 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);
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()); }
if (target.classList.contains('delete-user-btn') && confirm(`PERMANENTLY DELETE user '${username}' and all their data? This cannot be undone.`)) apiCall(`/admin/delete-user/${username}`, 'DELETE').then(res => res.success && renderAdminDashboard());
if (target.classList.contains('approve-request-btn')) { if (confirm('Set this request to "approved"?')) apiCall('/admin/update-time-off-status', 'POST', { requestId: id, status: 'approved' }).then(res => res.success && renderAdminDashboard()); }
if (target.classList.contains('deny-request-btn')) { if (confirm('Set this request to "denied"?')) apiCall('/admin/update-time-off-status', 'POST', { requestId: id, status: 'denied' }).then(res => res.success && renderAdminDashboard()); }
const target = e.target;
const { id, userid, username, role, noteId } = target.dataset;
if (target.classList.contains('edit-btn')) renderEditModal(id);
if (target.classList.contains('delete-btn') && confirm('Are you sure you want to delete this time entry?')) apiCall(`/admin/logs/${id}`, 'DELETE').then(res => res.success && renderAdminDashboard());
if (target.classList.contains('force-clock-out-btn') && confirm(`Are you sure you want to 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);
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()); }
if (target.classList.contains('delete-user-btn') && confirm(`PERMANENTLY DELETE user '${username}' and all their data? This cannot be undone.`)) apiCall(`/admin/delete-user/${username}`, 'DELETE').then(res => res.success && renderAdminDashboard());
if (target.classList.contains('approve-request-btn')) { if (confirm('Set this request to "approved"?')) apiCall('/admin/update-time-off-status', 'POST', { requestId: id, status: 'approved' }).then(res => res.success && renderAdminDashboard()); }
if (target.classList.contains('deny-request-btn')) { if (confirm('Set this request to "denied"?')) apiCall('/admin/update-time-off-status', 'POST', { requestId: id, status: 'denied' }).then(res => res.success && renderAdminDashboard()); }
// ADDED: Logic to delete a note
if (target.classList.contains('delete-note-btn')) {
if (confirm('Are you sure you want to delete this note?')) {
apiCall(`/admin/notes/${noteId}`, 'DELETE').then(res => {
if (res.success) {
showMessage('Note deleted.', 'success');
handleViewNotesClick(); // Refresh the notes list
}
});
}
}
}
async function handleEditSubmit(e) { e.preventDefault(); const id = e.target.elements['edit-id'].value; const punch_in_time = new Date(e.target.elements['edit-in'].value).toISOString(); const punch_out_value = e.target.elements['edit-out'].value; const punch_out_time = punch_out_value ? new Date(punch_out_value).toISOString() : null; const res = await apiCall(`/admin/logs/${id}`, 'PUT', { punch_in_time, punch_out_time }); if (res.success) { modalContainer.innerHTML = ''; renderAdminDashboard(); showMessage('Entry updated successfully.', 'success'); } }
const handleArchive = () => { if (confirm('Are you sure you want to archive all completed time entries? This action cannot be undone.')) { apiCall('/admin/archive', 'POST').then(res => { if(res.success) { showMessage('Records archived successfully.', 'success'); renderAdminDashboard(); } }); } };
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(); } }

View File

@ -387,6 +387,36 @@ app.get('/api/user/notes', authenticateToken, async (req, res) => {
}
});
// --- Note Management Routes ---
// Admin gets all notes for a specific employee
app.get('/api/admin/notes/:userId', authenticateToken, requireRole('admin'), async (req, res) => {
try {
const { userId } = req.params;
const notes = await db.all(
"SELECT id, admin_username, note_text, created_at FROM notes WHERE employee_user_id = ? ORDER BY created_at DESC",
[userId]
);
res.json(notes);
} catch (err) {
res.status(500).json({ message: 'Failed to fetch notes.' });
}
});
// Admin deletes a specific note
app.delete('/api/admin/notes/:noteId', authenticateToken, requireRole('admin'), async (req, res) => {
try {
const { noteId } = req.params;
const result = await db.run('DELETE FROM notes WHERE id = ?', [noteId]);
if (result.changes === 0) {
return res.status(404).json({ message: "Note not found." });
}
res.json({ message: 'Note deleted successfully.' });
} catch (err) {
res.status(500).json({ message: 'Failed to delete note.' });
}
});
}
startServer();