From c5aecb2af916e215ada905f988537dbe2d9e0b1e Mon Sep 17 00:00:00 2001 From: elangptra Date: Mon, 28 Oct 2024 09:36:05 +0700 Subject: [PATCH] feat: monitoring data to csv --- .envexample | 2 +- controllers/auth/auth.js | 2 +- .../learningControllers/stdExercise.js | 24 +- .../monitoringControllers/monitoring.js | 421 +++++++++++++++++- index.js | 4 +- package-lock.json | 6 + package.json | 1 + routes/monitoring/monitoring.js | 6 +- 8 files changed, 444 insertions(+), 22 deletions(-) diff --git a/.envexample b/.envexample index 564ee2c..f5f47b6 100644 --- a/.envexample +++ b/.envexample @@ -1,6 +1,6 @@ APP_PORT = 3001 NODE_ENV = development -FE_DOMAIN = http://localhost:5173 +CLIENT_URL = http://localhost:5173 DB_HOST = localhost DB_USER = root diff --git a/controllers/auth/auth.js b/controllers/auth/auth.js index 016c1fd..1036566 100644 --- a/controllers/auth/auth.js +++ b/controllers/auth/auth.js @@ -389,7 +389,7 @@ export const forgotPassword = async (req, res) => { } ); - const resetLink = `${process.env.FE_DOMAIN}/resetPassword/${resetToken}`; + const resetLink = `${process.env.CLIENT_URL}/resetPassword/${resetToken}`; const mailOptions = { from: process.env.EMAIL_USER, diff --git a/controllers/learningControllers/stdExercise.js b/controllers/learningControllers/stdExercise.js index ceb9247..5b6a017 100644 --- a/controllers/learningControllers/stdExercise.js +++ b/controllers/learningControllers/stdExercise.js @@ -170,7 +170,6 @@ export const getStudentAnswersByStdLearningId = async (req, res) => { const exerciseDetails = exercise.stdExerciseExercises; const questionType = exerciseDetails.QUESTION_TYPE; - // Create the formatted exercise object const formattedExercise = { ID_STUDENT_EXERCISE: exerciseData.ID_STUDENT_EXERCISE, ID_ADMIN_EXERCISE: exerciseDetails.ID_ADMIN_EXERCISE, @@ -183,7 +182,6 @@ export const getStudentAnswersByStdLearningId = async (req, res) => { RESULT_SCORE_STUDENT: exercise.RESULT_SCORE_STUDENT, }; - // Include appropriate details based on question type if (questionType === "MCQ") { formattedExercise.multipleChoices = exerciseDetails.multipleChoices; } else if (questionType === "MPQ") { @@ -193,12 +191,22 @@ export const getStudentAnswersByStdLearningId = async (req, res) => { return formattedExercise; }); - return response(200, { - ...stdLearningData, - stdExercises: mappedExercises, - }, "Student learning exercises retrieved successfully", res); + return response( + 200, + { + ...stdLearningData, + stdExercises: mappedExercises, + }, + "Student learning exercises retrieved successfully", + res + ); } catch (error) { console.error(error); - return response(500, null, "Error retrieving student learning exercises", res); + return response( + 500, + null, + "Error retrieving student learning exercises", + res + ); } -}; \ No newline at end of file +}; diff --git a/controllers/monitoringControllers/monitoring.js b/controllers/monitoringControllers/monitoring.js index ba9c44b..bff2503 100644 --- a/controllers/monitoringControllers/monitoring.js +++ b/controllers/monitoringControllers/monitoring.js @@ -1,5 +1,10 @@ import response from "../../response.js"; import models from "../../models/index.js"; +import { createObjectCsvWriter } from "csv-writer"; +import { Readable } from "stream"; +import fs from "fs"; +import os from "os"; +import path from "path"; export const getMonitorings = async (req, res) => { try { @@ -373,7 +378,21 @@ export const monitoringStudentProgressById = async (req, res) => { } else if (sort === "score") { return b.SCORE - a.SCORE; } else if (sort === "feedback") { - return a.FEEDBACK_STUDENT.localeCompare(b.FEEDBACK_STUDENT); + if (a.FEEDBACK_STUDENT === null && b.FEEDBACK_STUDENT !== null) { + return 1; + } else if ( + a.FEEDBACK_STUDENT !== null && + b.FEEDBACK_STUDENT === null + ) { + return -1; + } else if ( + a.FEEDBACK_STUDENT === null && + b.FEEDBACK_STUDENT === null + ) { + return 0; + } else { + return a.FEEDBACK_STUDENT.localeCompare(b.FEEDBACK_STUDENT); + } } else if (sort === "start") { return new Date(a.STUDENT_START) - new Date(b.STUDENT_START); } else if (sort === "finish") { @@ -848,27 +867,41 @@ export const getClassMonitoringDataByClassAndTopic = async (req, res) => { "STUDENT_START", "STUDENT_FINISH", ], + order: [["STUDENT_FINISH", "DESC"]], }); const sortedRecords = allStdLearning.sort((a, b) => { + const userComparison = a.learningUser.NAME_USERS.localeCompare( + b.learningUser.NAME_USERS + ); + if (userComparison !== 0) { + return userComparison; + } + if (sort === "nisn") { return String(a.learningUser.students.NISN).localeCompare( String(b.learningUser.students.NISN) ); - } else if (sort === "user") { - return a.learningUser.NAME_USERS.localeCompare( - b.learningUser.NAME_USERS - ); + } else if (sort === "name") { + return userComparison; } else if (sort === "level") { return a.level.NAME_LEVEL.localeCompare(b.level.NAME_LEVEL); } else if (sort === "score") { return b.SCORE - a.SCORE; } else if (sort === "feedback") { - return a.FEEDBACK_STUDENT.localeCompare(b.FEEDBACK_STUDENT); + if (a.FEEDBACK_STUDENT === null && b.FEEDBACK_STUDENT !== null) { + return 1; + } else if (a.FEEDBACK_STUDENT !== null && b.FEEDBACK_STUDENT === null) { + return -1; + } else if (a.FEEDBACK_STUDENT === null && b.FEEDBACK_STUDENT === null) { + return 0; + } else { + return a.FEEDBACK_STUDENT.localeCompare(b.FEEDBACK_STUDENT); + } } else if (sort === "start") { return new Date(a.STUDENT_START) - new Date(b.STUDENT_START); } - // Default sorting by student finish + return new Date(a.STUDENT_FINISH) - new Date(b.STUDENT_FINISH); }); @@ -956,8 +989,6 @@ export const monitoringFeedbackByClassAndTopic = async (req, res) => { ], }); - console.log("Monitoring Data:", JSON.stringify(monitoringData, null, 2)); - if (!monitoringData || monitoringData.length === 0) { return response( 404, @@ -1027,3 +1058,375 @@ export const monitoringFeedbackByClassAndTopic = async (req, res) => { response(500, null, "Error updating teacher feedback!", res); } }; + +export const monitoringStudentProgressCSVById = async (req, res) => { + const { id } = req.params; + + try { + const monitoring = await models.Monitoring.findOne({ + where: { ID_MONITORING: id }, + include: [ + { + model: models.StdLearning, + as: "stdLearningMonitoring", + attributes: [ + "ID", + "ID_LEVEL", + "SCORE", + "FEEDBACK_STUDENT", + "STUDENT_START", + "STUDENT_FINISH", + "ID_STUDENT_LEARNING", + ], + include: [ + { + model: models.Level, + as: "level", + attributes: ["ID_TOPIC", "NAME_LEVEL"], + include: [ + { + model: models.Topic, + as: "levelTopic", + attributes: ["NAME_TOPIC"], + include: [ + { + model: models.Section, + as: "topicSection", + attributes: ["NAME_SECTION"], + }, + ], + }, + ], + }, + { + model: models.User, + as: "learningUser", + attributes: ["NAME_USERS"], + include: [ + { + model: models.Student, + as: "students", + attributes: ["NISN"], + }, + ], + }, + ], + }, + ], + }); + + if (!monitoring) { + return response(404, null, "Monitoring data not found!", res); + } + + const stdLearning = monitoring.stdLearningMonitoring; + + if (!stdLearning || stdLearning.length === 0) { + return response(404, null, "No student learning data found!", res); + } + + const userID = stdLearning.ID; + const topicID = stdLearning.level.ID_TOPIC; + const studentName = stdLearning.learningUser.NAME_USERS; + const nisn = stdLearning.learningUser.students.NISN; + const topicName = stdLearning.level.levelTopic.NAME_TOPIC; + const sectionName = stdLearning.level.levelTopic.topicSection.NAME_SECTION; + + const levels = await models.StdLearning.findAll({ + where: { + ID: userID, + STUDENT_FINISH: { + [models.Sequelize.Op.ne]: null, + }, + }, + include: [ + { + model: models.Level, + as: "level", + attributes: ["NAME_LEVEL", "ID_TOPIC"], + where: { + ID_TOPIC: topicID, + }, + }, + ], + attributes: [ + "SCORE", + "FEEDBACK_STUDENT", + "STUDENT_START", + "STUDENT_FINISH", + ], + order: [["STUDENT_FINISH", "DESC"]], + distinct: true, + }); + + const levelArray = levels.map((learning) => ({ + NAME_LEVEL: learning.level.NAME_LEVEL, + SCORE: learning.SCORE, + FEEDBACK_STUDENT: learning.FEEDBACK_STUDENT, + STUDENT_START: learning.STUDENT_START, + STUDENT_FINISH: learning.STUDENT_FINISH, + })); + + const tempDir = os.tmpdir(); + const tempFilePath = path.join( + tempDir, + `Student_Monitoring_${nisn}_${studentName}.csv` + ); + + const csvWriter = createObjectCsvWriter({ + path: tempFilePath, + header: [ + { id: "field", title: "Field" }, + { id: "value", title: "Value" }, + ], + }); + + const records = [ + { field: "Section Name", value: sectionName }, + { field: "Topic Name", value: topicName }, + { field: "Student Name", value: studentName }, + { field: "NISN", value: nisn }, + ]; + + await csvWriter.writeRecords(records); + + const levelCsvWriter = createObjectCsvWriter({ + path: tempFilePath, + append: true, + header: [ + { id: "NAME_LEVEL", title: "Level Name" }, + { id: "SCORE", title: "Score" }, + { id: "FEEDBACK_STUDENT", title: "Student Feedback" }, + { id: "STUDENT_START", title: "Start Exercise" }, + { id: "STUDENT_FINISH", title: "Finish Exercise" }, + ], + }); + + fs.appendFileSync(tempFilePath, "\n"); + + fs.appendFileSync( + tempFilePath, + "Name Level,Score,Student Feedback,Start Exercise,Finish Exercise\n" + ); + + await levelCsvWriter.writeRecords(levelArray); + + res.setHeader("Content-Type", "text/csv"); + res.setHeader( + "Content-Disposition", + `attachment; filename="Student_Monitoring_${nisn}_${studentName}.csv.csv"` + ); + + const fileStream = fs.createReadStream(tempFilePath); + fileStream.pipe(res).on("finish", () => { + fs.unlinkSync(tempFilePath); + }); + } catch (error) { + console.error(error); + response(500, null, "Error retrieving student progress!", res); + } +}; + +export const classMonitoringCSVByClassAndTopic = async (req, res) => { + const { ID_CLASS, ID_TOPIC } = req.body; + + try { + const classData = await models.Class.findOne({ + where: { ID_CLASS: ID_CLASS }, + attributes: ["ID_CLASS", "NAME_CLASS"], + }); + + if (!classData) { + return response(404, null, "Class not found!", res); + } + + const className = classData.NAME_CLASS; + + const topicData = await models.Topic.findOne({ + where: { ID_TOPIC: ID_TOPIC }, + include: [ + { + model: models.Section, + as: "topicSection", + attributes: ["ID_SECTION", "NAME_SECTION"], + }, + ], + attributes: ["ID_TOPIC", "NAME_TOPIC"], + }); + + if (!topicData) { + return response(404, null, "Topic not found!", res); + } + + const topicName = topicData.NAME_TOPIC; + const sectionName = topicData.topicSection?.NAME_SECTION; + + const monitoringData = await models.Monitoring.findAll({ + where: { ID_CLASS: ID_CLASS }, + include: [ + { + model: models.StdLearning, + as: "stdLearningMonitoring", + required: true, + attributes: ["ID", "ID_LEVEL"], + include: [ + { + model: models.Level, + as: "level", + attributes: ["ID_LEVEL", "ID_TOPIC"], + where: { ID_TOPIC: ID_TOPIC }, + required: true, + }, + ], + }, + ], + }); + + if (!monitoringData || monitoringData.length === 0) { + const result = { + ID_CLASS: classData.ID_CLASS, + ID_SECTION: topicData.topicSection?.ID_SECTION, + ID_TOPIC: topicData.ID_TOPIC, + NAME_CLASS: className, + NAME_SECTION: sectionName, + NAME_TOPIC: topicName, + }; + return response( + 200, + result, + "No monitoring data found for this class and topic!", + res + ); + } + + const userIds = monitoringData.map( + (monitoring) => monitoring.stdLearningMonitoring.ID + ); + + const allStdLearning = await models.StdLearning.findAll({ + where: { + ID_LEVEL: { + [models.Sequelize.Op.in]: ( + await models.Level.findAll({ + where: { ID_TOPIC: ID_TOPIC }, + attributes: ["ID_LEVEL"], + }) + ).map((level) => level.ID_LEVEL), + }, + ID: { + [models.Sequelize.Op.in]: userIds, + }, + STUDENT_FINISH: { + [models.Sequelize.Op.ne]: null, + }, + }, + include: [ + { + model: models.Level, + as: "level", + attributes: ["NAME_LEVEL", "ID_TOPIC"], + }, + { + model: models.User, + as: "learningUser", + attributes: ["NAME_USERS"], + include: [ + { + model: models.Student, + as: "students", + attributes: ["NISN"], + }, + ], + }, + ], + attributes: [ + "SCORE", + "FEEDBACK_STUDENT", + "STUDENT_START", + "STUDENT_FINISH", + ], + order: [ + [{ model: models.User, as: "learningUser" }, "NAME_USERS", "ASC"], + ["STUDENT_FINISH", "DESC"], + ], + }); + + const formattedData = allStdLearning.map((stdLearning) => { + const level = stdLearning?.level; + const user = stdLearning?.learningUser; + const student = user?.students; + + return { + NISN: student?.NISN, + NAME_USERS: user?.NAME_USERS, + NAME_LEVEL: level?.NAME_LEVEL, + SCORE: stdLearning?.SCORE, + FEEDBACK_STUDENT: stdLearning?.FEEDBACK_STUDENT, + STUDENT_START: stdLearning?.STUDENT_START, + STUDENT_FINISH: stdLearning?.STUDENT_FINISH, + }; + }); + + const sanitizedClassName = className.replace(/\s+/g, "_"); + const sanitizedTopicName = topicName.replace(/\s+/g, "_"); + + const tempDir = os.tmpdir(); + const tempFilePath = path.join( + tempDir, + `Class_Monitoring_${sanitizedClassName}_${sanitizedTopicName}.csv` + ); + + const csvWriter = createObjectCsvWriter({ + path: tempFilePath, + header: [ + { id: "field", title: "Field" }, + { id: "value", title: "Value" }, + ], + }); + + const records = [ + { field: "Class Name", value: className }, + { field: "Section Name", value: sectionName }, + { field: "Topic Name", value: topicName }, + ]; + + await csvWriter.writeRecords(records); + + const levelCsvWriter = createObjectCsvWriter({ + path: tempFilePath, + append: true, + header: [ + { id: "NISN", title: "NISN" }, + { id: "NAME_USERS", title: "Student Name" }, + { id: "NAME_LEVEL", title: "Level Name" }, + { id: "SCORE", title: "Score" }, + { id: "FEEDBACK_STUDENT", title: "Student Feedback" }, + { id: "STUDENT_START", title: "Start Exercise" }, + { id: "STUDENT_FINISH", title: "Finish Exercise" }, + ], + }); + + fs.appendFileSync(tempFilePath, "\n"); + + fs.appendFileSync( + tempFilePath, + "NISN,Student Name,Level Name,Score,Student Feedback,Start Exercise,Finish Exercise\n" + ); + + await levelCsvWriter.writeRecords(formattedData); + + res.setHeader("Content-Type", "text/csv"); + res.setHeader( + "Content-Disposition", + `attachment; filename=Class_Monitoring_${sanitizedClassName}_${sanitizedTopicName}.csv` + ); + + const fileStream = fs.createReadStream(tempFilePath); + fileStream.pipe(res).on("finish", () => { + fs.unlinkSync(tempFilePath); + }); + } catch (error) { + console.error("Error fetching monitoring data:", error); + response(500, null, "Error retrieving monitoring data!", res); + } +}; diff --git a/index.js b/index.js index bf09f21..51410d7 100644 --- a/index.js +++ b/index.js @@ -9,7 +9,7 @@ dotenv.config(); const app = express(); const corsOptions = { - origin: "http://localhost:5173", + origin: `${process.env.CLIENT_URL}`, methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], allowedHeaders: ["Content-Type", "Authorization"], credentials: true, @@ -27,6 +27,6 @@ app.use(express.static("public")); app.listen(process.env.APP_PORT, () => { testConnection(); console.log( - `Server running on port http://localhost:${process.env.APP_PORT}` + `Server running on port ${process.env.APP_PORT}` ); }); diff --git a/package-lock.json b/package-lock.json index a401f0f..747b397 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "bcrypt": "^5.1.1", "cookie-parser": "^1.4.6", "cors": "^2.8.5", + "csv-writer": "^1.6.0", "dotenv": "^16.4.5", "express": "^4.19.2", "jsonwebtoken": "^9.0.2", @@ -646,6 +647,11 @@ "node": ">= 8" } }, + "node_modules/csv-writer": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/csv-writer/-/csv-writer-1.6.0.tgz", + "integrity": "sha512-NOx7YDFWEsM/fTRAJjRpPp8t+MKRVvniAg9wQlUKx20MFrPs73WLJhFf5iteqrxNYnsy924K3Iroh3yNHeYd2g==" + }, "node_modules/d": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", diff --git a/package.json b/package.json index 7c67b34..1656d79 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "bcrypt": "^5.1.1", "cookie-parser": "^1.4.6", "cors": "^2.8.5", + "csv-writer": "^1.6.0", "dotenv": "^16.4.5", "express": "^4.19.2", "jsonwebtoken": "^9.0.2", diff --git a/routes/monitoring/monitoring.js b/routes/monitoring/monitoring.js index ae6c127..2a6ae2f 100644 --- a/routes/monitoring/monitoring.js +++ b/routes/monitoring/monitoring.js @@ -1,5 +1,5 @@ import express from "express"; -import { getMonitorings, getMonitoringById,monitoringStudentsProgress, monitoringStudentProgressById, getClassMonitoringByClassId, getClassMonitoringDataByClassAndTopic, getMonitoringByTopicId, monitoringFeedback, monitoringFeedbackByClassAndTopic } from "../../controllers/monitoringControllers/monitoring.js"; +import { getMonitorings, getMonitoringById,monitoringStudentsProgress, monitoringStudentProgressById, getClassMonitoringByClassId, getClassMonitoringDataByClassAndTopic, getMonitoringByTopicId, monitoringFeedback, monitoringFeedbackByClassAndTopic, monitoringStudentProgressCSVById, classMonitoringCSVByClassAndTopic } from "../../controllers/monitoringControllers/monitoring.js"; import { verifyLoginUser, adminOrTeacherOnly } from "../../middlewares/User/authUser.js"; const router = express.Router(); @@ -12,12 +12,16 @@ router.get("/monitoring/class", verifyLoginUser, getClassMonitoringDataByClassAn router.get("/monitoring/:id", verifyLoginUser, getMonitoringById); +router.get("/monitoring/class/csv", verifyLoginUser, adminOrTeacherOnly, classMonitoringCSVByClassAndTopic); + router.get("/monitoring/class/:classId", verifyLoginUser, adminOrTeacherOnly, getClassMonitoringByClassId); router.get("/monitoring/topic/:topicId", verifyLoginUser, getMonitoringByTopicId); router.get("/monitoring/progress/:id", verifyLoginUser, adminOrTeacherOnly, monitoringStudentProgressById); +router.get("/monitoring/progress/csv/:id", verifyLoginUser, adminOrTeacherOnly, monitoringStudentProgressCSVById); + router.post("/monitoring/feedback/class", verifyLoginUser, adminOrTeacherOnly, monitoringFeedbackByClassAndTopic); router.post("/monitoring/feedback/:id", verifyLoginUser, adminOrTeacherOnly, monitoringFeedback);