fmatituy/selectospromanager
0
1from nicegui import ui
2from modules.personal.services_personal import get_usuarios_personal, create_or_update_user, get_empleados, delete_user
3from services.auth import require_auth, get_current_user
4from modules.layout import SelectosLayout
5from database.queries import get_all_roles
6
7@ui.page('/personal/usuarios')
8@require_auth
9def usuarios_personal_page():
10 user = get_current_user()
11 if user.get('rol') not in ["Administrador", "Gerente", "SST"]:
12 return ui.navigate.to('/')
13
14 roles = {r['id']: r['nombre'] for r in get_all_roles()}
15 empleados = {e['id']: e['nombre'] for e in get_empleados(activos_only=True)}
16
17 def refresh_table():
18 table.rows[:] = get_usuarios_personal()
19 table.update()
20
21 def abrir_form(usr=None):
22 with ui.dialog() as diag, ui.card().classes('w-[500px] p-0 rounded-[2rem] overflow-hidden shadow-2xl bg-white'):
23 with ui.row().classes('w-full bg-indigo-900 text-white p-8 items-center gap-4'):
24 ui.icon('manage_accounts', size='32px', color='indigo-200')
25 with ui.column().classes('gap-0'):
26 ui.label('Configuración de Acceso').classes('text-xl font-black')
27 ui.label('Control de credenciales y roles').classes('text-[10px] font-bold text-indigo-300 uppercase tracking-widest')
28
29 with ui.column().classes('w-full p-8 gap-6'):
30 with ui.column().classes('w-full gap-4'):
31 ui.label('VINCULACIÓN LABORAL').classes('text-[10px] font-black text-slate-400 tracking-widest')
32 emp = ui.select(empleados, label='Seleccionar Empleado *', value=usr['empleado_id'] if usr else None).props('outlined rounded-2xl').classes('w-full')
33
34 ui.label('CREDENCIALES DE ACCESO').classes('text-[10px] font-black text-slate-400 tracking-widest mt-2')
35 with ui.row().classes('w-full grid grid-cols-2 gap-4'):
36 uname = ui.input('Usuario / Login *', value=usr['username'] if usr else '').props('outlined rounded-2xl').classes('w-full')
37 with ui.column().classes('gap-1'):
38 pwd = ui.input('Nueva Contraseña *' if not usr else 'Reestablecer Contraseña', password=True, password_toggle_button=True).props('outlined rounded-2xl').classes('w-full')
39 if usr:
40 ui.label('Por seguridad (Hash SHA-256), no se puede ver la anterior. Escriba una nueva si desea cambiarla.').classes('text-[9px] font-bold text-slate-400 leading-tight')
41
42 rol_s = ui.select(roles, label='Nivel de Acceso (Rol) *', value=usr['rol_id'] if usr else None).props('outlined rounded-2xl').classes('w-full')
43
44 with ui.card().classes('w-full p-4 bg-slate-50 border-none rounded-2xl'):
45 act = ui.switch('Cuenta Habilitada / Activa', value=bool(usr['activo']) if usr else True).props('color=indigo').classes('font-bold text-slate-700')
46
47 async def salvar():
48 if not uname.value or (not usr and not pwd.value):
49 return ui.notify('Usuario y contraseña son obligatorios', type='warning', position='top')
50
51 payload = {
52 'id': usr['id'] if usr else None,
53 'username': uname.value,
54 'password': pwd.value if pwd.value else None,
55 'rol_id': rol_s.value,
56 'activo': 1 if act.value else 0,
57 'empleado_id': emp.value,
58 'nombre': empleados.get(emp.value, 'Usuario')
59 }
60 create_or_update_user(payload)
61 ui.notify('USUARIO CONFIGURADO EXITOSAMENTE', type='positive', position='top')
62 diag.close()
63 refresh_table()
64
65 with ui.row().classes('w-full justify-between items-center mt-6 pt-6 border-t border-slate-100'):
66 ui.button('Cancelar', on_click=diag.close).props('flat color=slate-400')
67 ui.button('GUARDAR CAMBIOS', icon='check_circle', on_click=salvar).props('unelevated color=indigo-600 rounded-2xl px-6').classes('font-black h-12 shadow-lg shadow-indigo-100')
68 diag.open()
69
70 with SelectosLayout('Gestión de Usuarios'):
71 with ui.row().classes('w-full justify-between items-center mb-10'):
72 with ui.column().classes('gap-1'):
73 ui.label('Accesos al Sistema').classes('text-3xl font-black text-slate-800')
74 ui.label('Administra quién puede entrar y con qué permisos.').classes('text-sm text-slate-400 font-bold')
75 ui.button('Crear Nuevo Usuario', icon='person_add', on_click=lambda: abrir_form()).props('unelevated color=indigo rounded-xl').classes('font-bold shadow-lg shadow-indigo-100')
76
77 cols = [
78 {'name': 'username', 'label': 'Usuario / Login', 'field': 'username', 'align': 'left', 'sortable': True},
79 {'name': 'empleado', 'label': 'Nombre Real', 'field': 'empleado_nombre', 'align': 'left'},
80 {'name': 'rol', 'label': 'Rol Asignado', 'field': 'rol_nombre', 'align': 'center'},
81 {'name': 'activo', 'label': 'Estado', 'field': 'activo', 'align': 'center'},
82 {'name': 'actions', 'label': 'Ajustes', 'field': 'actions', 'align': 'center'},
83 ]
84
85 table = ui.table(columns=cols, rows=get_usuarios_personal(), row_key='id').classes('w-full border-none shadow-sm rounded-2xl bg-white')
86 table.props('flat header-classes="bg-slate-50 text-slate-400 font-bold uppercase text-[10px]"')
87
88 table.add_slot('body-cell-activo', '''
89 <q-td :props="props">
90 <q-chip :color="props.value == 1 ? 'indigo-100' : 'slate-200'" :text-color="props.value == 1 ? 'indigo-900' : 'slate-500'" dense class="font-bold text-[10px] uppercase">
91 {{ props.value == 1 ? 'Activo' : 'Suspendido' }}
92 </q-chip>
93 </q-td>
94 ''')
95
96 table.add_slot('body-cell-actions', '''
97 <q-td :props="props">
98 <div class="row items-center justify-center gap-2">
99 <q-btn flat round color="indigo" icon="edit" size="sm" @click="$parent.$emit('manage', props.row)">
100 <q-tooltip>Modificar acceso o resetear contraseña</q-tooltip>
101 </q-btn>
102 <q-btn flat round color="red" icon="delete" size="sm" @click="$parent.$emit('delete_user', props.row)">
103 <q-tooltip>Eliminar Usuario</q-tooltip>
104 </q-btn>
105 </div>
106 </q-td>
107 ''')
108 table.on('manage', lambda e: abrir_form(e.args))
109
110 def confirmar_eliminacion(row):
111 def proceder():
112 delete_user(row['id'])
113 refresh_table()
114 ui.notify(f"Usuario '{row['username']}' eliminado del sistema.", type='positive', icon='delete')
115 conf.close()
116
117 with ui.dialog() as conf, ui.card().classes('p-6 rounded-[2rem] max-w-sm text-center'):
118 ui.icon('warning', size='xl', color='red')
119 ui.label(f"¿Eliminar cuenta de {row['username']}?").classes('text-xl font-black mt-2')
120 ui.label('Esta acción es irreversible y retirará todos sus permisos.').classes('text-xs text-slate-500 font-bold mb-6')
121 with ui.row().classes('w-full gap-2 justify-center'):
122 ui.button('Cancelar', on_click=conf.close).props('flat text-color=slate-500 rounded-xl')
123 ui.button('Eliminar Permanente', on_click=proceder, color='red').props('unelevated rounded-xl font-bold px-6 shadow-md')
124 conf.open()
125
126 table.on('delete_user', lambda e: confirmar_eliminacion(e.args))
127 