/* ============================================================ 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}
); } /* ===== 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 (
Etapa 4 de 6

Termo de aceite

Documento gerado automaticamente a partir da proposta.

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 {items.map(it => ( ))} {recorrente > 0 && }
ServiçoPrazoValor
{it.nome}{it.obs ? — {it.obs} : ''} {it.prazo} {fmtBRL(it.preco)}
Recorrente mensal{fmtBRL(recorrente)}
Valor total{fmtBRL(total)}
Forma de pagamento
{metodo}
{obsGeral &&
Observações
{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.url}
)} {linkState.err &&
{linkState.err}
}
ou assine agora você mesmo
setAceite({ ...aceite, cpf: maskDoc(e.target.value) })} inputMode="numeric" />
Assinatura digital setAceite(a => ({ ...a, assinado: v }))} onCapture={(img) => setAceite(a => ({ ...a, assinaturaImg: img }))} />
); } /* ===== 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 (
Aceite confirmado

Pronto para compartilhar

Envie a confirmação da proposta para o cliente. O pagamento será solicitado na próxima etapa.

{hasSig && ( )}
Texto formatado
{texto}
{hasSig && (
Assinatura anexada
Assinatura do cliente
)}

{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 (
Etapa 6 de 6

Pagamento via PIX

Escaneie o QR Code ou use o PIX copia e cola para concluir.

Valor a pagar {fmtBRL(total)}
Recebedor{pixCfg.nome}
Chave PIX{pixCfg.chave}
Data{hoje}
PIX copia e cola
{payload}
{!clientMode && ( )} {!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 });