CoolFace
Apppublic

vsxprope/deepsite-project-bk2fy

sourceHugging Faceupdated 4mo agoView on Hugging Face
0likes
script.js1144 linesDownload Raw Back to root
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});