CoolFace
Apppublic

fmatituy/selectospromanager

sourceHugging Faceupdated 6mo agoView on Hugging Face
0likes
analytics.py247 linesDownload Raw Back to sst
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