utn-cli 2.1.38 → 2.1.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/templates/backend/rutas/misc.js +14 -0
- package/templates/backend/servicios/Nucleo/Miscelaneas.js +125 -0
- package/templates/frontend/src/app/Componentes/Nucleo/calendario-publico/calendario-publico.component.css +236 -0
- package/templates/frontend/src/app/Componentes/Nucleo/calendario-publico/calendario-publico.component.html +172 -0
- package/templates/frontend/src/app/Componentes/Nucleo/calendario-publico/calendario-publico.component.ts +106 -0
- package/templates/frontend/src/app/Componentes/Nucleo/reordenar-menu/reordenar-menu.component.css +65 -0
- package/templates/frontend/src/app/Componentes/Nucleo/reordenar-menu/reordenar-menu.component.html +17 -0
- package/templates/frontend/src/app/Componentes/Nucleo/reordenar-menu/reordenar-menu.component.ts +41 -0
- package/templates/frontend/src/app/Paginas/Nucleo/accesibilidad/accesibilidad.component.css +239 -0
- package/templates/frontend/src/app/Paginas/Nucleo/accesibilidad/accesibilidad.component.html +289 -0
- package/templates/frontend/src/app/Paginas/Nucleo/accesibilidad/accesibilidad.component.ts +28 -0
- package/templates/frontend/src/app/Paginas/Nucleo/contenedor-componentes/contenedor-componentes.component.html +13 -12
- package/templates/frontend/src/app/Paginas/Nucleo/contenedor-componentes/contenedor-componentes.component.ts +102 -1
- package/templates/frontend/src/app/Paginas/Nucleo/declaracion-ia/declaracion-ia.component.css +149 -0
- package/templates/frontend/src/app/Paginas/Nucleo/declaracion-ia/declaracion-ia.component.html +69 -0
- package/templates/frontend/src/app/Paginas/Nucleo/declaracion-ia/declaracion-ia.component.ts +11 -0
- package/templates/frontend/src/app/app.routes.ts +8 -0
package/package.json
CHANGED
|
@@ -881,4 +881,18 @@ Router.get('/obtenerTarjetas', async (solicitud, respuesta, next) => {
|
|
|
881
881
|
}
|
|
882
882
|
});
|
|
883
883
|
|
|
884
|
+
Router.get('/calendarioPublico/:Anio?', async (solicitud, respuesta, next) => {
|
|
885
|
+
try {
|
|
886
|
+
try {
|
|
887
|
+
return respuesta.json({ body: await Miscelaneo.obtenerCalendarioPublico(solicitud.params.Anio), error: undefined });
|
|
888
|
+
} catch (error) {
|
|
889
|
+
const MensajeDeError = 'No fue posible obtener el calendario público';
|
|
890
|
+
console.error(new ManejadorDeErrores(MensajeDeError, ManejadorDeErrores.obtenerNumeroDeLinea(), true, `Dirección IP: ${solicitud.ip}`));
|
|
891
|
+
return respuesta.status(500).json({ body: undefined, error: MensajeDeError });
|
|
892
|
+
}
|
|
893
|
+
} catch (error) {
|
|
894
|
+
next(error);
|
|
895
|
+
}
|
|
896
|
+
});
|
|
897
|
+
|
|
884
898
|
module.exports = Router;
|
|
@@ -2633,6 +2633,131 @@ class Miscelaneo {
|
|
|
2633
2633
|
})
|
|
2634
2634
|
.sort((a, b) => a['Posición'] - b['Posición']);
|
|
2635
2635
|
}
|
|
2636
|
+
|
|
2637
|
+
async obtenerCalendarioPublico(Anio) {
|
|
2638
|
+
const anio = parseInt(Anio) || new Date().getFullYear();
|
|
2639
|
+
|
|
2640
|
+
const filas = await ejecutarConsultaSIGU(`
|
|
2641
|
+
SELECT
|
|
2642
|
+
c.CalendarioId, c.Anio, c.Version, c.Titulo,
|
|
2643
|
+
p.PeriodoId, p.TipoPeriodo, p.Nombre AS NombrePeriodo,
|
|
2644
|
+
p.Descripcion AS DescripcionPeriodo,
|
|
2645
|
+
p.FechaInicio AS PeriodoInicio, p.FechaFin AS PeriodoFin,
|
|
2646
|
+
p.Orden AS OrdenPeriodo,
|
|
2647
|
+
a.ActividadId, a.Descripcion AS DescripcionActividad,
|
|
2648
|
+
a.FechaInicio AS ActividadInicio, a.FechaFin AS ActividadFin,
|
|
2649
|
+
a.EsDestacada, a.Orden AS OrdenActividad,
|
|
2650
|
+
CASE
|
|
2651
|
+
WHEN a.FechaInicio IS NULL AND a.FechaFin IS NULL THEN 'sin_fecha'
|
|
2652
|
+
WHEN a.FechaFin < CURDATE() THEN 'pasada'
|
|
2653
|
+
WHEN a.FechaInicio > CURDATE() THEN 'proxima'
|
|
2654
|
+
ELSE 'en_curso'
|
|
2655
|
+
END AS EstadoActividad,
|
|
2656
|
+
CASE
|
|
2657
|
+
WHEN a.FechaInicio IS NULL OR a.FechaFin IS NULL THEN NULL
|
|
2658
|
+
WHEN a.FechaFin < CURDATE() THEN 100
|
|
2659
|
+
WHEN a.FechaInicio > CURDATE() THEN 0
|
|
2660
|
+
ELSE GREATEST(0, LEAST(100, ROUND(
|
|
2661
|
+
DATEDIFF(CURDATE(), a.FechaInicio) /
|
|
2662
|
+
NULLIF(DATEDIFF(a.FechaFin, a.FechaInicio), 0) * 100
|
|
2663
|
+
)))
|
|
2664
|
+
END AS PorcentajeActividad,
|
|
2665
|
+
CASE
|
|
2666
|
+
WHEN a.FechaInicio IS NULL THEN NULL
|
|
2667
|
+
WHEN a.FechaInicio > CURDATE() THEN DATEDIFF(a.FechaInicio, CURDATE())
|
|
2668
|
+
ELSE NULL
|
|
2669
|
+
END AS DiasParaInicioActividad
|
|
2670
|
+
FROM \`SIGU\`.\`SIGU_CalendarioInstitucional\` c
|
|
2671
|
+
JOIN \`SIGU\`.\`SIGU_CalendarioPeriodo\` p ON c.CalendarioId = p.CalendarioId
|
|
2672
|
+
LEFT JOIN \`SIGU\`.\`SIGU_CalendarioActividad\` a ON p.PeriodoId = a.PeriodoId
|
|
2673
|
+
WHERE c.Estado = 1 AND c.Anio = ?
|
|
2674
|
+
ORDER BY p.Orden ASC, p.PeriodoId ASC, a.Orden ASC, a.ActividadId ASC
|
|
2675
|
+
`, [anio]);
|
|
2676
|
+
|
|
2677
|
+
if (!filas.length) return null;
|
|
2678
|
+
|
|
2679
|
+
const hoy = new Date();
|
|
2680
|
+
hoy.setHours(0, 0, 0, 0);
|
|
2681
|
+
|
|
2682
|
+
const inicioAnio = new Date(filas[0].Anio, 0, 1);
|
|
2683
|
+
const finAnio = new Date(filas[0].Anio + 1, 0, 1);
|
|
2684
|
+
const porcentajeAnio = Math.max(0, Math.min(100, Math.round((hoy - inicioAnio) / (finAnio - inicioAnio) * 100)));
|
|
2685
|
+
|
|
2686
|
+
const calendario = {
|
|
2687
|
+
CalendarioId: filas[0].CalendarioId,
|
|
2688
|
+
Anio: filas[0].Anio,
|
|
2689
|
+
Version: filas[0].Version,
|
|
2690
|
+
Titulo: filas[0].Titulo,
|
|
2691
|
+
PorcentajeAnio: porcentajeAnio,
|
|
2692
|
+
Periodos: []
|
|
2693
|
+
};
|
|
2694
|
+
|
|
2695
|
+
const periodosMap = new Map();
|
|
2696
|
+
|
|
2697
|
+
for (const fila of filas) {
|
|
2698
|
+
if (!periodosMap.has(fila.PeriodoId)) {
|
|
2699
|
+
let estadoPeriodo = 'sin_fecha';
|
|
2700
|
+
let porcentajePeriodo = null;
|
|
2701
|
+
let diasParaInicioPeriodo = null;
|
|
2702
|
+
|
|
2703
|
+
if (fila.PeriodoInicio && fila.PeriodoFin) {
|
|
2704
|
+
const s = (v) => {
|
|
2705
|
+
const d = typeof v === 'string' ? v : new Date(v).toISOString();
|
|
2706
|
+
const [y, m, dd] = d.slice(0, 10).split('-').map(Number);
|
|
2707
|
+
const fecha = new Date(y, m - 1, dd);
|
|
2708
|
+
fecha.setHours(0, 0, 0, 0);
|
|
2709
|
+
return fecha;
|
|
2710
|
+
};
|
|
2711
|
+
const inicio = s(fila.PeriodoInicio);
|
|
2712
|
+
const fin = s(fila.PeriodoFin);
|
|
2713
|
+
|
|
2714
|
+
if (fin < hoy) {
|
|
2715
|
+
estadoPeriodo = 'pasada';
|
|
2716
|
+
porcentajePeriodo = 100;
|
|
2717
|
+
} else if (inicio > hoy) {
|
|
2718
|
+
estadoPeriodo = 'proxima';
|
|
2719
|
+
porcentajePeriodo = 0;
|
|
2720
|
+
diasParaInicioPeriodo = Math.ceil((inicio - hoy) / 86400000);
|
|
2721
|
+
} else {
|
|
2722
|
+
estadoPeriodo = 'en_curso';
|
|
2723
|
+
const total = fin - inicio;
|
|
2724
|
+
porcentajePeriodo = total > 0
|
|
2725
|
+
? Math.max(0, Math.min(100, Math.round((hoy - inicio) / total * 100)))
|
|
2726
|
+
: 0;
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
|
|
2730
|
+
periodosMap.set(fila.PeriodoId, {
|
|
2731
|
+
PeriodoId: fila.PeriodoId,
|
|
2732
|
+
TipoPeriodo: fila.TipoPeriodo,
|
|
2733
|
+
NombrePeriodo: fila.NombrePeriodo,
|
|
2734
|
+
DescripcionPeriodo: fila.DescripcionPeriodo,
|
|
2735
|
+
PeriodoInicio: fila.PeriodoInicio,
|
|
2736
|
+
PeriodoFin: fila.PeriodoFin,
|
|
2737
|
+
EstadoPeriodo: estadoPeriodo,
|
|
2738
|
+
PorcentajePeriodo: porcentajePeriodo,
|
|
2739
|
+
DiasParaInicioPeriodo: diasParaInicioPeriodo,
|
|
2740
|
+
Actividades: []
|
|
2741
|
+
});
|
|
2742
|
+
}
|
|
2743
|
+
|
|
2744
|
+
if (fila.ActividadId !== null) {
|
|
2745
|
+
periodosMap.get(fila.PeriodoId).Actividades.push({
|
|
2746
|
+
ActividadId: fila.ActividadId,
|
|
2747
|
+
DescripcionActividad: fila.DescripcionActividad,
|
|
2748
|
+
ActividadInicio: fila.ActividadInicio,
|
|
2749
|
+
ActividadFin: fila.ActividadFin,
|
|
2750
|
+
EsDestacada: Boolean(fila.EsDestacada),
|
|
2751
|
+
EstadoActividad: fila.EstadoActividad,
|
|
2752
|
+
PorcentajeActividad: fila.PorcentajeActividad,
|
|
2753
|
+
DiasParaInicioActividad: fila.DiasParaInicioActividad
|
|
2754
|
+
});
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2758
|
+
calendario.Periodos = [...periodosMap.values()];
|
|
2759
|
+
return calendario;
|
|
2760
|
+
}
|
|
2636
2761
|
}
|
|
2637
2762
|
|
|
2638
2763
|
module.exports = new Miscelaneo();
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
.titulo-dialog {
|
|
2
|
+
display: flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
gap: 8px;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.titulo-dialog small {
|
|
8
|
+
font-size: 0.65em;
|
|
9
|
+
opacity: 0.6;
|
|
10
|
+
font-weight: normal;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/* ── Buscador ── */
|
|
14
|
+
.buscador {
|
|
15
|
+
width: 100%;
|
|
16
|
+
margin-bottom: 4px;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.aviso-resultados {
|
|
20
|
+
font-size: 0.8em;
|
|
21
|
+
color: #555;
|
|
22
|
+
margin: 0 0 10px;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
mark {
|
|
26
|
+
background: #FFF176;
|
|
27
|
+
color: inherit;
|
|
28
|
+
border-radius: 2px;
|
|
29
|
+
padding: 0 1px;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* ── Barra de progreso anual ── */
|
|
33
|
+
.barra-anio {
|
|
34
|
+
display: flex;
|
|
35
|
+
align-items: center;
|
|
36
|
+
gap: 10px;
|
|
37
|
+
margin-bottom: 16px;
|
|
38
|
+
padding: 10px 12px;
|
|
39
|
+
background: #f5f5f5;
|
|
40
|
+
border-radius: 6px;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.barra-anio mat-progress-bar {
|
|
44
|
+
flex: 1;
|
|
45
|
+
border-radius: 4px;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.barra-anio-label {
|
|
49
|
+
font-size: 0.8em;
|
|
50
|
+
white-space: nowrap;
|
|
51
|
+
color: #555;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.barra-anio-pct {
|
|
55
|
+
font-size: 0.8em;
|
|
56
|
+
font-weight: 600;
|
|
57
|
+
white-space: nowrap;
|
|
58
|
+
color: #333;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* ── Paneles ── */
|
|
62
|
+
.panel {
|
|
63
|
+
margin-bottom: 8px;
|
|
64
|
+
border-radius: 6px !important;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.panel-en-curso {
|
|
68
|
+
border-left: 4px solid #4CAF50;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.panel-proximas {
|
|
72
|
+
border-left: 4px solid #1976D2;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.panel-pasadas {
|
|
76
|
+
border-left: 4px solid #9E9E9E;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.icono-estado {
|
|
80
|
+
margin-right: 6px;
|
|
81
|
+
font-size: 18px;
|
|
82
|
+
height: 18px;
|
|
83
|
+
width: 18px;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.icono-en-curso { color: #4CAF50; }
|
|
87
|
+
.icono-proximas { color: #1976D2; }
|
|
88
|
+
.icono-pasadas { color: #9E9E9E; }
|
|
89
|
+
|
|
90
|
+
.panel-vacio {
|
|
91
|
+
font-size: 0.85em;
|
|
92
|
+
color: #888;
|
|
93
|
+
margin: 4px 0 8px;
|
|
94
|
+
font-style: italic;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* ── Tarjeta de período ── */
|
|
98
|
+
.periodo-card {
|
|
99
|
+
border-radius: 4px;
|
|
100
|
+
padding: 10px 12px;
|
|
101
|
+
margin-bottom: 8px;
|
|
102
|
+
background: #fafafa;
|
|
103
|
+
border: 1px solid #e0e0e0;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.periodo-en-curso { border-left: 3px solid #4CAF50; }
|
|
107
|
+
.periodo-proximas { border-left: 3px solid #1976D2; }
|
|
108
|
+
.periodo-pasadas { border-left: 3px solid #BDBDBD; }
|
|
109
|
+
|
|
110
|
+
.periodo-encabezado {
|
|
111
|
+
display: flex;
|
|
112
|
+
align-items: center;
|
|
113
|
+
gap: 8px;
|
|
114
|
+
flex-wrap: wrap;
|
|
115
|
+
margin-bottom: 4px;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.periodo-nombre {
|
|
119
|
+
font-weight: 600;
|
|
120
|
+
font-size: 0.92em;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.periodo-tipo {
|
|
124
|
+
font-size: 0.75em;
|
|
125
|
+
background: #e8e8e8;
|
|
126
|
+
color: #555;
|
|
127
|
+
padding: 1px 6px;
|
|
128
|
+
border-radius: 10px;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.dias-chip {
|
|
132
|
+
font-size: 0.75em;
|
|
133
|
+
background: #1976D2;
|
|
134
|
+
color: #fff;
|
|
135
|
+
padding: 1px 8px;
|
|
136
|
+
border-radius: 10px;
|
|
137
|
+
margin-left: auto;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.periodo-rango {
|
|
141
|
+
font-size: 0.8em;
|
|
142
|
+
color: #666;
|
|
143
|
+
margin-bottom: 6px;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.periodo-barra {
|
|
147
|
+
display: flex;
|
|
148
|
+
align-items: center;
|
|
149
|
+
gap: 8px;
|
|
150
|
+
margin-bottom: 8px;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.periodo-barra mat-progress-bar {
|
|
154
|
+
flex: 1;
|
|
155
|
+
border-radius: 4px;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.periodo-pct {
|
|
159
|
+
font-size: 0.75em;
|
|
160
|
+
white-space: nowrap;
|
|
161
|
+
color: #555;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/* ── Actividades ── */
|
|
165
|
+
.actividades {
|
|
166
|
+
margin-top: 6px;
|
|
167
|
+
border-top: 1px solid #eeeeee;
|
|
168
|
+
padding-top: 6px;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.actividad-fila {
|
|
172
|
+
display: flex;
|
|
173
|
+
align-items: center;
|
|
174
|
+
gap: 6px;
|
|
175
|
+
padding: 3px 0;
|
|
176
|
+
font-size: 0.82em;
|
|
177
|
+
flex-wrap: wrap;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.act-icono {
|
|
181
|
+
font-size: 13px;
|
|
182
|
+
height: 14px;
|
|
183
|
+
width: 14px;
|
|
184
|
+
flex-shrink: 0;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.act-desc {
|
|
188
|
+
flex: 1;
|
|
189
|
+
min-width: 0;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.act-fecha {
|
|
193
|
+
color: #777;
|
|
194
|
+
white-space: nowrap;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.act-dias {
|
|
198
|
+
font-size: 0.85em;
|
|
199
|
+
background: #E3F2FD;
|
|
200
|
+
color: #1565C0;
|
|
201
|
+
padding: 0 6px;
|
|
202
|
+
border-radius: 8px;
|
|
203
|
+
white-space: nowrap;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/* Colores por estado de actividad */
|
|
207
|
+
.act-en_curso .act-icono { color: #4CAF50; }
|
|
208
|
+
.act-en_curso .act-desc { color: #2E7D32; font-weight: 500; }
|
|
209
|
+
|
|
210
|
+
.act-proxima .act-icono { color: #1976D2; }
|
|
211
|
+
|
|
212
|
+
.act-pasada .act-icono { color: #9E9E9E; }
|
|
213
|
+
.act-pasada .act-desc { color: #9E9E9E; }
|
|
214
|
+
.act-pasada .act-fecha { color: #BDBDBD; }
|
|
215
|
+
|
|
216
|
+
.act-sin_fecha .act-icono { color: #BDBDBD; }
|
|
217
|
+
|
|
218
|
+
/* Textos grises para períodos pasados */
|
|
219
|
+
.gris { color: #9E9E9E; }
|
|
220
|
+
|
|
221
|
+
/* Estados de carga / error */
|
|
222
|
+
.aviso-error {
|
|
223
|
+
display: flex;
|
|
224
|
+
align-items: center;
|
|
225
|
+
gap: 6px;
|
|
226
|
+
color: #c62828;
|
|
227
|
+
padding: 12px 0;
|
|
228
|
+
font-size: 0.9em;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.aviso-vacio {
|
|
232
|
+
color: #888;
|
|
233
|
+
font-size: 0.9em;
|
|
234
|
+
padding: 16px 0;
|
|
235
|
+
font-style: italic;
|
|
236
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
<h2 mat-dialog-title class="titulo-dialog">
|
|
2
|
+
<mat-icon>calendar_month</mat-icon>
|
|
3
|
+
<span *ngIf="cargando">Cargando...</span>
|
|
4
|
+
<span *ngIf="!cargando && calendario">{{ calendario.Titulo }} <small>{{ calendario.Version }}</small></span>
|
|
5
|
+
<span *ngIf="!cargando && !calendario">Calendario institucional</span>
|
|
6
|
+
</h2>
|
|
7
|
+
|
|
8
|
+
<mat-dialog-content>
|
|
9
|
+
|
|
10
|
+
<mat-progress-bar *ngIf="cargando" mode="indeterminate"></mat-progress-bar>
|
|
11
|
+
|
|
12
|
+
<div *ngIf="error" class="aviso-error">
|
|
13
|
+
<mat-icon>error_outline</mat-icon> {{ error }}
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<div *ngIf="!cargando && !error && !calendario" class="aviso-vacio">
|
|
17
|
+
No hay un calendario activo para el año actual.
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<ng-container *ngIf="!cargando && !error && calendario">
|
|
21
|
+
|
|
22
|
+
<div class="barra-anio">
|
|
23
|
+
<span class="barra-anio-label">Año {{ calendario.Anio }}</span>
|
|
24
|
+
<mat-progress-bar mode="determinate" [value]="calendario.PorcentajeAnio" color="primary"></mat-progress-bar>
|
|
25
|
+
<span class="barra-anio-pct">{{ calendario.PorcentajeAnio }}%</span>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<!-- ── Buscador ── -->
|
|
29
|
+
<mat-form-field appearance="outline" class="buscador">
|
|
30
|
+
<mat-label>Buscar actividad</mat-label>
|
|
31
|
+
<mat-icon matPrefix>search</mat-icon>
|
|
32
|
+
<input matInput [value]="terminoBusqueda" (input)="onBusqueda($event)" placeholder="Ej: matrícula, exámenes...">
|
|
33
|
+
<button *ngIf="busquedaActiva" matSuffix mat-icon-button (click)="limpiarBusqueda()" aria-label="Limpiar">
|
|
34
|
+
<mat-icon>close</mat-icon>
|
|
35
|
+
</button>
|
|
36
|
+
</mat-form-field>
|
|
37
|
+
|
|
38
|
+
<p *ngIf="busquedaActiva" class="aviso-resultados">
|
|
39
|
+
<ng-container *ngIf="totalResultados > 0">
|
|
40
|
+
{{ totalResultados }} resultado(s) para «{{ terminoBusqueda }}»
|
|
41
|
+
</ng-container>
|
|
42
|
+
<ng-container *ngIf="totalResultados === 0">
|
|
43
|
+
Sin resultados para «{{ terminoBusqueda }}»
|
|
44
|
+
</ng-container>
|
|
45
|
+
</p>
|
|
46
|
+
|
|
47
|
+
<!-- ── EN CURSO ── -->
|
|
48
|
+
<mat-expansion-panel [expanded]="true" class="panel panel-en-curso"
|
|
49
|
+
*ngIf="!busquedaActiva || periodosEnCursoFiltrados.length > 0">
|
|
50
|
+
<mat-expansion-panel-header>
|
|
51
|
+
<mat-panel-title>
|
|
52
|
+
<mat-icon class="icono-estado icono-en-curso">radio_button_checked</mat-icon>
|
|
53
|
+
En curso
|
|
54
|
+
</mat-panel-title>
|
|
55
|
+
<mat-panel-description>{{ periodosEnCurso.length }} período(s)</mat-panel-description>
|
|
56
|
+
</mat-expansion-panel-header>
|
|
57
|
+
|
|
58
|
+
<p *ngIf="periodosEnCurso.length === 0" class="panel-vacio">Sin períodos en curso actualmente.</p>
|
|
59
|
+
|
|
60
|
+
<ng-container *ngFor="let periodo of periodosEnCursoFiltrados">
|
|
61
|
+
<div class="periodo-card periodo-en-curso">
|
|
62
|
+
<div class="periodo-encabezado">
|
|
63
|
+
<span class="periodo-nombre">{{ periodo.NombrePeriodo }}</span>
|
|
64
|
+
<span class="periodo-tipo">{{ periodo.TipoPeriodo }}</span>
|
|
65
|
+
</div>
|
|
66
|
+
<div class="periodo-rango">
|
|
67
|
+
{{ formatearFecha(periodo.PeriodoInicio) }} → {{ formatearFecha(periodo.PeriodoFin) }}
|
|
68
|
+
</div>
|
|
69
|
+
<div class="periodo-barra" *ngIf="!busquedaActiva">
|
|
70
|
+
<mat-progress-bar mode="determinate" [value]="periodo.PorcentajePeriodo" color="accent"></mat-progress-bar>
|
|
71
|
+
<span class="periodo-pct">{{ periodo.PorcentajePeriodo }}% transcurrido</span>
|
|
72
|
+
</div>
|
|
73
|
+
<div class="actividades" *ngIf="actividadesFiltradas(periodo.Actividades).length">
|
|
74
|
+
<div *ngFor="let a of actividadesFiltradas(periodo.Actividades)"
|
|
75
|
+
class="actividad-fila" [ngClass]="'act-' + a.EstadoActividad">
|
|
76
|
+
<mat-icon class="act-icono">{{ a.EsDestacada ? 'star' : 'circle' }}</mat-icon>
|
|
77
|
+
<span class="act-desc" [innerHTML]="resaltarTexto(a.DescripcionActividad)"></span>
|
|
78
|
+
<span class="act-fecha" *ngIf="a.ActividadInicio">
|
|
79
|
+
{{ formatearFecha(a.ActividadInicio) }}<span *ngIf="a.ActividadFin"> – {{ formatearFecha(a.ActividadFin) }}</span>
|
|
80
|
+
</span>
|
|
81
|
+
<span class="act-dias" *ngIf="a.DiasParaInicioActividad !== null && a.DiasParaInicioActividad !== undefined">
|
|
82
|
+
en {{ a.DiasParaInicioActividad }}d
|
|
83
|
+
</span>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</ng-container>
|
|
88
|
+
</mat-expansion-panel>
|
|
89
|
+
|
|
90
|
+
<!-- ── PRÓXIMAS ── -->
|
|
91
|
+
<mat-expansion-panel [expanded]="true" class="panel panel-proximas"
|
|
92
|
+
*ngIf="!busquedaActiva || periodosProximosFiltrados.length > 0">
|
|
93
|
+
<mat-expansion-panel-header>
|
|
94
|
+
<mat-panel-title>
|
|
95
|
+
<mat-icon class="icono-estado icono-proximas">schedule</mat-icon>
|
|
96
|
+
Próximas
|
|
97
|
+
</mat-panel-title>
|
|
98
|
+
<mat-panel-description>{{ periodosProximos.length }} período(s)</mat-panel-description>
|
|
99
|
+
</mat-expansion-panel-header>
|
|
100
|
+
|
|
101
|
+
<p *ngIf="periodosProximos.length === 0" class="panel-vacio">Sin períodos próximos.</p>
|
|
102
|
+
|
|
103
|
+
<ng-container *ngFor="let periodo of periodosProximosFiltrados">
|
|
104
|
+
<div class="periodo-card periodo-proximas">
|
|
105
|
+
<div class="periodo-encabezado">
|
|
106
|
+
<span class="periodo-nombre">{{ periodo.NombrePeriodo }}</span>
|
|
107
|
+
<span class="periodo-tipo">{{ periodo.TipoPeriodo }}</span>
|
|
108
|
+
<span class="dias-chip" *ngIf="periodo.DiasParaInicioPeriodo !== null && periodo.DiasParaInicioPeriodo !== undefined">
|
|
109
|
+
en {{ periodo.DiasParaInicioPeriodo }} día(s)
|
|
110
|
+
</span>
|
|
111
|
+
</div>
|
|
112
|
+
<div class="periodo-rango">
|
|
113
|
+
{{ formatearFecha(periodo.PeriodoInicio) }} → {{ formatearFecha(periodo.PeriodoFin) }}
|
|
114
|
+
</div>
|
|
115
|
+
<div class="actividades" *ngIf="actividadesFiltradas(periodo.Actividades).length">
|
|
116
|
+
<div *ngFor="let a of actividadesFiltradas(periodo.Actividades)"
|
|
117
|
+
class="actividad-fila" [ngClass]="'act-' + a.EstadoActividad">
|
|
118
|
+
<mat-icon class="act-icono">{{ a.EsDestacada ? 'star' : 'circle' }}</mat-icon>
|
|
119
|
+
<span class="act-desc" [innerHTML]="resaltarTexto(a.DescripcionActividad)"></span>
|
|
120
|
+
<span class="act-fecha" *ngIf="a.ActividadInicio">
|
|
121
|
+
{{ formatearFecha(a.ActividadInicio) }}<span *ngIf="a.ActividadFin"> – {{ formatearFecha(a.ActividadFin) }}</span>
|
|
122
|
+
</span>
|
|
123
|
+
<span class="act-dias" *ngIf="a.DiasParaInicioActividad !== null && a.DiasParaInicioActividad !== undefined">
|
|
124
|
+
en {{ a.DiasParaInicioActividad }}d
|
|
125
|
+
</span>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
</ng-container>
|
|
130
|
+
</mat-expansion-panel>
|
|
131
|
+
|
|
132
|
+
<!-- ── PASADAS ── -->
|
|
133
|
+
<mat-expansion-panel [expanded]="busquedaActiva" class="panel panel-pasadas"
|
|
134
|
+
*ngIf="!busquedaActiva || periodosPasadosFiltrados.length > 0">
|
|
135
|
+
<mat-expansion-panel-header>
|
|
136
|
+
<mat-panel-title>
|
|
137
|
+
<mat-icon class="icono-estado icono-pasadas">check_circle_outline</mat-icon>
|
|
138
|
+
Pasadas
|
|
139
|
+
</mat-panel-title>
|
|
140
|
+
<mat-panel-description>{{ periodosPasados.length }} período(s)</mat-panel-description>
|
|
141
|
+
</mat-expansion-panel-header>
|
|
142
|
+
|
|
143
|
+
<p *ngIf="periodosPasados.length === 0" class="panel-vacio">Sin períodos pasados.</p>
|
|
144
|
+
|
|
145
|
+
<ng-container *ngFor="let periodo of periodosPasadosFiltrados">
|
|
146
|
+
<div class="periodo-card periodo-pasadas">
|
|
147
|
+
<div class="periodo-encabezado">
|
|
148
|
+
<span class="periodo-nombre gris">{{ periodo.NombrePeriodo }}</span>
|
|
149
|
+
<span class="periodo-tipo">{{ periodo.TipoPeriodo }}</span>
|
|
150
|
+
</div>
|
|
151
|
+
<div class="periodo-rango gris">
|
|
152
|
+
{{ formatearFecha(periodo.PeriodoInicio) }} → {{ formatearFecha(periodo.PeriodoFin) }}
|
|
153
|
+
</div>
|
|
154
|
+
<div class="actividades" *ngIf="actividadesFiltradas(periodo.Actividades).length">
|
|
155
|
+
<div *ngFor="let a of actividadesFiltradas(periodo.Actividades)" class="actividad-fila act-pasada">
|
|
156
|
+
<mat-icon class="act-icono">check</mat-icon>
|
|
157
|
+
<span class="act-desc" [innerHTML]="resaltarTexto(a.DescripcionActividad)"></span>
|
|
158
|
+
<span class="act-fecha" *ngIf="a.ActividadInicio">
|
|
159
|
+
{{ formatearFecha(a.ActividadInicio) }}<span *ngIf="a.ActividadFin"> – {{ formatearFecha(a.ActividadFin) }}</span>
|
|
160
|
+
</span>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
</ng-container>
|
|
165
|
+
</mat-expansion-panel>
|
|
166
|
+
|
|
167
|
+
</ng-container>
|
|
168
|
+
</mat-dialog-content>
|
|
169
|
+
|
|
170
|
+
<mat-dialog-actions align="end">
|
|
171
|
+
<button mat-button (click)="cerrar()">Cerrar</button>
|
|
172
|
+
</mat-dialog-actions>
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { Component, OnInit, inject } from '@angular/core';
|
|
2
|
+
import { HttpClient } from '@angular/common/http';
|
|
3
|
+
import { MatDialogActions, MatDialogContent, MatDialogRef, MatDialogTitle } from '@angular/material/dialog';
|
|
4
|
+
import { MatButtonModule } from '@angular/material/button';
|
|
5
|
+
import { MatIconModule } from '@angular/material/icon';
|
|
6
|
+
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
|
7
|
+
import { MatExpansionModule } from '@angular/material/expansion';
|
|
8
|
+
import { MatFormFieldModule } from '@angular/material/form-field';
|
|
9
|
+
import { MatInputModule } from '@angular/material/input';
|
|
10
|
+
import { CommonModule } from '@angular/common';
|
|
11
|
+
import { DatosGlobalesService } from '../../../datos-globales.service';
|
|
12
|
+
|
|
13
|
+
@Component({
|
|
14
|
+
selector: 'app-calendario-publico',
|
|
15
|
+
templateUrl: './calendario-publico.component.html',
|
|
16
|
+
styleUrl: './calendario-publico.component.css',
|
|
17
|
+
imports: [
|
|
18
|
+
CommonModule,
|
|
19
|
+
MatDialogContent, MatDialogActions, MatDialogTitle,
|
|
20
|
+
MatButtonModule, MatIconModule,
|
|
21
|
+
MatProgressBarModule, MatExpansionModule,
|
|
22
|
+
MatFormFieldModule, MatInputModule
|
|
23
|
+
]
|
|
24
|
+
})
|
|
25
|
+
export class CalendarioPublicoComponent implements OnInit {
|
|
26
|
+
readonly dialogRef = inject(MatDialogRef<CalendarioPublicoComponent>);
|
|
27
|
+
|
|
28
|
+
cargando = true;
|
|
29
|
+
error = '';
|
|
30
|
+
calendario: any = null;
|
|
31
|
+
terminoBusqueda = '';
|
|
32
|
+
|
|
33
|
+
get busquedaActiva(): boolean {
|
|
34
|
+
return this.terminoBusqueda.trim().length > 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get totalResultados(): number {
|
|
38
|
+
return this.calendario?.Periodos?.reduce((acc: number, p: any) =>
|
|
39
|
+
acc + this.actividadesFiltradas(p.Actividades).length, 0) ?? 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private porEstado(estado: string): any[] {
|
|
43
|
+
return this.calendario?.Periodos?.filter((p: any) => p.EstadoPeriodo === estado) ?? [];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
get periodosEnCurso(): any[] { return this.porEstado('en_curso'); }
|
|
47
|
+
get periodosProximos(): any[] { return this.porEstado('proxima'); }
|
|
48
|
+
get periodosPasados(): any[] { return this.porEstado('pasada'); }
|
|
49
|
+
|
|
50
|
+
get periodosEnCursoFiltrados(): any[] { return this.periodosEnCurso.filter(p => this.periodoConResultados(p)); }
|
|
51
|
+
get periodosProximosFiltrados(): any[] { return this.periodosProximos.filter(p => this.periodoConResultados(p)); }
|
|
52
|
+
get periodosPasadosFiltrados(): any[] { return this.periodosPasados.filter(p => this.periodoConResultados(p)); }
|
|
53
|
+
|
|
54
|
+
actividadesFiltradas(actividades: any[]): any[] {
|
|
55
|
+
if (!this.busquedaActiva) return actividades ?? [];
|
|
56
|
+
const t = this.terminoBusqueda.trim().toLowerCase();
|
|
57
|
+
return (actividades ?? []).filter(a => a.DescripcionActividad?.toLowerCase().includes(t));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
periodoConResultados(periodo: any): boolean {
|
|
61
|
+
return !this.busquedaActiva || this.actividadesFiltradas(periodo.Actividades).length > 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
resaltarTexto(texto: string): string {
|
|
65
|
+
if (!this.busquedaActiva || !texto) return texto;
|
|
66
|
+
const escaped = texto.replace(/[&<>"']/g, c =>
|
|
67
|
+
({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]!)
|
|
68
|
+
);
|
|
69
|
+
const regex = new RegExp(`(${this.terminoBusqueda.trim().replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
|
|
70
|
+
return escaped.replace(regex, '<mark>$1</mark>');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
onBusqueda(event: Event): void {
|
|
74
|
+
this.terminoBusqueda = (event.target as HTMLInputElement).value;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
limpiarBusqueda(): void {
|
|
78
|
+
this.terminoBusqueda = '';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
constructor(private http: HttpClient, private datosGlobalesService: DatosGlobalesService) {}
|
|
82
|
+
|
|
83
|
+
ngOnInit(): void {
|
|
84
|
+
this.http.get(this.datosGlobalesService.ObtenerURL() + 'misc/calendarioPublico').subscribe({
|
|
85
|
+
next: (datos: any) => {
|
|
86
|
+
this.calendario = datos.body;
|
|
87
|
+
this.cargando = false;
|
|
88
|
+
},
|
|
89
|
+
error: () => {
|
|
90
|
+
this.error = 'No fue posible cargar el calendario.';
|
|
91
|
+
this.cargando = false;
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
cerrar(): void {
|
|
97
|
+
this.dialogRef.close();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
formatearFecha(fecha: any): string {
|
|
101
|
+
if (!fecha) return '—';
|
|
102
|
+
const s = typeof fecha === 'string' ? fecha : new Date(fecha).toISOString();
|
|
103
|
+
const [y, m, d] = s.slice(0, 10).split('-').map(Number);
|
|
104
|
+
return new Date(y, m - 1, d).toLocaleDateString('es-CR', { day: 'numeric', month: 'short' });
|
|
105
|
+
}
|
|
106
|
+
}
|