| |
|
| | import streamlit as st
|
| | from dotenv import load_dotenv
|
| | from datetime import date, datetime, time
|
| |
|
| |
|
| | from utils_operacao import obter_grupos_disponiveis, obter_modulos_para_grupo
|
| |
|
| |
|
| | st.set_page_config(layout="wide")
|
| |
|
| |
|
| | load_dotenv()
|
| |
|
| |
|
| |
|
| |
|
| | import formulario
|
| | import consulta
|
| | import relatorio
|
| | import administracao
|
| | import quiz
|
| | import ranking
|
| | import quiz_admin
|
| | import usuarios_admin
|
| | import videos
|
| | import auditoria
|
| | import importar_excel
|
| | import calendario
|
| | import auditoria_cleanup
|
| | import jogos
|
| | import db_tools
|
| | import db_admin
|
| | import db_monitor
|
| | import operacao
|
| | import db_export_import
|
| | import resposta
|
| | import outlook_relatorio
|
| | import repositorio_load
|
| | import Produtividade_Especialista as produtividade_especialista
|
| | import rnc
|
| | import rnc_listagem
|
| | import rnc_relatorio
|
| | import sugestoes_usuario
|
| | import repo_rnc
|
| | import recebimento
|
| |
|
| | from utils_info import INFO_CONTEUDO, INFO_MODULOS, INFO_MAP_PAGINA_ID
|
| | from login import login
|
| | from utils_permissoes import verificar_permissao
|
| | from utils_layout import exibir_logo
|
| | from modules_map import MODULES
|
| | from banco import engine, Base, SessionLocal
|
| | from models import QuizPontuacao
|
| | from models import IOIRunSugestao
|
| | from models import AvisoGlobal
|
| |
|
| |
|
| | from uuid import uuid4
|
| | from sqlalchemy import text, func, or_
|
| |
|
| |
|
| | try:
|
| | from db_router import current_db_choice, bank_label
|
| | _HAS_ROUTER = True
|
| | except Exception:
|
| | _HAS_ROUTER = False
|
| | def current_db_choice() -> str:
|
| | return "prod"
|
| | def bank_label(choice: str) -> str:
|
| | return "🟢 Produção" if choice == "prod" else "🔴 Teste"
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | def _get_query_params():
|
| | """Compat: retorna query params como dict (Streamlit novo/antigo)."""
|
| | try:
|
| |
|
| | return dict(st.query_params)
|
| | except Exception:
|
| |
|
| | try:
|
| | return dict(st.experimental_get_query_params())
|
| | except Exception:
|
| | return {}
|
| |
|
| | def _set_query_params(new_params: dict):
|
| | """Compat: define query params (Streamlit novo/antigo)."""
|
| | try:
|
| | st.query_params = new_params
|
| | except Exception:
|
| | try:
|
| | st.experimental_set_query_params(**new_params)
|
| | except Exception:
|
| | pass
|
| |
|
| | def _check_rerun_qs(pagina_atual: str = ""):
|
| | """
|
| | Se a URL contiver rr=1 (ou true), força um rerun e limpa o parâmetro para evitar loop.
|
| | ✅ Não dispara quando estiver na página 'resposta' (Inbox Admin).
|
| | ✅ Consome apenas uma vez por sessão.
|
| | ✅ (PATCH) Também não dispara quando estiver em 'outlook_relatorio' para não interromper leitura COM.
|
| | """
|
| | try:
|
| | if st.session_state.get("__qs_rr_consumed__", False):
|
| | return
|
| |
|
| |
|
| | if pagina_atual in ("resposta", "outlook_relatorio", "formulario"):
|
| | return
|
| |
|
| | params = _get_query_params()
|
| | rr_raw = params.get("rr", ["0"])
|
| | rr = rr_raw[0] if isinstance(rr_raw, (list, tuple)) else str(rr_raw)
|
| | if str(rr).lower() in ("1", "true"):
|
| | new_params = {k: v for k, v in params.items() if k != "rr"}
|
| | _set_query_params(new_params)
|
| | st.session_state["__qs_rr_consumed__"] = True
|
| | st.rerun()
|
| | except Exception:
|
| | pass
|
| |
|
| |
|
| |
|
| |
|
| | def _get_db_session():
|
| | """Retorna uma sessão de banco consistente com o ambiente atual."""
|
| | try:
|
| | from db_router import get_session_for_current_db
|
| | return get_session_for_current_db()
|
| | except Exception:
|
| | pass
|
| |
|
| | try:
|
| | from db_router import get_engine_for_current_db
|
| | from sqlalchemy.orm import sessionmaker
|
| | Eng = get_engine_for_current_db()
|
| | return sessionmaker(bind=Eng)()
|
| | except Exception:
|
| | pass
|
| |
|
| | return SessionLocal()
|
| |
|
| |
|
| |
|
| |
|
| | Base.metadata.create_all(bind=engine)
|
| |
|
| | def quiz_respondido_hoje(usuario: str) -> bool:
|
| |
|
| | db = _get_db_session()
|
| | try:
|
| | inicio_dia = datetime.combine(date.today(), time.min)
|
| | return (
|
| | db.query(QuizPontuacao)
|
| | .filter(
|
| | QuizPontuacao.usuario == usuario,
|
| | QuizPontuacao.data >= inicio_dia
|
| | )
|
| | .first()
|
| | is not None
|
| | )
|
| | finally:
|
| | try:
|
| | db.close()
|
| | except Exception:
|
| | pass
|
| |
|
| |
|
| |
|
| |
|
| | _SESS_TTL_MIN = 5
|
| |
|
| | def _get_session_id() -> str:
|
| | if "_sid" not in st.session_state:
|
| | st.session_state["_sid"] = f"{uuid4()}"
|
| | return st.session_state["_sid"]
|
| |
|
| | def _ensure_sessao_table(db) -> None:
|
| | """Cria a tabela sessao_web caso não exista (SQLite/Postgres/MySQL)."""
|
| | dialect = db.bind.dialect.name
|
| | if dialect == "sqlite":
|
| | db.execute(text("""
|
| | CREATE TABLE IF NOT EXISTS sessao_web (
|
| | id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| | usuario TEXT NOT NULL,
|
| | session_id TEXT NOT NULL UNIQUE,
|
| | last_seen TIMESTAMP NOT NULL,
|
| | ativo INTEGER NOT NULL DEFAULT 1
|
| | )
|
| | """))
|
| | elif dialect in ("postgresql", "postgres"):
|
| | db.execute(text("""
|
| | CREATE TABLE IF NOT EXISTS sessao_web (
|
| | id SERIAL PRIMARY KEY,
|
| | usuario TEXT NOT NULL,
|
| | session_id TEXT NOT NULL UNIQUE,
|
| | last_seen TIMESTAMPTZ NOT NULL,
|
| | ativo BOOLEAN NOT NULL DEFAULT TRUE
|
| | )
|
| | """))
|
| | else:
|
| | db.execute(text("""
|
| | CREATE TABLE IF NOT EXISTS sessao_web (
|
| | id INT AUTO_INCREMENT PRIMARY KEY,
|
| | usuario VARCHAR(255) NOT NULL,
|
| | session_id VARCHAR(255) NOT NULL UNIQUE,
|
| | last_seen TIMESTAMP NOT NULL,
|
| | ativo TINYINT(1) NOT NULL DEFAULT 1
|
| | )
|
| | """))
|
| | db.commit()
|
| |
|
| | def _session_heartbeat(usuario: str) -> None:
|
| | """Atualiza/insere a sessão ativa do usuário com last_seen = now() e faz limpeza básica."""
|
| | if not usuario:
|
| | return
|
| | db = _get_db_session()
|
| | try:
|
| | _ensure_sessao_table(db)
|
| | sid = _get_session_id()
|
| | now_sql = "CURRENT_TIMESTAMP"
|
| |
|
| | upd = db.execute(
|
| | text(f"UPDATE sessao_web SET last_seen = {now_sql}, ativo = 1 WHERE session_id = :sid"),
|
| | {"sid": sid}
|
| | )
|
| | if upd.rowcount == 0:
|
| | db.execute(
|
| | text(f"INSERT INTO sessao_web (usuario, session_id, last_seen, ativo) "
|
| | f"VALUES (:usuario, :sid, {now_sql}, 1)"),
|
| | {"usuario": usuario, "sid": sid}
|
| | )
|
| |
|
| | dialect = db.bind.dialect.name
|
| | if dialect in ("postgresql", "postgres"):
|
| | cleanup_sql = f"UPDATE sessao_web SET ativo = FALSE WHERE last_seen < (NOW() - INTERVAL '{_SESS_TTL_MIN * 2} minutes')"
|
| | elif dialect == "sqlite":
|
| | cleanup_sql = f"UPDATE sessao_web SET ativo = 0 WHERE last_seen < datetime(CURRENT_TIMESTAMP, '-{_SESS_TTL_MIN * 2} minutes')"
|
| | else:
|
| | cleanup_sql = f"UPDATE sessao_web SET ativo = 0 WHERE last_seen < DATE_SUB(CURRENT_TIMESTAMP, INTERVAL {_SESS_TTL_MIN * 2} MINUTE)"
|
| | db.execute(text(cleanup_sql))
|
| | db.commit()
|
| | except Exception:
|
| | db.rollback()
|
| | finally:
|
| | try:
|
| | db.close()
|
| | except Exception:
|
| | pass
|
| |
|
| | def _get_active_users_count() -> int:
|
| | """Conta usuários distintos com last_seen dentro da janela (_SESS_TTL_MIN) e ativo=1."""
|
| | db = _get_db_session()
|
| | try:
|
| | _ensure_sessao_table(db)
|
| | dialect = db.bind.dialect.name
|
| | if dialect in ("postgresql", "postgres"):
|
| | threshold = f"(NOW() - INTERVAL '{_SESS_TTL_MIN} minutes')"
|
| | elif dialect == "sqlite":
|
| | threshold = f"datetime(CURRENT_TIMESTAMP, '-{_SESS_TTL_MIN} minutes')"
|
| | else:
|
| | threshold = f"DATE_SUB(CURRENT_TIMESTAMP, INTERVAL {_SESS_TTL_MIN} MINUTE)"
|
| | res = db.execute(
|
| | text(f"SELECT COUNT(DISTINCT usuario) AS c FROM sessao_web WHERE ativo = 1 AND last_seen >= {threshold}")
|
| | ).fetchone()
|
| | return int(res[0] if res and res[0] is not None else 0)
|
| | except Exception:
|
| | return 0
|
| | finally:
|
| | try:
|
| | db.close()
|
| | except Exception:
|
| | pass
|
| |
|
| | def _mark_session_inactive() -> None:
|
| | """Marca a sessão atual como inativa (chamar no logout)."""
|
| | sid = st.session_state.get("_sid")
|
| | if not sid:
|
| | return
|
| | db = _get_db_session()
|
| | try:
|
| | _ensure_sessao_table(db)
|
| | db.execute(text("UPDATE sessao_web SET ativo = 0 WHERE session_id = :sid"), {"sid": sid})
|
| | db.commit()
|
| | except Exception:
|
| | db.rollback()
|
| | finally:
|
| | try:
|
| | db.close()
|
| | except Exception:
|
| | pass
|
| |
|
| |
|
| |
|
| |
|
| | 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(db):
|
| | try:
|
| | aviso = (
|
| | db.query(AvisoGlobal)
|
| | .filter(AvisoGlobal.ativo == True)
|
| | .order_by(AvisoGlobal.updated_at.desc(), AvisoGlobal.created_at.desc())
|
| | .first()
|
| | )
|
| | return aviso
|
| | except Exception:
|
| | return None
|
| |
|
| |
|
| |
|
| |
|
| | def _render_aviso_global_topbar():
|
| | try:
|
| | db = _get_db_session()
|
| | except Exception as e:
|
| | st.sidebar.warning(f"Aviso desativado: sessão indisponível ({e})")
|
| | return
|
| |
|
| | aviso = None
|
| | try:
|
| | aviso = obter_aviso_ativo(db)
|
| | except Exception as e:
|
| | st.sidebar.warning(f"Aviso desativado: falha ao consultar ({e})")
|
| | aviso = None
|
| | finally:
|
| | try:
|
| | db.close()
|
| | except Exception:
|
| | pass
|
| |
|
| | if not aviso:
|
| | return
|
| |
|
| | try:
|
| | largura = _sanitize_largura(aviso.largura)
|
| | bg = aviso.bg_color or "#FFF3CD"
|
| | fg = aviso.text_color or "#664D03"
|
| | efeito = (aviso.efeito or "marquee").lower()
|
| | velocidade = int(aviso.velocidade or 20)
|
| | try:
|
| | font_size = max(10, min(int(getattr(aviso, "font_size", 14)), 48))
|
| | except Exception:
|
| | font_size = 14
|
| |
|
| | altura = 52
|
| |
|
| | st.markdown(
|
| | f"""
|
| | <style>
|
| | /* Não derrube overlays do Streamlit */
|
| | .stApp::before,
|
| | header[data-testid="stHeader"],
|
| | [data-testid="stToolbar"],
|
| | [data-testid="stDecoration"],
|
| | [data-testid="collapsedControl"],
|
| | .stApp [class*="stDialog"] {{
|
| | z-index: 1 !important;
|
| | }}
|
| | /* Reserva espaço para a barra */
|
| | [data-testid="stAppViewContainer"] {{
|
| | padding-top: {altura + 8}px !important;
|
| | }}
|
| |
|
| | .ag-topbar-wrap {{
|
| | position: fixed;
|
| | top: 0;
|
| | left: 0;
|
| | width: {largura};
|
| | z-index: 2147483647 !important;
|
| | background: {bg};
|
| | color: {fg};
|
| | border-bottom: 1px solid rgba(0,0,0,.12);
|
| | box-shadow: 0 2px 8px rgba(0,0,0,.15);
|
| | border-radius: 0 0 10px 10px;
|
| | pointer-events: none;
|
| | }}
|
| | .ag-topbar-inner {{
|
| | display: flex;
|
| | align-items: center;
|
| | height: {altura}px;
|
| | padding: 0 14px;
|
| | overflow: hidden;
|
| | font-weight: 700;
|
| | font-size: {font_size}px;
|
| | letter-spacing: .2px;
|
| | white-space: nowrap;
|
| | }}
|
| | .ag-topbar-marquee > span {{
|
| | display: inline-block;
|
| | padding-left: 100%;
|
| | animation: ag-marquee {velocidade}s linear infinite;
|
| | }}
|
| | @keyframes ag-marquee {{
|
| | 0% {{ transform: translateX(0); }}
|
| | 100% {{ transform: translateX(-100%); }}
|
| | }}
|
| | /* Acessibilidade: reduz movimento */
|
| | @media (prefers-reduced-motion: reduce) {{
|
| | .ag-topbar-marquee > span {{
|
| | animation: none !important;
|
| | padding-left: 0;
|
| | }}
|
| | }}
|
| | @media (max-width: 500px) {{
|
| | .ag-topbar-inner {{
|
| | font-size: {max(10, font_size-3)}px;
|
| | padding: 0 8px;
|
| | height: 44px;
|
| | }}
|
| | [data-testid="stAppViewContainer"] {{
|
| | padding-top: 52px !important;
|
| | }}
|
| | }}
|
| | </style>
|
| |
|
| | <div class="ag-topbar-wrap">
|
| | <div class="ag-topbar-inner {'ag-topbar-marquee' if efeito=='marquee' else ''}">
|
| | <span>{aviso.mensagem}</span>
|
| | </div>
|
| | </div>
|
| | """,
|
| | unsafe_allow_html=True
|
| | )
|
| | except Exception as e:
|
| | st.sidebar.warning(f"Aviso desativado: erro de render ({e})")
|
| | return
|
| |
|
| |
|
| |
|
| |
|
| | def logout():
|
| | """Finaliza a sessão do usuário, limpa estados e recarrega a aplicação."""
|
| | _mark_session_inactive()
|
| | st.session_state.logado = False
|
| | st.session_state.usuario = None
|
| | st.session_state.perfil = None
|
| | st.session_state.nome = None
|
| | st.session_state.email = None
|
| | st.session_state.quiz_verificado = False
|
| | st.rerun()
|
| |
|
| |
|
| |
|
| |
|
| | def _show_birthday_banner_if_needed():
|
| | if st.session_state.get("__show_birthday__"):
|
| | st.session_state["__show_birthday__"] = False
|
| | st.markdown(
|
| | """
|
| | <style>
|
| | .confetti-wrapper { position: relative; width: 100%; height: 0; }
|
| | .confetti-area { position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
| | pointer-events: none; z-index: 9999; }
|
| | .confetti { position: absolute; top: -5%; font-size: 24px; animation-name: confetti-fall;
|
| | animation-timing-function: linear; animation-iteration-count: 1; }
|
| | @keyframes confetti-fall {
|
| | 0% { transform: translateY(-5vh) rotate(0deg); opacity: 1; }
|
| | 100% { transform: translateY(105vh) rotate(360deg); opacity: 0; }
|
| | }
|
| | .confetti:nth-child(1) { left: 5%; animation-duration: 3.5s; }
|
| | .confetti:nth-child(2) { left: 12%; animation-duration: 4.0s; }
|
| | .confetti:nth-child(3) { left: 20%; animation-duration: 3.2s; }
|
| | .confetti:nth-child(4) { left: 28%; animation-duration: 4.3s; }
|
| | .confetti:nth-child(5) { left: 36%; animation-duration: 3.8s; }
|
| | .confetti:nth-child(6) { left: 44%; animation-duration: 4.1s; }
|
| | .confetti:nth-child(7) { left: 52%; animation-duration: 3.4s; }
|
| | .confetti:nth-child(8) { left: 60%; animation-duration: 4.4s; }
|
| | .confetti:nth-child(9) { left: 68%; animation-duration: 3.9s; }
|
| | .confetti:nth-child(10) { left: 76%; animation-duration: 4.2s; }
|
| | .confetti:nth-child(11) { left: 84%; animation-duration: 3.6s; }
|
| | .confetti:nth-child(12) { left: 92%; animation-duration: 4.0s; }
|
| | </style>
|
| | <div class="confetti-wrapper">
|
| | <div class="confetti-area">
|
| | <div class="confetti">🎊</div><div class="confetti">🎉</div>
|
| | <div class="confetti">🎊</div><div class="confetti">🎉</div>
|
| | <div class="confetti">🎊</div><div class="confetti">🎉</div>
|
| | <div class="confetti">🎊</div><div class="confetti">🎉</div>
|
| | <div class="confetti">🎊</div><div class="confetti">🎉</div>
|
| | <div class="confetti">🎊</div><div class="confetti">🎉</div>
|
| | </div>
|
| | </div>
|
| | """,
|
| | unsafe_allow_html=True
|
| | )
|
| | st.balloons()
|
| | nome = st.session_state.get("nome") or st.session_state.get("usuario") or "Usuário"
|
| | st.markdown(
|
| | f"""
|
| | <div style="display:flex;justify-content:center;align-items:center;text-align:center;width:100%;margin:40px 0;">
|
| | <div style="font-size: 36px; font-weight: 800; color:#A020F0;
|
| | background:linear-gradient(90deg,#FFF0F6,#F0E6FF);
|
| | padding:20px 30px; border-radius:16px; box-shadow:0 4px 10px rgba(0,0,0,.08);">
|
| | 🎉 Feliz Aniversário, {nome}! 🎉
|
| | </div>
|
| | </div>
|
| | """,
|
| | unsafe_allow_html=True
|
| | )
|
| | COR_FRASE = "#0d6efd"
|
| | st.markdown(
|
| | f"""
|
| | <div style="display:flex;justify-content:center;align-items:center;text-align:center;width:100%;margin-top:10px;">
|
| | <div style="font-size: 20px; font-weight: 500; color:{COR_FRASE}; padding:10px;">
|
| | Desejamos a você muitas conquistas e bons embarques ao longo do ano! 💜
|
| | </div>
|
| | </div>
|
| | """,
|
| | unsafe_allow_html=True
|
| | )
|
| |
|
| |
|
| |
|
| |
|
| | def main():
|
| |
|
| | if "logado" not in st.session_state:
|
| | st.session_state.logado = False
|
| | if "usuario" not in st.session_state:
|
| | st.session_state.usuario = None
|
| | if "quiz_verificado" not in st.session_state:
|
| | st.session_state.quiz_verificado = False
|
| | if "user_responses_viewed" not in st.session_state:
|
| | st.session_state.user_responses_viewed = False
|
| | if "nav_target" not in st.session_state:
|
| | st.session_state.nav_target = None
|
| |
|
| |
|
| | st.session_state.setdefault("__auto_refresh_interval_sec__", 60)
|
| |
|
| |
|
| | if not st.session_state.logado:
|
| | st.session_state.quiz_verificado = False
|
| | exibir_logo(top=True, sidebar=False)
|
| | login()
|
| | return
|
| |
|
| |
|
| | _session_heartbeat(st.session_state.usuario)
|
| | if (st.session_state.get("perfil") or "").strip().lower() == "admin":
|
| | try:
|
| | online_now = _get_active_users_count()
|
| | except Exception:
|
| | online_now = 0
|
| | st.sidebar.markdown(
|
| | f"""
|
| | <div style="padding:8px 10px;margin-top:6px;margin-bottom:6px;border-radius:8px;
|
| | background:#1e293b; color:#e2e8f0; border:1px solid #334155;">
|
| | <span style="font-size:13px;">🟢 Online (últimos {_SESS_TTL_MIN} min)</span><br>
|
| | <span style="font-size:22px;font-weight:800;">{online_now}</span>
|
| | </div>
|
| | """,
|
| | unsafe_allow_html=True
|
| | )
|
| |
|
| |
|
| | st.sidebar.markdown("---")
|
| |
|
| | col_reload, col_interval = st.sidebar.columns([1, 1])
|
| | if col_reload.button("🔄 Recarregar (sem sair)", key="__btn_reload_now__"):
|
| | st.rerun()
|
| |
|
| |
|
| | if hasattr(st, "popover"):
|
| | with col_interval.popover("⏱️ Autoatualização"):
|
| | new_val = st.number_input(
|
| | "Intervalo (segundos) — 0 desativa",
|
| | min_value=0, max_value=3600,
|
| | value=int(st.session_state["__auto_refresh_interval_sec__"]),
|
| | step=5, key="__auto_refresh_input__"
|
| | )
|
| | if st.button("Aplicar intervalo", key="__btn_apply_auto_refresh__"):
|
| | st.session_state["__auto_refresh_interval_sec__"] = int(new_val)
|
| | try:
|
| | if int(new_val) > 0:
|
| | st.toast(f"Autoatualização ajustada para {int(new_val)}s.", icon="⏱️")
|
| | else:
|
| | st.toast("Autoatualização desativada.", icon="⛔")
|
| | except Exception:
|
| | pass
|
| | st.rerun()
|
| | else:
|
| | with st.sidebar.expander("⏱️ Autoatualização", expanded=False):
|
| | new_val = st.number_input(
|
| | "Intervalo (segundos) — 0 desativa",
|
| | min_value=0, max_value=3600,
|
| | value=int(st.session_state["__auto_refresh_interval_sec__"]),
|
| | step=5, key="__auto_refresh_input__"
|
| | )
|
| | if st.button("Aplicar intervalo", key="__btn_apply_auto_refresh__"):
|
| | st.session_state["__auto_refresh_interval_sec__"] = int(new_val)
|
| | try:
|
| | if int(new_val) > 0:
|
| | st.toast(f"Autoatualização ajustada para {int(new_val)}s.", icon="⏱️")
|
| | else:
|
| | st.toast("Autoatualização desativada.", icon="⛔")
|
| | except Exception:
|
| | pass
|
| | st.rerun()
|
| |
|
| | usuario = st.session_state.usuario
|
| | perfil = (st.session_state.get("perfil") or "usuario").strip().lower()
|
| |
|
| |
|
| | if not st.session_state.quiz_verificado:
|
| | if not quiz_respondido_hoje(usuario):
|
| | exibir_logo(top=True, sidebar=False)
|
| | quiz.main()
|
| | return
|
| | else:
|
| | st.session_state.quiz_verificado = True
|
| | st.rerun()
|
| |
|
| |
|
| | exibir_logo(top=True, sidebar=True)
|
| | _render_aviso_global_topbar()
|
| | _show_birthday_banner_if_needed()
|
| |
|
| | st.sidebar.markdown("### Menu | 🎉 Carnaval 🎭 2026!")
|
| |
|
| |
|
| | try:
|
| | banco_label = bank_label(current_db_choice()) if _HAS_ROUTER else (
|
| | "🟢 Produção" if current_db_choice() == "prod" else "🔴 Teste"
|
| | )
|
| | st.sidebar.caption(f"🗄️ Banco ativo: {banco_label}")
|
| | except Exception:
|
| | pass
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | if perfil == "admin":
|
| | try:
|
| | db = _get_db_session()
|
| | pendentes = db.query(IOIRunSugestao).filter(func.lower(IOIRunSugestao.status) == "pendente").count()
|
| | except Exception:
|
| | pendentes = 0
|
| | finally:
|
| | try: db.close()
|
| | except Exception: pass
|
| |
|
| | if pendentes > 0:
|
| | st.sidebar.markdown(
|
| | """
|
| | <div style="padding:8px 10px;border-radius:8px;background:#FFF3CD;color:#664D03;
|
| | border:1px solid #FFECB5;margin-bottom:6px;">
|
| | <b>🔔 {pendentes} sugestão(ões) pendente(s)</b><br>
|
| | <span style="font-size:12px;">Acesse a caixa de entrada para responder.</span>
|
| | </div>
|
| | """.format(pendentes=pendentes),
|
| | unsafe_allow_html=True
|
| | )
|
| |
|
| |
|
| | if st.sidebar.button("📬 Abrir Caixa de Entrada (Admin)"):
|
| | st.session_state.nav_target = "resposta"
|
| | st.rerun()
|
| |
|
| |
|
| | if perfil != "admin":
|
| |
|
| | last_seen_dt = st.session_state.get("__user_last_answer_seen__")
|
| |
|
| | try:
|
| | db = _get_db_session()
|
| |
|
| |
|
| | last_answer_dt_row = (
|
| | db.query(IOIRunSugestao.data_resposta)
|
| | .filter(
|
| | IOIRunSugestao.usuario == usuario,
|
| | func.lower(IOIRunSugestao.status) == "respondida",
|
| | IOIRunSugestao.data_resposta != None
|
| | )
|
| | .order_by(IOIRunSugestao.data_resposta.desc())
|
| | .first()
|
| | )
|
| | last_answer_dt = last_answer_dt_row[0] if last_answer_dt_row else None
|
| |
|
| |
|
| | if last_answer_dt and (not last_seen_dt or last_answer_dt > last_seen_dt):
|
| | st.session_state.user_responses_viewed = False
|
| |
|
| |
|
| | novas_respostas = (
|
| | db.query(IOIRunSugestao)
|
| | .filter(
|
| | IOIRunSugestao.usuario == usuario,
|
| | func.lower(IOIRunSugestao.status) == "respondida",
|
| | (IOIRunSugestao.data_resposta > last_seen_dt) if last_seen_dt else (IOIRunSugestao.data_resposta != None)
|
| | )
|
| | .count()
|
| | )
|
| | except Exception:
|
| | novas_respostas = 0
|
| | finally:
|
| | try: db.close()
|
| | except Exception: pass
|
| |
|
| |
|
| | if novas_respostas > 0 and not st.session_state.get("user_responses_viewed", False):
|
| | st.sidebar.markdown(
|
| | """
|
| | <div style="padding:8px 10px;border-radius:8px;background:#D1E7DD;color:#0F5132;
|
| | border:1px solid #BADBCC;margin-bottom:6px;">
|
| | <b>🔔 {resps} resposta(s) nova(s) para suas sugestões</b><br>
|
| | <span style="font-size:12px;">Clique para ver suas respostas.</span>
|
| | </div>
|
| | """.format(resps=novas_respostas),
|
| | unsafe_allow_html=True
|
| | )
|
| |
|
| |
|
| | if not st.session_state.get("__user_toast_shown__"):
|
| | try:
|
| | st.toast("Você tem novas respostas do IOI‑RUN. Clique em '📥 Ver respostas'.", icon="💬")
|
| | except Exception:
|
| | pass
|
| | st.session_state["__user_toast_shown__"] = True
|
| |
|
| | if st.sidebar.button("📥 Ver respostas"):
|
| |
|
| | st.session_state.nav_target = "sugestoes_ioirun"
|
| | st.session_state.user_responses_viewed = True
|
| | st.rerun()
|
| | else:
|
| |
|
| | st.session_state["__user_toast_shown__"] = False
|
| |
|
| |
|
| | termo_busca = st.sidebar.text_input("Pesquisar módulo:").strip().lower()
|
| |
|
| | try:
|
| | ambiente_atual = current_db_choice() if _HAS_ROUTER else "prod"
|
| | except Exception:
|
| | ambiente_atual = "prod"
|
| |
|
| | grupos_disponiveis = obter_grupos_disponiveis(
|
| | MODULES,
|
| | perfil=st.session_state.get("perfil", "usuario"),
|
| | usuario=st.session_state.get("usuario"),
|
| | ambiente=ambiente_atual,
|
| | verificar_permissao=verificar_permissao
|
| | )
|
| |
|
| | if not grupos_disponiveis:
|
| | st.sidebar.selectbox("Selecione a operação:", ["Em desenvolvimento"], index=0)
|
| | st.warning("Nenhuma operação disponível para seu perfil/ambiente neste momento.")
|
| | return
|
| |
|
| | grupo_escolhido = st.sidebar.selectbox("Selecione a operação:", grupos_disponiveis)
|
| |
|
| | opcoes = obter_modulos_para_grupo(
|
| | MODULES, grupo_escolhido, termo_busca,
|
| | perfil=st.session_state.get("perfil", "usuario"),
|
| | usuario=st.session_state.get("usuario"),
|
| | ambiente=ambiente_atual,
|
| | verificar_permissao=verificar_permissao
|
| | )
|
| |
|
| | with st.sidebar.expander("🔧 Diagnóstico do menu", expanded=False):
|
| | st.caption(f"Perfil: **{st.session_state.get('perfil', '—')}** | Grupo: **{grupo_escolhido}** | Busca: **{termo_busca or '∅'}** | Ambiente: **{ambiente_atual}**")
|
| | try:
|
| | mods_dbg = [{"id": mid, "label": lbl} for mid, lbl in (opcoes or [])]
|
| | except Exception:
|
| | mods_dbg = []
|
| | st.write("Módulos visíveis (após regras):", mods_dbg if mods_dbg else "—")
|
| |
|
| |
|
| | try:
|
| | mod_outlook = MODULES.get("outlook_relatorio")
|
| | if mod_outlook:
|
| | mesmo_grupo = (mod_outlook.get("grupo") == grupo_escolhido)
|
| | perfil_ok = verificar_permissao(
|
| | perfil=st.session_state.get("perfil", "usuario"),
|
| | modulo_key="outlook_relatorio",
|
| | usuario=st.session_state.get("usuario"),
|
| | ambiente=ambiente_atual
|
| | )
|
| | ja_nas_opcoes = any(mid == "outlook_relatorio" for mid, _ in (opcoes or []))
|
| | passa_busca = (not termo_busca) or (termo_busca in mod_outlook.get("label", "").strip().lower())
|
| | if mesmo_grupo and perfil_ok and not ja_nas_opcoes and passa_busca:
|
| | opcoes = (opcoes or []) + [("outlook_relatorio", mod_outlook.get("label", "Relatorio portaria"))]
|
| | except Exception:
|
| | pass
|
| |
|
| |
|
| | try:
|
| | mod_repo = MODULES.get("repositorio_load")
|
| | if mod_repo:
|
| | mesmo_grupo_r = (mod_repo.get("grupo") == grupo_escolhido)
|
| | perfil_ok_r = verificar_permissao(
|
| | perfil=st.session_state.get("perfil", "usuario"),
|
| | modulo_key="repositorio_load",
|
| | usuario=st.session_state.get("usuario"),
|
| | ambiente=ambiente_atual
|
| | )
|
| | ja_nas_opcoes_r = any(mid == "repositorio_load" for mid, _ in (opcoes or []))
|
| | passa_busca_r = (not termo_busca) or (termo_busca in mod_repo.get("label", "").strip().lower())
|
| | if mesmo_grupo_r and perfil_ok_r and not ja_nas_opcoes_r and passa_busca_r:
|
| | opcoes = (opcoes or []) + [("repositorio_load", mod_repo.get("label", "Repositório Load"))]
|
| | except Exception:
|
| | pass
|
| |
|
| | if not opcoes:
|
| | st.sidebar.selectbox("Selecione o módulo:", ["Em desenvolvimento"], index=0)
|
| | st.warning(f"A operação '{grupo_escolhido}' está em desenvolvimento.")
|
| | return
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | labels = [label for _, label in opcoes]
|
| |
|
| |
|
| | if st.session_state.get("nav_target"):
|
| | target = st.session_state["nav_target"]
|
| | try:
|
| | target_label = next(lbl for mid, lbl in opcoes if mid == target)
|
| | st.session_state["mod_select_label"] = target_label
|
| | except StopIteration:
|
| | pass
|
| |
|
| |
|
| | if "mod_select_label" not in st.session_state or st.session_state["mod_select_label"] not in labels:
|
| | st.session_state["mod_select_label"] = labels[0]
|
| |
|
| | escolha_label = st.sidebar.selectbox(
|
| | "Selecione o módulo:",
|
| | labels,
|
| | index=labels.index(st.session_state["mod_select_label"]),
|
| | key="mod_select_label"
|
| | )
|
| |
|
| | pagina_id = next(mod_id for mod_id, label in opcoes if label == escolha_label)
|
| |
|
| |
|
| | if st.session_state.get("nav_target"):
|
| | pagina_id = st.session_state.nav_target
|
| | st.session_state["__nav_lock__"] = True
|
| | else:
|
| | st.session_state["__nav_lock__"] = False
|
| |
|
| |
|
| | _check_rerun_qs(pagina_atual=pagina_id)
|
| |
|
| |
|
| | try:
|
| | from streamlit_autorefresh import st_autorefresh
|
| | is_inbox_admin = (pagina_id == "resposta")
|
| | is_outlook_rel = (pagina_id == "outlook_relatorio")
|
| | is_formulario = (pagina_id == "formulario")
|
| | is_recebimento = (pagina_id == "recebimento")
|
| | interval_sec = int(st.session_state.get("__auto_refresh_interval_sec__", 60))
|
| | if (interval_sec > 0) and not (is_inbox_admin or is_outlook_rel or is_formulario or is_recebimento):
|
| |
|
| | st_autorefresh(interval=interval_sec * 1000, key=f"sidebar_autorefresh_{interval_sec}s")
|
| | except Exception:
|
| | pass
|
| |
|
| |
|
| | st.sidebar.markdown("---")
|
| | if st.session_state.get("logado"):
|
| | if st.sidebar.button("🚪 Sair (Logout)"):
|
| | logout()
|
| |
|
| | st.divider()
|
| |
|
| |
|
| | if pagina_id == "formulario":
|
| | formulario.main()
|
| | elif pagina_id == "consulta":
|
| | consulta.main()
|
| | elif pagina_id == "relatorio":
|
| | relatorio.main()
|
| | elif pagina_id == "ranking":
|
| | ranking.main()
|
| | elif pagina_id == "quiz":
|
| | quiz.main()
|
| | ranking.main()
|
| | elif pagina_id == "quiz_admin":
|
| | quiz_admin.main()
|
| | elif pagina_id == "usuarios":
|
| | usuarios_admin.main()
|
| | elif pagina_id == "administracao":
|
| | administracao.main()
|
| | elif pagina_id == "videos":
|
| | videos.main()
|
| | elif pagina_id == "auditoria":
|
| | auditoria.main()
|
| | elif pagina_id == "auditoria_cleanup":
|
| | auditoria_cleanup.main()
|
| | elif pagina_id == "importacao":
|
| | importar_excel.main()
|
| | elif pagina_id == "calendario":
|
| | calendario.main()
|
| | elif pagina_id == "jogos":
|
| | st.session_state.setdefault("pontuacao", 0)
|
| | st.session_state.setdefault("rodadas", 0)
|
| | st.session_state.setdefault("ultimo_resultado", None)
|
| | jogos.main()
|
| | elif pagina_id == "temporario":
|
| | db_tools.main()
|
| | elif pagina_id == "db_admin":
|
| | db_admin.main()
|
| | elif pagina_id == "db_monitor":
|
| | db_monitor.main()
|
| | elif pagina_id == "operacao":
|
| | operacao.main()
|
| | elif pagina_id == "resposta":
|
| | resposta.main()
|
| | elif pagina_id == "db_export_import":
|
| | db_export_import.main()
|
| | elif pagina_id == "produtividade_especialista":
|
| | produtividade_especialista.main()
|
| | elif pagina_id == "outlook_relatorio":
|
| | outlook_relatorio.main()
|
| | elif pagina_id == "sugestoes_ioirun":
|
| | if st.session_state.get("perfil") == "admin":
|
| | st.info("Use a **📬 Caixa de Entrada (Admin)** para responder sugestões.")
|
| | else:
|
| | sugestoes_usuario.main()
|
| | elif pagina_id == "repositorio_load":
|
| | repositorio_load.main()
|
| | elif pagina_id == "rnc":
|
| | rnc.pagina()
|
| | elif pagina_id == "rnc_listagem":
|
| | rnc_listagem.pagina()
|
| | elif pagina_id == "rnc_relatorio":
|
| | rnc_relatorio.pagina()
|
| | elif pagina_id == "repo_rnc":
|
| | repo_rnc.pagina()
|
| | elif pagina_id == "recebimento":
|
| | recebimento.main()
|
| |
|
| |
|
| |
|
| |
|
| | info_mod_default = INFO_MAP_PAGINA_ID.get(pagina_id, "Geral")
|
| | with st.sidebar.expander("ℹ️ Info • Como usar o sistema", expanded=False):
|
| | st.markdown("""
|
| | **Bem-vindo!**
|
| | Este painel reúne instruções rápidas para utilizar cada módulo e seus campos.
|
| | Selecione o módulo abaixo ou navegue pelo menu — o conteúdo ajusta automaticamente.
|
| | """)
|
| | mod_info_sel = st.selectbox(
|
| | "Escolha o módulo para ver instruções:",
|
| | INFO_MODULOS,
|
| | index=INFO_MODULOS.index(info_mod_default) if info_mod_default in INFO_MODULOS else 0,
|
| | key="info_mod_sel"
|
| | )
|
| | st.markdown(INFO_CONTEUDO.get(mod_info_sel, "_Conteúdo não disponível para este módulo._"))
|
| |
|
| |
|
| | if st.session_state.get("__nav_lock__"):
|
| | st.session_state["nav_target"] = None
|
| | st.session_state["__nav_lock__"] = False
|
| |
|
| | if __name__ == "__main__":
|
| | main()
|
| |
|
| |
|
| |
|
| | if st.session_state.get("logado") and st.session_state.get("email"):
|
| | st.sidebar.markdown(
|
| | f"""
|
| | <div style="display:inline-flex;align-items:center;gap:8px; padding:4px 8px;border-radius:8px;
|
| | background:#e7f1ff;color:#0d6efd;font-size:13px;line-height:1.2;">
|
| | <span style="font-size:16px;">👤</span>
|
| | <span>{st.session_state.email}</span>
|
| | </div>
|
| | """,
|
| | unsafe_allow_html=True
|
| | )
|
| |
|
| | st.sidebar.markdown(
|
| | """
|
| | <hr style="margin-top: 10px; margin-bottom: 6px;">
|
| | <p style="font-size: 12px; color: #6c757d;">
|
| | Versão: <strong>1.0.0</strong> • Desenvolvedor: <strong>Rodrigo Silva - Ideiasystem | 2026</strong>
|
| | </p>
|
| | """,
|
| | unsafe_allow_html=True
|
| | )
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|