diff --git a/.gitignore b/.gitignore index 8ee54e8..e2ff986 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ pnpm-debug.log* lerna-debug.log* node_modules +.env .DS_Store dist dist-ssr diff --git a/backend/server.js b/backend/server.js index e5f4c38..a883e60 100644 --- a/backend/server.js +++ b/backend/server.js @@ -2,269 +2,314 @@ 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' -const app = express() -const port = 3000 +// Main function to start the server +async function startServer() { + dotenv.config() -app.use(cors()) -app.use(express.json()) + const app = express() + const port = 3000 -// --- In-memory database for MVP --- -const users = [ - { id: 1, username: 'worker', password: 'password', role: 'worker', fullName: 'John Doe' }, - { id: 2, username: 'worker2', password: 'password', role: 'worker', fullName: 'Jane Smith' }, - { id: 3, username: 'manager', password: 'password', role: 'manager', fullName: 'Manager Bob' }, -] - -let qrCodes = [ - { - id: 'FACTORY-MAIN-ENTRANCE', - name: 'Factory Main Entrance', - isActive: true, - createdAt: new Date().toISOString(), - }, - { - id: 'WAREHOUSE-SECTION-A', - name: 'Warehouse Section A', - isActive: true, - createdAt: new Date().toISOString(), - }, - { - id: 'ASSEMBLY-LINE-1', - name: 'Assembly Line 1', - isActive: false, - createdAt: new Date().toISOString(), - }, -] - -let clockEvents = [ - // Sample data for testing reports - { - id: 1, - userId: 1, - eventType: 'clock_in', - timestamp: '2025-06-10T09:00:00.000Z', - qrCodeUsedId: 'FACTORY-MAIN-ENTRANCE', - qrCodeUsedName: 'Factory Main Entrance', - }, - { - id: 2, - userId: 1, - eventType: 'clock_out', - timestamp: '2025-06-10T17:30:00.000Z', - qrCodeUsedId: 'FACTORY-MAIN-ENTRANCE', - qrCodeUsedName: 'Factory Main Entrance', - }, - { - id: 3, - userId: 2, - eventType: 'clock_in', - timestamp: '2025-06-10T09:05:00.000Z', - qrCodeUsedId: 'WAREHOUSE-SECTION-A', - qrCodeUsedName: 'Warehouse Section A', - }, - { - id: 4, - userId: 2, - eventType: 'clock_out', - timestamp: '2025-06-10T17:35:00.000Z', - qrCodeUsedId: 'WAREHOUSE-SECTION-A', - qrCodeUsedName: 'Warehouse Section A', - }, - { - id: 5, - userId: 1, - eventType: 'clock_in', - timestamp: '2025-06-11T08:58:00.000Z', - qrCodeUsedId: 'FACTORY-MAIN-ENTRANCE', - qrCodeUsedName: 'Factory Main Entrance', - }, // Missing clock out -] - -let eventId = clockEvents.length + 1 - -// --- Helper Functions --- -const calculateHours = (events) => { - const userHours = {} - const pairedEvents = {} - - // Group events by user - events.forEach((event) => { - if (!pairedEvents[event.userId]) { - pairedEvents[event.userId] = [] - } - pairedEvents[event.userId].push(event) + // --- 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 || 3306, + waitForConnections: true, + connectionLimit: 10, + queueLimit: 0, }) - for (const userId in pairedEvents) { - const userEvents = pairedEvents[userId].sort( - (a, b) => new Date(a.timestamp) - new Date(b.timestamp), - ) - let totalHours = 0 - let clockInTime = null - - userEvents.forEach((event) => { - if (event.eventType === 'clock_in' && !clockInTime) { - clockInTime = new Date(event.timestamp) - } else if (event.eventType === 'clock_out' && clockInTime) { - const clockOutTime = new Date(event.timestamp) - const diffMs = clockOutTime - clockInTime - totalHours += diffMs / (1000 * 60 * 60) - clockInTime = null // Reset for next pair - } - }) - - const worker = users.find((u) => u.id === parseInt(userId)) - userHours[userId] = { - userId, - fullName: worker ? worker.fullName : `User ${userId}`, - totalHours: parseFloat(totalHours.toFixed(2)), - hasIncomplete: clockInTime !== null, // Mark if there's a pending clock-in - } + 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) } - return Object.values(userHours) + + app.use(cors()) + app.use(express.json()) + + // Helper functions can be placed here if any are needed in the future + + // --- 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.' }) + } + }) + + // 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' }) + } + } 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 + const [rows] = await db.execute( + `SELECT cr.id, cr.event_type, cr.timestamp, qc.name as qrCodeUsedName, cr.latitude, cr.longitude FROM clock_records cr 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.' }) + } + }) + + // 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(',') + let query = `SELECT cr.id, w.full_name, cr.event_type, cr.timestamp, qc.name as qrCodeUsedName, cr.latitude, cr.longitude FROM clock_records cr 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') { + const json2csvParser = new Parser({ + fields: ['full_name', 'event_type', 'timestamp', 'qrCodeUsedName'], + }) + 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}`) + }) } -// --- API Endpoints --- - -// Auth Endpoint -app.post('/api/auth/login', (req, res) => { - const { username, password } = req.body - const user = users.find((u) => u.username === username && u.password === password) - if (user) { - res.json({ message: 'Login successful', role: user.role, userId: user.id }) - } else { - res.status(401).json({ message: 'Invalid credentials' }) - } -}) - -// Worker Clock In/Out Endpoint -app.post('/api/clock', (req, res) => { - const { userId, eventType, qrCodeValue, latitude, longitude } = req.body - const validQrCode = qrCodes.find((qr) => qr.id === qrCodeValue && qr.isActive) - if (!validQrCode) return res.status(400).json({ message: 'Invalid or inactive QR Code.' }) - - const lastEvent = clockEvents.filter((e) => e.userId === userId).pop() - if (lastEvent && lastEvent.eventType === eventType) - return res - .status(400) - .json({ message: `You are already clocked ${eventType === 'clock_in' ? 'in' : 'out'}.` }) - - const newEvent = { - id: eventId++, - userId, - eventType, - timestamp: new Date().toISOString(), - qrCodeUsedId: qrCodeValue, - qrCodeUsedName: validQrCode.name, // Add human-readable name - latitude, - longitude, - } - clockEvents.push(newEvent) - res.status(201).json(newEvent) -}) - -// Worker Status Endpoint -app.get('/api/worker/status/:userId', (req, res) => { - const userId = parseInt(req.params.userId, 10) - const lastEvent = clockEvents.filter((event) => event.userId === userId).pop() - if (lastEvent) { - res.json(lastEvent) - } else { - res.json({ eventType: 'clock_out' }) - } -}) - -// Worker History Endpoint -app.get('/api/worker/clock-history/:userId', (req, res) => { - const userId = parseInt(req.params.userId, 10) - const userEvents = clockEvents.filter((event) => event.userId === userId) - res.json(userEvents) -}) - -// --- Manager Endpoints --- - -// Reporting Endpoint -app.get('/api/managers/hours-report', (req, res) => { - const { startDate, endDate, format } = req.query - - let filteredEvents = clockEvents - if (startDate && endDate) { - const endOfDay = new Date(endDate) - endOfDay.setHours(23, 59, 59, 999) // Include the whole end day - filteredEvents = clockEvents.filter((event) => { - const eventDate = new Date(event.timestamp) - return eventDate >= new Date(startDate) && eventDate <= endOfDay - }) - } - - const reportData = calculateHours(filteredEvents) - - if (format === 'csv') { - const json2csvParser = new Parser({ - fields: ['userId', 'fullName', 'totalHours', 'hasIncomplete'], - }) - const csv = json2csvParser.parse(reportData) - res.header('Content-Type', 'text/csv') - res.attachment(`hours-report-${new Date().toISOString().split('T')[0]}.csv`) - return res.send(csv) - } - - res.json(reportData) -}) - -// GET QR Codes Endpoint -app.get('/api/managers/qr-codes', (req, res) => { - res.json(qrCodes.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))) -}) - -// POST (Add new) QR Code Endpoint -app.post('/api/managers/qr-codes', (req, res) => { - const { name } = req.body - if (!name) { - return res.status(400).json({ message: 'QR Code name is required.' }) - } - const newQrCode = { - id: uuidv4(), - name, - isActive: true, - createdAt: new Date().toISOString(), - } - qrCodes.push(newQrCode) - res.status(201).json(newQrCode) -}) - -// PUT (Update) QR Code Endpoint -app.put('/api/managers/qr-codes/:id', (req, res) => { - const { id } = req.params - const { isActive } = req.body - const qrCodeIndex = qrCodes.findIndex((qr) => qr.id === id) - - if (qrCodeIndex === -1) { - return res.status(404).json({ message: 'QR Code not found.' }) - } - if (typeof isActive !== 'boolean') { - return res.status(400).json({ message: 'isActive must be a boolean.' }) - } - - qrCodes[qrCodeIndex].isActive = isActive - res.json(qrCodes[qrCodeIndex]) -}) - -// DELETE QR Code Endpoint -app.delete('/api/managers/qr-codes/:id', (req, res) => { - const { id } = req.params - const initialLength = qrCodes.length - qrCodes = qrCodes.filter((qr) => qr.id !== id) - - if (qrCodes.length === initialLength) { - return res.status(404).json({ message: 'QR Code not found.' }) - } - - res.status(204).send() -}) - -// --- Server Start --- -app.listen(port, () => { - console.log(`Server is running on http://localhost:${port}`) -}) +startServer() diff --git a/package-lock.json b/package-lock.json index 8d609b8..fd2d799 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,9 +10,11 @@ "dependencies": { "body-parser": "^2.2.0", "cors": "^2.8.5", + "dotenv": "^16.5.0", "express": "^5.1.0", "html5-qrcode": "^2.3.8", "json2csv": "^6.0.0-alpha.2", + "mysql2": "^3.14.1", "qrcode": "^1.5.4", "uuid": "^11.1.0", "vue": "^3.5.13", @@ -1934,6 +1936,15 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2363,6 +2374,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -2378,6 +2398,18 @@ "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", "license": "MIT" }, + "node_modules/dotenv": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -3034,6 +3066,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -3384,6 +3425,12 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, "node_modules/is-stream": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", @@ -3613,6 +3660,12 @@ "dev": true, "license": "MIT" }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -3623,6 +3676,21 @@ "yallist": "^3.0.2" } }, + "node_modules/lru.min": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.2.tgz", + "integrity": "sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, "node_modules/magic-string": { "version": "0.30.17", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", @@ -3719,6 +3787,47 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/mysql2": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.14.1.tgz", + "integrity": "sha512-7ytuPQJjQB8TNAYX/H2yhL+iQOnIBjAMam361R7UAL0lOVXWjtdrmoL9HYKqKoLp/8UUTRcvo1QPvK9KL7wA8w==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.6.3", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "license": "MIT", + "dependencies": { + "lru-cache": "^7.14.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/named-placeholders/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -4373,6 +4482,11 @@ "node": ">= 18" } }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, "node_modules/serve-static": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", @@ -4542,6 +4656,15 @@ "node": ">=0.10.0" } }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", diff --git a/package.json b/package.json index 868695c..7750f38 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,11 @@ "dependencies": { "body-parser": "^2.2.0", "cors": "^2.8.5", + "dotenv": "^16.5.0", "express": "^5.1.0", "html5-qrcode": "^2.3.8", "json2csv": "^6.0.0-alpha.2", + "mysql2": "^3.14.1", "qrcode": "^1.5.4", "uuid": "^11.1.0", "vue": "^3.5.13", diff --git a/src/App.vue b/src/App.vue index 369ad85..b08b324 100644 --- a/src/App.vue +++ b/src/App.vue @@ -2,9 +2,12 @@

Clock-In/Out System

- +
+ + +
@@ -13,10 +16,30 @@ diff --git a/src/components/HelloWorld.vue b/src/components/HelloWorld.vue deleted file mode 100644 index eff59f1..0000000 --- a/src/components/HelloWorld.vue +++ /dev/null @@ -1,44 +0,0 @@ - - - - - diff --git a/src/components/HoursReport.vue b/src/components/HoursReport.vue deleted file mode 100644 index 51531ea..0000000 --- a/src/components/HoursReport.vue +++ /dev/null @@ -1,169 +0,0 @@ - - - - - diff --git a/src/components/PersonnelManagement.vue b/src/components/PersonnelManagement.vue new file mode 100644 index 0000000..5ae0e43 --- /dev/null +++ b/src/components/PersonnelManagement.vue @@ -0,0 +1,277 @@ + + + + + diff --git a/src/components/QrCodeManagement.vue b/src/components/QrCodeManagement.vue index ceee1a0..79cfbd1 100644 --- a/src/components/QrCodeManagement.vue +++ b/src/components/QrCodeManagement.vue @@ -43,16 +43,18 @@ {{ qr.name }} - - {{ qr.isActive ? 'Active' : 'Inactive' }} + + + {{ qr.is_active ? 'Active' : 'Inactive' }} + @@ -65,14 +67,21 @@ +``` These targeted fixes should resolve the issues completely. The component will now correctly +display the status from the database on initial load and will properly update the state when you +activate or deactivate a QR co diff --git a/src/components/TheWelcome.vue b/src/components/TheWelcome.vue deleted file mode 100644 index fe48afc..0000000 --- a/src/components/TheWelcome.vue +++ /dev/null @@ -1,94 +0,0 @@ - - - diff --git a/src/components/WelcomeItem.vue b/src/components/WelcomeItem.vue deleted file mode 100644 index ac366d0..0000000 --- a/src/components/WelcomeItem.vue +++ /dev/null @@ -1,86 +0,0 @@ - - - diff --git a/src/components/WorkerManagement.vue b/src/components/WorkerManagement.vue deleted file mode 100644 index 6363b77..0000000 --- a/src/components/WorkerManagement.vue +++ /dev/null @@ -1,115 +0,0 @@ - - - - - diff --git a/src/router/index.js b/src/router/index.js index e950d59..cad739c 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -3,31 +3,63 @@ import LoginView from '../views/LoginView.vue' import WorkerDashboardView from '../views/WorkerDashboardView.vue' import ManagerDashboardView from '../views/ManagerDashboardView.vue' import WorkerHistoryView from '../views/WorkerHistoryView.vue' +import AttendanceRecordView from '../views/AttendanceRecordView.vue' // <-- Import new view const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ - { - path: '/', - name: 'login', - component: LoginView, - }, + { path: '/', name: 'login', component: LoginView }, { path: '/worker/dashboard', name: 'worker-dashboard', component: WorkerDashboardView, + meta: { requiresAuth: true, role: 'worker' }, }, { path: '/worker/history', name: 'worker-history', component: WorkerHistoryView, + meta: { requiresAuth: true, role: 'worker' }, }, { path: '/manager/dashboard', name: 'manager-dashboard', component: ManagerDashboardView, + meta: { requiresAuth: true, role: 'manager' }, + }, + // --- ADD THIS NEW ROUTE --- + { + path: '/manager/attendance/:workerId', + name: 'manager-attendance-record', + component: AttendanceRecordView, + meta: { requiresAuth: true, role: 'manager' }, }, ], }) +// --- ALIGNMENT CHANGE: Navigation Guard --- +router.beforeEach((to, from, next) => { + const isLoggedIn = !!sessionStorage.getItem('userId') + const userRole = sessionStorage.getItem('userRole') + + if (to.meta.requiresAuth) { + if (isLoggedIn) { + // Check if user has the required role + if (to.meta.role && to.meta.role === userRole) { + next() // User is logged in and has the correct role + } else { + // User is logged in but trying to access a page for another role + // Redirect them to their own dashboard + next(userRole === 'worker' ? '/worker/dashboard' : '/manager/dashboard') + } + } else { + // User is not logged in, redirect to login page + next('/') + } + } else { + // For public routes like the login page + next() + } +}) + export default router diff --git a/src/views/AttendanceRecordView.vue b/src/views/AttendanceRecordView.vue new file mode 100644 index 0000000..487e926 --- /dev/null +++ b/src/views/AttendanceRecordView.vue @@ -0,0 +1,139 @@ + + + + + diff --git a/src/views/LoginView.vue b/src/views/LoginView.vue index 770af84..72a48ee 100644 --- a/src/views/LoginView.vue +++ b/src/views/LoginView.vue @@ -12,7 +12,9 @@

Hint: worker/password or manager/password

- +

{{ error }}

@@ -27,8 +29,11 @@ const router = useRouter() const username = ref('') const password = ref('') const error = ref('') +const loading = ref(false) const handleLogin = async () => { + loading.value = true + error.value = '' try { const response = await fetch('http://localhost:3000/api/auth/login', { method: 'POST', @@ -39,7 +44,11 @@ const handleLogin = async () => { const data = await response.json() if (response.ok) { - // In a real app, you would save a token (e.g., JWT) here + // --- ALIGNMENT CHANGE --- + // Store user info in session storage + sessionStorage.setItem('userId', data.userId) + sessionStorage.setItem('userRole', data.role) + if (data.role === 'worker') { router.push('/worker/dashboard') } else if (data.role === 'manager') { @@ -50,11 +59,14 @@ const handleLogin = async () => { } } catch { error.value = 'Failed to connect to the server.' + } finally { + loading.value = false } }