edit time off requests
This commit is contained in:
parent
bb7e1e7950
commit
2a7f0b5762
@ -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() {
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
39
server.js
39
server.js
@ -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;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user