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 @@
@@ -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 @@
-
-
-
-
-
{{ msg }}
-
- You’ve successfully created a project with
- Vite +
- Vue 3 .
-
-
-
-
-
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 @@
-
-
-
-
-
-
- Start Date
-
-
-
- End Date
-
-
-
-
- {{ loadingReport ? 'Loading...' : 'Generate Report' }}
-
-
- Export as CSV
-
-
-
-
- Loading report data...
-
-
-
-
-
- Worker
- Total Hours
- Status
-
-
-
-
- {{ item.fullName }}
- {{ item.totalHours }}
-
- Incomplete
- Complete
-
-
-
-
-
- Total Collective Hours: {{ collectiveHours.toFixed(2) }}
-
-
-
-
- No data found for the selected period.
-
-
-
-
-
-
-
-
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 @@
+
+
+
+
+
+
+ {{ errorMessage }}
+
+
+
+
+
+
+
+
+
+
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' }}
⬇️ Download
+
- {{ qr.isActive ? 'Deactivate' : 'Activate' }}
+ {{ qr.is_active ? 'Deactivate' : 'Activate' }}
Delete
@@ -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 @@
-
-
-
-
-
-
-
- Documentation
-
- Vue’s
- official documentation
- provides you with all information you need to get started.
-
-
-
-
-
-
- Tooling
-
- This project is served and bundled with
- Vite . The
- recommended IDE setup is
- VSCode
- +
- Vue - Official . If
- you need to test your components and web pages, check out
- Vitest
- and
- Cypress
- /
- Playwright .
-
-
-
- More instructions are available in
- README.md .
-
-
-
-
-
-
- Ecosystem
-
- Get official tools and libraries for your project:
- Pinia ,
- Vue Router ,
- Vue Test Utils , and
- Vue Dev Tools . If
- you need more resources, we suggest paying
- Awesome Vue
- a visit.
-
-
-
-
-
-
- Community
-
- Got stuck? Ask your question on
- Vue Land
- (our official Discord server), or
- StackOverflow . You should also follow the official
- @vuejs.org
- Bluesky account or the
- @vuejs
- X account for latest news in the Vue world.
-
-
-
-
-
-
- Support Vue
-
- As an independent project, Vue relies on community backing for its sustainability. You can help
- us by
- becoming a sponsor .
-
-
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 @@
-
-
-
-
-
-
- {{ worker.fullName }}
-
-
-
-
-
-
- Loading details...
-
-
Total Hours
-
{{ totalHours.toFixed(2) }} hours worked (all time)
-
Clock History (Latest First)
-
-
-
- Event
- Timestamp
- QR Code Name
-
-
-
-
- {{ event.eventType }}
- {{ new Date(event.timestamp).toLocaleString() }}
- {{ event.qrCodeUsedName }}
-
-
-
-
-
-
Select a worker from the list to view their details.
-
-
-
-
-
-
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 @@
+
+
+
+
+
+
+ Start Date
+
+
+
+ End Date
+
+
+
Filter Records
+
+
+
+
+ Event
+ Timestamp
+ Location Name
+ Coordinates
+
+
+
+
+
+ No records found for this period.
+
+
+
+
+ {{
+ record.event_type.replace('_', ' ')
+ }}
+
+ {{ new Date(record.timestamp).toLocaleString() }}
+ {{ record.qrCodeUsedName }}
+
+ {{
+ record.latitude
+ ? `${Number(record.latitude).toFixed(4)}, ${Number(record.longitude).toFixed(4)}`
+ : 'N/A'
+ }}
+
+
+
+
+
+
+
+
+
+
+
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
- Login
+
+ {{ loading ? 'Logging in...' : 'Login' }}
+
{{ 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
}
}