const path = require('path'); const Qualification = require('../models/qualification'); const Department = require('../models/department'); const Level = require('../models/level'); const writeAuditLog = require('../audit/writeAuditLog'); const AUDIT_ACTIONS = require('../constants/auditAction'); const { generateSignedUrl } = require('../utils/signedUrl'); function normalizePath(filePath) { if (!filePath) return undefined; return path.basename(filePath.replace(/\\/g, '/')); } // GET /admin/qualification exports.index = async (req, res) => { try { const { search, status } = req.query; const filter = {}; if (search) { filter.$or = [ { qualification_number: { $regex: search, $options: 'i' } }, { student_name: { $regex: search, $options: 'i' } } ]; } if (status) filter.status = status; const [qualifications, departments, levels] = await Promise.all([ Qualification.find(filter).populate('department level').sort({ createdAt: -1 }), Department.find(), Level.find() ]); res.render('admin/qualification/index', { qualifications, departments, levels, query: req.query, user: req.session.user, layout: 'layouts/admin', title: 'Qualifications' }); } catch (err) { console.error(err); req.flash('error', 'Error loading qualifications'); res.redirect('/admin/dashboard'); } }; // GET /admin/qualification/create exports.createForm = async (req, res) => { try { const [departments, levels] = await Promise.all([Department.find(), Level.find()]); res.render('admin/qualification/create', { departments, levels, user: req.session.user, layout: 'layouts/admin', title: 'Create Qualification' }); } catch (err) { req.flash('error', 'Error'); res.redirect('/admin/qualification'); } }; // POST /admin/qualification/create exports.create = async (req, res) => { try { const data = { ...req.body }; const imgFiles = req.files?.degree_image; if (imgFiles?.length) data.degree_image = imgFiles.map(f => normalizePath(f.path)); const qual = new Qualification(data); await qual.save(); await writeAuditLog({ model: 'Qualification', documentId: qual._id, action: AUDIT_ACTIONS.CREATE_QUALIFICATION, before: null, after: qual.toObject(), req }); req.flash('success', 'Qualification created'); res.redirect('/admin/qualification'); } catch (err) { console.error(err); try { const [departments, levels] = await Promise.all([Department.find(), Level.find()]); res.render('admin/qualification/create', { error: err.message, formData: req.body, departments, levels, user: req.session.user, layout: 'layouts/admin', title: 'Create Qualification' }); } catch { req.flash('error', err.message); res.redirect('/admin/qualification'); } } }; // GET /admin/qualification/:id/edit exports.editForm = async (req, res) => { try { const qual = await Qualification.findById(req.params.id).populate('department level'); if (!qual) { req.flash('error', 'Not found'); return res.redirect('/admin/qualification'); } const [departments, levels] = await Promise.all([Department.find(), Level.find()]); res.render('admin/qualification/edit', { qual, departments, levels, user: req.session.user, layout: 'layouts/admin', title: 'Edit Qualification' }); } catch (err) { req.flash('error', 'Error'); res.redirect('/admin/qualification'); } }; // POST /admin/qualification/:id/edit exports.update = async (req, res) => { try { const qual = await Qualification.findById(req.params.id); if (!qual) { req.flash('error', 'Not found'); return res.redirect('/admin/qualification'); } const before = qual.toObject(); const fields = ['qualification_number','student_name','program_name','department','level', 'issued_date','status','passport_number','address','topic_name','topic_short_desc']; fields.forEach(f => { if (req.body[f] !== undefined) qual[f] = req.body[f]; }); const imgFiles = req.files?.degree_image; if (imgFiles?.length) qual.degree_image = imgFiles.map(f => normalizePath(f.path)); await qual.save(); await writeAuditLog({ model: 'Qualification', documentId: qual._id, action: AUDIT_ACTIONS.UPDATE_QUALIFICATION, before, after: qual.toObject(), req }); req.flash('success', 'Qualification updated'); res.redirect('/admin/qualification'); } catch (err) { req.flash('error', err.message); res.redirect('back'); } }; // POST /admin/qualification/:id/delete exports.destroy = async (req, res) => { try { const qual = await Qualification.findById(req.params.id); if (!qual) { req.flash('error', 'Not found'); return res.redirect('/admin/qualification'); } await writeAuditLog({ model: 'Qualification', documentId: qual._id, action: AUDIT_ACTIONS.DELETE_QUALIFICATION, before: qual.toObject(), after: null, req }); await qual.deleteOne(); req.flash('success', 'Qualification deleted'); res.redirect('/admin/qualification'); } catch (err) { req.flash('error', 'Error deleting'); res.redirect('/admin/qualification'); } }; // GET /api/verify-degree/:degree_id?api_key=xxx exports.apiVerify = async (req, res) => { try { const qual = await Qualification.findOne({ qualification_number: { $regex: new RegExp('^' + req.params.degree_id + '$', 'i') } }).populate('department level'); if (!qual) return res.status(404).json({ error: 'Degree not found' }); if (qual.status === 'revoked') return res.status(404).json({ error: 'Degree has been revoked' }); const baseUrl = process.env.BACKEND_URL ?? `${req.protocol}://${req.get('host')}`; const buildUrl = (files) => { if (!files?.length) return undefined; return files.map(f => generateSignedUrl(baseUrl, path.basename(f))); }; const response = { full_name: qual.student_name, program_name: qual.program_name, degree_id: qual.qualification_number, }; if (qual.passport_number) response.passport_number = qual.passport_number; if (qual.address) response.address = qual.address; const imgs = buildUrl(qual.degree_image); if (imgs) response.degree_image = imgs; if (qual.topic_name) { response.topic_name = qual.topic_name; if (qual.topic_short_desc) response.topic_short_desc = qual.topic_short_desc; } return res.json(response); } catch (err) { console.error(err); return res.status(500).json({ error: 'Internal server error' }); } };