/* ============================================================
client.jsx — Tela do CLIENTE (link provisório ?p=&t=&d=)
Termo → assinatura → pagamento PIX → conclusão.
Sem login, sem sidebar, sem config da conta.
============================================================ */
function ClientTerm({ deal }) {
const c = deal.cliente || {};
const metodo = (window.PAYMENT_METHODS.find(m => m.id === deal.pagamento) || {}).nome || '—';
const hoje = new Date().toLocaleDateString('pt-BR', { day: '2-digit', month: 'long', year: 'numeric' });
return (
Termo de Contratação
{hoje}
Contratante
{c.nome || '—'} {c.empresa ? ` · ${c.empresa}` : ''}
{c.doc || '—'}
{c.email || '—'}
{c.whatsapp || '—'}
{c.endereco &&
{c.endereco}
}
Serviços contratados
Serviço Prazo Valor
{(deal.items || []).map(it => (
{it.nome}{it.obs ? — {it.obs} : ''}
{it.prazo}
{fmtBRL(it.preco)}
))}
{deal.recorrente > 0 && Recorrente mensal {fmtBRL(deal.recorrente)} }
Valor total {fmtBRL(deal.total)}
Forma de pagamento {metodo}
{deal.obsGeral &&
Observações {deal.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.
);
}
function ClientPayment({ total, onPaid, busy }) {
const [copied, setCopied] = React.useState(false);
const pixCfg = window.PIX_DEFAULT;
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'}
{busy ? 'Confirmando…' : 'Já realizei o pagamento'}
);
}
function ClientView({ params }) {
const { p: id, t: token, d: enc, c } = params;
const code = c || token || '';
const [deal, setDeal] = React.useState(null);
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState('');
const [phase, setPhase] = React.useState('term'); // term | pay | done
const [aceito, setAceito] = React.useState(false);
const [nome, setNome] = React.useState('');
const [cpf, setCpf] = React.useState('');
const [assinaturaImg, setAssinaturaImg] = React.useState(null);
const [busy, setBusy] = React.useState(false);
const [warn, setWarn] = React.useState('');
const [diag, setDiag] = React.useState(null);
const [source, setSource] = React.useState(null); // 'cloud' | 'link'
const [syncErr, setSyncErr] = React.useState(''); // erro do client_set_status, se houver
const scrollRef = React.useRef(null);
const hasData = (d) => !!(d && d.cliente && (d.cliente.nome || (d.items && d.items.length)));
/* carga inicial: tenta nuvem (RPC) e cai no payload codificado; usa a fonte que tiver dados */
React.useEffect(() => {
(async () => {
const info = { mode: window.crmStore.mode, hasCloud: !!window.crmStore.hasCloud, id: id || '(curto)', token: code ? 'ok' : '(vazio)', encLen: (enc || '').length, cloud: '—', encOk: false };
let cloudDeal = null, encDeal = null;
if (window.crmStore.hasCloud && code) {
try { cloudDeal = await window.crmStore.clientGet(code, code); info.cloud = hasData(cloudDeal) ? 'ok' : 'vazio'; }
catch (e) { info.cloud = 'erro: ' + ((e && e.message) || e); }
} else { info.cloud = 'não tentado'; }
if (enc) {
encDeal = (typeof window.decodeShare === 'function')
? window.decodeShare(enc)
: (function () { try { let s = String(enc).replace(/ /g, '+').replace(/-/g, '+').replace(/_/g, '/'); while (s.length % 4) s += '='; return JSON.parse(decodeURIComponent(escape(atob(s)))); } catch (e) { return null; } })();
info.encOk = hasData(encDeal);
}
console.log('[D Digital CRM] diagnóstico do link:', info, { cloudDeal, encDeal });
// escolhe a melhor fonte: a que efetivamente tem dados (nuvem tem prioridade)
const fromCloud = hasData(cloudDeal);
const d = fromCloud ? cloudDeal : (hasData(encDeal) ? encDeal : (cloudDeal || encDeal));
if (!hasData(d)) {
setDiag(info);
setError('Não foi possível carregar os dados desta proposta.');
setLoading(false);
return;
}
setSource(fromCloud ? 'cloud' : 'link');
if (!window.crmStore.hasCloud) setWarn('offline');
setNome((d.cliente && d.cliente.nome) || '');
setCpf((d.cliente && d.cliente.doc) || '');
setDeal(d); setLoading(false);
})();
}, [code, id, token, enc]);
const goPay = async () => {
if (!aceito) { setWarn('Marque o aceite dos termos para continuar.'); return; }
if (!nome.trim()) { setWarn('Informe seu nome completo.'); return; }
setWarn(''); setBusy(true);
const assinatura = { nome: nome.trim(), cpf, signedAt: Date.now() };
try { if (window.crmStore.hasCloud) await window.crmStore.clientSetStatus(code, code, 'aceite', assinatura); }
catch (e) { console.error('[D Digital CRM] client_set_status (aceite) falhou:', e); setSyncErr((e && e.message) || String(e)); }
setBusy(false); setPhase('pay');
if (scrollRef.current) scrollRef.current.scrollTop = 0;
};
const confirmPaid = async () => {
setBusy(true);
let ok = true;
try { if (window.crmStore.hasCloud) await window.crmStore.clientSetStatus(code, code, 'pagamento', null); }
catch (e) { console.error('[D Digital CRM] client_set_status (pagamento) falhou:', e); setSyncErr((e && e.message) || String(e)); ok = false; }
// verificação: relê da nuvem para confirmar que o status realmente mudou
if (ok && window.crmStore.hasCloud) {
try {
const check = await window.crmStore.clientGet(code, code);
if (!check || check.stage !== 'pagamento') setSyncErr('A confirmação não chegou ao sistema (link antigo?). Avise seu contato pelo WhatsApp.');
} catch (e) { setSyncErr('Não foi possível confirmar com o sistema. Avise seu contato pelo WhatsApp.'); }
}
setBusy(false); setPhase('done');
if (scrollRef.current) scrollRef.current.scrollTop = 0;
};
if (loading) return ;
return (
{error && (
Link indisponível
{error} Solicite um novo ao seu contato na {window.brandName()}.
{diag && (
Detalhes técnicos
Modo: {diag.mode} · nuvem: {String(diag.hasCloud)}
ID: {diag.id} · token: {diag.token}
Busca na nuvem: {diag.cloud}
Dados no link (d=): {diag.encLen} chars · válido: {String(diag.encOk)}
Se "Busca na nuvem" indicar erro, falta rodar o passo 7 no Supabase. Se o "d=" estiver com 0 chars, o link foi cortado ao enviar — gere e copie o link inteiro.
)}
)}
{!error && deal && phase === 'term' && (
setAceito(e.target.checked)} />
Li e aceito os termos, valores e condições comerciais acima.
setNome(e.target.value)} />
setCpf(maskDoc(e.target.value))} inputMode="numeric" />
Assinatura digital
{}} onCapture={(img) => setAssinaturaImg(img)} />
{warn &&
{warn}
}
{busy ? 'Registrando…' : 'Confirmar aceite e ir para pagamento'}
)}
{!error && deal && phase === 'pay' && (
)}
{!error && deal && phase === 'done' && (
Tudo certo! 🎉
Recebemos seu aceite e a confirmação de pagamento. A equipe da {window.brandName()} dará
sequência ao seu projeto e entrará em contato em breve.
{syncErr ? (
{syncErr}
) : (
Confirmação registrada no sistema.
)}
)}
{window.brandName()}{window.brandSite() ? ' · ' + window.brandSite() : ''}
);
}
Object.assign(window, { ClientView });