feat: add student data with xlsx

This commit is contained in:
elangptra 2024-12-02 14:27:15 +07:00
parent 12d24466a8
commit b524a0a41a
13 changed files with 1055 additions and 35 deletions

View File

@ -1,5 +1,8 @@
import response from "../../response.js"; import response from "../../response.js";
import bcrypt from "bcryptjs"; import bcrypt from "bcryptjs";
import csvParser from "csv-parser";
import streamifier from "streamifier";
import ExcelJS from "exceljs";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import nodemailer from "nodemailer"; import nodemailer from "nodemailer";
import moment from "moment-timezone"; import moment from "moment-timezone";
@ -680,6 +683,145 @@ export const registerStudentForAdminAndTeacher = async (req, res) => {
} }
}; };
export const registerStudentCSV = async (req, res) => {
const { buffer, extension } = req.uploadedFile;
if (!buffer) {
return response(400, null, "File is required!", res);
}
const students = [];
try {
if (extension === ".csv") {
await new Promise((resolve, reject) => {
const csvStream = streamifier.createReadStream(buffer);
let headers = null;
let headerFound = false;
csvStream
.pipe(csvParser())
.on("data", (row) => {
if (!headerFound) {
const keys = Object.keys(row).filter((key) => row[key]?.trim());
if (keys.length > 0) {
headers = normalizeHeaders(keys);
headerFound = true;
}
return;
}
if (headers) {
const NISN = row[headers.NISN]?.trim();
const NAME_USERS = row[headers.NAME_USERS]?.trim();
if (NISN && NAME_USERS) {
students.push({ NISN, NAME_USERS });
}
}
})
.on("end", resolve)
.on("error", reject);
});
} else if (extension === ".xlsx") {
const workbook = new ExcelJS.Workbook();
await workbook.xlsx.load(buffer);
const worksheet = workbook.worksheets[0];
let headers = null;
worksheet.eachRow((row, rowNumber) => {
if (rowNumber === 1 || !headers) {
const headersRow = worksheet.getRow(rowNumber);
headers = normalizeHeadersXLSX(
headersRow.values.filter((value) => value)
);
if (Object.keys(headers).length === 0) {
return;
}
} else {
const isEmptyRow = row.values.every(
(cell) => !cell || !String(cell).trim()
);
if (isEmptyRow) {
return;
}
const NISN = row.getCell(headers.NISN)?.text?.trim();
const NAME_USERS = row.getCell(headers.NAME_USERS)?.text?.trim();
if (NISN && NAME_USERS) {
students.push({ NISN, NAME_USERS });
}
}
});
} else {
return response(400, null, "Unsupported file format!", res);
}
if (students.length === 0) {
return response(400, null, "File is empty or invalid format!", res);
}
} catch (error) {
console.error(error);
return response(500, null, "Error processing file!", res);
}
const transaction = await models.db.transaction();
try {
for (const student of students) {
const { NISN, NAME_USERS } = student;
const EMAIL = `${NISN}@gmail.com`;
const PASSWORD = "12345678";
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(PASSWORD, salt);
const newUser = await models.User.create(
{
NAME_USERS,
EMAIL,
PASSWORD: hashedPassword,
ROLE: "student",
IS_VALIDATED: 1,
},
{ transaction }
);
await models.Student.create(
{
ID: newUser.ID,
NISN,
},
{ transaction }
);
}
await transaction.commit();
response(200, { success: true }, "Students registered successfully!", res);
} catch (error) {
console.error(error);
await transaction.rollback();
if (error.name === "SequelizeUniqueConstraintError") {
const field = error.original.sqlMessage.match(/for key '(.+)'/)[1];
if (field === "student_unique_nisn") {
return response(400, null, "Duplicate NISN found in file!", res);
}
if (field === "user_unique_email") {
return response(400, null, "Duplicate email found in file!", res);
}
}
response(500, null, "Internal Server Error", res);
}
};
export const loginUser = async (req, res) => { export const loginUser = async (req, res) => {
const { IDENTIFIER, PASSWORD } = req.body; const { IDENTIFIER, PASSWORD } = req.body;
@ -1060,3 +1202,59 @@ export const resetPassword = async (req, res) => {
} }
} }
}; };
/**
* Normalize headers from file (case-insensitive) and map to field names.
* @param {Array} headers Array of headers from file
* @returns {Object} Mapping of normalized headers
*/
function normalizeHeaders(headers) {
const synonyms = {
NISN: ["nisn", "no nisn", "nomor nisn"],
NAME_USERS: ["nama", "nama lengkap", "full name", "nama siswa"],
};
const normalized = {};
headers.forEach((header, index) => {
const match = findClosestMatch(header, synonyms);
if (match) {
normalized[match] = header;
}
});
return normalized;
}
function findClosestMatch(header, synonyms) {
header = header.toLowerCase();
for (const [key, values] of Object.entries(synonyms)) {
if (values.some((value) => header.includes(value))) {
return key;
}
}
return null;
}
function normalizeHeadersXLSX(headers) {
const mapping = {
nisn: "NISN",
"no nisn": "NISN",
"nomor nisn": "NISN",
nama: "NAME_USERS",
"nama lengkap": "NAME_USERS",
"full name": "NAME_USERS",
"nama siswa": "NAME_USERS",
};
const normalized = {};
headers.forEach((header, index) => {
const normalizedHeader = header.trim().toLowerCase();
if (mapping[normalizedHeader]) {
normalized[mapping[normalizedHeader]] = index + 1;
}
});
return normalized;
}

View File

@ -335,7 +335,7 @@ export const getExerciseByLevelId = async (req, res) => {
}); });
if (!exercises || exercises.length === 0) { if (!exercises || exercises.length === 0) {
return response(404, null, "No exercises found for this level", res); return response(200, null, "No exercises found for this level", res);
} }
const formattedExercises = exercises.map((exercise) => { const formattedExercises = exercises.map((exercise) => {

View File

@ -268,7 +268,7 @@ export const getLevelsByTopicId = async (req, res) => {
if (!levels || levels.length === 0) { if (!levels || levels.length === 0) {
return res return res
.status(404) .status(200)
.json({ message: "No levels found for the given topic." }); .json({ message: "No levels found for the given topic." });
} }
@ -1025,7 +1025,7 @@ export const getLevelFiles = async (req, res) => {
const imageFiles = getFilesByLevelId(imageFolderPath, "IMAGE"); const imageFiles = getFilesByLevelId(imageFolderPath, "IMAGE");
if (audioFiles.length === 0 && imageFiles.length === 0) { if (audioFiles.length === 0 && imageFiles.length === 0) {
return response(404, null, "No files found for this level", res); return response(200, null, "No files found for this level", res);
} }
const levelFiles = { const levelFiles = {

View File

@ -49,7 +49,7 @@ export const getTopicBySectionId = async (req, res) => {
}); });
if (!topics || topics.length === 0) { if (!topics || topics.length === 0) {
return response(404, null, "No topics found for this section", res); return response(200, null, "No topics found for this section", res);
} }
const topicsWithCompletionStatus = await Promise.all( const topicsWithCompletionStatus = await Promise.all(

View File

@ -136,7 +136,7 @@ export const getStudentAnswersByStdLearningId = async (req, res) => {
const { ID_LEVEL } = stdLearningData; const { ID_LEVEL } = stdLearningData;
const exercises = await models.Exercise.findAll({ const exercises = await models.Exercise.findAll({
where: { ID_LEVEL }, where: { ID_LEVEL, IS_DELETED: 0 },
include: [ include: [
{ {
model: models.MultipleChoices, model: models.MultipleChoices,

View File

@ -289,7 +289,7 @@ export const learningHistory = async (req, res) => {
if (!stdLearnings.length) { if (!stdLearnings.length) {
return response( return response(
404, 200,
null, null,
"No learning history found for this user", "No learning history found for this user",
res res
@ -399,7 +399,7 @@ export const learningHistoryBySectionId = async (req, res) => {
if (!stdLearnings.length) { if (!stdLearnings.length) {
return response( return response(
404, 200,
null, null,
"No learning history found for the specified section", "No learning history found for the specified section",
res res
@ -513,7 +513,7 @@ export const learningHistoryByTopicId = async (req, res) => {
if (!stdLearnings.length) { if (!stdLearnings.length) {
return response( return response(
404, 200,
null, null,
"No learning history found for the specified topic", "No learning history found for the specified topic",
res res
@ -686,7 +686,7 @@ export const recentStudentActivities = async (req, res) => {
})); }));
if (!recentActivities.length) { if (!recentActivities.length) {
return res.status(404).json({ message: "No recent activities found" }); return res.status(200).json({ message: "No recent activities found" });
} }
const paginatedActivities = recentActivities.slice( const paginatedActivities = recentActivities.slice(
@ -838,7 +838,7 @@ export const recentStudentActivitiesByClassId = async (req, res) => {
})); }));
if (!recentActivities.length) { if (!recentActivities.length) {
return res.status(404).json({ message: "No recent activities found" }); return res.status(200).json({ message: "No recent activities found" });
} }
const paginatedActivities = recentActivities.slice( const paginatedActivities = recentActivities.slice(

View File

@ -309,13 +309,13 @@ export const monitoringStudentProgressById = async (req, res) => {
}); });
if (!monitoring) { if (!monitoring) {
return response(404, null, "Monitoring data not found!", res); return response(200, null, "Monitoring data not found!", res);
} }
const stdLearning = monitoring.stdLearningMonitoring; const stdLearning = monitoring.stdLearningMonitoring;
if (!stdLearning || stdLearning.length === 0) { if (!stdLearning || stdLearning.length === 0) {
return response(404, null, "No student learning data found!", res); return response(200, null, "No student learning data found!", res);
} }
const userID = stdLearning.ID; const userID = stdLearning.ID;
@ -448,7 +448,7 @@ export const monitoringFeedback = async (req, res) => {
return response(404, null, "Teacher not found!", res); return response(404, null, "Teacher not found!", res);
} }
const [updatedRows] = await models.Monitoring.update( await models.Monitoring.update(
{ {
ID_GURU: teacher.ID_GURU, ID_GURU: teacher.ID_GURU,
FEEDBACK_GURU: feedback, FEEDBACK_GURU: feedback,
@ -459,10 +459,6 @@ export const monitoringFeedback = async (req, res) => {
} }
); );
if (updatedRows === 0) {
return response(404, null, "Monitoring data not found!", res);
}
const monitoringWithRelations = await models.Monitoring.findOne({ const monitoringWithRelations = await models.Monitoring.findOne({
where: { ID_MONITORING: id }, where: { ID_MONITORING: id },
include: [ include: [
@ -481,10 +477,6 @@ export const monitoringFeedback = async (req, res) => {
], ],
}); });
if (!monitoringWithRelations) {
return response(404, null, "Updated monitoring data not found!", res);
}
const result = { const result = {
ID_MONITORING: monitoringWithRelations.ID_MONITORING, ID_MONITORING: monitoringWithRelations.ID_MONITORING,
FEEDBACK_GURU: monitoringWithRelations.FEEDBACK_GURU, FEEDBACK_GURU: monitoringWithRelations.FEEDBACK_GURU,
@ -537,7 +529,7 @@ export const getMonitoringByTopicId = async (req, res) => {
if (!stdLearning) { if (!stdLearning) {
return response( return response(
404, 200,
null, null,
"Student learning data not found for this topic!", "Student learning data not found for this topic!",
res res
@ -566,7 +558,7 @@ export const getMonitoringByTopicId = async (req, res) => {
if (!monitoringData) { if (!monitoringData) {
return response( return response(
404, 200,
null, null,
"Monitoring data not found for this learning record!", "Monitoring data not found for this learning record!",
res res
@ -645,7 +637,7 @@ export const getClassMonitoringByClassId = async (req, res) => {
if (!monitoringRecords || monitoringRecords.length === 0) { if (!monitoringRecords || monitoringRecords.length === 0) {
return response( return response(
404, 200,
null, null,
"No monitoring data found for this class!", "No monitoring data found for this class!",
res res
@ -991,7 +983,7 @@ export const monitoringFeedbackByClassAndTopic = async (req, res) => {
if (!monitoringData || monitoringData.length === 0) { if (!monitoringData || monitoringData.length === 0) {
return response( return response(
404, 200,
null, null,
"No monitoring data found for this class and topic!", "No monitoring data found for this class and topic!",
res res
@ -1122,7 +1114,7 @@ export const monitoringStudentProgressCSVById = async (req, res) => {
const stdLearning = monitoring.stdLearningMonitoring; const stdLearning = monitoring.stdLearningMonitoring;
if (!stdLearning || stdLearning.length === 0) { if (!stdLearning || stdLearning.length === 0) {
return response(404, null, "No student learning data found!", res); return response(200, null, "No student learning data found!", res);
} }
const userID = stdLearning.ID; const userID = stdLearning.ID;

View File

@ -140,8 +140,6 @@ export const getReportById = async (req, res) => {
} }
}; };
export const getReportForAdmin = async (req, res) => {};
export const getReportByUserId = async (req, res) => { export const getReportByUserId = async (req, res) => {
try { try {
const { id } = req.params; const { id } = req.params;
@ -158,7 +156,7 @@ export const getReportByUserId = async (req, res) => {
}); });
if (reports.length === 0) { if (reports.length === 0) {
return response(404, null, "No report data found for this user", res); return response(200, null, "No report data found for this user", res);
} }
const modifiedReports = reports.map((report) => { const modifiedReports = reports.map((report) => {

Binary file not shown.

View File

@ -0,0 +1,46 @@
import multer from "multer";
import path from "path";
import response from "../../response.js";
const memoryStorage = multer.memoryStorage();
const fileFilter = (req, file, cb) => {
const ext = path.extname(file.originalname).toLowerCase();
if (ext === ".csv" || ext === ".xlsx") {
cb(null, true);
} else {
cb(
new Error("Invalid file type, only .csv and .xlsx files are allowed!"),
false
);
}
};
const upload = multer({
storage: memoryStorage,
fileFilter,
limits: { fileSize: 2 * 1024 * 1024 },
}).fields([{ name: "file", maxCount: 1 }]);
const handleCsvUpload = (req, res, next) => {
upload(req, res, (err) => {
if (err) {
return response(400, null, err.message, res);
}
const files = req.files;
const file = files?.file ? files.file[0] : null;
if (!file) {
return response(400, null, "No file uploaded!", res);
}
req.uploadedFile = {
buffer: file.buffer,
extension: path.extname(file.originalname).toLowerCase(),
};
next();
});
};
export default handleCsvUpload;

786
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -24,8 +24,10 @@
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"cors": "^2.8.5", "cors": "^2.8.5",
"csv-parser": "^3.0.0",
"csv-writer": "^1.6.0", "csv-writer": "^1.6.0",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"exceljs": "^4.4.0",
"express": "^4.19.2", "express": "^4.19.2",
"express-prom-bundle": "^8.0.0", "express-prom-bundle": "^8.0.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
@ -38,6 +40,7 @@
"prom-client": "^15.1.3", "prom-client": "^15.1.3",
"sequelize": "^6.37.3", "sequelize": "^6.37.3",
"sequelize-cli": "^6.6.2", "sequelize-cli": "^6.6.2",
"streamifier": "^0.1.1",
"uuid": "^10.0.0" "uuid": "^10.0.0"
} }
} }

View File

@ -1,6 +1,7 @@
import express from "express"; import express from "express";
import { registerTeacher, registerStudent, registerStudentForAdminAndTeacher, registerTeacherForAdmin, registerAdmin, validateEmail, loginUser, refreshToken, logoutUser, forgotPassword, resetPassword } from "../../controllers/auth/auth.js"; import { registerTeacher, registerStudent, registerStudentForAdminAndTeacher, registerStudentCSV, registerTeacherForAdmin, registerAdmin, validateEmail, loginUser, refreshToken, logoutUser, forgotPassword, resetPassword } from "../../controllers/auth/auth.js";
import { verifyLoginUser, adminOnly } from "../../middlewares/User/authUser.js"; import { verifyLoginUser, adminOnly, adminOrTeacherOnly } from "../../middlewares/User/authUser.js";
import handleCsvUpload from "../../middlewares/User/uploadCSV.js";
const router = express.Router(); const router = express.Router();
@ -10,7 +11,9 @@ router.post("/register/student", registerStudent);
router.post("/admin/register/teacher", verifyLoginUser, adminOnly, registerTeacherForAdmin); router.post("/admin/register/teacher", verifyLoginUser, adminOnly, registerTeacherForAdmin);
router.post("/admin/register/student", verifyLoginUser, adminOnly, registerStudentForAdminAndTeacher); router.post("/admin/register/student", verifyLoginUser, adminOrTeacherOnly, registerStudentForAdminAndTeacher);
router.post("/admin/register/student/csv", verifyLoginUser, adminOrTeacherOnly, handleCsvUpload, registerStudentCSV);
router.post("/register/admin", verifyLoginUser, adminOnly, registerAdmin); router.post("/register/admin", verifyLoginUser, adminOnly, registerAdmin);