'use strict'; const { Op } = require('sequelize'); const mdl_Category = require('../../models/courses/categories.mdl'); const mdl_Users = require('../../models/users/users.mdl'); const { CourseProductCategory } = require('../../models/courses/courses.mdl'); const R = require('../../utils/response.util'); const logActivity = require('../../utils/logActivity.util'); const { paginate } = require('../../utils/paginate.util'); const { excludeAttributes: categoriesExclude, jsonbSchemas: categoriesSchemas, } = require('../../models/courses/categories.attributes'); const slugify = (str) => str.toLowerCase().trim().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, ''); exports.getCategories = async (req, res) => { try { const archived = req.query.archived === 'true'; const result = await paginate(mdl_Category, req, { excludeAttributes: categoriesExclude, jsonbSchemas: categoriesSchemas, context: archived ? 'archived' : 'list', auditOptions: { mdl_Users, parentAlias: 'Category' }, findOptions: archived ? { paranoid: false, where: { deletedAt: { [Op.ne]: null } } } : {}, }); return R.success(res, 'Categories retrieved.', result); } catch (err) { console.error('[ADMIN][CATEGORIES][GET ALL]', err); return R.error(res, 'Could not retrieve categories.', 500); } }; exports.getCategory = async (req, res) => { try { const row = await mdl_Category.findByPk(req.params.id, { paranoid: false }); if (!row) return R.error(res, 'Category not found.', 404); return R.success(res, 'Category retrieved.', row); } catch (err) { console.error('[ADMIN][CATEGORIES][GET ONE]', err); return R.error(res, 'Could not retrieve category.', 500); } }; exports.createCategory = async (req, res) => { try { const { name, description, is_active } = req.body; if (!name) return R.error(res, 'name is required.', 400); const slug = slugify(name); const row = await mdl_Category.create({ name, slug, description: description ?? null, is_active: is_active ?? true, createdBy: req.body.createdBy ?? req.user?.user_id ?? null, }); logActivity(req.user?.user_id, 'create_category', { entityType: 'category', entityId: row.category_id, details: { name: row.name } }); return R.success(res, 'Category created.', row, 201); } catch (err) { if (err.name === 'SequelizeUniqueConstraintError') return R.error(res, 'A category with that name already exists.', 409); console.error('[ADMIN][CATEGORIES][CREATE]', err); return R.error(res, 'Could not create category.', 500); } }; exports.updateCategory = async (req, res) => { try { const row = await mdl_Category.findByPk(req.params.id); if (!row) return R.error(res, 'Category not found.', 404); const { name, description, is_active } = req.body; const slug = name ? slugify(name) : row.slug; await row.update({ name: name ?? row.name, slug, description: description ?? row.description, is_active: is_active ?? row.is_active, updatedBy: req.body.updatedBy ?? req.user?.user_id ?? null, }); logActivity(req.user?.user_id, 'update_category', { entityType: 'category', entityId: row.category_id }); return R.success(res, 'Category updated.', row); } catch (err) { if (err.name === 'SequelizeUniqueConstraintError') return R.error(res, 'A category with that name already exists.', 409); console.error('[ADMIN][CATEGORIES][UPDATE]', err); return R.error(res, 'Could not update category.', 500); } }; exports.archiveCategory = async (req, res) => { try { const row = await mdl_Category.findByPk(req.params.id); if (!row) return R.error(res, 'Category not found.', 404); await row.update({ deletedBy: req.user?.user_id ?? null, is_active: false }); await row.destroy(); logActivity(req.user?.user_id, 'archive_category', { entityType: 'category', entityId: row.category_id }); return R.success(res, 'Category archived.'); } catch (err) { console.error('[ADMIN][CATEGORIES][ARCHIVE]', err); return R.error(res, 'Could not archive category.', 500); } }; exports.restoreCategory = async (req, res) => { try { const row = await mdl_Category.findByPk(req.params.id, { paranoid: false }); if (!row) return R.error(res, 'Category not found.', 404); if (!row.deletedAt) return R.error(res, 'Category is not archived.', 400); await row.restore(); await row.update({ deletedBy: null, is_active: true }); logActivity(req.user?.user_id, 'restore_category', { entityType: 'category', entityId: row.category_id }); return R.success(res, 'Category restored.', row); } catch (err) { console.error('[ADMIN][CATEGORIES][RESTORE]', err); return R.error(res, 'Could not restore category.', 500); } }; exports.bulkArchiveCategories = async (req, res) => { try { const { ids } = req.body; if (!Array.isArray(ids) || !ids.length) return R.error(res, 'No category IDs provided.', 400); const rows = await mdl_Category.findAll({ where: { id: ids } }); if (!rows.length) return R.error(res, 'No categories found.', 404); const activeIds = rows.map((r) => r.id); await Promise.all(rows.map((r) => r.update({ deletedBy: req.user?.user_id ?? null, is_active: false }))); await mdl_Category.destroy({ where: { id: activeIds } }); logActivity(req.user?.user_id, 'bulk_archive_categories', { entityType: 'category', details: { ids: activeIds, count: activeIds.length } }); return R.success(res, `${activeIds.length} categor${activeIds.length !== 1 ? 'ies' : 'y'} archived.`, { archived_ids: activeIds, skipped_ids: ids.filter((id) => !activeIds.includes(id)), }); } catch (err) { console.error('[ADMIN][CATEGORIES][BULK ARCHIVE]', err); return R.error(res, 'Could not archive categories.', 500); } }; exports.getCategoryPermanentDeleteImpact = async (req, res) => { try { const row = await mdl_Category.findByPk(req.params.id, { paranoid: false }); if (!row) return R.error(res, 'Category not found.', 404); const course_count = await CourseProductCategory.count({ where: { category_id: req.params.id } }); return R.success(res, 'Category permanent-delete impact retrieved.', { course_count }); } catch (err) { console.error('[ADMIN][CATEGORIES][PERMANENT DELETE IMPACT]', err); return R.error(res, 'Could not retrieve category permanent-delete impact.', 500); } }; exports.permanentlyDeleteCategory = async (req, res) => { try { const row = await mdl_Category.findByPk(req.params.id, { paranoid: false }); if (!row) return R.error(res, 'Category not found.', 404); if (!row.deletedAt) return R.error(res, 'Category must be archived before it can be permanently deleted.', 400); // course_product_categories.category_id has no DB-level cascade (only course_id does) — // clean up the junction rows explicitly or they'd be left orphaned. await CourseProductCategory.destroy({ where: { category_id: row.id } }); await row.destroy({ force: true }); logActivity(req.user?.user_id, 'permanently_delete_category', { entityType: 'category', entityId: row.id, details: { name: row.name } }); return R.success(res, 'Category permanently deleted.'); } catch (err) { console.error('[ADMIN][CATEGORIES][PERMANENT DELETE]', err); return R.error(res, 'Could not permanently delete category.', 500); } }; exports.bulkRestoreCategories = async (req, res) => { try { const { ids } = req.body; if (!Array.isArray(ids) || !ids.length) return R.error(res, 'No category IDs provided.', 400); const rows = await mdl_Category.findAll({ where: { id: ids }, paranoid: false }); if (!rows.length) return R.error(res, 'No categories found.', 404); const archivedRows = rows.filter((r) => r.deletedAt); if (!archivedRows.length) return R.error(res, 'All selected categories are already active.', 400); const archivedIds = archivedRows.map((r) => r.id); await mdl_Category.restore({ where: { id: archivedIds } }); await mdl_Category.update({ deletedBy: null, is_active: true }, { where: { id: archivedIds }, paranoid: false }); logActivity(req.user?.user_id, 'bulk_restore_categories', { entityType: 'category', details: { ids: archivedIds, count: archivedIds.length } }); return R.success(res, `${archivedIds.length} categor${archivedIds.length !== 1 ? 'ies' : 'y'} restored.`, { restored_ids: archivedIds, skipped_ids: ids.filter((id) => !archivedIds.includes(id)), }); } catch (err) { console.error('[ADMIN][CATEGORIES][BULK RESTORE]', err); return R.error(res, 'Could not restore categories.', 500); } }; exports.bulkPermanentlyDeleteCategories = async (req, res) => { try { const { ids } = req.body; if (!Array.isArray(ids) || !ids.length) return R.error(res, 'No category IDs provided.', 400); const rows = await mdl_Category.findAll({ where: { id: ids }, paranoid: false }); if (!rows.length) return R.error(res, 'No categories found.', 404); const archivedRows = rows.filter((r) => r.deletedAt); if (!archivedRows.length) return R.error(res, 'All selected categories must be archived before they can be permanently deleted.', 400); const archivedIds = archivedRows.map((r) => r.id); await CourseProductCategory.destroy({ where: { category_id: archivedIds } }); await mdl_Category.destroy({ where: { id: archivedIds }, force: true }); logActivity(req.user?.user_id, 'bulk_permanently_delete_categories', { entityType: 'category', details: { ids: archivedIds, count: archivedIds.length } }); return R.success(res, `${archivedIds.length} categor${archivedIds.length !== 1 ? 'ies' : 'y'} permanently deleted.`, { deleted_ids: archivedIds, skipped_ids: ids.filter((id) => !archivedIds.includes(id)), }); } catch (err) { console.error('[ADMIN][CATEGORIES][BULK PERMANENT DELETE]', err); return R.error(res, 'Could not permanently delete categories.', 500); } };