/* ============================================================
wizard_steps_b.jsx — Etapas 4, 5, 6
============================================================ */
/* ---------- Assinatura digital (canvas) ---------- */
function SignaturePad({ onChange, onCapture }) {
const ref = React.useRef(null);
const drawing = React.useRef(false);
const inked = React.useRef(false);
const last = React.useRef(null);
const [hasInk, setHasInk] = React.useState(false);
React.useEffect(() => {
const cv = ref.current;
const dpr = window.devicePixelRatio || 1;
const rect = cv.getBoundingClientRect();
cv.width = rect.width * dpr; cv.height = rect.height * dpr;
const ctx = cv.getContext('2d');
ctx.scale(dpr, dpr);
ctx.lineWidth = 2.4; ctx.lineCap = 'round'; ctx.lineJoin = 'round';
ctx.strokeStyle = '#0a0c10';
}, []);
// exporta a assinatura como PNG sobre fundo branco
const exportPng = () => {
if (!onCapture) return;
const cv = ref.current;
const out = document.createElement('canvas');
out.width = cv.width; out.height = cv.height;
const octx = out.getContext('2d');
octx.fillStyle = '#ffffff'; octx.fillRect(0, 0, out.width, out.height);
octx.drawImage(cv, 0, 0);
onCapture(out.toDataURL('image/png'));
};
const pos = (e) => {
const r = ref.current.getBoundingClientRect();
const t = e.touches ? e.touches[0] : e;
return { x: t.clientX - r.left, y: t.clientY - r.top };
};
const start = (e) => { e.preventDefault(); drawing.current = true; last.current = pos(e); };
const move = (e) => {
if (!drawing.current) return; e.preventDefault();
const ctx = ref.current.getContext('2d');
const p = pos(e);
ctx.beginPath(); ctx.moveTo(last.current.x, last.current.y); ctx.lineTo(p.x, p.y); ctx.stroke();
last.current = p;
if (!inked.current) { inked.current = true; setHasInk(true); onChange && onChange(true); }
};
const end = () => { if (drawing.current && inked.current) exportPng(); drawing.current = false; };
const clear = () => {
const cv = ref.current; const ctx = cv.getContext('2d');
ctx.clearRect(0, 0, cv.width, cv.height); inked.current = false; setHasInk(false);
onChange && onChange(false); onCapture && onCapture(null);
};
return (
{!hasInk && Assine aqui com o mouse ou o dedo }
Limpar
);
}
/* ===== ETAPA 4 — Termo de Aceite ===== */
function StepAceite({ cliente, items, total, recorrente, pagamento, obsGeral, aceite, setAceite, onClientLink }) {
const metodo = (PAYMENT_METHODS.find(m => m.id === pagamento) || {}).nome || '—';
const hoje = new Date().toLocaleDateString('pt-BR', { day: '2-digit', month: 'long', year: 'numeric' });
const upd = (k) => (e) => setAceite({ ...aceite, [k]: e.target.value });
const [linkState, setLinkState] = React.useState({ url: '', copied: false, busy: false, err: '' });
const gerarLink = async () => {
if (!onClientLink) return;
setLinkState(s => ({ ...s, busy: true, err: '' }));
try {
const url = await onClientLink();
if (url) setLinkState({ url, copied: false, busy: false, err: '' });
else setLinkState(s => ({ ...s, busy: false }));
} catch (e) {
setLinkState(s => ({ ...s, busy: false, err: (e && e.message) || 'Não foi possível gerar o link.' }));
}
};
const copiarLink = () => {
navigator.clipboard?.writeText(linkState.url);
setLinkState(s => ({ ...s, copied: true }));
setTimeout(() => setLinkState(s => ({ ...s, copied: false })), 1800);
};
const waLink = () => {
let d = onlyDigits(cliente.whatsapp); if (d && d.length <= 11) d = '55' + d;
const msg = `Olá${cliente.nome ? ' ' + cliente.nome.split(' ')[0] : ''}! Segue sua proposta da ${window.brandName()} para você revisar, assinar e efetuar o pagamento:\n\n${linkState.url}`;
window.open('https://wa.me/' + d + '?text=' + encodeURIComponent(msg), '_blank');
};
const mailLink = () => {
const body = `Olá ${cliente.nome || ''},\n\nSegue sua proposta da ${window.brandName()} para revisar, assinar e efetuar o pagamento:\n\n${linkState.url}\n\nQualquer dúvida, estamos à disposição.`;
window.open('mailto:' + (cliente.email || '') + '?subject=' + encodeURIComponent('Sua proposta — ' + window.brandName()) + '&body=' + encodeURIComponent(body), '_blank');
};
return (
Termo de Contratação
{hoje}
Contratante
{cliente.nome || '—'} {cliente.empresa ? ` · ${cliente.empresa}` : ''}
{cliente.doc || '—'}
{cliente.email || '—'}
{cliente.whatsapp || '—'}
{cliente.endereco &&
{cliente.endereco}
}
Serviços contratados
Serviço Prazo Valor
{items.map(it => (
{it.nome}{it.obs ? — {it.obs} : ''}
{it.prazo}
{fmtBRL(it.preco)}
))}
{recorrente > 0 && Recorrente mensal {fmtBRL(recorrente)} }
Valor total {fmtBRL(total)}
Forma de pagamento {metodo}
{obsGeral &&
}
Declaro estar de acordo com os serviços, valores e condições comerciais apresentados pela
{window.brandName()} , autorizando a continuidade do processo de contratação.
Enviar para o cliente assinar
Gere um link provisório para o cliente revisar, assinar e pagar pelo próprio celular.
{!linkState.url ? (
{linkState.busy ? 'Gerando…' : 'Gerar link do cliente'}
) : (
{linkState.url}
{linkState.copied ? 'Copiado' : 'Copiar'}
WhatsApp
E-mail
)}
{linkState.err &&
{linkState.err}
}
ou assine agora você mesmo
);
}
/* ===== ETAPA 5 — Compartilhamento ===== */
function StepCompartilhar({ cliente, items, total, pagamento, assinaturaImg }) {
const [copied, setCopied] = React.useState(false);
const metodo = (PAYMENT_METHODS.find(m => m.id === pagamento) || {}).nome || '—';
const linhas = items.map(it => `• ${it.nome} — ${fmtBRL(it.preco)} (${it.prazo})`).join('\n');
const texto =
`✅ Proposta ${window.brandName()}
Cliente: ${cliente.nome}${cliente.empresa ? ' · ' + cliente.empresa : ''}
Serviços:
${linhas}
Pagamento: ${metodo}
Total: ${fmtBRL(total)}
Aceite registrado. Em seguida enviaremos os dados para pagamento.`;
const copy = () => { navigator.clipboard?.writeText(texto); setCopied(true); setTimeout(() => setCopied(false), 1800); };
const waNum = (() => { let d = onlyDigits(cliente.whatsapp); if (d && d.length <= 11) d = '55' + d; return d; })();
const wpp = () => window.open('https://wa.me/' + waNum + '?text=' + encodeURIComponent(texto), '_blank');
const mail = () => window.open('mailto:' + (cliente.email || '') + '?subject=' + encodeURIComponent('Proposta — ' + window.brandName()) + '&body=' + encodeURIComponent(texto), '_blank');
// assinatura -> arquivo PNG
const fileName = 'assinatura-' + (cliente.nome || 'cliente').trim().replace(/\s+/g, '_').toLowerCase() + '.png';
const sigFile = () => {
if (!assinaturaImg) return null;
try {
const [head, b64] = assinaturaImg.split(',');
const mime = (head.match(/:(.*?);/) || [])[1] || 'image/png';
const bin = atob(b64); let n = bin.length; const u8 = new Uint8Array(n);
while (n--) u8[n] = bin.charCodeAt(n);
return new File([u8], fileName, { type: mime });
} catch (e) { return null; }
};
const downloadSig = () => {
if (!assinaturaImg) return;
const a = document.createElement('a'); a.href = assinaturaImg; a.download = fileName;
document.body.appendChild(a); a.click(); a.remove();
};
// compartilhamento nativo: anexa a imagem da assinatura (WhatsApp, e-mail, etc.)
const shareNative = async () => {
const file = sigFile();
if (file && navigator.canShare && navigator.canShare({ files: [file] })) {
try { await navigator.share({ files: [file], text: texto, title: 'Proposta — ' + window.brandName() }); return; }
catch (e) { if (e && e.name === 'AbortError') return; }
}
// desktop / sem suporte: baixa a assinatura e abre o WhatsApp com o texto
downloadSig();
window.open('https://wa.me/' + waNum + '?text=' + encodeURIComponent(texto), '_blank');
};
const hasSig = !!assinaturaImg;
return (
{hasSig && (
Enviar com assinatura
)}
WhatsApp{hasSig ? ' (só texto)' : ''}
E-mail{hasSig ? ' (só texto)' : ''}
{copied ? 'Copiado!' : 'Copiar texto'}
Texto formatado
{texto}
{hasSig && (
Assinatura anexada
Baixar PNG
)}
{hasSig
? ' No celular, “Enviar com assinatura” abre o WhatsApp/e-mail já com a imagem anexada. No computador, a assinatura é baixada para você anexar.'
: ' Geração de PDF e SMS ficam para a integração futura — por ora o envio é por WhatsApp ou e-mail.'}
);
}
/* ===== ETAPA 6 — Pagamento ===== */
function StepPagamento({ total, pixCfg, setPixCfg, clientMode }) {
const [copied, setCopied] = React.useState(false);
const [editCfg, setEditCfg] = React.useState(false);
const payload = window.buildPixPayload({ ...pixCfg, valor: total });
const hoje = new Date().toLocaleDateString('pt-BR');
const copy = () => { navigator.clipboard?.writeText(payload); setCopied(true); setTimeout(() => setCopied(false), 1800); };
return (
Valor a pagar
{fmtBRL(total)}
Recebedor {pixCfg.nome}
Chave PIX {pixCfg.chave}
Data {hoje}
PIX copia e cola
{payload}
{copied ? 'Código copiado' : 'Copiar código PIX'}
{!clientMode && (
setEditCfg(e => !e)}> Configurar dados da conta
)}
{!clientMode && editCfg && (
setPixCfg({ ...pixCfg, chave: e.target.value })} />
setPixCfg({ ...pixCfg, nome: e.target.value })} />
setPixCfg({ ...pixCfg, cidade: e.target.value })} />
)}
);
}
Object.assign(window, { StepAceite, StepCompartilhar, StepPagamento, SignaturePad });