Roudrigus commited on
Commit
8591822
·
verified ·
1 Parent(s): eeb2ebb

Update usuarios_admin.py

Browse files
Files changed (1) hide show
  1. usuarios_admin.py +374 -385
usuarios_admin.py CHANGED
@@ -1,385 +1,374 @@
1
-
2
- import streamlit as st
3
- from banco import SessionLocal
4
- from models import Usuario
5
- from utils_seguranca import gerar_hash_senha
6
- from utils_auditoria import registrar_log
7
- import re # para validação simples de e-mail
8
- from datetime import date # ✅ para trabalhar com st.date_input
9
-
10
-
11
- def email_valido(email: str) -> bool:
12
- """
13
- Validação simples do formato de e-mail.
14
- Evita espaços e verifica partes básicas (usuario@dominio.tld).
15
- """
16
- if not email:
17
- return False
18
- padrao = r"^[^@\s]+@[^@\s]+\.[^@\s]+$"
19
- return re.match(padrao, email.strip()) is not None
20
-
21
-
22
- def main():
23
- st.title("👥 Administração de Usuários")
24
-
25
- # 🔐 Segurança extra
26
- if st.session_state.get("perfil") != "admin":
27
- st.error("❌ Acesso restrito ao administrador.")
28
- return
29
-
30
- db = SessionLocal()
31
-
32
- try:
33
- # ==========================
34
- # CADASTRO DE NOVO USUÁRIO
35
- # ==========================
36
- st.subheader("➕ Cadastrar novo usuário")
37
-
38
- with st.form("form_novo_usuario"):
39
- usuario = st.text_input("Usuário")
40
- senha = st.text_input("Senha", type="password")
41
-
42
- perfil = st.selectbox(
43
- "Perfil",
44
- ["admin", "usuario", "consulta"]
45
- )
46
-
47
- ativo = st.checkbox("Usuário ativo", value=True)
48
-
49
- # Novo campo: e-mail
50
- email = st.text_input("E-mail (corporativo)", placeholder="nome.sobrenome@empresa.com")
51
-
52
- # ✅ NOVO: Data de aniversário (opcional) com limites explícitos + key único
53
- data_aniversario = st.date_input(
54
- "Data de aniversário (opcional)",
55
- value=None, # deixa sem valor inicial
56
- format="DD/MM/YYYY",
57
- min_value=date(1900, 1, 1),
58
- max_value=date.today(),
59
- key="data_aniv_novo" # 🔑 força widget novo com limites corretos
60
- )
61
-
62
- salvar = st.form_submit_button("💾 Criar usuário")
63
-
64
- if salvar:
65
- # Validação mínima
66
- if not usuario or not senha:
67
- st.warning("⚠️ Preencha usuário e senha.")
68
- elif not email or not email_valido(email):
69
- st.warning("⚠️ Informe um e-mail válido.")
70
- else:
71
- existe_login = db.query(Usuario).filter(Usuario.usuario == usuario).first()
72
- existe_email = db.query(Usuario).filter(Usuario.email == email.strip()).first()
73
-
74
- if existe_login:
75
- st.error("Usuário já existe.")
76
- elif existe_email:
77
- st.error(" E-mail cadastrado para outro usuário.")
78
- else:
79
- novo = Usuario(
80
- usuario=usuario,
81
- senha=gerar_hash_senha(senha),
82
- perfil=perfil,
83
- ativo=ativo,
84
- email=email.strip() # ✅ grava e-mail
85
- )
86
-
87
- # NOVO: grava data de aniversário se seu models suportar
88
- # - Se 'data_aniversario' vier de st.date_input como 'date' do Python,
89
- # vamos converter para string ISO (YYYY-MM-DD) se seu models usar String,
90
- # ou atribuir diretamente se seu models usar Date.
91
- if data_aniversario:
92
- try:
93
- # tenta atribuir diretamente (funciona se o campo for Date)
94
- setattr(novo, "data_aniversario", data_aniversario)
95
- except Exception:
96
- try:
97
- # fallback para String ISO (YYYY-MM-DD)
98
- setattr(novo, "data_aniversario", data_aniversario.isoformat())
99
- except Exception:
100
- # se o atributo não existir, não quebra a criação
101
- pass
102
-
103
- db.add(novo)
104
- db.commit()
105
- db.refresh(novo) # 🔑 garante ID
106
-
107
- # 🧾 AUDITORIA CORRETA
108
- registrar_log(
109
- usuario=st.session_state.get("usuario"),
110
- acao=f"Criou usuário {usuario} (email: {email.strip()})",
111
- tabela="usuarios",
112
- registro_id=novo.id
113
- )
114
-
115
- st.success("✅ Usuário criado com sucesso!")
116
- st.rerun()
117
-
118
- st.divider()
119
-
120
- # ==========================
121
- # LISTAGEM DE USUÁRIOS
122
- # ==========================
123
- st.subheader("📋 Usuários cadastrados")
124
-
125
- usuarios = db.query(Usuario).order_by(Usuario.usuario).all()
126
-
127
- if not usuarios:
128
- st.info("Nenhum usuário cadastrado.")
129
- return
130
-
131
- # 🔸 Estados auxiliares para edição/exclusão
132
- if "edit_user_id" not in st.session_state:
133
- st.session_state.edit_user_id = None
134
-
135
- for u in usuarios:
136
- # 👤 Cabeçalho do usuário com perfil
137
- with st.expander(f"👤 {u.usuario} ({u.perfil})"):
138
- col1, col2 = st.columns(2)
139
-
140
- with col1:
141
- st.write(f"**Perfil:** {u.perfil}")
142
- st.write(f"**Ativo:** {'✅ Sim' if u.ativo else '❌ Não'}")
143
- # ✅ Mostra o e-mail, se houver
144
- st.write(f"**E-mail:** {u.email or '—'}")
145
-
146
- # ✅ NOVO: Mostra a data de aniversário, se houver
147
- try:
148
- # tenta formatar se for objeto date
149
- if getattr(u, "data_aniversario", None):
150
- if isinstance(u.data_aniversario, date):
151
- st.write(f"**Aniversário:** {u.data_aniversario.strftime('%d/%m/%Y')}")
152
- else:
153
- # se for string ISO (YYYY-MM-DD)
154
- st.write(f"**Aniversário:** {str(u.data_aniversario)}")
155
- else:
156
- st.write("**Aniversário:** —")
157
- except Exception:
158
- st.write("**Aniversário:** —")
159
-
160
- with col2:
161
- novo_status = st.checkbox(
162
- "Usuário ativo",
163
- value=u.ativo,
164
- key=f"ativo_{u.id}"
165
- )
166
-
167
- if novo_status != u.ativo:
168
- u.ativo = novo_status
169
- db.commit()
170
-
171
- # 🧾 AUDITORIA STATUS
172
- registrar_log(
173
- usuario=st.session_state.get("usuario"),
174
- acao=f"{'Ativou' if novo_status else 'Desativou'} usuário {u.usuario}",
175
- tabela="usuarios",
176
- registro_id=u.id
177
- )
178
-
179
- st.success("Status atualizado")
180
- st.rerun()
181
-
182
- st.markdown("---")
183
-
184
- # ==========================
185
- # ✏️ BOTÕES: EDITAR e EXCLUIR
186
- # ==========================
187
- col_e1, col_e2 = st.columns([1, 1])
188
-
189
- # Clicar em "Editar" abre o formulário desse usuário
190
- if col_e1.button("✏️ Editar", key=f"btn_editar_{u.id}"):
191
- st.session_state.edit_user_id = u.id
192
- # 🔧 defensivo: limpa qualquer estado antigo do date_input desta linha
193
- st.session_state.pop(f"data_aniv_{u.id}", None)
194
- st.rerun()
195
-
196
- # Clicar em "Excluir" abre confirmação
197
- if col_e2.button("🗑️ Excluir", key=f"btn_excluir_{u.id}"):
198
- st.session_state[f"show_delete_{u.id}"] = True
199
- st.rerun()
200
-
201
- # ==========================
202
- # ✏️ FORMULÁRIO DE EDIÇÃO (por usuário)
203
- # ==========================
204
- if st.session_state.edit_user_id == u.id:
205
- with st.form(key=f"form_editar_{u.id}"):
206
- st.subheader("✏️ Editar usuário")
207
-
208
- novo_usuario = st.text_input("Usuário (login)", value=u.usuario)
209
- novo_nome = st.text_input("Nome completo (opcional)", value=(u.nome or ""))
210
- novo_email = st.text_input("E-mail (corporativo)", value=(u.email or ""))
211
-
212
- novo_perfil = st.selectbox(
213
- "Perfil",
214
- ["admin", "usuario", "consulta"],
215
- index=["admin", "usuario", "consulta"].index(u.perfil)
216
- )
217
-
218
- novo_ativo = st.checkbox("Usuário ativo", value=u.ativo)
219
-
220
- # NOVO: Data de aniversário (opcional) na edição
221
- # Inicializa valor padrão de forma segura
222
- valor_data = None
223
- try:
224
- if getattr(u, "data_aniversario", None):
225
- if isinstance(u.data_aniversario, date):
226
- valor_data = u.data_aniversario
227
- else:
228
- # tentativa de parse se for string ISO (YYYY-MM-DD)
229
- # (mantemos simples para não quebrar: se parse falhar, continua None)
230
- parts = str(u.data_aniversario).split("-")
231
- if len(parts) == 3:
232
- yy, mm, dd = map(int, parts)
233
- valor_data = date(yy, mm, dd)
234
- except Exception:
235
- valor_data = None
236
-
237
- # 🔧 Se o valor atual estiver fora dos limites novos, usa None
238
- min_d, max_d = date(1900, 1, 1), date.today()
239
- if valor_data and not (min_d <= valor_data <= max_d):
240
- valor_data = None
241
-
242
- novo_aniversario = st.date_input(
243
- "Data de aniversário (opcional)",
244
- value=valor_data,
245
- format="DD/MM/YYYY",
246
- min_value=min_d, # ⬅️ permite anos antigos
247
- max_value=max_d, # ⬅️ bloqueia datas futuras
248
- key=f"data_aniv_{u.id}" # 🔑 key único por usuário evita reuso de estado
249
- )
250
-
251
- st.markdown("**🔒 Reset de senha (opcional)**")
252
- nova_senha = st.text_input("Nova senha", type="password", help="Preencha para redefinir a senha do usuário.")
253
- confirm_senha = st.text_input("Confirme a nova senha", type="password")
254
-
255
- salvar_edicao = st.form_submit_button("💾 Salvar alterações")
256
- cancelar_edicao = st.form_submit_button("❌ Cancelar edição")
257
-
258
- # Cancela edição sem perder a lista
259
- if cancelar_edicao:
260
- st.session_state.edit_user_id = None
261
- st.info("Edição cancelada.")
262
- st.rerun()
263
-
264
- if salvar_edicao:
265
- # Validações
266
- if novo_email and not email_valido(novo_email):
267
- st.warning("⚠️ Informe um e-mail válido.")
268
- elif nova_senha and (nova_senha.strip() != confirm_senha.strip()):
269
- st.error("❌ As senhas não conferem.")
270
- else:
271
- try:
272
- # Verifica conflito de login
273
- if novo_usuario.strip() != u.usuario:
274
- conflito_login = db.query(Usuario).filter(Usuario.usuario == novo_usuario.strip()).first()
275
- if conflito_login and conflito_login.id != u.id:
276
- st.error("❌ Já existe outro usuário com esse login.")
277
- st.stop()
278
-
279
- # Verifica conflito de e-mail
280
- if novo_email.strip() and (novo_email.strip() != (u.email or "").strip()):
281
- conflito_email = db.query(Usuario).filter(Usuario.email == novo_email.strip()).first()
282
- if conflito_email and conflito_email.id != u.id:
283
- st.error("❌ E-mail já cadastrado para outro usuário.")
284
- st.stop()
285
-
286
- # Atualiza campos no objeto atual
287
- u.usuario = novo_usuario.strip()
288
- u.nome = (novo_nome or "").strip() or None
289
- u.email = novo_email.strip() or None
290
- u.perfil = novo_perfil
291
- u.ativo = novo_ativo
292
-
293
- # NOVO: Atualiza data de aniversário se fornecida
294
- if novo_aniversario:
295
- try:
296
- # se o campo for Date, funciona direto
297
- setattr(u, "data_aniversario", novo_aniversario)
298
- except Exception:
299
- try:
300
- # fallback para String ISO
301
- setattr(u, "data_aniversario", novo_aniversario.isoformat())
302
- except Exception:
303
- pass
304
- else:
305
- # permite limpar o campo (definir None)
306
- try:
307
- setattr(u, "data_aniversario", None)
308
- except Exception:
309
- pass
310
-
311
- # Reset de senha, se informado
312
- if nova_senha.strip():
313
- u.senha = gerar_hash_senha(nova_senha.strip())
314
-
315
- # �� Merge para garantir que a sessão aplique mudanças de forma robusta
316
- db.merge(u)
317
- db.commit()
318
-
319
- # Auditoria
320
- registrar_log(
321
- usuario=st.session_state.get("usuario"),
322
- acao=f"Editou usuário {u.usuario}",
323
- tabela="usuarios",
324
- registro_id=u.id
325
- )
326
-
327
- st.success("✅ Usuário atualizado com sucesso!")
328
- # Mantém o formulário aberto após o rerun (mesmo user) para visibilidade
329
- st.rerun()
330
- except Exception as e:
331
- db.rollback()
332
- st.error(f"Falha ao atualizar usuário: {e}")
333
-
334
- # ==========================
335
- # 🗑️ CONFIRMAÇÃO DE EXCLUSÃO (por usuário)
336
- # ==========================
337
- if st.session_state.get(f"show_delete_{u.id}"):
338
- with st.expander("⚠️ Confirmar exclusão", expanded=True):
339
- st.warning(
340
- "Esta ação irá **excluir definitivamente** o usuário selecionado. "
341
- "Não há como desfazer."
342
- )
343
- confirmar = st.checkbox("Eu entendo o risco e desejo prosseguir.", key=f"chk_del_{u.id}")
344
- codigo = st.text_input(
345
- f"Digite `DELETE {u.usuario}` para confirmar:",
346
- key=f"txt_del_{u.id}"
347
- )
348
- pode_excluir = confirmar and (codigo.strip() == f"DELETE {u.usuario}")
349
-
350
- col_x1, col_x2 = st.columns([1, 1])
351
- if col_x1.button("🗑️ Excluir usuário", disabled=not pode_excluir, type="primary", key=f"btn_del_conf_{u.id}"):
352
- try:
353
- # Evita excluir a si próprio (boa prática)
354
- if u.usuario == st.session_state.get("usuario"):
355
- st.error("❌ Você não pode excluir a si mesmo enquanto está logado.")
356
- else:
357
- db.delete(u)
358
- db.commit()
359
-
360
- registrar_log(
361
- usuario=st.session_state.get("usuario"),
362
- acao=f"Excluiu usuário {u.usuario}",
363
- tabela="usuarios",
364
- registro_id=u.id
365
- )
366
-
367
- st.success(" Usuário excluído com sucesso.")
368
- st.session_state[f"show_delete_{u.id}"] = False
369
- st.rerun()
370
- except Exception as e:
371
- db.rollback()
372
- st.error(f"Falha ao excluir usuário: {e}")
373
-
374
- if col_x2.button("❌ Cancelar", key=f"btn_del_cancel_{u.id}"):
375
- st.session_state[f"show_delete_{u.id}"] = False
376
- st.info("Exclusão cancelada.")
377
- st.rerun()
378
-
379
- finally:
380
- db.close()
381
-
382
-
383
-
384
-
385
-
 
1
+ # -*- coding: utf-8 -*-
2
+ import re
3
+ from datetime import date
4
+ from typing import Optional
5
+
6
+ import streamlit as st
7
+ from banco import SessionLocal
8
+ from models import Usuario
9
+ from utils_seguranca import gerar_hash_senha
10
+ from utils_auditoria import registrar_log
11
+
12
+
13
+ # -----------------------------
14
+ # Helpers de validação / parsing
15
+ # -----------------------------
16
+ _EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
17
+
18
+ def email_valido(email: Optional[str]) -> bool:
19
+ """Validação simples de e-mail (usuario@dominio.tld)."""
20
+ if not email:
21
+ return False
22
+ return _EMAIL_RE.match(email.strip()) is not None
23
+
24
+ def _try_parse_iso_to_date(s: Optional[str]) -> Optional[date]:
25
+ """Converte 'YYYY-MM-DD' date. Retorna None se falhar."""
26
+ if not s:
27
+ return None
28
+ try:
29
+ yy, mm, dd = map(int, str(s).split("-"))
30
+ return date(yy, mm, dd)
31
+ except Exception:
32
+ return None
33
+
34
+ def _set_data_aniversario(model_obj, valor: Optional[date]):
35
+ """
36
+ Seta data de aniversário no objeto:
37
+ - Se coluna for Date: atribuição direta
38
+ - Se for String: ISO (YYYY-MM-DD)
39
+ - Se a coluna não existir, não quebra
40
+ """
41
+ try:
42
+ if valor is None:
43
+ setattr(model_obj, "data_aniversario", None)
44
+ return
45
+ # tenta Date
46
+ setattr(model_obj, "data_aniversario", valor)
47
+ except Exception:
48
+ try:
49
+ setattr(model_obj, "data_aniversario", valor.isoformat() if valor else None)
50
+ except Exception:
51
+ pass
52
+
53
+
54
+ # -----------------------------
55
+ # UI principal
56
+ # -----------------------------
57
+ def main():
58
+ st.title("👥 Administração de Usuários")
59
+
60
+ # 🔐 Segurança extra
61
+ if (st.session_state.get("perfil") or "").strip().lower() != "admin":
62
+ st.error(" Acesso restrito ao administrador.")
63
+ return
64
+
65
+ db = SessionLocal()
66
+ try:
67
+ # ==================================================
68
+ # CADASTRO DE NOVO USUÁRIO
69
+ # ==================================================
70
+ st.subheader("➕ Cadastrar novo usuário")
71
+
72
+ with st.form("form_novo_usuario"):
73
+ col_a, col_b = st.columns([1, 1])
74
+ with col_a:
75
+ usuario = st.text_input("Usuário (login)")
76
+ senha = st.text_input("Senha", type="password")
77
+ perfil = st.selectbox("Perfil", ["admin", "usuario", "consulta"])
78
+ ativo = st.checkbox("Usuário ativo", value=True)
79
+ with col_b:
80
+ email = st.text_input("E-mail (corporativo)", placeholder="nome.sobrenome@empresa.com")
81
+ # 🗓️ Data de aniversário opcional (evita erro de value=None no date_input)
82
+ informar_aniv = st.checkbox("Informar data de aniversário?")
83
+ data_aniversario_val = None
84
+ if informar_aniv:
85
+ data_aniversario_val = st.date_input(
86
+ "Data de aniversário",
87
+ value=date(1990, 1, 1),
88
+ min_value=date(1900, 1, 1),
89
+ max_value=date.today(),
90
+ key="data_aniv_novo"
91
+ )
92
+
93
+ salvar = st.form_submit_button("💾 Criar usuário")
94
+
95
+ if salvar:
96
+ # --- Validações
97
+ if not usuario or not senha:
98
+ st.warning("⚠️ Preencha usuário e senha.")
99
+ elif not email or not email_valido(email):
100
+ st.warning("⚠️ Informe um e-mail válido.")
101
+ else:
102
+ existe_login = db.query(Usuario).filter(Usuario.usuario == usuario.strip()).first()
103
+ existe_email = db.query(Usuario).filter(Usuario.email == (email or "").strip()).first()
104
+
105
+ if existe_login:
106
+ st.error("❌ Usuário já existe.")
107
+ elif existe_email:
108
+ st.error("❌ E-mail já cadastrado para outro usuário.")
109
+ else:
110
+ try:
111
+ novo = Usuario(
112
+ usuario=usuario.strip(),
113
+ senha=gerar_hash_senha(senha.strip()),
114
+ perfil=perfil,
115
+ ativo=ativo,
116
+ email=(email or "").strip() or None,
117
+ )
118
+ # Data de aniversário (opcional)
119
+ if informar_aniv and data_aniversario_val:
120
+ _set_data_aniversario(novo, data_aniversario_val)
121
+
122
+ db.add(novo)
123
+ db.commit()
124
+ db.refresh(novo)
125
+
126
+ try:
127
+ registrar_log(
128
+ usuario=st.session_state.get("usuario"),
129
+ acao=f"Criou usuário {novo.usuario} (email: {novo.email or '—'})",
130
+ tabela="usuarios",
131
+ registro_id=getattr(novo, "id", None),
132
+ )
133
+ except Exception:
134
+ pass
135
+
136
+ st.success("✅ Usuário criado com sucesso!")
137
+ st.rerun()
138
+ except Exception as e:
139
+ db.rollback()
140
+ st.error(f"Falha ao criar usuário: {e}")
141
+
142
+ st.divider()
143
+
144
+ # ==================================================
145
+ # 📋 LISTAGEM / EDIÇÃO / EXCLUSÃO
146
+ # ==================================================
147
+ st.subheader("📋 Usuários cadastrados")
148
+
149
+ usuarios = db.query(Usuario).order_by(Usuario.usuario).all()
150
+ if not usuarios:
151
+ st.info("Nenhum usuário cadastrado.")
152
+ return
153
+
154
+ st.session_state.setdefault("edit_user_id", None)
155
+
156
+ for u in usuarios:
157
+ with st.expander(f"👤 {u.usuario} ({u.perfil})", expanded=False):
158
+ # --------- Cabeçalho de dados ao lado ---------
159
+ col1, col2 = st.columns([1, 1])
160
+ with col1:
161
+ st.write(f"**Perfil:** {u.perfil}")
162
+ st.write(f"**Ativo:** {'✅ Sim' if u.ativo else '❌ Não'}")
163
+ st.write(f"**E-mail:** {u.email or '—'}")
164
+
165
+ # Mostra aniversário (date ou string ISO)
166
+ try:
167
+ if getattr(u, "data_aniversario", None):
168
+ if isinstance(u.data_aniversario, date):
169
+ st.write(f"**Aniversário:** {u.data_aniversario.strftime('%d/%m/%Y')}")
170
+ else:
171
+ st.write(f"**Aniversário:** {str(u.data_aniversario)}")
172
+ else:
173
+ st.write("**Aniversário:** —")
174
+ except Exception:
175
+ st.write("**Aniversário:** —")
176
+
177
+ with col2:
178
+ novo_status = st.checkbox(
179
+ "Usuário ativo",
180
+ value=bool(u.ativo),
181
+ key=f"ativo_{u.id}"
182
+ )
183
+ if novo_status != u.ativo:
184
+ try:
185
+ u.ativo = bool(novo_status)
186
+ db.commit()
187
+ try:
188
+ registrar_log(
189
+ usuario=st.session_state.get("usuario"),
190
+ acao=f"{'Ativou' if novo_status else 'Desativou'} usuário {u.usuario}",
191
+ tabela="usuarios",
192
+ registro_id=u.id
193
+ )
194
+ except Exception:
195
+ pass
196
+ st.success("Status atualizado.")
197
+ st.rerun()
198
+ except Exception as e:
199
+ db.rollback()
200
+ st.error(f"Falha ao atualizar status: {e}")
201
+
202
+ st.markdown("---")
203
+
204
+ # --------- Ações: Editar / Excluir ---------
205
+ col_e1, col_e2 = st.columns([1, 1])
206
+
207
+ if col_e1.button("✏️ Editar", key=f"btn_editar_{u.id}"):
208
+ st.session_state.edit_user_id = u.id
209
+ st.session_state.pop(f"data_aniv_{u.id}", None)
210
+ st.rerun()
211
+
212
+ if col_e2.button("🗑️ Excluir", key=f"btn_excluir_{u.id}"):
213
+ st.session_state[f"show_delete_{u.id}"] = True
214
+ st.rerun()
215
+
216
+ # --------- Formulário de Edição ---------
217
+ if st.session_state.edit_user_id == u.id:
218
+ with st.form(key=f"form_editar_{u.id}"):
219
+ st.subheader("✏️ Editar usuário")
220
+ colx, coly = st.columns([1, 1])
221
+ with colx:
222
+ novo_usuario = st.text_input("Usuário (login)", value=u.usuario)
223
+ novo_nome = st.text_input("Nome completo (opcional)", value=(u.nome or ""))
224
+ novo_email = st.text_input("E-mail (corporativo)", value=(u.email or ""))
225
+ novo_perfil = st.selectbox(
226
+ "Perfil",
227
+ ["admin", "usuario", "consulta"],
228
+ index=["admin", "usuario", "consulta"].index(u.perfil) if u.perfil in ["admin", "usuario", "consulta"] else 1
229
+ )
230
+ novo_ativo = st.checkbox("Usuário ativo", value=bool(u.ativo))
231
+
232
+ with coly:
233
+ # Valor atual de aniversário (date ou ISO string)
234
+ valor_data = None
235
+ try:
236
+ cur = getattr(u, "data_aniversario", None)
237
+ if isinstance(cur, date):
238
+ valor_data = cur
239
+ else:
240
+ valor_data = _try_parse_iso_to_date(cur)
241
+ except Exception:
242
+ valor_data = None
243
+
244
+ min_d, max_d = date(1900, 1, 1), date.today()
245
+ # UI opcional para data
246
+ informar_aniv_edit = st.checkbox("Alterar data de aniversário?", value=(valor_data is not None), key=f"ck_aniv_{u.id}")
247
+ novo_aniversario = None
248
+ if informar_aniv_edit:
249
+ novo_aniversario = st.date_input(
250
+ "Data de aniversário (opcional)",
251
+ value=valor_data or date(1990, 1, 1),
252
+ min_value=min_d,
253
+ max_value=max_d,
254
+ key=f"data_aniv_{u.id}"
255
+ )
256
+
257
+ st.markdown("**🔒 Reset de senha (opcional)**")
258
+ nova_senha = st.text_input("Nova senha", type="password", help="Preencha para redefinir a senha do usuário.")
259
+ confirm_senha= st.text_input("Confirme a nova senha", type="password")
260
+
261
+ salvar_edicao = st.form_submit_button("💾 Salvar alterações")
262
+ cancelar_edicao = st.form_submit_button("❌ Cancelar edição")
263
+
264
+ if cancelar_edicao:
265
+ st.session_state.edit_user_id = None
266
+ st.info("Edição cancelada.")
267
+ st.rerun()
268
+
269
+ if salvar_edicao:
270
+ # Validações
271
+ if novo_email and not email_valido(novo_email):
272
+ st.warning("⚠️ Informe um e-mail válido.")
273
+ elif nova_senha and (nova_senha.strip() != confirm_senha.strip()):
274
+ st.error("❌ As senhas não conferem.")
275
+ else:
276
+ try:
277
+ # Conflito de login
278
+ if novo_usuario.strip() != u.usuario:
279
+ conflito_login = db.query(Usuario).filter(Usuario.usuario == novo_usuario.strip()).first()
280
+ if conflito_login and conflito_login.id != u.id:
281
+ st.error("❌ existe outro usuário com esse login.")
282
+ st.stop()
283
+
284
+ # Conflito de e-mail
285
+ novo_email_norm = (novo_email or "").strip()
286
+ if novo_email_norm and (novo_email_norm != (u.email or "").strip()):
287
+ conflito_email = db.query(Usuario).filter(Usuario.email == novo_email_norm).first()
288
+ if conflito_email and conflito_email.id != u.id:
289
+ st.error("❌ E-mail já cadastrado para outro usuário.")
290
+ st.stop()
291
+
292
+ # Atualiza campos
293
+ u.usuario = novo_usuario.strip()
294
+ u.nome = (novo_nome or "").strip() or None
295
+ u.email = novo_email_norm or None
296
+ u.perfil = novo_perfil
297
+ u.ativo = bool(novo_ativo)
298
+
299
+ # Atualiza aniversário
300
+ if informar_aniv_edit:
301
+ _set_data_aniversario(u, novo_aniversario)
302
+ else:
303
+ _set_data_aniversario(u, None)
304
+
305
+ # Reset de senha
306
+ if (nova_senha or "").strip():
307
+ u.senha = gerar_hash_senha(nova_senha.strip())
308
+
309
+ db.merge(u)
310
+ db.commit()
311
+
312
+ try:
313
+ registrar_log(
314
+ usuario=st.session_state.get("usuario"),
315
+ acao=f"Editou usuário {u.usuario}",
316
+ tabela="usuarios",
317
+ registro_id=u.id
318
+ )
319
+ except Exception:
320
+ pass
321
+
322
+ st.success(" Usuário atualizado com sucesso!")
323
+ st.rerun()
324
+ except Exception as e:
325
+ db.rollback()
326
+ st.error(f"Falha ao atualizar usuário: {e}")
327
+
328
+ # --------- Confirmação de Exclusão ---------
329
+ if st.session_state.get(f"show_delete_{u.id}"):
330
+ with st.expander("⚠️ Confirmar exclusão", expanded=True):
331
+ st.warning(
332
+ "Esta ação irá **excluir definitivamente** o usuário selecionado. "
333
+ "Não há como desfazer."
334
+ )
335
+ confirmar = st.checkbox("Eu entendo o risco e desejo prosseguir.", key=f"chk_del_{u.id}")
336
+ codigo = st.text_input(f"Digite `DELETE {u.usuario}` para confirmar:", key=f"txt_del_{u.id}")
337
+ pode_excluir = confirmar and (codigo.strip() == f"DELETE {u.usuario}")
338
+
339
+ col_x1, col_x2 = st.columns([1, 1])
340
+ if col_x1.button("🗑️ Excluir usuário", disabled=not pode_excluir, type="primary", key=f"btn_del_conf_{u.id}"):
341
+ try:
342
+ if u.usuario == st.session_state.get("usuario"):
343
+ st.error(" Você não pode excluir a si mesmo enquanto está logado.")
344
+ else:
345
+ db.delete(u)
346
+ db.commit()
347
+
348
+ try:
349
+ registrar_log(
350
+ usuario=st.session_state.get("usuario"),
351
+ acao=f"Excluiu usuário {u.usuario}",
352
+ tabela="usuarios",
353
+ registro_id=u.id
354
+ )
355
+ except Exception:
356
+ pass
357
+
358
+ st.success("✅ Usuário excluído com sucesso.")
359
+ st.session_state[f"show_delete_{u.id}"] = False
360
+ st.rerun()
361
+ except Exception as e:
362
+ db.rollback()
363
+ st.error(f"Falha ao excluir usuário: {e}")
364
+
365
+ if col_x2.button("❌ Cancelar", key=f"btn_del_cancel_{u.id}"):
366
+ st.session_state[f"show_delete_{u.id}"] = False
367
+ st.info("Exclusão cancelada.")
368
+ st.rerun()
369
+
370
+ finally:
371
+ try:
372
+ db.close()
373
+ except Exception:
374
+ pass