BLISS Business Studio
Script URL
Pipeline ativo
orçamentos abertos
A receber
este mês
Tarefas pendentes
por concluir
Foco ativo
negócio em crescimento
Negócios
Em crescimento ativo
Financeiro
Total previsto
Tarefas do dia
Nota rápida
// ─── CONSTANTS ─────────────────────────────────────────────── const LS_URL = 'bliss_script_url'; const LS_NOTA = 'bliss_nota'; let SCRIPT_URL = localStorage.getItem(LS_URL) || ''; // ─── STATE ─────────────────────────────────────────────────── let S = { cfg: { foco_ativo:'–', orcamentos_abertos:0, negocios:[ {id:'soul', nome:'SOUL / Andreia Ramalheiro',status:'active', statusLabel:'Em crescimento'}, {id:'cp', nome:'Cosmopolitan Party', status:'hold', statusLabel:'Operacional'}, {id:'cv', nome:'Cosmopolitan Viagens', status:'quiet', statusLabel:'Latente'}, {id:'hc', nome:'Home Concept', status:'hold', statusLabel:'Operacional'}, {id:'at', nome:'Andreia Terapias', status:'hold', statusLabel:'Ativo (agenda)'}, {id:'form', nome:'Formação (Freelance)', status:'active', statusLabel:'Em prospeção'}, ], metas:[{label:'Meta CP',meta:4000},{label:'Meta SOUL',meta:1500}] }, fin:{mes:getMes(), items:[], meses:[]}, tasks:[], leads:[], mesIdx:0, }; // ─── INIT ──────────────────────────────────────────────────── document.addEventListener('DOMContentLoaded', () => { document.getElementById('dateLbl').textContent = getDateStr(); document.getElementById('urlInput').value = SCRIPT_URL; document.getElementById('notaArea').value = localStorage.getItem(LS_NOTA)||''; document.querySelectorAll('.modal-overlay').forEach(m => { m.addEventListener('click', e => { if(e.target===m) m.classList.remove('open'); }); }); if(SCRIPT_URL) { document.getElementById('urlBar').classList.add('hidden'); loadAll(); } else { syncState('err','Sem ligação'); renderAll(); } }); // ─── API ───────────────────────────────────────────────────── async function get(params) { if(!SCRIPT_URL) return null; syncState('loading','A carregar…'); try { const r = await fetch(`${SCRIPT_URL}?${new URLSearchParams(params)}`); const d = await r.json(); if(d.error) throw new Error(d.error); syncState('ok','Sincronizado'); return d; } catch(e) { syncState('err','Erro: '+e.message); return null; } } async function post(body) { if(!SCRIPT_URL) return null; syncState('loading','A guardar…'); try { const r = await fetch(SCRIPT_URL,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}); const d = await r.json(); if(d.error) throw new Error(d.error); syncState('ok','Guardado'); return d; } catch(e) { syncState('err','Erro: '+e.message); return null; } } // ─── LOAD ──────────────────────────────────────────────────── async function loadAll() { const [cfg,fin,tasks,leadsRes] = await Promise.all([ get({action:'getConfig'}), get({action:'getFinanceiro',mes:S.fin.mes}), get({action:'getTarefas'}), get({action:'getLeads'}), ]); if(cfg) { if(typeof cfg.negocios==='string') cfg.negocios=JSON.parse(cfg.negocios); if(typeof cfg.metas ==='string') cfg.metas =JSON.parse(cfg.metas); S.cfg={...S.cfg,...cfg}; } if(fin) { S.fin=fin; S.mesIdx=fin.meses.indexOf(fin.mes); } if(tasks) { S.tasks=tasks.tarefas||[]; } if(leadsRes) { updateLeads(leadsRes.leads||[], false); } renderAll(); // Poll for new leads every 2 minutes setInterval(pollLeads, 120000); } async function loadFin(mes) { const fin = await get({action:'getFinanceiro',mes}); if(fin){ S.fin=fin; S.mesIdx=fin.meses.indexOf(fin.mes); renderFin(); renderPulse(); } } // ─── SETUP URL ─────────────────────────────────────────────── function saveUrl() { SCRIPT_URL=document.getElementById('urlInput').value.trim(); localStorage.setItem(LS_URL,SCRIPT_URL); } async function initSetup() { saveUrl(); if(!SCRIPT_URL) return; const ok = await get({action:'setup'}); if(ok) { document.getElementById('urlBar').classList.add('hidden'); document.getElementById('setupBanner').classList.add('hidden'); loadAll(); } } // ─── SYNC DOT ──────────────────────────────────────────────── function syncState(s,l) { document.getElementById('syncDot').className='sync-dot '+s; document.getElementById('syncLbl').textContent=l; } // ─── RENDER ────────────────────────────────────────────────── function renderAll() { renderPulse(); renderNegs(); renderFin(); renderTasks(); } function renderPulse() { const pend = S.tasks.filter(t=>!t.feita).length; const totPos = (S.fin.items||[]).filter(i=>i.tipo==='pos').reduce((a,b)=>a+(+b.valor||0),0); const foco = Array.isArray(S.cfg.negocios) ? S.cfg.negocios.find(n=>n.status==='active') : null; const focoNome = foco ? foco.nome.split('/')[0].trim() : (S.cfg.foco_ativo||'–'); document.getElementById('pOrc').textContent = S.cfg.orcamentos_abertos||0; document.getElementById('pRec').textContent = eur(totPos); document.getElementById('pRecMes').textContent = S.fin.mes||'este mês'; document.getElementById('pTar').textContent = pend; document.getElementById('pFoc').textContent = focoNome; } function renderNegs() { const negs = Array.isArray(S.cfg.negocios) ? S.cfg.negocios : []; const foco = negs.find(n=>n.status==='active'); document.getElementById('focoNome').textContent = foco ? foco.nome : 'Nenhum negócio em crescimento ativo.'; document.getElementById('negList').innerHTML = negs.map(n=>`
${esc(n.nome)} ${esc(n.statusLabel)}
`).join(''); } function renderFin() { const f=S.fin; document.getElementById('mesDisp').textContent=f.mes||'–'; let tot=0; document.getElementById('finRows').innerHTML=(f.items||[]).map(i=>{ tot+=+i.valor||0; const sign=i.valor>0?'+':''; return `
${esc(i.label)}${sign}${eur(i.valor)}
`; }).join(''); document.getElementById('finTot').textContent=(tot>=0?'+':'')+eur(tot); // Progressos const metas=Array.isArray(S.cfg.metas)?S.cfg.metas:[]; document.getElementById('progBox').innerHTML=metas.map(m=>{ const key=m.label.replace('Meta ','').toUpperCase(); const atual=(f.items||[]).filter(i=>i.tipo==='pos'&&((i.negocio||'').toUpperCase().includes(key)||(i.label||'').toUpperCase().includes(key))).reduce((a,b)=>a+(+b.valor||0),0); const pct=Math.min(100,Math.round(atual/m.meta*100)); return `
${esc(m.label)}${eur(atual)} / ${eur(m.meta)}
`; }).join(''); } function navMes(d) { const m=S.fin.meses||[]; if(!m.length) return; S.mesIdx=Math.max(0,Math.min(m.length-1,S.mesIdx+d)); loadFin(m[S.mesIdx]); } function renderTasks() { const ts=S.tasks||[]; const pend=ts.filter(t=>!t.feita).length; document.getElementById('taskCnt').textContent=`${pend} de ${ts.length} pendentes`; document.getElementById('pTar').textContent=pend; document.getElementById('taskList').innerHTML=ts.map(t=>`
${t.feita?'✓':''}
${esc(t.texto)} ${esc(t.tag||'')}
`).join(''); } // ─── TASK ACTIONS ──────────────────────────────────────────── async function toggleTask(id) { const t=S.tasks.find(t=>String(t.id)===String(id)); if(!t) return; t.feita=!t.feita; renderTasks(); await post({action:'setTarefa',id,feita:t.feita}); } async function addTask() { const txt=document.getElementById('newTxt').value.trim(); const tag=document.getElementById('newTag').value; if(!txt) return; document.getElementById('newTxt').value=''; const r=await post({action:'setTarefa',texto:txt,tag}); if(r){ S.tasks.unshift({id:r.id||Date.now(),texto:txt,tag,feita:false}); renderTasks(); } } async function addTaskModal() { const txt=document.getElementById('mTxt').value.trim(); const tag=document.getElementById('mTag').value; if(!txt) return; document.getElementById('mTxt').value=''; closeModal('taskModal'); const r=await post({action:'setTarefa',texto:txt,tag}); if(r){ S.tasks.unshift({id:r.id||Date.now(),texto:txt,tag,feita:false}); renderTasks(); } } async function delTask(id) { S.tasks=S.tasks.filter(t=>String(t.id)!==String(id)); renderTasks(); await post({action:'deleteTarefa',id}); } // ─── NOTA ──────────────────────────────────────────────────── let notaT; document.addEventListener('DOMContentLoaded',()=>{ document.getElementById('notaArea').addEventListener('input',()=>{ clearTimeout(notaT); notaT=setTimeout(saveNota,1800); }); }); function saveNota() { const txt=document.getElementById('notaArea').value; localStorage.setItem(LS_NOTA,txt); const now=new Date(); const ts=`${now.getHours().toString().padStart(2,'0')}:${now.getMinutes().toString().padStart(2,'0')}`; document.getElementById('notaTs').textContent='Auto-guardado '+ts; document.getElementById('notaSaved').textContent='✓'; setTimeout(()=>document.getElementById('notaSaved').textContent='',2000); post({action:'setConfig',config:{nota:txt,nota_ts:ts}}); } // ─── MODAL NEGÓCIOS ────────────────────────────────────────── function openNegModal() { populateNeRows(); openModal('negModal'); } function populateNeRows() { const negs=Array.isArray(S.cfg.negocios)?S.cfg.negocios:[]; document.getElementById('neRows').innerHTML=negs.map((n,i)=>neRowHtml(n,i)).join(''); } function neRowHtml(n,i) { return `
`; } function addNeRow() { const i=document.querySelectorAll('#neRows .ne-row').length; const d=document.createElement('div'); d.className='ne-row'; d.id='ne'+i; d.innerHTML=` `; document.getElementById('neRows').appendChild(d); } async function saveNegocios() { const rows=document.querySelectorAll('#neRows .ne-row'); const negocios=Array.from(rows).map((r,i)=>({ id: (r.querySelector('[data-f="nome"]')?.value||'neg'+i).toLowerCase().replace(/[\s\/]+/g,'_').slice(0,12), nome: r.querySelector('[data-f="nome"]')?.value||'', status: r.querySelector('[data-f="status"]')?.value||'hold', statusLabel: r.querySelector('[data-f="statusLabel"]')?.value||'', })); S.cfg.negocios=negocios; closeModal('negModal'); renderNegs(); renderPulse(); await post({action:'setConfig',config:{negocios}}); } // ─── MODAL FINANCEIRO ──────────────────────────────────────── function openFinEdit() { const mes=S.fin.mes||getMes(); document.getElementById('finEditMes').textContent=mes; document.getElementById('feRows').innerHTML=(S.fin.items||[]).map((it,i)=>feRowHtml(it,i)).join(''); document.getElementById('metasJson').value=JSON.stringify(Array.isArray(S.cfg.metas)?S.cfg.metas:[],null,2); openModal('finModal'); } function feRowHtml(it,i) { return `
`; } function addFeRow() { const i=document.querySelectorAll('#feRows .fe-row').length; const d=document.createElement('div'); d.className='fe-row'; d.id='fe'+i; d.innerHTML=` `; document.getElementById('feRows').appendChild(d); } async function saveFinanceiro() { const mes=S.fin.mes||getMes(); const rows=document.querySelectorAll('#feRows .fe-row'); const items=Array.from(rows).map(r=>({ label: r.querySelector('[data-f="label"]')?.value||'', negocio: r.querySelector('[data-f="negocio"]')?.value||'', valor: parseFloat(r.querySelector('[data-f="valor"]')?.value)||0, tipo: r.querySelector('[data-f="tipo"]')?.value||'pos', })); try { const mRaw=document.getElementById('metasJson').value.trim(); if(mRaw) S.cfg.metas=JSON.parse(mRaw); } catch(e) { alert('JSON inválido: '+e.message); return; } S.fin.items=items; if(!S.fin.meses.includes(mes)) S.fin.meses.push(mes); closeModal('finModal'); renderFin(); renderPulse(); await post({action:'setFinanceiro',mes,items}); await post({action:'setConfig',config:{metas:S.cfg.metas}}); } // ─── MODAL CONFIG ──────────────────────────────────────────── function openModal(id) { if(id==='cfgModal'){ document.getElementById('cfgOrc').value=S.cfg.orcamentos_abertos||0; document.getElementById('cfgFoc').value=S.cfg.foco_ativo||''; } document.getElementById(id).classList.add('open'); } function closeModal(id) { document.getElementById(id).classList.remove('open'); } async function saveConfig() { const orc=parseInt(document.getElementById('cfgOrc').value)||0; const foc=document.getElementById('cfgFoc').value.trim(); S.cfg.orcamentos_abertos=orc; S.cfg.foco_ativo=foc; closeModal('cfgModal'); renderPulse(); await post({action:'setConfig',config:{orcamentos_abertos:orc,foco_ativo:foc}}); } // ─── LEADS ─────────────────────────────────────────────────── async function loadLeads() { const r = await get({action:'getLeads'}); if(r) updateLeads(r.leads||[], true); } async function pollLeads() { const r = await get({action:'getLeads'}); if(r) updateLeads(r.leads||[], true); } function updateLeads(leads, notify) { const prevNew = S.leads.filter(l=>l.estado==='Novo').length; S.leads = leads; const nowNew = leads.filter(l=>l.estado==='Novo').length; // Notif badge const badge = document.getElementById('leadsNotif'); if(nowNew > 0) { badge.style.display = 'inline-flex'; badge.textContent = nowNew; } else { badge.style.display = 'none'; } // Toast para novos leads (apenas quando polling, não no load inicial) if(notify && nowNew > prevNew) { const newest = leads.find(l=>l.estado==='Novo'); if(newest) showToast(`🎉 Novo pedido de ${newest.nome} — ${newest.plano}`); } // Atualiza count no modal se estiver aberto const cnt = document.getElementById('leadsCount'); if(cnt) cnt.textContent = leads.length ? `(${leads.length})` : ''; renderLeads(); } function renderLeads() { const el = document.getElementById('leadsList'); if(!el) return; if(!S.leads.length) { el.innerHTML = '
Nenhum pedido ainda.
'; return; } el.innerHTML = S.leads.map(l => `
${esc(l.nome)}
${esc(l.email)}
${l.negocio ? `
${esc(l.negocio)}
` : ''}
${esc(String(l.data).slice(0,10))}
${esc(l.plano||'–')}
`).join(''); } async function updateLeadEstado(id, estado) { const lead = S.leads.find(l=>String(l.id)===String(id)); if(lead) lead.estado = estado; updateLeads(S.leads, false); await post({action:'updateLead', id, estado}); } function estadoCor(e) { return {Novo:'var(--crimson)',Contactado:'var(--amber)', 'Em proposta':'#1a6abf',Fechado:'var(--green)',Cancelado:'var(--gray)'}[e]||'var(--text)'; } function copyLeadEmail(email) { navigator.clipboard.writeText(email).then(()=>showToast('✉️ Email copiado: '+email)); } // ─── TOAST ─────────────────────────────────────────────────── function showToast(msg) { const c = document.getElementById('toastContainer'); const t = document.createElement('div'); t.className = 'toast'; t.innerHTML = `🔔${esc(msg)}`; c.appendChild(t); setTimeout(()=>t.remove(), 6000); } // ─── UTILS ─────────────────────────────────────────────────── function eur(v){ return (parseFloat(v)||0).toLocaleString('pt-PT',{minimumFractionDigits:0,maximumFractionDigits:0})+' €'; } function esc(s){ return String(s||'').replace(/&/g,'&').replace(//g,'>'); } function escA(s){ return String(s||'').replace(/"/g,'"'); } function getMes(){ const n=new Date(); const m=['Janeiro','Fevereiro','Março','Abril','Maio','Junho','Julho','Agosto','Setembro','Outubro','Novembro','Dezembro']; return m[n.getMonth()]+' '+n.getFullYear(); } function getDateStr(){ const n=new Date(); const d=['DOM','SEG','TER','QUA','QUI','SEX','SÁB']; const m=['JAN','FEV','MAR','ABR','MAI','JUN','JUL','AGO','SET','OUT','NOV','DEZ']; return `${d[n.getDay()]} ${n.getDate()} ${m[n.getMonth()]} ${n.getFullYear()}`; }