Acesso restrito

Digite a senha

Controle de contas domésticas

Agosto / 2026

lançamentos no mês
Total do mês
soma de todas as contas
Pago
Pendente
Atrasado

Gasto por categoria

Onde o dinheiro do mês está indo

Situação dos pagamentos

Proporção do valor total em cada status

Divisão por responsável

Quanto cada um está pagando este mês

Editar lançamentos

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

🔑 Alterar senha

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.

📎 Comprovantes anexados

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(''+comprovante.name+''); 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(); })();