|
|
|
|
| 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
|
|
|
|
|
| from sqlalchemy import inspect, text
|
|
|
|
|
| try:
|
| from models import AvisoGlobal
|
| _HAS_AVISO_GLOBAL = True
|
| except Exception:
|
| _HAS_AVISO_GLOBAL = False
|
|
|
|
|
|
|
|
|
| MODAL_LISTA = ["", "AÉREO", "MARÍTIMO", "EXPRESSO"]
|
|
|
|
|
|
|
|
|
|
|
| def menu_info():
|
|
|
|
|
| doc_appendix()
|
|
|
| st.info("📌 Documentação interna do sistema. Acesso restrito a administradores.")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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")
|
|
|
|
|
| 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).
|
| """)
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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
|
|
|
|
|
| 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:
|
|
|
| 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(),
|
| )
|
|
|
| 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()
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
| 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"""
|
| <style>
|
| .ag-topbar-wrap-preview {{
|
| position: relative; /* preview não fixa no topo global */
|
| width: {largura};
|
| margin: 8px auto 10px auto;
|
| z-index: 10;
|
| background: {bg}; color: {fg};
|
| border: 1px solid rgba(0,0,0,.08);
|
| box-shadow: 0 2px 6px rgba(0,0,0,.06);
|
| border-radius: 10px;
|
| }}
|
| .ag-topbar-inner-preview {{
|
| display: flex; align-items: center;
|
| min-height: 44px; padding: 8px 14px; overflow: hidden;
|
| font-weight: 700; font-size: {font_size}px; letter-spacing: .2px;
|
| white-space: nowrap;
|
| }}
|
| .ag-topbar-marquee-preview > span {{
|
| display: inline-block; padding-left: 100%;
|
| animation: ag-marquee-preview {velocidade}s linear infinite;
|
| }}
|
| @keyframes ag-marquee-preview {{
|
| 0% {{ transform: translateX(0); }}
|
| 100% {{ transform: translateX(-100%); }}
|
| }}
|
| </style>
|
| <div class="ag-topbar-wrap-preview">
|
| <div class="ag-topbar-inner-preview {'ag-topbar-marquee-preview' if efeito=='marquee' else ''}">
|
| <span>{mensagem}</span>
|
| </div>
|
| </div>
|
| """,
|
| unsafe_allow_html=True
|
| )
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
| _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
|
|
|
| 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)
|
|
|
|
|
| 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")
|
|
|
|
|
| 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.")
|
|
|
|
|
|
|
|
|
|
|
| def main():
|
|
|
|
|
| is_admin = verificar_permissao("administracao")
|
|
|
|
|
| if is_admin:
|
| st.title("🔒 Administração")
|
|
|
| tab_editar, tab_aviso, tab_info = st.tabs([
|
| "✏️ Editar / Excluir Registros",
|
| "📣 Aviso Global (Topo)",
|
| "📘 Info do Sistema"
|
| ])
|
| else:
|
| st.title("✏️ Edição de Registros")
|
|
|
| (tab_editar,) = st.tabs(["✏️ Editar Registros"])
|
|
|
|
|
|
|
|
|
| if is_admin:
|
| with tab_info:
|
| menu_info()
|
|
|
|
|
|
|
|
|
| with tab_aviso:
|
| menu_aviso_global()
|
|
|
|
|
|
|
|
|
| with tab_editar:
|
|
|
|
|
|
|
|
|
| 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:
|
|
|
|
|
|
|
|
|
| st.subheader("🔎 Filtro de Busca (opcional)")
|
|
|
|
|
| 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})
|
|
|
| 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)
|
|
|
|
|
| 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 = 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)
|
|
|
|
|
| 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:
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
| 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])
|
|
|
|
|
| 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)
|
|
|
|
|
| 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
|
| )
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
| 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")
|
|
|
|
|
|
|
|
|
| with st.form("form_edicao"):
|
|
|
|
|
| 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()
|
|
|
|
|
| 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()
|
|
|
|
|
| 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 "")
|
|
|
|
|
| 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)
|
| )
|
|
|
|
|
|
|
| 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")
|
|
|
|
|
|
|
|
|
| if submit:
|
|
|
| if acao == "Salvar Alterações":
|
|
|
| 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
|
|
|
|
|
|
|