GiovanniCRF's picture
Update app.py
49a0baf verified
Raw
History Blame Contribute Delete
31.9 kB
# app.py
# CIA – Central de Impressões APOGEU
# Streamlit (Hugging Face Spaces)
#
# Versão "nível Apple" consolidada:
# ✅ Sem sidebar: Tipo/Acesso/Solicitante/Unidade abaixo do cabeçalho
# ✅ Impressão + Acabamentos (acabamentos abaixo; total = soma)
# ✅ Quantidade de acabamentos = Tiragem (não pergunta quantidade)
# ✅ Regra OFICIAL: 1x1 = 1x0 × 2 (base sempre por página)
# ✅ Upload múltiplo obrigatório (1+ arquivos) — base64 só no clique/execução (evita rerun pesado)
# ✅ Persistência de contato: Nome/E-mail não somem ao anexar arquivos
# ✅ Segurança: não exibe links de arquivos após envio (e GAS deve estar sem links no e-mail)
# ✅ Bloqueio REAL anti-reenvio: trava otimista + sending inflight (sem “segundo envio”)
# ✅ Compatibilidade: envia tipo e tipo_pedido
from __future__ import annotations
import base64
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
import pandas as pd
import requests
import streamlit as st
# =========================
# Config / Assets
# =========================
BASE_DIR = Path(__file__).resolve().parent
HEADER_IMG = BASE_DIR / "CIA - Central de Impressões APOGEU.png"
EXCEL_BASE = BASE_DIR / "vale-o-que-custa-MODELO.xlsx"
UNIDADES = [
"Global School Cidade Alta",
"Global School Ferreira Guimarães",
"Santo Antônio 1",
"Santo Antônio 2",
"Zona Norte",
"Central",
]
SOLICITANTES = ["Unidade", "Central"]
TIPOS = [
"Material Didático",
"Avaliação/Cartão Resposta",
"Material Extra",
"Carta Dourada",
"Certificado A+",
"Certificado Olímpico",
]
# =========================
# Helpers
# =========================
def get_webhook_url() -> Optional[str]:
try:
return st.secrets.get("WEBHOOK_URL", None)
except Exception:
return None
def money_br(v: float) -> str:
return f"R$ {v:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
def split_multi(value: Any) -> list[str]:
if value is None:
return []
s = str(value).strip()
if not s:
return []
return [p.strip() for p in s.split(";") if p.strip()]
def verso_label(code: str) -> str:
c = (code or "").strip().lower()
if c == "1x0":
return "1 face (1x0)"
if c == "1x1":
return "Frente e verso (1x1)"
return code
def render_header_img(path: Path) -> None:
"""Header definitivo sem corte usando HTML <img> base64 e object-fit: contain."""
if not path.exists():
st.warning(
f"Imagem de cabeçalho não encontrada: **{path.name}**.\n\n"
"Confira se o arquivo está na raiz do repositório e com o nome EXATO."
)
return
b = path.read_bytes()
b64 = base64.b64encode(b).decode("utf-8")
st.markdown(
f"""
<div style="
width:100%;
border-radius:22px;
overflow:hidden;
border:1px solid rgba(0,0,0,0.08);
background: rgba(255,255,255,0.70);
">
<img
src="data:image/png;base64,{b64}"
style="width:100%; height:auto; display:block; object-fit:contain;"
/>
</div>
""",
unsafe_allow_html=True,
)
def uploads_to_payload(uploads) -> List[Dict[str, Any]]:
"""
Converte UploadedFiles em lista serializável.
IMPORTANTE: chamar somente na execução do envio (evita rerun pesado).
"""
out: List[Dict[str, Any]] = []
for up in uploads or []:
raw = up.getvalue()
out.append(
{
"filename": up.name,
"mimetype": up.type or "application/octet-stream",
"content_base64": base64.b64encode(raw).decode("utf-8"),
"size_bytes": len(raw),
}
)
return out
@st.cache_data(show_spinner=False)
def load_excel_tables(xlsx_path: Path) -> Tuple[pd.DataFrame, pd.DataFrame]:
if not xlsx_path.exists():
return pd.DataFrame(), pd.DataFrame()
df_imp = pd.read_excel(xlsx_path, sheet_name="impressao")
df_acab = pd.read_excel(xlsx_path, sheet_name="acabamentos")
df_imp.columns = [str(c).strip() for c in df_imp.columns]
df_acab.columns = [str(c).strip() for c in df_acab.columns]
if "ativo" in df_imp.columns:
df_imp = df_imp[df_imp["ativo"].astype(bool)]
if "ativo" in df_acab.columns:
df_acab = df_acab[df_acab["ativo"].astype(bool)]
return df_imp, df_acab
def filter_by_solicitante(df: pd.DataFrame, solicitante_tipo: str) -> pd.DataFrame:
if df.empty:
return df
if "Solicitante" not in df.columns:
return df
return df[df["Solicitante"].astype(str).str.strip().str.lower() == solicitante_tipo.strip().lower()]
def post_to_webhook(
payload: Dict[str, Any], webhook_url: str, timeout: int = 240
) -> Tuple[bool, str, int, str, Optional[Dict[str, Any]]]:
"""
Regra: Só OK se JSON tiver status="ok".
"""
try:
r = requests.post(webhook_url, json=payload, timeout=timeout)
raw = r.text or ""
parsed: Optional[Dict[str, Any]] = None
try:
parsed = r.json()
except Exception:
parsed = None
if not (200 <= r.status_code < 300):
return False, f"Falha HTTP {r.status_code}", r.status_code, raw[:2000], parsed
if not isinstance(parsed, dict) or "status" not in parsed:
return False, "Servidor não retornou JSON válido com status.", r.status_code, raw[:2000], parsed
if str(parsed.get("status")).lower() != "ok":
return False, f"Servidor respondeu erro: {parsed.get('message','(sem mensagem)')}", r.status_code, raw[:2000], parsed
return True, "Pedido confirmado pelo servidor.", r.status_code, raw[:2000], parsed
except Exception as e:
return False, f"Erro ao enviar: {e}", 0, "", None
def build_resumo_pedido(sim_imp: Dict[str, Any], acab_list: List[Dict[str, Any]], tipo: str) -> str:
verso = sim_imp.get("verso", "")
tiragem = int(sim_imp.get("tiragem", 0) or 0)
paginas_por_copia = int(sim_imp.get("paginas_por_copia", 0) or 0)
paginas_totais = int(sim_imp.get("paginas_totais", 0) or 0)
base = f"Tipo: **{tipo}**\n\n{tiragem} cópias • {paginas_por_copia} página(s) por cópia • total {paginas_totais} página(s)"
imp = (
f"Impressão: **{sim_imp.get('tipo_papel','')}** • **{sim_imp.get('formato','')}** • "
f"**{sim_imp.get('cor','')}** • verso **{verso}**"
)
if acab_list:
acabs = ", ".join([a.get("acabamento", "") for a in acab_list])
acab_txt = f"Acabamentos: **{acabs}** (quantidade = tiragem)"
else:
acab_txt = "Acabamentos: **nenhum**"
return f"{base}\n\n{imp}\n\n{acab_txt}"
# =========================
# Premium UI
# =========================
st.set_page_config(page_title="CIA - Central de Impressões APOGEU", layout="wide")
st.markdown(
"""
<style>
.block-container { padding-top: 1.15rem; padding-bottom: 2.2rem; max-width: 1120px; }
.ap-card {
border: 1px solid rgba(0,0,0,0.08);
border-radius: 18px;
padding: 16px 16px 12px 16px;
background: rgba(255,255,255,0.90);
box-shadow: 0 14px 34px rgba(0,0,0,0.06);
}
.ap-title { font-size: 1.05rem; font-weight: 820; margin: 0 0 8px 0; letter-spacing: 0.1px; }
.ap-badge {
display: inline-block; padding: 6px 12px; border-radius: 999px;
font-size: 0.85rem; border: 1px solid rgba(0,0,0,0.10);
background: rgba(0,0,0,0.03);
}
.ap-resumo {
border-radius: 18px; border: 1px solid rgba(0,0,0,0.08);
background: linear-gradient(180deg, rgba(0,0,0,0.02), rgba(0,0,0,0.01));
padding: 16px;
}
.ap-hero {
border-radius: 22px;
padding: 16px;
border: 1px solid rgba(0,0,0,0.08);
background: linear-gradient(180deg, rgba(0,0,0,0.02), rgba(0,0,0,0.00));
}
.ap-code {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-weight: 900;
padding: 8px 12px;
border-radius: 12px;
border: 1px solid rgba(0,0,0,0.10);
background: rgba(0,0,0,0.03);
display: inline-block;
}
.stButton > button { border-radius: 14px !important; padding: 0.86rem 1rem !important; font-weight: 820 !important; }
</style>
""",
unsafe_allow_html=True,
)
# =========================
# Estado
# =========================
st.session_state.setdefault("step", 1)
st.session_state.setdefault("perfil", "")
st.session_state.setdefault("solicitante_tipo", SOLICITANTES[0])
st.session_state.setdefault("unidade", UNIDADES[0])
st.session_state.setdefault("tipo_categoria", TIPOS[0])
# seleção dinâmica impressão
st.session_state.setdefault("tipo_papel", "")
st.session_state.setdefault("formato", "")
st.session_state.setdefault("cor", "")
st.session_state.setdefault("verso_ui", "")
# volumes
st.session_state.setdefault("paginas_por_copia", 1)
st.session_state.setdefault("tiragem", 1)
# acabamentos
st.session_state.setdefault("acabamentos_sel", [])
# resultados
st.session_state.setdefault("sim_imp", {})
st.session_state.setdefault("sim_acab", [])
st.session_state.setdefault("tot_impressao", 0.0)
st.session_state.setdefault("tot_acabamentos", 0.0)
st.session_state.setdefault("tot_geral", 0.0)
# solicitante (inputs)
st.session_state.setdefault("solicitante_nome", "")
st.session_state.setdefault("solicitante_email", "")
st.session_state.setdefault("observacoes", "")
# solicitante (salvos para etapa 3)
st.session_state.setdefault("solicitante_nome_saved", "")
st.session_state.setdefault("solicitante_email_saved", "")
st.session_state.setdefault("observacoes_saved", "")
# retorno envio
st.session_state.setdefault("last_send_debug", None)
st.session_state.setdefault("codigo_pedido", "")
st.session_state.setdefault("envio_ok", False)
# bloqueio real anti-reenvio + anti-duplo clique
st.session_state.setdefault("sending", False) # envio em andamento
st.session_state.setdefault("sent_lock", False) # pedido já registrado
st.session_state.setdefault("sent_codigo", "")
# =========================
# Topo
# =========================
render_header_img(HEADER_IMG)
st.markdown("## CIA – Central de Impressões APOGEU")
st.caption("Simule custos, valide e envie solicitações com anexos e confirmação por e-mail.")
df_imp, df_acab = load_excel_tables(EXCEL_BASE)
# Stepper
progress = {1: 0.33, 2: 0.66, 3: 1.0}.get(int(st.session_state.step), 0.33)
st.progress(progress)
st.markdown(f'<span class="ap-badge">Etapa {st.session_state.step} de 3</span>', unsafe_allow_html=True)
# Bloco inicial
st.markdown("---")
c0, c1, c2, c3 = st.columns([1.25, 1, 1, 1])
with c0:
st.markdown('<div class="ap-card">', unsafe_allow_html=True)
st.markdown('<div class="ap-title">Tipo</div>', unsafe_allow_html=True)
st.selectbox("Selecione o tipo", TIPOS, key="tipo_categoria")
st.markdown("</div>", unsafe_allow_html=True)
with c1:
st.markdown('<div class="ap-card">', unsafe_allow_html=True)
st.markdown('<div class="ap-title">Acesso</div>', unsafe_allow_html=True)
st.selectbox(
"Quem é você?",
["", "Coordenação", "Secretaria", "Professores", "Administrativo", "Direção"],
key="perfil",
)
st.markdown("</div>", unsafe_allow_html=True)
with c2:
st.markdown('<div class="ap-card">', unsafe_allow_html=True)
st.markdown('<div class="ap-title">Solicitante</div>', unsafe_allow_html=True)
st.selectbox("Selecione o solicitante", SOLICITANTES, key="solicitante_tipo")
st.markdown("</div>", unsafe_allow_html=True)
with c3:
st.markdown('<div class="ap-card">', unsafe_allow_html=True)
st.markdown('<div class="ap-title">Unidade</div>', unsafe_allow_html=True)
st.selectbox("Selecione a unidade", UNIDADES, key="unidade")
st.markdown("</div>", unsafe_allow_html=True)
perfil_ok = bool(st.session_state.perfil.strip())
solicitante_tipo = st.session_state.solicitante_tipo
df_imp_f = filter_by_solicitante(df_imp, solicitante_tipo)
df_acab_f = filter_by_solicitante(df_acab, solicitante_tipo)
# callbacks instantâneos
def on_change_tipo_papel():
st.session_state.formato = ""
st.session_state.cor = ""
st.session_state.verso_ui = ""
def on_change_formato():
st.session_state.cor = ""
st.session_state.verso_ui = ""
def on_change_cor():
st.session_state.verso_ui = ""
# =========================
# ETAPA 1 — Simulação
# =========================
if st.session_state.step == 1:
st.markdown("---")
st.markdown("### 1) Simulação")
if not perfil_ok:
st.info("Selecione **Quem é você?** para liberar a simulação.")
st.stop()
if df_imp_f.empty:
st.error("Não há opções de impressão disponíveis para este solicitante.")
st.stop()
tipo_papel_opts = sorted(df_imp_f["tipo_papel"].dropna().astype(str).unique().tolist())
if st.session_state.tipo_papel not in tipo_papel_opts:
st.session_state.tipo_papel = tipo_papel_opts[0] if tipo_papel_opts else ""
base_papel = df_imp_f[df_imp_f["tipo_papel"].astype(str) == str(st.session_state.tipo_papel)]
formato_opts = sorted(base_papel["formato"].dropna().astype(str).unique().tolist())
if st.session_state.formato not in formato_opts:
st.session_state.formato = formato_opts[0] if formato_opts else ""
base_formato = base_papel[base_papel["formato"].astype(str) == str(st.session_state.formato)]
cor_opts = sorted(base_formato["cor"].dropna().astype(str).unique().tolist())
if st.session_state.cor not in cor_opts:
st.session_state.cor = cor_opts[0] if cor_opts else ""
base_cor = base_formato[base_formato["cor"].astype(str) == str(st.session_state.cor)]
verso_raw_opts: List[str] = []
for v in base_cor["verso"].dropna().tolist():
verso_raw_opts.extend(split_multi(v))
verso_raw_opts = sorted(list(dict.fromkeys(verso_raw_opts))) or ["1x0"]
verso_map = {verso_label(v): v for v in verso_raw_opts}
verso_ui_opts = list(verso_map.keys())
if st.session_state.verso_ui not in verso_ui_opts:
st.session_state.verso_ui = verso_ui_opts[0] if verso_ui_opts else ""
st.markdown('<div class="ap-hero">', unsafe_allow_html=True)
topA, topB = st.columns([2, 1])
with topA:
st.markdown('<div class="ap-card">', unsafe_allow_html=True)
st.markdown('<div class="ap-title">Impressão</div>', unsafe_allow_html=True)
s1, s2, s3 = st.columns(3)
with s1:
st.selectbox("Tipo / Papel", tipo_papel_opts, key="tipo_papel", on_change=on_change_tipo_papel)
st.selectbox("Formato", formato_opts, key="formato", on_change=on_change_formato)
with s2:
st.selectbox("Cor", cor_opts, key="cor", on_change=on_change_cor)
st.selectbox("Verso", verso_ui_opts, key="verso_ui")
with s3:
st.number_input("Páginas (por cópia/conjunto)", min_value=1, step=1, key="paginas_por_copia")
st.number_input("Tiragem (quantidade de cópias)", min_value=1, step=1, key="tiragem")
st.caption("Regra oficial: **1x1 = 1x0 × 2** (base por página).")
st.markdown("</div>", unsafe_allow_html=True)
with topB:
st.markdown('<div class="ap-card">', unsafe_allow_html=True)
st.markdown('<div class="ap-title">Acabamentos (opcional)</div>', unsafe_allow_html=True)
if df_acab_f.empty:
st.info("Sem acabamentos disponíveis para este solicitante.")
st.session_state.acabamentos_sel = []
else:
acabamento_opts = sorted(df_acab_f["acabamento"].dropna().astype(str).unique().tolist())
st.multiselect(
"Selecione os acabamentos",
options=acabamento_opts,
default=st.session_state.acabamentos_sel,
key="acabamentos_sel",
help="Quantidade de cada acabamento = tiragem.",
)
st.caption("Cálculo: preço unitário × tiragem (para cada item).")
st.markdown("</div>", unsafe_allow_html=True)
st.markdown("</div>", unsafe_allow_html=True)
st.markdown("---")
if st.button("Calcular", type="primary", use_container_width=True):
verso_code = verso_map.get(st.session_state.verso_ui, st.session_state.verso_ui)
pick = df_imp_f[
(df_imp_f["tipo_papel"].astype(str) == str(st.session_state.tipo_papel))
& (df_imp_f["formato"].astype(str) == str(st.session_state.formato))
& (df_imp_f["cor"].astype(str) == str(st.session_state.cor))
].copy()
def verso_contains(cell: Any) -> bool:
return verso_code in split_multi(cell)
pick = pick[pick["verso"].apply(verso_contains)]
if pick.empty:
st.error("Não encontrei um preço no Excel para a combinação selecionada.")
else:
unit_base = float(pick.iloc[0]["preco_unit"])
paginas_totais = int(st.session_state.paginas_por_copia) * int(st.session_state.tiragem)
multiplicador = 2.0 if (verso_code or "").strip().lower() == "1x1" else 1.0
unit_aplicado = unit_base * multiplicador
tot_imp = paginas_totais * unit_aplicado
st.session_state.sim_imp = {
"tipo_papel": str(st.session_state.tipo_papel),
"formato": str(st.session_state.formato),
"cor": str(st.session_state.cor),
"verso": str(verso_code),
"preco_unit_base": float(unit_base),
"multiplicador_verso": float(multiplicador),
"preco_unit_aplicado": float(unit_aplicado),
"paginas_por_copia": int(st.session_state.paginas_por_copia),
"tiragem": int(st.session_state.tiragem),
"paginas_totais": int(paginas_totais),
"base_cobranca": "pagina",
"total_impressao": float(tot_imp),
}
itens_acab: List[Dict[str, Any]] = []
tot_acab = 0.0
if not df_acab_f.empty and st.session_state.acabamentos_sel:
for nome_acab in st.session_state.acabamentos_sel:
pick_a = df_acab_f[df_acab_f["acabamento"].astype(str) == str(nome_acab)]
if pick_a.empty:
continue
unit_a = float(pick_a.iloc[0]["preco_unit"])
subtotal = unit_a * int(st.session_state.tiragem)
itens_acab.append(
{
"acabamento": str(nome_acab),
"preco_unit": float(unit_a),
"quantidade": int(st.session_state.tiragem),
"subtotal": float(subtotal),
}
)
tot_acab += subtotal
st.session_state.sim_acab = itens_acab
st.session_state.tot_impressao = float(tot_imp)
st.session_state.tot_acabamentos = float(tot_acab)
st.session_state.tot_geral = float(tot_imp + tot_acab)
# nova simulação = novo pedido (reseta envio/travas)
st.session_state.envio_ok = False
st.session_state.codigo_pedido = ""
st.session_state.last_send_debug = None
st.session_state.sending = False
st.session_state.sent_lock = False
st.session_state.sent_codigo = ""
if st.session_state.sim_imp:
st.markdown("---")
c1, c2, c3 = st.columns(3)
c1.metric("Total Impressão", money_br(st.session_state.tot_impressao))
c2.metric("Total Acabamentos", money_br(st.session_state.tot_acabamentos))
c3.metric("Total Geral", money_br(st.session_state.tot_geral))
st.markdown('<div class="ap-resumo">', unsafe_allow_html=True)
st.markdown("#### Resumo do pedido")
st.write(build_resumo_pedido(st.session_state.sim_imp, st.session_state.sim_acab, st.session_state.tipo_categoria))
st.markdown("</div>", unsafe_allow_html=True)
st.markdown("---")
if st.button("Continuar", type="primary", use_container_width=True):
st.session_state.step = 2
st.rerun()
# =========================
# ETAPA 2 — Dados do solicitante
# =========================
if st.session_state.step == 2:
st.markdown("---")
st.markdown("### 2) Dados do solicitante")
if not st.session_state.sim_imp:
st.warning("Você precisa concluir a simulação primeiro.")
if st.button("Voltar para Simulação"):
st.session_state.step = 1
st.rerun()
st.stop()
left, right = st.columns([2, 1])
with left:
st.markdown('<div class="ap-card">', unsafe_allow_html=True)
st.markdown('<div class="ap-title">Contato</div>', unsafe_allow_html=True)
st.text_input("Nome completo", key="solicitante_nome")
st.text_input("E-mail", key="solicitante_email")
st.text_area("Observações (opcional)", key="observacoes", height=110)
st.markdown("</div>", unsafe_allow_html=True)
with right:
st.markdown('<div class="ap-card">', unsafe_allow_html=True)
st.markdown('<div class="ap-title">Resumo</div>', unsafe_allow_html=True)
st.write(build_resumo_pedido(st.session_state.sim_imp, st.session_state.sim_acab, st.session_state.tipo_categoria))
st.markdown("</div>", unsafe_allow_html=True)
st.markdown("---")
b1, b2 = st.columns(2)
with b1:
if st.button("Voltar", use_container_width=True):
st.session_state.step = 1
st.rerun()
with b2:
can_next = bool(st.session_state.solicitante_nome.strip()) and ("@" in st.session_state.solicitante_email.strip())
if st.button("Ir para envio", type="primary", use_container_width=True, disabled=not can_next):
st.session_state.solicitante_nome_saved = st.session_state.solicitante_nome.strip()
st.session_state.solicitante_email_saved = st.session_state.solicitante_email.strip()
st.session_state.observacoes_saved = st.session_state.observacoes.strip()
st.session_state.step = 3
st.rerun()
# =========================
# ETAPA 3 — Envio
# =========================
if st.session_state.step == 3:
st.markdown("---")
st.markdown("### 3) Enviar solicitação")
webhook_url = get_webhook_url()
if not webhook_url:
st.warning(
"Segredo **WEBHOOK_URL** não configurado.\n\n"
"No Hugging Face: Settings → Secrets → New secret\n"
"- Name: WEBHOOK_URL\n"
"- Value: URL /exec do Apps Script\n"
)
# fallback se chegou aqui sem congelar
if not st.session_state.solicitante_nome_saved.strip() and st.session_state.solicitante_nome.strip():
st.session_state.solicitante_nome_saved = st.session_state.solicitante_nome.strip()
if not st.session_state.solicitante_email_saved.strip() and st.session_state.solicitante_email.strip():
st.session_state.solicitante_email_saved = st.session_state.solicitante_email.strip()
if not st.session_state.observacoes_saved.strip() and st.session_state.observacoes.strip():
st.session_state.observacoes_saved = st.session_state.observacoes.strip()
left, right = st.columns([2, 1])
with left:
st.markdown('<div class="ap-card">', unsafe_allow_html=True)
st.markdown('<div class="ap-title">Anexos (obrigatório)</div>', unsafe_allow_html=True)
uploads = st.file_uploader(
"Envie um ou mais arquivos do pedido",
type=None,
accept_multiple_files=True,
help="Obrigatório enviar pelo menos 1 arquivo.",
)
if uploads and len(uploads) > 0:
st.success(f"{len(uploads)} arquivo(s) pronto(s) para envio.")
st.write("**Arquivos selecionados:**")
for u in uploads:
st.write(f"• {u.name}")
else:
st.info("Anexe ao menos 1 arquivo para habilitar o envio.")
st.markdown("</div>", unsafe_allow_html=True)
st.markdown('<div class="ap-card" style="margin-top:14px;">', unsafe_allow_html=True)
st.markdown('<div class="ap-title">Revisão</div>', unsafe_allow_html=True)
st.metric("Total Geral", money_br(st.session_state.tot_geral))
st.write(build_resumo_pedido(st.session_state.sim_imp, st.session_state.sim_acab, st.session_state.tipo_categoria))
st.markdown("</div>", unsafe_allow_html=True)
with right:
st.markdown('<div class="ap-card">', unsafe_allow_html=True)
st.markdown('<div class="ap-title">Identificação</div>', unsafe_allow_html=True)
st.write(f"**Tipo:** {st.session_state.tipo_categoria}")
st.write(f"**Perfil:** {st.session_state.perfil}")
st.write(f"**Solicitante:** {st.session_state.solicitante_tipo}")
st.write(f"**Unidade:** {st.session_state.unidade}")
st.write(f"**Nome:** {st.session_state.solicitante_nome_saved}")
st.write(f"**E-mail:** {st.session_state.solicitante_email_saved}")
st.markdown("</div>", unsafe_allow_html=True)
st.markdown("---")
# Botões de navegação (voltar desabilitado após sucesso)
if st.button("Voltar", use_container_width=True, disabled=st.session_state.sent_lock or st.session_state.sending):
st.session_state.step = 2
st.rerun()
if st.session_state.sent_lock:
st.info(
f"Pedido já registrado: **{st.session_state.sent_codigo}**. "
"Para enviar outro pedido, clique em **Nova solicitação**."
)
# Pode enviar?
can_send = (
bool(webhook_url)
and bool(uploads and len(uploads) > 0)
and bool(st.session_state.sim_imp)
and bool(st.session_state.solicitante_nome_saved.strip())
and ("@" in st.session_state.solicitante_email_saved.strip())
and (not st.session_state.sent_lock)
and (not st.session_state.sending)
)
send = st.button(
"Enviar agora",
type="primary",
use_container_width=True,
disabled=not can_send,
)
# TRAVA OTIMISTA imediata: mata o botão antes de qualquer POST/base64
if send:
st.session_state.sending = True
st.session_state.sent_lock = True
st.session_state.sent_codigo = "Processando…"
st.rerun()
# EXECUÇÃO DO ENVIO (após rerun, com sending=True)
if st.session_state.sending:
# Revalida pré-condições (segurança)
pre_ok = (
bool(webhook_url)
and bool(uploads and len(uploads) > 0)
and bool(st.session_state.sim_imp)
and bool(st.session_state.solicitante_nome_saved.strip())
and ("@" in st.session_state.solicitante_email_saved.strip())
)
if not pre_ok:
st.session_state.sending = False
st.session_state.sent_lock = False
st.session_state.sent_codigo = ""
st.warning("Faltam dados para envio. Verifique anexos e contato.")
st.stop()
with st.spinner("Enviando… registrando na planilha e salvando anexos…"):
arquivos_payload = uploads_to_payload(uploads)
payload = {
"timestamp": datetime.now().isoformat(timespec="seconds"),
"perfil": st.session_state.perfil,
# compatibilidade (novo + antigo)
"tipo": st.session_state.tipo_categoria,
"tipo_pedido": st.session_state.tipo_categoria,
"solicitante_tipo": st.session_state.solicitante_tipo,
"unidade": st.session_state.unidade,
"solicitante_nome": st.session_state.solicitante_nome_saved.strip(),
"solicitante_email": st.session_state.solicitante_email_saved.strip(),
"observacoes": st.session_state.observacoes_saved.strip(),
"pedido": {
"impressao": st.session_state.sim_imp,
"acabamentos": st.session_state.sim_acab,
"totais": {
"impressao": float(st.session_state.tot_impressao),
"acabamentos": float(st.session_state.tot_acabamentos),
"geral": float(st.session_state.tot_geral),
},
},
"arquivos": arquivos_payload,
}
ok, msg, code, raw, parsed = post_to_webhook(payload, webhook_url)
st.session_state.last_send_debug = {"ok": ok, "msg": msg, "http_code": code, "raw": raw, "parsed": parsed}
if ok:
st.session_state.envio_ok = True
st.session_state.codigo_pedido = str(parsed.get("codigo_pedido", "") or "").strip()
st.session_state.sent_codigo = st.session_state.codigo_pedido or "Pedido confirmado"
# mantém sent_lock = True
else:
st.session_state.envio_ok = False
st.session_state.codigo_pedido = ""
st.session_state.sent_lock = False
st.session_state.sent_codigo = ""
st.session_state.sending = False
st.rerun()
# Resultado pós-envio
if st.session_state.envio_ok:
st.markdown("---")
st.success("Solicitação enviada e confirmada.")
if st.session_state.codigo_pedido:
st.markdown(
f'**Código do pedido:** <span class="ap-code">{st.session_state.codigo_pedido}</span>',
unsafe_allow_html=True,
)
# Segurança: não exibir links. Se o GAS devolver files, só mostra nomes (opcional).
parsed = (st.session_state.last_send_debug or {}).get("parsed")
if isinstance(parsed, dict) and parsed.get("files"):
with st.expander("Arquivos registrados", expanded=False):
for f in parsed["files"]:
st.write(f"• {f.get('name','arquivo')}")
st.markdown("---")
col_a, col_b = st.columns(2)
with col_a:
if st.button("Nova solicitação", use_container_width=True):
st.session_state.step = 1
# reseta simulação (mantém dados de acesso pra UX premium)
st.session_state.sim_imp = {}
st.session_state.sim_acab = []
st.session_state.tot_impressao = 0.0
st.session_state.tot_acabamentos = 0.0
st.session_state.tot_geral = 0.0
st.session_state.paginas_por_copia = 1
st.session_state.tiragem = 1
st.session_state.acabamentos_sel = []
st.session_state.envio_ok = False
st.session_state.codigo_pedido = ""
st.session_state.last_send_debug = None
st.session_state.sending = False
st.session_state.sent_lock = False
st.session_state.sent_codigo = ""
st.rerun()
with col_b:
if st.button("Limpar contato", use_container_width=True):
st.session_state.solicitante_nome = ""
st.session_state.solicitante_email = ""
st.session_state.observacoes = ""
st.session_state.solicitante_nome_saved = ""
st.session_state.solicitante_email_saved = ""
st.session_state.observacoes_saved = ""
st.rerun()
elif st.session_state.last_send_debug and not st.session_state.envio_ok:
st.error("Não foi possível confirmar o envio com o servidor.")
with st.expander("Retorno técnico", expanded=True):
st.write(st.session_state.last_send_debug)