File size: 16,367 Bytes
e199519
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
# -*- coding: utf-8 -*-
"""
importar_excel.py
Importa planilhas Excel para a tabela Equipamento, com:
- Geração de modelo Excel
- Upload e pré-visualização
- Verificação/edição de duplicidades
- Conversões seguras de tipos (datas/números/strings)
- Gravação transacional + auditoria resiliente
Compatível com Linux (Spaces) e local/Windows.
"""

import streamlit as st
import pandas as pd
from io import BytesIO
from datetime import datetime, date

from banco import SessionLocal
from models import Equipamento
from utils_auditoria import registrar_log

# =========================================
# Configuração – nomes de colunas esperadas
# =========================================
COLUNAS_ESPERADAS = [
    "fpso1", "fpso", "data_coleta", "especialista", "conferente", "osm", "modal",
    "quant_equip", "mrob", "linhas_osm", "linhas_mrob", "linhas_erros",
    "erro_storekeeper", "erro_operacao", "erro_especialista", "erro_outros",
    "inclusao_exclusao", "po", "part_number", "material", "solicitante", "motivo",
    "requisitante", "nota_fiscal", "impacto", "dimensao", "observacoes", "dia_inclusao",
]

# Colunas que tratamos como números inteiros na gravação
COLS_INTEIRAS = {
    "quant_equip", "linhas_osm", "linhas_mrob", "linhas_erros"
}

# =====================================================
# FUNÇÕES AUXILIARES
# =====================================================
def _normalize_cols_case_insensitive(df: pd.DataFrame) -> pd.DataFrame:
    """
    Mapeia colunas do arquivo (case/espaços) para os nomes esperados em COLUNAS_ESPERADAS.
    Ex.: " FPSo " -> "fpso"; "DATA_COLETA" -> "data_coleta".
    Colunas não reconhecidas permanecem como estão.
    """
    if df is None or df.empty:
        return df

    # prioriza mapeamento por lower(strip)
    existing = list(df.columns)
    lower_map = {c.strip().lower(): c for c in existing}
    new_names = {}
    for wanted in COLUNAS_ESPERADAS:
        key = wanted.lower()
        if key in lower_map:
            new_names[lower_map[key]] = wanted  # renomeia p/ o nome "oficial" esperado

    # aplica renomeação
    try:
        df = df.rename(columns=new_names)
    except Exception:
        pass
    return df


def to_date(value):
    """
    Converte para date:
    - pandas.Timestamp/datetime/date -> date
    - string em formatos 'YYYY-MM-DD' ou 'DD/MM/YYYY'
    - retorna None se não puder converter
    Necessário para compatibilidade com SQLite e outros.
    """
    if value is None or (isinstance(value, float) and pd.isna(value)) or pd.isna(value):
        return None

    if isinstance(value, pd.Timestamp):
        return value.date()
    if isinstance(value, datetime):
        return value.date()
    if isinstance(value, date):
        return value

    if isinstance(value, str):
        s = value.strip()
        if not s:
            return None
        # tenta ISO
        try:
            return datetime.strptime(s, "%Y-%m-%d").date()
        except Exception:
            pass
        # tenta BR
        try:
            return datetime.strptime(s, "%d/%m/%Y").date()
        except Exception:
            pass
    return None


def safe_value(value):
    """
    Retorna 0 se o valor for vazio/NaN; senão o próprio valor.
    Usado para campos "livres" que não podem ir nulos.
    """
    if value is None or (isinstance(value, float) and pd.isna(value)) or pd.isna(value):
        return 0
    return value


def safe_int(value) -> int:
    """
    Converte para int com fallback (None/NaN/erro -> 0).
    """
    try:
        if value is None or pd.isna(value):
            return 0
        # strings vazias
        if isinstance(value, str) and value.strip() == "":
            return 0
        return int(float(value))
    except Exception:
        return 0


def safe_str(value) -> str:
    """
    Converte para string segura (None/NaN -> "").
    """
    if value is None or (isinstance(value, float) and pd.isna(value)) or pd.isna(value):
        return ""
    return str(value)


def _df_template() -> pd.DataFrame:
    """
    Retorna um DataFrame vazio com as colunas esperadas (para gerar o modelo).
    """
    return pd.DataFrame(columns=COLUNAS_ESPERADAS)


def _download_modelo_excel() -> BytesIO:
    """
    Gera um arquivo Excel de modelo (aba MODELO) e retorna buffer.
    """
    df = _df_template()
    buf = BytesIO()
    with pd.ExcelWriter(buf, engine="openpyxl") as writer:
        df.to_excel(writer, index=False, sheet_name="MODELO")
    buf.seek(0)
    return buf


def _validate_required_cols(df: pd.DataFrame) -> list:
    """
    Retorna a lista de colunas faltantes em relação a COLUNAS_ESPERADAS.
    Apenas informa; a importação pode prosseguir mesmo faltando algumas
    (desde que sua tabela/ETL aceite nulos).
    """
    missing = [c for c in COLUNAS_ESPERADAS if c not in df.columns]
    return missing


# =====================================================
# MÓDULO PRINCIPAL
# =====================================================
def main():
    st.title("📥 Importação de Dados via Excel")

    st.markdown(
        """
        Este módulo permite:
        - 📄 Baixar um **modelo Excel padrão**
        - ✍️ Preencher os dados offline
        - 🔍 Validar antes da gravação
        - 💾 Importar os registros para o banco
        """
    )

    # =====================================================
    # 1️⃣ GERAR MODELO EXCEL
    # =====================================================
    st.subheader("📄 Baixar modelo Excel")

    buffer = _download_modelo_excel()
    st.download_button(
        label="⬇️ Baixar modelo Excel",
        data=buffer,
        file_name="modelo_importacao_load.xlsx",
        mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
    )

    st.divider()

    # =====================================================
    # 2️⃣ UPLOAD DO ARQUIVO
    # =====================================================
    st.subheader("📤 Importar arquivo preenchido")

    arquivo = st.file_uploader(
        "Selecione o arquivo Excel (.xlsx)",
        type=["xlsx"]
    )

    if not arquivo:
        st.info("📌 Faça o upload de um arquivo para continuar.")
        return

    # Leitura com engine explícito para maior compatibilidade
    try:
        df = pd.read_excel(arquivo, engine="openpyxl")
    except Exception as e:
        st.error(f"❌ Erro ao ler o arquivo: {e}")
        return

    if df is None or df.empty:
        st.error("❌ O arquivo não possui dados (planilha vazia).")
        return

    # Normaliza colunas (case/espaços) para nomes esperados
    df = _normalize_cols_case_insensitive(df)

    # Garante um identificador estável para cada linha durante as edições
    if "_row_id" not in df.columns:
        df["_row_id"] = range(len(df))

    # Salva o DF bruto na sessão para persistência entre reruns
    st.session_state["df_raw"] = df.copy()

    # Diagnóstico de colunas
    faltantes = _validate_required_cols(df)
    if faltantes:
        st.warning(
            "⚠️ Algumas colunas esperadas **não** foram encontradas no arquivo ("
            + ", ".join(faltantes) + "). "
            "Se esses campos forem obrigatórios em seu banco, a importação pode falhar."
        )

    st.success("✅ Arquivo carregado com sucesso!")

    # =====================================================
    # 3️⃣ PRÉVIA DOS DADOS
    # =====================================================
    st.subheader("🔍 Prévia dos dados (arquivo lido)")
    st.dataframe(df, use_container_width=True)

    # =====================================================
    # 4️⃣ VERIFICAÇÃO DE DUPLICIDADE (com seleção de linhas a excluir)
    # =====================================================
    st.subheader("🧪 Verificação de duplicidade")

    st.caption(
        "Escolha as colunas que definem a duplicidade. "
        "Em seguida, marque o checkbox **_excluir** nas linhas que **não** deseja importar."
    )

    # Sugerimos um conjunto padrão de chaves, mas só usamos as que existem no arquivo
    sugestao_chaves = [c for c in ["fpso", "osm", "po", "part_number", "nota_fiscal", "data_coleta"] if c in df.columns]

    chaves = st.multiselect(
        "📌 Colunas para verificação de duplicidade:",
        options=list(df.columns),
        default=sugestao_chaves if len(sugestao_chaves) > 0 else []
    )

    # Prepara um DF de trabalho com colunas auxiliares
    work_df = df.copy()
    work_df["_duplicado"] = False
    work_df["_excluir"] = False  # será editado pelo usuário

    if len(chaves) == 0:
        st.info("Selecione ao menos **uma** coluna para verificar duplicidade.")
        # Mesmo sem duplicidade definida, permitimos marcar exclusões manuais:
        with st.expander("🔧 (Opcional) Excluir linhas manualmente mesmo sem duplicidade"):
            manual_view = work_df.set_index("_row_id")[
                [c for c in work_df.columns if c not in ["_duplicado"]] + ["_excluir"]
            ]
            edited_manual = st.data_editor(
                manual_view,
                use_container_width=True,
                num_rows="fixed",
                column_config={
                    "_excluir": st.column_config.CheckboxColumn("Excluir da importação")
                }
            )
            # Aplica exclusões manuais
            work_df = work_df.set_index("_row_id")
            work_df["_excluir"] = edited_manual["_excluir"].reindex(work_df.index).fillna(False).astype(bool)
            work_df = work_df.reset_index()

    else:
        # Marca duplicadas com base nas chaves selecionadas
        mask_dup_any = work_df.duplicated(subset=chaves, keep=False)
        work_df["_duplicado"] = mask_dup_any

        # Sugerimos exclusão automática das ocorrências não-primárias (o usuário pode alterar)
        mask_dup_not_first = work_df.duplicated(subset=chaves, keep="first")
        work_df.loc[mask_dup_not_first, "_excluir"] = True

        if mask_dup_any.any():
            st.warning("⚠️ Foram encontradas linhas duplicadas com base nas chaves selecionadas:")
            # Mostrar somente as duplicadas para facilitar a decisão
            cols_para_mostrar = chaves + [c for c in ["_duplicado", "_excluir"] if c not in chaves]
            # Evita colunas repetidas mantendo ordem
            seen = set()
            cols_para_mostrar = [c for c in cols_para_mostrar if not (c in seen or seen.add(c))]

            dup_view = work_df.loc[mask_dup_any].set_index("_row_id")[cols_para_mostrar]
            edited_dup = st.data_editor(
                dup_view,
                use_container_width=True,
                num_rows="fixed",
                column_config={
                    "_excluir": st.column_config.CheckboxColumn("Excluir da importação"),
                    "_duplicado": st.column_config.CheckboxColumn("Duplicado", disabled=True)
                }
            )

            # Mescla de volta as escolhas do usuário
            work_df = work_df.set_index("_row_id")
            if "_excluir" in edited_dup.columns:
                work_df.loc[edited_dup.index, "_excluir"] = (
                    edited_dup["_excluir"].reindex(work_df.index).fillna(work_df["_excluir"]).astype(bool)
                )
            work_df = work_df.reset_index()

            st.info(
                f"📊 Totais — Linhas: {len(work_df)} | Duplicadas: {mask_dup_any.sum()} | "
                f"Marcadas para excluir: {int(work_df['_excluir'].sum())}"
            )
        else:
            st.success("✅ Nenhuma duplicidade encontrada com as chaves selecionadas.")

    st.divider()

    # =====================================================
    # 4.1️⃣ Prévia final do que será importado + download
    # =====================================================
    df_para_importar = work_df[~work_df["_excluir"]].drop(columns=["_duplicado", "_excluir"], errors="ignore")
    st.session_state["df_para_importar"] = df_para_importar.copy()

    st.subheader("🧾 Prévia do que será importado")
    st.caption("A prévia abaixo desconsidera as linhas marcadas como **_excluir**.")
    st.dataframe(df_para_importar.drop(columns=["_row_id"], errors="ignore"), use_container_width=True)

    # Download da prévia para conferência
    buf_prev = BytesIO()
    with pd.ExcelWriter(buf_prev, engine="openpyxl") as writer:
        df_para_importar.drop(columns=["_row_id"], errors="ignore").to_excel(writer, index=False, sheet_name="A_IMPORTAR")
    buf_prev.seek(0)
    st.download_button(
        "⬇️ Baixar prévia (Excel)",
        data=buf_prev,
        file_name="previa_a_importar.xlsx",
        mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        help="Baixe a prévia do conjunto que será gravado."
    )

    # =====================================================
    # 5️⃣ GRAVAÇÃO NO BANCO (usa o DataFrame filtrado)
    # =====================================================
    st.subheader("💾 Gravar dados no banco")

    col1, col2 = st.columns(2)

    if col1.button("💾 Salvar registros importados"):
        df_import = st.session_state.get("df_para_importar", df)

        if df_import.empty:
            st.error("Não há registros para importar. Revise as exclusões.")
            return

        with SessionLocal() as db:
            try:
                for _, row in df_import.iterrows():
                    registro = Equipamento(
                        fpso1=safe_str(row.get("fpso1")),
                        fpso=safe_str(row.get("fpso")),
                        data_coleta=to_date(row.get("data_coleta")),
                        especialista=safe_str(row.get("especialista")),
                        conferente=safe_str(row.get("conferente")),
                        osm=safe_str(row.get("osm")),
                        modal=safe_str(row.get("modal")),
                        quant_equip=safe_int(row.get("quant_equip")),
                        mrob=safe_str(row.get("mrob")),
                        linhas_osm=safe_int(row.get("linhas_osm")),
                        linhas_mrob=safe_int(row.get("linhas_mrob")),
                        linhas_erros=safe_int(row.get("linhas_erros")),
                        erro_storekeeper=safe_str(row.get("erro_storekeeper")),
                        erro_operacao=safe_str(row.get("erro_operacao")),
                        erro_especialista=safe_str(row.get("erro_especialista")),
                        erro_outros=safe_str(row.get("erro_outros")),
                        inclusao_exclusao=safe_str(row.get("inclusao_exclusao")),
                        po=safe_str(row.get("po")),
                        part_number=safe_str(row.get("part_number")),
                        material=safe_str(row.get("material")),
                        solicitante=safe_str(row.get("solicitante")),
                        motivo=safe_str(row.get("motivo")),
                        requisitante=safe_str(row.get("requisitante")),
                        nota_fiscal=safe_str(row.get("nota_fiscal")),
                        impacto=safe_str(row.get("impacto")),
                        dimensao=safe_str(row.get("dimensao")),
                        observacoes=safe_str(row.get("observacoes")),
                        dia_inclusao=safe_str(row.get("dia_inclusao")),
                    )
                    db.add(registro)

                db.commit()

                try:
                    registrar_log(
                        usuario=st.session_state.get("usuario"),
                        acao=f"IMPORTAÇÃO EXCEL ({len(df_import)} registros) - com filtro de duplicidade",
                        tabela="equipamentos",
                        registro_id=None
                    )
                except Exception:
                    # Não quebra o fluxo se auditoria falhar
                    pass

                st.success(f"🎉 Importação concluída com sucesso! {len(df_import)} registros gravados.")

            except Exception as e:
                db.rollback()
                st.error(f"❌ Erro ao gravar no banco: {e}")

    if col2.button("❌ Cancelar importação"):
        st.warning("Importação cancelada pelo usuário.")


if __name__ == "__main__":
    main()