vsxprope/deepsite-project-bk2fy
0
1======= ==========================================2// DATA MANAGEMENT3// ==========================================4const STORAGE_KEY = 'financacontrol_data';5 6const defaultCategories = [7 { id: 1, name: 'Alimentação', type: 'expense', icon: 'utensils', color: '#ef4444' },8 { id: 2, name: 'Moradia', type: 'expense', icon: 'home', color: '#f97316' },9 { id: 3, name: 'Transporte', type: 'expense', icon: 'car', color: '#eab308' },10 { id: 4, name: 'Saúde', type: 'expense', icon: 'heart', color: '#ec4899' },11 { id: 5, name: 'Educação', type: 'expense', icon: 'book', color: '#8b5cf6' },12 { id: 6, name: 'Lazer', type: 'expense', icon: 'gamepad-2', color: '#06b6d4' },13 { id: 7, name: 'Vestuário', type: 'expense', icon: 'shirt', color: '#3b82f6' },14 { id: 8, name: 'Compras', type: 'expense', icon: 'shopping-cart', color: '#22c55e' },15 { id: 9, name: 'Salário', type: 'income', icon: 'briefcase', color: '#22c55e' },16 { id: 10, name: 'Investimentos', type: 'income', icon: 'trending-up', color: '#3b82f6' },17 { id: 11, name: 'Freelance', type: 'income', icon: 'briefcase', color: '#8b5cf6' },18 { id: 12, name: 'Outros', type: 'income', icon: 'gift', color: '#f97316' },19];20 21function loadData() {22 const stored = localStorage.getItem(STORAGE_KEY);23 if (stored) {24 return JSON.parse(stored);25 }26 // Seed with demo data27 const now = new Date();28 const data = {29 categories: [...defaultCategories],30 transactions: generateDemoTransactions(),31 budgets: [32 { id: 1, categoryId: 1, limit: 1500 },33 { id: 2, categoryId: 2, limit: 2500 },34 { id: 3, categoryId: 3, limit: 800 },35 { id: 4, categoryId: 6, limit: 600 },36 { id: 5, categoryId: 7, limit: 400 },37 ],38 goals: [39 { id: 1, name: 'Reserva de Emergência', target: 30000, current: 12500, deadline: '2025-12-31', color: '#3b82f6' },40 { id: 2, name: 'Viagem para Europa', target: 15000, current: 4500, deadline: '2025-06-30', color: '#22c55e' },41 { id: 3, name: 'Carro Novo', target: 80000, current: 22000, deadline: '2026-12-31', color: '#8b5cf6' },42 ],43 currentMonth: now.getMonth(),44 };45 saveData(data);46 return data;47}48 49function generateDemoTransactions() {50 const transactions = [];51 const now = new Date();52 const year = now.getFullYear();53 const month = now.getMonth();54 55 const expenseDescriptions = {56 1: ['Supermercado Extra', 'Restaurante Outback', 'Ifood delivery', 'Padaria Pão Quente', 'Lanche no trabalho'],57 2: ['Aluguel apartamento', 'Condomínio', 'Conta de luz', 'Conta de água', 'Gás'],58 3: ['Combustível', 'Uber', 'Estacionamento', 'Manutenção carro', 'Metrô'],59 4: ['Consultas médico', 'Farmácia', 'Academia', 'Plano de saúde', 'Exames'],60 5: ['Curso online', 'Livros', 'Faculdade', 'Workshop'],61 6: ['Netflix', 'Cinema', 'Show', 'Viagem fim de semana', 'Games'],62 7: ['Roupas Renner', 'Tênis Nike', 'Casaco winter', 'Meias e cuecas'],63 8: ['Amazon', 'Mercado Livre', 'Shopee', 'Presente aniversário'],64 };65 66 const incomeDescriptions = {67 9: ['Salário mensal', 'Bônus trimestral', '13º salário'],68 10: ['Dividendos ITSA4', 'Rendimento poupança', 'Lucro ações'],69 11: ['Projeto freelancer', 'Consultoria', 'Design website'],70 12: ['Venda usado', 'Cashback', 'Reembolso'],71 };72 73 // Generate 3 months of data74 for (let m = month - 2; m <= month; m++) {75 const actualMonth = m < 0 ? 12 + m : m;76 const actualYear = m < 0 ? year - 1 : year;77 78 // Income79 transactions.push({80 id: Date.now() + Math.random(),81 description: 'Salário mensal',82 value: 8500,83 type: 'income',84 categoryId: 9,85 date: `${actualYear}-${String(actualMonth + 1).padStart(2, '0')}-05`,86 notes: ''87 });88 transactions.push({89 id: Date.now() + Math.random(),90 description: 'Freelance projeto',91 value: 2500 + Math.random() * 1500,92 type: 'income',93 categoryId: 11,94 date: `${actualYear}-${String(actualMonth + 1).padStart(2, '0')}-15`,95 notes: ''96 });97 transactions.push({98 id: Date.now() + Math.random(),99 description: 'Dividendos',100 value: 350 + Math.random() * 200,101 type: 'income',102 categoryId: 10,103 date: `${actualYear}-${String(actualMonth + 1).padStart(2, '0')}-20`,104 notes: ''105 });106 107 // Expenses108 const expenseCats = [1, 2, 3, 4, 5, 6, 7, 8];109 expenseCats.forEach(catId => {110 const descs = expenseDescriptions[catId];111 const numItems = catId === 2 ? 2 : Math.floor(Math.random() * 3) + 1;112 for (let i = 0; i < numItems; i++) {113 const amounts = {114 1: [50, 250],115 2: [1800, 3500],116 3: [30, 400],117 4: [50, 350],118 5: [30, 300],119 6: [30, 200],120 7: [50, 350],121 8: [30, 250],122 };123 const [min, max] = amounts[catId];124 const value = min + Math.random() * (max - min);125 const day = Math.floor(Math.random() * 28) + 1;126 transactions.push({127 id: Date.now() + Math.random() + i + catId,128 description: descs[Math.floor(Math.random() * descs.length)],129 value: Math.round(value * 100) / 100,130 type: 'expense',131 categoryId: catId,132 date: `${actualYear}-${String(actualMonth + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`,133 notes: ''134 });135 }136 });137 }138 139 return transactions;140}141 142function saveData(data) {143 localStorage.setItem(STORAGE_KEY, JSON.stringify(data));144}145 146let appData = loadData();147let barChart, doughnutChart, lineChart, pieChart;148 149// ==========================================150// HELPERS151// ==========================================152function formatCurrency(value) {153 return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(value);154}155 156function formatDate(dateStr) {157 const d = new Date(dateStr + 'T00:00:00');158 return d.toLocaleDateString('pt-BR');159}160 161function getCategoryById(id) {162 return appData.categories.find(c => c.id === id) || { name: 'Sem categoria', icon: 'circle', color: '#94a3b8' };163}164 165function getCurrentMonthTransactions() {166 const month = appData.currentMonth;167 return appData.transactions.filter(t => {168 const d = new Date(t.date + 'T00:00:00');169 return d.getMonth() === month;170 });171}172 173function showToast(message, type = 'success') {174 const toast = document.getElementById('toast');175 const toastMessage = document.getElementById('toastMessage');176 const toastIcon = document.getElementById('toastIcon');177 178 toastMessage.textContent = message;179 if (type === 'success') {180 toastIcon.setAttribute('data-lucide', 'check-circle');181 toastIcon.className = 'w-5 h-5 text-green-400';182 } else if (type === 'error') {183 toastIcon.setAttribute('data-lucide', 'alert-circle');184 toastIcon.className = 'w-5 h-5 text-red-400';185 } else {186 toastIcon.setAttribute('data-lucide', 'info');187 toastIcon.className = 'w-5 h-5 text-blue-400';188 }189 lucide.createIcons();190 191 toast.classList.remove('hidden');192 toast.classList.remove('translate-y-4', 'opacity-0');193 toast.classList.add('translate-y-0', 'opacity-100');194 195 setTimeout(() => {196 toast.classList.add('translate-y-4', 'opacity-0');197 setTimeout(() => toast.classList.add('hidden'), 300);198 }, 3000);199}200 201// ==========================================202// NAVIGATION203// ==========================================204function showSection(section) {205 document.querySelectorAll('[id^="section-"]').forEach(el => el.classList.add('hidden'));206 document.getElementById(`section-${section}`).classList.remove('hidden');207 208 document.querySelectorAll('.nav-item').forEach(el => {209 el.classList.remove('active');210 el.classList.add('text-gray-600');211 });212 const activeBtn = document.querySelector(`[data-section="${section}"]`);213 if (activeBtn) {214 activeBtn.classList.add('active');215 activeBtn.classList.remove('text-gray-600');216 }217 218 const titles = {219 dashboard: ['Dashboard', 'Visão geral das suas finanças'],220 transactions: ['Transações', 'Gerencie suas receitas e despesas'],221 categories: ['Categorias', 'Organize suas transações por categoria'],222 budgets: ['Orçamentos', 'Defina limites de gastos por categoria'],223 goals: ['Metas', 'Acompanhe seus objetivos financeiros'],224 reports: ['Relatórios', 'Análise detalhada das suas finanças'],225 };226 227 document.getElementById('pageTitle').textContent = titles[section][0];228 document.getElementById('pageSubtitle').textContent = titles[section][1];229 230 // Close mobile sidebar231 const sidebar = document.getElementById('sidebar');232 const overlay = document.getElementById('sidebarOverlay');233 sidebar.classList.add('-translate-x-full');234 overlay.classList.add('hidden');235 236 // Refresh section data237 if (section === 'reports') renderReports();238 if (section === 'dashboard') refreshDashboard();239 if (section === 'transactions') renderTransactions();240 if (section === 'categories') renderCategories();241 if (section === 'budgets') renderBudgets();242 if (section === 'goals') renderGoals();243}244 245function toggleSidebar() {246 const sidebar = document.getElementById('sidebar');247 const overlay = document.getElementById('sidebarOverlay');248 sidebar.classList.toggle('-translate-x-full');249 overlay.classList.toggle('hidden');250}251 252function changeMonth(month) {253 appData.currentMonth = parseInt(month);254 saveData(appData);255 refreshDashboard();256}257 258// ==========================================259// DASHBOARD260// ==========================================261function refreshDashboard() {262 const transactions = getCurrentMonthTransactions();263 const income = transactions.filter(t => t.type === 'income').reduce((s, t) => s + t.value, 0);264 const expense = transactions.filter(t => t.type === 'expense').reduce((s, t) => s + t.value, 0);265 const balance = income - expense;266 const savingsRate = income > 0 ? ((income - expense) / income * 100) : 0;267 268 document.getElementById('totalIncome').textContent = formatCurrency(income);269 document.getElementById('totalExpense').textContent = formatCurrency(expense);270 document.getElementById('totalBalance').textContent = formatCurrency(balance);271 document.getElementById('savingsRate').textContent = `${Math.max(0, savingsRate).toFixed(1)}%`;272 document.getElementById('savingsBar').style.width = `${Math.max(0, Math.min(100, savingsRate))}%`;273 274 // Update changes275 const prevMonth = appData.currentMonth - 1;276 const prevTransactions = appData.transactions.filter(t => {277 const d = new Date(t.date + 'T00:00:00');278 return d.getMonth() === prevMonth;279 });280 const prevIncome = prevTransactions.filter(t => t.type === 'income').reduce((s, t) => s + t.value, 0);281 const prevExpense = prevTransactions.filter(t => t.type === 'expense').reduce((s, t) => s + t.value, 0);282 283 if (prevIncome > 0) {284 const incomeChange = ((income - prevIncome) / prevIncome * 100).toFixed(0);285 document.getElementById('incomeChange').textContent = `${incomeChange > 0 ? '+' : ''}${incomeChange}%`;286 }287 if (prevExpense > 0) {288 const expenseChange = ((expense - prevExpense) / prevExpense * 100).toFixed(0);289 document.getElementById('expenseChange').textContent = `${expenseChange > 0 ? '+' : ''}${expenseChange}%`;290 }291 292 renderRecentTransactions(transactions);293 renderBudgetOverview();294 renderBarChart();295 renderDoughnutChart();296}297 298function renderRecentTransactions(transactions) {299 const container = document.getElementById('recentTransactions');300 const sorted = [...transactions].sort((a, b) => new Date(b.date) - new Date(a.date)).slice(0, 6);301 302 if (sorted.length === 0) {303 container.innerHTML = `304 <div class="text-center py-8 text-gray-400">305 <i data-lucide="inbox" class="w-12 h-12 mx-auto mb-3 opacity-50"></i>306 <p>Nenhuma transação neste mês</p>307 </div>`;308 lucide.createIcons();309 return;310 }311 312 container.innerHTML = sorted.map(t => {313 const cat = getCategoryById(t.categoryId);314 const isIncome = t.type === 'income';315 return `316 <div class="flex items-center gap-3 p-3 rounded-xl hover:bg-gray-50 transition-colors">317 <div class="w-10 h-10 rounded-xl flex items-center justify-center flex-shrink-0" style="background: ${cat.color}15">318 <i data-lucide="${cat.icon}" class="w-5 h-5" style="color: ${cat.color}"></i>319 </div>320 <div class="flex-1 min-w-0">321 <p class="text-sm font-medium text-gray-800 truncate">${t.description}</p>322 <p class="text-xs text-gray-400">${cat.name} • ${formatDate(t.date)}</p>323 </div>324 <p class="text-sm font-semibold ${isIncome ? 'text-green-500' : 'text-red-500'}">325 ${isIncome ? '+' : '-'}${formatCurrency(t.value)}326 </p>327 </div>`;328 }).join('');329 lucide.createIcons();330}331 332function renderBudgetOverview() {333 const container = document.getElementById('budgetOverview');334 335 if (appData.budgets.length === 0) {336 container.innerHTML = `337 <div class="text-center py-8 text-gray-400">338 <i data-lucide="target" class="w-12 h-12 mx-auto mb-3 opacity-50"></i>339 <p>Nenhum orçamento definido</p>340 </div>`;341 lucide.createIcons();342 return;343 }344 345 const transactions = getCurrentMonthTransactions();346 347 container.innerHTML = appData.budgets.map(b => {348 const cat = getCategoryById(b.categoryId);349 const spent = transactions.filter(t => t.categoryId === b.categoryId && t.type === 'expense').reduce((s, t) => s + t.value, 0);350 const percent = Math.min((spent / b.limit) * 100, 100);351 const isOver = spent > b.limit;352 const barColor = isOver ? '#ef4444' : percent > 80 ? '#f97316' : cat.color;353 354 return `355 <div class="group">356 <div class="flex items-center justify-between mb-1">357 <div class="flex items-center gap-2">358 <i data-lucide="${cat.icon}" class="w-4 h-4" style="color: ${cat.color}"></i>359 <span class="text-sm font-medium text-gray-700">${cat.name}</span>360 </div>361 <span class="text-xs ${isOver ? 'text-red-500 font-bold' : 'text-gray-400'}">${formatCurrency(spent)} / ${formatCurrency(b.limit)}</span>362 </div>363 <div class="w-full bg-gray-100 rounded-full h-2">364 <div class="h-2 rounded-full progress-bar" style="width: ${percent}%; background: ${barColor}"></div>365 </div>366 </div>`;367 }).join('');368 lucide.createIcons();369}370 371function renderBarChart() {372 const ctx = document.getElementById('barChart').getContext('2d');373 const months = ['Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun', 'Jul', 'Ago', 'Set', 'Out', 'Nov', 'Dez'];374 375 const incomeData = [];376 const expenseData = [];377 378 for (let m = 0; m < 12; m++) {379 const monthTrans = appData.transactions.filter(t => {380 const d = new Date(t.date + 'T00:00:00');381 return d.getMonth() === m;382 });383 incomeData.push(monthTrans.filter(t => t.type === 'income').reduce((s, t) => s + t.value, 0));384 expenseData.push(monthTrans.filter(t => t.type === 'expense').reduce((s, t) => s + t.value, 0));385 }386 387 if (barChart) barChart.destroy();388 barChart = new Chart(ctx, {389 type: 'bar',390 data: {391 labels: months,392 datasets: [393 {394 label: 'Receitas',395 data: incomeData,396 backgroundColor: '#22c55e80',397 borderColor: '#22c55e',398 borderWidth: 2,399 borderRadius: 6,400 },401 {402 label: 'Despesas',403 data: expenseData,404 backgroundColor: '#ef444480',405 borderColor: '#ef4444',406 borderWidth: 2,407 borderRadius: 6,408 }409 ]410 },411 options: {412 responsive: true,413 maintainAspectRatio: false,414 plugins: { legend: { display: false } },415 scales: {416 x: { grid: { display: false }, ticks: { font: { size: 11 } } },417 y: {418 grid: { color: '#f1f5f9' },419 ticks: {420 font: { size: 11 },421 callback: v => 'R$ ' + (v / 1000).toFixed(0) + 'k'422 }423 }424 }425 }426 });427}428 429function renderDoughnutChart() {430 const ctx = document.getElementById('doughnutChart').getContext('2d');431 const transactions = getCurrentMonthTransactions().filter(t => t.type === 'expense');432 433 const categorySpending = {};434 transactions.forEach(t => {435 const cat = getCategoryById(t.categoryId);436 if (!categorySpending[t.categoryId]) {437 categorySpending[t.categoryId] = { total: 0, name: cat.name, color: cat.color };438 }439 categorySpending[t.categoryId].total += t.value;440 });441 442 const sortedCategories = Object.values(categorySpending).sort((a, b) => b.total - a.total);443 444 if (doughnutChart) doughnutChart.destroy();445 doughnutChart = new Chart(ctx, {446 type: 'doughnut',447 data: {448 labels: sortedCategories.map(c => c.name),449 datasets: [{450 data: sortedCategories.map(c => c.total),451 backgroundColor: sortedCategories.map(c => c.color),452 borderWidth: 0,453 hoverOffset: 8,454 }]455 },456 options: {457 responsive: true,458 maintainAspectRatio: false,459 cutout: '65%',460 plugins: {461 legend: {462 position: 'bottom',463 labels: {464 usePointStyle: true,465 pointStyle: 'circle',466 padding: 16,467 font: { size: 11 }468 }469 }470 }471 }472 });473}474 475// ==========================================476// TRANSACTIONS477// ==========================================478function renderTransactions() {479 const filterType = document.getElementById('filterType').value;480 const filterCategory = document.getElementById('filterCategory').value;481 const transactions = getCurrentMonthTransactions();482 483 let filtered = transactions;484 if (filterType !== 'all') filtered = filtered.filter(t => t.type === filterType);485 if (filterCategory !== 'all') filtered = filtered.filter(t => t.categoryId === parseInt(filterCategory));486 487 filtered.sort((a, b) => new Date(b.date) - new Date(a.date));488 489 // Populate category filter490 const filterCatEl = document.getElementById('filterCategory');491 const currentVal = filterCatEl.value;492 filterCatEl.innerHTML = '<option value="all">Todas categorias</option>';493 appData.categories.forEach(c => {494 filterCatEl.innerHTML += `<option value="${c.id}">${c.name}</option>`;495 });496 filterCatEl.value = currentVal;497 498 const tbody = document.getElementById('transactionsTable');499 500 if (filtered.length === 0) {501 tbody.innerHTML = `502 <tr>503 <td colspan="5" class="text-center py-12 text-gray-400">504 <i data-lucide="inbox" class="w-12 h-12 mx-auto mb-3 opacity-50"></i>505 <p>Nenhuma transação encontrada</p>506 </td>507 </tr>`;508 lucide.createIcons();509 return;510 }511 512 tbody.innerHTML = filtered.map(t => {513 const cat = getCategoryById(t.categoryId);514 const isIncome = t.type === 'income';515 return `516 <tr class="hover:bg-gray-50 transition-colors">517 <td class="px-6 py-4">518 <div class="flex items-center gap-3">519 <div class="w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0" style="background: ${cat.color}15">520 <i data-lucide="${cat.icon}" class="w-4 h-4" style="color: ${cat.color}"></i>521 </div>522 <div>523 <p class="text-sm font-medium text-gray-800">${t.description}</p>524 ${t.notes ? `<p class="text-xs text-gray-400">${t.notes}</p>` : ''}525 </div>526 </div>527 </td>528 <td class="px-6 py-4">529 <span class="px-2 py-1 text-xs font-medium rounded-full" style="background: ${cat.color}15; color: ${cat.color}">${cat.name}</span>530 </td>531 <td class="px-6 py-4 text-sm text-gray-500">${formatDate(t.date)}</td>532 <td class="px-6 py-4 text-right">533 <span class="text-sm font-semibold ${isIncome ? 'text-green-500' : 'text-red-500'}">534 ${isIncome ? '+' : '-'}${formatCurrency(t.value)}535 </span>536 </td>537 <td class="px-6 py-4 text-center">538 <div class="flex items-center justify-center gap-1">539 <button onclick="editTransaction(${t.id})" class="p-1.5 hover:bg-gray-100 rounded-lg transition-colors">540 <i data-lucide="pencil" class="w-4 h-4 text-gray-400"></i>541 </button>542 <button onclick="deleteTransaction(${t.id})" class="p-1.5 hover:bg-red-50 rounded-lg transition-colors">543 <i data-lucide="trash-2" class="w-4 h-4 text-red-400"></i>544 </button>545 </div>546 </td>547 </tr>`;548 }).join('');549 lucide.createIcons();550}551 552function openTransactionModal(id = null) {553 const modal = document.getElementById('transactionModal');554 modal.classList.remove('hidden');555 modal.classList.add('flex');556 557 // Populate categories558 const catSelect = document.getElementById('transactionCategory');559 catSelect.innerHTML = '';560 appData.categories.forEach(c => {561 catSelect.innerHTML += `<option value="${c.id}">${c.name} (${c.type === 'income' ? 'Receita' : 'Despesa'})</option>`;562 });563 564 if (id) {565 const t = appData.transactions.find(t => t.id === id);566 if (!t) return;567 document.getElementById('transactionModalTitle').textContent = 'Editar Transação';568 document.getElementById('transactionId').value = id;569 document.getElementById('transactionDesc').value = t.description;570 document.getElementById('transactionValue').value = t.value;571 document.getElementById('transactionCategory').value = t.categoryId;572 document.getElementById('transactionDate').value = t.date;573 document.getElementById('transactionNotes').value = t.notes || '';574 setTransactionType(t.type);575 } else {576 document.getElementById('transactionModalTitle').textContent = 'Nova Transação';577 document.getElementById('transactionId').value = '';578 document.getElementById('transactionForm').reset();579 document.getElementById('transactionDate').value = new Date().toISOString().split('T')[0];580 setTransactionType('expense');581 }582}583 584function closeTransactionModal() {585 const modal = document.getElementById('transactionModal');586 modal.classList.add('hidden');587 modal.classList.remove('flex');588}589 590function setTransactionType(type) {591 document.getElementById('transactionType').value = type;592 const btnExpense = document.getElementById('btnExpense');593 const btnIncome = document.getElementById('btnIncome');594 595 if (type === 'expense') {596 btnExpense.className = 'flex-1 py-2 rounded-lg text-sm font-medium transition-colors bg-red-500 text-white';597 btnIncome.className = 'flex-1 py-2 rounded-lg text-sm font-medium transition-colors text-gray-500';598 } else {599 btnIncome.className = 'flex-1 py-2 rounded-lg text-sm font-medium transition-colors bg-green-500 text-white';600 btnExpense.className = 'flex-1 py-2 rounded-lg text-sm font-medium transition-colors text-gray-500';601 }602 603 // Filter categories604 const catSelect = document.getElementById('transactionCategory');605 const currentCat = catSelect.value;606 catSelect.innerHTML = '';607 appData.categories.filter(c => c.type === type).forEach(c => {608 catSelect.innerHTML += `<option value="${c.id}">${c.name}</option>`;609 });610 // Try to keep selection611 const filtered = appData.categories.filter(c => c.type === type);612 if (filtered.length > 0) catSelect.value = filtered[0].id;613}614 615function saveTransaction(e) {616 e.preventDefault();617 const id = document.getElementById('transactionId').value;618 const transaction = {619 id: id ? parseFloat(id) : Date.now() + Math.random(),620 description: document.getElementById('transactionDesc').value,621 value: parseFloat(document.getElementById('transactionValue').value),622 type: document.getElementById('transactionType').value,623 categoryId: parseInt(document.getElementById('transactionCategory').value),624 date: document.getElementById('transactionDate').value,625 notes: document.getElementById('transactionNotes').value,626 };627 628 if (id) {629 const idx = appData.transactions.findIndex(t => t.id === parseFloat(id));630 if (idx !== -1) appData.transactions[idx] = transaction;631 } else {632 appData.transactions.push(transaction);633 }634 635 saveData(appData);636 closeTransactionModal();637 refreshDashboard();638 renderTransactions();639 showToast(id ? 'Transação atualizada!' : 'Transação criada!');640}641 642function editTransaction(id) {643 openTransactionModal(id);644}645 646function deleteTransaction(id) {647 if (confirm('Tem certeza que deseja excluir esta transação?')) {648 appData.transactions = appData.transactions.filter(t => t.id !== id);649 saveData(appData);650 refreshDashboard();651 renderTransactions();652 showToast('Transação excluída!', 'info');653 }654}655 656// ==========================================657// CATEGORIES658// ==========================================659function renderCategories() {660 const container = document.getElementById('categoriesList');661 662 container.innerHTML = appData.categories.map(c => {663 const transactions = getCurrentMonthTransactions().filter(t => t.categoryId === c.id);664 const total = transactions.reduce((s, t) => s + (t.type === 'expense' ? t.value : 0), 0);665 666 return `667 <div class="card-hover border border-gray-100 rounded-xl p-4 relative group">668 <button onclick="deleteCategory(${c.id})" class="absolute top-3 right-3 opacity-0 group-hover:opacity-100 p-1 hover:bg-red-50 rounded-lg transition-all">669 <i data-lucide="trash-2" class="w-4 h-4 text-red-400"></i>670 </button>671 <div class="flex items-center gap-3 mb-3">672 <div class="w-10 h-10 rounded-xl flex items-center justify-center" style="background: ${c.color}15">673 <i data-lucide="${c.icon}" class="w-5 h-5" style="color: ${c.color}"></i>674 </div>675 <div>676 <p class="text-sm font-semibold text-gray-800">${c.name}</p>677 <p class="text-xs text-gray-400">${c.type === 'income' ? 'Receita' : 'Despesa'} • ${transactions.length} transações</p>678 </div>679 </div>680 <div class="text-sm font-medium" style="color: ${c.color}">${formatCurrency(total)}</div>681 </div>`;682 }).join('');683 lucide.createIcons();684}685 686function openCategoryModal() {687 const modal = document.getElementById('categoryModal');688 modal.classList.remove('hidden');689 modal.classList.add('flex');690 document.getElementById('categoryName').value = '';691 document.getElementById('categoryType').value = 'expense';692 document.getElementById('categoryIcon').value = 'shopping-cart';693 document.getElementById('categoryColor').value = '#22c55e';694 selectColor('#22c55e');695}696 697function closeCategoryModal() {698 const modal = document.getElementById('categoryModal');699 modal.classList.add('hidden');700 modal.classList.remove('flex');701}702 703function selectColor(color) {704 document.getElementById('categoryColor').value = color;705 document.querySelectorAll('.color-btn').forEach(btn => {706 btn.classList.remove('border-gray-800');707 btn.classList.add('border-transparent');708 if (btn.dataset.color === color) {709 btn.classList.add('border-gray-800');710 btn.classList.remove('border-transparent');711 }712 });713}714 715function saveCategory(e) {716 e.preventDefault();717 const category = {718 id: Date.now(),719 name: document.getElementById('categoryName').value,720 type: document.getElementById('categoryType').value,721 icon: document.getElementById('categoryIcon').value.split(' ')[0],722 color: document.getElementById('categoryColor').value,723 };724 appData.categories.push(category);725 saveData(appData);726 closeCategoryModal();727 renderCategories();728 showToast('Categoria criada!');729}730 731function deleteCategory(id) {732 const hasTransactions = appData.transactions.some(t => t.categoryId === id);733 if (hasTransactions) {734 showToast('Esta categoria tem transações associadas!', 'error');735 return;736 }737 if (confirm('Tem certeza que deseja excluir esta categoria?')) {738 appData.categories = appData.categories.filter(c => c.id !== id);739 saveData(appData);740 renderCategories();741 showToast('Categoria excluída!', 'info');742 }743}744 745// ==========================================746// BUDGETS747// ==========================================748function renderBudgets() {749 const container = document.getElementById('budgetsList');750 const transactions = getCurrentMonthTransactions();751 752 if (appData.budgets.length === 0) {753 container.innerHTML = `754 <div class="text-center py-12 text-gray-400">755 <i data-lucide="target" class="w-12 h-12 mx-auto mb-3 opacity-50"></i>756 <p>Nenhum orçamento definido</p>757 <p class="text-xs mt-1">Clique em "Novo Orçamento" para começar</p>758 </div>`;759 lucide.createIcons();760 return;761 }762 763 container.innerHTML = appData.budgets.map(b => {764 const cat = getCategoryById(b.categoryId);765 const spent = transactions.filter(t => t.categoryId === b.categoryId && t.type === 'expense').reduce((s, t) => s + t.value, 0);766 const percent = Math.min((spent / b.limit) * 100, 100);767 const remaining = b.limit - spent;768 const isOver = spent > b.limit;769 const barColor = isOver ? '#ef4444' : percent > 80 ? '#f97316' : cat.color;770 771 return `772 <div class="border border-gray-100 rounded-xl p-5 card-hover">773 <div class="flex items-center justify-between mb-3">774 <div class="flex items-center gap-3">775 <div class="w-10 h-10 rounded-xl flex items-center justify-center" style="background: ${cat.color}15">776 <i data-lucide="${cat.icon}" class="w-5 h-5" style="color: ${cat.color}"></i>777 </div>778 <div>779 <p class="text-sm font-semibold text-gray-800">${cat.name}</p>780 <p class="text-xs text-gray-400">Limite: ${formatCurrency(b.limit)}</p>781 </div>782 </div>783 <div class="flex items-center gap-2">784 <span class="text-sm font-bold ${isOver ? 'text-red-500' : 'text-gray-800'}">${percent.toFixed(0)}%</span>785 <button onclick="deleteBudget(${b.id})" class="p-1.5 hover:bg-red-50 rounded-lg transition-colors">786 <i data-lucide="trash-2" class="w-4 h-4 text-red-400"></i>787 </button>788 </div>789 </div>790 <div class="w-full bg-gray-100 rounded-full h-3 mb-2">791 <div class="h-3 rounded-full progress-bar" style="width: ${percent}%; background: ${barColor}"></div>792 </div>793 <div class="flex justify-between text-xs">794 <span class="text-gray-400">Gasto: ${formatCurrency(spent)}</span>795 <span class="${isOver ? 'text-red-500 font-semibold' : 'text-green-500'}">${isOver ? 'Excedido: ' + formatCurrency(Math.abs(remaining)) : 'Restam: ' + formatCurrency(remaining)}</span>796 </div>797 </div>`;798 }).join('');799 lucide.createIcons();800}801 802function openBudgetModal() {803 const modal = document.getElementById('budgetModal');804 modal.classList.remove('hidden');805 modal.classList.add('flex');806 807 const catSelect = document.getElementById('budgetCategory');808 catSelect.innerHTML = '';809 appData.categories.filter(c => c.type === 'expense').forEach(c => {810 const exists = appData.budgets.some(b => b.categoryId === c.id);811 if (!exists) {812 catSelect.innerHTML += `<option value="${c.id}">${c.name}</option>`;813 }814 });815 816 if (catSelect.options.length === 0) {817 catSelect.innerHTML = '<option value="">Todas as categorias já têm orçamento</option>';818 }819 820 document.getElementById('budgetLimit').value = '';821}822 823function closeBudgetModal() {824 const modal = document.getElementById('budgetModal');825 modal.classList.add('hidden');826 modal.classList.remove('flex');827}828 829function saveBudget(e) {830 e.preventDefault();831 const budget = {832 id: Date.now(),833 categoryId: parseInt(document.getElementById('budgetCategory').value),834 limit: parseFloat(document.getElementById('budgetLimit').value),835 };836 837 if (!budget.categoryId) {838 showToast('Selecione uma categoria', 'error');839 return;840 }841 842 appData.budgets.push(budget);843 saveData(appData);844 closeBudgetModal();845 renderBudgets();846 refreshDashboard();847 showToast('Orçamento criado!');848}849 850function deleteBudget(id) {851 if (confirm('Tem certeza que deseja excluir este orçamento?')) {852 appData.budgets = appData.budgets.filter(b => b.id !== id);853 saveData(appData);854 renderBudgets();855 refreshDashboard();856 showToast('Orçamento excluído!', 'info');857 }858}859 860// ==========================================861// GOALS862// ==========================================863function renderGoals() {864 const container = document.getElementById('goalsList');865 866 if (appData.goals.length === 0) {867 container.innerHTML = `868 <div class="col-span-full text-center py-12 text-gray-400">869 <i data-lucide="flag" class="w-12 h-12 mx-auto mb-3 opacity-50"></i>870 <p>Nenhuma meta definida</p>871 <p class="text-xs mt-1">Clique em "Nova Meta" para começar</p>872 </div>`;873 lucide.createIcons();874 return;875 }876 877 container.innerHTML = appData.goals.map(g => {878 const percent = Math.min((g.current / g.target) * 100, 100);879 const remaining = g.target - g.current;880 const daysLeft = Math.ceil((new Date(g.deadline) - new Date()) / (1000 * 60 * 60 * 24));881 882 return `883 <div class="card-hover border border-gray-100 rounded-2xl p-5 relative group">884 <button onclick="deleteGoal(${g.id})" class="absolute top-3 right-3 opacity-0 group-hover:opacity-100 p-1 hover:bg-red-50 rounded-lg transition-all">885 <i data-lucide="trash-2" class="w-4 h-4 text-red-400"></i>886 </button>887 <div class="w-12 h-12 rounded-xl flex items-center justify-center mb-4" style="background: ${g.color}15">888 <i data-lucide="flag" class="w-6 h-6" style="color: ${g.color}"></i>889 </div>890 <h4 class="text-base font-semibold text-gray-800 mb-1">${g.name}</h4>891 <p class="text-xs text-gray-400 mb-3">892 ${daysLeft > 0 ? `${daysLeft} dias restantes` : 'Prazo encerrado'} • ${formatDate(g.deadline)}893 </p>894 <div class="w-full bg-gray-100 rounded-full h-2.5 mb-2">895 <div class="h-2.5 rounded-full progress-bar" style="width: ${percent}%; background: ${g.color}"></div>896 </div>897 <div class="flex justify-between text-xs mb-1">898 <span class="font-semibold" style="color: ${g.color}">${percent.toFixed(1)}%</span>899 <span class="text-gray-400">${formatCurrency(g.current)} / ${formatCurrency(g.target)}</span>900 </div>901 <p class="text-xs text-gray-400">Faltam: ${formatCurrency(Math.max(0, remaining))}</p>902 <button onclick="addToGoal(${g.id})" class="mt-3 w-full py-2 text-xs font-medium rounded-lg border transition-colors hover:bg-gray-50" style="border-color: ${g.color}30; color: ${g.color}">903 <i data-lucide="plus" class="w-3 h-3 inline"></i> Adicionar Valor904 </button>905 </div>`;906 }).join('');907 lucide.createIcons();908}909 910function openGoalModal() {911 const modal = document.getElementById('goalModal');912 modal.classList.remove('hidden');913 modal.classList.add('flex');914 document.getElementById('goalName').value = '';915 document.getElementById('goalTarget').value = '';916 document.getElementById('goalCurrent').value = '0';917 document.getElementById('goalDeadline').value = '';918 selectGoalColor('#3b82f6');919}920 921function closeGoalModal() {922 const modal = document.getElementById('goalModal');923 modal.classList.add('hidden');924 modal.classList.remove('flex');925}926 927function selectGoalColor(color) {928 document.getElementById('goalColor').value = color;929 document.querySelectorAll('.goal-color-btn').forEach(btn => {930 btn.classList.remove('border-gray-800');931 btn.classList.add('border-transparent');932 if (btn.dataset.color === color) {933 btn.classList.add('border-gray-800');934 btn.classList.remove('border-transparent');935 }936 });937}938 939function saveGoal(e) {940 e.preventDefault();941 const goal = {942 id: Date.now(),943 name: document.getElementById('goalName').value,944 target: parseFloat(document.getElementById('goalTarget').value),945 current: parseFloat(document.getElementById('goalCurrent').value) || 0,946 deadline: document.getElementById('goalDeadline').value,947 color: document.getElementById('goalColor').value,948 };949 appData.goals.push(goal);950 saveData(appData);951 closeGoalModal();952 renderGoals();953 showToast('Meta criada!');954}955 956function addToGoal(id) {957 const value = parseFloat(prompt('Quanto deseja adicionar?'));958 if (!isNaN(value) && value > 0) {959 const goal = appData.goals.find(g => g.id === id);960 if (goal) {961 goal.current = Math.min(goal.current + value, goal.target);962 saveData(appData);963 renderGoals();964 showToast(`+${formatCurrency(value)} adicionado à meta!`);965 }966 }967}968 969function deleteGoal(id) {970 if (confirm('Tem certeza que deseja excluir esta meta?')) {971 appData.goals = appData.goals.filter(g => g.id !== id);972 saveData(appData);973 renderGoals();974 showToast('Meta excluída!', 'info');975 }976}977 978// ==========================================979// REPORTS980// ==========================================981function renderReports() {982 renderLineChart();983 renderPieChart();984 renderReportSummary();985}986 987function renderLineChart() {988 const ctx = document.getElementById('lineChart').getContext('2d');989 const months = ['Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun', 'Jul', 'Ago', 'Set', 'Out', 'Nov', 'Dez'];990 991 const balanceData = [];992 const savingsData = [];993 994 for (let m = 0; m < 12; m++) {995 const monthTrans = appData.transactions.filter(t => {996 const d = new Date(t.date + 'T00:00:00');997 return d.getMonth() === m;998 });999 const income = monthTrans.filter(t => t.type === 'income').reduce((s, t) => s + t.value, 0);1000 const expense = monthTrans.filter(t => t.type === 'expense').reduce((s, t) => s + t.value, 0);1001 balanceData.push(income - expense);1002 savingsData.push(income > 0 ? ((income - expense) / income * 100) : 0);1003 }1004 1005 if (lineChart) lineChart.destroy();1006 lineChart = new Chart(ctx, {1007 type: 'line',1008 data: {1009 labels: months,1010 datasets: [1011 {1012 label: 'Saldo (R$)',1013 data: balanceData,1014 borderColor: '#3b82f6',1015 backgroundColor: '#3b82f620',1016 fill: true,1017 tension: 0.4,1018 pointBackgroundColor: '#3b82f6',1019 pointBorderColor: '#fff',1020 pointBorderWidth: 2,1021 pointRadius: 4,1022 }1023 ]1024 },1025 options: {1026 responsive: true,1027 maintainAspectRatio: false,1028 plugins: {1029 legend: { display: false },1030 tooltip: {1031 callbacks: {1032 label: ctx => `Saldo: ${formatCurrency(ctx.parsed.y)}`1033 }1034 }1035 },1036 scales: {1037 x: { grid: { display: false }, ticks: { font: { size: 11 } } },1038 y: {1039 grid: { color: '#f1f5f9' },1040 ticks: {1041 font: { size: 11 },1042 callback: v => 'R$ ' + (v / 1000).toFixed(1) + 'k'1043 }1044 }1045 }1046 }1047 });1048}1049 1050function renderPieChart() {1051 const ctx = document.getElementById('pieChart').getContext('2d');1052 const transactions = getCurrentMonthTransactions().filter(t => t.type === 'expense');1053 1054 const categorySpending = {};1055 transactions.forEach(t => {1056 const cat = getCategoryById(t.categoryId);1057 if (!categorySpending[t.categoryId]) {1058 categorySpending[t.categoryId] = { total: 0, name: cat.name, color: cat.color };1059 }1060 categorySpending[t.categoryId].total += t.value;1061 });1062 1063 const sorted = Object.values(categorySpending).sort((a, b) => b.total - a.total);1064 1065 if (pieChart) pieChart.destroy();1066 pieChart = new Chart(ctx, {1067 type: 'pie',1068 data: {1069 labels: sorted.map(c => c.name),1070 datasets: [{1071 data: sorted.map(c => c.total),1072 backgroundColor: sorted.map(c => c.color),1073 borderWidth: 2,1074 borderColor: '#fff',1075 }]1076 },1077 options: {1078 responsive: true,1079 maintainAspectRatio: false,1080 plugins: {1081 legend: {1082 position: 'bottom',1083 labels: {1084 usePointStyle: true,1085 pointStyle: 'circle',1086 padding: 16,1087 font: { size: 11 }1088 }1089 },1090 tooltip: {1091 callbacks: {1092 label: ctx => `${ctx.label}: ${formatCurrency(ctx.parsed)}`1093 }1094 }1095 }1096 }1097 });1098}1099 1100function renderReportSummary() {1101 const container = document.getElementById('reportSummary');1102 const transactions = getCurrentMonthTransactions();1103 const income = transactions.filter(t => t.type === 'income').reduce((s, t) => s + t.value, 0);1104 const expense = transactions.filter(t => t.type === 'expense').reduce((s, t) => s + t.value, 0);1105 const balance = income - expense;1106 const avgExpense = expense / 30;1107 const topCategory = (() => {1108 const catSpend = {};1109 transactions.filter(t => t.type === 'expense').forEach(t => {1110 catSpend[t.categoryId] = (catSpend[t.categoryId] || 0) + t.value;1111 });1112 const top = Object.entries(catSpend).sort((a, b) => b[1] - a[1])[0];1113 return top ? getCategoryById(parseInt(top[0])).name : '-';1114 })();1115 1116 const items = [1117 { label: 'Receita Total', value: formatCurrency(income), color: 'text-green-500', icon: 'trending-up' },1118 { label: 'Despesa Total', value: formatCurrency(expense), color: 'text-red-500', icon: 'trending-down' },1119 { label: 'Saldo do Mês', value: formatCurrency(balance), color: 'text-blue-500', icon: 'wallet' },1120 { label: 'Maior Gasto', value: topCategory, color: 'text-purple-500', icon: 'crown' },1121 ];1122 1123 container.innerHTML = items.map(item => `1124 <div class="bg-gray-50 rounded-xl p-4">1125 <div class="flex items-center gap-2 mb-2">1126 <i data-lucide="${item.icon}" class="w-4 h-4 ${item.color}"></i>1127 <span class="text-xs text-gray-400">${item.label}</span>1128 </div>1129 <p class="text-lg font-bold ${item.color}">${item.value}</p>1130 </div>`).join('');1131 lucide.createIcons();1132}1133 1134// ==========================================1135// INITIALIZATION1136// ==========================================1137document.addEventListener('DOMContentLoaded', () => {1138 lucide.createIcons();1139 1140 // Set current month1141 document.getElementById('monthSelector').value = appData.currentMonth;1142 1143 refreshDashboard();1144});