diff --git "a/rnc.py" "b/rnc.py"
--- "a/rnc.py"
+++ "b/rnc.py"
@@ -1,1275 +1,1276 @@
-
-# rnc.py
-# -*- coding: utf-8 -*-
-"""
-Módulo: RNC • Registro de Não Conformidades (FOR-SGQ-08 Rev 01)
-Recursos:
-- Cadastro de RNC com campos do formulário FOR-SGQ-08 (cabeçalho, descrição)
-- KPIs (cards)
-- Edição de RNC existente (carrega por código ou lista recente)
-- Timeline/comentários com atualização de status/prazo/responsável (regras de permissão)
-- Plano de Ação (imediata, corretiva, preventiva) – criar e atualizar status/eficácia
-- Anexos (upload incremental)
-- Auditoria (utils_auditoria.registrar_log) — opcional
-- Notificações por e-mail (opcional) se utils_email ou utils_notificacao existir
-"""
-
-import os
-from datetime import datetime, date
-from typing import Optional, Dict, List
-
-import streamlit as st
-
-from banco import SessionLocal
-from models import RNC, RNCComentario, RNCAcaoCorretiva, RNCAnexo
-
-
-# ===============================
-# Configurações do módulo
-# ===============================
-
-# Diretório para anexos: pode definir via .env (RNC_UPLOAD_DIR)
-UPLOAD_DIR = os.getenv("RNC_UPLOAD_DIR", os.path.join("uploads", "rnc"))
-
-# Controle de permissões
-ALLOW_CREATOR_OR_RESP_TO_UPDATE = True # criador ou responsável podem atualizar campos
-ONLY_ADMIN_CAN_CLOSE_OR_CANCEL = True # apenas admin pode encerrar/cancelar
-
-# Status, severidade etc.
-STATUS_OPCOES = [
- "Aberta", "Em Análise", "Plano de Ação",
- "Implementada", "Verificação", "Encerrada", "Cancelada"
-]
-SEVERIDADES = ["Crítica", "Maior", "Menor"]
-PRIORIDADES = ["Alta", "Média", "Baixa"]
-TIPOS = ["Produto", "Processo", "Sistema", "Documentação", "Outro"]
-
-ORIGENS_FORMS = ["Auditoria Interna", "Auditoria Externa", "Outras"]
-
-
-# ===============================
-# Utilidades auxiliares
-# ===============================
-def _ensure_upload_dir(path: str):
- try:
- os.makedirs(path, exist_ok=True)
- except Exception:
- pass
-
-
-def _registrar_log(usuario: Optional[str], acao: str, tabela: str, registro_id: Optional[int] = None):
- try:
- from utils_auditoria import registrar_log
- registrar_log(usuario=usuario, acao=acao, tabela=tabela, registro_id=registro_id)
- except Exception:
- # fallback silencioso
- pass
-
-
-def _send_email_para_responsavel(assunto: str, corpo: str, destinatarios: List[str]):
- """Envia e-mail (opcional). Tenta utils_email ou utils_notificacao. Ignora se indisponível."""
- if not destinatarios:
- return
- try:
- from utils_email import send_email # assinatura esperada: (to, subject, body)
- for to in destinatarios:
- try:
- send_email(to, assunto, corpo)
- except Exception:
- pass
- return
- except Exception:
- pass
-
- try:
- from utils_notificacao import send_email # alternativa
- for to in destinatarios:
- try:
- send_email(to, assunto, corpo)
- except Exception:
- pass
- return
- except Exception:
- pass
-
-
-def _get_user_email(login: Optional[str]) -> Optional[str]:
- """Tenta obter e-mail do usuário em st.session_state."""
- # Ajuste conforme sua infra. Aqui utiliza e-mail da sessão, se existir.
- return st.session_state.get("email")
-
-
-def gerar_codigo_rnc(db) -> str:
- """Gera código sequencial anual: RNC-YYYY-XXXX (4 dígitos)."""
- ano = datetime.utcnow().year
- prefixo = f"RNC-{ano}-"
- ultimo = (
- db.query(RNC)
- .filter(RNC.codigo.like(f"{prefixo}%"))
- .order_by(RNC.codigo.desc())
- .first()
- )
- if ultimo and ultimo.codigo and ultimo.codigo.startswith(prefixo):
- try:
- seq = int(ultimo.codigo.split("-")[-1]) + 1
- except Exception:
- seq = 1
- else:
- seq = 1
- return f"{prefixo}{seq:04d}"
-
-
-def _is_admin(perfil: str) -> bool:
- return (perfil or "").lower() == "admin"
-
-
-def can_edit(rnc: RNC, usuario: Optional[str], perfil: str) -> bool:
- """Pode editar campos (status/prazo/responsável)?"""
- if _is_admin(perfil):
- return True
- if not ALLOW_CREATOR_OR_RESP_TO_UPDATE:
- return False
- if not usuario:
- return False
- return usuario == (rnc.criado_por or "") or usuario == (rnc.responsavel or "")
-
-
-def can_close_or_cancel(perfil: str) -> bool:
- """Pode ENCERRAR ou CANCELAR?"""
- if ONLY_ADMIN_CAN_CLOSE_OR_CANCEL:
- return _is_admin(perfil)
- return True
-
-
-# ===============================
-# KPIs
-# ===============================
-def _kpis_area(db, usuario: str, perfil: str):
- total = db.query(RNC).count()
- em_andamento = db.query(RNC).filter(~RNC.status.in_(["Encerrada", "Cancelada"])).count()
- atrasadas = db.query(RNC).filter(
- RNC.prazo != None,
- RNC.prazo < datetime.utcnow(),
- ~RNC.status.in_(["Encerrada", "Cancelada"])
- ).count()
- minhas = db.query(RNC).filter(
- (RNC.responsavel == usuario) | (RNC.criado_por == usuario)
- ).count()
-
- col1, col2, col3, col4 = st.columns(4)
- col1.metric("Total RNC", total)
- col2.metric("Em andamento", em_andamento)
- col3.metric("Atrasadas", atrasadas, help="Prazo vencido e não encerradas/canceladas")
- col4.metric("Minhas (resp./criadas)", minhas)
-
-
-# ===============================
-# Formulário: Nova RNC (FOR-SGQ-08)
-# ===============================
-def _form_nova_rnc(db):
- st.subheader("➕ Nova RNC • FOR-SGQ-08")
-
- # >>> Controles fora do form para permitir re-render dinâmico das linhas de Ações
- qtd_ai = st.number_input(
- "Quantidade de ações imediatas",
- min_value=1, max_value=15,
- value=st.session_state.get("__qtd_ai__", 1),
- step=1,
- help="Ajuste aqui o número de linhas de ações imediatas (Descrição/Responsável/Data)."
- )
- st.session_state["__qtd_ai__"] = int(qtd_ai)
-
- qtd_ac = st.number_input(
- "Quantidade de ações CORRETIVAS",
- min_value=0, max_value=25,
- value=st.session_state.get("__qtd_ac__", 0),
- step=1,
- help="Defina o número de linhas para Ações Corretivas (Descrição/Responsável/Data)."
- )
- st.session_state["__qtd_ac__"] = int(qtd_ac)
-
- qtd_ap = st.number_input(
- "Quantidade de ações PREVENTIVAS",
- min_value=0, max_value=25,
- value=st.session_state.get("__qtd_ap__", 0),
- step=1,
- help="Defina o número de linhas para Ações Preventivas (Descrição/Responsável/Data)."
- )
- st.session_state["__qtd_ap__"] = int(qtd_ap)
-
- with st.form("form_nova_rnc"):
- st.markdown("#### Cabeçalho do Formulário")
- col_h1, col_h2, col_h3 = st.columns([1, 1, 1])
- data_form = col_h1.date_input("DATA", value=date.today(), format="DD/MM/YYYY")
- emitente = col_h2.text_input("EMITENTE")
- rnc_cliente_numero = col_h3.text_input("RNC CLIENTE Nº (opcional)", placeholder="N/A")
-
- col_h4, col_h5 = st.columns([1, 1])
- cliente_emitente = col_h4.text_input("CLIENTE EMITENTE (opcional)", placeholder="N/A")
- origem_form = col_h5.selectbox("Origem", ORIGENS_FORMS, index=2) # padrão: Outras
-
- st.markdown("##### Envolvidos")
- col_e1, col_e2, col_e3 = st.columns([2, 1, 1])
- inv1_nome = col_e1.text_input("Envolvido 1 — Nome", placeholder="Ex.: Andreia Araújo")
- inv1_matr = col_e2.text_input("Matrícula 1", placeholder="")
- inv1_func = col_e3.text_input("Função 1", placeholder="Ex.: Operations Leader")
-
- col_e4, col_e5, col_e6 = st.columns([2, 1, 1])
- inv2_nome = col_e4.text_input("Envolvido 2 — Nome", placeholder="")
- inv2_matr = col_e5.text_input("Matrícula 2", placeholder="")
- inv2_func = col_e6.text_input("Função 2", placeholder="")
-
- col_h6, col_h7 = st.columns([1, 1])
- area_solicitante = col_h6.text_input("Área Solicitante", placeholder="Ex.: Operacional SBM")
- area_notificada = col_h7.text_input("Área Notificada", placeholder="Ex.: QSMS")
-
- st.markdown("---")
- st.markdown("#### Classificação e Contexto")
- col_c1, col_c2, col_c3 = st.columns([1, 1, 1])
- tipo = col_c1.selectbox("Tipo", TIPOS, index=0)
- severidade = col_c2.selectbox("Severidade", SEVERIDADES, index=1)
- prioridade = col_c3.selectbox("Prioridade", PRIORIDADES, index=1)
-
- col_c4, col_c5 = st.columns([1, 1])
- cliente = col_c4.text_input("Cliente (opcional)")
- local = col_c5.text_input("Local (opcional)")
-
- st.markdown("---")
- st.markdown("#### Descrição da Não Conformidade")
- descricao_nc = st.text_area("Descrição detalhada", height=160)
-
- st.markdown("---")
- st.markdown("#### Ação Imediata/Contenção (linha avulsa / legado)")
- ac_imediata_desc = st.text_area("Ação Imediata/Contenção (disposição quando aplicável)", height=120)
- col_ai1, col_ai2 = st.columns([1, 1])
- ac_imediata_resp = col_ai1.text_input("Responsável pela Ação Imediata")
- ac_imediata_data = col_ai2.date_input("Data de Conclusão (Ação Imediata)", value=None, format="DD/MM/YYYY")
-
- # >>> Linha avulsa (legado)
- st.markdown("#### Ação Corretiva (linha avulsa / legado)")
- ac_corretiva_desc = st.text_area("Ação Corretiva (quando aplicável)", height=120)
- col_cx1, col_cx2 = st.columns([1, 1])
- ac_corretiva_resp = col_cx1.text_input("Responsável pela Ação Corretiva")
- ac_corretiva_data = col_cx2.date_input("Data de Conclusão (Ação Corretiva)", value=None, format="DD/MM/YYYY")
-
- st.markdown("#### Ação Preventiva (linha avulsa / legado)")
- ac_preventiva_desc = st.text_area("Ação Preventiva (quando aplicável)", height=120)
- col_px1, col_px2 = st.columns([1, 1])
- ac_preventiva_resp = col_px1.text_input("Responsável pela Ação Preventiva")
- ac_preventiva_data = col_px2.date_input("Data de Conclusão (Ação Preventiva)", value=None, format="DD/MM/YYYY")
-
- # >>> Múltiplas linhas
- st.markdown("---")
- st.markdown("#### Ações Imediatas/Contenção (múltiplas linhas)")
- if "__ai_rows__" not in st.session_state:
- st.session_state["__ai_rows__"] = {}
- N = st.session_state.get("__qtd_ai__", 1)
- for idx in range(N):
- st.caption(f"— Ação imediata #{idx+1}")
- col_m1, col_m2, col_m3 = st.columns([2, 1, 1])
- prev = st.session_state["__ai_rows__"].get(idx, {})
- desc_val = prev.get("desc", "")
- resp_val = prev.get("resp", "")
- date_val = prev.get("date", None)
- desc_i = col_m1.text_input(f"Descrição da ação #{idx+1}", value=desc_val, key=f"ai_desc_{idx}")
- resp_i = col_m2.text_input(f"Responsável #{idx+1}", value=resp_val, key=f"ai_resp_{idx}")
- date_i = col_m3.date_input(f"Data conclusão #{idx+1}", value=date_val, format="DD/MM/YYYY", key=f"ai_date_{idx}")
- st.session_state["__ai_rows__"][idx] = {"desc": desc_i, "resp": resp_i, "date": date_i}
-
- st.markdown("---")
- st.markdown("#### Ações Corretivas (múltiplas linhas)")
- if "__ac_rows__" not in st.session_state:
- st.session_state["__ac_rows__"] = {}
- NC = st.session_state.get("__qtd_ac__", 0)
- for idx in range(NC):
- st.caption(f"— Ação corretiva #{idx+1}")
- col_c1, col_c2, col_c3 = st.columns([2, 1, 1])
- prevc = st.session_state["__ac_rows__"].get(idx, {})
- desc_val = prevc.get("desc", "")
- resp_val = prevc.get("resp", "")
- date_val = prevc.get("date", None)
- desc_i = col_c1.text_input(f"Descrição da ação corretiva #{idx+1}", value=desc_val, key=f"ac_desc_{idx}")
- resp_i = col_c2.text_input(f"Responsável (corretiva) #{idx+1}", value=resp_val, key=f"ac_resp_{idx}")
- date_i = col_c3.date_input(f"Data conclusão (corretiva) #{idx+1}", value=date_val, format="DD/MM/YYYY", key=f"ac_date_{idx}")
- st.session_state["__ac_rows__"][idx] = {"desc": desc_i, "resp": resp_i, "date": date_i}
-
- st.markdown("---")
- st.markdown("#### Ações Preventivas (múltiplas linhas)")
- if "__ap_rows__" not in st.session_state:
- st.session_state["__ap_rows__"] = {}
- NP = st.session_state.get("__qtd_ap__", 0)
- for idx in range(NP):
- st.caption(f"— Ação preventiva #{idx+1}")
- col_p1, col_p2, col_p3 = st.columns([2, 1, 1])
- prevp = st.session_state["__ap_rows__"].get(idx, {})
- desc_val = prevp.get("desc", "")
- resp_val = prevp.get("resp", "")
- date_val = prevp.get("date", None)
- desc_i = col_p1.text_input(f"Descrição da ação preventiva #{idx+1}", value=desc_val, key=f"ap_desc_{idx}")
- resp_i = col_p2.text_input(f"Responsável (preventiva) #{idx+1}", value=resp_val, key=f"ap_resp_{idx}")
- date_i = col_p3.date_input(f"Data conclusão (preventiva) #{idx+1}", value=date_val, format="DD/MM/YYYY", key=f"ap_date_{idx}")
- st.session_state["__ap_rows__"][idx] = {"desc": desc_i, "resp": resp_i, "date": date_i}
-
- st.markdown("---")
- st.markdown("#### Análise das Causas")
- metodologia = st.selectbox(
- "Metodologia utilizada",
- ["Ishikawa (Diagrama de Causa e Efeito)", "5 Porquês", "FMEA", "Análise de Processo", "Outra"],
- index=0
- )
- causa_raiz = st.text_area("Causa Raiz Identificada", height=120)
-
- with st.expander("Metodologia Ishikawa (opcional) — preencher 1ª, 2ª, 3ª, 4ª por categoria"):
- st.caption("Preencha quando a metodologia for Ishikawa. Caso contrário, deixe em branco.")
- def ishikawa_inputs(label_prefix):
- col_i1, col_i2 = st.columns([1, 1])
- a1 = col_i1.text_input(f"{label_prefix} — 1ª")
- a2 = col_i2.text_input(f"{label_prefix} — 2ª")
- col_i3, col_i4 = st.columns([1, 1])
- a3 = col_i3.text_input(f"{label_prefix} — 3ª")
- a4 = col_i4.text_input(f"{label_prefix} — 4ª")
- return [a1, a2, a3, a4]
-
- pessoa = ishikawa_inputs("Mão de Obra - Pessoa")
- material = ishikawa_inputs("Material")
- medida = ishikawa_inputs("Medida")
- meio_ambiente = ishikawa_inputs("Meio Ambiente")
- maquina = ishikawa_inputs("Máquina ou Equipamento")
- metodo = ishikawa_inputs("Método")
-
- anexos = st.file_uploader("Anexos (opcional)", type=None, accept_multiple_files=True)
-
- enviar = st.form_submit_button("Salvar RNC")
-
- if enviar:
- if not emitente or not descricao_nc:
- st.warning("Preencha ao menos **Emitente** e **Descrição da Não Conformidade**.")
- return
-
- try:
- codigo = gerar_codigo_rnc(db)
-
- # Cabeçalho estruturado (persistido em 'descricao' antes do texto da NC)
- header_md = f"""**FOR-SGQ-08 • Rev 01**
-**DATA:** {data_form.strftime('%d/%m/%Y')} • **EMITENTE:** {emitente} • **RNC Nº:** {codigo}
-**RNC CLIENTE Nº:** {rnc_cliente_numero or 'N/A'} • **CLIENTE EMITENTE:** {cliente_emitente or 'N/A'}
-**Área Solicitante:** {area_solicitante or '—'} • **Origem:** {origem_form} • **Área Notificada:** {area_notificada or '—'}
-**Envolvidos:**
-- {inv1_nome or '—'} • Matr.: {inv1_matr or '—'} • Função: {inv1_func or '—'}
-- {inv2_nome or '—'} • Matr.: {inv2_matr or '—'} • Função: {inv2_func or '—'}
-
-**Descrição da Não Conformidade:**
-{descricao_nc.strip()}
-"""
-
- # Bloco de causa raiz / metodologia (persistido em 'causa_raiz')
- causa_md = f"""**Metodologia utilizada:** {metodologia}
-**Causa Raiz Identificada:**
-{(causa_raiz or '').strip() or '—'}
-"""
-
- def _format_cat(nome, vals):
- filas = [v for v in (vals or []) if (v or "").strip()]
- if not filas:
- return ""
- lines = "\n".join([f"- {i+1}ª: {filas[i]}" for i in range(len(filas))])
- return f"**{nome}:**\n{lines}\n"
-
- ish_md_parts = []
- ish_md_parts.append(_format_cat("Mão de Obra - Pessoa", pessoa))
- ish_md_parts.append(_format_cat("Material", material))
- ish_md_parts.append(_format_cat("Medida", medida))
- ish_md_parts.append(_format_cat("Meio Ambiente", meio_ambiente))
- ish_md_parts.append(_format_cat("Máquina ou Equipamento", maquina))
- ish_md_parts.append(_format_cat("Método", metodo))
- ish_md = "\n".join([p for p in ish_md_parts if p])
-
- if ish_md.strip():
- causa_md += "\n**Ishikawa (Diagrama de Causa e Efeito):**\n" + ish_md
-
- # Cria RNC
- rnc = RNC(
- codigo=codigo,
- titulo=f"RNC {codigo} • {emitente}",
- descricao=header_md, # cabeçalho + descrição NC
- origem=origem_form,
- tipo=(tipo or "").strip() or None,
- severidade=(severidade or "").strip() or None,
- prioridade=(prioridade or "").strip() or None,
- status="Aberta",
- data_abertura=datetime.utcnow(),
- prazo=None,
- responsavel=None, # pode ser definido depois
- area_solicitante=(area_solicitante or "").strip() or None,
- area_notificada=(area_notificada or "").strip() or None,
- criado_por=st.session_state.get("usuario") or "desconhecido",
- cliente=(cliente or "").strip() or None,
- local=(local or "").strip() or None,
- causa_raiz=causa_md, # bloco estruturado
- emitente=emitente,
- data_form=data_form,
- rnc_cliente_numero=(rnc_cliente_numero or "").strip() or None,
- cliente_emitente=(cliente_emitente or "").strip() or None
- )
- db.add(rnc)
- db.commit()
-
- # Ação Imediata/Contenção (LEGADO — uma linha)
- if (ac_imediata_desc or "").strip():
- try:
- ac = RNCAcaoCorretiva(
- rnc_id=rnc.id,
- descricao=f"[IMEDIATA/CONTENÇÃO] {ac_imediata_desc.strip()}",
- responsavel=(ac_imediata_resp or "").strip() or None,
- prazo=datetime.combine(ac_imediata_data, datetime.min.time()) if isinstance(ac_imediata_data, date) else None,
- status="Concluída" if isinstance(ac_imediata_data, date) else "Em execução",
- conclusao_em=datetime.combine(ac_imediata_data, datetime.min.time()) if isinstance(ac_imediata_data, date) else None,
- eficacia=None
- )
- db.add(ac)
- db.commit()
- except Exception as e:
- db.rollback()
- st.error(f"Erro ao salvar a Ação Imediata (linha única): {e}")
-
- # Ação Corretiva (LEGADO — uma linha)
- if (ac_corretiva_desc or "").strip():
- try:
- acx = RNCAcaoCorretiva(
- rnc_id=rnc.id,
- descricao=f"[Corretiva] {ac_corretiva_desc.strip()}",
- responsavel=(ac_corretiva_resp or "").strip() or None,
- prazo=datetime.combine(ac_corretiva_data, datetime.min.time()) if isinstance(ac_corretiva_data, date) else None,
- status="Concluída" if isinstance(ac_corretiva_data, date) else "Em execução",
- conclusao_em=datetime.combine(ac_corretiva_data, datetime.min.time()) if isinstance(ac_corretiva_data, date) else None,
- eficacia=None
- )
- db.add(acx)
- db.commit()
- except Exception as e:
- db.rollback()
- st.error(f"Erro ao salvar a Ação Corretiva (linha única): {e}")
-
- # Ação Preventiva (LEGADO — uma linha)
- if (ac_preventiva_desc or "").strip():
- try:
- apx = RNCAcaoCorretiva(
- rnc_id=rnc.id,
- descricao=f"[Preventiva] {ac_preventiva_desc.strip()}",
- responsavel=(ac_preventiva_resp or "").strip() or None,
- prazo=datetime.combine(ac_preventiva_data, datetime.min.time()) if isinstance(ac_preventiva_data, date) else None,
- status="Concluída" if isinstance(ac_preventiva_data, date) else "Em execução",
- conclusao_em=datetime.combine(ac_preventiva_data, datetime.min.time()) if isinstance(ac_preventiva_data, date) else None,
- eficacia=None
- )
- db.add(apx)
- db.commit()
- except Exception as e:
- db.rollback()
- st.error(f"Erro ao salvar a Ação Preventiva (linha única): {e}")
-
- # salvar múltiplas Ações Imediatas/Contenção
- try:
- ac_rows = st.session_state.get("__ai_rows__", {})
- N = st.session_state.get("__qtd_ai__", 1)
- if ac_rows:
- for idx in range(N):
- row = ac_rows.get(idx, {})
- desc = (row.get("desc") or "").strip()
- resp = (row.get("resp") or "").strip() or None
- dt = row.get("date", None)
- if desc:
- ac = RNCAcaoCorretiva(
- rnc_id=rnc.id,
- descricao=f"[IMEDIATA/CONTENÇÃO] {desc}",
- responsavel=resp,
- prazo=datetime.combine(dt, datetime.min.time()) if isinstance(dt, date) else None,
- status="Concluída" if isinstance(dt, date) else "Em execução",
- conclusao_em=datetime.combine(dt, datetime.min.time()) if isinstance(dt, date) else None,
- eficacia=None
- )
- db.add(ac)
- db.commit()
- except Exception as e:
- db.rollback()
- st.error(f"Erro ao salvar ações imediatas (múltiplas): {e}")
-
- # salvar múltiplas Ações CORRETIVAS
- try:
- ac_rows2 = st.session_state.get("__ac_rows__", {})
- NC = st.session_state.get("__qtd_ac__", 0)
- if ac_rows2 and NC > 0:
- for idx in range(NC):
- row = ac_rows2.get(idx, {})
- desc = (row.get("desc") or "").strip()
- resp = (row.get("resp") or "").strip() or None
- dt = row.get("date", None)
- if desc:
- acor = RNCAcaoCorretiva(
- rnc_id=rnc.id,
- descricao=f"[Corretiva] {desc}",
- responsavel=resp,
- prazo=datetime.combine(dt, datetime.min.time()) if isinstance(dt, date) else None,
- status="Concluída" if isinstance(dt, date) else "Em execução",
- conclusao_em=datetime.combine(dt, datetime.min.time()) if isinstance(dt, date) else None,
- eficacia=None
- )
- db.add(acor)
- db.commit()
- except Exception as e:
- db.rollback()
- st.error(f"Erro ao salvar ações corretivas (múltiplas): {e}")
-
- # salvar múltiplas Ações PREVENTIVAS
- try:
- ap_rows2 = st.session_state.get("__ap_rows__", {})
- NP = st.session_state.get("__qtd_ap__", 0)
- if ap_rows2 and NP > 0:
- for idx in range(NP):
- row = ap_rows2.get(idx, {})
- desc = (row.get("desc") or "").strip()
- resp = (row.get("resp") or "").strip() or None
- dt = row.get("date", None)
- if desc:
- apre = RNCAcaoCorretiva(
- rnc_id=rnc.id,
- descricao=f"[Preventiva] {desc}",
- responsavel=resp,
- prazo=datetime.combine(dt, datetime.min.time()) if isinstance(dt, date) else None,
- status="Concluída" if isinstance(dt, date) else "Em execução",
- conclusao_em=datetime.combine(dt, datetime.min.time()) if isinstance(dt, date) else None,
- eficacia=None
- )
- db.add(apre)
- db.commit()
- except Exception as e:
- db.rollback()
- st.error(f"Erro ao salvar ações preventivas (múltiplas): {e}")
-
- # Anexos
- if anexos:
- dest_dir = os.path.join(UPLOAD_DIR, rnc.codigo)
- _ensure_upload_dir(dest_dir)
- for up in anexos:
- dest_path = os.path.join(dest_dir, up.name)
- with open(dest_path, "wb") as f:
- f.write(up.getbuffer())
- anexo = RNCAnexo(
- rnc_id=rnc.id,
- nome_arquivo=up.name,
- caminho=dest_path,
- conteudo_tipo=getattr(up, "type", None),
- enviado_por=st.session_state.get("usuario"),
- enviado_em=datetime.utcnow()
- )
- db.add(anexo)
- db.commit()
-
- # Auditoria
- _registrar_log(st.session_state.get("usuario"), f"Criou RNC {rnc.codigo}", "rnc", rnc.id)
-
- # Notificação (opcional)
- emails = []
- criador_email = _get_user_email(rnc.criado_por)
- if criador_email:
- emails.append(criador_email)
- assunto = f"[IOI-RUN] Nova RNC criada: {rnc.codigo}"
- corpo = f"""Uma nova RNC foi criada.
-
-Código: {rnc.codigo}
-Emitente: {emitente}
-Origem: {origem_form}
-Tipo: {rnc.tipo or '—'} | Severidade: {rnc.severidade or '—'} | Prioridade: {rnc.prioridade or '—'}
-Área Solicitante: {area_solicitante or '—'} | Área Notificada: {area_notificada or '—'}
-Cliente: {rnc.cliente or '—'} | Local: {rnc.local or '—'}
-Abertura: {rnc.data_abertura.strftime('%d/%m/%Y %H:%M')}
-
-Descrição da NC:
-{descricao_nc.strip()}
-"""
- _send_email_para_responsavel(assunto, corpo, emails)
-
- st.success(f"RNC **{rnc.codigo}** criada com sucesso!")
- st.rerun()
- except Exception as e:
- db.rollback()
- st.error(f"Erro ao salvar a RNC: {e}")
-
-
-# ===============================
-# EDIÇÃO de RNC existente
-# ===============================
-def _extrair_descricao_nc(descricao_md: str) -> str:
- """Tenta extrair o corpo após o marcador '**Descrição da Não Conformidade:**'."""
- if not descricao_md:
- return ""
- marker = "**Descrição da Não Conformidade:**"
- if marker in descricao_md:
- try:
- return descricao_md.split(marker, 1)[1].strip()
- except Exception:
- return descricao_md
- return descricao_md
-
-
-def _montar_descricao_md(data_form: date, emitente: str, codigo: str,
- rnc_cliente_numero: Optional[str], cliente_emitente: Optional[str],
- area_solicitante: Optional[str], origem_form: str, area_notificada: Optional[str],
- inv1: Dict[str, str], inv2: Dict[str, str],
- descricao_nc: str) -> str:
- header_md = f"""**FOR-SGQ-08 • Rev 01**
-**DATA:** {data_form.strftime('%d/%m/%Y')} • **EMITENTE:** {emitente} • **RNC Nº:** {codigo}
-**RNC CLIENTE Nº:** {rnc_cliente_numero or 'N/A'} • **CLIENTE EMITENTE:** {cliente_emitente or 'N/A'}
-**Área Solicitante:** {area_solicitante or '—'} • **Origem:** {origem_form} • **Área Notificada:** {area_notificada or '—'}
-**Envolvidos:**
-- {inv1.get('nome') or '—'} • Matr.: {inv1.get('matr') or '—'} • Função: {inv1.get('func') or '—'}
-- {inv2.get('nome') or '—'} • Matr.: {inv2.get('matr') or '—'} • Função: {inv2.get('func') or '—'}
-
-**Descrição da Não Conformidade:**
-{(descricao_nc or '').strip()}
-"""
- return header_md
-
-
-def _editar_rnc(db):
- st.subheader("✏️ Editar RNC existente")
-
- # Controles de busca
- col_s1, col_s2, col_s3, col_s4 = st.columns([1.2, 1.6, 0.6, 0.6])
- cod_informado = col_s1.text_input("Informe o código (ex.: RNC-2026-0001)", key="__rnc_edit_code__")
- # últimas 30
- recentes = db.query(RNC).order_by(RNC.data_abertura.desc()).limit(30).all()
- opts = [f"{r.codigo} — {r.titulo or '—'}" for r in recentes]
- sel = col_s2.selectbox("ou selecione uma RNC recente", ["(nenhuma)"] + opts, index=0)
- trigger = col_s3.button("Carregar")
- limpar = col_s4.button("Limpar seleção")
-
- # Botão limpar: NÃO alterar __rnc_edit_code__ para evitar erro do Streamlit
- if limpar:
- st.session_state.pop("__rnc_edit_id__", None)
- st.rerun()
-
- # Botão carregar
- if trigger:
- # Se "(nenhuma)" e sem código — limpar seleção anterior
- if sel == "(nenhuma)" and not (cod_informado or "").strip():
- st.session_state.pop("__rnc_edit_id__", None)
- st.info("Nenhuma RNC selecionada. A seleção foi limpa.")
- st.rerun()
-
- # Resolve código digitado ou selecionado
- codigo = (cod_informado or "").strip() or (sel.split(" — ")[0] if sel != "(nenhuma)" else "")
- alvo = db.query(RNC).filter(RNC.codigo == codigo).first() if codigo else None
-
- if not alvo:
- st.session_state.pop("__rnc_edit_id__", None)
- st.warning("RNC não encontrada ou nenhuma selecionada. A seleção foi limpa.")
- st.rerun()
- else:
- st.session_state["__rnc_edit_id__"] = alvo.id
- st.success(f"RNC {alvo.codigo} carregada para edição.")
- st.rerun()
-
- # RNC carregada
- rnc = db.query(RNC).filter(RNC.id == st.session_state.get("__rnc_edit_id__")).first() if st.session_state.get("__rnc_edit_id__") else None
-
- if not rnc:
- st.info("Carregue uma RNC para editar.")
- return
-
- # Form de edição
- with st.form(f"form_edit_rnc_{rnc.id}"):
- st.markdown(f"**Editando:** {rnc.codigo} • {rnc.titulo or '—'}")
-
- col_eh1, col_eh2, col_eh3 = st.columns([1, 1, 1])
- data_form = col_eh1.date_input("DATA", value=rnc.data_form or date.today(), format="DD/MM/YYYY")
- emitente = col_eh2.text_input("EMITENTE", value=rnc.emitente or "")
- origem_form = col_eh3.selectbox("Origem", ORIGENS_FORMS, index=(ORIGENS_FORMS.index(rnc.origem) if rnc.origem in ORIGENS_FORMS else 2))
-
- col_eh4, col_eh5 = st.columns([1, 1])
- rnc_cliente_numero = col_eh4.text_input("RNC CLIENTE Nº (opcional)", value=rnc.rnc_cliente_numero or "")
- cliente_emitente = col_eh5.text_input("CLIENTE EMITENTE (opcional)", value=rnc.cliente_emitente or "")
-
- col_h6, col_h7 = st.columns([1, 1])
- area_solicitante = col_h6.text_input("Área Solicitante", value=rnc.area_solicitante or "")
- area_notificada = col_h7.text_input("Área Notificada", value=rnc.area_notificada or "")
-
- st.markdown("##### Classificação")
- col_c1, col_c2, col_c3 = st.columns([1, 1, 1])
- tipo = col_c1.selectbox("Tipo", TIPOS, index=(TIPOS.index(rnc.tipo) if rnc.tipo in TIPOS else 0))
- severidade = col_c2.selectbox("Severidade", SEVERIDADES, index=(SEVERIDADES.index(rnc.severidade) if rnc.severidade in SEVERIDADES else 1))
- prioridade = col_c3.selectbox("Prioridade", PRIORIDADES, index=(PRIORIDADES.index(rnc.prioridade) if rnc.prioridade in PRIORIDADES else 1))
-
- col_c4, col_c5 = st.columns([1, 1])
- cliente = col_c4.text_input("Cliente (opcional)", value=rnc.cliente or "")
- local = col_c5.text_input("Local (opcional)", value=rnc.local or "")
-
- st.markdown("##### Atribuição e Prazo")
- col_as1, col_as2, col_as3 = st.columns([1, 1, 1])
- responsavel = col_as1.text_input("Responsável", value=rnc.responsavel or "")
- prazo = col_as2.date_input("Prazo (conclusão)", value=(rnc.prazo.date() if isinstance(rnc.prazo, datetime) else rnc.prazo) if rnc.prazo else None, format="DD/MM/YYYY")
- status = col_as3.selectbox("Status", STATUS_OPCOES, index=(STATUS_OPCOES.index(rnc.status) if rnc.status in STATUS_OPCOES else 0))
-
- st.markdown("##### Descrição e Causa Raiz")
- # tenta extrair apenas a descrição da NC a partir do markdown original
- desc_nc_atual = _extrair_descricao_nc(rnc.descricao or "")
- descricao_nc = st.text_area("Descrição da Não Conformidade (substituir corpo)", value=desc_nc_atual or "", height=140)
- causa_raiz = st.text_area("Causa Raiz Identificada", value=rnc.causa_raiz or "", height=120)
-
- # Envolvidos (mantemos inputs simples; no cadastro ficam no cabeçalho)
- with st.expander("Envolvidos (opcional)"):
- col_en1, col_en2, col_en3 = st.columns([2, 1, 1])
- inv1_nome = col_en1.text_input("Envolvido 1 — Nome", value="")
- inv1_matr = col_en2.text_input("Matrícula 1", value="")
- inv1_func = col_en3.text_input("Função 1", value="")
-
- col_en4, col_en5, col_en6 = st.columns([2, 1, 1])
- inv2_nome = col_en4.text_input("Envolvido 2 — Nome", value="")
- inv2_matr = col_en5.text_input("Matrícula 2", value="")
- inv2_func = col_en6.text_input("Função 2", value="")
-
- salvar = st.form_submit_button("💾 Salvar alterações")
-
- if salvar:
- usuario = st.session_state.get("usuario")
- perfil = (st.session_state.get("perfil") or "user").lower()
-
- # Permissão para Encerrar/Cancelar
- if status in ["Encerrada", "Cancelada"] and not can_close_or_cancel(perfil):
- st.warning("Somente administradores podem **encerrar** ou **cancelar** RNC.")
- return
-
- try:
- # Atualiza campos básicos
- rnc.emitente = (emitente or "").strip() or None
- rnc.origem = origem_form
- rnc.rnc_cliente_numero = (rnc_cliente_numero or "").strip() or None
- rnc.cliente_emitente = (cliente_emitente or "").strip() or None
- rnc.area_solicitante = (area_solicitante or "").strip() or None
- rnc.area_notificada = (area_notificada or "").strip() or None
- rnc.tipo = (tipo or "").strip() or None
- rnc.severidade = (severidade or "").strip() or None
- rnc.prioridade = (prioridade or "").strip() or None
- rnc.cliente = (cliente or "").strip() or None
- rnc.local = (local or "").strip() or None
- rnc.responsavel = (responsavel or "").strip() or None
- rnc.prazo = datetime.combine(prazo, datetime.min.time()) if isinstance(prazo, date) else None
- rnc.status = status
- rnc.data_form = data_form
- rnc.titulo = f"RNC {rnc.codigo} • {emitente or '—'}"
- rnc.causa_raiz = (causa_raiz or "").strip() or None
-
- # Reconstrói o markdown de 'descricao' no mesmo padrão do cadastro
- inv1 = {"nome": inv1_nome, "matr": inv1_matr, "func": inv1_func}
- inv2 = {"nome": inv2_nome, "matr": inv2_matr, "func": inv2_func}
- rnc.descricao = _montar_descricao_md(
- data_form=data_form,
- emitente=emitente,
- codigo=rnc.codigo,
- rnc_cliente_numero=rnc_cliente_numero,
- cliente_emitente=cliente_emitente,
- area_solicitante=area_solicitante,
- origem_form=origem_form,
- area_notificada=area_notificada,
- inv1=inv1,
- inv2=inv2,
- descricao_nc=descricao_nc,
- )
-
- # Encerramento: registra data
- if status == "Encerrada":
- rnc.encerrada_em = datetime.utcnow()
- elif status != "Encerrada":
- rnc.encerrada_em = None
-
- db.add(rnc)
- db.commit()
-
- _registrar_log(usuario, f"Editou RNC {rnc.codigo}", "rnc", rnc.id)
- st.success("RNC atualizada com sucesso!")
- st.rerun()
- except Exception as e:
- db.rollback()
- st.error(f"Erro ao salvar alterações: {e}")
-
-
-# ===============================
-# Anexos / Plano de Ação / Timeline / Aprovação
-# ===============================
-def _mostrar_anexos(rnc: RNC, db):
- st.markdown("**Anexos**")
- if rnc.anexos:
- for an in rnc.anexos:
- st.caption(f"📎 {an.nome_arquivo} • {an.enviado_por or '—'} • {an.enviado_em.strftime('%d/%m/%Y %H:%M')}")
- if os.path.exists(an.caminho):
- try:
- with open(an.caminho, "rb") as f:
- st.download_button(f"Baixar {an.nome_arquivo}", f.read(), file_name=an.nome_arquivo, key=f"dl_{rnc.id}_{an.id}")
- except Exception:
- st.caption("Arquivo indisponível.")
- else:
- st.caption("Sem anexos")
-
- up_more = st.file_uploader(f"Adicionar anexos — {rnc.codigo}", accept_multiple_files=True, key=f"up_{rnc.id}")
- if up_more:
- dest_dir = os.path.join(UPLOAD_DIR, rnc.codigo)
- _ensure_upload_dir(dest_dir)
- try:
- for up in up_more:
- dest_path = os.path.join(dest_dir, up.name)
- with open(dest_path, "wb") as f:
- f.write(up.getbuffer())
- anexo = RNCAnexo(
- rnc_id=rnc.id,
- nome_arquivo=up.name,
- caminho=dest_path,
- conteudo_tipo=getattr(up, "type", None),
- enviado_por=st.session_state.get("usuario"),
- enviado_em=datetime.utcnow()
- )
- db.add(anexo)
- db.commit()
- st.success("Anexos adicionados.")
- st.rerun()
- except Exception as e:
- db.rollback()
- st.error(f"Erro ao salvar anexos: {e}")
-
-
-def _mostrar_plano_acao(rnc: RNC, db):
- st.markdown("### 🛠️ Plano de Ação — Separado por categorias")
-
- def _cat(descricao: str) -> str:
- s = (descricao or "").strip()
- if s.startswith("[IMEDIATA/CONTENÇÃO]"):
- return "imediata"
- elif s.startswith("[Preventiva]"):
- return "preventiva"
- elif s.startswith("[Corretiva]"):
- return "corretiva"
- else:
- return "outras"
-
- acoes = rnc.acoes or []
- acoes_imediatas = [a for a in acoes if _cat(a.descricao) == "imediata"]
- acoes_corretivas = [a for a in acoes if _cat(a.descricao) == "corretiva"]
- acoes_preventivas = [a for a in acoes if _cat(a.descricao) == "preventiva"]
- acoes_outras = [a for a in acoes if _cat(a.descricao) == "outras"]
-
- def _render_bloco(titulo: str, lista: List[RNCAcaoCorretiva]):
- st.markdown(f"#### {titulo}")
- if not lista:
- st.caption("— Nenhuma ação cadastrada nesta categoria.")
- return
- lista = sorted(lista, key=lambda a: (a.status, a.prazo or datetime.max))
- for ac in lista:
- line = f"- {ac.descricao}"
- line += f" • Resp: {ac.responsavel or '—'}"
- line += f" • Prazo: {ac.prazo.strftime('%d/%m/%Y') if ac.prazo else '—'}"
- line += f" • Status: {ac.status}"
- if ac.eficacia:
- line += f" • Eficácia: {ac.eficacia}"
- st.write(line)
-
- with st.expander(f"Atualizar ação • ID {ac.id}", expanded=False):
- col_s1, col_s2, col_s3 = st.columns(3)
- novo_status = col_s1.selectbox(
- "Status",
- ["Planejada", "Em execução", "Concluída", "Ineficaz"],
- index=["Planejada", "Em execução", "Concluída", "Ineficaz"].index(ac.status)
- if ac.status in ["Planejada", "Em execução", "Concluída", "Ineficaz"] else 0,
- key=f"ac_st_{ac.id}"
- )
- nova_eficacia = col_s2.selectbox(
- "Eficácia (resultado da ação)",
- ["(não avaliada)", "Eficaz", "Parcial", "Ineficaz"],
- index=0 if not ac.eficacia else ["(não avaliada)", "Eficaz", "Parcial", "Ineficaz"].index(
- ac.eficacia if ac.eficacia in ["Eficaz", "Parcial", "Ineficaz"] else "(não avaliada)"
- ),
- key=f"ac_eff_{ac.id}"
- )
- concluir = col_s3.button("Salvar atualização", key=f"ac_sv_{ac.id}")
-
- if concluir:
- try:
- ac.status = novo_status
- if novo_status == "Concluída":
- ac.conclusao_em = datetime.utcnow()
- ac.eficacia = None if nova_eficacia == "(não avaliada)" else nova_eficacia
- db.add(ac)
- db.commit()
- _registrar_log(st.session_state.get("usuario"), f"Atualizou ação RNC {rnc.codigo} (ID ação {ac.id})", "rnc_acao", ac.id)
- st.success("Ação atualizada.")
- st.rerun()
- except Exception as e:
- db.rollback()
- st.error(f"Erro ao atualizar ação: {e}")
-
- _render_bloco("🚀 Ações Imediatas/Contenção", acoes_imediatas)
- _render_bloco("🔧 Ações Corretivas", acoes_corretivas)
- _render_bloco("🛡️ Ações Preventivas", acoes_preventivas)
- _render_bloco("📁 Outras (sem prefixo)", acoes_outras)
-
- # Form nova ação
- with st.form(f"form_new_action_{rnc.id}"):
- col_a1, col_a2, col_a3, col_a4 = st.columns([1.6, 1, 1, 1])
- ac_tipo = col_a1.selectbox("Tipo da ação", ["Imediata/Contenção", "Corretiva", "Preventiva"], index=1, key=f"ac_tipo_{rnc.id}")
- ac_desc = col_a1.text_input("Descrição da ação", key=f"ac_desc_{rnc.id}")
- ac_resp = col_a2.text_input("Responsável", key=f"ac_resp_{rnc.id}")
- ac_prazo = col_a3.date_input("Data de Conclusão (prazo)", value=None, format="DD/MM/YYYY", key=f"ac_prazo_{rnc.id}")
- ac_enviar = col_a4.form_submit_button("Adicionar ação")
- if ac_enviar:
- if not ac_desc:
- st.warning("Informe a descrição da ação.")
- else:
- try:
- tipo_prefix = "[IMEDIATA/CONTENÇÃO]" if ac_tipo == "Imediata/Contenção" else ("[Preventiva]" if ac_tipo == "Preventiva" else "[Corretiva]")
- new_ac = RNCAcaoCorretiva(
- rnc_id=rnc.id,
- descricao=f"{tipo_prefix} {ac_desc.strip()}",
- responsavel=(ac_resp or "").strip() or None,
- prazo=datetime.combine(ac_prazo, datetime.min.time()) if isinstance(ac_prazo, date) else None,
- status="Planejada" if not isinstance(ac_prazo, date) else "Concluída",
- conclusao_em=datetime.combine(ac_prazo, datetime.min.time()) if isinstance(ac_prazo, date) else None
- )
- db.add(new_ac)
- db.commit()
- _registrar_log(st.session_state.get("usuario"), f"Adicionou ação em {rnc.codigo}", "rnc_acao", new_ac.id)
- st.success("Ação adicionada.")
- st.rerun()
- except Exception as e:
- db.rollback()
- st.error(f"Erro ao salvar ação: {e}")
-
-
-def _mostrar_verificacao_eficacia(rnc: RNC, db, usuario: str, perfil: str):
- st.markdown("### ✅ Verificação da Eficácia e Aprovação")
- st.caption("Descreva como a eficácia será monitorada e registre o resultado das ações. No caso de **Não Eficaz**, a RNC será reaberta.")
- with st.form(f"form_verif_{rnc.id}"):
- plano = st.text_area("O que será feito para manter e acompanhar a eficácia das ações aplicadas?", height=120, key=f"verif_plano_{rnc.id}")
- col_v1, col_v2, col_v3 = st.columns([1, 1, 1])
- resultado = col_v1.selectbox("Resultado das ações", ["(pendente)", "Eficaz", "Não Eficaz"], index=0, key=f"verif_res_{rnc.id}")
- encerrar_agora = col_v2.checkbox("Encerrar RNC (se Eficaz)", value=False, key=f"verif_close_{rnc.id}")
- resp_enc = col_v3.text_input("Responsável pelo encerramento (opcional)", value=usuario, key=f"verif_enc_{rnc.id}")
- enviar = st.form_submit_button("Registrar verificação")
- if enviar:
- if not plano.strip() and resultado == "(pendente)":
- st.warning("Registre o plano de verificação ou o resultado.")
- return
- try:
- msg = f"[Verificação da Eficácia]\n{plano.strip() or '(sem plano)'}\n\nResultado: {resultado}"
- cm = RNCComentario(
- rnc_id=rnc.id,
- autor=usuario,
- mensagem=msg,
- data=datetime.utcnow()
- )
- db.add(cm)
-
- # Regras: se Não Eficaz → reabrir (Em Análise). Se Eficaz e encerrar → Encerrada.
- if resultado == "Não Eficaz":
- rnc.status = "Em Análise"
- rnc.encerrada_em = None
- db.add(rnc)
- elif resultado == "Eficaz" and encerrar_agora and can_close_or_cancel(perfil):
- rnc.status = "Encerrada"
- rnc.encerrada_em = datetime.utcnow()
- cm2 = RNCComentario(
- rnc_id=rnc.id,
- autor=resp_enc or usuario,
- mensagem=f"[Encerramento] RNC encerrada por {resp_enc or usuario} em {datetime.utcnow().strftime('%d/%m/%Y %H:%M')}",
- status_novo="Encerrada",
- data=datetime.utcnow()
- )
- db.add(cm2)
- db.add(rnc)
- db.commit()
-
- _registrar_log(usuario, f"Verificação de eficácia em {rnc.codigo} (resultado: {resultado})", "rnc", rnc.id)
- st.success("Verificação registrada.")
- st.rerun()
- except Exception as e:
- db.rollback()
- st.error(f"Erro ao registrar verificação: {e}")
-
-
-def _mostrar_timeline(rnc: RNC, db, usuario: str, perfil: str):
- st.markdown("### 🕒 Timeline / Andamento")
- if rnc.comentarios:
- for cm in sorted(rnc.comentarios, key=lambda c: c.data, reverse=True):
- line = f"**{cm.data.strftime('%d/%m/%Y %H:%M')}** • {cm.autor}: {cm.mensagem}"
- changes = []
- if cm.status_novo:
- changes.append(f"Status → {cm.status_novo}")
- if cm.responsavel_novo:
- changes.append(f"Responsável → {cm.responsavel_novo}")
- if cm.prazo_novo:
- changes.append(f"Prazo → {cm.prazo_novo.strftime('%d/%m/%Y')}")
- if changes:
- line += " \n" + " • ".join(changes)
- st.write(line)
- else:
- st.caption("Sem comentários ainda.")
-
- st.markdown("#### Comentário/Atualização")
- with st.form(f"form_comment_{rnc.id}"):
- msg = st.text_area("Comentário/atualização (ex.: progresso, decisões)", height=80, key=f"cm_msg_{rnc.id}")
- col_u1, col_u2, col_u3, col_u4 = st.columns(4)
- novo_status = col_u1.selectbox("Atualizar status", ["(sem mudança)"] + STATUS_OPCOES, index=0, key=f"cm_st_{rnc.id}")
- novo_resp = col_u2.text_input("Atualizar responsável", key=f"cm_resp_{rnc.id}")
- novo_prazo = col_u3.date_input("Atualizar prazo", value=None, format="DD/MM/YYYY", key=f"cm_prazo_{rnc.id}")
- salvar_cm = col_u4.form_submit_button("Registrar")
-
- if salvar_cm:
- if not msg.strip() and novo_status == "(sem mudança)" and not novo_resp and not novo_prazo:
- st.warning("Registre um comentário ou alguma mudança.")
- return
-
- aplicar_mudancas = can_edit(rnc, usuario, perfil)
- status_desejado = None if novo_status == "(sem mudança)" else novo_status
-
- if status_desejado in ["Encerrada", "Cancelada"] and not can_close_or_cancel(perfil):
- st.warning("Somente administradores podem **encerrar** ou **cancelar** RNC.")
- aplicar_mudancas = False # não aplicar status proibido
-
- try:
- cm = RNCComentario(
- rnc_id=rnc.id,
- autor=usuario,
- mensagem=((msg or "").strip() or "(sem mensagem)"),
- status_novo=status_desejado,
- responsavel_novo=(novo_resp or "").strip() or None,
- prazo_novo=datetime.combine(novo_prazo, datetime.min.time()) if isinstance(novo_prazo, date) else None,
- data=datetime.utcnow()
- )
- db.add(cm)
-
- # Aplica mudanças
- if aplicar_mudancas:
- if status_desejado:
- rnc.status = status_desejado
- if status_desejado == "Encerrada":
- rnc.encerrada_em = datetime.utcnow()
- if cm.responsavel_novo:
- rnc.responsavel = cm.responsavel_novo
- if cm.prazo_novo:
- rnc.prazo = cm.prazo_novo
- db.add(rnc)
- db.commit()
-
- _registrar_log(usuario, f"Atualizou RNC {rnc.codigo}", "rnc", rnc.id)
-
- # Notificação (opcional)
- if status_desejado in ["Verificação", "Encerrada"]:
- emails = []
- if rnc.responsavel:
- resp_email = _get_user_email(rnc.responsavel)
- if resp_email:
- emails.append(resp_email)
- criador_email = _get_user_email(rnc.criado_por)
- if criador_email:
- emails.append(criador_email)
- assunto = f"[IOI-RUN] RNC {rnc.codigo} — Status: {status_desejado}"
- corpo = f"""A RNC {rnc.codigo} teve seu status atualizado para: {status_desejado}.
-
-Título: {rnc.titulo}
-Responsável: {rnc.responsavel or '—'}
-Prazo: {rnc.prazo.strftime('%d/%m/%Y') if rnc.prazo else '—'}
-Atualizado por: {usuario}
-Data: {datetime.utcnow().strftime('%d/%m/%Y %H:%M')}
-
-Comentário:
-{(msg or '').strip() or '(sem mensagem)'}"""
- _send_email_para_responsavel(assunto, corpo, emails)
-
- st.success("Atualização registradoa.")
- st.rerun()
- except Exception as e:
- db.rollback()
- st.error(f"Erro ao registrar atualização: {e}")
-
-
-def _mostrar_analise_aprovacao(rnc: RNC, db, usuario: str):
- st.markdown("### 🧾 Análise Crítica e Aprovação")
- st.caption("Registre os responsáveis pela análise crítica (QSMS) e aprovação (Diretoria).")
- with st.form(f"form_aprov_{rnc.id}"):
- st.markdown("**Análise Crítica (QSMS)**")
- col_a1, col_a2 = st.columns([1, 1])
- analise_nome = col_a1.text_input("Nome (QSMS)", key=f"ap_nome_{rnc.id}")
- analise_setor = col_a2.text_input("Setor (QSMS)", value="QSMS", key=f"ap_setor_{rnc.id}")
-
- st.markdown("**Aprovação (Diretoria)**")
- col_b1, col_b2 = st.columns([1, 1])
- aprov_nome = col_b1.text_input("Nome (Diretoria)", key=f"ap_dir_nome_{rnc.id}")
- aprov_setor = col_b2.text_input("Setor (Diretoria)", value="Diretor Executivo e de Operações", key=f"ap_dir_setor_{rnc.id}")
-
- enviar = st.form_submit_button("Registrar análise/aprovação")
- if enviar:
- try:
- msg = f"[Análise Crítica / Aprovação]\nQSMS: {analise_nome or '—'} • Setor: {analise_setor or '—'}\nDiretoria: {aprov_nome or '—'} • Setor: {aprov_setor or '—'}"
- cm = RNCComentario(
- rnc_id=rnc.id,
- autor=usuario,
- mensagem=msg,
- data=datetime.utcnow()
- )
- db.add(cm)
- db.commit()
- except Exception as e:
- db.rollback()
- st.error(f"Erro ao registrar análise/aprovação: {e}")
-
-
-# ===============================
-# Tema/Background específico do módulo RNC
-# ===============================
-def _apply_rnc_theme():
- """
- Aplica um tema visual específico para a página do módulo RNC
- (background diferenciado, header destacado e sutis ajustes visuais).
- """
- st.markdown(
- """
-
- """,
- unsafe_allow_html=True
- )
-
-
-# ===============================
-# Página principal (container)
-# ===============================
-def _card_rnc_header(rnc: RNC):
- col_a, col_b = st.columns([3, 1])
- with col_a:
- st.markdown(f"**{rnc.codigo}** — {rnc.titulo or '—'}")
- st.caption(
- f"Origem: {rnc.origem or '—'} • "
- f"Tipo: {rnc.tipo or '—'} • "
- f"Severidade: {rnc.severidade or '—'} • "
- f"Prioridade: {rnc.prioridade or '—'}"
- )
- st.caption(
- f"Responsável: {rnc.responsavel or '—'} • "
- f"Prazo: {rnc.prazo.strftime('%d/%m/%Y') if rnc.prazo else '—'} • "
- f"Abertura: {rnc.data_abertura.strftime('%d/%m/%Y %H:%M') if rnc.data_abertura else '—'}"
- )
- with col_b:
- col_b.metric("Status", rnc.status or "—")
-
-
-def _blocos_rnc(rnc: RNC, db, usuario: str, perfil: str):
- st.markdown("---")
- # Conteúdo principal do card/expander
- st.markdown("#### 📄 Cabeçalho / Descrição")
- st.markdown(rnc.descricao or "—")
-
- st.markdown("---")
- _mostrar_anexos(rnc, db)
-
- st.markdown("---")
- _mostrar_plano_acao(rnc, db)
-
- st.markdown("---")
- _mostrar_timeline(rnc, db, usuario, perfil)
-
- st.markdown("---")
- _mostrar_verificacao_eficacia(rnc, db, usuario, perfil)
-
- st.markdown("---")
- _mostrar_analise_aprovacao(rnc, db, usuario)
-
-
-def pagina():
- """Tela principal do módulo RNC (cadastro + edição)."""
- _apply_rnc_theme()
-
- st.header("RNC • Registro de Não Conformidades (FOR-SGQ-08 Rev 01)")
-
- usuario = st.session_state.get("usuario") or "desconhecido"
- perfil = (st.session_state.get("perfil") or "user").lower()
-
- db = SessionLocal()
- try:
- # KPIs
- _kpis_area(db, usuario, perfil)
- st.divider()
-
- # Formulário de Nova RNC
- _form_nova_rnc(db)
- st.divider()
-
- # ✏️ Edição de RNC existente (sem listagem/export aqui)
- _editar_rnc(db)
-
- # Se houver RNC carregada para edição, renderiza blocos auxiliares
- if st.session_state.get("__rnc_edit_id__"):
- rnc = db.query(RNC).filter(RNC.id == st.session_state["__rnc_edit_id__"]).first()
- if rnc:
- with st.expander(f"📂 Detalhes • {rnc.codigo} — {rnc.titulo or '—'}", expanded=False):
- _card_rnc_header(rnc)
- _blocos_rnc(rnc, db, usuario, perfil)
-
- except Exception as e:
- st.error(f"Erro ao montar a página de RNC: {e}")
- finally:
- try:
- db.close()
- except Exception:
- pass
-
-
-# Alias opcional para compatibilidade com app.py
-def view():
- """Alias de pagina() para compatibilidade."""
- return pagina()
-
-
-
-
-
-
+
+# rnc.py
+# -*- coding: utf-8 -*-
+"""
+Módulo: RNC • Registro de Não Conformidades (FOR-SGQ-08 Rev 01)
+Recursos:
+- Cadastro de RNC com campos do formulário FOR-SGQ-08 (cabeçalho, descrição)
+- KPIs (cards)
+- Edição de RNC existente (carrega por código ou lista recente)
+- Timeline/comentários com atualização de status/prazo/responsável (regras de permissão)
+- Plano de Ação (imediata, corretiva, preventiva) – criar e atualizar status/eficácia
+- Anexos (upload incremental)
+- Auditoria (utils_auditoria.registrar_log) — opcional
+- Notificações por e-mail (opcional) se utils_email ou utils_notificacao existir
+"""
+
+import os
+from datetime import datetime, date
+from typing import Optional, Dict, List
+
+import streamlit as st
+
+from banco import SessionLocal
+from models import RNC, RNCComentario, RNCAcaoCorretiva, RNCAnexo
+
+
+# ===============================
+# Configurações do módulo
+# ===============================
+
+# Diretório para anexos: pode definir via .env (RNC_UPLOAD_DIR)
+UPLOAD_DIR = os.getenv("RNC_UPLOAD_DIR", os.path.join("uploads", "rnc"))
+
+# Controle de permissões
+ALLOW_CREATOR_OR_RESP_TO_UPDATE = True # criador ou responsável podem atualizar campos
+ONLY_ADMIN_CAN_CLOSE_OR_CANCEL = True # apenas admin pode encerrar/cancelar
+
+# Status, severidade etc.
+STATUS_OPCOES = [
+ "Aberta", "Em Análise", "Plano de Ação",
+ "Implementada", "Verificação", "Encerrada", "Cancelada"
+]
+SEVERIDADES = ["Crítica", "Maior", "Menor"]
+PRIORIDADES = ["Alta", "Média", "Baixa"]
+TIPOS = ["Produto", "Processo", "Sistema", "Documentação", "Outro"]
+
+ORIGENS_FORMS = ["Auditoria Interna", "Auditoria Externa", "Outras"]
+
+
+# ===============================
+# Utilidades auxiliares
+# ===============================
+def _ensure_upload_dir(path: str):
+ try:
+ os.makedirs(path, exist_ok=True)
+ except Exception:
+ pass
+
+
+def _registrar_log(usuario: Optional[str], acao: str, tabela: str, registro_id: Optional[int] = None):
+ try:
+ from utils_auditoria import registrar_log
+ registrar_log(usuario=usuario, acao=acao, tabela=tabela, registro_id=registro_id)
+ except Exception:
+ # fallback silencioso
+ pass
+
+
+def _send_email_para_responsavel(assunto: str, corpo: str, destinatarios: List[str]):
+ """Envia e-mail (opcional). Tenta utils_email ou utils_notificacao. Ignora se indisponível."""
+ if not destinatarios:
+ return
+ try:
+ from utils_email import send_email # assinatura esperada: (to, subject, body)
+ for to in destinatarios:
+ try:
+ send_email(to, assunto, corpo)
+ except Exception:
+ pass
+ return
+ except Exception:
+ pass
+
+ try:
+ from utils_notificacao import send_email # alternativa
+ for to in destinatarios:
+ try:
+ send_email(to, assunto, corpo)
+ except Exception:
+ pass
+ return
+ except Exception:
+ pass
+
+
+def _get_user_email(login: Optional[str]) -> Optional[str]:
+ """Tenta obter e-mail do usuário em st.session_state."""
+ # Ajuste conforme sua infra. Aqui utiliza e-mail da sessão, se existir.
+ return st.session_state.get("email")
+
+
+def gerar_codigo_rnc(db) -> str:
+ """Gera código sequencial anual: RNC-YYYY-XXXX (4 dígitos)."""
+ ano = datetime.utcnow().year
+ prefixo = f"RNC-{ano}-"
+ ultimo = (
+ db.query(RNC)
+ .filter(RNC.codigo.like(f"{prefixo}%"))
+ .order_by(RNC.codigo.desc())
+ .first()
+ )
+ if ultimo and ultimo.codigo and ultimo.codigo.startswith(prefixo):
+ try:
+ seq = int(ultimo.codigo.split("-")[-1]) + 1
+ except Exception:
+ seq = 1
+ else:
+ seq = 1
+ return f"{prefixo}{seq:04d}"
+
+
+def _is_admin(perfil: str) -> bool:
+ return (perfil or "").lower() == "admin"
+
+
+def can_edit(rnc: RNC, usuario: Optional[str], perfil: str) -> bool:
+ """Pode editar campos (status/prazo/responsável)?"""
+ if _is_admin(perfil):
+ return True
+ if not ALLOW_CREATOR_OR_RESP_TO_UPDATE:
+ return False
+ if not usuario:
+ return False
+ return usuario == (rnc.criado_por or "") or usuario == (rnc.responsavel or "")
+
+
+def can_close_or_cancel(perfil: str) -> bool:
+ """Pode ENCERRAR ou CANCELAR?"""
+ if ONLY_ADMIN_CAN_CLOSE_OR_CANCEL:
+ return _is_admin(perfil)
+ return True
+
+
+# ===============================
+# KPIs
+# ===============================
+def _kpis_area(db, usuario: str, perfil: str):
+ total = db.query(RNC).count()
+ em_andamento = db.query(RNC).filter(~RNC.status.in_(["Encerrada", "Cancelada"])).count()
+ atrasadas = db.query(RNC).filter(
+ RNC.prazo != None,
+ RNC.prazo < datetime.utcnow(),
+ ~RNC.status.in_(["Encerrada", "Cancelada"])
+ ).count()
+ minhas = db.query(RNC).filter(
+ (RNC.responsavel == usuario) | (RNC.criado_por == usuario)
+ ).count()
+
+ col1, col2, col3, col4 = st.columns(4)
+ col1.metric("Total RNC", total)
+ col2.metric("Em andamento", em_andamento)
+ col3.metric("Atrasadas", atrasadas, help="Prazo vencido e não encerradas/canceladas")
+ col4.metric("Minhas (resp./criadas)", minhas)
+
+
+# ===============================
+# Formulário: Nova RNC (FOR-SGQ-08)
+# ===============================
+def _form_nova_rnc(db):
+ st.subheader("➕ Nova RNC • FOR-SGQ-08")
+
+ # >>> Controles fora do form para permitir re-render dinâmico das linhas de Ações
+ qtd_ai = st.number_input(
+ "Quantidade de ações imediatas",
+ min_value=1, max_value=15,
+ value=st.session_state.get("__qtd_ai__", 1),
+ step=1,
+ help="Ajuste aqui o número de linhas de ações imediatas (Descrição/Responsável/Data)."
+ )
+ st.session_state["__qtd_ai__"] = int(qtd_ai)
+
+ qtd_ac = st.number_input(
+ "Quantidade de ações CORRETIVAS",
+ min_value=0, max_value=25,
+ value=st.session_state.get("__qtd_ac__", 0),
+ step=1,
+ help="Defina o número de linhas para Ações Corretivas (Descrição/Responsável/Data)."
+ )
+ st.session_state["__qtd_ac__"] = int(qtd_ac)
+
+ qtd_ap = st.number_input(
+ "Quantidade de ações PREVENTIVAS",
+ min_value=0, max_value=25,
+ value=st.session_state.get("__qtd_ap__", 0),
+ step=1,
+ help="Defina o número de linhas para Ações Preventivas (Descrição/Responsável/Data)."
+ )
+ st.session_state["__qtd_ap__"] = int(qtd_ap)
+
+ with st.form("form_nova_rnc"):
+ st.markdown("#### Cabeçalho do Formulário")
+ col_h1, col_h2, col_h3 = st.columns([1, 1, 1])
+ data_form = col_h1.date_input("DATA", value=date.today(), format="DD/MM/YYYY")
+ emitente = col_h2.text_input("EMITENTE")
+ rnc_cliente_numero = col_h3.text_input("RNC CLIENTE Nº (opcional)", placeholder="N/A")
+
+ col_h4, col_h5 = st.columns([1, 1])
+ cliente_emitente = col_h4.text_input("CLIENTE EMITENTE (opcional)", placeholder="N/A")
+ origem_form = col_h5.selectbox("Origem", ORIGENS_FORMS, index=2) # padrão: Outras
+
+ st.markdown("##### Envolvidos")
+ col_e1, col_e2, col_e3 = st.columns([2, 1, 1])
+ inv1_nome = col_e1.text_input("Envolvido 1 — Nome", placeholder="Ex.: Andreia Araújo")
+ inv1_matr = col_e2.text_input("Matrícula 1", placeholder="")
+ inv1_func = col_e3.text_input("Função 1", placeholder="Ex.: Operations Leader")
+
+ col_e4, col_e5, col_e6 = st.columns([2, 1, 1])
+ inv2_nome = col_e4.text_input("Envolvido 2 — Nome", placeholder="")
+ inv2_matr = col_e5.text_input("Matrícula 2", placeholder="")
+ inv2_func = col_e6.text_input("Função 2", placeholder="")
+
+ col_h6, col_h7 = st.columns([1, 1])
+ area_solicitante = col_h6.text_input("Área Solicitante", placeholder="Ex.: Operacional SBM")
+ area_notificada = col_h7.text_input("Área Notificada", placeholder="Ex.: QSMS")
+
+ st.markdown("---")
+ st.markdown("#### Classificação e Contexto")
+ col_c1, col_c2, col_c3 = st.columns([1, 1, 1])
+ tipo = col_c1.selectbox("Tipo", TIPOS, index=0)
+ severidade = col_c2.selectbox("Severidade", SEVERIDADES, index=1)
+ prioridade = col_c3.selectbox("Prioridade", PRIORIDADES, index=1)
+
+ col_c4, col_c5 = st.columns([1, 1])
+ cliente = col_c4.text_input("Cliente (opcional)")
+ local = col_c5.text_input("Local (opcional)")
+
+ st.markdown("---")
+ st.markdown("#### Descrição da Não Conformidade")
+ descricao_nc = st.text_area("Descrição detalhada", height=160)
+
+ st.markdown("---")
+ st.markdown("#### Ação Imediata/Contenção (linha avulsa / legado)")
+ ac_imediata_desc = st.text_area("Ação Imediata/Contenção (disposição quando aplicável)", height=120)
+ col_ai1, col_ai2 = st.columns([1, 1])
+ ac_imediata_resp = col_ai1.text_input("Responsável pela Ação Imediata")
+ ac_imediata_data = col_ai2.date_input("Data de Conclusão (Ação Imediata)", value=None, format="DD/MM/YYYY")
+
+ # >>> Linha avulsa (legado)
+ st.markdown("#### Ação Corretiva (linha avulsa / legado)")
+ ac_corretiva_desc = st.text_area("Ação Corretiva (quando aplicável)", height=120)
+ col_cx1, col_cx2 = st.columns([1, 1])
+ ac_corretiva_resp = col_cx1.text_input("Responsável pela Ação Corretiva")
+ ac_corretiva_data = col_cx2.date_input("Data de Conclusão (Ação Corretiva)", value=None, format="DD/MM/YYYY")
+
+ st.markdown("#### Ação Preventiva (linha avulsa / legado)")
+ ac_preventiva_desc = st.text_area("Ação Preventiva (quando aplicável)", height=120)
+ col_px1, col_px2 = st.columns([1, 1])
+ ac_preventiva_resp = col_px1.text_input("Responsável pela Ação Preventiva")
+ ac_preventiva_data = col_px2.date_input("Data de Conclusão (Ação Preventiva)", value=None, format="DD/MM/YYYY")
+
+ # >>> Múltiplas linhas
+ st.markdown("---")
+ st.markdown("#### Ações Imediatas/Contenção (múltiplas linhas)")
+ if "__ai_rows__" not in st.session_state:
+ st.session_state["__ai_rows__"] = {}
+ N = st.session_state.get("__qtd_ai__", 1)
+ for idx in range(N):
+ st.caption(f"— Ação imediata #{idx+1}")
+ col_m1, col_m2, col_m3 = st.columns([2, 1, 1])
+ prev = st.session_state["__ai_rows__"].get(idx, {})
+ desc_val = prev.get("desc", "")
+ resp_val = prev.get("resp", "")
+ date_val = prev.get("date", None)
+ desc_i = col_m1.text_input(f"Descrição da ação #{idx+1}", value=desc_val, key=f"ai_desc_{idx}")
+ resp_i = col_m2.text_input(f"Responsável #{idx+1}", value=resp_val, key=f"ai_resp_{idx}")
+ date_i = col_m3.date_input(f"Data conclusão #{idx+1}", value=date_val, format="DD/MM/YYYY", key=f"ai_date_{idx}")
+ st.session_state["__ai_rows__"][idx] = {"desc": desc_i, "resp": resp_i, "date": date_i}
+
+ st.markdown("---")
+ st.markdown("#### Ações Corretivas (múltiplas linhas)")
+ if "__ac_rows__" not in st.session_state:
+ st.session_state["__ac_rows__"] = {}
+ NC = st.session_state.get("__qtd_ac__", 0)
+ for idx in range(NC):
+ st.caption(f"— Ação corretiva #{idx+1}")
+ col_c1, col_c2, col_c3 = st.columns([2, 1, 1])
+ prevc = st.session_state["__ac_rows__"].get(idx, {})
+ desc_val = prevc.get("desc", "")
+ resp_val = prevc.get("resp", "")
+ date_val = prevc.get("date", None)
+ desc_i = col_c1.text_input(f"Descrição da ação corretiva #{idx+1}", value=desc_val, key=f"ac_desc_{idx}")
+ resp_i = col_c2.text_input(f"Responsável (corretiva) #{idx+1}", value=resp_val, key=f"ac_resp_{idx}")
+ date_i = col_c3.date_input(f"Data conclusão (corretiva) #{idx+1}", value=date_val, format="DD/MM/YYYY", key=f"ac_date_{idx}")
+ st.session_state["__ac_rows__"][idx] = {"desc": desc_i, "resp": resp_i, "date": date_i}
+
+ st.markdown("---")
+ st.markdown("#### Ações Preventivas (múltiplas linhas)")
+ if "__ap_rows__" not in st.session_state:
+ st.session_state["__ap_rows__"] = {}
+ NP = st.session_state.get("__qtd_ap__", 0)
+ for idx in range(NP):
+ st.caption(f"— Ação preventiva #{idx+1}")
+ col_p1, col_p2, col_p3 = st.columns([2, 1, 1])
+ prevp = st.session_state["__ap_rows__"].get(idx, {})
+ desc_val = prevp.get("desc", "")
+ resp_val = prevp.get("resp", "")
+ date_val = prevp.get("date", None)
+ desc_i = col_p1.text_input(f"Descrição da ação preventiva #{idx+1}", value=desc_val, key=f"ap_desc_{idx}")
+ resp_i = col_p2.text_input(f"Responsável (preventiva) #{idx+1}", value=resp_val, key=f"ap_resp_{idx}")
+ date_i = col_p3.date_input(f"Data conclusão (preventiva) #{idx+1}", value=date_val, format="DD/MM/YYYY", key=f"ap_date_{idx}")
+ st.session_state["__ap_rows__"][idx] = {"desc": desc_i, "resp": resp_i, "date": date_i}
+
+ st.markdown("---")
+ st.markdown("#### Análise das Causas")
+ metodologia = st.selectbox(
+ "Metodologia utilizada",
+ ["Ishikawa (Diagrama de Causa e Efeito)", "5 Porquês", "FMEA", "Análise de Processo", "Outra"],
+ index=0
+ )
+ causa_raiz = st.text_area("Causa Raiz Identificada", height=120)
+
+ with st.expander("Metodologia Ishikawa (opcional) — preencher 1ª, 2ª, 3ª, 4ª por categoria"):
+ st.caption("Preencha quando a metodologia for Ishikawa. Caso contrário, deixe em branco.")
+ def ishikawa_inputs(label_prefix):
+ col_i1, col_i2 = st.columns([1, 1])
+ a1 = col_i1.text_input(f"{label_prefix} — 1ª")
+ a2 = col_i2.text_input(f"{label_prefix} — 2ª")
+ col_i3, col_i4 = st.columns([1, 1])
+ a3 = col_i3.text_input(f"{label_prefix} — 3ª")
+ a4 = col_i4.text_input(f"{label_prefix} — 4ª")
+ return [a1, a2, a3, a4]
+
+ pessoa = ishikawa_inputs("Mão de Obra - Pessoa")
+ material = ishikawa_inputs("Material")
+ medida = ishikawa_inputs("Medida")
+ meio_ambiente = ishikawa_inputs("Meio Ambiente")
+ maquina = ishikawa_inputs("Máquina ou Equipamento")
+ metodo = ishikawa_inputs("Método")
+
+ anexos = st.file_uploader("Anexos (opcional)", type=None, accept_multiple_files=True)
+
+ enviar = st.form_submit_button("Salvar RNC")
+
+ if enviar:
+ if not emitente or not descricao_nc:
+ st.warning("Preencha ao menos **Emitente** e **Descrição da Não Conformidade**.")
+ return
+
+ try:
+ codigo = gerar_codigo_rnc(db)
+
+ # Cabeçalho estruturado (persistido em 'descricao' antes do texto da NC)
+ header_md = f"""**FOR-SGQ-08 • Rev 01**
+**DATA:** {data_form.strftime('%d/%m/%Y')} • **EMITENTE:** {emitente} • **RNC Nº:** {codigo}
+**RNC CLIENTE Nº:** {rnc_cliente_numero or 'N/A'} • **CLIENTE EMITENTE:** {cliente_emitente or 'N/A'}
+**Área Solicitante:** {area_solicitante or '—'} • **Origem:** {origem_form} • **Área Notificada:** {area_notificada or '—'}
+**Envolvidos:**
+- {inv1_nome or '—'} • Matr.: {inv1_matr or '—'} • Função: {inv1_func or '—'}
+- {inv2_nome or '—'} • Matr.: {inv2_matr or '—'} • Função: {inv2_func or '—'}
+
+**Descrição da Não Conformidade:**
+{descricao_nc.strip()}
+"""
+
+ # Bloco de causa raiz / metodologia (persistido em 'causa_raiz')
+ causa_md = f"""**Metodologia utilizada:** {metodologia}
+**Causa Raiz Identificada:**
+{(causa_raiz or '').strip() or '—'}
+"""
+
+ def _format_cat(nome, vals):
+ filas = [v for v in (vals or []) if (v or "").strip()]
+ if not filas:
+ return ""
+ lines = "\n".join([f"- {i+1}ª: {filas[i]}" for i in range(len(filas))])
+ return f"**{nome}:**\n{lines}\n"
+
+ ish_md_parts = []
+ ish_md_parts.append(_format_cat("Mão de Obra - Pessoa", pessoa))
+ ish_md_parts.append(_format_cat("Material", material))
+ ish_md_parts.append(_format_cat("Medida", medida))
+ ish_md_parts.append(_format_cat("Meio Ambiente", meio_ambiente))
+ ish_md_parts.append(_format_cat("Máquina ou Equipamento", maquina))
+ ish_md_parts.append(_format_cat("Método", metodo))
+ ish_md = "\n".join([p for p in ish_md_parts if p])
+
+ if ish_md.strip():
+ causa_md += "\n**Ishikawa (Diagrama de Causa e Efeito):**\n" + ish_md
+
+ # Cria RNC
+ rnc = RNC(
+ codigo=codigo,
+ titulo=f"RNC {codigo} • {emitente}",
+ descricao=header_md, # cabeçalho + descrição NC
+ origem=origem_form,
+ tipo=(tipo or "").strip() or None,
+ severidade=(severidade or "").strip() or None,
+ prioridade=(prioridade or "").strip() or None,
+ status="Aberta",
+ data_abertura=datetime.utcnow(),
+ prazo=None,
+ responsavel=None, # pode ser definido depois
+ area_solicitante=(area_solicitante or "").strip() or None,
+ area_notificada=(area_notificada or "").strip() or None,
+ criado_por=st.session_state.get("usuario") or "desconhecido",
+ cliente=(cliente or "").strip() or None,
+ local=(local or "").strip() or None,
+ causa_raiz=causa_md, # bloco estruturado
+ emitente=emitente,
+ data_form=data_form,
+ rnc_cliente_numero=(rnc_cliente_numero or "").strip() or None,
+ cliente_emitente=(cliente_emitente or "").strip() or None
+ )
+ db.add(rnc)
+ db.commit()
+
+ # Ação Imediata/Contenção (LEGADO — uma linha)
+ if (ac_imediata_desc or "").strip():
+ try:
+ ac = RNCAcaoCorretiva(
+ rnc_id=rnc.id,
+ descricao=f"[IMEDIATA/CONTENÇÃO] {ac_imediata_desc.strip()}",
+ responsavel=(ac_imediata_resp or "").strip() or None,
+ prazo=datetime.combine(ac_imediata_data, datetime.min.time()) if isinstance(ac_imediata_data, date) else None,
+ status="Concluída" if isinstance(ac_imediata_data, date) else "Em execução",
+ conclusao_em=datetime.combine(ac_imediata_data, datetime.min.time()) if isinstance(ac_imediata_data, date) else None,
+ eficacia=None
+ )
+ db.add(ac)
+ db.commit()
+ except Exception as e:
+ db.rollback()
+ st.error(f"Erro ao salvar a Ação Imediata (linha única): {e}")
+
+ # Ação Corretiva (LEGADO — uma linha)
+ if (ac_corretiva_desc or "").strip():
+ try:
+ acx = RNCAcaoCorretiva(
+ rnc_id=rnc.id,
+ descricao=f"[Corretiva] {ac_corretiva_desc.strip()}",
+ responsavel=(ac_corretiva_resp or "").strip() or None,
+ prazo=datetime.combine(ac_corretiva_data, datetime.min.time()) if isinstance(ac_corretiva_data, date) else None,
+ status="Concluída" if isinstance(ac_corretiva_data, date) else "Em execução",
+ conclusao_em=datetime.combine(ac_corretiva_data, datetime.min.time()) if isinstance(ac_corretiva_data, date) else None,
+ eficacia=None
+ )
+ db.add(acx)
+ db.commit()
+ except Exception as e:
+ db.rollback()
+ st.error(f"Erro ao salvar a Ação Corretiva (linha única): {e}")
+
+ # Ação Preventiva (LEGADO — uma linha)
+ if (ac_preventiva_desc or "").strip():
+ try:
+ apx = RNCAcaoCorretiva(
+ rnc_id=rnc.id,
+ descricao=f"[Preventiva] {ac_preventiva_desc.strip()}",
+ responsavel=(ac_preventiva_resp or "").strip() or None,
+ prazo=datetime.combine(ac_preventiva_data, datetime.min.time()) if isinstance(ac_preventiva_data, date) else None,
+ status="Concluída" if isinstance(ac_preventiva_data, date) else "Em execução",
+ conclusao_em=datetime.combine(ac_preventiva_data, datetime.min.time()) if isinstance(ac_preventiva_data, date) else None,
+ eficacia=None
+ )
+ db.add(apx)
+ db.commit()
+ except Exception as e:
+ db.rollback()
+ st.error(f"Erro ao salvar a Ação Preventiva (linha única): {e}")
+
+ # salvar múltiplas Ações Imediatas/Contenção
+ try:
+ ac_rows = st.session_state.get("__ai_rows__", {})
+ N = st.session_state.get("__qtd_ai__", 1)
+ if ac_rows:
+ for idx in range(N):
+ row = ac_rows.get(idx, {})
+ desc = (row.get("desc") or "").strip()
+ resp = (row.get("resp") or "").strip() or None
+ dt = row.get("date", None)
+ if desc:
+ ac = RNCAcaoCorretiva(
+ rnc_id=rnc.id,
+ descricao=f"[IMEDIATA/CONTENÇÃO] {desc}",
+ responsavel=resp,
+ prazo=datetime.combine(dt, datetime.min.time()) if isinstance(dt, date) else None,
+ status="Concluída" if isinstance(dt, date) else "Em execução",
+ conclusao_em=datetime.combine(dt, datetime.min.time()) if isinstance(dt, date) else None,
+ eficacia=None
+ )
+ db.add(ac)
+ db.commit()
+ except Exception as e:
+ db.rollback()
+ st.error(f"Erro ao salvar ações imediatas (múltiplas): {e}")
+
+ # salvar múltiplas Ações CORRETIVAS
+ try:
+ ac_rows2 = st.session_state.get("__ac_rows__", {})
+ NC = st.session_state.get("__qtd_ac__", 0)
+ if ac_rows2 and NC > 0:
+ for idx in range(NC):
+ row = ac_rows2.get(idx, {})
+ desc = (row.get("desc") or "").strip()
+ resp = (row.get("resp") or "").strip() or None
+ dt = row.get("date", None)
+ if desc:
+ acor = RNCAcaoCorretiva(
+ rnc_id=rnc.id,
+ descricao=f"[Corretiva] {desc}",
+ responsavel=resp,
+ prazo=datetime.combine(dt, datetime.min.time()) if isinstance(dt, date) else None,
+ status="Concluída" if isinstance(dt, date) else "Em execução",
+ conclusao_em=datetime.combine(dt, datetime.min.time()) if isinstance(dt, date) else None,
+ eficacia=None
+ )
+ db.add(acor)
+ db.commit()
+ except Exception as e:
+ db.rollback()
+ st.error(f"Erro ao salvar ações corretivas (múltiplas): {e}")
+
+ # salvar múltiplas Ações PREVENTIVAS
+ try:
+ ap_rows2 = st.session_state.get("__ap_rows__", {})
+ NP = st.session_state.get("__qtd_ap__", 0)
+ if ap_rows2 and NP > 0:
+ for idx in range(NP):
+ row = ap_rows2.get(idx, {})
+ desc = (row.get("desc") or "").strip()
+ resp = (row.get("resp") or "").strip() or None
+ dt = row.get("date", None)
+ if desc:
+ apre = RNCAcaoCorretiva(
+ rnc_id=rnc.id,
+ descricao=f"[Preventiva] {desc}",
+ responsavel=resp,
+ prazo=datetime.combine(dt, datetime.min.time()) if isinstance(dt, date) else None,
+ status="Concluída" if isinstance(dt, date) else "Em execução",
+ conclusao_em=datetime.combine(dt, datetime.min.time()) if isinstance(dt, date) else None,
+ eficacia=None
+ )
+ db.add(apre)
+ db.commit()
+ except Exception as e:
+ db.rollback()
+ st.error(f"Erro ao salvar ações preventivas (múltiplas): {e}")
+
+ # Anexos
+ if anexos:
+ dest_dir = os.path.join(UPLOAD_DIR, rnc.codigo)
+ _ensure_upload_dir(dest_dir)
+ for up in anexos:
+ dest_path = os.path.join(dest_dir, up.name)
+ with open(dest_path, "wb") as f:
+ f.write(up.getbuffer())
+ anexo = RNCAnexo(
+ rnc_id=rnc.id,
+ nome_arquivo=up.name,
+ caminho=dest_path,
+ conteudo_tipo=getattr(up, "type", None),
+ enviado_por=st.session_state.get("usuario"),
+ enviado_em=datetime.utcnow()
+ )
+ db.add(anexo)
+ db.commit()
+
+ # Auditoria
+ _registrar_log(st.session_state.get("usuario"), f"Criou RNC {rnc.codigo}", "rnc", rnc.id)
+
+ # Notificação (opcional)
+ emails = []
+ criador_email = _get_user_email(rnc.criado_por)
+ if criador_email:
+ emails.append(criador_email)
+ assunto = f"[IOI-RUN] Nova RNC criada: {rnc.codigo}"
+ corpo = f"""Uma nova RNC foi criada.
+
+Código: {rnc.codigo}
+Emitente: {emitente}
+Origem: {origem_form}
+Tipo: {rnc.tipo or '—'} | Severidade: {rnc.severidade or '—'} | Prioridade: {rnc.prioridade or '—'}
+Área Solicitante: {area_solicitante or '—'} | Área Notificada: {area_notificada or '—'}
+Cliente: {rnc.cliente or '—'} | Local: {rnc.local or '—'}
+Abertura: {rnc.data_abertura.strftime('%d/%m/%Y %H:%M')}
+
+Descrição da NC:
+{descricao_nc.strip()}
+"""
+ _send_email_para_responsavel(assunto, corpo, emails)
+
+ st.success(f"RNC **{rnc.codigo}** criada com sucesso!")
+ st.rerun()
+ except Exception as e:
+ db.rollback()
+ st.error(f"Erro ao salvar a RNC: {e}")
+
+
+# ===============================
+# EDIÇÃO de RNC existente
+# ===============================
+def _extrair_descricao_nc(descricao_md: str) -> str:
+ """Tenta extrair o corpo após o marcador '**Descrição da Não Conformidade:**'."""
+ if not descricao_md:
+ return ""
+ marker = "**Descrição da Não Conformidade:**"
+ if marker in descricao_md:
+ try:
+ return descricao_md.split(marker, 1)[1].strip()
+ except Exception:
+ return descricao_md
+ return descricao_md
+
+
+def _montar_descricao_md(data_form: date, emitente: str, codigo: str,
+ rnc_cliente_numero: Optional[str], cliente_emitente: Optional[str],
+ area_solicitante: Optional[str], origem_form: str, area_notificada: Optional[str],
+ inv1: Dict[str, str], inv2: Dict[str, str],
+ descricao_nc: str) -> str:
+ header_md = f"""**FOR-SGQ-08 • Rev 01**
+**DATA:** {data_form.strftime('%d/%m/%Y')} • **EMITENTE:** {emitente} • **RNC Nº:** {codigo}
+**RNC CLIENTE Nº:** {rnc_cliente_numero or 'N/A'} • **CLIENTE EMITENTE:** {cliente_emitente or 'N/A'}
+**Área Solicitante:** {area_solicitante or '—'} • **Origem:** {origem_form} • **Área Notificada:** {area_notificada or '—'}
+**Envolvidos:**
+- {inv1.get('nome') or '—'} • Matr.: {inv1.get('matr') or '—'} • Função: {inv1.get('func') or '—'}
+- {inv2.get('nome') or '—'} • Matr.: {inv2.get('matr') or '—'} • Função: {inv2.get('func') or '—'}
+
+**Descrição da Não Conformidade:**
+{(descricao_nc or '').strip()}
+"""
+ return header_md
+
+
+def _editar_rnc(db):
+ st.subheader("✏️ Editar RNC existente")
+
+ # Controles de busca
+ col_s1, col_s2, col_s3, col_s4 = st.columns([1.2, 1.6, 0.6, 0.6])
+ cod_informado = col_s1.text_input("Informe o código (ex.: RNC-2026-0001)", key="__rnc_edit_code__")
+ # últimas 30
+ recentes = db.query(RNC).order_by(RNC.data_abertura.desc()).limit(30).all()
+ opts = [f"{r.codigo} — {r.titulo or '—'}" for r in recentes]
+ sel = col_s2.selectbox("ou selecione uma RNC recente", ["(nenhuma)"] + opts, index=0)
+ trigger = col_s3.button("Carregar")
+ limpar = col_s4.button("Limpar seleção")
+
+ # Botão limpar: NÃO alterar __rnc_edit_code__ para evitar erro do Streamlit
+ if limpar:
+ st.session_state.pop("__rnc_edit_id__", None)
+ st.rerun()
+
+ # Botão carregar
+ if trigger:
+ # Se "(nenhuma)" e sem código — limpar seleção anterior
+ if sel == "(nenhuma)" and not (cod_informado or "").strip():
+ st.session_state.pop("__rnc_edit_id__", None)
+ st.info("Nenhuma RNC selecionada. A seleção foi limpa.")
+ st.rerun()
+
+ # Resolve código digitado ou selecionado
+ codigo = (cod_informado or "").strip() or (sel.split(" — ")[0] if sel != "(nenhuma)" else "")
+ alvo = db.query(RNC).filter(RNC.codigo == codigo).first() if codigo else None
+
+ if not alvo:
+ st.session_state.pop("__rnc_edit_id__", None)
+ st.warning("RNC não encontrada ou nenhuma selecionada. A seleção foi limpa.")
+ st.rerun()
+ else:
+ st.session_state["__rnc_edit_id__"] = alvo.id
+ st.success(f"RNC {alvo.codigo} carregada para edição.")
+ st.rerun()
+
+ # RNC carregada
+ rnc = db.query(RNC).filter(RNC.id == st.session_state.get("__rnc_edit_id__")).first() if st.session_state.get("__rnc_edit_id__") else None
+
+ if not rnc:
+ st.info("Carregue uma RNC para editar.")
+ return
+
+ # Form de edição
+ with st.form(f"form_edit_rnc_{rnc.id}"):
+ st.markdown(f"**Editando:** {rnc.codigo} • {rnc.titulo or '—'}")
+
+ col_eh1, col_eh2, col_eh3 = st.columns([1, 1, 1])
+ data_form = col_eh1.date_input("DATA", value=rnc.data_form or date.today(), format="DD/MM/YYYY")
+ emitente = col_eh2.text_input("EMITENTE", value=rnc.emitente or "")
+ origem_form = col_eh3.selectbox("Origem", ORIGENS_FORMS, index=(ORIGENS_FORMS.index(rnc.origem) if rnc.origem in ORIGENS_FORMS else 2))
+
+ col_eh4, col_eh5 = st.columns([1, 1])
+ rnc_cliente_numero = col_eh4.text_input("RNC CLIENTE Nº (opcional)", value=rnc.rnc_cliente_numero or "")
+ cliente_emitente = col_eh5.text_input("CLIENTE EMITENTE (opcional)", value=rnc.cliente_emitente or "")
+
+ col_h6, col_h7 = st.columns([1, 1])
+ area_solicitante = col_h6.text_input("Área Solicitante", value=rnc.area_solicitante or "")
+ area_notificada = col_h7.text_input("Área Notificada", value=rnc.area_notificada or "")
+
+ st.markdown("##### Classificação")
+ col_c1, col_c2, col_c3 = st.columns([1, 1, 1])
+ tipo = col_c1.selectbox("Tipo", TIPOS, index=(TIPOS.index(rnc.tipo) if rnc.tipo in TIPOS else 0))
+ severidade = col_c2.selectbox("Severidade", SEVERIDADES, index=(SEVERIDADES.index(rnc.severidade) if rnc.severidade in SEVERIDADES else 1))
+ prioridade = col_c3.selectbox("Prioridade", PRIORIDADES, index=(PRIORIDADES.index(rnc.prioridade) if rnc.prioridade in PRIORIDADES else 1))
+
+ col_c4, col_c5 = st.columns([1, 1])
+ cliente = col_c4.text_input("Cliente (opcional)", value=rnc.cliente or "")
+ local = col_c5.text_input("Local (opcional)", value=rnc.local or "")
+
+ st.markdown("##### Atribuição e Prazo")
+ col_as1, col_as2, col_as3 = st.columns([1, 1, 1])
+ responsavel = col_as1.text_input("Responsável", value=rnc.responsavel or "")
+ prazo = col_as2.date_input("Prazo (conclusão)", value=(rnc.prazo.date() if isinstance(rnc.prazo, datetime) else rnc.prazo) if rnc.prazo else None, format="DD/MM/YYYY")
+ status = col_as3.selectbox("Status", STATUS_OPCOES, index=(STATUS_OPCOES.index(rnc.status) if rnc.status in STATUS_OPCOES else 0))
+
+ st.markdown("##### Descrição e Causa Raiz")
+ # tenta extrair apenas a descrição da NC a partir do markdown original
+ desc_nc_atual = _extrair_descricao_nc(rnc.descricao or "")
+ descricao_nc = st.text_area("Descrição da Não Conformidade (substituir corpo)", value=desc_nc_atual or "", height=140)
+ causa_raiz = st.text_area("Causa Raiz Identificada", value=rnc.causa_raiz or "", height=120)
+
+ # Envolvidos (mantemos inputs simples; no cadastro ficam no cabeçalho)
+ with st.expander("Envolvidos (opcional)"):
+ col_en1, col_en2, col_en3 = st.columns([2, 1, 1])
+ inv1_nome = col_en1.text_input("Envolvido 1 — Nome", value="")
+ inv1_matr = col_en2.text_input("Matrícula 1", value="")
+ inv1_func = col_en3.text_input("Função 1", value="")
+
+ col_en4, col_en5, col_en6 = st.columns([2, 1, 1])
+ inv2_nome = col_en4.text_input("Envolvido 2 — Nome", value="")
+ inv2_matr = col_en5.text_input("Matrícula 2", value="")
+ inv2_func = col_en6.text_input("Função 2", value="")
+
+ salvar = st.form_submit_button("💾 Salvar alterações")
+
+ if salvar:
+ usuario = st.session_state.get("usuario")
+ perfil = (st.session_state.get("perfil") or "user").lower()
+
+ # Permissão para Encerrar/Cancelar
+ if status in ["Encerrada", "Cancelada"] and not can_close_or_cancel(perfil):
+ st.warning("Somente administradores podem **encerrar** ou **cancelar** RNC.")
+ return
+
+ try:
+ # Atualiza campos básicos
+ rnc.emitente = (emitente or "").strip() or None
+ rnc.origem = origem_form
+ rnc.rnc_cliente_numero = (rnc_cliente_numero or "").strip() or None
+ rnc.cliente_emitente = (cliente_emitente or "").strip() or None
+ rnc.area_solicitante = (area_solicitante or "").strip() or None
+ rnc.area_notificada = (area_notificada or "").strip() or None
+ rnc.tipo = (tipo or "").strip() or None
+ rnc.severidade = (severidade or "").strip() or None
+ rnc.prioridade = (prioridade or "").strip() or None
+ rnc.cliente = (cliente or "").strip() or None
+ rnc.local = (local or "").strip() or None
+ rnc.responsavel = (responsavel or "").strip() or None
+ rnc.prazo = datetime.combine(prazo, datetime.min.time()) if isinstance(prazo, date) else None
+ rnc.status = status
+ rnc.data_form = data_form
+ rnc.titulo = f"RNC {rnc.codigo} • {emitente or '—'}"
+ rnc.causa_raiz = (causa_raiz or "").strip() or None
+
+ # Reconstrói o markdown de 'descricao' no mesmo padrão do cadastro
+ inv1 = {"nome": inv1_nome, "matr": inv1_matr, "func": inv1_func}
+ inv2 = {"nome": inv2_nome, "matr": inv2_matr, "func": inv2_func}
+ rnc.descricao = _montar_descricao_md(
+ data_form=data_form,
+ emitente=emitente,
+ codigo=rnc.codigo,
+ rnc_cliente_numero=rnc_cliente_numero,
+ cliente_emitente=cliente_emitente,
+ area_solicitante=area_solicitante,
+ origem_form=origem_form,
+ area_notificada=area_notificada,
+ inv1=inv1,
+ inv2=inv2,
+ descricao_nc=descricao_nc,
+ )
+
+ # Encerramento: registra data
+ if status == "Encerrada":
+ rnc.encerrada_em = datetime.utcnow()
+ elif status != "Encerrada":
+ rnc.encerrada_em = None
+
+ db.add(rnc)
+ db.commit()
+
+ _registrar_log(usuario, f"Editou RNC {rnc.codigo}", "rnc", rnc.id)
+ st.success("RNC atualizada com sucesso!")
+ st.rerun()
+ except Exception as e:
+ db.rollback()
+ st.error(f"Erro ao salvar alterações: {e}")
+
+
+# ===============================
+# Anexos / Plano de Ação / Timeline / Aprovação
+# ===============================
+def _mostrar_anexos(rnc: RNC, db):
+ st.markdown("**Anexos**")
+ if rnc.anexos:
+ for an in rnc.anexos:
+ st.caption(f"📎 {an.nome_arquivo} • {an.enviado_por or '—'} • {an.enviado_em.strftime('%d/%m/%Y %H:%M')}")
+ if os.path.exists(an.caminho):
+ try:
+ with open(an.caminho, "rb") as f:
+ st.download_button(f"Baixar {an.nome_arquivo}", f.read(), file_name=an.nome_arquivo, key=f"dl_{rnc.id}_{an.id}")
+ except Exception:
+ st.caption("Arquivo indisponível.")
+ else:
+ st.caption("Sem anexos")
+
+ up_more = st.file_uploader(f"Adicionar anexos — {rnc.codigo}", accept_multiple_files=True, key=f"up_{rnc.id}")
+ if up_more:
+ dest_dir = os.path.join(UPLOAD_DIR, rnc.codigo)
+ _ensure_upload_dir(dest_dir)
+ try:
+ for up in up_more:
+ dest_path = os.path.join(dest_dir, up.name)
+ with open(dest_path, "wb") as f:
+ f.write(up.getbuffer())
+ anexo = RNCAnexo(
+ rnc_id=rnc.id,
+ nome_arquivo=up.name,
+ caminho=dest_path,
+ conteudo_tipo=getattr(up, "type", None),
+ enviado_por=st.session_state.get("usuario"),
+ enviado_em=datetime.utcnow()
+ )
+ db.add(anexo)
+ db.commit()
+ st.success("Anexos adicionados.")
+ st.rerun()
+ except Exception as e:
+ db.rollback()
+ st.error(f"Erro ao salvar anexos: {e}")
+
+
+def _mostrar_plano_acao(rnc: RNC, db):
+ st.markdown("### 🛠️ Plano de Ação — Separado por categorias")
+
+ def _cat(descricao: str) -> str:
+ s = (descricao or "").strip()
+ if s.startswith("[IMEDIATA/CONTENÇÃO]"):
+ return "imediata"
+ elif s.startswith("[Preventiva]"):
+ return "preventiva"
+ elif s.startswith("[Corretiva]"):
+ return "corretiva"
+ else:
+ return "outras"
+
+ acoes = rnc.acoes or []
+ acoes_imediatas = [a for a in acoes if _cat(a.descricao) == "imediata"]
+ acoes_corretivas = [a for a in acoes if _cat(a.descricao) == "corretiva"]
+ acoes_preventivas = [a for a in acoes if _cat(a.descricao) == "preventiva"]
+ acoes_outras = [a for a in acoes if _cat(a.descricao) == "outras"]
+
+ def _render_bloco(titulo: str, lista: List[RNCAcaoCorretiva]):
+ st.markdown(f"#### {titulo}")
+ if not lista:
+ st.caption("— Nenhuma ação cadastrada nesta categoria.")
+ return
+ lista = sorted(lista, key=lambda a: (a.status, a.prazo or datetime.max))
+ for ac in lista:
+ line = f"- {ac.descricao}"
+ line += f" • Resp: {ac.responsavel or '—'}"
+ line += f" • Prazo: {ac.prazo.strftime('%d/%m/%Y') if ac.prazo else '—'}"
+ line += f" • Status: {ac.status}"
+ if ac.eficacia:
+ line += f" • Eficácia: {ac.eficacia}"
+ st.write(line)
+
+ with st.expander(f"Atualizar ação • ID {ac.id}", expanded=False):
+ col_s1, col_s2, col_s3 = st.columns(3)
+ novo_status = col_s1.selectbox(
+ "Status",
+ ["Planejada", "Em execução", "Concluída", "Ineficaz"],
+ index=["Planejada", "Em execução", "Concluída", "Ineficaz"].index(ac.status)
+ if ac.status in ["Planejada", "Em execução", "Concluída", "Ineficaz"] else 0,
+ key=f"ac_st_{ac.id}"
+ )
+ nova_eficacia = col_s2.selectbox(
+ "Eficácia (resultado da ação)",
+ ["(não avaliada)", "Eficaz", "Parcial", "Ineficaz"],
+ index=0 if not ac.eficacia else ["(não avaliada)", "Eficaz", "Parcial", "Ineficaz"].index(
+ ac.eficacia if ac.eficacia in ["Eficaz", "Parcial", "Ineficaz"] else "(não avaliada)"
+ ),
+ key=f"ac_eff_{ac.id}"
+ )
+ concluir = col_s3.button("Salvar atualização", key=f"ac_sv_{ac.id}")
+
+ if concluir:
+ try:
+ ac.status = novo_status
+ if novo_status == "Concluída":
+ ac.conclusao_em = datetime.utcnow()
+ ac.eficacia = None if nova_eficacia == "(não avaliada)" else nova_eficacia
+ db.add(ac)
+ db.commit()
+ _registrar_log(st.session_state.get("usuario"), f"Atualizou ação RNC {rnc.codigo} (ID ação {ac.id})", "rnc_acao", ac.id)
+ st.success("Ação atualizada.")
+ st.rerun()
+ except Exception as e:
+ db.rollback()
+ st.error(f"Erro ao atualizar ação: {e}")
+
+ _render_bloco("🚀 Ações Imediatas/Contenção", acoes_imediatas)
+ _render_bloco("🔧 Ações Corretivas", acoes_corretivas)
+ _render_bloco("🛡️ Ações Preventivas", acoes_preventivas)
+ _render_bloco("📁 Outras (sem prefixo)", acoes_outras)
+
+ # Form nova ação
+ with st.form(f"form_new_action_{rnc.id}"):
+ col_a1, col_a2, col_a3, col_a4 = st.columns([1.6, 1, 1, 1])
+ ac_tipo = col_a1.selectbox("Tipo da ação", ["Imediata/Contenção", "Corretiva", "Preventiva"], index=1, key=f"ac_tipo_{rnc.id}")
+ ac_desc = col_a1.text_input("Descrição da ação", key=f"ac_desc_{rnc.id}")
+ ac_resp = col_a2.text_input("Responsável", key=f"ac_resp_{rnc.id}")
+ ac_prazo = col_a3.date_input("Data de Conclusão (prazo)", value=None, format="DD/MM/YYYY", key=f"ac_prazo_{rnc.id}")
+ ac_enviar = col_a4.form_submit_button("Adicionar ação")
+ if ac_enviar:
+ if not ac_desc:
+ st.warning("Informe a descrição da ação.")
+ else:
+ try:
+ tipo_prefix = "[IMEDIATA/CONTENÇÃO]" if ac_tipo == "Imediata/Contenção" else ("[Preventiva]" if ac_tipo == "Preventiva" else "[Corretiva]")
+ new_ac = RNCAcaoCorretiva(
+ rnc_id=rnc.id,
+ descricao=f"{tipo_prefix} {ac_desc.strip()}",
+ responsavel=(ac_resp or "").strip() or None,
+ prazo=datetime.combine(ac_prazo, datetime.min.time()) if isinstance(ac_prazo, date) else None,
+ status="Planejada" if not isinstance(ac_prazo, date) else "Concluída",
+ conclusao_em=datetime.combine(ac_prazo, datetime.min.time()) if isinstance(ac_prazo, date) else None
+ )
+ db.add(new_ac)
+ db.commit()
+ _registrar_log(st.session_state.get("usuario"), f"Adicionou ação em {rnc.codigo}", "rnc_acao", new_ac.id)
+ st.success("Ação adicionada.")
+ st.rerun()
+ except Exception as e:
+ db.rollback()
+ st.error(f"Erro ao salvar ação: {e}")
+
+
+def _mostrar_verificacao_eficacia(rnc: RNC, db, usuario: str, perfil: str):
+ st.markdown("### ✅ Verificação da Eficácia e Aprovação")
+ st.caption("Descreva como a eficácia será monitorada e registre o resultado das ações. No caso de **Não Eficaz**, a RNC será reaberta.")
+ with st.form(f"form_verif_{rnc.id}"):
+ plano = st.text_area("O que será feito para manter e acompanhar a eficácia das ações aplicadas?", height=120, key=f"verif_plano_{rnc.id}")
+ col_v1, col_v2, col_v3 = st.columns([1, 1, 1])
+ resultado = col_v1.selectbox("Resultado das ações", ["(pendente)", "Eficaz", "Não Eficaz"], index=0, key=f"verif_res_{rnc.id}")
+ encerrar_agora = col_v2.checkbox("Encerrar RNC (se Eficaz)", value=False, key=f"verif_close_{rnc.id}")
+ resp_enc = col_v3.text_input("Responsável pelo encerramento (opcional)", value=usuario, key=f"verif_enc_{rnc.id}")
+ enviar = st.form_submit_button("Registrar verificação")
+ if enviar:
+ if not plano.strip() and resultado == "(pendente)":
+ st.warning("Registre o plano de verificação ou o resultado.")
+ return
+ try:
+ msg = f"[Verificação da Eficácia]\n{plano.strip() or '(sem plano)'}\n\nResultado: {resultado}"
+ cm = RNCComentario(
+ rnc_id=rnc.id,
+ autor=usuario,
+ mensagem=msg,
+ data=datetime.utcnow()
+ )
+ db.add(cm)
+
+ # Regras: se Não Eficaz → reabrir (Em Análise). Se Eficaz e encerrar → Encerrada.
+ if resultado == "Não Eficaz":
+ rnc.status = "Em Análise"
+ rnc.encerrada_em = None
+ db.add(rnc)
+ elif resultado == "Eficaz" and encerrar_agora and can_close_or_cancel(perfil):
+ rnc.status = "Encerrada"
+ rnc.encerrada_em = datetime.utcnow()
+ cm2 = RNCComentario(
+ rnc_id=rnc.id,
+ autor=resp_enc or usuario,
+ mensagem=f"[Encerramento] RNC encerrada por {resp_enc or usuario} em {datetime.utcnow().strftime('%d/%m/%Y %H:%M')}",
+ status_novo="Encerrada",
+ data=datetime.utcnow()
+ )
+ db.add(cm2)
+ db.add(rnc)
+ db.commit()
+
+ _registrar_log(usuario, f"Verificação de eficácia em {rnc.codigo} (resultado: {resultado})", "rnc", rnc.id)
+ st.success("Verificação registrada.")
+ st.rerun()
+ except Exception as e:
+ db.rollback()
+ st.error(f"Erro ao registrar verificação: {e}")
+
+
+def _mostrar_timeline(rnc: RNC, db, usuario: str, perfil: str):
+ st.markdown("### 🕒 Timeline / Andamento")
+ if rnc.comentarios:
+ for cm in sorted(rnc.comentarios, key=lambda c: c.data, reverse=True):
+ line = f"**{cm.data.strftime('%d/%m/%Y %H:%M')}** • {cm.autor}: {cm.mensagem}"
+ changes = []
+ if cm.status_novo:
+ changes.append(f"Status → {cm.status_novo}")
+ if cm.responsavel_novo:
+ changes.append(f"Responsável → {cm.responsavel_novo}")
+ if cm.prazo_novo:
+ changes.append(f"Prazo → {cm.prazo_novo.strftime('%d/%m/%Y')}")
+ if changes:
+ line += " \n" + " • ".join(changes)
+ st.write(line)
+ else:
+ st.caption("Sem comentários ainda.")
+
+ st.markdown("#### Comentário/Atualização")
+ with st.form(f"form_comment_{rnc.id}"):
+ msg = st.text_area("Comentário/atualização (ex.: progresso, decisões)", height=80, key=f"cm_msg_{rnc.id}")
+ col_u1, col_u2, col_u3, col_u4 = st.columns(4)
+ novo_status = col_u1.selectbox("Atualizar status", ["(sem mudança)"] + STATUS_OPCOES, index=0, key=f"cm_st_{rnc.id}")
+ novo_resp = col_u2.text_input("Atualizar responsável", key=f"cm_resp_{rnc.id}")
+ novo_prazo = col_u3.date_input("Atualizar prazo", value=None, format="DD/MM/YYYY", key=f"cm_prazo_{rnc.id}")
+ salvar_cm = col_u4.form_submit_button("Registrar")
+
+ if salvar_cm:
+ if not msg.strip() and novo_status == "(sem mudança)" and not novo_resp and not novo_prazo:
+ st.warning("Registre um comentário ou alguma mudança.")
+ return
+
+ aplicar_mudancas = can_edit(rnc, usuario, perfil)
+ status_desejado = None if novo_status == "(sem mudança)" else novo_status
+
+ if status_desejado in ["Encerrada", "Cancelada"] and not can_close_or_cancel(perfil):
+ st.warning("Somente administradores podem **encerrar** ou **cancelar** RNC.")
+ aplicar_mudancas = False # não aplicar status proibido
+
+ try:
+ cm = RNCComentario(
+ rnc_id=rnc.id,
+ autor=usuario,
+ mensagem=((msg or "").strip() or "(sem mensagem)"),
+ status_novo=status_desejado,
+ responsavel_novo=(novo_resp or "").strip() or None,
+ prazo_novo=datetime.combine(novo_prazo, datetime.min.time()) if isinstance(novo_prazo, date) else None,
+ data=datetime.utcnow()
+ )
+ db.add(cm)
+
+ # Aplica mudanças
+ if aplicar_mudancas:
+ if status_desejado:
+ rnc.status = status_desejado
+ if status_desejado == "Encerrada":
+ rnc.encerrada_em = datetime.utcnow()
+ if cm.responsavel_novo:
+ rnc.responsavel = cm.responsavel_novo
+ if cm.prazo_novo:
+ rnc.prazo = cm.prazo_novo
+ db.add(rnc)
+ db.commit()
+
+ _registrar_log(usuario, f"Atualizou RNC {rnc.codigo}", "rnc", rnc.id)
+
+ # Notificação (opcional)
+ if status_desejado in ["Verificação", "Encerrada"]:
+ emails = []
+ if rnc.responsavel:
+ resp_email = _get_user_email(rnc.responsavel)
+ if resp_email:
+ emails.append(resp_email)
+ criador_email = _get_user_email(rnc.criado_por)
+ if criador_email:
+ emails.append(criador_email)
+ assunto = f"[IOI-RUN] RNC {rnc.codigo} — Status: {status_desejado}"
+ corpo = f"""A RNC {rnc.codigo} teve seu status atualizado para: {status_desejado}.
+
+Título: {rnc.titulo}
+Responsável: {rnc.responsavel or '—'}
+Prazo: {rnc.prazo.strftime('%d/%m/%Y') if rnc.prazo else '—'}
+Atualizado por: {usuario}
+Data: {datetime.utcnow().strftime('%d/%m/%Y %H:%M')}
+
+Comentário:
+{(msg or '').strip() or '(sem mensagem)'}"""
+ _send_email_para_responsavel(assunto, corpo, emails)
+
+ st.success("Atualização registradoa.")
+ st.rerun()
+ except Exception as e:
+ db.rollback()
+ st.error(f"Erro ao registrar atualização: {e}")
+
+
+def _mostrar_analise_aprovacao(rnc: RNC, db, usuario: str):
+ st.markdown("### 🧾 Análise Crítica e Aprovação")
+ st.caption("Registre os responsáveis pela análise crítica (QSMS) e aprovação (Diretoria).")
+ with st.form(f"form_aprov_{rnc.id}"):
+ st.markdown("**Análise Crítica (QSMS)**")
+ col_a1, col_a2 = st.columns([1, 1])
+ analise_nome = col_a1.text_input("Nome (QSMS)", key=f"ap_nome_{rnc.id}")
+ analise_setor = col_a2.text_input("Setor (QSMS)", value="QSMS", key=f"ap_setor_{rnc.id}")
+
+ st.markdown("**Aprovação (Diretoria)**")
+ col_b1, col_b2 = st.columns([1, 1])
+ aprov_nome = col_b1.text_input("Nome (Diretoria)", key=f"ap_dir_nome_{rnc.id}")
+ aprov_setor = col_b2.text_input("Setor (Diretoria)", value="Diretor Executivo e de Operações", key=f"ap_dir_setor_{rnc.id}")
+
+ enviar = st.form_submit_button("Registrar análise/aprovação")
+ if enviar:
+ try:
+ msg = f"[Análise Crítica / Aprovação]\nQSMS: {analise_nome or '—'} • Setor: {analise_setor or '—'}\nDiretoria: {aprov_nome or '—'} • Setor: {aprov_setor or '—'}"
+ cm = RNCComentario(
+ rnc_id=rnc.id,
+ autor=usuario,
+ mensagem=msg,
+ data=datetime.utcnow()
+ )
+ db.add(cm)
+ db.commit()
+ except Exception as e:
+ db.rollback()
+ st.error(f"Erro ao registrar análise/aprovação: {e}")
+
+
+# ===============================
+# Tema/Background específico do módulo RNC
+# ===============================
+def _apply_rnc_theme():
+ """
+ Aplica um tema visual específico para a página do módulo RNC
+ (background diferenciado, header destacado e sutis ajustes visuais).
+ """
+ st.markdown(
+ """
+
+ """,
+ unsafe_allow_html=True
+ )
+
+
+# ===============================
+# Página principal (container)
+# ===============================
+def _card_rnc_header(rnc: RNC):
+ col_a, col_b = st.columns([3, 1])
+ with col_a:
+ st.markdown(f"**{rnc.codigo}** — {rnc.titulo or '—'}")
+ st.caption(
+ f"Origem: {rnc.origem or '—'} • "
+ f"Tipo: {rnc.tipo or '—'} • "
+ f"Severidade: {rnc.severidade or '—'} • "
+ f"Prioridade: {rnc.prioridade or '—'}"
+ )
+ st.caption(
+ f"Responsável: {rnc.responsavel or '—'} • "
+ f"Prazo: {rnc.prazo.strftime('%d/%m/%Y') if rnc.prazo else '—'} • "
+ f"Abertura: {rnc.data_abertura.strftime('%d/%m/%Y %H:%M') if rnc.data_abertura else '—'}"
+ )
+ with col_b:
+ col_b.metric("Status", rnc.status or "—")
+
+
+def _blocos_rnc(rnc: RNC, db, usuario: str, perfil: str):
+ st.markdown("---")
+ # Conteúdo principal do card/expander
+ st.markdown("#### 📄 Cabeçalho / Descrição")
+ st.markdown(rnc.descricao or "—")
+
+ st.markdown("---")
+ _mostrar_anexos(rnc, db)
+
+ st.markdown("---")
+ _mostrar_plano_acao(rnc, db)
+
+ st.markdown("---")
+ _mostrar_timeline(rnc, db, usuario, perfil)
+
+ st.markdown("---")
+ _mostrar_verificacao_eficacia(rnc, db, usuario, perfil)
+
+ st.markdown("---")
+ _mostrar_analise_aprovacao(rnc, db, usuario)
+
+
+def pagina():
+ """Tela principal do módulo RNC (cadastro + edição)."""
+ _apply_rnc_theme()
+
+ st.header("RNC • Registro de Não Conformidades (FOR-SGQ-08 Rev 01)")
+
+ usuario = st.session_state.get("usuario") or "desconhecido"
+ perfil = (st.session_state.get("perfil") or "user").lower()
+
+ db = SessionLocal()
+ try:
+ # KPIs
+ _kpis_area(db, usuario, perfil)
+ st.divider()
+
+ # Formulário de Nova RNC
+ _form_nova_rnc(db)
+ st.divider()
+
+ # ✏️ Edição de RNC existente (sem listagem/export aqui)
+ _editar_rnc(db)
+
+ # Se houver RNC carregada para edição, renderiza blocos auxiliares
+ if st.session_state.get("__rnc_edit_id__"):
+ rnc = db.query(RNC).filter(RNC.id == st.session_state["__rnc_edit_id__"]).first()
+ if rnc:
+ with st.expander(f"📂 Detalhes • {rnc.codigo} — {rnc.titulo or '—'}", expanded=False):
+ _card_rnc_header(rnc)
+ _blocos_rnc(rnc, db, usuario, perfil)
+
+ except Exception as e:
+ st.error(f"Erro ao montar a página de RNC: {e}")
+ finally:
+ try:
+ db.close()
+ except Exception:
+ pass
+
+
+# Alias opcional para compatibilidade com app.py
+def view():
+ """Alias de pagina() para compatibilidade."""
+ return pagina()
+
+
+
+
+
+
+