feat(ui): migrate to Tailwind UI and add result matrix tooling

This commit is contained in:
2026-03-03 22:54:06 -05:00
parent 1592434192
commit 87f52777c4
21 changed files with 1575 additions and 263 deletions
+61
View File
@@ -18,6 +18,13 @@ const {
createQuestionSetEmpty,
addQuestionToSet
} = require('../services/questionService');
const {
listResultModel,
createResultModel,
updateResultModel,
exportResultModelTemplate,
importResultModelExcel
} = require('../services/resultModelService');
const upload = multer();
const router = express.Router();
@@ -183,6 +190,60 @@ router.get('/question-sets/:id/questions', async (req, res, next) => {
}
});
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);
@@ -0,0 +1,21 @@
const path = require('path');
const ExcelJS = require('exceljs');
async function main() {
const workbook = new ExcelJS.Workbook();
const sheet = workbook.addWorksheet('Resultados');
sheet.addRow(['dimension', 'variable', 'subvariable', 'max_score', 'sort_order']);
sheet.addRow(['CREDIBILIDAD', 'Comunicación', 'Accesibilidad', 100, 1]);
sheet.addRow(['CREDIBILIDAD', 'Comunicación', 'Información', 100, 2]);
sheet.addRow(['RESPETO', 'Apoyo', 'Desarrollo', 100, 3]);
sheet.addRow(['IMPARCIALIDAD', 'Equidad', 'Ausencia de favoritismo', 100, 4]);
const outPath = path.resolve(__dirname, '../../../templates/plantilla_matriz_clima.xlsx');
await workbook.xlsx.writeFile(outPath);
console.log('Template generado:', outPath);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
@@ -0,0 +1,89 @@
const ExcelJS = require('exceljs');
async function listResultModel(pool) {
const [rows] = await pool.query(
'SELECT id, dimension, variable, subvariable, score_value, max_score, sort_order FROM climate_result_model ORDER BY sort_order, id'
);
return rows;
}
async function createResultModel(pool, data) {
const { dimension, variable, subvariable, score_value, max_score, sort_order } = data;
const [res] = await pool.query(
'INSERT INTO climate_result_model (dimension, variable, subvariable, score_value, max_score, sort_order) VALUES (?, ?, ?, ?, ?, ?)',
[dimension, variable, subvariable, score_value || null, max_score || null, sort_order || 0]
);
return { id: res.insertId };
}
async function updateResultModel(pool, id, data) {
const { score_value, max_score, sort_order } = data;
await pool.query(
'UPDATE climate_result_model SET score_value = COALESCE(?, score_value), max_score = COALESCE(?, max_score), sort_order = COALESCE(?, sort_order) WHERE id = ?',
[score_value, max_score, sort_order, id]
);
}
async function exportResultModelTemplate() {
const workbook = new ExcelJS.Workbook();
const sheet = workbook.addWorksheet('Resultados');
sheet.addRow(['dimension', 'variable', 'subvariable', 'score_value', 'max_score', 'sort_order']);
sheet.addRow(['CREDIBILIDAD', 'Comunicación', 'Accesibilidad', 37.33, '', 1]);
return workbook.xlsx.writeBuffer();
}
async function importResultModelExcel(buffer, pool, mode = 'replace') {
const workbook = new ExcelJS.Workbook();
await workbook.xlsx.load(buffer);
const sheet = workbook.worksheets[0];
if (!sheet) return { processed: 0, errors: ['No hay hoja en el archivo'] };
const headers = {};
const headerRow = sheet.getRow(1);
headerRow.eachCell((cell, idx) => {
const key = String(cell.value || '').trim().toLowerCase();
if (key) headers[key] = idx;
});
const required = ['dimension', 'variable', 'subvariable'];
for (const r of required) {
if (!headers[r]) return { processed: 0, errors: [`Falta columna ${r}`] };
}
if (mode === 'replace') {
await pool.query('DELETE FROM climate_result_model');
}
let processed = 0;
const errors = [];
for (let i = 2; i <= sheet.rowCount; i++) {
const row = sheet.getRow(i);
const dimension = String(row.getCell(headers.dimension).value || '').trim();
const variable = String(row.getCell(headers.variable).value || '').trim();
const subvariable = String(row.getCell(headers.subvariable).value || '').trim();
if (!dimension || !variable || !subvariable) {
errors.push(`Fila ${i}: dimension, variable y subvariable son obligatorios`);
continue;
}
const score_value = headers.score_value ? row.getCell(headers.score_value).value : null;
const max_score = headers.max_score ? row.getCell(headers.max_score).value : null;
const sort_order = headers.sort_order ? row.getCell(headers.sort_order).value : null;
await pool.query(
'INSERT INTO climate_result_model (dimension, variable, subvariable, score_value, max_score, sort_order) VALUES (?, ?, ?, ?, ?, ?)',
[dimension, variable, subvariable, score_value || null, max_score || null, sort_order || 0]
);
processed += 1;
}
return { processed, errors };
}
module.exports = {
listResultModel,
createResultModel,
updateResultModel,
exportResultModelTemplate,
importResultModelExcel
};
+1
View File
@@ -1,4 +1,5 @@
SET FOREIGN_KEY_CHECKS=0;
SOURCE tenant_schema.sql;
SOURCE seed_questions.sql;
SOURCE seed_result_model.sql;
SET FOREIGN_KEY_CHECKS=1;
@@ -75,3 +75,47 @@ UPDATE survey_questions sq JOIN questions q ON q.id = sq.question_id SET sq.ques
UPDATE responses r JOIN questions q ON q.id=r.question_id SET r.question_code = q.code WHERE r.question_code IS NULL OR r.question_code='';
-- Add unique constraints if not exists (run separately if needed)
-- Result model table and seed
CREATE TABLE IF NOT EXISTS climate_result_model (
id INT AUTO_INCREMENT PRIMARY KEY,
dimension VARCHAR(120) NOT NULL,
variable VARCHAR(120) NOT NULL,
subvariable VARCHAR(160) NOT NULL,
score_value DECIMAL(6,2) NULL,
max_score DECIMAL(6,2) NULL,
sort_order INT NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO climate_result_model (dimension, variable, subvariable, score_value, max_score, sort_order)
SELECT * FROM (
SELECT 'CREDIBILIDAD', 'Comunicación', 'Accesibilidad', NULL, 100, 1 UNION ALL
SELECT 'CREDIBILIDAD', 'Comunicación', 'Información', NULL, 100, 2 UNION ALL
SELECT 'CREDIBILIDAD', 'Integridad', 'Confiabilidad', NULL, 100, 3 UNION ALL
SELECT 'CREDIBILIDAD', 'Integridad', 'Honestidad', NULL, 100, 4 UNION ALL
SELECT 'CREDIBILIDAD', 'Liderazgo', 'Coordinación', NULL, 100, 5 UNION ALL
SELECT 'CREDIBILIDAD', 'Liderazgo', 'Delegación', NULL, 100, 6 UNION ALL
SELECT 'CREDIBILIDAD', 'Liderazgo', 'Visión', NULL, 100, 7 UNION ALL
SELECT 'RESPETO', 'Apoyo', 'Desarrollo', NULL, 100, 8 UNION ALL
SELECT 'RESPETO', 'Apoyo', 'Reconocimiento', NULL, 100, 9 UNION ALL
SELECT 'RESPETO', 'Cuidado', 'Entorno de Trabajo', NULL, 100, 10 UNION ALL
SELECT 'RESPETO', 'Cuidado', 'Vida Personal', NULL, 100, 11 UNION ALL
SELECT 'RESPETO', 'Sinergia de Equipo', 'Participación', NULL, 100, 12 UNION ALL
SELECT 'IMPARCIALIDAD', 'Equidad', 'Ausencia de favoritismo', NULL, 100, 13 UNION ALL
SELECT 'IMPARCIALIDAD', 'Equidad', 'Equidad en el trato', NULL, 100, 14 UNION ALL
SELECT 'IMPARCIALIDAD', 'Equidad', 'Equidad en la remuneración', NULL, 100, 15 UNION ALL
SELECT 'IMPARCIALIDAD', 'Justicia', 'Capacidad de apelación', NULL, 100, 16 UNION ALL
SELECT 'IMPARCIALIDAD', 'Justicia', 'Justicia en el trato a las personas', NULL, 100, 17 UNION ALL
SELECT 'CAMARADERIA', 'Fraternidad', 'Compañerismo', NULL, 100, 18 UNION ALL
SELECT 'CAMARADERIA', 'Hospitalidad', 'Hospitalidad de personas', NULL, 100, 19 UNION ALL
SELECT 'CAMARADERIA', 'Hospitalidad', 'Hospitalidad del lugar', NULL, 100, 20 UNION ALL
SELECT 'CAMARADERIA', 'Hospitalidad', 'Sentido de equipo', NULL, 100, 21 UNION ALL
SELECT 'PERTENENCIA', 'Orgullo', 'Orgullo del Equipo', NULL, 100, 22 UNION ALL
SELECT 'PERTENENCIA', 'Orgullo', 'Orgullo del Trabajo', NULL, 100, 23 UNION ALL
SELECT 'PERTENENCIA', 'Orgullo', 'Orgullo por la Institución', NULL, 100, 24
) AS data
WHERE NOT EXISTS (SELECT 1 FROM climate_result_model);
UPDATE climate_result_model SET max_score = 100 WHERE max_score IS NULL;
UPDATE climate_result_model SET score_value = NULL;
+11
View File
@@ -157,3 +157,14 @@ CREATE TABLE IF NOT EXISTS survey_rules (
rule_json JSON NOT NULL,
is_active TINYINT(1) NOT NULL DEFAULT 1
);
CREATE TABLE IF NOT EXISTS climate_result_model (
id INT AUTO_INCREMENT PRIMARY KEY,
dimension VARCHAR(120) NOT NULL,
variable VARCHAR(120) NOT NULL,
subvariable VARCHAR(160) NOT NULL,
score_value DECIMAL(6,2) NULL,
max_score DECIMAL(6,2) NULL,
sort_order INT NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);