import response from "../../response.js"; import models from "../../models/index.js"; import { clearFileBuffers, saveFileToDisk, } from "../../middlewares/Level/uploadLevel.js"; import fs from "fs"; import path from "path"; import { updateOtherLevelsRoutes, updateOtherLevelsRoutesOnDelete, } from "../../middlewares/Level/checkLevel.js"; export const getLevels = async (req, res) => { try { const levels = await models.Level.findAll({ where: { IS_DELETED: 0, }, attributes: { exclude: [ "ROUTE_1", "ROUTE_2", "ROUTE_3", "ROUTE_4", "ROUTE_5", "ROUTE_6", ], }, }); response(200, levels, "Success", res); } catch (error) { console.log(error); response(500, null, "Error retrieving levels data!", res); } }; export const getLevelById = async (req, res) => { try { const { id } = req.params; const level = await models.Level.findByPk(id, { where: { IS_DELETED: 0, }, attributes: { exclude: [ "ROUTE_1", "ROUTE_2", "ROUTE_3", "ROUTE_4", "ROUTE_5", "ROUTE_6", ], }, include: [ { model: models.Topic, as: "levelTopic", attributes: ["NAME_TOPIC"], include: { model: models.Section, as: "topicSection", attributes: ["NAME_SECTION"], }, }, ], }); if (!level) { return response(404, null, "Level not found", res); } const levelJSON = level.toJSON(); const NAME_SECTION = levelJSON.levelTopic.topicSection.NAME_SECTION; const NAME_TOPIC = levelJSON.levelTopic.NAME_TOPIC; delete levelJSON.levelTopic; delete levelJSON.levelTopic?.topicSection; const responsePayload = { NAME_SECTION, NAME_TOPIC, ...levelJSON, }; response(200, responsePayload, "Success", res); } catch (error) { console.log(error); response(500, null, "Internal Server Error", res); } }; export const getLevelForAdmin = async (req, res) => { const { page = 1, limit = 10, search = "", sort = "time" } = req.query; try { const { count, rows: levels } = await models.Level.findAndCountAll({ where: { IS_DELETED: 0, ...(search && { [models.Op.or]: [ { "$levelTopic->topicSection.NAME_SECTION$": { [models.Op.like]: `%${search}%`, }, }, { "$levelTopic.NAME_TOPIC$": { [models.Op.like]: `%${search}%`, }, }, { NAME_LEVEL: { [models.Op.like]: `%${search}%` }, }, ], }), }, attributes: ["ID_LEVEL", "NAME_LEVEL", "TIME_LEVEL"], include: [ { model: models.Topic, as: "levelTopic", attributes: ["ID_TOPIC", "NAME_TOPIC"], include: [ { model: models.Section, as: "topicSection", attributes: ["ID_SECTION", "NAME_SECTION"], }, ], }, ], distinct: true, }); const formattedLevels = levels.map((level) => ({ ID_LEVEL: level.ID_LEVEL, ID_SECTION: level.levelTopic.topicSection.ID_SECTION, ID_TOPIC: level.levelTopic.ID_TOPIC, NAME_SECTION: level.levelTopic.topicSection.NAME_SECTION, NAME_TOPIC: level.levelTopic.NAME_TOPIC, NAME_LEVEL: level.NAME_LEVEL, TIME_LEVEL: level.TIME_LEVEL, })); if (sort === "section") { formattedLevels.sort((a, b) => a.NAME_SECTION.localeCompare(b.NAME_SECTION) ); } else if (sort === "topic") { formattedLevels.sort((a, b) => { const topicComparison = a.NAME_TOPIC.localeCompare(b.NAME_TOPIC); if (topicComparison === 0) { if (a.NAME_LEVEL === "Pretest") return -1; if (b.NAME_LEVEL === "Pretest") return 1; const levelA = parseInt(a.NAME_LEVEL.replace("Level ", "")) || 0; const levelB = parseInt(b.NAME_LEVEL.replace("Level ", "")) || 0; return levelA - levelB; } return topicComparison; }); } else if (sort === "level") { formattedLevels.sort((a, b) => { if (a.NAME_LEVEL === "Pretest") return -1; if (b.NAME_LEVEL === "Pretest") return 1; const levelA = parseInt(a.NAME_LEVEL.replace("Level ", "")) || 0; const levelB = parseInt(b.NAME_LEVEL.replace("Level ", "")) || 0; return levelA - levelB; }); } else { formattedLevels.sort( (a, b) => new Date(b.TIME_LEVEL) - new Date(a.TIME_LEVEL) ); } const paginatedLevels = formattedLevels.slice( (page - 1) * limit, page * limit ); const totalPages = Math.ceil(count / limit); const currentPage = parseInt(page); response( 200, { levels: paginatedLevels, currentPage, totalPages, totalItems: count, }, "Levels retrieved successfully", res ); } catch (error) { console.log(error); response(500, null, "Error retrieving levels data!", res); } }; export const getLevelsByTopicId = async (req, res) => { try { const { idTopic } = req.params; const { ID } = req.user; const topicExists = await models.Topic.findByPk(idTopic, { include: { model: models.Section, as: "topicSection", attributes: ["NAME_SECTION"], }, }); if (!topicExists) { return res.status(404).json({ message: "Topic not found" }); } const levels = await models.Level.findAll({ where: { ID_TOPIC: idTopic, IS_DELETED: 0, }, attributes: { exclude: [ "ROUTE_1", "ROUTE_2", "ROUTE_3", "ROUTE_4", "ROUTE_5", "ROUTE_6", ], }, include: [ { model: models.StdLearning, as: "stdLearning", attributes: [ "SCORE", "ID_STUDENT_LEARNING", "STUDENT_START", "STUDENT_FINISH", "NEXT_LEARNING", ], where: { ID: ID, }, required: false, order: [["STUDENT_START", "DESC"]], limit: 1, }, { model: models.Topic, as: "levelTopic", attributes: ["NAME_TOPIC"], include: { model: models.Section, as: "topicSection", attributes: ["NAME_SECTION"], }, }, ], }); if (!levels || levels.length === 0) { return res .status(404) .json({ message: "No levels found for the given topic." }); } const lastCompletedLearning = await models.StdLearning.findOne({ where: { ID: ID, STUDENT_FINISH: { [models.Op.not]: null }, }, include: [ { model: models.Level, as: "level", attributes: ["ID_LEVEL", "NAME_LEVEL"], where: { ID_TOPIC: idTopic, IS_DELETED: 0, }, }, ], order: [["STUDENT_FINISH", "DESC"]], }); let currentLearningLevel = null; if (lastCompletedLearning?.NEXT_LEARNING) { currentLearningLevel = await models.Level.findOne({ where: { ID_LEVEL: lastCompletedLearning.NEXT_LEARNING, IS_DELETED: 0, }, attributes: ["ID_LEVEL", "NAME_LEVEL"], }); } const nextLearningLevel = currentLearningLevel; const unlockedLevels = lastCompletedLearning ? await models.Level.findAll({ where: { ID_TOPIC: idTopic, IS_DELETED: 0, [models.Op.or]: [ { NAME_LEVEL: { [models.Op.lt]: nextLearningLevel?.NAME_LEVEL } }, { NAME_LEVEL: "Pretest" }, ], }, attributes: ["NAME_LEVEL"], order: [["NAME_LEVEL", "ASC"]], }) : [{ NAME_LEVEL: "Pretest" }]; const unlockedLevelNames = unlockedLevels.map((lvl) => lvl.NAME_LEVEL); if ( nextLearningLevel && !unlockedLevelNames.includes(nextLearningLevel.NAME_LEVEL) ) { unlockedLevelNames.push(nextLearningLevel.NAME_LEVEL); } const levelsWithScore = levels.map((level) => { const SCORE = level.stdLearning && level.stdLearning.length > 0 ? level.stdLearning[0]?.SCORE : null; const ID_STUDENT_LEARNING = level.stdLearning && level.stdLearning.length > 0 ? level.stdLearning[0]?.ID_STUDENT_LEARNING : null; const levelJSON = level.toJSON(); const NAME_SECTION = levelJSON.levelTopic.topicSection.NAME_SECTION; const NAME_TOPIC = levelJSON.levelTopic.NAME_TOPIC; delete levelJSON.stdLearning; delete levelJSON.levelTopic; delete levelJSON.levelTopic?.topicSection; const isUnlocked = unlockedLevelNames.includes(levelJSON.NAME_LEVEL); if (isUnlocked) { return { NAME_SECTION, NAME_TOPIC, ...levelJSON, ID_STUDENT_LEARNING, SCORE, IS_PRETEST: levelJSON.NAME_LEVEL === "Pretest" ? 1 : 0, }; } else { return { ID_LEVEL: levelJSON.ID_LEVEL, NAME_LEVEL: levelJSON.NAME_LEVEL, IS_PRETEST: levelJSON.NAME_LEVEL === "Pretest" ? 1 : 0, SCORE, }; } }); const sortedLevels = levelsWithScore.sort((a, b) => { if (a.NAME_LEVEL === "Pretest") return -1; if (b.NAME_LEVEL === "Pretest") return 1; const levelA = parseInt(a.NAME_LEVEL.replace("Level ", "")); const levelB = parseInt(b.NAME_LEVEL.replace("Level ", "")); return levelA - levelB; }); const responsePayload = { lastCompletedLevel: lastCompletedLearning ? { ID_STUDENT_LEARNING: lastCompletedLearning.ID_STUDENT_LEARNING, ID_LEVEL: lastCompletedLearning.level.ID_LEVEL, NAME_LEVEL: lastCompletedLearning.level.NAME_LEVEL, SCORE: lastCompletedLearning.SCORE, NEXT_LEARNING: lastCompletedLearning.NEXT_LEARNING, NEXT_LEARNING_LEVEL: nextLearningLevel ? nextLearningLevel.NAME_LEVEL : null, FINISHED_AT: lastCompletedLearning.STUDENT_FINISH, UNLOCKED_LEVELS: unlockedLevelNames, } : { UNLOCKED_LEVELS: unlockedLevelNames, }, levels: sortedLevels, }; res.status(200).json({ message: "Success", data: responsePayload }); } catch (error) { console.error(error); res.status(500).json({ message: "Internal Server Error" }); } }; export const createLevel = async (req, res, next) => { const { NAME_LEVEL, ID_SECTION, ID_TOPIC, CONTENT, VIDEO } = req.body; const { AUDIO, IMAGE } = req.filesToSave || {}; if (!NAME_LEVEL) { clearFileBuffers({ AUDIO, IMAGE }); return response(400, null, "Level name is required", res); } if (!ID_SECTION) { clearFileBuffers({ AUDIO, IMAGE }); return response(400, null, "Section is required", res); } if (!ID_TOPIC) { clearFileBuffers({ AUDIO, IMAGE }); return response(400, null, "Topic is required", res); } const transaction = await models.db.transaction(); try { const sectionWithTopic = await models.Topic.findOne({ where: { ID_SECTION, ID_TOPIC }, transaction, }); if (!sectionWithTopic) { clearFileBuffers({ AUDIO, IMAGE }); await transaction.rollback(); return response( 400, null, "Topic does not relate to the provided Section!", res ); } const existingLevel = await models.Level.findOne({ where: { NAME_LEVEL, ID_TOPIC, IS_DELETED: 0, }, transaction, }); if (existingLevel) { clearFileBuffers({ AUDIO, IMAGE }); await transaction.rollback(); return response( 409, null, "A level with this name already exists under this topic", res ); } const newLevel = await models.Level.create( { NAME_LEVEL, ID_SECTION, ID_TOPIC, IS_PRETEST: req.body.IS_PRETEST || 0, CONTENT, VIDEO: VIDEO || null, AUDIO: null, IMAGE: null, ROUTE_1: req.body.ROUTE_1, ROUTE_2: req.body.ROUTE_2, ROUTE_3: req.body.ROUTE_3, ROUTE_4: req.body.ROUTE_4, ROUTE_5: req.body.ROUTE_5, ROUTE_6: req.body.ROUTE_6, }, { transaction } ); req.body.newLevelId = newLevel.ID_LEVEL; const audioFilename = AUDIO ? saveFileToDisk(AUDIO, "AUDIO", ID_TOPIC, ID_SECTION, newLevel.ID_LEVEL) : null; const imageFilename = IMAGE ? saveFileToDisk(IMAGE, "IMAGE", ID_TOPIC, ID_SECTION, newLevel.ID_LEVEL) : null; newLevel.AUDIO = audioFilename; newLevel.IMAGE = imageFilename; await newLevel.save({ transaction }); await transaction.commit(); await updateOtherLevelsRoutes(req, res, next); response(201, newLevel, "Level created successfully", res); } catch (error) { console.log(error); clearFileBuffers({ AUDIO, IMAGE }); await transaction.rollback(); return response(500, null, "Internal Server Error", res); } }; export const updateLevelById = async (req, res, next) => { const { id } = req.params; const { NAME_LEVEL, ID_SECTION, ID_TOPIC, CONTENT, VIDEO } = req.body; const { AUDIO, IMAGE } = req.filesToSave || {}; const transaction = await models.db.transaction(); try { const level = await models.Level.findByPk(id, { transaction }); if (!level) { clearFileBuffers({ AUDIO, IMAGE }); await transaction.rollback(); return response(404, null, "Level not found", res); } const sectionWithTopic = await models.Topic.findOne({ where: { ID_SECTION, ID_TOPIC }, transaction, }); if (!sectionWithTopic) { clearFileBuffers({ AUDIO, IMAGE }); await transaction.rollback(); return response( 400, null, "Topic does not relate to the provided Section", res ); } if (NAME_LEVEL && ID_TOPIC) { const existingLevel = await models.Level.findOne({ where: { NAME_LEVEL, ID_TOPIC, ID_LEVEL: { [models.Sequelize.Op.ne]: id }, IS_DELETED: 0, }, transaction, }); if (existingLevel) { clearFileBuffers({ AUDIO, IMAGE }); await transaction.rollback(); return response( 409, null, "A level with this name already exists under this topic", res ); } } if (NAME_LEVEL) { level.NAME_LEVEL = NAME_LEVEL; level.IS_PRETEST = NAME_LEVEL === "Pretest" ? 1 : 0; } if (ID_SECTION) level.ID_SECTION = ID_SECTION; if (ID_TOPIC) level.ID_TOPIC = ID_TOPIC; if (CONTENT) level.CONTENT = CONTENT; if (VIDEO) level.VIDEO = VIDEO; if (AUDIO) { if (level.AUDIO) { const oldAudioPath = path.join( process.cwd(), "media/uploads/level/audio", level.AUDIO ); if (fs.existsSync(oldAudioPath)) fs.unlinkSync(oldAudioPath); } level.AUDIO = saveFileToDisk( AUDIO, "AUDIO", ID_TOPIC || level.ID_TOPIC, ID_SECTION || level.ID_SECTION, level.ID_LEVEL ); } if (IMAGE) { if (level.IMAGE) { const oldImagePath = path.join( process.cwd(), "media/uploads/level/image", level.IMAGE ); if (fs.existsSync(oldImagePath)) fs.unlinkSync(oldImagePath); } level.IMAGE = saveFileToDisk( IMAGE, "IMAGE", ID_TOPIC || level.ID_TOPIC, ID_SECTION || level.ID_SECTION, level.ID_LEVEL ); } await level.save({ transaction }); await transaction.commit(); await updateOtherLevelsRoutes(req, res, next); response(200, level, "Level updated successfully", res); } catch (error) { console.log(error); clearFileBuffers({ AUDIO, IMAGE }); await transaction.rollback(); return response(500, null, "Internal Server Error", res); } }; export const deleteLevelById = async (req, res, next) => { const { id } = req.params; try { const level = await models.Level.findByPk(id); if (!level) { return response(404, null, "Level not found", res); } level.IS_DELETED = 1; await level.save(); await models.Exercise.update( { IS_DELETED: 1 }, { where: { ID_LEVEL: id } } ); req.body.newLevelId = level.ID_LEVEL; await updateOtherLevelsRoutesOnDelete(req, res, next); response( 200, null, "Level and related exercises soft deleted successfully", res ); } catch (error) { console.log(error); return response(500, null, "Internal Server Error", res); } }; export const deleteLevelFileById = async (req, res) => { const { id } = req.params; const { fileType } = req.body; if (!["audio", "image", "video"].includes(fileType)) { return response(400, null, "Invalid file type specified", res); } try { const level = await models.Level.findByPk(id); if (!level) { return response(404, null, "Level not found", res); } let filePath; let fileName; if (fileType === "audio" && level.AUDIO) { fileName = level.AUDIO; filePath = path.join( process.cwd(), "media/uploads/level/audio", fileName ); level.AUDIO = null; } else if (fileType === "image" && level.IMAGE) { fileName = level.IMAGE; filePath = path.join( process.cwd(), "media/uploads/level/image", fileName ); level.IMAGE = null; } else if (fileType === "video" && level.VIDEO) { level.VIDEO = null; } else { return response( 404, null, `${ fileType.charAt(0).toUpperCase() + fileType.slice(1) } file not found`, res ); } if (filePath && fs.existsSync(filePath)) { fs.unlinkSync(filePath); } await level.save(); response( 200, level, `${ fileType.charAt(0).toUpperCase() + fileType.slice(1) } file deleted successfully`, res ); } catch (error) { console.log(error); response(500, null, "Internal Server Error", res); } }; export const deleteLevelFilesById = async (req, res) => { const { id } = req.params; const { fileName } = req.body; const filePattern = /^(AUDIO|IMAGE)-([a-f0-9-]{36})-[a-f0-9]{32}\.(mp3|jpg|jpeg|png)$/; const match = fileName.match(filePattern); if (!match) { return response(400, null, "Invalid file name format", res); } const fileType = match[1]; const levelIdFromFile = match[2]; if (levelIdFromFile !== id) { return response(400, null, "Level ID in file name does not match", res); } try { const level = await models.Level.findByPk(id); if (!level) { return response(404, null, "Level not found", res); } let filePath; if (fileType === "AUDIO") { filePath = path.join( process.cwd(), "media/uploads/level/audio", fileName ); } else if (fileType === "IMAGE") { filePath = path.join( process.cwd(), "media/uploads/level/image", fileName ); } else { return response(400, null, "Invalid file type", res); } if (filePath && fs.existsSync(filePath)) { fs.unlinkSync(filePath); return response(200, null, `${fileType} file deleted successfully`, res); } else { return response(404, null, "File not found on the server", res); } } catch (error) { console.log(error); response(500, null, "Internal Server Error", res); } }; export const getPreviousLevel = async (req, res) => { try { const { next_learning } = req.params; const { ID } = req.user; const currentLevel = await models.Level.findByPk(next_learning, { where: { IS_DELETED: 0, }, include: [ { model: models.StdLearning, as: "stdLearning", attributes: [ "SCORE", "ID_STUDENT_LEARNING", "STUDENT_START", "STUDENT_FINISH", ], where: { ID: ID, }, required: false, order: [["STUDENT_FINISH", "DESC"]], limit: 1, }, ], attributes: { exclude: [ "ROUTE_1", "ROUTE_2", "ROUTE_3", "ROUTE_4", "ROUTE_5", "ROUTE_6", ], }, }); if (!currentLevel) { return res.status(404).json({ message: "Level not found" }); } const { NAME_LEVEL, ID_TOPIC } = currentLevel; const levelNumber = parseInt(NAME_LEVEL.replace("Level ", "")); if (isNaN(levelNumber)) { return res.status(400).json({ message: "Invalid level format" }); } const previousLevels = await models.Level.findAll({ where: { ID_TOPIC: ID_TOPIC, IS_DELETED: 0, NAME_LEVEL: { [models.Op.or]: [ { [models.Op.like]: "Pretest" }, { [models.Op.regexp]: `^Level [0-9]+$` }, ], }, [models.Op.or]: [ { IS_PRETEST: 1 }, { NAME_LEVEL: { [models.Op.lt]: `Level ${levelNumber}`, }, }, ], }, attributes: { exclude: [ "ROUTE_1", "ROUTE_2", "ROUTE_3", "ROUTE_4", "ROUTE_5", "ROUTE_6", ], }, include: [ { model: models.StdLearning, as: "stdLearning", attributes: [ "SCORE", "ID_STUDENT_LEARNING", "STUDENT_START", "STUDENT_FINISH", ], where: { ID: ID, }, required: false, order: [["STUDENT_START", "DESC"]], limit: 1, }, ], }); const previousLevelsWithScore = previousLevels.map((level) => { const SCORE = level.stdLearning && level.stdLearning.length > 0 ? level.stdLearning[0]?.SCORE : null; const ID_STUDENT_LEARNING = level.stdLearning && level.stdLearning.length > 0 ? level.stdLearning[0]?.ID_STUDENT_LEARNING : null; const levelJSON = level.toJSON(); delete levelJSON.stdLearning; return { ...levelJSON, ID_STUDENT_LEARNING, SCORE, }; }); const currentLevelWithScore = { ...currentLevel.toJSON(), ID_STUDENT_LEARNING: currentLevel.stdLearning && currentLevel.stdLearning.length > 0 ? currentLevel.stdLearning[0]?.ID_STUDENT_LEARNING : null, SCORE: currentLevel.stdLearning && currentLevel.stdLearning.length > 0 ? currentLevel.stdLearning[0]?.SCORE : null, }; delete currentLevelWithScore.stdLearning; const sortedLevels = previousLevelsWithScore.sort((a, b) => { if (a.NAME_LEVEL === "Pretest") return -1; if (b.NAME_LEVEL === "Pretest") return 1; const levelA = parseInt(a.NAME_LEVEL.replace("Level ", "")); const levelB = parseInt(b.NAME_LEVEL.replace("Level ", "")); return levelA - levelB; }); if (!sortedLevels.length && !currentLevelWithScore) { return res.status(404).json({ message: "No levels found" }); } const result = { currentLevel: currentLevelWithScore, previousLevels: sortedLevels, }; res.status(200).json({ message: "Success", data: result }); } catch (error) { console.error(error); res.status(500).json({ message: "Internal Server Error" }); } }; export const uploadLevelFile = async (req, res) => { const { levelId } = req.params; try { const level = await models.Level.findByPk(levelId); if (!level) { return response(404, null, "Level not found", res); } const filesToSave = req.filesToSave; if (!filesToSave || Object.keys(filesToSave).length === 0) { return response(400, null, "No files to upload", res); } const { ID_TOPIC, ID_SECTION } = level; const savedFiles = {}; Object.keys(filesToSave).forEach((key) => { let filename; if (key.startsWith("AUDIO")) { const audioFile = filesToSave[key]; filename = saveFileToDisk( audioFile, "AUDIO", ID_TOPIC, ID_SECTION, levelId ); } else if (key.startsWith("IMAGE")) { const imageFile = filesToSave[key]; filename = saveFileToDisk( imageFile, "IMAGE", ID_TOPIC, ID_SECTION, levelId ); } if (filename) { savedFiles[key] = filename; } }); if (Object.keys(savedFiles).length === 0) { return response(400, null, "Failed to save files", res); } return response(200, savedFiles, "Files uploaded successfully", res); } catch (error) { console.error(error); return response(500, null, "Internal server error", res); } }; export const getLevelFiles = async (req, res) => { const { levelId } = req.params; try { const audioFolderPath = path.join( process.cwd(), "media/uploads/level/audio" ); const imageFolderPath = path.join( process.cwd(), "media/uploads/level/image" ); const getFilesByLevelId = (folderPath, fileType) => { if (fs.existsSync(folderPath)) { const files = fs.readdirSync(folderPath); return files.filter((file) => file.startsWith(`${fileType}-${levelId}`) ); } return []; }; const audioFiles = getFilesByLevelId(audioFolderPath, "AUDIO"); const imageFiles = getFilesByLevelId(imageFolderPath, "IMAGE"); if (audioFiles.length === 0 && imageFiles.length === 0) { return response(404, null, "No files found for this level", res); } const levelFiles = { audioFiles, imageFiles, }; return response(200, levelFiles, "Files retrieved successfully", res); } catch (error) { console.error(error); return response(500, null, "Internal server error", res); } };