File size: 30,559 Bytes
5be94d3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
# -*- coding: utf-8 -*-
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

# Tentativa segura do componente de calendário
try:
    from streamlit_calendar import calendar
    _HAS_CAL = True
    _CAL_ERR = None
except Exception as _e:
    _HAS_CAL = False
    _CAL_ERR = _e

# Ambiente atual (opcional)
try:
    from db_router import current_db_choice
    _HAS_ROUTER = True
except Exception:
    _HAS_ROUTER = False
    def current_db_choice() -> str:
        return "prod"

# =====================================================
# 📅 CALENDÁRIO + CRONOGRAMA ANUAL (D-3 / D-2 / D-1 / D 🚢)
# =====================================================

# ------------------------------
# ⚙️ Regras de embarque (fase/seed e passo)
# ------------------------------
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},  # (ACDI → CDI)
    "CDA":  {"seed_day": 5, "step": 5},
    "SEP":  {"seed_day": 4, "step": 4},  # sem dia vazio
    "ESS":  {"seed_day": 3, "step": 7},  # blocos com pausa maior
}

# 🎨 Paleta
COLOR_MAP = {
    "D-3": "#00B050",  # verde
    "D-2": "#FF0000",  # vermelho
    "D-1": "#C00000",  # vermelho escuro
    "D":   "#7F7F7F",  # cinza
}

EMOJI_NAVIO = " 🚢"  # adicionado aos títulos no dia D


# ------------------------------
# Helpers de ambiente e auditoria
# ------------------------------
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:
        # Mantém a UX mesmo se auditoria falhar
        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:
        # Fallback assinatura simples
        try:
            return bool(verificar_permissao(mod_key))
        except Exception:
            return False
    except Exception:
        return False


# ------------------------------
# Builders de eventos do cronograma
# ------------------------------
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))  # segurança (fev)
        seed = date(ano, 1, seed_day)
        step = int(cfg["step"])

        # Todos os D do ano
        d = seed
        while d <= dt_fim:
            if d >= dt_ini:
                # D (com emoji) + cor
                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:
                    # D-1..D-3
                    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)

        # Cobertura no início do ano (apenas rótulos anteriores ao D-semente)
        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()}"


# ------------------------------
# Persistência no banco
# ------------------------------
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,  # mantém o emoji nos D
            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"]
        # inclui com emoji para D
        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


# ------------------------------
# UI principal
# ------------------------------
def main():
    # =====================================================
    # 🔒 PROTEÇÃO POR PERFIL
    # =====================================================
    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()

    # Helper: cor por status (eventos do banco)
    def _cor_evento_db(e: "EventoCalendario") -> str:
        if not e.ativo:
            return "#95a5a6"  # Cinza
        if e.data_evento < hoje:
            return "#e74c3c"  # Vermelho (passado)
        if e.data_lembrete and e.data_lembrete == hoje:
            return "#f39c12"  # Laranja (lembrete hoje)
        return "#2ecc71"      # Verde (ativo futuro)

    # Converte EventoCalendario do banco → FullCalendar
    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:
        # =====================================================
        # 🔔 LEMBRETES DO DIA
        # =====================================================
        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()

        # =====================================================
        # 🎛️ CONTROLES DO CRONOGRAMA
        # =====================================================
        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)

        # Gera cronograma em memória para o ANO selecionado (visualização)
        eventos_auto = _gerar_cronograma_ano(
            ano_sel, fpsos_sel, incluir_anteriores=True, apenas_D=apenas_D
        )

        # 🔁 Ações de banco: ANO
        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()

        # 🔁 Ações de banco: INTERVALO ATÉ 2030
        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()

        # =====================================================
        # ➕ NOVO EVENTO / LEMBRETE (manual)
        # =====================================================
        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()

        # =====================================================
        # 📆 CALENDÁRIO (eventos do banco + cronograma do ANO selecionado)
        # =====================================================
        st.subheader("📆 Calendário (clique no dia ou no evento para ver a observação)")

        # Banco (apenas ano selecionado na visualizaçã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]

        # Junta cronograma automático (memória) + banco (para a visualização do ano)
        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}"
        )

        # Legenda
        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()

        # =====================================================
        # 🔎 Detalhe por clique (evento ou dia)
        # =====================================================
        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 {}

            # Se for do banco, traz detalhes atualizados
            e = None
            if ev_id and not str(ev_id).startswith("auto::"):
                try:
                    # SQLAlchemy 2.x: use Session.get(Model, pk)
                    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:
                # Evento do cronograma automático (memória)
                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)}")

                # Banco
                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()

                # Cronograma automático (memória) – ano selecionado
                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()

        # =====================================================
        # 📆 Consultar Eventos por Data (modo antigo) — inclui AUTO
        # =====================================================
        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")

            # Banco
            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()

            # AUTO (memória) no ano selecionado
            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