fmatituy/selectospromanager
0
1from nicegui import ui, run
2from datetime import datetime
3from services.auth import require_auth, get_current_user
4from modules.layout import SelectosLayout
5from .services_sst_ext import get_stats_sst
6
7@ui.page('/sst/analytics')
8@require_auth
9def sst_analytics_page():
10 user = get_current_user()
11
12 # State for filters
13 state = {
14 'sede': 'Todas las Sedes',
15 'periodo': 'Mes Actual',
16 'last_update': datetime.now().strftime('%Y-%m-%d %H:%M'),
17 'show_report_config': False
18 }
19
20 def refresh_stats():
21 state['last_update'] = datetime.now().strftime('%Y-%m-%d %H:%M')
22 ui.notify('Datos actualizados')
23
24 def open_report_config():
25 state['show_report_config'] = True
26 report_config_dialog.open()
27
28 async def generate_executive_report():
29 ui.notify('Generando reporte institucional...', type='ongoing', spin=True)
30 from services.pdf_generator import generar_reporte_dashboard
31 from database.queries import get_projects
32
33 proyectos = get_projects()
34 stats = get_stats_sst()
35 # Adaptar stats para el formato de reporte ejecutivo
36 report_stats = {
37 'proyectos_activos': len([p for p in proyectos if p['estado'] != 'Finalizado']),
38 'pendientes_sst': stats.get('planes_pendientes', 0),
39 'solicitudes_material': 0, # Placeholder
40 'eficiencia_oee': 95.5
41 }
42
43 pdf_url = await run.io_bound(generar_reporte_dashboard, proyectos, report_stats)
44 ui.notify('Reporte generado exitosamente', type='positive')
45 ui.download(pdf_url)
46 report_config_dialog.close()
47
48 # Paleta de Colores Institucional
49 CORP_BLUE = '#1F3A5F' # Azul Corporativo
50 CORP_GREEN = '#2E7D32' # Verde Éxito
51 CORP_RED = '#B71C1C' # Rojo Crítico
52 CORP_ORANGE = '#F57C00' # Naranja Alerta
53 CORP_GRAY = '#2C3E50' # Gris Texto Principal
54 CORP_BG = '#F4F7F9' # Fondo Institucional
55
56 with SelectosLayout('Consola de Gestión y Cumplimiento SST'):
57 # Aplicamos fondo institucional al contenedor principal
58 ui.query('body').style(f'background-color: {CORP_BG}')
59
60 with ui.column().classes('w-full max-w-screen-2xl mx-auto gap-6 p-4 md:p-8'):
61
62 # 1. HEADER INSTITUCIONAL (Sobrio y Profesional)
63 with ui.row().classes('w-full justify-between items-center bg-white p-6 rounded-xl shadow-sm border-b-4').style(f'border-color: {CORP_BLUE}'):
64 with ui.column().classes('gap-1'):
65 with ui.row().classes('items-center gap-2'):
66 ui.icon('description', color='slate-600', size='sm')
67 ui.label('SISTEMA DE GESTIÓN DE SEGURIDAD Y SALUD EN EL TRABAJO').classes('text-[10px] font-black tracking-widest text-slate-500 uppercase')
68 ui.label('Consola Técnica de Reportes y Analítica').classes(f'text-3xl font-bold text-[{CORP_GRAY}] tracking-tight')
69 ui.label('Cumplimiento Normativo Resolución 0312 / ISO 45001').classes('text-sm text-slate-400 font-medium')
70
71 with ui.row().classes('gap-4 items-center'):
72 with ui.column().classes('gap-0 items-end'):
73 ui.label(f"Referencia: {state['last_update']}").classes('text-[10px] text-slate-400 font-bold mb-2 uppercase')
74 with ui.row().classes('gap-3 bg-slate-50 p-2 rounded-lg border border-slate-200'):
75 ui.select(['Todas las Sedes', 'Sede Principal', 'Planta Norte', 'Proyecto Costa'], value=state['sede']).props('borderless dense').classes('min-w-[150px] font-bold text-slate-700')
76 ui.select(['MES ACTUAL', 'TRIMESTRE 1', 'SEMESTRE 1', 'ANUAL'], value='MES ACTUAL').props('borderless dense').classes('min-w-[120px] font-bold text-slate-700')
77
78 ui.button('GENERAR INFORME OFICIAL', icon='print', on_click=open_report_config) \
79 .props('unelevated rounded-lg').classes('px-6 py-3 font-bold text-white shadow-md transition-all uppercase text-[11px] tracking-wider') \
80 .style(f'background-color: {CORP_BLUE}')
81
82 # 2. RESUMEN EJECUTIVO (KPIs INSTITUCIONALES)
83 stats = get_stats_sst()
84 kpis = [
85 {'label': 'Accidentalidad (Período)', 'value': '02', 'trend': '+1', 'dir': 'up', 'color': CORP_RED},
86 {'label': 'Índice de Frecuencia (IF)', 'value': '12.4', 'trend': '-2.1', 'dir': 'down', 'color': CORP_BLUE},
87 {'label': 'Índice de Severidad (IS)', 'value': '45.8', 'trend': '+5.2', 'dir': 'up', 'color': CORP_ORANGE},
88 {'label': 'Cumplimiento Estándares', 'value': '94.2%', 'trend': '+3.5%', 'dir': 'up', 'color': CORP_GREEN},
89 ]
90
91 with ui.row().classes('w-full grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6'):
92 for kpi in kpis:
93 with ui.card().classes('p-6 rounded-xl bg-white border border-slate-200 shadow-sm flex flex-col justify-between w-full'):
94 with ui.row().classes('w-full justify-between items-start mb-4'):
95 ui.label(kpi['label']).classes('text-[10px] font-bold text-slate-500 uppercase tracking-widest flex-1')
96 with ui.row().classes("items-center gap-1 opacity-80"):
97 ui.icon('trending_up' if kpi['dir'] == 'up' else 'trending_down', size='12px', color=kpi['color'])
98 ui.label(kpi['trend']).classes("text-[10px] font-bold").style(f"color: {kpi['color']}")
99
100 ui.label(kpi['value']).classes(f"text-4xl font-bold text-[{CORP_GRAY}] tracking-tighter")
101
102 ui.separator().classes('my-4 opacity-50')
103 with ui.row().classes('w-full items-center gap-2'):
104 with ui.element('div').classes('h-1.5 flex-1 bg-slate-100 rounded-full overflow-hidden'):
105 ui.element('div').classes("h-full w-[70%]").style(f"background-color: {kpi['color']}")
106 ui.label('Obj: 90%').classes('text-[9px] font-bold text-slate-400 uppercase')
107
108 # 3. CONTENIDO TÉCNICO
109 with ui.tabs().classes('w-full bg-white px-4 border-b border-slate-200') as tabs:
110 ui.tab('ANÁLISIS DE RIESGOS', icon='bar_chart').classes('text-[11px] font-bold py-4')
111 ui.tab('HISTÓRICO EVENTOS', icon='list_alt').classes('text-[11px] font-bold py-4')
112 ui.tab('GESTIÓN DOCUMENTAL', icon='folder_special').classes('text-[11px] font-bold py-4')
113
114 with ui.tab_panels(tabs, value='ANÁLISIS DE RIESGOS').classes('w-full bg-transparent p-0'):
115
116 # TAB 1: Análisis de Riesgos
117 with ui.tab_panel('ANÁLISIS DE RIESGOS').classes('p-0 gap-6 mt-6'):
118 with ui.row().classes('w-full grid grid-cols-1 lg:grid-cols-2 gap-6'):
119 # Gráfico de Distribución (Dona con leyenda lateral para evitar superposición)
120 with ui.card().classes('p-6 rounded-xl bg-white border border-slate-200 shadow-sm'):
121 ui.label('Distribución Normativa de Riesgos Detectados').classes(f'text-sm font-bold text-[{CORP_GRAY}] mb-4 uppercase tracking-widest')
122 ui.echart({
123 'tooltip': {'trigger': 'item', 'formatter': '{b}: {c} ({d}%)'},
124 'legend': {
125 'orient': 'vertical',
126 'right': '2%',
127 'top': 'middle',
128 'textStyle': {'color': '#64748b', 'fontSize': 11, 'fontWeight': 'bold'},
129 'itemWidth': 12,
130 'itemHeight': 12,
131 'icon': 'circle'
132 },
133 'series': [{
134 'name': 'Incidencia',
135 'type': 'pie',
136 'radius': ['50%', '80%'],
137 'center': ['35%', '50%'],
138 'avoidLabelOverlap': False,
139 'itemStyle': {
140 'borderRadius': 5,
141 'borderColor': '#fff',
142 'borderWidth': 2
143 },
144 'label': {'show': False, 'position': 'center'},
145 'emphasis': {
146 'label': {
147 'show': True,
148 'fontSize': 14,
149 'fontWeight': 'bold',
150 'formatter': '{b}\n{d}%',
151 'color': '#475569'
152 }
153 },
154 'labelLine': {'show': False},
155 'data': [
156 {'value': 15, 'name': 'Ergonómico', 'itemStyle': {'color': CORP_BLUE}},
157 {'value': 12, 'name': 'Alturas', 'itemStyle': {'color': CORP_ORANGE}},
158 {'value': 10, 'name': 'Químico', 'itemStyle': {'color': CORP_RED}},
159 {'value': 8, 'name': 'Psicosocial', 'itemStyle': {'color': '#9C27B0'}},
160 {'value': 5, 'name': 'Eléctrico', 'itemStyle': {'color': CORP_GREEN}}
161 ]
162 }]
163 }).classes('h-56 w-full')
164
165 # Tabla de Semáforo de Riesgos
166 with ui.card().classes('p-8 rounded-xl bg-white border border-slate-200 shadow-sm'):
167 ui.label('Estatus de Control por Área (Semáforo)').classes(f'text-sm font-bold text-[{CORP_GRAY}] mb-6 uppercase tracking-widest')
168 with ui.column().classes('w-full gap-3'):
169 areas = [
170 ('Planta de Producción', 'Crítico', CORP_RED),
171 ('Bodega Logística', 'Controlado', CORP_GREEN),
172 ('Mantenimiento Externo', 'Alerta', CORP_ORANGE),
173 ('Administración', 'Cumplimiento', CORP_GREEN),
174 ('Área de Carga', 'En Observación', CORP_ORANGE)
175 ]
176 for area, status, color in areas:
177 with ui.row().classes('w-full p-3 rounded-lg border border-slate-100 items-center justify-between'):
178 with ui.row().classes('items-center gap-3'):
179 ui.element('div').classes('w-3 h-3 rounded-full shadow-sm').style(f'background-color: {color}')
180 ui.label(area).classes('text-xs font-bold text-slate-700')
181 ui.label(status).classes('text-[9px] font-black uppercase tracking-wider px-2 py-0.5 rounded bg-slate-50 border').style(f'color: {color}; border-color: {color}20')
182
183 # TAB 2: Histórico
184 with ui.tab_panel('HISTÓRICO EVENTOS').classes('p-0 mt-6'):
185 with ui.card().classes('w-full border border-slate-200 rounded-xl bg-white shadow-sm overflow-hidden'):
186 ui.label('Bitácora Oficial de Eventos').classes('p-6 text-sm font-bold text-slate-800 border-b')
187 columns = [
188 {'name': 'fecha', 'label': 'FECHA', 'field': 'fecha', 'align': 'left'},
189 {'name': 'evento', 'label': 'EVENTO', 'field': 'evento', 'align': 'left'},
190 {'name': 'severidad', 'label': 'SEVERIDAD', 'field': 'sev', 'align': 'center'},
191 {'name': 'estado', 'label': 'ESTADO CIERRE', 'field': 'estado', 'align': 'right'},
192 ]
193 rows = [
194 {'fecha': '2026-02-25', 'evento': 'Corte mano derecha', 'sev': 'Media', 'estado': 'Investigando'},
195 {'fecha': '2026-02-22', 'evento': 'Caída mismo nivel', 'sev': 'Baja', 'estado': 'Cerrado'},
196 {'fecha': '2026-02-18', 'evento': 'Falla en arnés', 'sev': 'Crítica', 'estado': 'Plan de Acción'},
197 ]
198 t = ui.table(columns=columns, rows=rows).classes('w-full border-none shadow-none')
199 t.props('flat header-classes="bg-slate-50 text-[10px] font-bold text-slate-500 uppercase tracking-widest"')
200
201 # TAB 3: Gestión Documental
202 with ui.tab_panel('GESTIÓN DOCUMENTAL').classes('p-0 gap-6 mt-6'):
203 with ui.row().classes('w-full grid grid-cols-1 md:grid-cols-3 gap-4'):
204 docs = [
205 ('Informes Mensuales', 'Resumen técnico para gerencia.', 'description'),
206 ('Actas COPASST', 'Registros legales de reuniones.', 'groups'),
207 ('Certificados EPP', 'Fichas técnicas y entregas.', 'verified')
208 ]
209 for t, d, i in docs:
210 with ui.card().classes('p-6 rounded-xl bg-white border border-slate-200 hover:border-blue-400 transition-colors cursor-pointer'):
211 with ui.row().classes('items-center gap-4 mb-2'):
212 ui.icon(i, color='blue-900').classes('p-2 bg-slate-50 rounded-lg')
213 ui.label(t).classes('font-bold text-slate-800')
214 ui.label(d).classes('text-xs text-slate-400 mb-4')
215 ui.button('ACCEDER A CARPETA', icon='open_in_new').props('flat color=primary size=xs').classes('font-bold')
216
217 # --- BOTÓN DE ANÁLISIS AUTOMATIZADO (ESTILO INSTITUCIONAL PLANO) ---
218 with ui.row().classes('w-full justify-center mt-12 py-8 border-t border-slate-200'):
219 ui.button('ANÁLISIS AUTOMATIZADO DE CUMPLIMIENTO', icon='query_stats',
220 on_click=lambda: ui.notify('Generando diagnóstico técnico...')
221 ).props('unelevated rounded-lg').classes('px-12 py-5 font-black tracking-[0.1em] text-white shadow-lg shadow-slate-200') \
222 .style(f'background-color: {CORP_GRAY}')
223
224 # --- DIÁLOGO DE CONFIGURACIÓN DE REPORTES (SOBRIO) ---
225 with ui.dialog() as report_config_dialog, ui.card().classes('p-10 rounded-xl w-full max-w-2xl bg-white'):
226 with ui.column().classes('w-full gap-6'):
227 ui.label('Generación de Informe Ejecutivo').classes('text-2xl font-bold text-slate-800 uppercase tracking-tight')
228
229 with ui.column().classes('w-full gap-4'):
230 ui.select(['INFORME DE GESTIÓN MENSUAL', 'CONSOLIDADO DE ACCIDENTALIDAD', 'MATRIZ DE INDICADORES'], label='Tipo de Informe').classes('w-full').props('outlined dense')
231 with ui.row().classes('w-full gap-4'):
232 ui.input('Fecha Inicial').props('type=date outlined dense').classes('flex-1')
233 ui.input('Fecha Final').props('type=date outlined dense').classes('flex-1')
234
235 ui.select(['Sede Principal', 'Todas las Sedes', 'Planta Norte'], label='Centro de Trabajo').classes('w-full').props('outlined dense')
236
237 ui.separator()
238 with ui.row().classes('w-full gap-4'):
239 ui.checkbox('Firmas Certificadas').classes('text-xs font-bold text-slate-500')
240 ui.checkbox('Anexos de Evidencia').classes('text-xs font-bold text-slate-500')
241
242 with ui.row().classes('w-full gap-4 mt-4'):
243 ui.button('CANCELAR', on_click=report_config_dialog.close).props('flat color=slate-500 rounded-lg flex-1').classes('font-bold')
244 ui.button('DESCARGAR PDF', icon='download', on_click=generate_executive_report).props('unelevated rounded-lg flex-1 font-bold shadow-lg shadow-blue-100').style(f'background-color: {CORP_BLUE}') \
245 .classes('text-white')
246
247 