| |
| import streamlit as st |
| from datetime import date, datetime, timedelta |
| from typing import Dict, List, Optional |
|
|
| from banco import SessionLocal |
| from models import EventoCalendario |
| from utils_permissoes import verificar_permissao |
| from utils_auditoria import registrar_log |
| from utils_datas import formatar_data_br |
|
|
| |
| try: |
| from streamlit_calendar import calendar |
| _HAS_CAL = True |
| _CAL_ERR = None |
| except Exception as _e: |
| _HAS_CAL = False |
| _CAL_ERR = _e |
|
|
| |
| try: |
| from db_router import current_db_choice |
| _HAS_ROUTER = True |
| except Exception: |
| _HAS_ROUTER = False |
| def current_db_choice() -> str: |
| return "prod" |
|
|
| |
| |
| |
|
|
| |
| |
| |
| REGRAS_FPSO: Dict[str, Dict[str, int]] = { |
| "ATD": {"seed_day": 1, "step": 5}, |
| "ADG": {"seed_day": 1, "step": 5}, |
| "CDM": {"seed_day": 2, "step": 5}, |
| "CDP": {"seed_day": 2, "step": 5}, |
| "CDS": {"seed_day": 2, "step": 5}, |
| "CDI": {"seed_day": 5, "step": 5}, |
| "CDA": {"seed_day": 5, "step": 5}, |
| "SEP": {"seed_day": 4, "step": 4}, |
| "ESS": {"seed_day": 3, "step": 7}, |
| } |
|
|
| |
| COLOR_MAP = { |
| "D-3": "#00B050", |
| "D-2": "#FF0000", |
| "D-1": "#C00000", |
| "D": "#7F7F7F", |
| } |
|
|
| EMOJI_NAVIO = " 🚢" |
|
|
|
|
| |
| |
| |
| def _usuario_atual() -> str: |
| return (st.session_state.get("usuario") or "sistema") |
|
|
| def _audit(acao: str, registro_id: Optional[int] = None): |
| """Chama registrar_log com ambiente quando possível (sem quebrar a UX).""" |
| try: |
| registrar_log( |
| usuario=_usuario_atual(), |
| acao=acao, |
| tabela="eventos_calendario", |
| registro_id=registro_id, |
| ambiente=current_db_choice() if _HAS_ROUTER else "prod", |
| ) |
| except Exception: |
| |
| pass |
|
|
| def _can_access(mod_key: str = "calendario") -> bool: |
| """ |
| Verifica permissão com assinatura ampla (perfil/usuario/ambiente) e, |
| se necessário, faz fallback para assinatura simples verificar_permissao(mod_key). |
| """ |
| try: |
| return bool( |
| verificar_permissao( |
| perfil=st.session_state.get("perfil", "usuario"), |
| modulo_key=mod_key, |
| usuario=st.session_state.get("usuario"), |
| ambiente=current_db_choice() if _HAS_ROUTER else "prod", |
| ) |
| ) |
| except TypeError: |
| |
| try: |
| return bool(verificar_permissao(mod_key)) |
| except Exception: |
| return False |
| except Exception: |
| return False |
|
|
|
|
| |
| |
| |
| def _criar_evento_fc(title: str, dt: date, color: str, extra: Dict = None) -> dict: |
| """Monta um evento no formato FullCalendar/streamlit_calendar.""" |
| ev = { |
| "id": f"auto::{title}::{dt.isoformat()}", |
| "title": title, |
| "start": dt.isoformat(), |
| "allDay": True, |
| "color": color, |
| "extendedProps": {"gerado_auto": True}, |
| } |
| if extra: |
| ev["extendedProps"].update(extra) |
| return ev |
|
|
| def _rotulo_antes_de_d(dias: int) -> str: |
| """Converte o deslocamento até D para rótulo: 0->D, 1->D-1, 2->D-2, 3->D-3, outros->''""" |
| if dias == 0: |
| return "D" |
| if dias in (1, 2, 3): |
| return f"D-{dias}" |
| return "" |
|
|
| def _gerar_cronograma_ano( |
| ano: int, |
| fpsos_sel: List[str], |
| incluir_anteriores: bool = True, |
| apenas_D: bool = False, |
| ) -> List[dict]: |
| """ |
| Gera eventos 'D-3/D-2/D-1/D 🚢' para TODO o ano. |
| - incluir_anteriores: inclui D-1..D-3 que caem no começo do ano (vindo do D-semente). |
| - apenas_D: se True, somente 'D 🚢'. |
| """ |
| events: List[dict] = [] |
| dt_ini = date(ano, 1, 1) |
| dt_fim = date(ano, 12, 31) |
|
|
| for fpso in fpsos_sel: |
| cfg = REGRAS_FPSO.get(fpso) |
| if not cfg: |
| continue |
| seed_day = max(1, min(cfg["seed_day"], 28)) |
| seed = date(ano, 1, seed_day) |
| step = int(cfg["step"]) |
|
|
| |
| d = seed |
| while d <= dt_fim: |
| if d >= dt_ini: |
| |
| titulo_d = f"{fpso} – D{EMOJI_NAVIO}" |
| events.append( |
| _criar_evento_fc( |
| titulo_d, d, COLOR_MAP["D"], |
| {"tipo": "D", "fpso": fpso} |
| ) |
| ) |
| if not apenas_D: |
| |
| for k in (1, 2, 3): |
| dk = d - timedelta(days=k) |
| if dt_ini <= dk <= dt_fim: |
| label = f"D-{k}" |
| events.append( |
| _criar_evento_fc( |
| f"{fpso} – {label}", |
| dk, |
| COLOR_MAP[label], |
| {"tipo": label, "fpso": fpso}, |
| ) |
| ) |
| d += timedelta(days=step) |
|
|
| |
| if incluir_anteriores and not apenas_D: |
| for k in (1, 2, 3): |
| dk = seed - timedelta(days=k) |
| if dt_ini <= dk <= dt_fim: |
| label = f"D-{k}" |
| events.append( |
| _criar_evento_fc( |
| f"{fpso} – {label}", |
| dk, |
| COLOR_MAP[label], |
| {"tipo": label, "fpso": fpso}, |
| ) |
| ) |
| return events |
|
|
| def _gerar_cronograma_intervalo( |
| ano_ini: int, |
| ano_fim: int, |
| fpsos_sel: List[str], |
| apenas_D: bool = False, |
| ) -> List[dict]: |
| """Gera eventos para [ano_ini..ano_fim].""" |
| out: List[dict] = [] |
| for y in range(ano_ini, ano_fim + 1): |
| out.extend(_gerar_cronograma_ano(y, fpsos_sel, incluir_anteriores=True, apenas_D=apenas_D)) |
| return out |
|
|
| def _titulo_normalizado(titulo: str) -> str: |
| """Remove o emoji ' 🚢' apenas para comparação/deduplicação.""" |
| return titulo.replace(EMOJI_NAVIO, "") |
|
|
| def _dedup_chave(titulo: str, data_evt: date) -> str: |
| """Chave de de-duplicação (título normalizado + data).""" |
| return f"{_titulo_normalizado(titulo)}::{data_evt.isoformat()}" |
|
|
|
|
| |
| |
| |
| def _gravar_cronograma_no_banco(db, eventos_fc: List[dict]) -> int: |
| """ |
| Grava no banco eventos 'gerado_auto' evitando duplicados (ignorando emoji). |
| Retorna contagem de inserções. |
| """ |
| if not eventos_fc: |
| return 0 |
|
|
| min_day = min(date.fromisoformat(ev["start"][:10]) for ev in eventos_fc) |
| max_day = max(date.fromisoformat(ev["start"][:10]) for ev in eventos_fc) |
|
|
| existentes = ( |
| db.query(EventoCalendario) |
| .filter(EventoCalendario.data_evento >= min_day) |
| .filter(EventoCalendario.data_evento <= max_day) |
| .filter(EventoCalendario.ativo.is_(True)) |
| .all() |
| ) |
| idx_existentes = { |
| _dedup_chave(e.titulo, e.data_evento): e.id for e in existentes |
| } |
|
|
| ins = 0 |
| for ev in eventos_fc: |
| if not ev.get("extendedProps", {}).get("gerado_auto"): |
| continue |
| titulo = ev["title"] |
| dt = date.fromisoformat(ev["start"][:10]) |
| k = _dedup_chave(titulo, dt) |
| if k in idx_existentes: |
| continue |
| novo = EventoCalendario( |
| titulo=titulo, |
| descricao=f"Cronograma automático ({ev['extendedProps'].get('tipo','')})", |
| data_evento=dt, |
| data_lembrete=None, |
| ativo=True, |
| usuario_criacao=_usuario_atual(), |
| data_criacao=datetime.now(), |
| ) |
| db.add(novo) |
| try: |
| db.commit() |
| ins += 1 |
| except Exception: |
| db.rollback() |
| return ins |
|
|
| def _remover_cronograma_do_banco_intervalo(db, fpsos_sel: List[str], ano_ini: int, ano_fim: int) -> int: |
| """ |
| Remove do banco os eventos gerados por este módulo, para [ano_ini..ano_fim] e FPSOs. |
| Busca por títulos ('<FPSO> – D' / ' – D 🚢' / ' – D-1/2/3') e data no intervalo. |
| """ |
| ini = date(ano_ini, 1, 1) |
| fim = date(ano_fim, 12, 31) |
| total = 0 |
| for fpso in fpsos_sel: |
| base = [f"{fpso} – D", f"{fpso} – D-1", f"{fpso} – D-2", f"{fpso} – D-3"] |
| |
| variantes = base + [f"{fpso} – D{EMOJI_NAVIO}"] |
| to_del = ( |
| db.query(EventoCalendario) |
| .filter(EventoCalendario.data_evento >= ini) |
| .filter(EventoCalendario.data_evento <= fim) |
| .filter(EventoCalendario.titulo.in_(variantes)) |
| .all() |
| ) |
| for e in to_del: |
| db.delete(e) |
| total += 1 |
| try: |
| db.commit() |
| except Exception: |
| db.rollback() |
| return total |
|
|
|
|
| |
| |
| |
| def main(): |
| |
| |
| |
| if not _can_access("calendario"): |
| st.error("⛔ Acesso não autorizado.") |
| return |
|
|
| if not _HAS_CAL: |
| st.warning( |
| f"O componente `streamlit-calendar` não está disponível ({_CAL_ERR}). " |
| f"Adicione `streamlit-calendar==1.0.0` ao requirements.txt." |
| ) |
| return |
|
|
| st.title("📅 Calendário e Lembretes") |
|
|
| hoje = date.today() |
| db = SessionLocal() |
|
|
| |
| def _cor_evento_db(e: "EventoCalendario") -> str: |
| if not e.ativo: |
| return "#95a5a6" |
| if e.data_evento < hoje: |
| return "#e74c3c" |
| if e.data_lembrete and e.data_lembrete == hoje: |
| return "#f39c12" |
| return "#2ecc71" |
|
|
| |
| def _to_fc_event_db(e: "EventoCalendario") -> dict: |
| return { |
| "id": str(e.id), |
| "title": e.titulo, |
| "start": e.data_evento.isoformat(), |
| "allDay": True, |
| "color": _cor_evento_db(e), |
| "extendedProps": { |
| "descricao": (e.descricao or ""), |
| "data_evento": e.data_evento.isoformat(), |
| "data_lembrete": e.data_lembrete.isoformat() if e.data_lembrete else None, |
| "ativo": e.ativo, |
| "gerado_auto": False, |
| }, |
| } |
|
|
| try: |
| |
| |
| |
| st.subheader("⏰ Lembretes de Hoje | Adicione o calendário da sua embarcação") |
|
|
| lembretes = ( |
| db.query(EventoCalendario) |
| .filter(EventoCalendario.data_lembrete == hoje) |
| .filter(EventoCalendario.ativo.is_(True)) |
| .order_by(EventoCalendario.data_evento) |
| .all() |
| ) |
|
|
| if lembretes: |
| for l in lembretes: |
| st.warning(f"🔔 **{l.titulo}** — Evento em {formatar_data_br(l.data_evento)}") |
| else: |
| st.info("Nenhum lembrete para hoje.") |
|
|
| st.divider() |
|
|
| |
| |
| |
| st.subheader("🛠️ Cronograma de Embarques (D-3 / D-2 / D-1 / D 🚢)") |
|
|
| col_a, col_b, col_c = st.columns([1, 2, 2]) |
| with col_a: |
| ano_sel = st.number_input( |
| "Ano", |
| min_value=2000, max_value=2100, |
| value=hoje.year, step=1, key="cal_ano_sel" |
| ) |
|
|
| fpsos_all = list(REGRAS_FPSO.keys()) |
| with col_b: |
| fpsos_sel = st.multiselect( |
| "FPSOs", |
| options=fpsos_all, |
| default=fpsos_all, |
| key="cal_fpsos_sel", |
| ) |
| if not fpsos_sel: |
| fpsos_sel = fpsos_all |
|
|
| with col_c: |
| apenas_D = st.checkbox("Exibir apenas dias de Embarque (D)", value=False) |
|
|
| |
| eventos_auto = _gerar_cronograma_ano( |
| ano_sel, fpsos_sel, incluir_anteriores=True, apenas_D=apenas_D |
| ) |
|
|
| |
| col_b1, col_b2, col_b3, col_b4 = st.columns([1.7, 1.7, 2, 2]) |
| with col_b1: |
| if st.button("💾 Gravar cronograma (ano) no banco"): |
| qtd = _gravar_cronograma_no_banco(db, eventos_auto) |
| if qtd > 0: |
| _audit("CRIAR", None) |
| st.success(f"Cronograma do ano {ano_sel} gravado/atualizado. Inserções: {qtd}.") |
| st.rerun() |
| with col_b2: |
| if st.button("🧹 Remover cronograma (ano) do banco"): |
| qtd = _remover_cronograma_do_banco_intervalo(db, fpsos_sel, ano_sel, ano_sel) |
| if qtd > 0: |
| _audit("EXCLUIR", None) |
| st.warning(f"Eventos removidos do banco (ano {ano_sel}): {qtd}.") |
| st.rerun() |
|
|
| |
| with col_b3: |
| if st.button("💾 Gravar cronograma até 2030 (banco)"): |
| eventos_lote = _gerar_cronograma_intervalo( |
| ano_ini=ano_sel, ano_fim=2030, fpsos_sel=fpsos_sel, apenas_D=apenas_D |
| ) |
| qtd = _gravar_cronograma_no_banco(db, eventos_lote) |
| if qtd > 0: |
| _audit("CRIAR", None) |
| st.success(f"Cronogramas {ano_sel}–2030 gravados/atualizados. Inserções: {qtd}.") |
| st.rerun() |
| with col_b4: |
| if st.button("🧹 Remover cronograma até 2030 (banco)"): |
| qtd = _remover_cronograma_do_banco_intervalo(db, fpsos_sel, ano_sel, 2030) |
| if qtd > 0: |
| _audit("EXCLUIR", None) |
| st.warning(f"Eventos removidos do banco ({ano_sel}–2030): {qtd}.") |
| st.rerun() |
|
|
| st.caption( |
| "• A geração automática **não** altera seus eventos manuais. " |
| "Use os botões para **gravar** ou **remover** do banco apenas os eventos criados por este módulo. " |
| "Nos dias de **D**, o título inclui o ícone de navio (🚢)." |
| ) |
|
|
| st.divider() |
|
|
| |
| |
| |
| with st.expander("➕ Novo Evento / Lembrete"): |
| with st.form("form_evento"): |
| titulo = st.text_input("Título *") |
| descricao = st.text_area("Descrição") |
| data_evento = st.date_input("Data do Evento", value=hoje, format="DD/MM/YYYY") |
|
|
| informar_lembrete = st.checkbox("Definir lembrete?") |
| data_lembrete = None |
| if informar_lembrete: |
| data_lembrete = st.date_input( |
| "Data do Lembrete", |
| value=hoje, |
| format="DD/MM/YYYY", |
| key="dt_lembrete_novo" |
| ) |
|
|
| ativo = st.checkbox("Evento ativo", value=True) |
| salvar = st.form_submit_button("💾 Salvar Evento") |
|
|
| if salvar: |
| if not titulo.strip(): |
| st.error("⚠️ O título é obrigatório.") |
| elif data_lembrete and (data_lembrete > data_evento): |
| st.error("⚠️ O lembrete não pode ser após a data do evento.") |
| else: |
| evento = EventoCalendario( |
| titulo=titulo.strip(), |
| descricao=(descricao or "").strip(), |
| data_evento=data_evento, |
| data_lembrete=data_lembrete, |
| ativo=ativo, |
| usuario_criacao=_usuario_atual(), |
| data_criacao=datetime.now() |
| ) |
| db.add(evento) |
| try: |
| db.commit() |
| except Exception as e: |
| db.rollback() |
| st.error(f"❌ Erro ao salvar evento: {e}") |
| else: |
| _audit("CRIAR", getattr(evento, "id", None)) |
| st.success("✅ Evento criado com sucesso!") |
| st.rerun() |
|
|
| st.divider() |
|
|
| |
| |
| |
| st.subheader("📆 Calendário (clique no dia ou no evento para ver a observação)") |
|
|
| |
| ini_year = date(ano_sel, 1, 1) |
| end_year = date(ano_sel, 12, 31) |
| eventos_db = ( |
| db.query(EventoCalendario) |
| .filter(EventoCalendario.data_evento >= ini_year) |
| .filter(EventoCalendario.data_evento <= end_year) |
| .order_by(EventoCalendario.data_evento.asc()) |
| .all() |
| ) |
| eventos_fc_db = [_to_fc_event_db(e) for e in eventos_db] |
|
|
| |
| eventos_fc = eventos_fc_db + eventos_auto |
|
|
| options = { |
| "initialView": "dayGridMonth", |
| "locale": "pt-br", |
| "height": 700, |
| "firstDay": 1, |
| "weekNumbers": False, |
| "headerToolbar": { |
| "left": "prev,next today", |
| "center": "title", |
| "right": "dayGridMonth,dayGridWeek,listWeek" |
| }, |
| "buttonText": { |
| "today": "Hoje", |
| "month": "Mês", |
| "week": "Semana", |
| "day": "Dia", |
| "list": "Lista" |
| }, |
| "dayMaxEventRows": True, |
| "navLinks": True, |
| } |
|
|
| state = calendar( |
| events=eventos_fc, |
| options=options, |
| custom_css="", |
| key=f"calendario_eventos_{ano_sel}" |
| ) |
|
|
| |
| with st.container(): |
| cols = st.columns([1.2, 1.2, 1.2, 1.2, 2.2, 3]) |
| cols[0].markdown("⬛ **D** (cinza) " + EMOJI_NAVIO) |
| cols[1].markdown("🟥 **D‑1** (vinho)") |
| cols[2].markdown("🟥 **D‑2** (vermelho)") |
| cols[3].markdown("🟩 **D‑3** (verde)") |
| cols[4].markdown("🟧 **Lembrete hoje (eventos do banco)**") |
| cols[5].markdown("🟦 **Outros eventos (banco)**") |
|
|
| st.divider() |
|
|
| |
| |
| |
| clicked_event = None |
| if isinstance(state, dict): |
| clicked_event = (state.get("eventClick") or {}).get("event") |
| clicked_date_str = (state.get("dateClick") or {}).get("dateStr") |
| else: |
| clicked_date_str = None |
|
|
| if clicked_event: |
| ev_id = clicked_event.get("id") |
| ev_title = clicked_event.get("title") |
| ev_start = clicked_event.get("start") |
| ev_ext = clicked_event.get("extendedProps") or {} |
|
|
| |
| e = None |
| if ev_id and not str(ev_id).startswith("auto::"): |
| try: |
| |
| e = db.get(EventoCalendario, int(ev_id)) |
| except Exception: |
| e = None |
|
|
| st.subheader(f"📌 {ev_title or 'Evento'}") |
| if e: |
| st.markdown( |
| f""" |
| **Descrição:** |
| {e.descricao or "_Sem descrição_"} |
| |
| **📅 Data do Evento:** {formatar_data_br(e.data_evento)} |
| **⏰ Data do Lembrete:** {formatar_data_br(e.data_lembrete) if e.data_lembrete else "_Sem lembrete_"} |
| **📌 Status:** {"Ativo ✅" if e.ativo else "Inativo ❌"} |
| """ |
| ) |
| if _can_access("administracao"): |
| col1, col2 = st.columns(2) |
| with col1: |
| if e.ativo and st.button("🚫 Desativar", key=f"desativar_{e.id}"): |
| e.ativo = False |
| try: |
| db.commit() |
| except Exception as ex: |
| db.rollback() |
| st.error(f"Erro ao desativar: {ex}") |
| else: |
| _audit("DESATIVAR", e.id) |
| st.success("Evento desativado.") |
| st.rerun() |
| with col2: |
| if st.button("🗑️ Excluir", key=f"excluir_{e.id}"): |
| db.delete(e) |
| try: |
| db.commit() |
| except Exception as ex: |
| db.rollback() |
| st.error(f"Erro ao excluir: {ex}") |
| else: |
| _audit("EXCLUIR", e.id) |
| st.success("Evento excluído.") |
| st.rerun() |
| else: |
| |
| try: |
| dt_evt = date.fromisoformat(ev_start[:10]) |
| except Exception: |
| dt_evt = None |
| st.markdown( |
| f""" |
| **FPSO:** {ev_title.split(' – ')[0] if ev_title and ' – ' in ev_title else '—'} |
| **Tipo:** {ev_ext.get('tipo', '—')} |
| **📅 Data:** {formatar_data_br(dt_evt) if dt_evt else '—'} |
| **Origem:** _Cronograma automático (não gravado no banco)_ |
| """ |
| ) |
|
|
| elif clicked_date_str: |
| try: |
| data_clicada = date.fromisoformat(clicked_date_str) |
| except Exception: |
| data_clicada = None |
|
|
| if data_clicada: |
| st.subheader(f"🗓️ Eventos em {formatar_data_br(data_clicada)}") |
|
|
| |
| eventos_no_dia_db = ( |
| db.query(EventoCalendario) |
| .filter(EventoCalendario.data_evento == data_clicada) |
| .order_by(EventoCalendario.id.desc()) |
| .all() |
| ) |
| if not eventos_no_dia_db: |
| st.info("Nenhum evento do banco para este dia.") |
| else: |
| st.markdown("**📦 Eventos do banco**") |
| for e in eventos_no_dia_db: |
| with st.expander(f"📌 {e.titulo}"): |
| st.markdown( |
| f""" |
| **Descrição:** |
| {e.descricao or "_Sem descrição_"} |
| |
| **📅 Data do Evento:** {formatar_data_br(e.data_evento)} |
| **⏰ Data do Lembrete:** {formatar_data_br(e.data_lembrete) if e.data_lembrete else "_Sem lembrete_"} |
| **📌 Status:** {"Ativo ✅" if e.ativo else "Inativo ❌"} |
| """ |
| ) |
| if _can_access("administracao"): |
| c1, c2 = st.columns(2) |
| with c1: |
| if e.ativo and st.button("🚫 Desativar", key=f"desativar_list_{e.id}"): |
| e.ativo = False |
| try: |
| db.commit() |
| except Exception as ex: |
| db.rollback() |
| st.error(f"Erro ao desativar: {ex}") |
| else: |
| _audit("DESATIVAR", e.id) |
| st.success("Evento desativado.") |
| st.rerun() |
| with c2: |
| if st.button("🗑️ Excluir", key=f"excluir_list_{e.id}"): |
| db.delete(e) |
| try: |
| db.commit() |
| except Exception as ex: |
| db.rollback() |
| st.error(f"Erro ao excluir: {ex}") |
| else: |
| _audit("EXCLUIR", e.id) |
| st.success("Evento excluído.") |
| st.rerun() |
|
|
| |
| eventos_auto_no_dia = [ |
| ev for ev in eventos_auto |
| if ev.get("start", "")[:10] == data_clicada.isoformat() |
| ] |
| if eventos_auto_no_dia: |
| st.markdown("**🛠️ Eventos gerados automaticamente (cronograma)**") |
| for ev in sorted(eventos_auto_no_dia, key=lambda x: x.get("title","")): |
| fps = ev.get("title","").split(" – ")[0] if " – " in ev.get("title","") else "—" |
| tipo = ev.get("extendedProps", {}).get("tipo", "—") |
| st.write(f"• **{fps}** — **{tipo}** ({formatar_data_br(data_clicada)})") |
|
|
| st.divider() |
|
|
| |
| |
| |
| with st.expander("📆 Consultar Eventos por Data (modo antigo)"): |
| data_consulta = st.date_input("Selecione uma data", |
| value=hoje, format="DD/MM/YYYY", |
| key="consulta_antiga") |
|
|
| |
| eventos = ( |
| db.query(EventoCalendario) |
| .filter(EventoCalendario.data_evento == data_consulta) |
| .order_by(EventoCalendario.id.desc()) |
| .all() |
| ) |
| if not eventos: |
| st.info("Nenhum evento do banco para esta data.") |
| else: |
| st.markdown("**📦 Eventos do banco**") |
| for e in eventos: |
| with st.expander(f"📌 {e.titulo}"): |
| st.markdown( |
| f""" |
| **Descrição:** |
| {e.descricao or "_Sem descrição_"} |
| |
| **📅 Data do Evento:** {formatar_data_br(e.data_evento)} |
| **⏰ Data do Lembrete:** {formatar_data_br(e.data_lembrete) if e.data_lembrete else "_Sem lembrete_"} |
| **📌 Status:** {"Ativo ✅" if e.ativo else "Inativo ❌"} |
| """ |
| ) |
| if _can_access("administracao"): |
| col1, col2 = st.columns(2) |
| with col1: |
| if e.ativo and st.button("🚫 Desativar", key=f"desativar_old_{e.id}"): |
| e.ativo = False |
| try: |
| db.commit() |
| except Exception as ex: |
| db.rollback() |
| st.error(f"Erro ao desativar: {ex}") |
| else: |
| _audit("DESATIVAR", e.id) |
| st.success("Evento desativado.") |
| st.rerun() |
| with col2: |
| if st.button("🗑️ Excluir", key=f"excluir_old_{e.id}"): |
| db.delete(e) |
| try: |
| db.commit() |
| except Exception as ex: |
| db.rollback() |
| st.error(f"Erro ao excluir: {ex}") |
| else: |
| _audit("EXCLUIR", e.id) |
| st.success("Evento excluído.") |
| st.rerun() |
|
|
| |
| eventos_auto_antigo = [ |
| ev for ev in eventos_auto |
| if ev.get("start", "")[:10] == data_consulta.isoformat() |
| ] |
| if eventos_auto_antigo: |
| st.markdown("**🛠️ Eventos gerados automaticamente (cronograma)**") |
| for ev in sorted(eventos_auto_antigo, key=lambda x: x.get("title","")): |
| fps = ev.get("title","").split(" – ")[0] if " – " in ev.get("title","") else "—" |
| tipo = ev.get("extendedProps", {}).get("tipo", "—") |
| st.write(f"• **{fps}** — **{tipo}** ({formatar_data_br(data_consulta)})") |
|
|
| finally: |
| try: |
| db.close() |
| except Exception: |
| pass |