mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
264 lines
11 KiB
JavaScript
264 lines
11 KiB
JavaScript
/***********************************************************************************************************************************************************************
|
|
* File Name: scores.ctrl.js (staff)
|
|
* Type of Program: Controller
|
|
* Description: Staff-scoped scores and progress endpoints.
|
|
* Staff can view:
|
|
* - Task completion per user per task list
|
|
* - Quiz attempt scores (UnitQuiz)
|
|
* - Assessment attempt scores (CourseAssessment)
|
|
* - A combined progress summary per group/task list
|
|
***********************************************************************************************************************************************************************/
|
|
const { Op } = require('sequelize');
|
|
const sequelize = require('../../config/db.config');
|
|
const mdl_Users = require('../../models/users/users.mdl');
|
|
const { mdl_UserGroupMembers } = require('../../models/users/user_groups.mdl');
|
|
const { Task, TaskList, TaskListGroup } = require('../../models/task/task.mdl');
|
|
const TaskCompletion = require('../../models/scores/task_completions.mdl');
|
|
const QuizAttempt = require('../../models/scores/quiz_attempts.mdl');
|
|
const AssessmentAttempt = require('../../models/scores/assessment_attempts.mdl');
|
|
const UnitQuiz = require('../../models/courses/unit_quiz.mdl');
|
|
const CourseAssessment = require('../../models/courses/course_assessment.mdl');
|
|
const { Unit } = require('../../models/courses/units.mdl');
|
|
const { Course } = require('../../models/courses/courses.mdl');
|
|
|
|
// ─── Helper ───────────────────────────────────────────────────────────────────
|
|
async function getStaffGroupIds(staffUserId) {
|
|
const memberships = await mdl_UserGroupMembers.findAll({
|
|
where: { user_id: staffUserId },
|
|
attributes: ['group_id'],
|
|
raw: true,
|
|
});
|
|
return memberships.map(m => m.group_id);
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════════════════════
|
|
// TASK COMPLETION TRACKING
|
|
// ════════════════════════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* GET /api/staff/progress/task-list/:task_list_id
|
|
* Returns completion status for ALL members in the groups attached to this task list.
|
|
* Response shape: { members: [{ user, tasks: [{ task, completion }] }] }
|
|
*/
|
|
const getTaskListProgress = async (req, res) => {
|
|
try {
|
|
const staffUserId = req.user.user_id;
|
|
const { task_list_id } = req.params;
|
|
|
|
const scopedGroupIds = await getStaffGroupIds(staffUserId);
|
|
|
|
// Verify staff has access to this task list
|
|
const access = await TaskListGroup.findOne({
|
|
where: { task_list_id, group_id: { [Op.in]: scopedGroupIds } },
|
|
});
|
|
if (!access) return res.status(403).json({ success: false, message: 'No access to this task list.' });
|
|
|
|
// Get all groups linked to this task list (within staff's scope)
|
|
const linkedGroups = await TaskListGroup.findAll({
|
|
where: { task_list_id, group_id: { [Op.in]: scopedGroupIds } },
|
|
attributes: ['group_id'],
|
|
raw: true,
|
|
});
|
|
const linkedGroupIds = linkedGroups.map(g => g.group_id);
|
|
|
|
// Get all unique members in those groups
|
|
const memberships = await mdl_UserGroupMembers.findAll({
|
|
where: { group_id: { [Op.in]: linkedGroupIds } },
|
|
attributes: ['user_id'],
|
|
raw: true,
|
|
});
|
|
const memberIds = [...new Set(memberships.map(m => m.user_id))];
|
|
|
|
// Get all tasks in the task list
|
|
const tasks = await Task.findAll({ where: { task_list_id }, attributes: ['task_id', 'name', 'deadline', 'status'] });
|
|
|
|
// Get all completions for these tasks + members
|
|
const completions = await TaskCompletion.findAll({
|
|
where: {
|
|
task_id: { [Op.in]: tasks.map(t => t.task_id) },
|
|
user_id: { [Op.in]: memberIds },
|
|
},
|
|
attributes: ['task_id', 'user_id', 'status', 'completed_at'],
|
|
raw: true,
|
|
});
|
|
|
|
// Index completions: { user_id: { task_id: completion } }
|
|
const completionIndex = {};
|
|
completions.forEach(c => {
|
|
if (!completionIndex[c.user_id]) completionIndex[c.user_id] = {};
|
|
completionIndex[c.user_id][c.task_id] = c;
|
|
});
|
|
|
|
// Fetch user info
|
|
const users = await mdl_Users.findAll({
|
|
where: { user_id: { [Op.in]: memberIds } },
|
|
attributes: ['user_id', 'email', 'personal_info'],
|
|
});
|
|
|
|
// Build response
|
|
const data = users.map(u => ({
|
|
user: u,
|
|
tasks: tasks.map(t => ({
|
|
task: t,
|
|
completion: completionIndex[u.user_id]?.[t.task_id] ?? { status: 'pending', completed_at: null },
|
|
})),
|
|
completed_count: tasks.filter(t => completionIndex[u.user_id]?.[t.task_id]?.status === 'completed').length,
|
|
total_tasks: tasks.length,
|
|
}));
|
|
|
|
return res.status(200).json({ success: true, task_list_id, data });
|
|
} catch (err) {
|
|
console.error('[staff/scores] getTaskListProgress error:', err);
|
|
return res.status(500).json({ success: false, message: 'Internal server error.' });
|
|
}
|
|
};
|
|
|
|
// ════════════════════════════════════════════════════════════════════════════════
|
|
// QUIZ SCORES
|
|
// ════════════════════════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* GET /api/staff/scores/quiz/:quiz_id
|
|
* Returns all users' quiz attempts for a given UnitQuiz.
|
|
* Shows latest attempt + best score per user.
|
|
*/
|
|
const getQuizScores = async (req, res) => {
|
|
try {
|
|
const staffUserId = req.user.user_id;
|
|
const quizId = parseInt(req.params.quiz_id);
|
|
|
|
const scopedGroupIds = await getStaffGroupIds(staffUserId);
|
|
|
|
// Get all members in the staff's groups
|
|
const memberships = await mdl_UserGroupMembers.findAll({
|
|
where: { group_id: { [Op.in]: scopedGroupIds } },
|
|
attributes: ['user_id'],
|
|
raw: true,
|
|
});
|
|
const memberIds = [...new Set(memberships.map(m => m.user_id))];
|
|
|
|
const quiz = await UnitQuiz.findByPk(quizId, {
|
|
include: [{ model: Unit, as: 'unit', attributes: ['title', 'course_id'] }],
|
|
});
|
|
if (!quiz) return res.status(404).json({ success: false, message: 'Quiz not found.' });
|
|
|
|
// All attempts by members
|
|
const attempts = await QuizAttempt.findAll({
|
|
where: { quiz_id: quizId, user_id: { [Op.in]: memberIds } },
|
|
include: [
|
|
{ model: mdl_Users, as: 'user', attributes: ['user_id', 'email', 'personal_info'] },
|
|
],
|
|
order: [['user_id', 'ASC'], ['attempt_number', 'DESC']],
|
|
});
|
|
|
|
// Group by user, extract best + latest
|
|
const byUser = {};
|
|
attempts.forEach(a => {
|
|
const uid = a.user_id;
|
|
if (!byUser[uid]) byUser[uid] = { user: a.user, attempts: [], best_score: null, latest: null };
|
|
byUser[uid].attempts.push(a);
|
|
if (byUser[uid].best_score === null || a.score > byUser[uid].best_score) {
|
|
byUser[uid].best_score = a.score;
|
|
}
|
|
if (!byUser[uid].latest || a.attempt_number > byUser[uid].latest.attempt_number) {
|
|
byUser[uid].latest = a;
|
|
}
|
|
});
|
|
|
|
// Include members who haven't attempted yet
|
|
const attemptedIds = new Set(Object.keys(byUser).map(Number));
|
|
const nonAttempted = memberIds.filter(id => !attemptedIds.has(id));
|
|
const nonAttemptedUsers = await mdl_Users.findAll({
|
|
where: { user_id: { [Op.in]: nonAttempted } },
|
|
attributes: ['user_id', 'email', 'personal_info'],
|
|
});
|
|
nonAttemptedUsers.forEach(u => {
|
|
byUser[u.user_id] = { user: u, attempts: [], best_score: null, latest: null };
|
|
});
|
|
|
|
return res.status(200).json({
|
|
success: true,
|
|
quiz: { quiz_id: quiz.quiz_id, title: quiz.title, passing_score: quiz.passing_score, unit: quiz.unit },
|
|
data: Object.values(byUser),
|
|
});
|
|
} catch (err) {
|
|
console.error('[staff/scores] getQuizScores error:', err);
|
|
return res.status(500).json({ success: false, message: 'Internal server error.' });
|
|
}
|
|
};
|
|
|
|
// ════════════════════════════════════════════════════════════════════════════════
|
|
// ASSESSMENT SCORES
|
|
// ════════════════════════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* GET /api/staff/scores/assessment/:assessment_id
|
|
* Returns all users' assessment attempts for a CourseAssessment.
|
|
*/
|
|
const getAssessmentScores = async (req, res) => {
|
|
try {
|
|
const staffUserId = req.user.user_id;
|
|
const assessmentId = parseInt(req.params.assessment_id);
|
|
|
|
const scopedGroupIds = await getStaffGroupIds(staffUserId);
|
|
const memberships = await mdl_UserGroupMembers.findAll({
|
|
where: { group_id: { [Op.in]: scopedGroupIds } },
|
|
attributes: ['user_id'],
|
|
raw: true,
|
|
});
|
|
const memberIds = [...new Set(memberships.map(m => m.user_id))];
|
|
|
|
const assessment = await CourseAssessment.findByPk(assessmentId, {
|
|
include: [{ model: Course, as: 'course', attributes: ['title', 'course_code'] }],
|
|
});
|
|
if (!assessment) return res.status(404).json({ success: false, message: 'Assessment not found.' });
|
|
|
|
const attempts = await AssessmentAttempt.findAll({
|
|
where: { assessment_id: assessmentId, user_id: { [Op.in]: memberIds } },
|
|
include: [
|
|
{ model: mdl_Users, as: 'user', attributes: ['user_id', 'email', 'personal_info'] },
|
|
],
|
|
order: [['user_id', 'ASC'], ['attempt_number', 'DESC']],
|
|
});
|
|
|
|
const byUser = {};
|
|
attempts.forEach(a => {
|
|
const uid = a.user_id;
|
|
if (!byUser[uid]) byUser[uid] = { user: a.user, attempts: [], best_score: null, latest: null };
|
|
byUser[uid].attempts.push(a);
|
|
if (byUser[uid].best_score === null || a.score > byUser[uid].best_score) byUser[uid].best_score = a.score;
|
|
if (!byUser[uid].latest || a.attempt_number > byUser[uid].latest.attempt_number) byUser[uid].latest = a;
|
|
});
|
|
|
|
const attemptedIds = new Set(Object.keys(byUser).map(Number));
|
|
const nonAttempted = memberIds.filter(id => !attemptedIds.has(id));
|
|
const nonAttemptedUsers = await mdl_Users.findAll({
|
|
where: { user_id: { [Op.in]: nonAttempted } },
|
|
attributes: ['user_id', 'email', 'personal_info'],
|
|
});
|
|
nonAttemptedUsers.forEach(u => {
|
|
byUser[u.user_id] = { user: u, attempts: [], best_score: null, latest: null };
|
|
});
|
|
|
|
return res.status(200).json({
|
|
success: true,
|
|
assessment: {
|
|
assessment_id: assessment.assessment_id,
|
|
title: assessment.title,
|
|
passing_score: assessment.passing_score,
|
|
course: assessment.course,
|
|
},
|
|
data: Object.values(byUser),
|
|
});
|
|
} catch (err) {
|
|
console.error('[staff/scores] getAssessmentScores error:', err);
|
|
return res.status(500).json({ success: false, message: 'Internal server error.' });
|
|
}
|
|
};
|
|
|
|
module.exports = {
|
|
getTaskListProgress,
|
|
getQuizScores,
|
|
getAssessmentScores,
|
|
};
|