feat: add student data with xlsx
This commit is contained in:
parent
12d24466a8
commit
b524a0a41a
|
|
@ -1,5 +1,8 @@
|
|||
import response from "../../response.js";
|
||||
import bcrypt from "bcryptjs";
|
||||
import csvParser from "csv-parser";
|
||||
import streamifier from "streamifier";
|
||||
import ExcelJS from "exceljs";
|
||||
import jwt from "jsonwebtoken";
|
||||
import nodemailer from "nodemailer";
|
||||
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) => {
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -335,7 +335,7 @@ export const getExerciseByLevelId = async (req, res) => {
|
|||
});
|
||||
|
||||
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) => {
|
||||
|
|
|
|||
|
|
@ -268,7 +268,7 @@ export const getLevelsByTopicId = async (req, res) => {
|
|||
|
||||
if (!levels || levels.length === 0) {
|
||||
return res
|
||||
.status(404)
|
||||
.status(200)
|
||||
.json({ message: "No levels found for the given topic." });
|
||||
}
|
||||
|
||||
|
|
@ -1025,7 +1025,7 @@ export const getLevelFiles = async (req, res) => {
|
|||
const imageFiles = getFilesByLevelId(imageFolderPath, "IMAGE");
|
||||
|
||||
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 = {
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ export const getTopicBySectionId = async (req, res) => {
|
|||
});
|
||||
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ export const getStudentAnswersByStdLearningId = async (req, res) => {
|
|||
const { ID_LEVEL } = stdLearningData;
|
||||
|
||||
const exercises = await models.Exercise.findAll({
|
||||
where: { ID_LEVEL },
|
||||
where: { ID_LEVEL, IS_DELETED: 0 },
|
||||
include: [
|
||||
{
|
||||
model: models.MultipleChoices,
|
||||
|
|
|
|||
|
|
@ -289,7 +289,7 @@ export const learningHistory = async (req, res) => {
|
|||
|
||||
if (!stdLearnings.length) {
|
||||
return response(
|
||||
404,
|
||||
200,
|
||||
null,
|
||||
"No learning history found for this user",
|
||||
res
|
||||
|
|
@ -399,7 +399,7 @@ export const learningHistoryBySectionId = async (req, res) => {
|
|||
|
||||
if (!stdLearnings.length) {
|
||||
return response(
|
||||
404,
|
||||
200,
|
||||
null,
|
||||
"No learning history found for the specified section",
|
||||
res
|
||||
|
|
@ -513,7 +513,7 @@ export const learningHistoryByTopicId = async (req, res) => {
|
|||
|
||||
if (!stdLearnings.length) {
|
||||
return response(
|
||||
404,
|
||||
200,
|
||||
null,
|
||||
"No learning history found for the specified topic",
|
||||
res
|
||||
|
|
@ -686,7 +686,7 @@ export const recentStudentActivities = async (req, res) => {
|
|||
}));
|
||||
|
||||
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(
|
||||
|
|
@ -838,7 +838,7 @@ export const recentStudentActivitiesByClassId = async (req, res) => {
|
|||
}));
|
||||
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -309,13 +309,13 @@ export const monitoringStudentProgressById = async (req, res) => {
|
|||
});
|
||||
|
||||
if (!monitoring) {
|
||||
return response(404, null, "Monitoring data not found!", res);
|
||||
return response(200, 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);
|
||||
return response(200, null, "No student learning data found!", res);
|
||||
}
|
||||
|
||||
const userID = stdLearning.ID;
|
||||
|
|
@ -448,7 +448,7 @@ export const monitoringFeedback = async (req, res) => {
|
|||
return response(404, null, "Teacher not found!", res);
|
||||
}
|
||||
|
||||
const [updatedRows] = await models.Monitoring.update(
|
||||
await models.Monitoring.update(
|
||||
{
|
||||
ID_GURU: teacher.ID_GURU,
|
||||
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({
|
||||
where: { ID_MONITORING: id },
|
||||
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 = {
|
||||
ID_MONITORING: monitoringWithRelations.ID_MONITORING,
|
||||
FEEDBACK_GURU: monitoringWithRelations.FEEDBACK_GURU,
|
||||
|
|
@ -537,7 +529,7 @@ export const getMonitoringByTopicId = async (req, res) => {
|
|||
|
||||
if (!stdLearning) {
|
||||
return response(
|
||||
404,
|
||||
200,
|
||||
null,
|
||||
"Student learning data not found for this topic!",
|
||||
res
|
||||
|
|
@ -566,7 +558,7 @@ export const getMonitoringByTopicId = async (req, res) => {
|
|||
|
||||
if (!monitoringData) {
|
||||
return response(
|
||||
404,
|
||||
200,
|
||||
null,
|
||||
"Monitoring data not found for this learning record!",
|
||||
res
|
||||
|
|
@ -645,7 +637,7 @@ export const getClassMonitoringByClassId = async (req, res) => {
|
|||
|
||||
if (!monitoringRecords || monitoringRecords.length === 0) {
|
||||
return response(
|
||||
404,
|
||||
200,
|
||||
null,
|
||||
"No monitoring data found for this class!",
|
||||
res
|
||||
|
|
@ -991,7 +983,7 @@ export const monitoringFeedbackByClassAndTopic = async (req, res) => {
|
|||
|
||||
if (!monitoringData || monitoringData.length === 0) {
|
||||
return response(
|
||||
404,
|
||||
200,
|
||||
null,
|
||||
"No monitoring data found for this class and topic!",
|
||||
res
|
||||
|
|
@ -1122,7 +1114,7 @@ export const monitoringStudentProgressCSVById = async (req, res) => {
|
|||
const stdLearning = monitoring.stdLearningMonitoring;
|
||||
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -140,8 +140,6 @@ export const getReportById = async (req, res) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const getReportForAdmin = async (req, res) => {};
|
||||
|
||||
export const getReportByUserId = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
|
@ -158,7 +156,7 @@ export const getReportByUserId = async (req, res) => {
|
|||
});
|
||||
|
||||
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) => {
|
||||
|
|
|
|||
BIN
media/uploads/excel/excel template.xlsx
Normal file
BIN
media/uploads/excel/excel template.xlsx
Normal file
Binary file not shown.
46
middlewares/User/uploadCSV.js
Normal file
46
middlewares/User/uploadCSV.js
Normal 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
786
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
|
@ -24,8 +24,10 @@
|
|||
"bcryptjs": "^2.4.3",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cors": "^2.8.5",
|
||||
"csv-parser": "^3.0.0",
|
||||
"csv-writer": "^1.6.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"exceljs": "^4.4.0",
|
||||
"express": "^4.19.2",
|
||||
"express-prom-bundle": "^8.0.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
|
|
@ -38,6 +40,7 @@
|
|||
"prom-client": "^15.1.3",
|
||||
"sequelize": "^6.37.3",
|
||||
"sequelize-cli": "^6.6.2",
|
||||
"streamifier": "^0.1.1",
|
||||
"uuid": "^10.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import express from "express";
|
||||
import { registerTeacher, registerStudent, registerStudentForAdminAndTeacher, registerTeacherForAdmin, registerAdmin, validateEmail, loginUser, refreshToken, logoutUser, forgotPassword, resetPassword } from "../../controllers/auth/auth.js";
|
||||
import { verifyLoginUser, adminOnly } from "../../middlewares/User/authUser.js";
|
||||
import { registerTeacher, registerStudent, registerStudentForAdminAndTeacher, registerStudentCSV, registerTeacherForAdmin, registerAdmin, validateEmail, loginUser, refreshToken, logoutUser, forgotPassword, resetPassword } from "../../controllers/auth/auth.js";
|
||||
import { verifyLoginUser, adminOnly, adminOrTeacherOnly } from "../../middlewares/User/authUser.js";
|
||||
import handleCsvUpload from "../../middlewares/User/uploadCSV.js";
|
||||
|
||||
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/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);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user