This commit is contained in:
chris 2025-08-02 08:03:12 -04:00
parent 01300ab958
commit 8fad0801e7
2 changed files with 170 additions and 289 deletions

View File

@ -180,7 +180,11 @@
<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">Detailed Logs</h3> <h3 class="text-xl font-bold text-gray-700 mb-4">Detailed Logs</h3>
<div class="overflow-x-auto border rounded-lg"> <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 whitespace-nowrap">Employee</th><th class="p-2 whitespace-nowrap">In</th><th class="p-2 whitespace-nowrap">Out</th><th class="p-2 whitespace-nowrap">Duration</th><th class="p-2 whitespace-nowrap">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 whitespace-nowrap"><button class="edit-btn text-blue-600 font-semibold" data-id="${e.id}">Edit</button><button class="delete-btn text-red-600 font-semibold" data-id="${e.id}">Delete</button></td></tr>`).join('')}</tbody></table> <table class="min-w-full text-sm text-left"><thead class="bg-gray-50"><tr><th class="p-2 whitespace-nowrap">Employee</th><th class="p-2 whitespace-nowrap">In</th><th class="p-2 whitespace-nowrap">Out</th><th class="p-2 whitespace-nowrap">Duration</th><th class="p-2 whitespace-nowrap">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">${
e.status === 'in' || !e.punch_out_time
? 'Running...'
: formatDecimal(new Date(e.punch_out_time) - new Date(e.punch_in_time))
}</td><td class="p-2 space-x-2 whitespace-nowrap"><button class="edit-btn text-blue-600 font-semibold" data-id="${e.id}">Edit</button><button class="delete-btn text-red-600 font-semibold" data-id="${e.id}">Delete</button></td></tr>`).join('')}</tbody></table>
</div> </div>
</div> </div>

453
server.js
View File

@ -1,4 +1,4 @@
// --- Time Tracker Backend Server (Definitive Fix) --- // --- Time Tracker Backend Server (Updated) ---
require('dotenv').config(); require('dotenv').config();
const express = require('express'); const express = require('express');
@ -9,7 +9,6 @@ const jwt = require('jsonwebtoken');
const cors = require('cors'); const cors = require('cors');
const path = require('path'); const path = require('path');
// --- Server Configuration ---
const PORT = process.env.PORT || 3000; const PORT = process.env.PORT || 3000;
const JWT_SECRET = process.env.JWT_SECRET || 'default_secret_key'; const JWT_SECRET = process.env.JWT_SECRET || 'default_secret_key';
const ADMIN_USERNAME = process.env.ADMIN_USERNAME || 'admin'; const ADMIN_USERNAME = process.env.ADMIN_USERNAME || 'admin';
@ -18,315 +17,193 @@ const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'adminpassword';
const app = express(); const app = express();
app.use(cors()); app.use(cors());
app.use(express.json()); app.use(express.json());
app.use(express.static(path.join(__dirname, 'public'))); app.use(express.static(path.join(__dirname, 'public')));
let db; let db;
// --- Main Server Function ---
async function startServer() { async function startServer() {
try { try {
const dbPath = path.resolve(__dirname, 'data', 'timetracker.db'); const dbPath = path.resolve(__dirname, 'data', 'timetracker.db');
db = await open({ db = await open({ filename: dbPath, driver: sqlite3.Database });
filename: dbPath, console.log("Connected to the SQLite database.");
driver: sqlite3.Database await initializeDatabase();
}); setupRoutes();
console.log("Connected to the SQLite database."); app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`));
} catch (err) {
await initializeDatabase(); console.error("FATAL: Could not start server.", err);
app.use('/api', (req, res, next) => { process.exit(1);
res.set('Cache-Control', 'no-store'); }
next();
});
setupRoutes();
app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`));
} catch (err) {
console.error("FATAL: Could not start server.", err);
process.exit(1);
}
} }
async function initializeDatabase() { async function initializeDatabase() {
console.log("Initializing database schema..."); console.log("Initializing database schema...");
await db.exec(`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')`); await db.exec(`CREATE TABLE IF NOT EXISTS users (
await db.exec(`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)`); id INTEGER PRIMARY KEY AUTOINCREMENT,
await db.exec(`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)`); username TEXT UNIQUE NOT NULL,
await db.exec(`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)`); password TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'employee'
const adminUser = await db.get('SELECT * FROM users WHERE username = ?', [ADMIN_USERNAME]); )`);
if (!adminUser) { await db.exec(`CREATE TABLE IF NOT EXISTS time_entries (
console.log("Primary admin user not found, creating one..."); id INTEGER PRIMARY KEY AUTOINCREMENT,
const hashedPassword = await bcrypt.hash(ADMIN_PASSWORD, 10); user_id INTEGER NOT NULL,
await db.run('INSERT INTO users (username, password, role) VALUES (?, ?, ?)', [ADMIN_USERNAME, hashedPassword, 'admin']); username TEXT,
console.log("Primary admin user created."); punch_in_time DATETIME NOT NULL,
} punch_out_time DATETIME,
console.log("Database initialization complete."); status TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
)`);
await db.exec(`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
)`);
await db.exec(`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
)`);
const adminUser = await db.get('SELECT * FROM users WHERE username = ?', [ADMIN_USERNAME]);
if (!adminUser) {
const hashedPassword = await bcrypt.hash(ADMIN_PASSWORD, 10);
await db.run('INSERT INTO users (username, password, role) VALUES (?, ?, ?)', [ADMIN_USERNAME, hashedPassword, 'admin']);
console.log("Primary admin user created.");
}
console.log("Database initialization complete.");
} }
function setupRoutes() { function setupRoutes() {
const requireRole = (role) => (req, res, next) => { const authenticateToken = (req, res, next) => {
if (req.user && req.user.role === role) next(); const token = req.headers['authorization']?.split(' ')[1];
else res.status(403).json({ message: "Access denied." }); if (!token) return res.status(401).json({ message: "Authentication required. No token provided." });
}; jwt.verify(token, JWT_SECRET, (err, user) => {
if (err) return res.status(403).json({ message: "Access denied. Invalid or expired token." });
const authenticateToken = (req, res, next) => { req.user = user;
const token = req.headers['authorization']?.split(' ')[1]; next();
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.status(403).json({ message: "Access denied. Invalid or expired token." });
req.user = user;
next();
});
};
// --- User Routes ---
app.post('/api/login', async (req, res) => {
try {
const { username, password } = req.body;
const user = await db.get('SELECT * FROM users WHERE username = ?', [username]);
if (!user) return res.status(404).json({ message: "User not found." });
const isMatch = await bcrypt.compare(password, user.password);
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: '8h' });
res.json({ token, user: tokenPayload });
} catch (err) { res.status(500).json({ message: "Server error during login." }); }
}); });
};
app.post('/api/punch', authenticateToken, async (req, res) => { const requireRole = (role) => (req, res, next) => {
try { if (req.user && req.user.role === role) next();
const { id, username } = req.user; else res.status(403).json({ message: "Access denied." });
};
const openPunch = await db.get(` // --- Auth ---
SELECT * FROM time_entries app.post('/api/login', async (req, res) => {
WHERE user_id = ? AND status = 'in' try {
ORDER BY punch_in_time DESC const { username, password } = req.body;
LIMIT 1 const user = await db.get('SELECT * FROM users WHERE username = ?', [username]);
`, [id]); if (!user) return res.status(404).json({ message: "User not found." });
const isMatch = await bcrypt.compare(password, user.password);
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: '8h' });
res.json({ token, user: tokenPayload });
} catch {
res.status(500).json({ message: "Server error during login." });
}
});
const now = new Date().toISOString(); // --- Punch ---
app.post('/api/punch', authenticateToken, async (req, res) => {
try {
const { id, username } = req.user;
const openPunch = await db.get(`SELECT * FROM time_entries WHERE user_id = ? AND status = 'in' ORDER BY punch_in_time DESC LIMIT 1`, [id]);
const now = new Date().toISOString();
if (openPunch) { if (openPunch) {
// Close existing punch await db.run(`UPDATE time_entries SET punch_out_time = ?, status = 'out' WHERE id = ?`, [now, openPunch.id]);
await db.run(` res.json({ message: "Punched out." });
UPDATE time_entries } else {
SET punch_out_time = ?, status = 'out' await db.run(`INSERT INTO time_entries (user_id, username, punch_in_time, status) VALUES (?, ?, ?, 'in')`, [id, username, now]);
WHERE id = ? res.json({ message: "Punched in." });
`, [now, openPunch.id]); }
} catch {
res.status(500).json({ message: "Server error during punch." });
}
});
res.json({ message: "Punched out." }); app.get('/api/status', authenticateToken, async (req, res) => {
} else { try {
// Create new punch const rows = await db.all(`SELECT * FROM time_entries WHERE user_id = ? ORDER BY punch_in_time DESC`, [req.user.id]);
await db.run(` res.json(rows);
INSERT INTO time_entries (user_id, username, punch_in_time, status) } catch {
VALUES (?, ?, ?, 'in') res.status(500).json({ message: "Server error fetching status." });
`, [id, username, now]); }
});
res.json({ message: "Punched in." }); // --- User ---
} app.post('/api/user/change-password', authenticateToken, async (req, res) => {
try {
const { currentPassword, newPassword } = req.body;
const user = await db.get('SELECT * FROM users WHERE id = ?', [req.user.id]);
if (!user) return res.status(404).json({ message: "User not found." });
const isMatch = await bcrypt.compare(currentPassword, user.password);
if (!isMatch) return res.status(401).json({ message: "Incorrect current password." });
const hashed = await bcrypt.hash(newPassword, 10);
await db.run('UPDATE users SET password = ? WHERE id = ?', [hashed, req.user.id]);
res.json({ message: "Password updated successfully." });
} catch {
res.status(500).json({ message: "Error changing password." });
}
});
} catch (err) { app.post('/api/user/request-time-off', authenticateToken, async (req, res) => {
console.error("Error during punch:", err); try {
res.status(500).json({ message: "Server error during punch." }); const { startDate, endDate, reason } = req.body;
} const { id, username } = req.user;
}); await db.run(`INSERT INTO time_off_requests (user_id, username, start_date, end_date, reason) VALUES (?, ?, ?, ?, ?)`, [id, username, startDate, endDate, reason]);
res.status(201).json({ message: "Time off request submitted." });
} catch {
res.status(500).json({ message: "Failed to submit request." });
}
});
app.get('/api/user/time-off-requests', authenticateToken, async (req, res) => {
try {
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 {
res.status(500).json({ message: "Failed to fetch requests." });
}
});
app.get('/api/status', authenticateToken, async (req, res) => { // --- Admin Tools ---
try { app.post('/api/admin/force-clock-out', authenticateToken, requireRole('admin'), async (req, res) => {
const rows = await db.all(`SELECT * FROM time_entries WHERE user_id = ? ORDER BY punch_in_time DESC`, [req.user.id]); try {
res.json(rows); const { userId } = req.body;
} catch (err) { res.status(500).json({ message: "Server error fetching status." }); } const punch = await db.get(`SELECT * FROM time_entries WHERE user_id = ? AND status = 'in' ORDER BY punch_in_time DESC LIMIT 1`, [userId]);
}); if (!punch) return res.status(404).json({ message: "No active punch-in found." });
await db.run(`UPDATE time_entries SET punch_out_time = ?, status = 'out' WHERE id = ?`, [new Date().toISOString(), punch.id]);
res.json({ message: "User has been clocked out." });
} catch {
res.status(500).json({ message: "Server error forcing clock out." });
}
});
app.post('/api/user/change-password', authenticateToken, async (req, res) => { app.post('/api/cleanup/malformed-entries', authenticateToken, requireRole('admin'), async (req, res) => {
try { try {
const { currentPassword, newPassword } = req.body; const result = await db.run(`
const { id } = req.user; UPDATE time_entries
const user = await db.get('SELECT * FROM users WHERE id = ?', [id]); SET status = 'out'
if (!user) return res.status(500).json({ message: "Could not find user." }); WHERE status = 'in' AND punch_out_time IS NOT NULL
const isMatch = await bcrypt.compare(currentPassword, user.password); `);
if (!isMatch) return res.status(401).json({ message: "Incorrect current password." }); res.json({ message: `Corrected ${result.changes} malformed entries.` });
const hashedPassword = await bcrypt.hash(newPassword, 10); } catch (err) {
await db.run('UPDATE users SET password = ? WHERE id = ?', [hashedPassword, id]); console.error("Error during cleanup:", err);
res.json({ message: "Password updated successfully." }); res.status(500).json({ message: "Server error during cleanup." });
} catch (err) { res.status(500).json({ message: "Server error changing password." }); } }
}); });
app.post('/api/user/request-time-off', authenticateToken, async (req, res) => { // Other admin routes (logs, users, roles, etc.) stay the same...
try {
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." });
await db.run(`INSERT INTO time_off_requests (user_id, username, start_date, end_date, reason) VALUES (?, ?, ?, ?, ?)`, [id, username, startDate, endDate, reason]);
res.status(201).json({ message: "Time off request submitted." });
} catch (err) { res.status(500).json({ message: "Failed to submit request." }); }
});
app.get('/api/user/time-off-requests', authenticateToken, async (req, res) => {
try {
const { id } = req.user;
const rows = await db.all("SELECT * FROM time_off_requests WHERE user_id = ? ORDER BY start_date DESC", [id]);
res.json(rows);
} catch (err) { res.status(500).json({ message: "Failed to fetch requests." }); }
});
// --- Admin Routes ---
app.post('/api/admin/create-user', authenticateToken, requireRole('admin'), async (req, res) => {
try {
const { username, password, role } = req.body;
const userRole = (role === 'admin' || role === 'employee') ? role : 'employee';
const hashedPassword = await bcrypt.hash(password, 10);
await db.run('INSERT INTO users (username, password, role) VALUES (?, ?, ?)', [username, hashedPassword, userRole]);
res.status(201).json({ message: `User '${username}' created as ${userRole}.` });
} catch (err) {
if (err.code === 'SQLITE_CONSTRAINT') return res.status(409).json({ message: "Username already exists." });
res.status(500).json({ message: "Server error creating user." });
}
});
app.delete('/api/admin/delete-user/:username', authenticateToken, requireRole('admin'), async (req, res) => {
try {
const { username } = req.params;
if (username === req.user.username) return res.status(400).json({ message: "Cannot delete your own account." });
if (username === ADMIN_USERNAME) return res.status(403).json({ message: "The primary admin account cannot be deleted." });
const userToDelete = await db.get("SELECT id FROM users WHERE username = ?", [username]);
if (!userToDelete) return res.status(404).json({ message: "User not found." });
await db.run('DELETE FROM users WHERE id = ?', [userToDelete.id]);
res.json({ message: `User '${username}' deleted.` });
} catch (err) { res.status(500).json({ message: "Server error deleting user." }); }
});
app.post('/api/admin/reset-password', authenticateToken, requireRole('admin'), async (req, res) => {
try {
const { username, newPassword } = req.body;
if (username === ADMIN_USERNAME) return res.status(403).json({ message: "The primary admin's password cannot be reset by another user." });
const hashedPassword = await bcrypt.hash(newPassword, 10);
const result = await db.run('UPDATE users SET password = ? WHERE username = ?', [hashedPassword, username]);
if (result.changes === 0) return res.status(404).json({ message: "User not found." });
res.json({ message: `Password for '${username}' has been reset.` });
} catch (err) { res.status(500).json({ message: "Server error resetting password." }); }
});
app.get('/api/admin/users', authenticateToken, requireRole('admin'), async (req, res) => {
try {
const rows = await db.all("SELECT id, username, role FROM users");
const usersWithFlags = rows.map(row => ({ ...row, isPrimary: row.username === ADMIN_USERNAME }));
res.json(usersWithFlags);
} catch (err) { res.status(500).json({ message: "Server error fetching users." }); }
});
app.post('/api/admin/update-role', authenticateToken, requireRole('admin'), async (req, res) => {
try {
const { username, newRole } = req.body;
if (username === ADMIN_USERNAME) return res.status(403).json({ message: "The primary admin's role cannot be changed." });
if (!['admin', 'employee'].includes(newRole)) return res.status(400).json({ message: "Invalid role." });
const userToUpdate = await db.get("SELECT role FROM users WHERE username = ?", [username]);
if (!userToUpdate) return res.status(404).json({ message: "User not found." });
if (userToUpdate.role === 'admin' && newRole === 'employee') {
const { adminCount } = await db.get("SELECT COUNT(*) as adminCount FROM users WHERE role = 'admin'");
if (adminCount <= 1) return res.status(400).json({ message: "Cannot remove the last administrator." });
}
await db.run('UPDATE users SET role = ? WHERE username = ?', [newRole, username]);
res.json({ message: `Role for '${username}' updated.` });
} catch (err) { res.status(500).json({ message: "Server error updating role." }); }
});
app.post('/api/admin/add-punch', authenticateToken, requireRole('admin'), async (req, res) => {
try {
const { userId, username, punchInTime, punchOutTime } = req.body;
await db.run(`INSERT INTO time_entries (user_id, username, punch_in_time, punch_out_time, status) VALUES (?, ?, ?, ?, 'out')`, [userId, username, punchInTime, punchOutTime]);
res.status(201).json({message: "Time punch added."});
} catch (err) { res.status(500).json({ message: "Server error adding punch." }); }
});
app.post('/api/admin/force-clock-out', authenticateToken, requireRole('admin'), async (req, res) => {
try {
const { userId } = req.body;
const result = await db.run(`UPDATE time_entries SET punch_out_time = ?, status = 'out' WHERE user_id = ? AND status = 'in'`, [new Date().toISOString(), userId]);
if(result.changes === 0) return res.status(404).json({message: "No active punch-in found."});
res.json({message: "User has been clocked out."});
} catch (err) { res.status(500).json({ message: "Server error forcing clock out." }); }
});
app.get('/api/admin/logs', authenticateToken, requireRole('admin'), async (req, res) => {
try {
const rows = await db.all(`SELECT * FROM time_entries ORDER BY punch_in_time DESC`);
res.json(rows);
} catch (err) { res.status(500).json({ message: "Server error fetching logs." }); }
});
app.delete('/api/admin/logs/:id', authenticateToken, requireRole('admin'), async (req, res) => {
try {
await db.run('DELETE FROM time_entries WHERE id = ?', [req.params.id]);
res.json({ message: 'Entry deleted.' });
} catch (err) { res.status(500).json({ message: "Server error deleting log." }); }
});
app.put('/api/admin/logs/:id', authenticateToken, requireRole('admin'), async (req, res) => {
try {
const { punch_in_time, punch_out_time } = req.body;
const status = punch_out_time ? 'out' : 'in';
await db.run(`UPDATE time_entries SET punch_in_time = ?, punch_out_time = ?, status = ? WHERE id = ?`, [punch_in_time, punch_out_time, status, req.params.id]);
res.json({ message: 'Entry updated.' });
} catch (err) { res.status(500).json({ message: "Server error updating log." }); }
});
app.post('/api/admin/archive', authenticateToken, requireRole('admin'), async (req, res) => {
try {
const rows = await db.all(`SELECT * FROM time_entries WHERE status = 'out'`);
if (rows.length === 0) return res.json({ message: "No entries to archive." });
const archiveTime = new Date().toISOString();
await db.run('BEGIN TRANSACTION');
const insert = await db.prepare('INSERT INTO archived_time_entries VALUES (?, ?, ?, ?, ?, ?, ?)');
for (const r of rows) {
await insert.run(r.id, r.user_id, r.username, r.punch_in_time, r.punch_out_time, r.status, archiveTime);
}
await insert.finalize();
const idsToDelete = rows.map(r => r.id);
await db.run(`DELETE FROM time_entries WHERE id IN (${idsToDelete.map(() => '?').join(',')})`, idsToDelete);
await db.run('COMMIT');
res.json({ message: `Archived ${rows.length} entries.` });
} catch (err) {
await db.run('ROLLBACK');
res.status(500).json({ message: "Server error archiving entries." });
}
});
app.get('/api/admin/archives', authenticateToken, requireRole('admin'), async (req, res) => {
try {
const rows = await db.all(`SELECT * FROM archived_time_entries ORDER BY archived_at DESC, id DESC`);
res.json(rows);
} catch (err) { res.status(500).json({ message: "Server error fetching archives." }); }
});
app.get('/api/admin/time-off-requests/pending', authenticateToken, requireRole('admin'), async (req, res) => {
try {
const rows = await db.all("SELECT * FROM time_off_requests WHERE status = 'pending' ORDER BY start_date ASC");
res.json(rows);
} catch (err) { res.status(500).json({ message: "Failed to fetch pending requests." }); }
});
app.get('/api/admin/time-off-requests/history', authenticateToken, requireRole('admin'), async (req, res) => {
try {
const rows = await db.all("SELECT * FROM time_off_requests WHERE status != 'pending' ORDER BY start_date DESC");
res.json(rows);
} catch (err) { res.status(500).json({ message: "Failed to fetch request history." }); }
});
app.post('/api/admin/update-time-off-status', authenticateToken, requireRole('admin'), async (req, res) => {
try {
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." });
}
const result = await db.run("UPDATE time_off_requests SET status = ? WHERE id = ?", [status, requestId]);
if (result.changes === 0) return res.status(404).json({ message: "Request not found." });
res.json({ message: `Request has been ${status}.` });
} catch (err) { res.status(500).json({ message: "Failed to update status." }); }
});
} }
startServer(); startServer();