fix view archives and add delete time off request

This commit is contained in:
chris 2025-07-29 17:00:15 -04:00
parent 53b9087078
commit 64da46717a
5 changed files with 162 additions and 74 deletions

3
.env.example Normal file
View File

@ -0,0 +1,3 @@
ADMIN_USERNAME="admin"
ADMIN_PASSWORD="adminpassword"
JWT_SECRET="" ##random number string

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
# gitignore
node_modules
package-lock.json
.env
.git

View File

@ -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://<your-server-ip>: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

View File

@ -71,10 +71,9 @@
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) : {};
@ -123,6 +122,9 @@
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 = `
<div class="max-w-4xl mx-auto space-y-8">
<div class="bg-white rounded-xl shadow-md p-6">
@ -138,8 +140,8 @@
<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>
<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">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">Start Date</label><input type="date" id="start-date" class="w-full p-2 border rounded" required min="${today}"></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 min="${today}"></div>
<button type="submit" class="w-full bg-indigo-600 text-white p-2 rounded">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>
</form>
@ -171,7 +173,7 @@
<div class="max-w-6xl mx-auto space-y-8">
<div class="bg-white rounded-xl shadow-md p-6"><div class="flex justify-between items-center mb-4"><h2 class="text-2xl font-bold">Admin Dashboard</h2><div class="space-x-2"><button id="view-archives-btn" class="px-4 py-2 bg-gray-500 text-white rounded-lg">View Archives</button><button id="archive-btn" class="px-4 py-2 bg-amber-500 text-white rounded-lg">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-2">Currently Punched In</h3><ul id="punched-in-list" class="border rounded-lg divide-y">${punchedInEntries.map(e => `<li class="flex justify-between items-center p-3"><span class="font-medium">${e.username}</span><div class="flex items-center space-x-4"><span class="text-sm text-gray-600">Since: ${formatDateTime(e.punch_in_time)}</span><button class="force-clock-out-btn px-2 py-1 text-xs bg-red-500 text-white rounded" data-userid="${e.user_id}">Clock Out</button></div></li>`).join('') || '<li class="p-4 text-center">None</li>'}</ul></div>
<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="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">Employee</th><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-table">${allRequests.map(r => `<tr class="border-t"><td class="p-2">${r.username}</td><td class="p-2">${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><td class="p-2 space-x-1">${r.status === 'pending' ? `<button class="approve-request-btn text-green-600" data-id="${r.id}">Approve</button><button class="deny-request-btn text-red-600" data-id="${r.id}">Deny</button>` : ''}</td></tr>`).join('') || '<tr><td colspan="5" class="text-center p-4">No requests.</td></tr>'}</tbody></table></div></div>
<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="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">Employee</th><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-table">${allRequests.map(r => `<tr class="border-t"><td class="p-2">${r.username}</td><td class="p-2">${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><td class="p-2 space-x-2">${r.status === 'pending' ? `<button class="approve-request-btn text-green-600" data-id="${r.id}">Approve</button><button class="deny-request-btn text-red-600" data-id="${r.id}">Deny</button>` : ''}<button class="delete-request-btn text-gray-500" data-id="${r.id}">Delete</button></td></tr>`).join('') || '<tr><td colspan="5" class="text-center p-4">No requests.</td></tr>'}</tbody></table></div></div>
<div class="bg-white rounded-xl shadow-md p-6"><h3 class="text-xl font-bold text-gray-700 mb-2">Hours by Employee</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">Employee</th><th class="p-2">Total Hours</th></tr></thead><tbody>${Object.keys(employeeTotals).map(u => `<tr class="border-t"><td class="p-2 font-medium">${u}</td><td class="p-2">${formatDecimal(employeeTotals[u])}</td></tr>`).join('')}</tbody></table></div></div>
<div class="bg-white rounded-xl shadow-md p-6"><h3 class="text-xl font-bold text-gray-700 mb-4">Detailed Logs</h3><div class="overflow-x-auto"><table class="min-w-full text-sm text-left"><thead class="bg-gray-50"><tr><th class="p-2">Employee</th><th class="p-2">In</th><th class="p-2">Out</th><th class="p-2">Duration</th><th class="p-2">Actions</th></tr></thead><tbody id="admin-table">${allTimeEntries.map(e => `<tr class="border-t"><td class="p-2">${e.username||'N/A'}</td><td class="p-2">${formatDateTime(e.punch_in_time)}</td><td class="p-2">${formatDateTime(e.punch_out_time)}</td><td class="p-2">${formatDecimal(new Date(e.punch_out_time) - new Date(e.punch_in_time))}</td><td class="p-2 space-x-2"><button class="edit-btn text-blue-600" data-id="${e.id}">Edit</button><button class="delete-btn text-red-600" data-id="${e.id}">Delete</button></td></tr>`).join('')}</tbody></table></div></div>
<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">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 Punch</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><input type="datetime-local" id="add-punch-in" class="w-full p-2 border rounded" required><input type="datetime-local" id="add-punch-out" class="w-full p-2 border rounded" required><button type="submit" class="w-full bg-purple-600 text-white p-2 rounded">Add Punch</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 id="manage-users-table">${allUsers.map(u => `<tr class="border-t"><td class="p-2 font-medium">${u.username}</td><td class="p-2">${u.role}</td><td class="p-2 space-x-2">${u.isPrimary ? `<span class="text-sm text-gray-500">Primary Admin</span>` : `<button class="reset-pw-btn text-blue-600" data-username="${u.username}">Reset PW</button><button class="change-role-btn text-purple-600" data-username="${u.username}" data-role="${u.role}">${u.role === 'admin' ? 'Demote' : 'Promote'}</button>${u.username !== user.username ? `<button class="delete-user-btn text-red-600" data-username="${u.username}">Delete</button>` : ''}`}</td></tr>`).join('')}</tbody></table></div></div></div>
@ -183,7 +185,54 @@
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 = `<div class="text-center p-4">Could not load archives.</div>`;
return;
}
const archives = res.data;
mainViews.archive.innerHTML = `
<div class="max-w-6xl mx-auto space-y-6">
<div class="bg-white rounded-xl shadow-md p-6">
<div class="flex justify-between items-center mb-4">
<h2 class="text-2xl font-bold">Archived Records</h2>
<button id="back-to-admin-btn" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">← Back to Dashboard</button>
</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">Employee</th>
<th class="p-2">Punch In</th>
<th class="p-2">Punch Out</th>
<th class="p-2">Duration (Hours)</th>
<th class="p-2">Archived At</th>
</tr>
</thead>
<tbody>
${archives.map(e => `
<tr class="border-t">
<td class="p-2">${e.username || 'N/A'}</td>
<td class="p-2">${formatDateTime(e.punch_in_time)}</td>
<td class="p-2">${formatDateTime(e.punch_out_time)}</td>
<td class="p-2">${formatDecimal(new Date(e.punch_out_time) - new Date(e.punch_in_time))}</td>
<td class="p-2">${formatDateTime(e.archived_at)}</td>
</tr>
`).join('') || '<tr><td colspan="5" class="text-center p-4">No archived records found.</td></tr>'}
</tbody>
</table>
</div>
</div>
</div>`;
document.getElementById('back-to-admin-btn').addEventListener('click', () => {
showView('admin');
renderAdminDashboard();
});
}
function renderModal(title, formHTML, submitHandler) {
modalContainer.innerHTML = `<div class="modal-overlay"><div class="modal-content"><h3 class="text-xl font-bold mb-4">${title}</h3><form id="modal-form" class="space-y-4">${formHTML}<div class="flex justify-end space-x-2"><button type="button" class="cancel-modal-btn px-4 py-2 bg-gray-200 rounded">Cancel</button><button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded">Save</button></div></form></div></div>`;
@ -224,6 +273,8 @@
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(); } }
@ -232,8 +283,7 @@
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());

120
server.js
View File

@ -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.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}`));