From 64da46717afeffa6f4d277996363405814102edf Mon Sep 17 00:00:00 2001 From: chris Date: Tue, 29 Jul 2025 17:00:15 -0400 Subject: [PATCH] fix view archives and add delete time off request --- .env.example | 3 ++ .gitignore | 6 +++ docker-compose.yaml | 3 +- index.html | 102 ++++++++++++++++++++++++++---------- server.js | 122 +++++++++++++++++++++++++++----------------- 5 files changed, 162 insertions(+), 74 deletions(-) create mode 100644 .env.example create mode 100644 .gitignore diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..879c665 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +ADMIN_USERNAME="admin" +ADMIN_PASSWORD="adminpassword" +JWT_SECRET="" ##random number string diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b7a3255 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ + +# gitignore +node_modules +package-lock.json +.env +.git \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index 2c60f3e..fa2370d 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -14,7 +14,7 @@ services: # Port mapping: map port 3000 on the host machine to port 3000 in the container. # This allows you to access the application via http://:3000 ports: - - "3002:3000" + - "3004:3000" # Volume mapping: persist the database file. # This creates a 'data' folder in your project directory on the host machine # and links it to the /usr/src/app/data directory inside the container. @@ -25,3 +25,4 @@ services: # current directory to set environment variables inside the container. env_file: - .env + network_mode: bridge diff --git a/index.html b/index.html index c161a11..29bdc79 100644 --- a/index.html +++ b/index.html @@ -52,7 +52,7 @@ const navUserControls = document.getElementById('nav-user-controls'), welcomeMessage = document.getElementById('welcome-message'), signOutBtn = document.getElementById('sign-out-btn'); const messageBox = document.getElementById('message-box'), loadingSpinner = document.getElementById('loading-spinner'), modalContainer = document.getElementById('modal-container'); let authToken = localStorage.getItem('authToken'), user = JSON.parse(localStorage.getItem('user')), allTimeEntries = [], allUsers = [], employeeTimerInterval = null; - + // --- Helper Functions --- const showLoading = (show) => loadingSpinner.innerHTML = show ? `
` : ''; const showMessage = (message, type = 'success') => { messageBox.innerHTML = ``; messageBox.classList.remove('hidden'); }; @@ -61,7 +61,7 @@ const formatDate = (s) => s ? new Date(s).toLocaleDateString() : 'N/A'; const toLocalISO = (d) => { if (!d) return ''; const date = new Date(d); return new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(0, 16); }; const formatDuration = (ms) => { if (!ms || ms < 0) return '00:00:00'; const s = Math.floor(ms / 1000); const h = Math.floor(s / 3600); const m = Math.floor((s % 3600) / 60); return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s % 60).padStart(2, '0')}`; }; - + // --- API Calls --- async function apiCall(endpoint, method = 'GET', body = null) { const headers = { 'Content-Type': 'application/json' }; @@ -71,16 +71,15 @@ const response = await fetch(`/api${endpoint}`, { method, headers, body: body ? JSON.stringify(body) : null }); if (!response.ok) { - // Check for token expiration / invalid token if (response.status === 401 || response.status === 403) { handleSignOut('Your session has expired. Please log in again.'); - return { success: false }; // Stop further processing + return { success: false }; } const text = await response.text(); const data = text ? JSON.parse(text) : {}; throw new Error(data.message || `HTTP error ${response.status}`); } - + const text = await response.text(); return { success: true, data: text ? JSON.parse(text) : null }; } catch (error) { @@ -90,10 +89,10 @@ showLoading(false); } } - + // --- View Management --- const showView = (viewName) => { clearInterval(employeeTimerInterval); Object.keys(mainViews).forEach(v => mainViews[v].classList.toggle('hidden', v !== viewName)); } - + // --- UI Rendering --- function updateUI() { if (authToken && user) { @@ -106,12 +105,12 @@ renderAuthView(); } } - + function renderAuthView() { mainViews.auth.innerHTML = `

Employee Login

`; document.getElementById('auth-form').addEventListener('submit', handleAuthSubmit); } - + async function renderEmployeeDashboard() { clearInterval(employeeTimerInterval); const [statusRes, timeOffRes] = await Promise.all([apiCall('/status'), apiCall('/user/time-off-requests')]); @@ -122,7 +121,10 @@ const last = entries[0]; const punchedIn = last?.status === 'in'; let totalMilliseconds = entries.reduce((acc, e) => e.status === 'out' ? acc + (new Date(e.punch_out_time) - new Date(e.punch_in_time)) : acc, 0); - + + // --- NEW: Get today's date for the min attribute on date inputs --- + const today = new Date().toISOString().split('T')[0]; + mainViews.employee.innerHTML = `
@@ -138,8 +140,8 @@

Time Off Requests

-
-
+
+
@@ -153,25 +155,25 @@ document.getElementById('punch-btn').addEventListener('click', handlePunch); document.getElementById('change-password-btn').addEventListener('click', renderChangePasswordModal); document.getElementById('time-off-form').addEventListener('submit', handleTimeOffRequest); - + if (punchedIn) { const durationCell = document.getElementById(`duration-${last.id}`), totalHoursCell = document.getElementById('employee-total-hours'), punchInTime = new Date(last.punch_in_time); employeeTimerInterval = setInterval(() => { const elapsed = new Date() - punchInTime; durationCell.textContent = formatDuration(elapsed); totalHoursCell.textContent = formatDecimal(totalMilliseconds + elapsed); }, 1000); } } - + async function renderAdminDashboard() { const [logsRes, usersRes, requestsRes] = await Promise.all([apiCall('/admin/logs'), apiCall('/admin/users'), apiCall('/admin/time-off-requests')]); if (!logsRes.success || !usersRes.success || !requestsRes.success) return; allTimeEntries = logsRes.data; allUsers = usersRes.data; const allRequests = requestsRes.data; const employeeTotals = allTimeEntries.reduce((acc, entry) => { const dur = (entry.status === 'out' ? new Date(entry.punch_out_time) - new Date(entry.punch_in_time) : new Date() - new Date(entry.punch_in_time)); acc[entry.username] = (acc[entry.username] || 0) + dur; return acc; }, {}); const punchedInEntries = allTimeEntries.filter(e => e.status === 'in'); - + mainViews.admin.innerHTML = `

Admin Dashboard

Currently Punched In

    ${punchedInEntries.map(e => `
  • ${e.username}
    Since: ${formatDateTime(e.punch_in_time)}
  • `).join('') || '
  • None
  • '}
-

Time Off Requests

${allRequests.map(r => ``).join('') || ''}
EmployeeDatesReasonStatusActions
${r.username}${formatDate(r.start_date)} - ${formatDate(r.end_date)}${r.reason||''}${r.status}${r.status === 'pending' ? `` : ''}
No requests.
+

Time Off Requests

${allRequests.map(r => ``).join('') || ''}
EmployeeDatesReasonStatusActions
${r.username}${formatDate(r.start_date)} - ${formatDate(r.end_date)}${r.reason||''}${r.status}${r.status === 'pending' ? `` : ''}
No requests.

Hours by Employee

${Object.keys(employeeTotals).map(u => ``).join('')}
EmployeeTotal Hours
${u}${formatDecimal(employeeTotals[u])}

Detailed Logs

${allTimeEntries.map(e => ``).join('')}
EmployeeInOutDurationActions
${e.username||'N/A'}${formatDateTime(e.punch_in_time)}${formatDateTime(e.punch_out_time)}${formatDecimal(new Date(e.punch_out_time) - new Date(e.punch_in_time))}

User & Payroll Management

Create User

Add Manual Punch

Manage Users

${allUsers.map(u => ``).join('')}
UsernameRoleActions
${u.username}${u.role}${u.isPrimary ? `Primary Admin` : `${u.username !== user.username ? `` : ''}`}
@@ -182,15 +184,62 @@ document.getElementById('add-punch-form').addEventListener('submit', handleAddPunch); document.getElementById('admin-dashboard').addEventListener('click', handleAdminDashboardClick); } - - function renderArchiveView() { /* ... unchanged ... */ } + + async function renderArchiveView() { + showView('archive'); + const res = await apiCall('/admin/archives'); + if (!res.success) { + mainViews.archive.innerHTML = `
Could not load archives.
`; + return; + } + const archives = res.data; + + mainViews.archive.innerHTML = ` +
+
+
+

Archived Records

+ +
+
+ + + + + + + + + + + + ${archives.map(e => ` + + + + + + + + `).join('') || ''} + +
EmployeePunch InPunch OutDuration (Hours)Archived At
${e.username || 'N/A'}${formatDateTime(e.punch_in_time)}${formatDateTime(e.punch_out_time)}${formatDecimal(new Date(e.punch_out_time) - new Date(e.punch_in_time))}${formatDateTime(e.archived_at)}
No archived records found.
+
+
+
`; + + document.getElementById('back-to-admin-btn').addEventListener('click', () => { + showView('admin'); + renderAdminDashboard(); + }); + } function renderModal(title, formHTML, submitHandler) { modalContainer.innerHTML = ``; document.getElementById('modal-form').addEventListener('submit', submitHandler); document.querySelector('.cancel-modal-btn').addEventListener('click', () => modalContainer.innerHTML = ''); } - + function renderEditModal(id) { const entry = allTimeEntries.find(e => e.id == id); const formHTML = `
`; @@ -201,12 +250,12 @@ const formHTML = ``; renderModal('Change My Password', formHTML, handleChangePassword); } - + function renderResetPasswordModal(username) { const formHTML = ``; renderModal(`Reset Password for ${username}`, formHTML, handleResetPassword); } - + // --- Event Handlers --- async function handleAuthSubmit(e) { e.preventDefault(); const username = e.target.elements.username.value, 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, 'error'); }; @@ -215,7 +264,7 @@ 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('Delete entry?')) apiCall(`/admin/logs/${id}`, 'DELETE').then(res => res.success && renderAdminDashboard()); if (target.classList.contains('force-clock-out-btn') && confirm('Force clock out?')) apiCall('/admin/force-clock-out', 'POST', { userId: userid }).then(res => res.success && renderAdminDashboard()); @@ -224,17 +273,18 @@ 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()); } + // --- NEW: Handle deleting a time-off request --- + if (target.classList.contains('delete-request-btn')) { if (confirm(`Permanently delete this request?`)) apiCall(`/admin/time-off-requests/${id}`, 'DELETE').then(res => res.success && renderAdminDashboard()); } } - + async function handleEditSubmit(e) { e.preventDefault(); const id = e.target.elements['edit-id'].value, punch_in_time = new Date(e.target.elements['edit-in'].value).toISOString(), punch_out_time = e.target.elements['edit-out'].value ? new Date(e.target.elements['edit-out'].value).toISOString() : null; const res = await apiCall(`/admin/logs/${id}`, 'PUT', { punch_in_time, punch_out_time }); if (res.success) { modalContainer.innerHTML = ''; renderAdminDashboard(); } } const handleArchive = () => confirm('Archive all completed entries?') && apiCall('/admin/archive', 'POST').then(res => res.success && renderAdminDashboard()); async function handleCreateUser(e) { e.preventDefault(); const username = e.target.elements['new-username'].value, password = e.target.elements['new-password'].value, 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(); } } async function handleChangePassword(e) { e.preventDefault(); const currentPassword = e.target.elements['modal-current-pw'].value, newPassword = e.target.elements['modal-new-pw'].value; const res = await apiCall('/user/change-password', 'POST', { currentPassword, newPassword }); if (res.success) { showMessage(res.data.message, 'success'); modalContainer.innerHTML = ''; } } async function handleResetPassword(e) { e.preventDefault(); const username = e.target.elements['reset-username'].value, newPassword = e.target.elements['reset-new-pw'].value; const res = await apiCall('/admin/reset-password', 'POST', { username, newPassword }); if (res.success) { showMessage(res.data.message, 'success'); modalContainer.innerHTML = ''; } } async function handleAddPunch(e) { e.preventDefault(); const selected = e.target.elements['add-punch-user']; const userId = selected.value; const username = selected.options[selected.selectedIndex].dataset.username; const punchInTime = new Date(e.target.elements['add-punch-in'].value).toISOString(); const punchOutTime = new Date(e.target.elements['add-punch-out'].value).toISOString(); const res = await apiCall('/admin/add-punch', 'POST', { userId, username, punchInTime, punchOutTime }); if (res.success) { showMessage(res.data.message, 'success'); e.target.reset(); renderAdminDashboard(); } } - const handleViewArchivesBtn = renderArchiveView; - async function handleTimeOffRequest(e) { e.preventDefault(); const startDate = e.target.elements['start-date'].value, endDate = e.target.elements['end-date'].value, reason = e.target.elements['reason'].value; const res = await apiCall('/user/request-time-off', 'POST', { startDate, endDate, reason }); if (res.success) { showMessage(res.data.message, 'success'); e.target.reset(); renderEmployeeDashboard(); } } - + async function handleTimeOffRequest(e) { e.preventDefault(); const startDate = e.target.elements['start-date'].value, endDate = e.target.elements['end-date'].value, reason = e.target.elements['reason'].value; if(new Date(endDate) < new Date(startDate)) return showMessage("End date cannot be before start date.", 'error'); const res = await apiCall('/user/request-time-off', 'POST', { startDate, endDate, reason }); if (res.success) { showMessage(res.data.message, 'success'); e.target.reset(); renderEmployeeDashboard(); } } + // --- Initializer --- signOutBtn.addEventListener('click', () => handleSignOut()); updateUI(); diff --git a/server.js b/server.js index 0537251..4fc9629 100644 --- a/server.js +++ b/server.js @@ -1,4 +1,4 @@ -// --- Time Tracker Backend Server (with Time Off Requests) --- +// --- Time Tracker Backend Server (Corrected) --- require('dotenv').config(); const express = require('express'); @@ -32,8 +32,7 @@ function initializeDatabase() { db.run(`CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'employee')`); db.run(`CREATE TABLE IF NOT EXISTS time_entries (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, username TEXT, punch_in_time DATETIME NOT NULL, punch_out_time DATETIME, status TEXT NOT NULL, FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE)`); db.run(`CREATE TABLE IF NOT EXISTS archived_time_entries (id INTEGER PRIMARY KEY, user_id INTEGER NOT NULL, username TEXT, punch_in_time DATETIME NOT NULL, punch_out_time DATETIME, status TEXT NOT NULL, archived_at DATETIME NOT NULL)`); - // NEW: Table for time off requests - db.run(`CREATE TABLE IF NOT EXISTS time_off_requests (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, username TEXT, start_date TEXT NOT NULL, end_date TEXT NOT NULL, reason TEXT, status TEXT NOT NULL DEFAULT 'pending', FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE)`); + db.run(`CREATE TABLE IF NOT EXISTS time_off_requests (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, username TEXT, start_date DATE NOT NULL, end_date DATE NOT NULL, reason TEXT, status TEXT NOT NULL DEFAULT 'pending', FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE)`); db.get('SELECT * FROM users WHERE username = ?', [ADMIN_USERNAME], (err, row) => { if (!row) { @@ -42,6 +41,16 @@ function initializeDatabase() { }); } }); + + // --- NEW: Clean up past time-off requests on server start --- + const today = new Date().toISOString().split('T')[0]; + db.run(`DELETE FROM time_off_requests WHERE end_date < ?`, [today], function(err) { + if (err) { + console.error("Error cleaning up past time-off requests:", err.message); + } else if (this.changes > 0) { + console.log(`Cleaned up ${this.changes} past time-off requests.`); + } + }); }); } @@ -53,9 +62,9 @@ const requireRole = (role) => (req, res, next) => { function authenticateToken(req, res, next) { const token = req.headers['authorization']?.split(' ')[1]; - if (token == null) return res.sendStatus(401); + if (token == null) return res.status(401).json({ message: "Authentication required. No token provided." }); jwt.verify(token, JWT_SECRET, (err, user) => { - if (err) return res.sendStatus(403); + if (err) return res.status(403).json({ message: "Access denied. Invalid or expired token." }); req.user = user; next(); }); @@ -69,7 +78,7 @@ app.post('/api/login', (req, res) => { bcrypt.compare(password, user.password, (err, isMatch) => { if (!isMatch) return res.status(401).json({ message: "Invalid credentials." }); const tokenPayload = { id: user.id, username: user.username, role: user.role }; - const token = jwt.sign(tokenPayload, JWT_SECRET, { expiresIn: '30d' }); + const token = jwt.sign(tokenPayload, JWT_SECRET, { expiresIn: '8h' }); res.json({ token, user: tokenPayload }); }); }); @@ -87,7 +96,10 @@ app.post('/api/punch', authenticateToken, (req, res) => { }); app.get('/api/status', authenticateToken, (req, res) => { - db.all(`SELECT * FROM time_entries WHERE user_id = ? ORDER BY punch_in_time DESC`, [req.user.id], (err, rows) => res.json(rows)); + db.all(`SELECT * FROM time_entries WHERE user_id = ? ORDER BY punch_in_time DESC`, [req.user.id], (err, rows) => { + if(err) return res.status(500).json({ message: "Database error."}); + res.json(rows) + }); }); app.post('/api/user/change-password', authenticateToken, (req, res) => { @@ -99,6 +111,7 @@ app.post('/api/user/change-password', authenticateToken, (req, res) => { if (!isMatch) return res.status(401).json({ message: "Incorrect current password." }); bcrypt.hash(newPassword, 10, (err, hashedPassword) => { db.run('UPDATE users SET password = ? WHERE id = ?', [hashedPassword, id], (err) => { + if (err) return res.status(500).json({ message: "Failed to update password." }); res.json({ message: "Password updated successfully." }); }); }); @@ -106,28 +119,27 @@ app.post('/api/user/change-password', authenticateToken, (req, res) => { }); }); -// NEW: Employee requests time off app.post('/api/user/request-time-off', authenticateToken, (req, res) => { const { startDate, endDate, reason } = req.body; const { id, username } = req.user; - if (!startDate || !endDate) return res.status(400).json({ message: "Start and end dates are required." }); - - const sql = `INSERT INTO time_off_requests (user_id, username, start_date, end_date, reason) VALUES (?, ?, ?, ?, ?)`; - db.run(sql, [id, username, startDate, endDate, reason], function(err) { - if (err) return res.status(500).json({ message: "Failed to submit request." }); - res.status(201).json({ message: "Time off request submitted." }); - }); + db.run( + 'INSERT INTO time_off_requests (user_id, username, start_date, end_date, reason) VALUES (?, ?, ?, ?, ?)', + [id, username, startDate, endDate, reason], + function(err) { + if (err) return res.status(500).json({ message: "Failed to submit request." }); + res.status(201).json({ message: "Time off request submitted." }); + } + ); }); -// NEW: Employee views their time off requests app.get('/api/user/time-off-requests', authenticateToken, (req, res) => { - const { id } = req.user; - db.all("SELECT * FROM time_off_requests WHERE user_id = ? ORDER BY start_date DESC", [id], (err, rows) => { - if (err) return res.status(500).json({ message: "Failed to fetch requests." }); + db.all('SELECT * FROM time_off_requests WHERE user_id = ? ORDER BY start_date DESC', [req.user.id], (err, rows) => { + if (err) return res.status(500).json({ message: "Failed to retrieve requests." }); res.json(rows); }); }); + // --- Admin Routes --- app.post('/api/admin/create-user', authenticateToken, requireRole('admin'), (req, res) => { const { username, password, role } = req.body; @@ -151,7 +163,14 @@ app.delete('/api/admin/delete-user/:username', authenticateToken, requireRole('a db.run('BEGIN TRANSACTION'); db.run('DELETE FROM time_entries WHERE user_id = ?', [userToDelete.id]); db.run('DELETE FROM archived_time_entries WHERE user_id = ?', [userToDelete.id]); - db.run('DELETE FROM users WHERE id = ?', [userToDelete.id], () => db.run('COMMIT', () => res.json({ message: `User '${username}' deleted.` }))); + db.run('DELETE FROM time_off_requests WHERE user_id = ?', [userToDelete.id]); + db.run('DELETE FROM users WHERE id = ?', [userToDelete.id], (err) => { + if(err) { + db.run('ROLLBACK'); + return res.status(500).json({message: "Failed to delete user data."}); + } + db.run('COMMIT', () => res.json({ message: `User '${username}' deleted.` })) + }); }); }); }); @@ -170,7 +189,10 @@ app.post('/api/admin/reset-password', authenticateToken, requireRole('admin'), ( app.get('/api/admin/users', authenticateToken, requireRole('admin'), (req, res) => { db.all("SELECT id, username, role FROM users", [], (err, rows) => { if(err) return res.status(500).json({message: "Database error fetching users."}); - const usersWithFlags = rows.map(row => ({ ...row, isPrimary: row.username === ADMIN_USERNAME })); + const usersWithFlags = rows.map(row => ({ + ...row, + isPrimary: row.username === ADMIN_USERNAME + })); res.json(usersWithFlags); }); }); @@ -205,30 +227,8 @@ app.post('/api/admin/force-clock-out', authenticateToken, requireRole('admin'), }); }); -// NEW: Get all time off requests -app.get('/api/admin/time-off-requests', authenticateToken, requireRole('admin'), (req, res) => { - db.all("SELECT * FROM time_off_requests ORDER BY start_date DESC", [], (err, rows) => { - if (err) return res.status(500).json({ message: "Failed to fetch requests." }); - res.json(rows); - }); -}); - -// NEW: Update status of a time off request -app.post('/api/admin/update-time-off-status', authenticateToken, requireRole('admin'), (req, res) => { - const { requestId, status } = req.body; - if (!requestId || !['approved', 'denied'].includes(status)) { - return res.status(400).json({ message: "Request ID and a valid status are required." }); - } - db.run("UPDATE time_off_requests SET status = ? WHERE id = ?", [status, requestId], function(err) { - if (err) return res.status(500).json({ message: "Failed to update status." }); - if (this.changes === 0) return res.status(404).json({ message: "Request not found." }); - res.json({ message: `Request has been ${status}.` }); - }); -}); - - app.get('/api/admin/logs', authenticateToken, requireRole('admin'), (req, res) => { - db.all(`SELECT * FROM time_entries ORDER BY punch_in_time DESC`, (err, rows) => res.json(rows)); + db.all(`SELECT * FROM time_entries ORDER BY punch_in_time DESC`, [], (err, rows) => res.json(rows)); }); app.delete('/api/admin/logs/:id', authenticateToken, requireRole('admin'), (req, res) => { @@ -242,7 +242,7 @@ app.put('/api/admin/logs/:id', authenticateToken, requireRole('admin'), (req, re }); app.post('/api/admin/archive', authenticateToken, requireRole('admin'), (req, res) => { - db.all(`SELECT * FROM time_entries WHERE status = 'out'`, (err, rows) => { + db.all(`SELECT * FROM time_entries WHERE status = 'out'`, [], (err, rows) => { if (rows.length === 0) return res.json({ message: "No entries to archive." }); const archiveTime = new Date().toISOString(); db.serialize(() => { @@ -258,7 +258,35 @@ app.post('/api/admin/archive', authenticateToken, requireRole('admin'), (req, re }); app.get('/api/admin/archives', authenticateToken, requireRole('admin'), (req, res) => { - db.all(`SELECT * FROM archived_time_entries ORDER BY archived_at DESC, id DESC`, (err, rows) => res.json(rows)); + db.all(`SELECT * FROM archived_time_entries ORDER BY archived_at DESC, id DESC`, [], (err, rows) => res.json(rows)); }); -app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`)); +app.get('/api/admin/time-off-requests', authenticateToken, requireRole('admin'), (req, res) => { + db.all('SELECT * FROM time_off_requests ORDER BY start_date DESC', [], (err, rows) => { + if (err) return res.status(500).json({ message: "Failed to retrieve time off requests." }); + res.json(rows); + }); +}); + +app.post('/api/admin/update-time-off-status', authenticateToken, requireRole('admin'), (req, res) => { + const { requestId, status } = req.body; + if (!['approved', 'denied'].includes(status)) { + return res.status(400).json({ message: "Invalid status." }); + } + db.run('UPDATE time_off_requests SET status = ? WHERE id = ?', [status, requestId], function (err) { + if (err) return res.status(500).json({ message: "Failed to update request." }); + if (this.changes === 0) return res.status(404).json({ message: "Request not found." }); + res.json({ message: `Request status updated to ${status}.` }); + }); +}); + +// --- NEW: Route to delete a time-off request --- +app.delete('/api/admin/time-off-requests/:id', authenticateToken, requireRole('admin'), (req, res) => { + db.run('DELETE FROM time_off_requests WHERE id = ?', [req.params.id], function(err) { + if (err) return res.status(500).json({ message: "Failed to delete request." }); + if (this.changes === 0) return res.status(404).json({ message: "Request not found." }); + res.json({ message: 'Request deleted.' }); + }); +}); + +app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`)); \ No newline at end of file