# -*- coding: utf-8 -*- import streamlit as st from dotenv import load_dotenv from datetime import date, datetime, time # ⬇️ Import correto das utils de operação from utils_operacao import obter_grupos_disponiveis, obter_modulos_para_grupo # ✅ Usa toda a largura da página (chamar antes de qualquer outro st.*) st.set_page_config(layout="wide") # Carrega variáveis de ambiente load_dotenv() # =============================== # IMPORTAÇÃO DOS MÓDULOS # =============================== 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 # 📬 Admin: Caixa de Entrada IOI‑RUN (módulo interno) import outlook_relatorio import repositorio_load import Produtividade_Especialista as produtividade_especialista import rnc import rnc_listagem import rnc_relatorio import sugestoes_usuario # 💡 Usuário: Sugestões IOI‑RUN (módulo separado) 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 # Extras p/ sessões ativas from uuid import uuid4 from sqlalchemy import text, func, or_ # 🗄️ Banco ativo (Produção/Teste/Treinamento) 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" # ❌ REMOVIDO: não chamar nenhuma página ao importar/rodar o app principal # if __name__ == "__main__": # rnc.pagina() # =============================== # RERUN por querystring (atalho ?rr=1) # =============================== def _get_query_params(): """Compat: retorna query params como dict (Streamlit novo/antigo).""" try: # Streamlit >= 1.32 return dict(st.query_params) except Exception: # Streamlit antigo (experimental) 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 # Streamlit >= 1.32 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 # 🔒 Evita rr=1 em módulos sensíveis a rerun/refresh # 🟩 AJUSTE: incluir 'formulario' para não aplicar rr=1 quando o formulário estiver ativo if pagina_atual in ("resposta", "outlook_relatorio", "formulario"): return # não aplicar rr=1 dentro destes módulos (evita 'piscar' e cancelamentos) 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 # ========================================= # DB helper — sessão ciente do ambiente # ========================================= 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() # =============================== # CONFIGURAÇÃO INICIAL # =============================== Base.metadata.create_all(bind=engine) def quiz_respondido_hoje(usuario: str) -> bool: # ✅ Usar sessão ciente do ambiente 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ões ativas (usuários logados agora) # =============================== _SESS_TTL_MIN = 5 # janela para considerar "online" 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: # mysql / mariadb 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 # =============================== # Aviso Global — Util (leitura e sanitização) # =============================== 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 # =============================== # Aviso Global — Render do banner superior (robusto) # =============================== 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 # px st.markdown( f"""
{aviso.mensagem}
""", unsafe_allow_html=True ) except Exception as e: st.sidebar.warning(f"Aviso desativado: erro de render ({e})") return # =============================== # Logout (utilitário) # =============================== def logout(): """Finaliza a sessão do usuário, limpa estados e recarrega a aplicação.""" _mark_session_inactive() # marca esta sessão como inativa 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() # =============================== # 🎂 Banner/efeito de aniversário # =============================== def _show_birthday_banner_if_needed(): if st.session_state.get("__show_birthday__"): st.session_state["__show_birthday__"] = False st.markdown( """
🎊
🎉
🎊
🎉
🎊
🎉
🎊
🎉
🎊
🎉
🎊
🎉
""", unsafe_allow_html=True ) st.balloons() nome = st.session_state.get("nome") or st.session_state.get("usuario") or "Usuário" st.markdown( f"""
🎉 Feliz Aniversário, {nome}! 🎉
""", unsafe_allow_html=True ) COR_FRASE = "#0d6efd" st.markdown( f"""
Desejamos a você muitas conquistas e bons embarques ao longo do ano! 💜
""", unsafe_allow_html=True ) # =============================== # MAIN # =============================== def main(): # Estados iniciais 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 # ✅ Estado do intervalo de autoatualização (padrão aumentado p/ 60s; 0 = desligado) st.session_state.setdefault("__auto_refresh_interval_sec__", 60) # LOGIN if not st.session_state.logado: st.session_state.quiz_verificado = False exibir_logo(top=True, sidebar=False) login() return # 👥 Heartbeat + Badge de usuários logados (APENAS ADMIN) _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"""
🟢 Online (últimos {_SESS_TTL_MIN} min)
{online_now}
""", unsafe_allow_html=True ) # 🔄 Botão de Recarregar (mantém a sessão ativa) + ⏱️ Controle do intervalo st.sidebar.markdown("---") # Linha com botão de recarregar e popover para o intervalo col_reload, col_interval = st.sidebar.columns([1, 1]) if col_reload.button("🔄 Recarregar (sem sair)", key="__btn_reload_now__"): st.rerun() # Popover (se disponível) para configurar intervalo; fallback para expander 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() # QUIZ 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() # SISTEMA LIBERADO exibir_logo(top=True, sidebar=True) _render_aviso_global_topbar() _show_birthday_banner_if_needed() st.sidebar.markdown("### Menu | 🎉 Carnaval 🎭 2026!") # Banco ativo na sidebar 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 # ========================= # Notificações no sidebar # ========================= # --- Admin: pendentes --- 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( """
🔔 {pendentes} sugestão(ões) pendente(s)
Acesse a caixa de entrada para responder.
""".format(pendentes=pendentes), unsafe_allow_html=True ) # 👉 Direciona para o MESMO módulo do menu (resposta.main()) if st.sidebar.button("📬 Abrir Caixa de Entrada (Admin)"): st.session_state.nav_target = "resposta" st.rerun() # --- Usuário: respostas novas (após último 'visto') --- if perfil != "admin": # Última vez que o usuário realmente abriu e visualizou as respostas last_seen_dt = st.session_state.get("__user_last_answer_seen__") try: db = _get_db_session() # Qual é a resposta mais recente existente 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 # Se há algo mais novo do que o 'visto', marcamos como não visto if last_answer_dt and (not last_seen_dt or last_answer_dt > last_seen_dt): st.session_state.user_responses_viewed = False # ✅ Conta SOMENTE respostas novas (depois do 'last_seen_dt') 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 # ✅ Exibir card de nova mensagem até o usuário clicar em "Ver respostas" if novas_respostas > 0 and not st.session_state.get("user_responses_viewed", False): st.sidebar.markdown( """
🔔 {resps} resposta(s) nova(s) para suas sugestões
Clique para ver suas respostas.
""".format(resps=novas_respostas), unsafe_allow_html=True ) # (Opcional) Toast discreto — aparece uma única vez por sessão enquanto houver novidade 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"): # Não atualizamos last_seen aqui; isso é feito dentro do módulo do usuário st.session_state.nav_target = "sugestoes_ioirun" st.session_state.user_responses_viewed = True st.rerun() else: # Se não há novidades, libera o toast para a próxima vez que houver st.session_state["__user_toast_shown__"] = False # ------------------------- Menu lateral ------------------------- 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 "—") # Failsafe outlook_relatorio 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 # Failsafe repositorio_load 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 # ============================================================ # 🔒 Fix: selectbox com 'key' + seleção forçada para 'resposta' # quando vier de nav_target (sidebar) ou quando já estivermos na página. # ============================================================ labels = [label for _, label in opcoes] # Se foi solicitado nav_target, injeta a label alvo antes do selectbox 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 # Inicializa/persiste seleção 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) # ✅ Navegação com lock (evita disputa com outros reruns) 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 # 🔎 Agora que sabemos a página atual, tratamos rr=1 com segurança _check_rerun_qs(pagina_atual=pagina_id) # ⏱️ Auto-refresh leve do sidebar — NÃO quando em Inbox/Admin/Outlook/Formulário/Recebimento 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): # key dinâmica por intervalo evita conflitos ao trocar o valor st_autorefresh(interval=interval_sec * 1000, key=f"sidebar_autorefresh_{interval_sec}s") except Exception: pass # Logout st.sidebar.markdown("---") if st.session_state.get("logado"): if st.sidebar.button("🚪 Sair (Logout)"): logout() st.divider() # ------------------------- Roteamento ------------------------- 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": # 📬 Admin 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": # 💡 Usuário 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 — Guia passo a passo de uso (no sidebar) # ------------------------------------------------------ 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._")) # ✅ Libera o nav_target após a 1ª render da página de destino if st.session_state.get("__nav_lock__"): st.session_state["nav_target"] = None st.session_state["__nav_lock__"] = False if __name__ == "__main__": main() # ------------------------- # Desenvolvedor e versão # ------------------------- if st.session_state.get("logado") and st.session_state.get("email"): st.sidebar.markdown( f"""
👤 {st.session_state.email}
""", unsafe_allow_html=True ) st.sidebar.markdown( """

Versão: 1.0.0 • Desenvolvedor: Rodrigo Silva - Ideiasystem | 2026

""", unsafe_allow_html=True )