|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|