/* ============================================================
KeS Metas — E-MAILS
------------------------------------------------------------
• SEQUÊNCIA POR ETAPA (drip): para cada etapa do funil, uma
régua de e-mails com tempo próprio (ex.: na hora, +2 dias, +5 dias).
Quando o lead ENTRA na etapa, os e-mails são agendados na fila.
• ENVIO ESPECÍFICO: um e-mail avulso para um lead (modal).
• A fila (outbox) é lida pelo n8n, que envia pelo Brevo
(remetente comercial@kesassessoria.com.br) e marca "enviado".
O app NUNCA guarda senha/API — igual ao WhatsApp.
============================================================ */
/* ---- Variáveis do texto: {nome} {empresa} {responsavel} ---- */
function aplicarVars(txt, lead, users) {
const resp = lead && lead.responsavelId ? (users || []).find((u) => u.id === lead.responsavelId) : null;
return String(txt || "")
.replace(/\{nome\}/gi, (lead && lead.nome) || "")
.replace(/\{empresa\}/gi, (lead && lead.empresa) || "")
.replace(/\{responsavel\}/gi, (resp && resp.nome) || "KeS Assessoria");
}
/* ---- Texto simples -> HTML (escapa e mantém parágrafos/quebras) ---- */
function corpoParaHtml(txt) {
const esc = String(txt || "")
.replace(/&/g, "&").replace(//g, ">");
return esc.split(/\n{2,}/).map((par) =>
'
' + par.replace(/\n/g, " ") + "
").join("");
}
/* ---- TEMPLATES: monta o HTML "de site" a partir das opções escolhidas ----
O lugar onde a mensagem digitada entra é o marcador {mensagem}. */
function montarTemplateHtml(p) {
p = p || {};
const cor = p.cor || "#132a4a";
const logo = p.logoUrl ? ' ' : "";
const cab = p.cabecalho || "KeS Assessoria";
const titulo = p.titulo ? '' + p.titulo + " " : "";
const botao = (p.botaoTexto && p.botaoLink)
? '"
: "";
const rodape = p.rodape || "KeS Assessoria · comercial@kesassessoria.com.br";
const base = p.base || "classico";
const abre = '' +
'
';
const fecha = '
' + rodape + "
";
const corpoMeio = '' + titulo + "{mensagem}" + botao + "
";
if (base === "moderno") {
return abre +
'' +
(logo ? logo + "
" : "") +
'
' + cab + "
" +
corpoMeio + fecha;
}
if (base === "minimal") {
return abre.replace("border:1px solid #e3e7ee", "border:1px solid #e3e7ee;border-top:4px solid " + cor) +
'' +
(logo ? logo + " " : "") + cab + "
" + corpoMeio + fecha;
}
// clássico: faixa de cor no topo com logo + nome
return abre +
'' +
(logo ? logo + " " : "") + cab + "
" + corpoMeio + fecha;
}
/* ---- Junta template + mensagem digitada + variáveis do lead ---- */
function renderEmailHtml(template, corpoTexto, lead, users) {
if (!template || !template.html) return "";
const html = String(template.html).replace(/\{mensagem\}/gi, corpoParaHtml(corpoTexto));
return aplicarVars(html, lead, users);
}
/* ---- Monta os e-mails de uma sequência para 1 lead (sem enviar) ---- */
function montarSequencia(seq, lead, users, stageId, deId, templates) {
if (!seq || !seq.ativo || !Array.isArray(seq.emails) || !lead || !lead.email) return [];
const agora = Date.now();
return seq.emails.filter((e) => (e.assunto || "").trim() && (e.corpo || "").trim()).map((e, i) => {
const tpl = e.templateId ? (templates || []).find((t) => t.id === e.templateId) : null;
const m = {
para: String(lead.email).trim().toLowerCase(),
paraNome: lead.nome || "",
leadId: lead.id,
assunto: aplicarVars(e.assunto, lead, users),
corpo: aplicarVars(e.corpo, lead, users),
ts: agora,
agendadoPara: agora + (parseInt(e.dias, 10) || 0) * 86400000,
status: "pendente",
origem: "sequencia",
seqStage: stageId,
seqIndex: i,
de: deId || null,
};
if (tpl) { m.templateId = tpl.id; m.html = renderEmailHtml(tpl, e.corpo, lead, users); }
if (e.cc && String(e.cc).trim()) m.cc = String(e.cc).trim();
if (Array.isArray(e.anexos) && e.anexos.length) m.anexos = e.anexos.map((a) => ({ nome: a.nome, url: a.url }));
return m;
});
}
window.KESMAIL = { aplicarVars, montarSequencia, montarTemplateHtml, renderEmailHtml, corpoParaHtml };
/* ============================ TELA ============================ */
function EmailScreen({ state, dispatch, viewer }) {
const { useState, useEffect } = React;
const DB = window.KESDB;
const tenantId = tenantIdAtual(state);
const [aba, setAba] = useState("seq"); // seq | fila
const [fila, setFila] = useState([]);
const [erroBanco, setErroBanco] = useState(false);
useEffect(() => {
setFila([]); setErroBanco(false);
if (!DB || !DB.mailListen || !tenantId) return;
return DB.mailListen(tenantId, {}, (lista, err) => {
if (err) { setErroBanco(true); return; }
setFila(lista || []);
});
}, [tenantId]);
const seqs = (state.config && state.config.emailSeqs) || {};
const salvarSeqs = (novo) => dispatch({ type: "SET_CONFIG", patch: { emailSeqs: novo } });
const etapasAtivas = Object.keys(seqs).filter((k) => seqs[k] && seqs[k].ativo).length;
const pend = fila.filter((m) => m.status === "pendente");
const env = fila.filter((m) => m.status === "enviado");
const err = fila.filter((m) => m.status === "erro");
return (
E-mails
{etapasAtivas > 0 && {etapasAtivas} sequência(s) ativa(s) }
Régua de e-mails por etapa do funil + envios avulsos. Remetente: comercial@kesassessoria.com.br
setAba("seq")}>Sequências
setAba("modelos")}>Modelos
setAba("fila")}>Fila {pend.length ? "(" + pend.length + ")" : ""}
{erroBanco && (
O banco recusou a leitura da fila de e-mails — falta publicar as regras.
)}
{aba === "seq" ? (
) : aba === "modelos" ? (
) : (
)}
);
}
/* -------- Sequências por etapa (a régua) -------- */
function SequenciasPorEtapa({ state, seqs, onSalvar, dispatch }) {
const { useState } = React;
const stages = state.stages || [];
const templates = (state.config && state.config.emailTemplates) || [];
const tenantId = tenantIdAtual(state);
const [aberta, setAberta] = useState(stages[0] ? stages[0].id : null);
const seq = seqs[aberta] || { ativo: false, emails: [] };
const setSeq = (novo) => onSalvar(Object.assign({}, seqs, { [aberta]: novo }));
const setEmail = (i, campo, val) => {
const emails = (seq.emails || []).slice();
emails[i] = Object.assign({}, emails[i], { [campo]: val });
setSeq(Object.assign({}, seq, { emails }));
};
const addEmail = () => setSeq(Object.assign({}, seq, { emails: (seq.emails || []).concat([{ assunto: "", corpo: "", dias: (seq.emails || []).length === 0 ? 0 : 2 }]) }));
const delEmail = (i) => setSeq(Object.assign({}, seq, { emails: (seq.emails || []).filter((_, x) => x !== i) }));
return (
{/* etapas */}
Etapas do funil
{stages.map((s) => {
const q = (seqs[s.id] && seqs[s.id].emails || []).length;
const on = seqs[s.id] && seqs[s.id].ativo;
return (
setAberta(s.id)}>
{s.nome}
{q > 0 && {q} }
);
})}
{/* régua da etapa */}
{!aberta ?
Escolha uma etapa
: (
<>
{(seq.emails || []).length === 0 && (
Nenhum e-mail nesta etapa ainda.
)}
{(seq.emails || []).map((e, i) => (
))}
Adicionar e-mail à régua
>
)}
);
}
/* -------- MODELOS (templates HTML "de site") -------- */
function ModelosEmail({ state, dispatch }) {
const { useState } = React;
const templates = (state.config && state.config.emailTemplates) || [];
const [editando, setEditando] = useState(null); // objeto template ou "novo"
const [confirmaDel, setConfirmaDel] = useState(null);
const salvarTodos = (lista) => dispatch({ type: "SET_CONFIG", patch: { emailTemplates: lista } });
const salvarUm = (tpl) => {
const existe = templates.some((t) => t.id === tpl.id);
salvarTodos(existe ? templates.map((t) => (t.id === tpl.id ? tpl : t)) : templates.concat([tpl]));
setEditando(null);
dispatch({ type: "TOAST", msg: "Modelo “" + tpl.nome + "” salvo." });
};
const duplicar = (tpl) => {
const copia = Object.assign({}, tpl, { id: "tpl-" + Date.now(), nome: tpl.nome + " (cópia)" });
salvarTodos(templates.concat([copia]));
};
const excluir = (tpl) => { salvarTodos(templates.filter((t) => t.id !== tpl.id)); setConfirmaDel(null); };
// Amostra do modelo com uma mensagem de exemplo (miniatura)
const amostra = (tpl) => String(tpl.html || "").replace(/\{mensagem\}/gi,
"Olá {nome}, esta é uma amostra do modelo.
");
return (
<>
Modelos deixam o e-mail com cara de site: cabeçalho, cores, botão e rodapé. A mensagem digitada entra no lugar do marcador {"{mensagem}"} .
setEditando("novo")}>Novo modelo
{templates.length === 0 && (
Nenhum modelo ainda
Crie um modelo e use no envio avulso ou nas sequências.
)}
{templates.map((tpl) => (
{tpl.nome}
setEditando(tpl)}>
duplicar(tpl)}>
setConfirmaDel(tpl)}>
))}
{editando && setEditando(null)} />}
{confirmaDel && (
setConfirmaDel(null)}
footer={<> setConfirmaDel(null)}>Cancelar
excluir(confirmaDel)}>Excluir >}>
O modelo {confirmaDel.nome} será removido. Sequências que o usam voltam a mandar texto simples.
)}
>
);
}
/* -------- Editor de modelo: opções visuais + HTML livre + prévia ao vivo -------- */
function TemplateEmailModal({ template, onSalvar, onClose }) {
const { useState, useMemo } = React;
const KM = window.KESMAIL;
const [nome, setNome] = useState(template ? template.nome : "");
const [modo, setModo] = useState(template && template.modo === "html" ? "html" : "visual");
const [props, setProps] = useState((template && template.props) || {
base: "classico", cor: "#132a4a", cabecalho: "KeS Assessoria", logoUrl: "",
titulo: "", botaoTexto: "", botaoLink: "", rodape: "KeS Assessoria · comercial@kesassessoria.com.br",
});
const [htmlLivre, setHtmlLivre] = useState(template && template.modo === "html" ? template.html
: KM.montarTemplateHtml((template && template.props) || { base: "classico", cor: "#132a4a", cabecalho: "KeS Assessoria" }));
const [err, setErr] = useState("");
const setP = (k, v) => setProps((s) => Object.assign({}, s, { [k]: v }));
const htmlAtual = modo === "visual" ? KM.montarTemplateHtml(props) : htmlLivre;
const previa = useMemo(() => String(htmlAtual || "").replace(/\{mensagem\}/gi,
"Olá {nome}, esta é uma mensagem de exemplo para você ver o modelo.
O texto digitado na hora do envio aparece aqui.
"), [htmlAtual]);
function salvar() {
if (!nome.trim()) return setErr("Dê um nome ao modelo.");
if (modo === "html" && htmlLivre.indexOf("{mensagem}") < 0) return setErr("O HTML precisa ter o marcador {mensagem} — é onde a mensagem digitada entra.");
onSalvar({
id: template ? template.id : "tpl-" + Date.now(),
nome: nome.trim(), modo, props: modo === "visual" ? props : undefined,
html: htmlAtual,
});
}
const BASES = [["classico", "Clássico"], ["moderno", "Moderno"], ["minimal", "Minimalista"]];
return (
Cancelar
Salvar modelo >}>
setNome(e.target.value)} placeholder="Ex.: Boas-vindas" />
setModo("visual")}>Visual
{ if (modo === "visual") setHtmlLivre(KM.montarTemplateHtml(props)); setModo("html"); }}>HTML livre
{modo === "visual" ? (
<>
{BASES.map(([id, rot]) => (
setP("base", id)}>{rot}
))}
setP("cor", e.target.value)} style={{ width: 42, height: 32, border: "none", background: "none", cursor: "pointer" }} />
setP("cor", e.target.value)} style={{ width: 110 }} />
setP("cabecalho", e.target.value)} />
setP("logoUrl", e.target.value)} placeholder="https://…/logo.png" />
setP("titulo", e.target.value)} placeholder="Ex.: Bem-vindo, {nome}!" />
setP("botaoTexto", e.target.value)} placeholder="Ex.: Agendar reunião" />
setP("botaoLink", e.target.value)} placeholder="https://…" />
setP("rodape", e.target.value)} />
>
) : (
)}
{err &&
{err}
}
Prévia (como o cliente vê)
);
}
/* -------- Campo de ANEXOS (upload pro Storage, chips com remover) -------- */
function AnexosCampo({ anexos, onChange, teamId }) {
const { useState, useRef } = React;
const DB = window.KESDB;
const [subindo, setSubindo] = useState(false);
const [erro, setErro] = useState("");
const inputRef = useRef(null);
const lista = anexos || [];
const fmtTam = (b) => (b > 1048576 ? (b / 1048576).toFixed(1) + " MB" : Math.max(1, Math.round(b / 1024)) + " KB");
async function escolher(ev) {
const arqs = Array.from(ev.target.files || []);
ev.target.value = "";
if (!arqs.length) return;
setErro("");
for (const arq of arqs) {
if (arq.size > 15 * 1024 * 1024) { setErro("“" + arq.name + "” passa de 15MB."); continue; }
setSubindo(true);
const r = await DB.mailUpload(teamId, arq, arq.name);
setSubindo(false);
if (!r || !r.ok) { setErro("Não consegui subir “" + arq.name + "” (" + ((r && r.error) || "erro") + ")."); continue; }
onChange(lista.concat([{ nome: arq.name, url: r.url, tam: arq.size }]));
}
}
return (
{lista.map((a, i) => (
{a.nome}{a.tam ? " · " + fmtTam(a.tam) : ""}
onChange(lista.filter((_, x) => x !== i))}>
))}
inputRef.current && inputRef.current.click()} disabled={subindo}>
{subindo ? "Subindo…" : "Anexar arquivo"}
{erro && {erro}
}
);
}
/* -------- Fila / enviados -------- */
function FilaEmails({ pend, env, err }) {
const K = window.KES;
const quando = (ts) => { const d = new Date(ts || 0); return d.toLocaleString("pt-BR", { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit" }); };
const Linha = ({ m, tag, tone }) => (
{tag}
{m.assunto}
{m.paraNome ? m.paraNome + " · " : ""}{m.para}
{m.cc ? " · CC: " + m.cc : ""}
{m.anexos && m.anexos.length ? " · 📎" + m.anexos.length : ""}
{m.templateId ? " · modelo" : ""}
{m.origem === "sequencia" ? " · régua" : m.origem === "campanha" ? " · campanha" : " · avulso"}
{m.status === "pendente" ? "sai em " + quando(m.agendadoPara) : quando(m.ts)}
);
return (
<>
{pend.length === 0 && env.length === 0 && err.length === 0 ? (
Nenhum e-mail ainda
Ative uma régua ou envie um e-mail pelo cartão do lead.
) : (<>
{err.map((m) =>
)}
{pend.map((m) =>
)}
{env.slice(0, 40).map((m) =>
)}
>)}
>
);
}
/* -------- Modal de envio avulso (usado no cartão do lead) -------- */
function EnviarEmailModal({ lead, state, viewer, dispatch, onClose }) {
const { useState } = React;
const DB = window.KESDB;
const KM = window.KESMAIL;
const tenantId = tenantIdAtual(state);
const templates = (state.config && state.config.emailTemplates) || [];
const [f, setF] = useState({ assunto: "", corpo: "", cc: "", templateId: "", anexos: [] });
const [previa, setPrevia] = useState(false);
const [busy, setBusy] = useState(false);
const [err, setErr] = useState("");
const set = (k, v) => setF((s) => ({ ...s, [k]: v }));
const tpl = f.templateId ? templates.find((t) => t.id === f.templateId) : null;
const ccInvalido = () => String(f.cc || "").split(/[,;]/).map((s) => s.trim()).filter(Boolean)
.find((s) => !/^\S+@\S+\.\S+$/.test(s));
async function enviar() {
if (!lead.email) return setErr("Este lead não tem e-mail cadastrado.");
if (!f.assunto.trim()) return setErr("Informe o assunto.");
if (!f.corpo.trim()) return setErr("Escreva a mensagem.");
const ccRuim = ccInvalido();
if (ccRuim) return setErr("E-mail inválido na cópia: " + ccRuim);
setBusy(true); setErr("");
const K = window.KES;
const para = String(lead.email).trim().toLowerCase();
const assunto = KM.aplicarVars(f.assunto, lead, state.users);
const corpo = KM.aplicarVars(f.corpo, lead, state.users);
const html = tpl ? KM.renderEmailHtml(tpl, f.corpo, lead, state.users) : "";
const cc = String(f.cc || "").trim();
const anexos = (f.anexos || []).map((a) => ({ nome: a.nome, url: a.url }));
const base = { para, paraNome: lead.nome || "", leadId: lead.id, assunto, corpo,
ts: Date.now(), agendadoPara: Date.now(), origem: "avulso", de: viewer.id };
if (html) { base.html = html; base.templateId = tpl.id; }
if (cc) base.cc = cc;
if (anexos.length) base.anexos = anexos;
// Envio individual: sai AGORA pelo Google (Apps Script da conta comercial@).
// As respostas do cliente voltam para quem enviou (replyTo).
if (K.EmailService && K.EmailService.configured) {
const env = await K.EmailService.enviar({ para, assunto, corpo, replyTo: viewer.email || "", cc, html, anexos });
setBusy(false);
if (!env.ok) {
await DB.mailQueue(tenantId, Object.assign({}, base, { status: "erro", erro: env.error || "" }));
return setErr("Não foi possível enviar: " + (env.error || "erro desconhecido"));
}
await DB.mailQueue(tenantId, Object.assign({}, base, { status: "enviado", enviadoEm: Date.now() }));
dispatch({ type: "TOAST", msg: "E-mail enviado para " + para + " ✅" });
return onClose();
}
// Motor ainda não configurado → só guarda na fila (não sai).
const r = await DB.mailQueue(tenantId, Object.assign({}, base, { status: "pendente" }));
setBusy(false);
if (!r || !r.ok) return setErr("Não foi possível colocar o e-mail na fila.");
dispatch({ type: "TOAST", msg: "E-mail guardado na fila (motor de e-mail ainda não configurado)." });
onClose();
}
return (
Cancelar
{busy ? "Enviando…" : "Enviar e-mail"} >}>
Para: {lead.email || "— sem e-mail cadastrado —"} {lead.empresa ? " · " + lead.empresa : ""}
set("cc", e.target.value)} placeholder="fulano@empresa.com, chefe@empresa.com" />
{ set("templateId", e.target.value); if (!e.target.value) setPrevia(false); }}>
Texto simples (sem modelo)
{templates.map((t) => {t.nome} )}
set("assunto", e.target.value)} placeholder="Ex.: {nome}, sobre nossa conversa" />
set("anexos", lista)} teamId={tenantId} />
{tpl && (
setPrevia(!previa)}>{previa ? "Fechar prévia" : "Ver prévia com o modelo"}
{previa && (
)}
)}
{err && {err}
}
);
}
window.EmailScreen = EmailScreen;
window.EnviarEmailModal = EnviarEmailModal;