fmatituy/selectospromanager
0
1from nicegui import ui
2from services.auth import require_auth, get_current_user
3from modules.layout import SelectosLayout
4from database.db import DB_PATH
5import sqlite3
6from datetime import datetime
7
8@ui.page('/proyectos/materiales/solicitud')
9@require_auth
10def solicitud_materiales_page():
11 user = get_current_user()
12
13 # Obtener proyectos activos
14 def get_active_projects():
15 conn = sqlite3.connect(DB_PATH)
16 c = conn.cursor()
17 c.execute("SELECT id, nombre FROM proyectos WHERE estado != 'Cerrado'")
18 res = c.fetchall()
19 conn.close()
20 return {r[0]: r[1] for r in res}
21
22 proyectos = get_active_projects()
23
24 # Listas de items para los checklists
25 ITEMS_CONSTRUCCION = ["Cemento Gris (Bulto)", "Arena de Río (m3)", "Ladrillo Limpio", "Varilla 1/2\"", "Alambre Negro", "Malla Electrosoldada"]
26 HERRAMIENTAS = ["Taladro Percutor", "Esmeril Angular", "Nivel Láser", "Pala Redonda", "Carretilla Reforzada", "Andamio Certificado"]
27 REPUESTOS = ["Disco de Corte 4.5\"", "Broca para Concreto", "Filtro de Aire Compresor", "Cable de Acero 1/4\"", "Válvula Reguladora"]
28
29 with SelectosLayout('Solicitud de Recursos'):
30 with ui.column().classes('w-full max-w-4xl mx-auto gap-8'):
31 # Header
32 with ui.row().classes('w-full justify-between items-center bg-emerald-900 p-10 rounded-[2.5rem] shadow-2xl relative overflow-hidden'):
33 ui.element('div').classes('absolute -right-10 -bottom-10 w-48 h-48 bg-emerald-800 rounded-full opacity-50 blur-2xl')
34 with ui.column().classes('gap-1'):
35 ui.label('SUMINISTROS Y LOGÍSTICA').classes('text-[10px] font-black text-emerald-300 tracking-[0.4em]')
36 ui.label('Solicitud de Materiales y Repuestos').classes('text-3xl font-black text-white')
37 ui.label('Gestiona los recursos necesarios para la ejecución del proyecto.').classes('text-sm text-emerald-100/60 font-medium')
38 ui.icon('inventory_2', size='64px', color='white').classes('opacity-20')
39
40 with ui.card().classes('w-full p-8 rounded-[2rem] border-none shadow-xl bg-white'):
41 ui.label('DATOS DE LA SOLICITUD').classes('text-[10px] font-black text-slate-400 tracking-widest uppercase mb-6')
42
43 with ui.row().classes('w-full grid grid-cols-1 md:grid-cols-2 gap-6 mb-8'):
44 proy_sel = ui.select(proyectos, label='Proyecto Destino *').props('outlined rounded-xl').classes('w-full')
45 prioridad = ui.select(['Baja', 'Media', 'Alta', 'CRÍTICO'], label='Prioridad', value='Media').props('outlined rounded-xl').classes('w-full')
46
47 # SECCIONES DE ITEMS
48 with ui.tabs().classes('w-full bg-slate-50 p-1 rounded-2xl mb-6 shadow-inner') as tabs:
49 t_mat = ui.tab('MATERIALES', icon='architecture')
50 t_her = ui.tab('HERRAMIENTAS', icon='construction')
51 t_rep = ui.tab('REPUESTOS', icon='settings_suggest')
52
53 with ui.tab_panels(tabs, value=t_mat).classes('w-full bg-transparent'):
54 # Pestaña Materiales
55 with ui.tab_panel(t_mat).classes('p-0 pt-4'):
56 ui.label('Seleccione los materiales básicos requeridos:').classes('text-xs font-bold text-slate-500 mb-4')
57 sel_materiales = {}
58 with ui.row().classes('w-full grid grid-cols-1 sm:grid-cols-2 gap-4'):
59 for item in ITEMS_CONSTRUCCION:
60 with ui.card().classes('p-4 rounded-xl border border-slate-100 flex-row items-center gap-3 bg-slate-50/50'):
61 sel_materiales[item] = ui.checkbox(item).props('color=emerald')
62 ui.number('Cant.', value=0).props('dense borderless').classes('w-16 ml-auto font-bold')
63
64 # Pestaña Herramientas
65 with ui.tab_panel(t_her).classes('p-0 pt-4'):
66 ui.label('Equipos y herramientas de construcción:').classes('text-xs font-bold text-slate-500 mb-4')
67 sel_herramientas = {}
68 with ui.row().classes('w-full grid grid-cols-1 sm:grid-cols-2 gap-4'):
69 for item in HERRAMIENTAS:
70 with ui.card().classes('p-4 rounded-xl border border-slate-100 flex-row items-center gap-3 bg-slate-50/50'):
71 sel_herramientas[item] = ui.checkbox(item).props('color=blue')
72 ui.number('Cant.', value=1).props('dense borderless').classes('w-16 ml-auto font-bold')
73
74 # Pestaña Repuestos
75 with ui.tab_panel(t_rep).classes('p-0 pt-4'):
76 ui.label('Repuestos y consumibles específicos:').classes('text-xs font-bold text-slate-500 mb-4')
77 sel_repuestos = {}
78 with ui.row().classes('w-full grid grid-cols-1 sm:grid-cols-2 gap-4'):
79 for item in REPUESTOS:
80 with ui.card().classes('p-4 rounded-xl border border-slate-100 flex-row items-center gap-3 bg-slate-50/50'):
81 sel_repuestos[item] = ui.checkbox(item).props('color=amber')
82 ui.number('Cant.', value=0).props('dense borderless').classes('w-16 ml-auto font-bold')
83
84 ui.label('OBSERVACIONES ADICIONALES').classes('text-[10px] font-black text-slate-400 tracking-widest uppercase mt-8 mb-4')
85 obs = ui.textarea('Detalles específicos del pedido...').props('outlined rounded-2xl bg-slate-50 border-none px-4').classes('w-full')
86
87 def enviar_solicitud():
88 if not proy_sel.value:
89 return ui.notify('Seleccione un proyecto para continuar', type='warning')
90
91 # Consolidar JSON de items
92 solicitados = []
93 # Materiales
94 for item, obj in sel_materiales.items():
95 if obj.value: solicitados.append({"item": item, "tipo": "Material"})
96 # Herramientas
97 for item, obj in sel_herramientas.items():
98 if obj.value: solicitados.append({"item": item, "tipo": "Herramienta"})
99 # Repuestos
100 for item, obj in sel_repuestos.items():
101 if obj.value: solicitados.append({"item": item, "tipo": "Repuesto"})
102
103 if not solicitados:
104 return ui.notify('No ha seleccionado ningún item', type='warning')
105
106 import json
107 from database.queries import db_execute
108
109 db_execute("""
110 INSERT INTO material_requests (usuario_id, proyecto_id, prioridad, items_json, observaciones, fecha_solicitud)
111 VALUES (?, ?, ?, ?, ?, ?)
112 """, (user['id'], proy_sel.value, prioridad.value, json.dumps(solicitados), obs.value, datetime.now().strftime('%Y-%m-%d %H:%M')))
113
114 ui.notify('SOLICITUD RADICADA EXITOSAMENTE', type='positive', position='top')
115 refresh_list()
116 # Limpiar campos
117 obs.value = ""
118 for obj in {**sel_materiales, **sel_herramientas, **sel_repuestos}.values():
119 obj.value = False
120
121 with ui.row().classes('w-full justify-end mt-12'):
122 ui.button('ENVIAR PEDIDO DE RECURSOS', icon='send', on_click=enviar_solicitud).props('unelevated rounded-2xl color=emerald-700 px-10 h-14').classes('font-black shadow-xl shadow-emerald-100')
123
124 # --- LISTADO DE SEGUIMIENTO DE SOLICITUDES ---
125 ui.label('ESTADO DE MIS SOLICITUDES RECIENTES').classes('text-xs font-black text-slate-400 tracking-[0.3em] mt-10 mb-4')
126 container = ui.column().classes('w-full gap-4')
127
128 def refresh_list():
129 container.clear()
130 from services.navigation import is_manager
131 from database.queries import db_fetchall
132
133 query = """
134 SELECT m.*, p.nombre as proyecto_nombre, u.nombre as solicitante
135 FROM material_requests m
136 JOIN proyectos p ON m.proyecto_id = p.id
137 JOIN usuarios u ON m.usuario_id = u.id
138 """
139 params = []
140 if not is_manager(user.get('rol')):
141 query += " WHERE m.usuario_id = ?"
142 params.append(user['id'])
143
144 query += " ORDER BY m.id DESC LIMIT 10"
145
146 rows = db_fetchall(query, params)
147 with container:
148 if not rows:
149 ui.label('No tienes solicitudes pendientes.').classes('text-slate-400 italic')
150 for r in rows:
151 color_status = {
152 'Pendiente': 'amber',
153 'En Proceso': 'blue',
154 'Comprado': 'emerald',
155 'Entregado': 'indigo',
156 'Rechazado': 'rose'
157 }.get(r['estado'], 'slate')
158
159 with ui.card().classes('w-full p-6 rounded-2xl border border-slate-100 shadow-sm bg-white hover:border-emerald-200 transition-all'):
160 with ui.row().classes('w-full justify-between items-center'):
161 with ui.column().classes('gap-1'):
162 ui.label(f"Solicitud #{r['id']} - {r['proyecto_nombre']}").classes('font-black text-slate-800')
163 ui.label(f"Fecha: {r['fecha_solicitud']}").classes('text-[10px] text-slate-400 font-bold uppercase tracking-widest')
164
165 ui.badge(r['estado']).props(f'color={color_status}-100 text-color={color_status}-800 px-4 py-1').classes('font-bold rounded-pill')
166
167 import json
168 items = json.loads(r['items_json'])
169 items_summary = ", ".join([i['item'] for i in items])
170 ui.label(items_summary).classes('text-xs text-slate-600 mt-3 line-clamp-1')
171
172 if r['respuesta_gestion'] if 'respuesta_gestion' in r else None:
173 with ui.row().classes('mt-4 gap-2 items-center bg-blue-50 p-2 rounded-lg'):
174 ui.icon('info', color='blue-600', size='xs')
175 ui.label(f"Nota Gerencia: {r['respuesta_gestion']}").classes('text-[11px] text-blue-700 font-medium')
176
177 refresh_list()
178 