time-off history

This commit is contained in:
chris 2025-08-02 11:17:21 -04:00
parent bf3bbc379a
commit 68ba7c4a5e
2 changed files with 82 additions and 25 deletions

View File

@ -151,13 +151,12 @@
async function renderEmployeeDashboard() { async function renderEmployeeDashboard() {
clearInterval(employeeTimerInterval); clearInterval(employeeTimerInterval);
// Fetch notes along with other data
const [statusRes, timeOffRes, notesRes] = await Promise.all([apiCall('/status'), apiCall('/user/time-off-requests'), apiCall('/user/notes')]); const [statusRes, timeOffRes, notesRes] = await Promise.all([apiCall('/status'), apiCall('/user/time-off-requests'), apiCall('/user/notes')]);
if (!statusRes.success || !timeOffRes.success || !notesRes.success) return; if (!statusRes.success || !timeOffRes.success || !notesRes.success) return;
const entries = statusRes.data; const entries = statusRes.data;
const requests = timeOffRes.data; const requests = timeOffRes.data;
const notes = notesRes.data; // Get notes data const notes = notesRes.data;
const last = entries[0]; const last = entries[0];
const punchedIn = last?.status === 'in'; const punchedIn = last?.status === 'in';
let totalMilliseconds = entries.reduce((acc, e) => { let totalMilliseconds = entries.reduce((acc, e) => {
@ -181,36 +180,28 @@
<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-4">Notes from Admin</h3> <h3 class="text-xl font-bold text-gray-700 mb-4">Notes from Admin</h3>
<ul class="space-y-4 mt-4 max-h-60 overflow-y-auto"> <ul class="space-y-4 mt-4 max-h-60 overflow-y-auto">${notes.length > 0 ? notes.map(note => `<li class="bg-amber-50 p-3 rounded-lg border-l-4 border-amber-400"><p class="text-gray-800 break-words">"${note.note_text}"</p><p class="text-xs text-gray-500 text-right mt-2">- ${note.admin_username} on ${formatDate(note.created_at)}</p></li>`).join('') : '<p class="text-gray-500 text-center">You have no new notes.</p>'}</ul>
${notes.length > 0 ? notes.map(note => `
<li class="bg-amber-50 p-3 rounded-lg border-l-4 border-amber-400">
<p class="text-gray-800 break-words">"${note.note_text}"</p>
<p class="text-xs text-gray-500 text-right mt-2">- ${note.admin_username} on ${formatDate(note.created_at)}</p>
</li>
`).join('') : '<p class="text-gray-500 text-center">You have no new notes.</p>'}
</ul>
</div> </div>
<div class="bg-white rounded-xl shadow-md p-6"> <div class="bg-white rounded-xl shadow-md p-6">
<div class="flex justify-between items-center"> <div class="flex justify-between items-center"><h3 class="text-xl font-bold text-gray-700">My Account</h3><button id="change-password-btn" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">Change Password</button></div>
<h3 class="text-xl font-bold text-gray-700">My Account</h3> <div class="mt-4 p-4 bg-blue-50 rounded-lg"><h4 class="font-semibold text-blue-800">My Total Hours (This Pay Period)</h4><p class="text-3xl font-bold text-blue-600" id="employee-total-hours">${formatDecimal(totalMilliseconds)}</p></div>
<button id="change-password-btn" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">Change Password</button>
</div>
<div class="mt-4 p-4 bg-blue-50 rounded-lg">
<h4 class="font-semibold text-blue-800">My Total Hours (This Pay Period)</h4>
<p class="text-3xl font-bold text-blue-600" id="employee-total-hours">${formatDecimal(totalMilliseconds)}</p>
</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-4">Time Off Requests</h3> <div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-bold text-gray-700">Time Off Requests</h3>
<button id="view-request-history-btn" class="text-sm px-3 py-1 bg-gray-200 rounded-lg hover:bg-gray-300">View History</button>
</div>
<form id="time-off-form" class="grid md:grid-cols-3 gap-4 items-end bg-gray-50 p-4 rounded-lg"> <form id="time-off-form" class="grid md:grid-cols-3 gap-4 items-end bg-gray-50 p-4 rounded-lg">
<div><label class="block text-sm font-medium">Start Date</label><input type="date" id="start-date" class="w-full p-2 border rounded" required></div> <div><label class="block text-sm font-medium">Start Date</label><input type="date" id="start-date" class="w-full p-2 border rounded" required></div>
<div><label class="block text-sm font-medium">End Date</label><input type="date" id="end-date" class="w-full p-2 border rounded" required></div> <div><label class="block text-sm font-medium">End Date</label><input type="date" id="end-date" class="w-full p-2 border rounded" required></div>
<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">${formatDate(r.start_date)} - ${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 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></tr></thead><tbody>${requests.map(r => `<tr class="border-t"><td class="p-2 whitespace-nowrap">${formatDate(r.start_date)} - ${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> </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>
<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">In</th><th class="p-2">Out</th><th class="p-2">Duration (Hours)</th></tr></thead><tbody>${entries.map(e => `<tr class="border-t"><td class="p-2">${formatDateTime(e.punch_in_time)}</td><td class="p-2">${formatDateTime(e.punch_out_time)}</td><td class="p-2" id="duration-${e.id}">${e.status === 'in' ? 'Running...' : formatDecimal(new Date(e.punch_out_time) - new Date(e.punch_in_time))}</td></tr>`).join('') || '<tr><td colspan="3" class="text-center p-4">No entries.</td></tr>'}</tbody></table></div> <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">In</th><th class="p-2">Out</th><th class="p-2">Duration (Hours)</th></tr></thead><tbody>${entries.map(e => `<tr class="border-t"><td class="p-2">${formatDateTime(e.punch_in_time)}</td><td class="p-2">${formatDateTime(e.punch_out_time)}</td><td class="p-2" id="duration-${e.id}">${e.status === 'in' ? 'Running...' : formatDecimal(new Date(e.punch_out_time) - new Date(e.punch_in_time))}</td></tr>`).join('') || '<tr><td colspan="3" class="text-center p-4">No entries.</td></tr>'}</tbody></table></div>
@ -219,6 +210,8 @@
document.getElementById('punch-btn').addEventListener('click', handlePunch); document.getElementById('punch-btn').addEventListener('click', handlePunch);
document.getElementById('change-password-btn').addEventListener('click', renderChangePasswordModal); document.getElementById('change-password-btn').addEventListener('click', renderChangePasswordModal);
document.getElementById('time-off-form').addEventListener('submit', handleTimeOffRequest); document.getElementById('time-off-form').addEventListener('submit', handleTimeOffRequest);
document.getElementById('view-request-history-btn').addEventListener('click', handleViewRequestHistoryClick); // Attach handler to new button
if (punchedIn) { if (punchedIn) {
const durationCell = document.getElementById(`duration-${last.id}`); const durationCell = document.getElementById(`duration-${last.id}`);
const totalHoursCell = document.getElementById('employee-total-hours'); const totalHoursCell = document.getElementById('employee-total-hours');
@ -400,6 +393,48 @@ async function renderAdminDashboard() {
renderAdminDashboard(); renderAdminDashboard();
} }
} }
function renderRequestHistoryModal(requests) {
const modalBody = `
<div class="max-h-[70vh] overflow-y-auto">
<table class="min-w-full text-sm text-left">
<thead class="bg-gray-50 sticky top-0">
<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">${formatDate(r.start_date)} - ${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 history found.</td></tr>'}
</tbody>
</table>
</div>`;
// Using a simplified modal since we only need a "Close" button
modalContainer.innerHTML = `
<div class="modal-overlay">
<div class="modal-content">
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-bold">Full Time Off History</h3>
<button class="cancel-modal-btn font-bold text-2xl">&times;</button>
</div>
${modalBody}
</div>
</div>`;
document.querySelector('.cancel-modal-btn').addEventListener('click', () => modalContainer.innerHTML = '');
document.querySelector('.modal-overlay').addEventListener('click', (e) => { if (e.target === e.currentTarget) modalContainer.innerHTML = ''; });
}
async function handleViewRequestHistoryClick() {
const res = await apiCall('/user/time-off-requests/history');
if (res.success) {
renderRequestHistoryModal(res.data);
}
}
async function handleAddNote(e) { async function handleAddNote(e) {
e.preventDefault(); e.preventDefault();
const userId = e.target.elements['note-user-select'].value; const userId = e.target.elements['note-user-select'].value;

View File

@ -177,12 +177,34 @@ function setupRoutes() {
app.get('/api/user/time-off-requests', authenticateToken, async (req, res) => { app.get('/api/user/time-off-requests', authenticateToken, async (req, res) => {
try { try {
const rows = await db.all("SELECT * FROM time_off_requests WHERE user_id = ? ORDER BY start_date DESC", [req.user.id]); // This query now only gets requests that are pending OR have an end date in the future.
res.json(rows); const rows = await db.all(
} catch { `SELECT * FROM time_off_requests
res.status(500).json({ message: "Failed to fetch requests." }); WHERE user_id = ? AND (status = 'pending' OR end_date >= date('now'))
ORDER BY start_date ASC`,
[req.user.id]
);
res.json(rows);
} catch (err) {
console.error("Error fetching time off requests:", err);
res.status(500).json({ message: "Failed to fetch requests." });
} }
}); });
app.get('/api/user/time-off-requests/history', authenticateToken, async (req, res) => {
try {
// This query gets ALL requests for the user, sorted by newest first.
const rows = await db.all(
"SELECT * FROM time_off_requests WHERE user_id = ? ORDER BY start_date DESC",
[req.user.id]
);
res.json(rows);
} catch (err) {
console.error("Error fetching time off history:", err);
res.status(500).json({ message: "Failed to fetch request history." });
}
});
// --- Admin Tools --- // --- Admin Tools ---
app.post('/api/admin/force-clock-out', authenticateToken, requireRole('admin'), async (req, res) => { app.post('/api/admin/force-clock-out', authenticateToken, requireRole('admin'), async (req, res) => {