feat(ui): migrate to Tailwind UI and add result matrix tooling
This commit is contained in:
@@ -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,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;
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user