Acesso de 1 dia
Teste a plataforma por 24 horas
`;
}).join('');
}
// ==================== DATA ====================
function getData() {
const p = getActivePlanilha();
if (!p) return emptyData();
if (!p.data.savings) p.data.savings = { balance: 0, goal: 0, movements: [] };
return p.data;
}
function saveData(data) {
const user = getCurrentUser();
if (!user) return;
const p = user.planilhas.find(x => x.id === user.activePlanilha);
if (p) { p.data = data; saveCurrentUser(user); }
}
function formatBRL(v) {
return 'R$ ' + (v||0).toLocaleString('pt-BR', {minimumFractionDigits:2, maximumFractionDigits:2});
}
function num(id) { return parseFloat(document.getElementById(id).value) || 0; }
// ==================== LOAD ====================
function loadPlanilhaData() {
const data = getData();
document.getElementById('incomeSalary').value = data.income.salary || '';
document.getElementById('incomeExtra').value = data.income.extra || '';
document.getElementById('incomeOther').value = data.income.other || '';
document.getElementById('expHousing').value = data.expenses.housing || '';
document.getElementById('expFood').value = data.expenses.food || '';
document.getElementById('expTransport').value = data.expenses.transport || '';
document.getElementById('expHealth').value = data.expenses.health || '';
document.getElementById('expEducation').value = data.expenses.education || '';
document.getElementById('expLeisure').value = data.expenses.leisure || '';
document.getElementById('expSubs').value = data.expenses.subs || '';
document.getElementById('expOther').value = data.expenses.other || '';
document.getElementById('savGoal').value = data.savings.goal || '';
updateBudget();
renderGoals();
renderExpenses();
renderSavings();
calcCompound(); calcGoal(); calcDebt(); calcRetirement();
}
// ==================== TABS ====================
function showTab(name) {
document.querySelectorAll('.tab-panel').forEach(p => p.classList.add('hidden'));
document.getElementById('panel-'+name).classList.remove('hidden');
document.querySelectorAll('.tab-btn').forEach(b => {
b.classList.remove('tab-active');
b.classList.add('bg-slate-800/50');
});
const btn = document.getElementById('tab-'+name);
btn.classList.add('tab-active');
btn.classList.remove('bg-slate-800/50');
if (name === 'dashboard') updateCharts();
}
// ==================== BUDGET ====================
function updateBudget() {
const data = getData();
data.income.salary = num('incomeSalary');
data.income.extra = num('incomeExtra');
data.income.other = num('incomeOther');
data.expenses.housing = num('expHousing');
data.expenses.food = num('expFood');
data.expenses.transport = num('expTransport');
data.expenses.health = num('expHealth');
data.expenses.education = num('expEducation');
data.expenses.leisure = num('expLeisure');
data.expenses.subs = num('expSubs');
data.expenses.other = num('expOther');
saveData(data);
const income = data.income.salary + data.income.extra + data.income.other;
const exp = Object.values(data.expenses).reduce((a,b)=>a+b,0);
const balance = income - exp;
const rate = income > 0 ? ((balance / income) * 100).toFixed(0) : 0;
const savBal = data.savings.balance || 0;
document.getElementById('summaryIncome').textContent = formatBRL(income);
document.getElementById('summaryExpenses').textContent = formatBRL(exp);
document.getElementById('summaryBalance').textContent = formatBRL(balance);
document.getElementById('summarySavings').textContent = formatBRL(savBal);
document.getElementById('summarySavingsRate').textContent = rate + '%';
const needs = data.expenses.housing + data.expenses.food + data.expenses.transport + data.expenses.health + data.expenses.education;
const wants = data.expenses.leisure + data.expenses.subs + data.expenses.other;
const saves = Math.max(0, balance);
document.getElementById('needIdeal').textContent = formatBRL(income * 0.5);
document.getElementById('wantIdeal').textContent = formatBRL(income * 0.3);
document.getElementById('saveIdeal').textContent = formatBRL(income * 0.2);
const nPct = income > 0 ? Math.min(100, (needs/income)*100) : 0;
const wPct = income > 0 ? Math.min(100, (wants/income)*100) : 0;
const sPct = income > 0 ? Math.min(100, (saves/income)*100) : 0;
document.getElementById('needPct').textContent = nPct.toFixed(0) + '%';
document.getElementById('wantPct').textContent = wPct.toFixed(0) + '%';
document.getElementById('savePct').textContent = sPct.toFixed(0) + '%';
document.getElementById('needBar').style.width = nPct + '%';
document.getElementById('wantBar').style.width = wPct + '%';
document.getElementById('saveBar').style.width = sPct + '%';
updateCharts();
}
// ==================== SAVINGS ====================
function addSavings() {
const type = document.getElementById('savType').value;
const value = parseFloat(document.getElementById('savValue').value) || 0;
const desc = document.getElementById('savDesc').value.trim() || (type === 'deposito' ? 'Depósito' : 'Saque');
if (value <= 0) return alert('Informe um valor válido');
const data = getData();
if (type === 'saque' && value > data.savings.balance) return alert('Saldo insuficiente na poupança');
const delta = type === 'deposito' ? value : -value;
data.savings.balance = (data.savings.balance || 0) + delta;
data.savings.movements.unshift({
id: Date.now(), type, value, desc, date: new Date().toLocaleDateString('pt-BR')
});
saveData(data);
document.getElementById('savValue').value = '';
document.getElementById('savDesc').value = '';
renderSavings();
updateBudget();
}
function updateSavingsGoal() {
const data = getData();
data.savings.goal = num('savGoal');
saveData(data);
renderSavings();
}
function renderSavings() {
const data = getData();
const bal = data.savings.balance || 0;
const goal = data.savings.goal || 0;
const deposited = data.savings.movements.filter(m => m.type === 'deposito').reduce((a,b)=>a+b.value,0);
document.getElementById('savingsBalance').textContent = formatBRL(bal);
document.getElementById('savingsDeposited').textContent = formatBRL(deposited);
document.getElementById('savingsGoalDisplay').textContent = formatBRL(goal);
document.getElementById('summarySavings').textContent = formatBRL(bal);
const pct = goal > 0 ? Math.min(100, (bal / goal * 100)).toFixed(0) : 0;
document.getElementById('savProgressPct').textContent = pct + '%';
document.getElementById('savProgressBar').style.width = pct + '%';
const el = document.getElementById('savingsList');
if (!data.savings.movements.length) {
el.innerHTML = '
Nenhum movimento ainda.
';
return;
}
el.innerHTML = data.savings.movements.map(m => `
${m.type==='deposito'?'+':'-'}${formatBRL(m.value)}
`).join('');
}
// ==================== SMART CALCULATOR ====================
function setSmartExample(txt) {
document.getElementById('smartInput').value = txt;
runSmartCalc();
}
function runSmartCalc() {
const input = document.getElementById('smartInput').value.toLowerCase().trim();
if (!input) return;
const resultBox = document.getElementById('smartResult');
const answerEl = document.getElementById('smartAnswer');
const explainEl = document.getElementById('smartExplain');
resultBox.classList.remove('hidden');
const numbers = input.match(/[\d.,]+/g)?.map(n => parseFloat(n.replace(',','.'))) || [];
if (input.includes('saldo') && (input.includes('meu') || input.includes('qual'))) {
const data = getData();
const income = data.income.salary + data.income.extra + data.income.other;
const exp = Object.values(data.expenses).reduce((a,b)=>a+b,0);
answerEl.textContent = formatBRL(income - exp);
explainEl.textContent = 'Saldo = Renda total − Despesas totais da planilha atual.';
return;
}
if (input.includes('poupança') || input.includes('poupanca')) {
const data = getData();
answerEl.textContent = formatBRL(data.savings.balance || 0);
explainEl.textContent = 'Saldo atual registrado na aba Poupança.';
return;
}
if ((input.includes('juntar') || input.includes('guardar') || input.includes('poupar') || input.includes('meta')) && numbers.length >= 1) {
const target = numbers[0] >= 100 ? numbers[0] : numbers[0] * 1000;
const months = numbers.find(n => n > 1 && n < 120) || 12;
const monthly = target / months;
answerEl.textContent = formatBRL(monthly) + ' por mês';
explainEl.textContent = `Para juntar ${formatBRL(target)} em ${months} meses (sem rendimento), você precisa guardar ${formatBRL(monthly)} todo mês.`;
return;
}
if (input.includes('juros') || input.includes('composto') || input.includes('rendimento')) {
const principal = numbers[0] || 1000;
const rate = numbers.find(n => n > 0 && n <= 30) || 12;
const years = numbers.find(n => n > 1 && n <= 50) || 5;
const monthly = numbers.find(n => n >= 50 && n !== principal) || 0;
const r = rate / 100 / 12;
const n = years * 12;
let total = principal;
for (let i = 0; i < n; i++) total = total * (1 + r) + monthly;
answerEl.textContent = formatBRL(total);
explainEl.textContent = `Investindo ${formatBRL(principal)} inicial + ${formatBRL(monthly)}/mês a ${rate}% a.a. durante ${years} anos.`;
return;
}
if (input.includes('dívida') || input.includes('divida') || input.includes('quitar') || input.includes('pagar')) {
const debt = numbers[0] || 5000;
const rate = numbers.find(n => n > 0 && n < 10) || 2;
const payment = numbers.find(n => n >= 50 && n !== debt) || 300;
if (payment <= debt * (rate/100)) {
answerEl.textContent = 'Pagamento insuficiente';
explainEl.textContent = 'O valor mensal não cobre nem os juros. Aumente o pagamento.';
return;
}
let bal = debt, months = 0, total = 0;
while (bal > 0.01 && months < 600) {
bal = bal * (1 + rate/100) - payment;
total += payment;
months++;
}
answerEl.textContent = months + ' meses (' + (months/12).toFixed(1) + ' anos)';
explainEl.textContent = `Dívida de ${formatBRL(debt)} a ${rate}% a.m. pagando ${formatBRL(payment)}/mês. Total pago: ${formatBRL(total)}.`;
return;
}
answerEl.textContent = 'Não entendi completamente';
explainEl.textContent = 'Tente frases como: "quanto preciso guardar para juntar 10 mil em 12 meses", "qual meu saldo", "juros compostos de 5000 a 12% por 5 anos".';
}
// ==================== VOICE ====================
let recognition = null;
let isListening = false;
function initVoice() {
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
if (!SpeechRecognition) return null;
const rec = new SpeechRecognition();
rec.lang = 'pt-BR';
rec.continuous = false;
rec.interimResults = false;
return rec;
}
function showVoiceFeedback(msg) {
const el = document.getElementById('voiceFeedback');
el.textContent = msg;
el.classList.remove('hidden');
setTimeout(() => el.classList.add('hidden'), 3000);
}
function toggleVoice() {
if (!recognition) recognition = initVoice();
if (!recognition) return alert('Seu navegador não suporta comando de voz. Use o Chrome.');
const btn = document.getElementById('voiceBtn');
if (isListening) {
recognition.stop();
isListening = false;
btn.classList.remove('listening', 'bg-emerald-500/40');
return;
}
isListening = true;
btn.classList.add('listening', 'bg-emerald-500/40');
showVoiceFeedback('Ouvindo... fale agora');
recognition.onresult = (e) => {
const text = e.results[0][0].transcript.toLowerCase();
showVoiceFeedback('Você disse: ' + text);
processVoiceCommand(text);
};
recognition.onerror = () => {
isListening = false;
btn.classList.remove('listening', 'bg-emerald-500/40');
showVoiceFeedback('Erro no reconhecimento. Tente de novo.');
};
recognition.onend = () => {
isListening = false;
btn.classList.remove('listening', 'bg-emerald-500/40');
};
recognition.start();
}
function toggleVoiceSmart() {
if (!recognition) recognition = initVoice();
if (!recognition) return alert('Seu navegador não suporta comando de voz. Use o Chrome.');
showVoiceFeedback('Ouvindo para a calculadora...');
recognition.onresult = (e) => {
const text = e.results[0][0].transcript;
document.getElementById('smartInput').value = text;
showVoiceFeedback('Você disse: ' + text);
runSmartCalc();
};
recognition.onerror = () => showVoiceFeedback('Erro no reconhecimento');
recognition.start();
}
function processVoiceCommand(text) {
if (text.includes('dashboard') || text.includes('início') || text.includes('inicio')) { showTab('dashboard'); return; }
if (text.includes('orçamento') || text.includes('orcamento')) { showTab('budget'); return; }
if (text.includes('poupança') || text.includes('poupanca')) { showTab('savings'); return; }
if (text.includes('calculadora') || text.includes('inteligente')) { showTab('smart'); return; }
if (text.includes('metas')) { showTab('goals'); return; }
if (text.includes('despesas')) { showTab('tracker'); return; }
if (text.includes('saldo')) {
showTab('smart');
document.getElementById('smartInput').value = 'qual meu saldo';
runSmartCalc();
return;
}
const despesaMatch = text.match(/(?:despesa|gastei|gastos?)\s+(\d+[\d.,]*)\s*(?:reais?)?\s*(?:de\s+)?(.+)?/i);
if (despesaMatch) {
const value = parseFloat(despesaMatch[1].replace(',','.'));
const cat = (despesaMatch[2] || 'Outros').trim();
document.getElementById('expDesc').value = cat;
document.getElementById('expValue').value = value;
const cats = ['Alimentação','Transporte','Moradia','Lazer','Saúde','Educação','Outros'];
const found = cats.find(c => cat.toLowerCase().includes(c.toLowerCase().slice(0,4)));
if (found) document.getElementById('expCat').value = found;
addExpense();
showVoiceFeedback('Despesa de ' + formatBRL(value) + ' registrada!');
showTab('tracker');
return;
}
const depMatch = text.match(/(?:dep[oó]sito|guardar|poupar)\s+(\d+[\d.,]*)/i);
if (depMatch) {
const value = parseFloat(depMatch[1].replace(',','.'));
document.getElementById('savType').value = 'deposito';
document.getElementById('savValue').value = value;
addSavings();
showVoiceFeedback('Depósito de ' + formatBRL(value) + ' na poupança!');
showTab('savings');
return;
}
showTab('smart');
document.getElementById('smartInput').value = text;
runSmartCalc();
}
// ==================== CHARTS ====================
let expenseChart, balanceChart;
function updateCharts() {
const data = getData();
const exp = data.expenses;
const labels = ['Moradia','Alimentação','Transporte','Saúde','Educação','Lazer','Assinaturas','Outros'];
const values = [exp.housing, exp.food, exp.transport, exp.health, exp.education, exp.leisure, exp.subs, exp.other];
const colors = ['#10b981','#34d399','#6ee7b7','#a7f3d0','#fbbf24','#f472b6','#60a5fa','#a78bfa'];
if (expenseChart) expenseChart.destroy();
const ctx1 = document.getElementById('expenseChart');
if (ctx1) {
expenseChart = new Chart(ctx1, {
type: 'doughnut',
data: { labels, datasets: [{ data: values, backgroundColor: colors, borderWidth: 0 }] },
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'right', labels: { color: '#94a3b8', font: { size: 11 } } } } }
});
}
if (!data.history.length) {
const income = data.income.salary + data.income.extra + data.income.other;
const totalExp = Object.values(data.expenses).reduce((a,b)=>a+b,0);
data.history = Array.from({length:6}, (_,i) => ({
month: ['Jan','Fev','Mar','Abr','Mai','Jun'][i],
balance: Math.max(0, (income - totalExp) * (i+1) * 0.7 + Math.random()*400)
}));
saveData(data);
}
if (balanceChart) balanceChart.destroy();
const ctx2 = document.getElementById('balanceChart');
if (ctx2) {
balanceChart = new Chart(ctx2, {
type: 'line',
data: {
labels: data.history.map(h=>h.month),
datasets: [{ label: 'Saldo', data: data.history.map(h=>h.balance), borderColor: '#38bdf8', backgroundColor: 'rgba(56,189,248,0.1)', fill: true, tension: 0.4 }]
},
options: {
responsive: true, maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
x: { ticks: { color: '#94a3b8' }, grid: { color: 'rgba(148,163,184,0.08)' } },
y: { ticks: { color: '#94a3b8' }, grid: { color: 'rgba(148,163,184,0.08)' } }
}
}
});
}
}
// ==================== CALCULATORS ====================
function calcCompound() {
const P = num('ciPrincipal'), M = num('ciMonthly'), r = num('ciRate')/100/12, n = num('ciYears')*12;
let total = P;
for (let i=0; i
0.01 && months < 600) {
balance = balance * (1 + rate) - payment;
totalPaid += payment;
months++;
}
document.getElementById('debtResult').textContent = months + ' meses (' + (months/12).toFixed(1) + ' anos)';
document.getElementById('debtTotal').textContent = formatBRL(totalPaid);
document.getElementById('debtInterest').textContent = formatBRL(totalPaid - num('debtAmount'));
}
function calcRetirement() {
const years = num('retTarget') - num('retAge');
const M = num('retMonthly'), r = num('retRate')/100/12, n = Math.max(0, years) * 12;
let total = 0;
for (let i=0; i x.id === id);
if (g) { g.current = parseFloat(val) || 0; saveData(data); renderGoals(); }
}
function deleteGoal(id) {
const data = getData();
data.goals = data.goals.filter(x => x.id !== id);
saveData(data);
renderGoals();
}
function renderGoals() {
const data = getData();
const el = document.getElementById('goalsList');
if (!data.goals.length) {
el.innerHTML = 'Nenhuma meta ainda. Adicione uma acima!
';
return;
}
el.innerHTML = data.goals.map(g => {
const pct = Math.min(100, (g.current / g.target * 100)).toFixed(0);
return ``;
}).join('');
}
// ==================== EXPENSES ====================
function addExpense() {
const desc = document.getElementById('expDesc').value.trim();
const cat = document.getElementById('expCat').value;
const value = parseFloat(document.getElementById('expValue').value) || 0;
if (!desc || value <= 0) return alert('Preencha descrição e valor');
const data = getData();
data.transactions.unshift({ id: Date.now(), desc, cat, value, date: new Date().toLocaleDateString('pt-BR') });
saveData(data);
document.getElementById('expDesc').value = '';
document.getElementById('expValue').value = '';
renderExpenses();
}
function deleteExpense(id) {
const data = getData();
data.transactions = data.transactions.filter(x => x.id !== id);
saveData(data);
renderExpenses();
}
function renderExpenses() {
const data = getData();
const el = document.getElementById('expenseList');
const total = data.transactions.reduce((a,b)=>a+b.value,0);
document.getElementById('trackerTotal').textContent = formatBRL(total);
if (!data.transactions.length) {
el.innerHTML = 'Nenhuma despesa registrada.
';
return;
}
el.innerHTML = data.transactions.map(t => `
${t.desc}
${t.cat} · ${t.date}
${formatBRL(t.value)}
`).join('');
}
// ==================== ENTER APP ====================
function enterApp(user) {
document.getElementById('authScreen').classList.add('hidden');
document.getElementById('paywallScreen').classList.add('hidden');
document.getElementById('appScreen').classList.remove('hidden');
document.getElementById('userGreeting').textContent = 'Olá, ' + user.name.split(' ')[0] + '!';
renderPlanilhas();
loadPlanilhaData();
}
// ==================== INIT ====================
(async function init() {
try {
const result = await api('me');
currentUser = result.user;
setSession(currentUser);
if (!currentUser.paid) showPaywall(currentUser);
else enterApp(currentUser);
} catch (_) {
// Usuário não autenticado: permanece na tela de login.
}
})();