Onde o dinheiro do mês está indo
Proporção do valor total em cada status
Quanto cada um está pagando este mês
Altere qualquer campo direto na tabela — o dashboard acima recalcula sozinho. Use "Baixar planilha" para salvar uma cópia atualizada quando quiser.
| Categoria | Descrição | Valor | Vencimento | Responsável | Pagamento | Pago | Status | Comprovante |
|---|
Se a senha vazar ou você quiser trocar por segurança, altere aqui. Ao salvar, um novo arquivo .html com a senha atualizada é baixado — use-o daqui pra frente.
Antes de trocar, se tiver lançamentos ou comprovantes recentes que ainda não foram salvos automaticamente, baixe a planilha (botão no topo) e o backup de comprovantes (abaixo) — o arquivo novo com a senha atualizada parte dos dados originais neste navegador, exceto quando o salvamento automático estiver ativo.
Anexe o comprovante em qualquer linha da tabela acima — o lançamento é marcado como PAGO automaticamente, sem precisar mexer em mais nada. Tudo o que já foi guardado aparece aqui.
'); } else { w.document.write('
'); w.location.href = comprovante.dataUrl; } } function renderGallery(){ const gallery = document.getElementById('gallery'); gallery.innerHTML = ''; const withFile = rows.map((r,idx)=>({r,idx})).filter(x=>x.r.comprovante); if(withFile.length===0){ gallery.innerHTML = '
'; return; } withFile.forEach(({r,idx})=>{ const card = document.createElement('div'); card.className='gcard'; if(r.comprovante.type && r.comprovante.type.startsWith('image/')){ const img = document.createElement('img'); img.src = r.comprovante.dataUrl; img.alt = r.comprovante.name; card.appendChild(img); } else { const icon = document.createElement('div'); icon.className='gicon'; icon.textContent = '📄'; card.appendChild(icon); } const name = document.createElement('div'); name.className='gname'; name.title=r.comprovante.name; name.textContent = r.desc || r.comprovante.name; const sub = document.createElement('div'); sub.className='gsub'; sub.textContent = fmtMoney(r.valor) + ' · ' + (r.resp||'—') + (r.comprovante.addedAt ? ' · anexado em '+r.comprovante.addedAt : ''); card.appendChild(name); card.appendChild(sub); const actions = document.createElement('div'); actions.className='gactions'; const viewBtn = document.createElement('button'); viewBtn.textContent='Ver'; viewBtn.addEventListener('click', ()=> verComprovante(r.comprovante)); const rmBtn = document.createElement('button'); rmBtn.textContent='Remover'; rmBtn.addEventListener('click', ()=>{ if(confirm('Remover este comprovante?')){ rows[idx].comprovante = null; renderTable(); renderGallery(); scheduleSave(); } }); actions.appendChild(viewBtn); actions.appendChild(rmBtn); card.appendChild(actions); gallery.appendChild(card); }); } const anexoInput = document.getElementById('anexoInput'); anexoInput.addEventListener('change', e=>{ const file = e.target.files[0]; if(!file || anexoIdx===null) return; if(file.size > 8*1024*1024){ alert('Esse arquivo tem mais de 8MB. Tente uma foto/scan mais leve do comprovante.'); anexoInput.value=''; return; } const reader = new FileReader(); reader.onload = ev=>{ rows[anexoIdx].comprovante = { name: file.name, type: file.type, dataUrl: ev.target.result, addedAt: new Date().toLocaleDateString('pt-BR') }; // anexar o comprovante marca a conta como paga automaticamente rows[anexoIdx].pago = 'SIM'; rows[anexoIdx].status = 'PAGO'; anexoIdx = null; renderTable(); recalc(); renderGallery(); anexoInput.value=''; }; reader.readAsDataURL(file); }); // ---- backup manual (.json) — garante que os comprovantes fiquem guardados de verdade ---- document.getElementById('backupExportBtn').addEventListener('click', ()=>{ const payload = { savedAt: new Date().toISOString(), rows }; const blob = new Blob([JSON.stringify(payload)], {type:'application/json'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'comprovantes-backup.json'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }); const backupImportInput = document.getElementById('backupImportInput'); document.getElementById('backupImportBtn').addEventListener('click', ()=> backupImportInput.click()); backupImportInput.addEventListener('change', e=>{ const file = e.target.files[0]; if(!file) return; const reader = new FileReader(); reader.onload = ev=>{ try{ const parsed = JSON.parse(ev.target.result); const restored = Array.isArray(parsed) ? parsed : parsed.rows; if(!Array.isArray(restored)) throw new Error('formato inesperado'); rows = restored; renderTable(); recalc(); renderGallery(); scheduleSave(); }catch(err){ alert('Não consegui ler esse backup: ' + err.message); } }; reader.readAsText(file); backupImportInput.value=''; }); // ---- persistência automática (quando disponível no ambiente) ---- function scheduleSave(){ clearTimeout(saveTimer); saveTimer = setTimeout(saveStorage, 700); } async function saveStorage(){ try{ if(window.storage){ await window.storage.set('despesas-rows', JSON.stringify(rows)); } }catch(e){ /* ambiente sem armazenamento — o backup manual continua disponível */ } } async function loadStorage(){ const note = document.getElementById('storageNote'); try{ if(window.storage){ const res = await window.storage.get('despesas-rows'); if(res && res.value){ const parsed = JSON.parse(res.value); if(Array.isArray(parsed) && parsed.length) rows = parsed; } note.textContent = 'Os comprovantes ficam salvos automaticamente neste ambiente. De todo modo, baixe o backup (.json) de vez em quando para ter uma cópia seguraem seu computador.'; return; } }catch(e){ /* segue sem storage automático */ } note.textContent = 'Este arquivo guarda os comprovantes apenas enquanto a página estiver aberta. Ao terminar de anexar, clique em "Baixar backup dos comprovantes (.json)" — e da próxima vez, use "Restaurar backup" para carregar tudo de volta.'; } document.getElementById('addRowBtn').addEventListener('click', ()=>{ rows.push({cat:'', desc:'', valor:0, venc:'', resp:'', forma:'', pago:'NÃO', status:'PENDENTE'}); renderTable(); recalc(); }); document.getElementById('exportBtn').addEventListener('click', ()=>{ const exportRows = rows.map(r=>({ 'CATEGORIA': r.cat, 'DESCRIÇÃO': r.desc, 'VALOR': r.valor, 'VENCIMENTO': r.venc, 'RESPONSÁVEL': r.resp, 'FORMA PAGAMENTO': r.forma, 'PAGO': r.pago, 'STATUS': r.status, 'COMPROVANTE ANEXADO': r.comprovante ? 'SIM' : 'NÃO' })); const ws = XLSX.utils.json_to_sheet(exportRows); const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, 'DESPESAS'); XLSX.writeFile(wb, 'despesas-atualizado.xlsx'); }); const importInput = document.getElementById('importInput'); document.getElementById('importBtn').addEventListener('click', ()=> importInput.click()); importInput.addEventListener('change', e=>{ const file = e.target.files[0]; if(!file) return; const reader = new FileReader(); reader.onload = ev=>{ try{ const data = new Uint8Array(ev.target.result); const wb = XLSX.read(data, {type:'array', cellDates:true}); const preferredNames = ['despesas','despesa','gastos','lançamentos','lancamentos','contas']; const nameMatch = wb.SheetNames.find(n => preferredNames.includes(n.trim().toLowerCase())); const orderedNames = nameMatch ? [nameMatch, ...wb.SheetNames.filter(n=>n!==nameMatch)] : wb.SheetNames; let newRows = []; for(const sn of orderedNames){ const json = XLSX.utils.sheet_to_json(wb.Sheets[sn], {defval:''}); if(json.length===0) continue; const sample = json[0]; const keys = Object.keys(sample); const findKey = cands => keys.find(k=>cands.includes(k.trim().toLowerCase())) || keys.find(k=>cands.some(c=>k.trim().toLowerCase().includes(c))); const kCat=findKey(['categoria']), kDesc=findKey(['descrição','descricao']), kValor=findKey(['valor']), kVenc=findKey(['vencimento']), kResp=findKey(['responsável','responsavel']), kForma=findKey(['forma pagamento','forma de pagamento','pagamento']), kPago=findKey(['pago']), kStatus=findKey(['status']); if(!kDesc && !kCat) continue; const candidate = json.map(r=>{ const desc = kDesc?String(r[kDesc]||'').trim():''; const cat = kCat?String(r[kCat]||'').trim():''; if(!desc && !cat) return null; let valRaw = kValor?r[kValor]:0; let valor = typeof valRaw==='number' ? valRaw : parseFloat(String(valRaw).replace('R$','').replace(/\./g,'').replace(',','.'))||0; let venc = kVenc ? (r[kVenc] instanceof Date ? r[kVenc].toLocaleDateString('pt-BR') : String(r[kVenc]||'').trim()) : ''; let pago = kPago?String(r[kPago]||'').trim().toUpperCase():''; let status = kStatus?String(r[kStatus]||'').trim().toUpperCase():(pago==='SIM'?'PAGO':'PENDENTE'); return {cat:cat||'OUTROS', desc:desc||'(sem descrição)', valor, venc, resp:kResp?String(r[kResp]||'').trim():'', forma:kForma?String(r[kForma]||'').trim():'', pago:pago||'NÃO', status:status||'PENDENTE'}; }).filter(Boolean); if(candidate.length>0){ newRows = candidate; break; } } if(newRows.length===0){ alert('Não encontrei uma aba com lançamentos reconhecíveis nesse arquivo.'); return; } rows = newRows; renderTable(); recalc(); }catch(err){ alert('Não consegui ler esse arquivo: ' + err.message); } }; reader.readAsArrayBuffer(file); importInput.value=''; }); (async ()=>{ await loadStorage(); renderTable(); recalc(); renderGallery(); })();