271 lines
7.4 KiB
JavaScript
271 lines
7.4 KiB
JavaScript
import express from 'express'
|
|
import cors from 'cors'
|
|
import { Parser } from 'json2csv'
|
|
import { v4 as uuidv4 } from 'uuid'
|
|
|
|
const app = express()
|
|
const port = 3000
|
|
|
|
app.use(cors())
|
|
app.use(express.json())
|
|
|
|
// --- 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)
|
|
})
|
|
|
|
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
|
|
}
|
|
}
|
|
return Object.values(userHours)
|
|
}
|
|
|
|
// --- 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}`)
|
|
})
|