# -*- coding: utf-8 -*- import streamlit as st from datetime import datetime, date from banco import SessionLocal from models import Equipamento from log import registrar_log from utils_fpso import campo_fpso from utils_permissoes import verificar_permissao # 🔎 Utilitários SQLAlchemy para diagnóstico e migração simples from sqlalchemy import inspect, text # ⬇️ Import seguro do modelo AvisoGlobal (não quebra se ainda não existir) try: from models import AvisoGlobal _HAS_AVISO_GLOBAL = True except Exception: _HAS_AVISO_GLOBAL = False # ===================================================== # LISTAS FIXAS # ===================================================== MODAL_LISTA = ["", "AÉREO", "MARÍTIMO", "EXPRESSO"] # ===================================================== # MENU INFO (DOCUMENTAÇÃO INTERNA DO SISTEMA) # ===================================================== def menu_info(): # ✅ Apêndice de documentação: novas funcionalidades e módulos (adicional) doc_appendix() st.info("📌 Documentação interna do sistema. Acesso restrito a administradores.") # ===================================================== # APÊNDICE DE DOCUMENTAÇÃO (NOVAS FUNCIONALIDADES) # ===================================================== def doc_appendix(): """ Adendo de documentação profissional que descreve as novas funcionalidades, módulos e diretrizes sem alterar o comportamento existente. """ st.divider() st.subheader("📘 Atualizações e Diretrizes Profissionais") # ✅ NOVO: documentação padronizada do Módulo Formulário dentro do apêndice with st.expander("🧾 Módulo Formulário (padrão)", expanded=False): st.markdown(""" **Objetivo** Registrar, de forma padronizada, os dados operacionais de equipamentos (FPSO, Modal, OSM, MROB, métricas e administrativos), garantindo rastreabilidade e qualidade das informações. **Funcionalidades** - Sugestões para **FPSO** e **FPSO1** via `campo_fpso` - Campo controlado **“Outro”** quando aplicável - Validação de **campos obrigatórios** (ex.: FPSO, Modal, OSM, MROB) - Registro automático de **data/hora** (`data_hora_input`) - Persistência completa em **banco de dados** (tabela `equipamentos`) - **Auditoria**: ações de criação/edição/exclusão registradas **Campos Principais (Operacionais)** - **FPSO / FPSO1**: identificação - **Data de Coleta** - **Especialista / Conferente / OSM** - **Modal / Quantidade de Equipamentos / MROB** - **Métricas**: Linhas OSM, Linhas MROB, Linhas com Erro - **Erros**: Storekeeper, Operação WH, Especialista WH, Outros - **Inclusão / Exclusão** (D1, D2, D3) **Dados Administrativos** - **PO**, **Part Number**, **Material**, **Nota Fiscal** - **Solicitante / Requisitante** - **Impacto / Dimensão** - **Motivo** (Inclusão/Exclusão) - **Observações** (campo livre) **Validações** - Checagem de obrigatoriedade em campos críticos - Tratamento de valores ausentes (fallback seguro) - Índices/sugestões pré-carregados (FPSO/Modal/OSM) **Fluxo de Dados** 1. Usuário preenche o formulário com apoio de listas/sugestões 2. Sistema valida campos e persiste em `equipamentos` 3. Ação administrativa é registrada em **auditoria** (`log_acesso`) 4. Registros editáveis posteriormente via **Administração de Registros** **Perfis / Permissões** - Acesso controlado por **perfil** (admin / usuario / consulta) via `verificar_permissao` **Impacto** - **Padronização** dos cadastros - **Redução de erros** operacionais - **Rastreabilidade** completa (auditoria + carimbo de data/hora) """) with st.expander("📚 Estrutura de Módulos e Grupos (modules_map.py)", expanded=False): st.markdown(""" - **Grupos suportados**: Operação Load, Backload, Operação, Terceiros, BI. - Cada módulo deve ter: `key`, `label`, `descricao`, `perfis`, `grupo`. - O **menu lateral** exibe: `Pesquisar módulo` → `Selecione a operação (grupo)` → `Selecione o módulo`. - Grupos **sem módulos** (ou sem permissão) exibem: _“Em desenvolvimento”_. - **Boas práticas**: labels padronizados, `key` único (sem acentos e espaços), controle de acesso via `perfis`. """) with st.expander("🧭 Navegação e UI (menu lateral)", expanded=False): st.markdown(""" - **Pesquisa**: filtra módulos pelo `label`. - **Selectbox de Operação**: lista grupos disponíveis. - **Selectbox de Módulo**: exibe módulos filtrados por grupo e permissões. - **Rodapé da sidebar**: apresenta **e-mail do usuário logado** (badge alinhado) e bloco de **versão + desenvolvedor**. - **Layout**: `st.set_page_config(layout="wide")` habilitado, área de conteúdo responsiva. """) with st.expander("📧 E-mail do Usuário Logado (login + sidebar)", expanded=False): st.markdown(""" - `login.py` grava na sessão: `st.session_state.email` e `st.session_state.nome` (se disponíveis). - Rodapé da sidebar exibe o e-mail em **formato badge** com ícone e alinhamento (`inline-flex`). - Caso o e-mail não apareça: verifique se o usuário possui e-mail cadastrado e/ou revalide o login. """) with st.expander("🧾 Auditoria com E-mail", expanded=False): st.markdown(""" - O módulo de auditoria realiza **JOIN** com `Usuario` e agora inclui **E-mail** na consulta. - Exportação para Excel também leva a coluna **E-mail**. - Observação: `JOIN` padrão é interno; para logs órfãos, use `outerjoin` (se necessário). """) with st.expander("🛠️ Banco de Dados e Ferramentas (db_tools)", expanded=False): st.markdown(""" - Em **SQLite** e **PostgreSQL**, as alterações (ex.: adicionar `nome` e `email` em `usuarios`) podem ser aplicadas via módulo **`db_tools`** com `ALTER TABLE` e criação de índice único (`email`). - **Atenção**: `Base.metadata.create_all()` **não migra** tabelas existentes; para mudanças de esquema use `ALTER TABLE`, **Alembic** (recomendado) ou recrie o banco (backup antes). - **Verificação de colunas**: `PRAGMA table_info(usuarios)` (SQLite) ou `information_schema.columns` (Postgres/MySQL). """) with st.expander("🎮 Jogos / Treinamento (módulo jogos)", expanded=False): st.markdown(""" - **Jogo da Forca (Treinamento)**: perguntas por categoria, avanço de nível, contagem de tentativas. - **Caça ao Tesouro (Níveis)**: pistas Sim/Não com feedback visual e avanço até o limite de perguntas. - **Dado (Curiosidades)**: número de lados configurável, curiosidades de FPSO/Estoque/Óleo e Gás. - **Pontuação e balões**: opção de efeitos visuais e pontuação acumulada. """) with st.expander("🧠 Quiz e Ranking", expanded=False): st.markdown(""" - **Quiz**: perguntas dinâmicas via banco; fluxo ajustável (sem limitadores) e com opção de **Voltar ao sistema**. - **Ranking**: consolida pontuação por rodada/período e oferece exportação. """) with st.expander("🎨 Diretrizes de Layout e Acessibilidade", expanded=False): st.markdown(""" - **Responsividade**: usar `use_container_width=True` em tabelas/gráficos. - **Colunas fluidas**: `st.columns()` para KPIs (ajuste automático em telas menores). - **Expansores**: `st.expander()` para reduzir poluição visual. - **Temas**: arquivo `.streamlit/config.toml` pode definir `primaryColor`, `secondaryBackgroundColor`, etc. """) with st.expander("🔐 Segurança e Boas Práticas", expanded=False): st.markdown(""" - **Senhas**: sempre criptografadas (ex.: `utils_seguranca`), nunca armazenar em texto claro. - **Perfis**: `verificar_permissao(mod_id)` controla acesso; mantenha perfis atualizados. - **Auditoria**: registrar ações administrativas via `registrar_log(...)`. """) with st.expander("📦 Versionamento e Suporte", expanded=False): st.markdown(""" - **Versão atual**: exibida no rodapé da sidebar. - **Desenvolvedor**: contato visível na sidebar | Rodrigo Silva. - **Próximos passos**: documentação dos novos grupos/módulos, criação de migrations com Alembic, e manuais por equipe (Operação, Backload, Terceiros, BI). """) # ===================================================== # 🔔 Aviso Global — helpers # ===================================================== def _get_db_session_admin(): """ Sessão ciente do ambiente atual (via db_router, quando disponível). Fallback para SessionLocal(). """ try: from db_router import get_session_for_current_db # ajuste o nome se necessário return get_session_for_current_db() except Exception: return SessionLocal() def _sanitize_largura(largura_raw: str) -> str: val = (largura_raw or "").strip() if not val: return "100%" if val.endswith("%") or val.endswith("px"): return val if val.isdigit(): return f"{val}px" return "100%" def _obter_aviso_ativo_admin(): if not _HAS_AVISO_GLOBAL: return None db = _get_db_session_admin() try: return ( db.query(AvisoGlobal) .filter(AvisoGlobal.ativo == True) .order_by(AvisoGlobal.updated_at.desc(), AvisoGlobal.created_at.desc()) .first() ) except Exception: return None finally: try: db.close() except Exception: pass # 🔧 Diagnóstico e correção de schema (colunas) da tabela aviso_global def _verificar_schema_aviso_global(show_ui: bool = True) -> bool: """ Retorna True se o schema está OK (inclui font_size). Se show_ui=True, exibe UI com botão para criar coluna ausente. """ if not _HAS_AVISO_GLOBAL: if show_ui: st.error("Modelo AvisoGlobal não encontrado.") return False db = _get_db_session_admin() try: insp = inspect(db.bind) cols = [c["name"] for c in insp.get_columns("aviso_global")] falta_font = "font_size" not in cols if show_ui: with st.expander("🧪 Diagnóstico do schema (aviso_global)", expanded=False): st.caption("Colunas atuais: " + (", ".join(cols) if cols else "—")) if falta_font: st.warning("A coluna **font_size** não existe neste banco/ambiente.") col_btn1, col_btn2 = st.columns([1, 3]) if col_btn1.button("⚙️ Criar coluna font_size (DEFAULT 14)"): try: dialect = db.bind.dialect.name if dialect == "sqlite": sql = "ALTER TABLE aviso_global ADD COLUMN font_size INTEGER DEFAULT 14" elif dialect == "postgresql": sql = "ALTER TABLE aviso_global ADD COLUMN font_size integer DEFAULT 14" elif dialect in ("mysql", "mariadb"): sql = "ALTER TABLE aviso_global ADD COLUMN font_size INT DEFAULT 14" else: st.error(f"Dialeto não suportado para criação automática: {dialect}") return False db.execute(text(sql)) db.commit() st.success("Coluna 'font_size' criada com sucesso. Recarregando...") st.rerun() except Exception as e: db.rollback() st.error(f"Erro ao criar coluna: {e}") else: st.success("Schema OK ✔ (coluna 'font_size' presente).") return not falta_font except Exception as e: if show_ui: st.error(f"Falha ao inspecionar o schema: {e}") return False finally: try: db.close() except Exception: pass def _publicar_aviso_admin(mensagem, bg_color, text_color, largura, efeito, velocidade, font_size) -> bool: if not _HAS_AVISO_GLOBAL: return False db = _get_db_session_admin() try: # desativa os ativos db.query(AvisoGlobal).filter(AvisoGlobal.ativo == True).update({AvisoGlobal.ativo: False}) novo = AvisoGlobal( mensagem=(mensagem or "").strip(), bg_color=(bg_color or "#FFF3CD").strip(), text_color=(text_color or "#664D03").strip(), largura=_sanitize_largura(largura), efeito=efeito if efeito in ("marquee", "fixo") else "marquee", velocidade=max(5, min(int(velocidade or 20), 120)), ativo=True, updated_at=datetime.now(), ) # salva font_size quando o atributo/coluna existir (fallback seguro) try: setattr(novo, "font_size", max(10, min(int(font_size or 14), 48))) except Exception: pass db.add(novo) db.commit() db.expire_all() return True except Exception as e: db.rollback() # Diagnóstico visível para o admin st.error(f"Falha ao publicar o aviso: {e}") try: insp = inspect(db.bind) cols = [c["name"] for c in insp.get_columns("aviso_global")] st.caption("Colunas em aviso_global: " + ", ".join(cols)) except Exception: pass return False finally: try: db.close() except Exception: pass def _desativar_aviso_admin() -> bool: if not _HAS_AVISO_GLOBAL: return False db = _get_db_session_admin() try: db.query(AvisoGlobal).filter(AvisoGlobal.ativo == True)\ .update({AvisoGlobal.ativo: False, AvisoGlobal.updated_at: datetime.now()}) db.commit() db.expire_all() return True except Exception: db.rollback() return False finally: try: db.close() except Exception: pass # =============================== # 🔎 Pré-visualização do Aviso Global (somente render local) # =============================== def _render_preview_aviso_topbar(mensagem: str, bg_color: str, text_color: str, largura: str, efeito: str, velocidade: int, font_size: int): largura = _sanitize_largura(largura) bg = (bg_color or "#FFF3CD").strip() fg = (text_color or "#664D03").strip() efeito = (efeito or "marquee").lower() try: velocidade = int(velocidade or 20) except Exception: velocidade = 20 try: font_size = max(10, min(int(font_size or 14), 48)) except Exception: font_size = 14 st.markdown( f"""
""", unsafe_allow_html=True ) # ===================================================== # 🔔 Menu: Aviso Global (Topo) # ===================================================== def menu_aviso_global(): st.subheader("📣 Aviso Global (Topo)") st.caption("Envie um aviso global exibido no topo para todos os usuários.") perfil = (st.session_state.get("perfil") or "usuario").strip().lower() if perfil != "admin": st.warning("Apenas administradores podem publicar avisos globais.") return if not _HAS_AVISO_GLOBAL: st.error( "O modelo `AvisoGlobal` não foi encontrado em `models.py`." ) with st.expander("📄 Modelo necessário (copie para models.py)"): st.code( """from banco import Base from sqlalchemy import Column, Integer, String, Boolean, DateTime, Text from sqlalchemy.sql import func class AvisoGlobal(Base): __tablename__ = "aviso_global" id = Column(Integer, primary_key=True, index=True) mensagem = Column(Text, nullable=False) bg_color = Column(String(32), default="#FFF3CD") text_color = Column(String(32), default="#664D03") largura = Column(String(16), default="100%") efeito = Column(String(16), default="marquee") velocidade = Column(Integer, default=20) font_size = Column(Integer, default=14) # tamanho da fonte (px) ativo = Column(Boolean, default=True, index=True) created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())""", language="python", ) return # 🔎 Diagnóstico/migração simples do schema (font_size) _verificar_schema_aviso_global(show_ui=True) aviso_atual = _obter_aviso_ativo_admin() msg_default = aviso_atual.mensagem if aviso_atual else "" bg_default = aviso_atual.bg_color if aviso_atual else "#FFF3CD" fg_default = aviso_atual.text_color if aviso_atual else "#664D03" w_default = aviso_atual.largura if aviso_atual else "100%" ef_default = (aviso_atual.efeito if aviso_atual else "marquee") vel_default = int(aviso_atual.velocidade if aviso_atual else 20) fs_default = int(getattr(aviso_atual, "font_size", 14)) if aviso_atual else 14 # ⬅️ NOVO mensagem = st.text_input("Mensagem do aviso:", value=msg_default, placeholder="Ex.: Manutenção hoje às 18h...") colc1, colc2 = st.columns(2) bg_color = colc1.color_picker("Cor de fundo", value=bg_default) text_color = colc2.color_picker("Cor do texto", value=fg_default) colw1, colw2 = st.columns([2,1]) largura = colw1.text_input("Largura (ex.: 100% ou 1200px)", value=w_default) efeito = colw2.selectbox("Efeito", ["marquee", "fixo"], index=(0 if ef_default=="marquee" else 1)) colv1, colv2 = st.columns(2) velocidade = colv1.slider("Velocidade (segundos por ciclo)", min_value=5, max_value=120, value=vel_default, step=1, help="Usado apenas no modo 'marquee'.") font_size = colv2.slider("Tamanho da fonte (px)", min_value=10, max_value=48, value=fs_default, step=1) # ⬅️ NOVO # --- Pré-visualização ao vivo (sem salvar) --- st.markdown("**Pré-visualização:**") if (mensagem or "").strip(): _render_preview_aviso_topbar(mensagem, bg_color, text_color, largura, efeito, velocidade, font_size) else: st.info("Digite a mensagem para ver a pré-visualização aqui.") colb1, colb2, colb3 = st.columns(3) publicar = colb1.button("📢 Publicar/Atualizar aviso") desativar = colb2.button("🛑 Desativar aviso atual") atualizar_preview = colb3.button("🔄 Atualizar prévia") # Botão opcional de refresh da prévia (não salva nada; rerenderiza a página). if atualizar_preview: st.rerun() if publicar: if not (mensagem or "").strip(): st.warning("Digite a mensagem do aviso.") else: ok = _publicar_aviso_admin(mensagem, bg_color, text_color, largura, efeito, velocidade, font_size) if ok: try: registrar_log( usuario=st.session_state.get("usuario"), acao="PUBLICAR_AVISO_GLOBAL", tabela="aviso_global", registro_id=None ) except Exception: pass st.success("Aviso publicado/atualizado!") st.rerun() else: st.error("Não foi possível publicar o aviso. Verifique o banco/logs.") if desativar: ok = _desativar_aviso_admin() if ok: try: registrar_log( usuario=st.session_state.get("usuario"), acao="DESATIVAR_AVISO_GLOBAL", tabela="aviso_global", registro_id=None ) except Exception: pass st.info("Aviso desativado.") st.rerun() else: st.error("Não foi possível desativar o aviso.") # ===================================================== # ADMINISTRAÇÃO (variação com abas/tabs) # ===================================================== def main(): # ✅ Detecta se usuário é admin; abas administrativas aparecem apenas para admin. is_admin = verificar_permissao("administracao") # Título conforme perfil if is_admin: st.title("🔒 Administração") # Admin vê todas as abas tab_editar, tab_aviso, tab_info = st.tabs([ "✏️ Editar / Excluir Registros", "📣 Aviso Global (Topo)", "📘 Info do Sistema" ]) else: st.title("✏️ Edição de Registros") # Não-admin vê apenas a aba de edição (tab_editar,) = st.tabs(["✏️ Editar Registros"]) # ===================================================== # BLOCO: INFO DO SISTEMA (apenas admin) # ===================================================== if is_admin: with tab_info: menu_info() # ===================================================== # BLOCO: AVISO GLOBAL (apenas admin) # ===================================================== with tab_aviso: menu_aviso_global() # ===================================================== # BLOCO: EDIÇÃO / EXCLUSÃO (excluir só admin) # ===================================================== with tab_editar: # ===================================================== # FUNÇÃO UTILITÁRIA # ===================================================== def safe_index(lista, valor): """Evita erro quando o valor salvo no banco não existe na lista""" try: return lista.index(valor) except ValueError: return 0 db = SessionLocal() try: # ===================================================== # 🔎 FILTROS OPCIONAIS COM SUGESTÕES DO BANCO # (disponível para todos os perfis) # ===================================================== st.subheader("🔎 Filtro de Busca (opcional)") # IMPORTANTE: usar .distinct() sobre a coluna, como já estava fpsos = [""] + sorted({r.fpso for r in db.query(Equipamento.fpso).distinct() if r.fpso}) modais = [""] + sorted({r.modal for r in db.query(Equipamento.modal).distinct() if r.modal}) osms = [""] + sorted({r.osm for r in db.query(Equipamento.osm).distinct() if r.osm}) # 🟩 NOVO: lista de Nota Fiscal para multiselect assistido notas_dist = [""] + sorted({str(r.nota_fiscal) for r in db.query(Equipamento.nota_fiscal).distinct() if r.nota_fiscal}) col1, col2, col3, col4 = st.columns(4) with col1: filtro_fpso = st.selectbox("FPSO", fpsos) with col2: filtro_modal = st.selectbox("Modal", modais) with col3: filtro_osm = st.selectbox("OSM", osms) with col4: filtro_data = st.date_input("Data Coleta", value=None) # 🟩 NOVO: filtros de Nota Fiscal + opção de ver só duplicadas st.markdown("**🧾 Filtro por Nota Fiscal**") nf_col1, nf_col2, nf_col3 = st.columns([2, 2, 1.2]) with nf_col1: filtro_nf_text = st.text_input( "Digite uma ou mais NFs (separadas por vírgula)", value="" ) with nf_col2: filtro_nf_multi = st.multiselect( "Ou selecione", options=[x for x in notas_dist if x != ""] ) with nf_col3: mostrar_apenas_nf_duplicadas = st.checkbox( "Somente duplicadas", value=False ) # ===================================================== # QUERY BASE (COMPORTAMENTO ORIGINAL) + NOVO FILTRO NF # ===================================================== query = db.query(Equipamento) if filtro_fpso: query = query.filter(Equipamento.fpso == filtro_fpso) if filtro_modal: query = query.filter(Equipamento.modal == filtro_modal) if filtro_osm: query = query.filter(Equipamento.osm == filtro_osm) if filtro_data: query = query.filter(Equipamento.data_coleta == filtro_data) # 🟩 NOVO: aplica filtro de Nota Fiscal (tratando como string) notas_escolhidas = set() if filtro_nf_text.strip(): partes = [p.strip() for p in filtro_nf_text.split(",") if p.strip()] notas_escolhidas.update(partes) if filtro_nf_multi: notas_escolhidas.update([str(x).strip() for x in filtro_nf_multi if str(x).strip()]) if notas_escolhidas: # Como a coluna é do tipo texto no modelo, filtramos por igualdade textual. # Para outros dialetos/formatos numéricos, garantir cast adequado. query = query.filter(Equipamento.nota_fiscal.in_(list(notas_escolhidas))) registros = query.order_by(Equipamento.id.desc()).all() if not registros: st.info("Nenhum registro encontrado.") return # ===================================================== # 🧭 SINALIZAÇÃO DE NF DUPLICADA (no conjunto filtrado) # ===================================================== # Monta DF auxiliar só com campos relevantes para contagem de NF import pandas as pd df_aux = pd.DataFrame([{ "ID": r.id, "Nota Fiscal": ("" if r.nota_fiscal is None else str(r.nota_fiscal).strip()) } for r in registros]) # Contagem de ocorrências por NF (string, ignorando vazias) if not df_aux.empty: contagem = df_aux.loc[df_aux["Nota Fiscal"] != "", "Nota Fiscal"].value_counts() notas_duplicadas = contagem[contagem > 1] else: notas_duplicadas = pd.Series(dtype=int) # Aviso e expander com a lista das duplicadas if len(notas_duplicadas.index) > 0: total_ocorrencias = int(notas_duplicadas.sum()) st.warning( f"⚠️ Foram encontradas **{total_ocorrencias}** ocorrências em **{len(notas_duplicadas)}** " f"números de Nota Fiscal duplicados no resultado filtrado." ) with st.expander("Ver lista de notas duplicadas"): st.dataframe( notas_duplicadas.rename("Ocorrências").reset_index().rename(columns={"index": "Nota Fiscal"}), use_container_width=True ) # Se marcado: mantém na lista apenas as duplicadas if mostrar_apenas_nf_duplicadas: set_dup = set(notas_duplicadas.index.tolist()) registros = [r for r in registros if (r.nota_fiscal is not None and str(r.nota_fiscal).strip() in set_dup)] if not registros: st.info("Nenhum registro duplicado após aplicar o filtro de 'Somente duplicadas'.") return else: if mostrar_apenas_nf_duplicadas: st.info("Não há notas duplicadas no conjunto filtrado.") return # ===================================================== # SELECTBOX DE ESCOLHA E FORMULÁRIO # ===================================================== mapa = { f"ID {r.id} | FPSO {r.fpso} | {r.modal} | {r.osm} | {r.data_coleta} | NF: {r.nota_fiscal or '—'}": r.id for r in registros } escolha = st.selectbox("Selecione o registro", list(mapa.keys())) registro = db.get(Equipamento, mapa[escolha]) st.divider() st.subheader("✏️ Editar Registro") # ===================================================== # FORMULÁRIO COMPLETO (MESMO DO MÓDULO FORMULÁRIO) # ===================================================== with st.form("form_edicao"): # ================== DADOS OPERACIONAIS ================== st.subheader("📦 Dados Operacionais") col1, col2, col3 = st.columns(3) with col1: fpso1 = campo_fpso("FPSO1", registro.fpso1) fpso = campo_fpso("FPSO", registro.fpso) data_coleta = st.date_input("Data de Coleta", registro.data_coleta) especialista = st.text_input("Especialista", registro.especialista or "") conferente = st.text_input("Conferente", registro.conferente or "") osm = st.text_input("OSM", registro.osm or "") with col2: modal = st.selectbox( "Modal", MODAL_LISTA, index=safe_index(MODAL_LISTA, registro.modal) ) quant_equip = st.number_input( "Quantidade de Equipamentos", min_value=0, value=registro.quant_equip or 0 ) mrob = st.text_input("MROB", registro.mrob or "") with col3: linhas_osm = st.number_input("Total de Linhas OSM", value=registro.linhas_osm or 0) linhas_mrob = st.number_input("Total de Linhas MROB", value=registro.linhas_mrob or 0) linhas_erros = st.number_input("Total de Linhas com Erro", value=registro.linhas_erros or 0) st.divider() # ================== ANÁLISE DE ERROS ================== st.subheader("⚠️ Análise de Erros") op_sim_nao = ["", "Sim", "Não"] col_e1, col_e2, col_e3, col_e4 = st.columns(4) with col_e1: erro_storekeeper = st.selectbox( "Storekeeper", op_sim_nao, index=safe_index(op_sim_nao, registro.erro_storekeeper) ) with col_e2: erro_operacao = st.selectbox( "Operação WH", op_sim_nao, index=safe_index(op_sim_nao, registro.erro_operacao) ) with col_e3: erro_especialista = st.selectbox( "Especialista WH", op_sim_nao, index=safe_index(op_sim_nao, registro.erro_especialista) ) with col_e4: erro_outros = st.selectbox( "Outros", op_sim_nao, index=safe_index(op_sim_nao, registro.erro_outros) ) op_inc_exc = ["", "INCLUSÃO", "EXCLUSÃO"] inclusao_exclusao = st.selectbox( "Inclusão / Exclusão", op_inc_exc, index=safe_index(op_inc_exc, registro.inclusao_exclusao) ) st.divider() # ================== DADOS ADMINISTRATIVOS ================== st.subheader("🧾 Dados Administrativos") col_a1, col_a2, col_a3 = st.columns(3) with col_a1: po = st.text_input("PO", registro.po or "") part_number = st.text_input("Part Number", registro.part_number or "") with col_a2: material = st.text_input("Material", registro.material or "") nota_fiscal = st.text_input("Nota Fiscal", registro.nota_fiscal or "") with col_a3: solicitante = st.text_input("Solicitante", registro.solicitante or "") requisitante = st.text_input("Requisitante", registro.requisitante or "") impacto = st.text_input("Impacto", registro.impacto or "") dimensao = st.text_input("Dimensão", registro.dimensao or "") # ✅ AJUSTE: corrigido para 'motivo' motivo = st.text_input("Motivo da Inclusão / Exclusão", registro.motivo or "") observacoes = st.text_area( "Observações", registro.observacoes or "", height=120 ) op_dia = ["", "D1", "D2", "D3"] dia_inclusao = st.selectbox( "Dia de Inclusão (D)", op_dia, index=safe_index(op_dia, registro.dia_inclusao) ) # ================== AÇÃO ================== # 🔐 Apenas admin pode excluir opcoes_acao = ["Salvar Alterações"] + (["Excluir Registro"] if is_admin else []) acao = st.radio( "Ação", opcoes_acao, horizontal=True ) submit = st.form_submit_button("Confirmar") # ===================================================== # AÇÕES # ===================================================== if submit: if acao == "Salvar Alterações": # Atualiza todos os campos dinamicamente (exceto id) for campo in registro.__table__.columns.keys(): if campo != "id": setattr(registro, campo, locals().get(campo, getattr(registro, campo))) registro.data_hora_input = datetime.now() db.commit() try: registrar_log( usuario=st.session_state.get("usuario"), acao="EDITAR", tabela="equipamentos", registro_id=registro.id ) except Exception: pass st.success("✅ Registro atualizado com sucesso!") st.rerun() elif acao == "Excluir Registro" and is_admin: db.delete(registro) db.commit() try: registrar_log( usuario=st.session_state.get("usuario"), acao="EXCLUIR", tabela="equipamentos", registro_id=registro.id ) except Exception: pass st.success("🗑️ Registro excluído com sucesso!") st.rerun() finally: try: db.close() except Exception: pass