edit time off requests

This commit is contained in:
chris 2025-08-10 09:51:16 -04:00
parent bb7e1e7950
commit 2a7f0b5762
3 changed files with 166 additions and 20 deletions

View File

@ -14,7 +14,8 @@ import {
handleViewNotesClick, handleViewNotesClick,
renderArchiveView, renderArchiveView,
renderTimeOffHistoryView, renderTimeOffHistoryView,
updatePendingRequestsList updatePendingRequestsList,
renderEditTimeOffModal
} from './ui.js'; } from './ui.js';
// --- STATE MANAGEMENT --- // --- STATE MANAGEMENT ---
@ -214,17 +215,78 @@ function handleAdminDashboardClick(e) {
if (target.classList.contains('delete-note-btn')) { if (confirm('Delete this note?')) { apiCall(`/admin/notes/${noteId}`, 'DELETE').then(res => { if (res.success) { showMessage('Note deleted.', 'success'); handleViewNotesClick(); }});}} if (target.classList.contains('delete-note-btn')) { if (confirm('Delete this note?')) { apiCall(`/admin/notes/${noteId}`, 'DELETE').then(res => { if (res.success) { showMessage('Note deleted.', 'success'); handleViewNotesClick(); }});}}
} }
// --- NEW: Handler for editing a time-off request ---
async function handleEditTimeOffSubmit(e) {
e.preventDefault();
const id = e.target.elements['edit-request-id'].value;
const startDate = e.target.elements['edit-start-date'].value;
const endDate = e.target.elements['edit-end-date'].value;
const reason = e.target.elements['edit-reason'].value;
if (new Date(endDate) < new Date(startDate)) {
return showMessage('End date cannot be before start date.', 'error');
}
const res = await apiCall(`/user/time-off-requests/${id}`, 'PUT', { startDate, endDate, reason });
if (res.success) {
showMessage(res.data.message, 'success');
document.getElementById('modal-container').innerHTML = '';
renderEmployeeDashboard(); // Refresh the dashboard to show changes
}
}
// --- LISTENER ATTACHMENT FUNCTIONS --- // --- LISTENER ATTACHMENT FUNCTIONS ---
export function attachAuthFormListener() { export function attachAuthFormListener() {
const form = document.getElementById('auth-form'); const form = document.getElementById('auth-form');
if (form) form.addEventListener('submit', handleAuthSubmit); if (form) form.addEventListener('submit', handleAuthSubmit);
} }
// --- UPDATED: Employee dashboard listeners now use event delegation ---
export function attachEmployeeDashboardListeners() { export function attachEmployeeDashboardListeners() {
document.getElementById('punch-btn')?.addEventListener('click', handlePunch); const dashboard = document.getElementById('employee-dashboard');
document.getElementById('change-password-btn')?.addEventListener('click', () => renderChangePasswordModal(handleChangePassword)); if (!dashboard) return;
document.getElementById('time-off-form')?.addEventListener('submit', handleTimeOffRequest);
document.getElementById('view-request-history-btn')?.addEventListener('click', handleViewRequestHistoryClick); // Use one listener for the entire dashboard
dashboard.addEventListener('click', async (e) => {
const target = e.target;
if (target.id === 'punch-btn') {
handlePunch();
}
if (target.id === 'change-password-btn') {
renderChangePasswordModal(handleChangePassword);
}
if (target.id === 'view-request-history-btn') {
handleViewRequestHistoryClick();
}
if (target.classList.contains('delete-request-btn')) {
const id = target.dataset.id;
if (confirm('Are you sure you want to delete this time off request?')) {
const res = await apiCall(`/user/time-off-requests/${id}`, 'DELETE');
if (res.success) {
showMessage(res.data.message, 'success');
renderEmployeeDashboard();
}
}
}
if (target.classList.contains('edit-request-btn')) {
const id = target.dataset.id;
// We need to get the full request data to pre-fill the form
const res = await apiCall('/user/time-off-requests/history');
if (res.success) {
const requestToEdit = res.data.find(r => r.id == id);
if (requestToEdit) {
renderEditTimeOffModal(requestToEdit, handleEditTimeOffSubmit);
}
}
}
});
// Handle form submissions separately
const timeOffForm = document.getElementById('time-off-form');
if (timeOffForm) {
timeOffForm.addEventListener('submit', handleTimeOffRequest);
}
} }
export function attachAdminDashboardListeners() { export function attachAdminDashboardListeners() {

View File

@ -38,7 +38,6 @@ export function showView(viewName) {
// --- MASTER UI UPDATE FUNCTION --- // --- MASTER UI UPDATE FUNCTION ---
export function updateUI() { export function updateUI() {
// This function was empty, it has been corrected.
try { try {
const storedUser = localStorage.getItem('user'); const storedUser = localStorage.getItem('user');
const authToken = localStorage.getItem('authToken'); const authToken = localStorage.getItem('authToken');
@ -126,7 +125,35 @@ export async function renderEmployeeDashboard() {
<button type="submit" class="w-full bg-indigo-600 text-white p-2 rounded hover:bg-indigo-700">Submit Request</button> <button type="submit" class="w-full bg-indigo-600 text-white p-2 rounded hover:bg-indigo-700">Submit Request</button>
<div class="md:col-span-3"><label class="block text-sm font-medium">Reason (optional)</label><input type="text" id="reason" placeholder="e.g., Vacation" class="w-full p-2 border rounded"></div> <div class="md:col-span-3"><label class="block text-sm font-medium">Reason (optional)</label><input type="text" id="reason" placeholder="e.g., Vacation" class="w-full p-2 border rounded"></div>
</form> </form>
<div class="mt-4 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">Dates</th><th class="p-2">Reason</th><th class="p-2">Status</th></tr></thead><tbody>${requests.map(r => `<tr class="border-t"><td class="p-2 whitespace-nowrap">${utils.formatDate(r.start_date)} - ${utils.formatDate(r.end_date)}</td><td class="p-2">${r.reason || ''}</td><td class="p-2 font-medium capitalize text-${r.status === 'approved' ? 'green' : r.status === 'denied' ? 'red' : 'gray'}-600">${r.status}</td></tr>`).join('') || '<tr><td colspan="3" class="text-center p-4">No upcoming or pending requests.</td></tr>'}</tbody></table></div> <div class="mt-4 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">Dates</th>
<th class="p-2">Reason</th>
<th class="p-2">Status</th>
<th class="p-2">Actions</th>
</tr>
</thead>
<tbody id="time-off-requests-tbody">
${requests.map(r => `
<tr class="border-t">
<td class="p-2 whitespace-nowrap">${utils.formatDate(r.start_date)} - ${utils.formatDate(r.end_date)}</td>
<td class="p-2">${r.reason || ''}</td>
<td class="p-2 font-medium capitalize text-${r.status === 'approved' ? 'green' : r.status === 'denied' ? 'red' : 'gray'}-600">${r.status}</td>
<td class="p-2">
${r.status === 'pending' ? `
<div class="flex gap-2">
<button class="edit-request-btn font-medium text-blue-600 hover:underline" data-id="${r.id}">Edit</button>
<button class="delete-request-btn font-medium text-red-600 hover:underline" data-id="${r.id}">Delete</button>
</div>
` : ''}
</td>
</tr>
`).join('') || '<tr><td colspan="4" class="text-center p-4">No upcoming or pending requests.</td></tr>'}
</tbody>
</table>
</div>
</div> </div>
<div class="bg-white rounded-xl shadow-md p-6"> <div class="bg-white rounded-xl shadow-md p-6">
<h3 class="text-xl font-bold text-gray-700 mb-2">My Time Log</h3> <h3 class="text-xl font-bold text-gray-700 mb-2">My Time Log</h3>
@ -201,6 +228,13 @@ export async function renderAdminDashboard() {
</div> </div>
`; `;
if (lastAdminTab && lastAdminTab !== 'overview') {
document.querySelector('.tab-btn[data-tab="overview"]').classList.remove('active-tab');
document.getElementById('tab-content-overview').classList.add('hidden');
document.querySelector(`.tab-btn[data-tab="${lastAdminTab}"]`).classList.add('active-tab');
document.getElementById(`tab-content-${lastAdminTab}`).classList.remove('hidden');
}
attachAdminDashboardListeners(); attachAdminDashboardListeners();
punchedInEntries.forEach(entry => { punchedInEntries.forEach(entry => {
const durationCell = document.getElementById(`admin-duration-${entry.id}`); const durationCell = document.getElementById(`admin-duration-${entry.id}`);
@ -212,16 +246,6 @@ export async function renderAdminDashboard() {
adminTimerIntervals.push(intervalId); adminTimerIntervals.push(intervalId);
} }
}); });
if (lastAdminTab && lastAdminTab !== 'overview') {
// Remove active state from the default tab and panel
document.querySelector('.tab-btn[data-tab="overview"]').classList.remove('active-tab');
document.getElementById('tab-content-overview').classList.add('hidden');
// Add active state to the last viewed tab and panel
document.querySelector(`.tab-btn[data-tab="${lastAdminTab}"]`).classList.add('active-tab');
document.getElementById(`tab-content-${lastAdminTab}`).classList.remove('hidden');
}
} }
export function renderArchiveView() { export function renderArchiveView() {
@ -276,6 +300,28 @@ export function renderRequestHistoryModal(requests) {
document.querySelector('.modal-overlay').addEventListener('click', (e) => { if (e.target === e.currentTarget) modalContainer.innerHTML = ''; }); document.querySelector('.modal-overlay').addEventListener('click', (e) => { if (e.target === e.currentTarget) modalContainer.innerHTML = ''; });
} }
export function renderEditTimeOffModal(request, submitHandler) {
const startDate = new Date(request.start_date).toISOString().split('T')[0];
const endDate = new Date(request.end_date).toISOString().split('T')[0];
const formHTML = `
<input type="hidden" id="edit-request-id" value="${request.id}">
<div>
<label class="block text-sm font-medium">Start Date</label>
<input type="date" id="edit-start-date" class="w-full p-2 border rounded" value="${startDate}" required>
</div>
<div>
<label class="block text-sm font-medium">End Date</label>
<input type="date" id="edit-end-date" class="w-full p-2 border rounded" value="${endDate}" required>
</div>
<div>
<label class="block text-sm font-medium">Reason (optional)</label>
<input type="text" id="edit-reason" placeholder="e.g., Vacation" class="w-full p-2 border rounded" value="${request.reason || ''}">
</div>
`;
renderModal('Edit Time Off Request', formHTML, submitHandler);
}
// --- UI HELPER FUNCTIONS --- // --- UI HELPER FUNCTIONS ---
export async function handleViewNotesClick() { export async function handleViewNotesClick() {
@ -309,17 +355,16 @@ export async function handleViewNotesClick() {
container.innerHTML = '<p class="text-red-500 text-center">Could not load notes.</p>'; container.innerHTML = '<p class="text-red-500 text-center">Could not load notes.</p>';
} }
} }
// This function ONLY updates the pending requests table.
export function updatePendingRequestsList(requests) { export function updatePendingRequestsList(requests) {
const tableBody = document.querySelector('#tab-content-overview table tbody'); const tableBody = document.querySelector('#tab-content-overview table tbody');
if (!tableBody) return; // Exit if the table isn't on the page if (!tableBody) return;
if (requests.length === 0) { if (requests.length === 0) {
tableBody.innerHTML = '<tr><td colspan="4" class="text-center p-4">No pending requests.</td></tr>'; tableBody.innerHTML = '<tr><td colspan="4" class="text-center p-4">No pending requests.</td></tr>';
return; return;
} }
// Rebuild only the rows of the table
tableBody.innerHTML = requests.map(r => ` tableBody.innerHTML = requests.map(r => `
<tr class="border-t"> <tr class="border-t">
<td class="p-2">${r.username}</td> <td class="p-2">${r.username}</td>

View File

@ -397,6 +397,45 @@ function setupRoutes() {
} }
}); });
// NEW: Endpoint to UPDATE a specific time-off request
app.put('/api/user/time-off-requests/:id', authenticateToken, async (req, res) => {
try {
const { id } = req.params;
const { startDate, endDate, reason } = req.body;
// Ensure users can only edit their own pending requests
const result = await db.run(
`UPDATE time_off_requests
SET start_date = ?, end_date = ?, reason = ?
WHERE id = ? AND user_id = ? AND status = 'pending'`,
[startDate, endDate, reason, id, req.user.id]
);
if (result.changes === 0) {
return res.status(404).json({ message: "Pending request not found or you don't have permission to edit it." });
}
res.json({ message: 'Time off request updated successfully.' });
} catch (err) {
res.status(500).json({ message: 'Error updating time off request.' });
}
});
// NEW: Endpoint to DELETE a specific time-off request
app.delete('/api/user/time-off-requests/:id', authenticateToken, async (req, res) => {
try {
const { id } = req.params;
// Ensure users can only delete their own pending requests
const result = await db.run(
`DELETE FROM time_off_requests
WHERE id = ? AND user_id = ? AND status = 'pending'`,
[id, req.user.id]
);
if (result.changes === 0) {
return res.status(404).json({ message: "Pending request not found or you don't have permission to delete it." });
}
res.json({ message: 'Time off request deleted successfully.' });
} catch (err) {
res.status(500).json({ message: 'Error deleting time off request.' });
}
});
app.get('/api/admin/notes/:userId', authenticateToken, requireRole('admin'), async (req, res) => { app.get('/api/admin/notes/:userId', authenticateToken, requireRole('admin'), async (req, res) => {
try { try {
const { userId } = req.params; const { userId } = req.params;