This commit is contained in:
sudomarcma
2025-06-12 13:50:51 +08:00
commit f839e9d6b2
35 changed files with 7328 additions and 0 deletions
+270
View File
@@ -0,0 +1,270 @@
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}`)
})