2b2947c0ce
- 在考勤记录页面添加手动打卡表单 - 实现后端API处理手动打卡记录 - 新增工人仪表盘显示姓名 - 在考勤报表中添加加班工资计算功能
376 lines
14 KiB
JavaScript
376 lines
14 KiB
JavaScript
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.DBHOST,
|
|
user: process.env.DBUSER,
|
|
password: process.env.DBPASSWORD,
|
|
database: process.env.DBNAME,
|
|
port: process.env.DBPORT,
|
|
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, cr.latitude, cr.longitude, cr.notes 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}.` })
|
|
}
|
|
|
|
await db.execute(
|
|
'INSERT INTO clock_records (worker_id, event_type, timestamp, notes, qr_code_id, latitude, longitude) VALUES (?, ?, ?, ?, NULL, NULL, NULL)',
|
|
[workerId, eventType, timestamp, 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()
|