import express from 'express' import cors from 'cors' import { Parser } from 'json2csv' import { v4 as uuidv4 } from 'uuid' import mysql from 'mysql2/promise' import dotenv from 'dotenv' // Main function to start the server async function startServer() { dotenv.config() const app = express() const port = 3000 // --- Database Connection --- const db = mysql.createPool({ host: process.env.DB_HOST, user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, port: process.env.DB_PORT, waitForConnections: true, connectionLimit: 10, queueLimit: 0, }) try { const connection = await db.getConnection() console.log('Database connected successfully!') connection.release() } catch (error) { console.error('!!! DATABASE CONNECTION FAILED !!!') console.error('Error:', error.message) process.exit(1) } app.use(cors()) app.use(express.json()) // --- API Endpoints --- // Auth Endpoint app.post('/api/auth/login', async (req, res) => { try { const { username, password } = req.body // In a real app, use a secure hashing library like bcrypt to compare passwords const [rows] = await db.execute( 'SELECT id, role FROM workers WHERE username = ? AND password_hash = ?', [username, password], ) if (rows.length > 0) { const user = rows[0] res.json({ message: 'Login successful', role: user.role, userId: user.id }) } else { res.status(401).json({ message: 'Invalid credentials' }) } } catch (error) { console.error('Login error:', error) res.status(500).json({ message: 'Database error during login.' }) } }) // Worker Clock In/Out Endpoint app.post('/api/clock', async (req, res) => { try { const { userId, eventType, qrCodeValue, latitude, longitude } = req.body const [qrRows] = await db.execute('SELECT name, is_active FROM qr_codes WHERE id = ?', [ qrCodeValue, ]) if (qrRows.length === 0 || !qrRows[0].is_active) { return res.status(400).json({ message: 'Invalid or inactive QR Code.' }) } const [lastEventRows] = await db.execute( 'SELECT event_type FROM clock_records WHERE worker_id = ? ORDER BY timestamp DESC LIMIT 1', [userId], ) if (lastEventRows.length > 0 && lastEventRows[0].event_type === eventType) { return res .status(400) .json({ message: `You are already clocked ${eventType === 'clock_in' ? 'in' : 'out'}.` }) } const timestamp = new Date() await db.execute( 'INSERT INTO clock_records (worker_id, event_type, timestamp, qr_code_id, latitude, longitude) VALUES (?, ?, ?, ?, ?, ?)', [userId, eventType, timestamp, qrCodeValue, latitude, longitude], ) res.status(201).json({ message: 'Clock event recorded successfully' }) } catch (error) { console.error('Clock event error:', error) res.status(500).json({ message: 'Database error during clock event.' }) } }) // Fetch worker details endpoint app.get('/api/workers/:id', async (req, res) => { try { const { id } = req.params const [rows] = await db.execute( 'SELECT full_name FROM workers WHERE id = ? AND role = "worker"', [id], ) if (rows.length > 0) { res.json({ full_name: rows[0].full_name }) } else { res.status(404).json({ message: 'Worker not found.' }) } } catch (error) { console.error('Get worker details error:', error) res.status(500).json({ message: 'Database error fetching worker details.' }) } }) // Worker Status Endpoint app.get('/api/worker/status/:userId', async (req, res) => { try { const { userId } = req.params const [rows] = await db.execute( 'SELECT event_type FROM clock_records WHERE worker_id = ? ORDER BY timestamp DESC LIMIT 1', [userId], ) if (rows.length > 0) { res.json({ eventType: rows[0].event_type }) } else { res.json({ eventType: 'clock_out' }) // Default to clocked out } } catch (error) { console.error('Worker status error:', error) res.status(500).json({ message: 'Database error fetching status.' }) } }) // Worker History Endpoint app.get('/api/worker/clock-history/:userId', async (req, res) => { try { const { userId } = req.params // MODIFIED: Use LEFT JOIN and COALESCE to handle manual entries const [rows] = await db.execute( `SELECT cr.id, cr.event_type, cr.timestamp, COALESCE(qc.name, 'Manual Entry') as qrCodeUsedName FROM clock_records cr LEFT JOIN qr_codes qc ON cr.qr_code_id = qc.id WHERE cr.worker_id = ? ORDER BY cr.timestamp DESC`, [userId], ) res.json(rows) } catch (error) { console.error('Worker history error:', error) res.status(500).json({ message: 'Database error fetching history.' }) } }) // Manager: GET All Workers with Search and Pagination app.get('/api/managers/workers', async (req, res) => { try { const { search = '', page = 1, limit = 20 } = req.query const offset = (parseInt(page) - 1) * parseInt(limit) const searchTerm = `%${search}%` const [workers] = await db.execute( `SELECT id, username, full_name, created_at FROM workers WHERE role = 'worker' AND (full_name LIKE ? OR username LIKE ?) ORDER BY created_at DESC LIMIT ? OFFSET ?`, [searchTerm, searchTerm, parseInt(limit), offset], ) const [[{ totalCount }]] = await db.execute( `SELECT COUNT(*) as totalCount FROM workers WHERE role = 'worker' AND (full_name LIKE ? OR username LIKE ?)`, [searchTerm, searchTerm], ) res.json({ workers, totalCount }) } catch (error) { console.error('Get workers error:', error) res.status(500).json({ message: 'Database error fetching workers.' }) } }) // Manager: POST (Add new) Worker app.post('/api/managers/workers', async (req, res) => { try { const { username, password, fullName } = req.body if (!username || !password || !fullName) { return res.status(400).json({ message: 'Username, password, and full name are required.' }) } const [result] = await db.execute( "INSERT INTO workers (username, password_hash, full_name, role) VALUES (?, ?, ?, 'worker')", [username, password, fullName], ) res.status(201).json({ id: result.insertId, username, fullName }) } catch (error) { if (error.code === 'ER_DUP_ENTRY') { return res.status(409).json({ message: 'Username already exists.' }) } console.error('Add worker error:', error) res.status(500).json({ message: 'Database error adding worker.' }) } }) // Manager: DELETE Worker app.delete('/api/managers/workers/:id', async (req, res) => { try { const { id } = req.params const [result] = await db.execute("DELETE FROM workers WHERE id = ? AND role = 'worker'", [ id, ]) if (result.affectedRows === 0) { return res.status(404).json({ message: 'Worker not found or user is not a worker.' }) } res.status(204).send() } catch (error) { console.error('Delete worker error:', error) res.status(500).json({ message: 'Database error deleting worker.' }) } }) // --- NEW --- Manager: POST (Add Manual Attendance Record) // Note: For this to work, you may need to alter your database table: // ALTER TABLE clock_records ADD COLUMN notes TEXT; app.post('/api/managers/add-record', async (req, res) => { try { const { workerId, eventType, timestamp, notes } = req.body if (!workerId || !eventType || !timestamp) { return res .status(400) .json({ message: 'Worker ID, event type, and timestamp are required.' }) } // Check last event to prevent adding a duplicate event type const [lastEventRows] = await db.execute( 'SELECT event_type FROM clock_records WHERE worker_id = ? ORDER BY timestamp DESC LIMIT 1', [workerId], ) if (lastEventRows.length > 0 && lastEventRows[0].event_type === eventType) { const status = eventType === 'clock_in' ? 'in' : 'out' return res.status(409).json({ message: `Worker is already clocked ${status}.` }) } // --- THIS IS THE FIX --- // Sanitize the timestamp from "YYYY-MM-DDTHH:mm" to "YYYY-MM-DD HH:mm" const sanitizedTimestamp = timestamp.replace('T', ' ') await db.execute( 'INSERT INTO clock_records (worker_id, event_type, timestamp, notes, qr_code_id, latitude, longitude) VALUES (?, ?, ?, ?, NULL, NULL, NULL)', [workerId, eventType, sanitizedTimestamp, notes], ) res.status(201).json({ message: 'Manual record added successfully.' }) } catch (error) { console.error('Add manual record error:', error) res.status(500).json({ message: 'Database error adding manual record.' }) } }) // Manager: GET Attendance Records app.get('/api/managers/attendance-records', async (req, res) => { try { const { workerIds, startDate, endDate, format } = req.query if (!workerIds) { return res.status(400).json({ message: 'Worker IDs are required.' }) } const idsArray = workerIds.split(',').map(Number) if (idsArray.length === 0) return res.json([]) const placeholders = idsArray.map(() => '?').join(',') // MODIFIED: Use LEFT JOIN and COALESCE to handle manual entries, and select `notes` let query = `SELECT cr.id, w.full_name, cr.event_type, cr.timestamp, COALESCE(qc.name, 'Manual Entry') as qrCodeUsedName, cr.latitude, cr.longitude, cr.notes FROM clock_records cr LEFT JOIN qr_codes qc ON cr.qr_code_id = qc.id JOIN workers w ON cr.worker_id = w.id WHERE cr.worker_id IN (${placeholders})` const params = [...idsArray] if (startDate && endDate) { const endOfDay = new Date(endDate) endOfDay.setHours(23, 59, 59, 999) query += ' AND cr.timestamp BETWEEN ? AND ?' params.push(startDate, endOfDay) } query += ' ORDER BY w.full_name, cr.timestamp DESC' const [rows] = await db.execute(query, params) if (format === 'csv') { // MODIFIED: Add 'notes' to CSV export const json2csvParser = new Parser({ fields: ['full_name', 'event_type', 'timestamp', 'qrCodeUsedName', 'notes'], }) const csv = json2csvParser.parse(rows) res.header('Content-Type', 'text/csv') res.attachment(`attendance-report-${new Date().toISOString().split('T')[0]}.csv`) return res.send(csv) } res.json(rows) } catch (error) { console.error('Attendance records error:', error) res.status(500).json({ message: 'Database error fetching attendance records.' }) } }) // Manager: GET QR Codes app.get('/api/managers/qr-codes', async (req, res) => { try { const [rows] = await db.execute( 'SELECT id, name, is_active, created_at FROM qr_codes ORDER BY created_at DESC', ) res.json(rows) } catch (error) { console.error('Get QR codes error:', error) res.status(500).json({ message: 'Database error fetching QR codes.' }) } }) // Manager: POST QR Code app.post('/api/managers/qr-codes', async (req, res) => { try { const { name } = req.body if (!name) return res.status(400).json({ message: 'QR Code name is required.' }) const newQrCode = { id: uuidv4(), name, isActive: true } await db.execute('INSERT INTO qr_codes (id, name, is_active) VALUES (?, ?, ?)', [ newQrCode.id, newQrCode.name, newQrCode.isActive, ]) res .status(201) .json({ id: newQrCode.id, name: newQrCode.name, is_active: newQrCode.isActive }) } catch (error) { console.error('Add QR code error:', error) res.status(500).json({ message: 'Database error adding QR code.' }) } }) // Manager: PUT QR Code app.put('/api/managers/qr-codes/:id', async (req, res) => { try { const { id } = req.params const { isActive } = req.body if (typeof isActive !== 'boolean') return res.status(400).json({ message: 'isActive must be a boolean.' }) const [result] = await db.execute('UPDATE qr_codes SET is_active = ? WHERE id = ?', [ isActive, id, ]) if (result.affectedRows === 0) return res.status(404).json({ message: 'QR Code not found.' }) res.json({ id, isActive }) } catch (error) { console.error('Update QR code error:', error) res.status(500).json({ message: 'Database error updating QR code.' }) } }) // Manager: DELETE QR Code app.delete('/api/managers/qr-codes/:id', async (req, res) => { try { const { id } = req.params const [result] = await db.execute('DELETE FROM qr_codes WHERE id = ?', [id]) if (result.affectedRows === 0) return res.status(404).json({ message: 'QR Code not found.' }) res.status(204).send() } catch (error) { console.error('Delete QR code error:', error) res.status(500).json({ message: 'Database error deleting QR code.' }) } }) // Manager: GET single worker's details app.get('/api/managers/worker/:id', async (req, res) => { try { const { id } = req.params const [rows] = await db.execute( "SELECT full_name FROM workers WHERE id = ? AND role = 'worker'", [id], ) if (rows.length > 0) { res.json(rows[0]) } else { res.status(404).json({ message: 'Worker not found.' }) } } catch (error) { console.error('Get single worker error:', error) res.status(500).json({ message: 'Database error fetching worker details.' }) } }) // --- Server Start --- app.listen(port, () => { console.log(`Server is running on http://localhost:${port}`) }) } startServer()