import express from 'express'; import bcrypt from 'bcryptjs'; import { authenticateToken } from '../middleware/auth.js'; import db from '../database/db.js'; import { v4 as uuidv4 } from 'uuid'; const router = express.Router(); // Get all employees with current status (PUBLIC - no auth required) router.get('/status', async (req, res) => { try { const now = new Date(); const today = now.toISOString().split('T')[0]; const currentTime = now.toTimeString().split(' ')[0].substring(0, 5); // HH:mm format // Get all active employees with their current status const employees = await db.allAsync(` SELECT u.id, u.name, u.email, e.job_title, s.start_time as scheduled_start, s.end_time as scheduled_end FROM users u LEFT JOIN employees e ON u.id = e.user_id LEFT JOIN shifts s ON u.id = s.employee_id AND s.date = ? WHERE e.status = 'Active' AND u.role != 'admin' ORDER BY u.name `, today); // Get current timecard status for each employee const statuses = await Promise.all(employees.map(async (emp) => { // Get ALL timecards for today (to show all clocked-in periods) const timecards = await db.allAsync(` SELECT clock_in, clock_out, break_start, break_end, status FROM timecards WHERE employee_id = ? AND date = ? ORDER BY created_at ASC `, emp.id, today); // Get the most recent timecard for current status const timecard = timecards.length > 0 ? timecards[timecards.length - 1] : null; let currentStatus = 'clocked_out'; let clockInTime = null; let clockOutTime = null; let breakStartTime = null; let isLate = false; let leftEarly = false; if (timecard) { clockInTime = timecard.clock_in; clockOutTime = timecard.clock_out; // Status is only "clocked_in" or "on_break" if they clocked in but haven't clocked out yet if (timecard.clock_in && !timecard.clock_out) { if (timecard.break_start && !timecard.break_end) { currentStatus = 'on_break'; breakStartTime = timecard.break_start; } else { currentStatus = 'clocked_in'; } } else { // If they've clocked out, status is clocked_out (even if they clocked in earlier) currentStatus = 'clocked_out'; } // Check if late (clocked in after scheduled start) if (emp.scheduled_start && clockInTime) { const scheduledTime = emp.scheduled_start; const clockInTimeOnly = clockInTime.includes(' ') ? clockInTime.split(' ')[1]?.substring(0, 5) : clockInTime.substring(0, 5); if (clockInTimeOnly && clockInTimeOnly > scheduledTime) { isLate = true; } } // Check if left early (clocked out before scheduled end) if (emp.scheduled_end && timecard.clock_out) { const scheduledEndTime = emp.scheduled_end; const clockOutTimeOnly = timecard.clock_out.includes(' ') ? timecard.clock_out.split(' ')[1]?.substring(0, 5) : timecard.clock_out.substring(0, 5); if (clockOutTimeOnly && clockOutTimeOnly < scheduledEndTime) { leftEarly = true; } } } // Format times to HH:mm in LOCAL timezone // Helper function to extract HH:mm from datetime, converting to local time const extractTime = (datetime) => { if (!datetime) return null; try { // Parse the datetime string (could be ISO format with Z or SQLite datetime) // new Date() automatically converts UTC to local timezone const date = new Date(datetime); // Check if date is valid if (isNaN(date.getTime())) { console.error('Invalid date:', datetime); return null; } // Get local time hours and minutes (getHours/getMinutes return local time) const hours = date.getHours(); const minutes = date.getMinutes(); // Format as HH:mm (24-hour format) return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`; } catch (error) { console.error('Error parsing datetime:', datetime, error); return null; } }; const formattedClockIn = extractTime(clockInTime); const formattedClockOut = extractTime(clockOutTime); const formattedBreakStart = extractTime(breakStartTime); // Extract all clocked-in periods from all timecards const clockedInPeriods = timecards .filter(tc => tc.clock_in) // Only timecards with clock_in .map(tc => ({ clockIn: extractTime(tc.clock_in), clockOut: extractTime(tc.clock_out), })) .filter(period => period.clockIn); // Only include periods with valid clock-in time return { id: emp.id, name: emp.name, email: emp.email, currentStatus, clockInTime: formattedClockIn, clockOutTime: formattedClockOut, breakStartTime: formattedBreakStart, scheduledStart: emp.scheduled_start, scheduledEnd: emp.scheduled_end, isLate, leftEarly, clockedInPeriods, // Include all periods for visualization }; })); res.json(statuses); } catch (error) { console.error('Error fetching employee status:', error); res.status(500).json({ error: 'Internal server error' }); } }); // Clock in/out action (PUBLIC - password only) router.post('/action', async (req, res) => { try { const { employeeId, password, action } = req.body; if (!employeeId || !password || !action) { return res.status(400).json({ error: 'Employee ID, password, and action are required' }); } if (!['clock_in', 'clock_out', 'break_start', 'break_end'].includes(action)) { return res.status(400).json({ error: 'Invalid action' }); } // Verify password const user = await db.getAsync('SELECT * FROM users WHERE id = ?', employeeId); if (!user) { return res.status(404).json({ error: 'Employee not found' }); } const isValid = await bcrypt.compare(password, user.password_hash); if (!isValid) { return res.status(401).json({ error: 'Invalid password' }); } const now = new Date(); const today = now.toISOString().split('T')[0]; const currentDateTime = now.toISOString(); // Get or create today's timecard let timecard = await db.getAsync(` SELECT * FROM timecards WHERE employee_id = ? AND date = ? ORDER BY created_at DESC LIMIT 1 `, employeeId, today); if (!timecard) { // Create new timecard const timecardId = uuidv4(); await db.runAsync(` INSERT INTO timecards (id, employee_id, date, status) VALUES (?, ?, ?, 'pending') `, timecardId, employeeId, today); timecard = { id: timecardId, employee_id: employeeId, date: today }; } // Get scheduled shift for today const shift = await db.getAsync(` SELECT start_time, end_time FROM shifts WHERE employee_id = ? AND date = ? LIMIT 1 `, employeeId, today); // Perform action if (action === 'clock_in') { // Allow clock in if they haven't clocked in today, OR if they've already clocked out if (timecard.clock_in && !timecard.clock_out) { return res.status(400).json({ error: 'Already clocked in. Please clock out first.' }); } // If they clocked out earlier today, create a new timecard entry for a new shift if (timecard.clock_out) { const newTimecardId = uuidv4(); await db.runAsync(` INSERT INTO timecards (id, employee_id, date, status) VALUES (?, ?, ?, 'pending') `, newTimecardId, employeeId, today); timecard = { id: newTimecardId, employee_id: employeeId, date: today, clock_in: null, clock_out: null }; } await db.runAsync(` UPDATE timecards SET clock_in = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? `, currentDateTime, timecard.id); // Check if late if (shift && shift.start_time) { const scheduledTime = new Date(`${today}T${shift.start_time}`); if (now > scheduledTime) { // Mark as late in notes await db.runAsync(` UPDATE timecards SET notes = 'Late arrival', updated_at = CURRENT_TIMESTAMP WHERE id = ? `, timecard.id); } } } else if (action === 'clock_out') { if (!timecard.clock_in) { return res.status(400).json({ error: 'Must clock in first' }); } if (timecard.clock_out) { return res.status(400).json({ error: 'Already clocked out' }); } // Calculate hours const clockIn = new Date(timecard.clock_in); const clockOut = now; const diffMs = clockOut - clockIn; const diffHours = diffMs / (1000 * 60 * 60); let breakTime = 0; if (timecard.break_start && timecard.break_end) { const breakStart = new Date(timecard.break_start); const breakEnd = new Date(timecard.break_end); breakTime = (breakEnd - breakStart) / (1000 * 60 * 60); } const totalHours = diffHours - breakTime; await db.runAsync(` UPDATE timecards SET clock_out = ?, hours = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? `, currentDateTime, totalHours, timecard.id); // Check if left early if (shift && shift.end_time) { const scheduledEnd = new Date(`${today}T${shift.end_time}`); if (now < scheduledEnd) { const currentNotes = timecard.notes || ''; await db.runAsync(` UPDATE timecards SET notes = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? `, currentNotes ? `${currentNotes}; Left early` : 'Left early', timecard.id); } } } else if (action === 'break_start') { if (!timecard.clock_in) { return res.status(400).json({ error: 'Must clock in first' }); } if (timecard.break_start && !timecard.break_end) { return res.status(400).json({ error: 'Already on break' }); } await db.runAsync(` UPDATE timecards SET break_start = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? `, currentDateTime, timecard.id); } else if (action === 'break_end') { if (!timecard.break_start) { return res.status(400).json({ error: 'Not on break' }); } if (timecard.break_end) { return res.status(400).json({ error: 'Break already ended' }); } await db.runAsync(` UPDATE timecards SET break_end = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? `, currentDateTime, timecard.id); } res.json({ success: true, message: `Successfully ${action.replace('_', ' ')}`, timecardId: timecard.id }); } catch (error) { console.error('Error processing clock action:', error); res.status(500).json({ error: 'Internal server error' }); } }); // Authenticated clock action (uses JWT instead of password) router.post('/action/authenticated', authenticateToken, async (req, res) => { try { const { action } = req.body; const employeeId = req.user.id; // Use authenticated user's ID if (!action) { return res.status(400).json({ error: 'Action is required' }); } if (!['clock_in', 'clock_out', 'break_start', 'break_end'].includes(action)) { return res.status(400).json({ error: 'Invalid action' }); } // Verify user is an employee (not admin) const user = await db.getAsync(` SELECT u.*, e.status as employee_status FROM users u LEFT JOIN employees e ON u.id = e.user_id WHERE u.id = ? `, employeeId); if (!user) { return res.status(404).json({ error: 'User not found' }); } if (!user) { return res.status(404).json({ error: 'User not found' }); } if (user.role === 'admin') { return res.status(403).json({ error: 'Admin users cannot clock in/out' }); } const now = new Date(); const today = now.toISOString().split('T')[0]; const currentDateTime = now.toISOString(); // Get or create today's timecard // For clock_out and break actions, we need the most recent timecard that's not completed let timecard = await db.getAsync(` SELECT * FROM timecards WHERE employee_id = ? AND date = ? ORDER BY created_at DESC LIMIT 1 `, employeeId, today); // For clock_in, create new timecard if needed or if previous one is completed if (action === 'clock_in') { if (!timecard || timecard.clock_out) { // Create new timecard for new shift const timecardId = uuidv4(); await db.runAsync(` INSERT INTO timecards (id, employee_id, date, status) VALUES (?, ?, ?, 'pending') `, timecardId, employeeId, today); timecard = { id: timecardId, employee_id: employeeId, date: today, clock_in: null, clock_out: null, break_start: null, break_end: null }; } else if (timecard.clock_in && !timecard.clock_out) { return res.status(400).json({ error: 'Already clocked in. Please clock out first.' }); } } else { // For other actions, timecard must exist if (!timecard) { return res.status(400).json({ error: 'No timecard found. Please clock in first.' }); } } // Get scheduled shift for today const shift = await db.getAsync(` SELECT start_time, end_time FROM shifts WHERE employee_id = ? AND date = ? LIMIT 1 `, employeeId, today); // Perform action if (action === 'clock_in') { await db.runAsync(` UPDATE timecards SET clock_in = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? `, currentDateTime, timecard.id); // Check if late if (shift && shift.start_time) { const scheduledTime = new Date(`${today}T${shift.start_time}`); if (now > scheduledTime) { await db.runAsync(` UPDATE timecards SET notes = 'Late arrival', updated_at = CURRENT_TIMESTAMP WHERE id = ? `, timecard.id); } } } else if (action === 'clock_out') { if (!timecard.clock_in) { return res.status(400).json({ error: 'Must clock in first' }); } if (timecard.clock_out) { return res.status(400).json({ error: 'Already clocked out' }); } // Calculate hours const clockIn = new Date(timecard.clock_in); const clockOut = now; const diffMs = clockOut - clockIn; const diffHours = diffMs / (1000 * 60 * 60); let breakTime = 0; if (timecard.break_start && timecard.break_end) { const breakStart = new Date(timecard.break_start); const breakEnd = new Date(timecard.break_end); breakTime = (breakEnd - breakStart) / (1000 * 60 * 60); } const totalHours = diffHours - breakTime; await db.runAsync(` UPDATE timecards SET clock_out = ?, hours = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? `, currentDateTime, totalHours, timecard.id); // Check if left early if (shift && shift.end_time) { const scheduledEnd = new Date(`${today}T${shift.end_time}`); if (clockOut < scheduledEnd) { const currentNotes = timecard.notes || ''; await db.runAsync(` UPDATE timecards SET notes = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? `, currentNotes ? `${currentNotes}; Left early` : 'Left early', timecard.id); } } } else if (action === 'break_start') { if (!timecard.clock_in) { return res.status(400).json({ error: 'Must clock in first' }); } if (timecard.clock_out) { return res.status(400).json({ error: 'Already clocked out' }); } if (timecard.break_start && !timecard.break_end) { return res.status(400).json({ error: 'Already on break' }); } await db.runAsync(` UPDATE timecards SET break_start = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? `, currentDateTime, timecard.id); } else if (action === 'break_end') { if (!timecard.clock_in) { return res.status(400).json({ error: 'Must clock in first' }); } if (timecard.clock_out) { return res.status(400).json({ error: 'Already clocked out' }); } if (!timecard.break_start) { return res.status(400).json({ error: 'Not on break' }); } if (timecard.break_end) { return res.status(400).json({ error: 'Break already ended' }); } await db.runAsync(` UPDATE timecards SET break_end = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? `, currentDateTime, timecard.id); } res.json({ message: 'Action completed successfully', action, timestamp: currentDateTime }); } catch (error) { console.error('Clock action error:', error); res.status(500).json({ error: 'Internal server error' }); } }); export default router;