279 lines
9.1 KiB
JavaScript
279 lines
9.1 KiB
JavaScript
const express = require('express');
|
|
const multer = require('multer');
|
|
const { authRequired } = require('../middleware/auth');
|
|
const { requireRole } = require('../middleware/rbac');
|
|
const { resolveTenant } = require('../middleware/tenant');
|
|
const { createSurveyPeriod, activateSurvey, closeSurvey } = require('../services/surveyService');
|
|
const { importUsersFromExcel } = require('../services/importService');
|
|
const { getParticipationSummary } = require('../services/participationService');
|
|
const {
|
|
listQuestions,
|
|
exportQuestionsExcel,
|
|
exportQuestionsTemplate,
|
|
importQuestionsExcel,
|
|
listQuestionSets,
|
|
listDimensions,
|
|
createDimension,
|
|
createQuestionSetFromCurrent,
|
|
createQuestionSetEmpty,
|
|
addQuestionToSet
|
|
} = require('../services/questionService');
|
|
const {
|
|
listResultModel,
|
|
createResultModel,
|
|
updateResultModel,
|
|
exportResultModelTemplate,
|
|
importResultModelExcel
|
|
} = require('../services/resultModelService');
|
|
|
|
const upload = multer();
|
|
const router = express.Router();
|
|
|
|
router.use(authRequired, requireRole('admin', 'superadmin'), resolveTenant);
|
|
|
|
router.get('/survey-periods', async (req, res, next) => {
|
|
try {
|
|
const [rows] = await req.tenantPool.query('SELECT * FROM survey_periods ORDER BY id DESC');
|
|
res.json(rows);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
|
|
router.post('/survey-periods', async (req, res, next) => {
|
|
try {
|
|
const { year, month, fecha_inicio, fecha_fin } = req.body;
|
|
if (!year || !month || !fecha_inicio || !fecha_fin) return res.status(400).json({ error: 'Campos requeridos' });
|
|
const period = await createSurveyPeriod(req.tenantPool, { year, month, fecha_inicio, fecha_fin });
|
|
res.json(period);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
|
|
router.patch('/survey-periods/:id/activate', async (req, res, next) => {
|
|
try {
|
|
await activateSurvey(req.tenantPool, req.params.id);
|
|
res.json({ ok: true });
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
|
|
router.patch('/survey-periods/:id/close', async (req, res, next) => {
|
|
try {
|
|
const result = await closeSurvey(req.tenantPool, req.params.id);
|
|
res.json({ ok: true, result });
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
|
|
router.get('/dashboard', async (req, res, next) => {
|
|
try {
|
|
const [[current]] = await req.tenantPool.query(
|
|
'SELECT score, classification, participation_rate FROM survey_results WHERE scope = "global" ORDER BY survey_period_id DESC LIMIT 1'
|
|
);
|
|
const [dimensions] = await req.tenantPool.query(
|
|
'SELECT d.name, r.score, r.classification FROM survey_results r JOIN dimensions d ON d.id = r.dimension_id WHERE r.scope = "dimension" AND r.survey_period_id = (SELECT MAX(id) FROM survey_periods WHERE estado = "cerrada")'
|
|
);
|
|
const [history] = await req.tenantPool.query(
|
|
'SELECT sp.codigo_medicion, r.score FROM survey_results r JOIN survey_periods sp ON sp.id = r.survey_period_id WHERE r.scope = "global" ORDER BY sp.id DESC'
|
|
);
|
|
const participation = await getParticipationSummary(req.tenantPool);
|
|
res.json({ current, dimensions, history, participation });
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
|
|
router.post('/users/import', upload.single('file'), async (req, res, next) => {
|
|
try {
|
|
if (!req.file) return res.status(400).json({ error: 'Archivo requerido' });
|
|
const result = await importUsersFromExcel(req.file.buffer, req.tenantPool);
|
|
res.json(result);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
|
|
router.get('/questions', async (req, res, next) => {
|
|
try {
|
|
const rows = await listQuestions(req.tenantPool);
|
|
res.json(rows);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
|
|
router.get('/questions/export', async (req, res, next) => {
|
|
try {
|
|
const buffer = await exportQuestionsExcel(req.tenantPool);
|
|
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
|
res.setHeader('Content-Disposition', 'attachment; filename=\"preguntas.xlsx\"');
|
|
res.send(buffer);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
|
|
router.get('/questions/template', async (req, res, next) => {
|
|
try {
|
|
const buffer = await exportQuestionsTemplate();
|
|
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
|
res.setHeader('Content-Disposition', 'attachment; filename=\"preguntas_template.xlsx\"');
|
|
res.send(buffer);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
|
|
router.post('/questions/import', upload.single('file'), async (req, res, next) => {
|
|
try {
|
|
if (!req.file) return res.status(400).json({ error: 'Archivo requerido' });
|
|
const setId = req.body?.set_id ? Number(req.body.set_id) : null;
|
|
const result = await importQuestionsExcel(req.file.buffer, req.tenantPool, setId);
|
|
res.json(result);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
|
|
router.get('/question-sets', async (req, res, next) => {
|
|
try {
|
|
const rows = await listQuestionSets(req.tenantPool);
|
|
res.json(rows);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
|
|
router.post('/question-sets', async (req, res, next) => {
|
|
try {
|
|
const { name, empty } = req.body;
|
|
const result = empty ? await createQuestionSetEmpty(req.tenantPool, name) : await createQuestionSetFromCurrent(req.tenantPool, name);
|
|
res.json(result);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
|
|
router.delete('/question-sets/:id', async (req, res, next) => {
|
|
try {
|
|
const [rows] = await req.tenantPool.query('SELECT id, code, is_active FROM question_sets WHERE id = ?', [req.params.id]);
|
|
if (!rows.length) return res.status(404).json({ error: 'Set no encontrado' });
|
|
const setRow = rows[0];
|
|
if (setRow.is_active) {
|
|
return res.status(400).json({ error: 'No se puede eliminar un set activo' });
|
|
}
|
|
const [[{ used }]] = await req.tenantPool.query('SELECT COUNT(*) AS used FROM survey_periods WHERE question_set_code = ?', [setRow.code]);
|
|
if (used > 0) {
|
|
return res.status(400).json({ error: 'No se puede eliminar: ya tiene mediciones' });
|
|
}
|
|
await req.tenantPool.query('DELETE FROM question_set_questions WHERE question_set_id = ?', [setRow.id]);
|
|
await req.tenantPool.query('DELETE FROM question_sets WHERE id = ?', [setRow.id]);
|
|
res.json({ ok: true });
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
|
|
router.get('/question-sets/:id/questions', async (req, res, next) => {
|
|
try {
|
|
const [rows] = await req.tenantPool.query(
|
|
'SELECT qs.question_code, qs.text_snapshot, d.name AS dimension FROM question_set_questions qs JOIN dimensions d ON d.id = qs.dimension_id WHERE qs.question_set_id = ? ORDER BY qs.sort_order',
|
|
[req.params.id]
|
|
);
|
|
res.json(rows);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
|
|
router.get('/result-model', async (req, res, next) => {
|
|
try {
|
|
const rows = await listResultModel(req.tenantPool);
|
|
res.json(rows);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
|
|
router.post('/result-model', async (req, res, next) => {
|
|
try {
|
|
const { dimension, variable, subvariable, score_value, max_score, sort_order } = req.body;
|
|
if (!dimension || !variable || !subvariable) {
|
|
return res.status(400).json({ error: 'dimension, variable y subvariable requeridos' });
|
|
}
|
|
const result = await createResultModel(req.tenantPool, { dimension, variable, subvariable, score_value, max_score, sort_order });
|
|
res.json(result);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
|
|
router.patch('/result-model/:id', async (req, res, next) => {
|
|
try {
|
|
const { score_value, max_score, sort_order } = req.body;
|
|
await updateResultModel(req.tenantPool, req.params.id, { score_value, max_score, sort_order });
|
|
res.json({ ok: true });
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
|
|
router.get('/result-model/template', async (req, res, next) => {
|
|
try {
|
|
const buffer = await exportResultModelTemplate();
|
|
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
|
res.setHeader('Content-Disposition', 'attachment; filename=\"clima_result_model.xlsx\"');
|
|
res.send(buffer);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
|
|
router.post('/result-model/import', upload.single('file'), async (req, res, next) => {
|
|
try {
|
|
if (!req.file) return res.status(400).json({ error: 'Archivo requerido' });
|
|
const mode = req.body?.mode || 'replace';
|
|
const result = await importResultModelExcel(req.file.buffer, req.tenantPool, mode);
|
|
res.json(result);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
|
|
router.get('/dimensions', async (req, res, next) => {
|
|
try {
|
|
const rows = await listDimensions(req.tenantPool);
|
|
res.json(rows);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
|
|
router.post('/dimensions', async (req, res, next) => {
|
|
try {
|
|
const { name, code, description } = req.body;
|
|
if (!name) return res.status(400).json({ error: 'Nombre requerido' });
|
|
const result = await createDimension(req.tenantPool, { name, code, description });
|
|
res.json(result);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
|
|
router.post('/question-sets/:id/questions', async (req, res, next) => {
|
|
try {
|
|
const { dimension_id, text } = req.body;
|
|
if (!dimension_id || !text) return res.status(400).json({ error: 'dimension_id y text requeridos' });
|
|
const result = await addQuestionToSet(req.tenantPool, { questionSetId: req.params.id, dimensionId: dimension_id, text });
|
|
res.json(result);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|