348 lines
No EOL
9.7 KiB
JavaScript
348 lines
No EOL
9.7 KiB
JavaScript
const express = require('express');
|
|
const { body, validationResult } = require('express-validator');
|
|
const { Attendance, User, Course } = require('../models');
|
|
const { auth, requireTrainee } = require('../middleware/auth');
|
|
|
|
const router = express.Router();
|
|
|
|
// @route POST /api/attendance/sign-in
|
|
// @desc Sign in for a course session
|
|
// @access Private (Trainee)
|
|
router.post('/sign-in', [
|
|
auth,
|
|
requireTrainee,
|
|
body('courseId').isUUID(),
|
|
body('location').optional().isString(),
|
|
body('deviceInfo').optional().isString()
|
|
], async (req, res) => {
|
|
try {
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) {
|
|
return res.status(400).json({ errors: errors.array() });
|
|
}
|
|
|
|
const { courseId, location, deviceInfo } = req.body;
|
|
const today = new Date().toISOString().split('T')[0];
|
|
|
|
// Check if course exists
|
|
const course = await Course.findByPk(courseId);
|
|
if (!course) {
|
|
return res.status(404).json({ error: 'Course not found.' });
|
|
}
|
|
|
|
// Check if already signed in today
|
|
const existingAttendance = await Attendance.findOne({
|
|
where: {
|
|
userId: req.user.id,
|
|
courseId,
|
|
date: today
|
|
}
|
|
});
|
|
|
|
if (existingAttendance && existingAttendance.signInTime) {
|
|
return res.status(400).json({ error: 'Already signed in for today.' });
|
|
}
|
|
|
|
let attendance;
|
|
if (existingAttendance) {
|
|
// Update existing record
|
|
attendance = await existingAttendance.update({
|
|
signInTime: new Date(),
|
|
status: 'present',
|
|
location,
|
|
deviceInfo,
|
|
ipAddress: req.ip
|
|
});
|
|
} else {
|
|
// Create new record
|
|
attendance = await Attendance.create({
|
|
userId: req.user.id,
|
|
courseId,
|
|
date: today,
|
|
signInTime: new Date(),
|
|
status: 'present',
|
|
location,
|
|
deviceInfo,
|
|
ipAddress: req.ip
|
|
});
|
|
}
|
|
|
|
const attendanceWithDetails = await Attendance.findByPk(attendance.id, {
|
|
include: [
|
|
{
|
|
model: Course,
|
|
as: 'course',
|
|
attributes: ['id', 'title']
|
|
},
|
|
{
|
|
model: User,
|
|
as: 'user',
|
|
attributes: ['id', 'firstName', 'lastName']
|
|
}
|
|
]
|
|
});
|
|
|
|
res.status(201).json({
|
|
message: 'Successfully signed in.',
|
|
attendance: attendanceWithDetails
|
|
});
|
|
} catch (error) {
|
|
console.error('Sign in error:', error);
|
|
res.status(500).json({ error: 'Server error.' });
|
|
}
|
|
});
|
|
|
|
// @route POST /api/attendance/sign-out
|
|
// @desc Sign out from a course session
|
|
// @access Private (Trainee)
|
|
router.post('/sign-out', [
|
|
auth,
|
|
requireTrainee,
|
|
body('courseId').isUUID()
|
|
], async (req, res) => {
|
|
try {
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) {
|
|
return res.status(400).json({ errors: errors.array() });
|
|
}
|
|
|
|
const { courseId } = req.body;
|
|
const today = new Date().toISOString().split('T')[0];
|
|
|
|
// Find today's attendance record
|
|
const attendance = await Attendance.findOne({
|
|
where: {
|
|
userId: req.user.id,
|
|
courseId,
|
|
date: today
|
|
}
|
|
});
|
|
|
|
if (!attendance) {
|
|
return res.status(404).json({ error: 'No sign-in record found for today.' });
|
|
}
|
|
|
|
if (attendance.signOutTime) {
|
|
return res.status(400).json({ error: 'Already signed out for today.' });
|
|
}
|
|
|
|
const signOutTime = new Date();
|
|
const duration = Math.round((signOutTime - attendance.signInTime) / (1000 * 60)); // in minutes
|
|
|
|
await attendance.update({
|
|
signOutTime,
|
|
duration,
|
|
status: duration < 30 ? 'early_departure' : 'present' // Less than 30 minutes is early departure
|
|
});
|
|
|
|
const updatedAttendance = await Attendance.findByPk(attendance.id, {
|
|
include: [
|
|
{
|
|
model: Course,
|
|
as: 'course',
|
|
attributes: ['id', 'title']
|
|
},
|
|
{
|
|
model: User,
|
|
as: 'user',
|
|
attributes: ['id', 'firstName', 'lastName']
|
|
}
|
|
]
|
|
});
|
|
|
|
res.json({
|
|
message: 'Successfully signed out.',
|
|
attendance: updatedAttendance
|
|
});
|
|
} catch (error) {
|
|
console.error('Sign out error:', error);
|
|
res.status(500).json({ error: 'Server error.' });
|
|
}
|
|
});
|
|
|
|
// @route GET /api/attendance/my
|
|
// @desc Get user's attendance records
|
|
// @access Private
|
|
router.get('/my', auth, async (req, res) => {
|
|
try {
|
|
const { courseId, startDate, endDate, page = 1, limit = 20 } = req.query;
|
|
const offset = (page - 1) * limit;
|
|
|
|
const whereClause = { userId: req.user.id };
|
|
if (courseId) whereClause.courseId = courseId;
|
|
if (startDate) whereClause.date = { [require('sequelize').Op.gte]: startDate };
|
|
if (endDate) whereClause.date = { [require('sequelize').Op.lte]: endDate };
|
|
|
|
const { count, rows: attendance } = await Attendance.findAndCountAll({
|
|
where: whereClause,
|
|
include: [
|
|
{
|
|
model: Course,
|
|
as: 'course',
|
|
attributes: ['id', 'title', 'thumbnail']
|
|
}
|
|
],
|
|
order: [['date', 'DESC']],
|
|
limit: parseInt(limit),
|
|
offset: parseInt(offset)
|
|
});
|
|
|
|
res.json({
|
|
attendance,
|
|
pagination: {
|
|
currentPage: parseInt(page),
|
|
totalPages: Math.ceil(count / limit),
|
|
totalItems: count,
|
|
itemsPerPage: parseInt(limit)
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Get attendance error:', error);
|
|
res.status(500).json({ error: 'Server error.' });
|
|
}
|
|
});
|
|
|
|
// @route GET /api/attendance/course/:courseId
|
|
// @desc Get attendance for a specific course (Trainer or Super Admin)
|
|
// @access Private
|
|
router.get('/course/:courseId', auth, async (req, res) => {
|
|
try {
|
|
const { courseId } = req.params;
|
|
const { date, page = 1, limit = 20 } = req.query;
|
|
const offset = (page - 1) * limit;
|
|
|
|
// Check if user can access this course attendance
|
|
const course = await Course.findByPk(courseId);
|
|
if (!course) {
|
|
return res.status(404).json({ error: 'Course not found.' });
|
|
}
|
|
|
|
if (req.user.role !== 'super_admin' && course.trainerId !== req.user.id) {
|
|
return res.status(403).json({ error: 'Not authorized to view this course attendance.' });
|
|
}
|
|
|
|
const whereClause = { courseId };
|
|
if (date) whereClause.date = date;
|
|
|
|
const { count, rows: attendance } = await Attendance.findAndCountAll({
|
|
where: whereClause,
|
|
include: [
|
|
{
|
|
model: User,
|
|
as: 'user',
|
|
attributes: ['id', 'firstName', 'lastName', 'avatar']
|
|
}
|
|
],
|
|
order: [['date', 'DESC'], ['signInTime', 'ASC']],
|
|
limit: parseInt(limit),
|
|
offset: parseInt(offset)
|
|
});
|
|
|
|
res.json({
|
|
attendance,
|
|
pagination: {
|
|
currentPage: parseInt(page),
|
|
totalPages: Math.ceil(count / limit),
|
|
totalItems: count,
|
|
itemsPerPage: parseInt(limit)
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Get course attendance error:', error);
|
|
res.status(500).json({ error: 'Server error.' });
|
|
}
|
|
});
|
|
|
|
// @route PUT /api/attendance/:id
|
|
// @desc Update attendance record (Trainer or Super Admin)
|
|
// @access Private
|
|
router.put('/:id', [
|
|
auth,
|
|
body('status').optional().isIn(['present', 'absent', 'late', 'early_departure']),
|
|
body('notes').optional().isString()
|
|
], async (req, res) => {
|
|
try {
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) {
|
|
return res.status(400).json({ errors: errors.array() });
|
|
}
|
|
|
|
const attendance = await Attendance.findByPk(req.params.id, {
|
|
include: [
|
|
{
|
|
model: Course,
|
|
as: 'course'
|
|
}
|
|
]
|
|
});
|
|
|
|
if (!attendance) {
|
|
return res.status(404).json({ error: 'Attendance record not found.' });
|
|
}
|
|
|
|
// Check permissions
|
|
if (req.user.role !== 'super_admin' && attendance.course.trainerId !== req.user.id) {
|
|
return res.status(403).json({ error: 'Not authorized to update this attendance record.' });
|
|
}
|
|
|
|
const updateData = {};
|
|
if (req.body.status) updateData.status = req.body.status;
|
|
if (req.body.notes !== undefined) updateData.notes = req.body.notes;
|
|
|
|
await attendance.update(updateData);
|
|
|
|
res.json({
|
|
message: 'Attendance record updated successfully.',
|
|
attendance: {
|
|
id: attendance.id,
|
|
status: attendance.status,
|
|
notes: attendance.notes
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Update attendance error:', error);
|
|
res.status(500).json({ error: 'Server error.' });
|
|
}
|
|
});
|
|
|
|
// @route GET /api/attendance/stats/my
|
|
// @desc Get user's attendance statistics
|
|
// @access Private
|
|
router.get('/stats/my', auth, async (req, res) => {
|
|
try {
|
|
const { courseId, startDate, endDate } = req.query;
|
|
|
|
const whereClause = { userId: req.user.id };
|
|
if (courseId) whereClause.courseId = courseId;
|
|
if (startDate) whereClause.date = { [require('sequelize').Op.gte]: startDate };
|
|
if (endDate) whereClause.date = { [require('sequelize').Op.lte]: endDate };
|
|
|
|
const totalSessions = await Attendance.count({ where: whereClause });
|
|
const presentSessions = await Attendance.count({
|
|
where: { ...whereClause, status: 'present' }
|
|
});
|
|
const absentSessions = await Attendance.count({
|
|
where: { ...whereClause, status: 'absent' }
|
|
});
|
|
const lateSessions = await Attendance.count({
|
|
where: { ...whereClause, status: 'late' }
|
|
});
|
|
|
|
const attendanceRate = totalSessions > 0 ? (presentSessions / totalSessions) * 100 : 0;
|
|
|
|
res.json({
|
|
stats: {
|
|
totalSessions,
|
|
presentSessions,
|
|
absentSessions,
|
|
lateSessions,
|
|
attendanceRate: Math.round(attendanceRate * 100) / 100
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Get attendance stats error:', error);
|
|
res.status(500).json({ error: 'Server error.' });
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|