/* ============================================================ wizard.jsx — Shell do fluxo comercial (Etapas 1–6) ============================================================ */ function Wizard({ onCommit, onPaid, onExit, resume }) { // mapeia a etapa salva do pipeline -> passo do wizard para retomar const stageToStep = { reuniao: 1, proposta: 3, aceite: 4, pagamento: 6, contrato: 6, projeto: 6 }; const startStep = resume ? (stageToStep[resume.stage] || 1) : 1; const [step, setStep] = React.useState(startStep); const [maxStep, setMaxStep] = React.useState(startStep); const [dealId, setDealId] = React.useState(resume ? resume.id : null); const [toast, setToast] = React.useState(''); const [cliente, setCliente] = React.useState( resume ? { ...{ nome: '', empresa: '', doc: '', whatsapp: '', email: '', endereco: '' }, ...resume.cliente } : { nome: '', empresa: '', doc: '', whatsapp: '', email: '', endereco: '' }); const [items, setItems] = React.useState(resume && resume.items ? resume.items.map(i => ({ ...i })) : []); const [pagamento, setPagamento] = React.useState(resume && resume.pagamento ? resume.pagamento : 'pix'); const [obsGeral, setObsGeral] = React.useState(resume && resume.obsGeral ? resume.obsGeral : ''); const [aceite, setAceite] = React.useState({ aceito: false, nome: '', cpf: '', assinado: false }); const [pixCfg, setPixCfg] = React.useState({ ...window.PIX_DEFAULT }); const createdAtRef = React.useRef(resume && resume.createdAt ? resume.createdAt : Date.now()); const tokenRef = React.useRef(resume && resume.shareToken ? resume.shareToken : null); const total = items.reduce((s, i) => s + (Number(i.preco) || 0), 0); const recorrente = items.filter(i => i.tipo === 'recorrente').reduce((s, i) => s + (Number(i.preco) || 0), 0); const scrollRef = React.useRef(null); const go = (n) => { setStep(n); setMaxStep(m => Math.max(m, n)); if (scrollRef.current) scrollRef.current.scrollTop = 0; }; const flash = (m) => { setToast(m); setTimeout(() => setToast(''), 2200); }; React.useEffect(() => { if (resume) flash('Retomando · ' + (resume.cliente && resume.cliente.nome || 'cliente')); }, []); /* ----- serviços ----- */ const toggleService = (svc) => { setItems(prev => prev.find(i => i.id === svc.id) ? prev.filter(i => i.id !== svc.id) : [...prev, { id: svc.id, nome: svc.nome, tipo: svc.tipo, cat: svc.cat, preco: svc.preco, prazo: svc.prazo, obs: '' }]); }; const editItem = (id, patch) => setItems(prev => prev.map(i => i.id === id ? { ...i, ...patch } : i)); const removeItem = (id) => setItems(prev => prev.filter(i => i.id !== id)); const buildDeal = (stage) => ({ id: dealId || window.uid(), cliente: { ...cliente }, items: items.map(i => ({ ...i })), total, recorrente, pagamento, obsGeral, stage, createdAt: createdAtRef.current, updatedAt: Date.now(), ...(tokenRef.current ? { shareToken: tokenRef.current } : {}), }); /* ----- gerar link do cliente (assina + paga pelo próprio aparelho) ----- */ // fallbacks locais — funcionam mesmo se um data.js antigo estiver em cache no servidor const genToken = () => (typeof window.shareCode === 'function') ? window.shareCode() : (typeof window.shareToken === 'function' ? window.shareToken() : (Math.random().toString(36).slice(2) + Date.now().toString(36)).slice(0, 12)); const encShare = (obj) => { if (typeof window.encodeShare === 'function') return window.encodeShare(obj); try { const b64 = btoa(unescape(encodeURIComponent(JSON.stringify(obj)))); return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); } catch (e) { return ''; } }; const sendClientLink = async () => { if (!cliente.nome.trim()) { flash('Informe o nome do cliente.'); return null; } if (items.length === 0) { flash('Adicione ao menos um serviço.'); return null; } if (!tokenRef.current) tokenRef.current = genToken(); const deal = buildDeal('proposta'); setDealId(deal.id); // aguarda de fato a gravação na nuvem (se houver), para o link já achar o registro let cloudOk = true; try { await Promise.resolve(onCommit(deal)); } catch (e) { cloudOk = false; console.warn('Falha ao salvar na nuvem antes de gerar link', e); } const base = location.origin + location.pathname; if (cloudOk) { // link curto: só o código; os dados são buscados na nuvem flash('Link gerado · negócio em Proposta Enviada ✓'); return base + '?c=' + encodeURIComponent(tokenRef.current); } // sem nuvem: embute os dados no link (maior, porém funcional) const enc = encShare({ id: deal.id, cliente: deal.cliente, items: deal.items, total: deal.total, recorrente: deal.recorrente, pagamento: deal.pagamento, obsGeral: deal.obsGeral, }); flash('Link gerado (dados no próprio link)'); return base + '?p=' + encodeURIComponent(deal.id) + '&t=' + encodeURIComponent(tokenRef.current) + '&d=' + encodeURIComponent(enc); }; /* ----- validações de avanço ----- */ const next = () => { if (step === 1) { if (!cliente.nome.trim()) return flash('Informe ao menos o nome do cliente.'); return go(2); } if (step === 3) { if (!pagamento) return flash('Selecione a forma de pagamento.'); return go(4); } if (step === 4) { if (!aceite.aceito) return flash('É necessário marcar o aceite dos termos.'); if (!aceite.nome.trim()) return flash('Informe o nome de quem aceita.'); const deal = buildDeal('aceite'); setDealId(deal.id); onCommit(deal); flash('Aceite registrado no pipeline ✓'); return go(5); } if (step === 5) return go(6); }; const finishPayment = () => { if (dealId) onPaid(dealId); flash('Pagamento confirmado ✓'); setTimeout(() => onExit('pipeline'), 700); }; /* ----- salvar progresso numa etapa anterior do pipeline ----- */ const stageForStep = { 1: 'reuniao', 2: 'proposta', 3: 'proposta' }; const saveProgress = () => { if (!cliente.nome.trim()) return flash('Informe o nome do cliente para salvar.'); if (step >= 3 && items.length === 0) return flash('Selecione ao menos um serviço para salvar a proposta.'); const stage = stageForStep[step] || 'proposta'; const deal = buildDeal(stage); setDealId(deal.id); onCommit(deal); const nome = (window.PIPELINE_STAGES.find(s => s.id === stage) || {}).nome; flash((dealId ? 'Atualizado' : 'Salvo') + ' no pipeline · ' + nome + ' ✓'); }; const showSave = step === 1 || step === 3; /* ----- footer CTA por etapa ----- */ const footer = () => { if (step === 2) return null; // o carrinho controla o avanço const cfg = { 1: { label: 'Continuar', icon: 'arrowR', variant: 'primary' }, 3: { label: 'Gerar termo de aceite', icon: 'doc', variant: 'primary' }, 4: { label: 'Confirmar aceite', icon: 'check', variant: 'yellow' }, 5: { label: 'Ir para pagamento', icon: 'arrowR', variant: 'primary' }, }[step]; return (
{showSave && ( )} {step === 6 ? ( ) : cfg ? ( ) : null}
); }; return (
{step === 1 && } {step === 2 && ( go(3)} /> )} {step === 3 && ( )} {step === 4 && ( )} {step === 5 && } {step === 6 && }
{footer()} {toast &&
{toast}
}
); } Object.assign(window, { Wizard });