Skip to content

Commit 314c2c9

Browse files
committed
feat(backend): bloquear cierre automático si faltan notas y exponer alertas de pendientes
Integra validación previa con SP antes del cierre de período. Si la validación falla, el cierre no se ejecuta. Agrega endpoint de alertas de notas faltantes para docentes y lo registra en server.
1 parent 25c6bbf commit 314c2c9

4 files changed

Lines changed: 179 additions & 23 deletions

File tree

controllers/alertas.controller.js

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
const { sequelize } = require('../config/sequelize.config')
2+
3+
const getNotasFaltantes = async (req, res) => {
4+
try {
5+
const nroCedula = req.user.nroCedula
6+
7+
// 1) Buscar el periodo activo
8+
const [[periodo]] = await sequelize.query(
9+
`SELECT ID, fecha_fin FROM periodos_academicos WHERE estado = 'Activo' LIMIT 1`
10+
)
11+
12+
if (!periodo) {
13+
return res.json({ tiene_faltantes: false, mensaje: 'No hay período activo' })
14+
}
15+
16+
const hoy = new Date()
17+
const fechaFin = new Date(periodo.fecha_fin)
18+
const diasRestantes = Math.ceil((fechaFin - hoy) / (1000 * 60 * 60 * 24))
19+
20+
// 2) Contar inscripciones asignadas a este docente en el periodo activo
21+
const [[totales]] = await sequelize.query(
22+
`SELECT COUNT(i.ID) AS total
23+
FROM inscripciones i
24+
INNER JOIN asignaciones a ON a.ID = i.ID_asignacion
25+
WHERE a.nroCedula_docente = :cedula
26+
AND a.ID_periodo_academico = :periodoId`,
27+
{ replacements: { cedula: nroCedula, periodoId: periodo.ID } }
28+
)
29+
30+
const totalInscripciones = parseInt(totales.total, 10)
31+
if (totalInscripciones === 0) {
32+
return res.json({ tiene_faltantes: false, mensaje: 'Sin asignaciones en el período activo' })
33+
}
34+
35+
// 3) Contar inscripciones REGULARES con parciales incompletos
36+
const [[regularParciales]] = await sequelize.query(
37+
`SELECT COUNT(DISTINCT i.ID) AS faltantes
38+
FROM inscripciones i
39+
INNER JOIN asignaciones a ON a.ID = i.ID_asignacion
40+
INNER JOIN matriculas m ON m.ID = i.ID_matricula
41+
WHERE a.nroCedula_docente = :cedula
42+
AND a.ID_periodo_academico = :periodoId
43+
AND m.nivel COLLATE utf8mb4_unicode_ci NOT LIKE '%Básico Elemental%'
44+
AND (
45+
SELECT COUNT(*)
46+
FROM calificaciones_parciales cp
47+
WHERE cp.ID_inscripcion = i.ID
48+
AND cp.insumo1 IS NOT NULL AND cp.insumo2 IS NOT NULL AND cp.evaluacion IS NOT NULL
49+
) < 4`,
50+
{ replacements: { cedula: nroCedula, periodoId: periodo.ID } }
51+
)
52+
53+
// 4) Contar inscripciones REGULARES con quimestrales incompletos
54+
const [[regularQuimestrales]] = await sequelize.query(
55+
`SELECT COUNT(DISTINCT i.ID) AS faltantes
56+
FROM inscripciones i
57+
INNER JOIN asignaciones a ON a.ID = i.ID_asignacion
58+
INNER JOIN matriculas m ON m.ID = i.ID_matricula
59+
WHERE a.nroCedula_docente = :cedula
60+
AND a.ID_periodo_academico = :periodoId
61+
AND m.nivel COLLATE utf8mb4_unicode_ci NOT LIKE '%Básico Elemental%'
62+
AND (
63+
SELECT COUNT(*)
64+
FROM calificaciones_quimestrales cq
65+
WHERE cq.ID_inscripcion = i.ID
66+
AND cq.examen IS NOT NULL
67+
) < 2`,
68+
{ replacements: { cedula: nroCedula, periodoId: periodo.ID } }
69+
)
70+
71+
// 5) Contar inscripciones BE con parciales incompletos
72+
const [[beParciales]] = await sequelize.query(
73+
`SELECT COUNT(DISTINCT i.ID) AS faltantes
74+
FROM inscripciones i
75+
INNER JOIN asignaciones a ON a.ID = i.ID_asignacion
76+
INNER JOIN matriculas m ON m.ID = i.ID_matricula
77+
WHERE a.nroCedula_docente = :cedula
78+
AND a.ID_periodo_academico = :periodoId
79+
AND m.nivel COLLATE utf8mb4_unicode_ci LIKE '%Básico Elemental%'
80+
AND (
81+
SELECT COUNT(*)
82+
FROM calificaciones_parciales_be cpbe
83+
WHERE cpbe.ID_inscripcion = i.ID
84+
AND cpbe.insumo1 IS NOT NULL AND cpbe.insumo2 IS NOT NULL AND cpbe.evaluacion IS NOT NULL
85+
) < 4`,
86+
{ replacements: { cedula: nroCedula, periodoId: periodo.ID } }
87+
)
88+
89+
// 6) Contar inscripciones BE con quimestrales incompletos
90+
const [[beQuimestrales]] = await sequelize.query(
91+
`SELECT COUNT(DISTINCT i.ID) AS faltantes
92+
FROM inscripciones i
93+
INNER JOIN asignaciones a ON a.ID = i.ID_asignacion
94+
INNER JOIN matriculas m ON m.ID = i.ID_matricula
95+
WHERE a.nroCedula_docente = :cedula
96+
AND a.ID_periodo_academico = :periodoId
97+
AND m.nivel COLLATE utf8mb4_unicode_ci LIKE '%Básico Elemental%'
98+
AND (
99+
SELECT COUNT(*)
100+
FROM calificaciones_quimestrales_be cqbe
101+
WHERE cqbe.ID_inscripcion = i.ID
102+
AND cqbe.examen IS NOT NULL
103+
) < 2`,
104+
{ replacements: { cedula: nroCedula, periodoId: periodo.ID } }
105+
)
106+
107+
const totalFaltantes =
108+
parseInt(regularParciales.faltantes, 10) +
109+
parseInt(regularQuimestrales.faltantes, 10) +
110+
parseInt(beParciales.faltantes, 10) +
111+
parseInt(beQuimestrales.faltantes, 10)
112+
113+
if (totalFaltantes === 0) {
114+
return res.json({ tiene_faltantes: false })
115+
}
116+
117+
// 7) Calcular severidad según días al cierre
118+
let severidad
119+
if (diasRestantes > 15) severidad = 'suave'
120+
else if (diasRestantes > 7) severidad = 'fuerte'
121+
else if (diasRestantes > 3) severidad = 'critico'
122+
else severidad = 'bloqueante'
123+
124+
return res.json({
125+
tiene_faltantes: true,
126+
total_faltantes: totalFaltantes,
127+
dias_restantes: diasRestantes,
128+
severidad,
129+
desglose: {
130+
regular_parciales: parseInt(regularParciales.faltantes, 10),
131+
regular_quimestrales: parseInt(regularQuimestrales.faltantes, 10),
132+
be_parciales: parseInt(beParciales.faltantes, 10),
133+
be_quimestrales: parseInt(beQuimestrales.faltantes, 10),
134+
}
135+
})
136+
} catch (error) {
137+
console.error('Error en getNotasFaltantes:', error)
138+
return res.status(500).json({ message: 'Error interno del servidor' })
139+
}
140+
}
141+
142+
module.exports = { getNotasFaltantes }

controllers/programarCierre.controller.js

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,47 +16,48 @@ async function cerrarPeriodo(periodoId) {
1616

1717
console.log(`🕒 Cerrando automáticamente el periodo: ${periodo.descripcion}`);
1818

19-
// validar que existan inscripciones con los dos quimestres completos
20-
const [validasReg] = await sequelize.query(`
21-
SELECT i.ID
22-
FROM inscripciones i
23-
JOIN matriculas m ON i.ID_matricula = m.ID
24-
JOIN estudiantes e ON e.ID = m.ID_estudiante AND e.nivel NOT LIKE '%Básico Elemental%'
25-
JOIN calificaciones_quimestrales cq ON cq.ID_inscripcion = i.ID
26-
WHERE m.ID_periodo_academico = ?
27-
GROUP BY i.ID
28-
HAVING COUNT(DISTINCT cq.quimestre) = 2
29-
`, { replacements: [periodoId] });
19+
const [validacionSPRaw] = await sequelize.query('CALL sp_validar_notas_completas(?)', {
20+
replacements: [periodoId]
21+
});
3022

31-
const [validasBE] = await sequelize.query(`
32-
SELECT i.ID
23+
const validacionSP = Array.isArray(validacionSPRaw) ? validacionSPRaw[0] : validacionSPRaw;
24+
25+
if (!validacionSP || Number(validacionSP.notas_completas) !== 1) {
26+
console.warn(`⚠️ No se puede cerrar el periodo ${periodoId}, validación de notas incompleta`, validacionSP || {});
27+
return;
28+
}
29+
30+
const [totalesNivelRaw] = await sequelize.query(`
31+
SELECT
32+
SUM(CASE WHEN m.nivel COLLATE utf8mb4_unicode_ci NOT LIKE '%Básico Elemental%' THEN 1 ELSE 0 END) AS total_regular,
33+
SUM(CASE WHEN m.nivel COLLATE utf8mb4_unicode_ci LIKE '%Básico Elemental%' THEN 1 ELSE 0 END) AS total_be
3334
FROM inscripciones i
3435
JOIN matriculas m ON i.ID_matricula = m.ID
35-
JOIN estudiantes e ON e.ID = m.ID_estudiante AND e.nivel LIKE '%Básico Elemental%'
36-
JOIN calificaciones_quimestrales_be cq ON cq.ID_inscripcion = i.ID
3736
WHERE m.ID_periodo_academico = ?
38-
GROUP BY i.ID
39-
HAVING COUNT(DISTINCT cq.quimestre) = 2
4037
`, { replacements: [periodoId] });
4138

42-
if (validasReg.length === 0 && validasBE.length === 0) {
43-
console.warn(`⚠️ No se puede cerrar el periodo ${periodoId}, faltan calificaciones completas`);
39+
const totalesNivel = Array.isArray(totalesNivelRaw) ? totalesNivelRaw[0] : totalesNivelRaw;
40+
const totalRegular = Number(totalesNivel?.total_regular || 0);
41+
const totalBE = Number(totalesNivel?.total_be || 0);
42+
43+
if (totalRegular === 0 && totalBE === 0) {
44+
console.warn(`⚠️ No existen inscripciones en el periodo ${periodoId}`);
4445
return;
4546
}
4647

4748
// ejecutar dentro de una transacción para que todo sea atómico
4849
await sequelize.transaction(async (t) => {
49-
if (validasReg.length > 0) {
50-
console.log(`📊 Ejecutando sp_cerrar_periodo_regular para ${validasReg.length} estudiantes...`);
50+
if (totalRegular > 0) {
51+
console.log(`📊 Ejecutando sp_cerrar_periodo_regular para ${totalRegular} inscripciones...`);
5152
const [resultReg] = await sequelize.query('CALL sp_cerrar_periodo_regular(?)', {
5253
replacements: [periodoId],
5354
transaction: t
5455
});
5556
console.log('➤ Resumen periodo regular:', resultReg || '[sin resultado]');
5657
}
5758

58-
if (validasBE.length > 0) {
59-
console.log(`📊 Ejecutando sp_cerrar_periodo_basico para ${validasBE.length} estudiantes de Básico Elemental...`);
59+
if (totalBE > 0) {
60+
console.log(`📊 Ejecutando sp_cerrar_periodo_basico para ${totalBE} inscripciones de Básico Elemental...`);
6061
const [resultBE] = await sequelize.query('CALL sp_cerrar_periodo_basico(?)', {
6162
replacements: [periodoId],
6263
transaction: t

routes/alertas.routes.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
const express = require('express')
2+
const router = express.Router()
3+
const { docenteProfesor } = require('../middlewares/protect')
4+
const { getNotasFaltantes } = require('../controllers/alertas.controller')
5+
6+
router.get('/notas-faltantes', docenteProfesor, getNotasFaltantes)
7+
8+
module.exports = (app) => {
9+
app.use('/api/alertas', router)
10+
}

server.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ AllPassword(app)
9393
const AllFiles = require('./routes/downlodadFile.routes')
9494
AllFiles(app)
9595

96+
const AllAlertas = require('./routes/alertas.routes')
97+
AllAlertas(app)
98+
9699
reprogramarPeriodosPendientes()
97100
startServer();
98101

0 commit comments

Comments
 (0)