Compare commits
	
		
			13 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					96f20dc7e5 | ||
| 
						 | 
					2f3441d435 | ||
| 
						 | 
					89cac2cf8e | ||
| 
						 | 
					afeaf36479 | ||
| 
						 | 
					aac7fa0317 | ||
| 
						 | 
					880c892a70 | ||
| 
						 | 
					4a14f533d2 | ||
| 
						 | 
					f201c8edbd | ||
| 
						 | 
					33334980a6 | ||
| 
						 | 
					746c34879e | ||
| 
						 | 
					f599424ae9 | ||
| 
						 | 
					f3a05d4bb4 | ||
| 
						 | 
					eb43b636c0 | 
							
								
								
									
										36
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -1,36 +0,0 @@
 | 
			
		||||
# Logs
 | 
			
		||||
logs
 | 
			
		||||
*.log
 | 
			
		||||
npm-debug.log*
 | 
			
		||||
yarn-debug.log*
 | 
			
		||||
yarn-error.log*
 | 
			
		||||
pnpm-debug.log*
 | 
			
		||||
lerna-debug.log*
 | 
			
		||||
 | 
			
		||||
node_modules
 | 
			
		||||
.DS_Store
 | 
			
		||||
dist
 | 
			
		||||
dist-ssr
 | 
			
		||||
coverage
 | 
			
		||||
*.local
 | 
			
		||||
venv
 | 
			
		||||
# Editor directories and files
 | 
			
		||||
.vscode/*
 | 
			
		||||
!.vscode/extensions.json
 | 
			
		||||
.idea
 | 
			
		||||
*.suo
 | 
			
		||||
*.ntvs*
 | 
			
		||||
*.njsproj
 | 
			
		||||
*.sln
 | 
			
		||||
*.sw?
 | 
			
		||||
 | 
			
		||||
*.tsbuildinfo
 | 
			
		||||
 | 
			
		||||
.eslintcache
 | 
			
		||||
 | 
			
		||||
# Cypress
 | 
			
		||||
/cypress/videos/
 | 
			
		||||
/cypress/screenshots/
 | 
			
		||||
 | 
			
		||||
# Vitest
 | 
			
		||||
__screenshots__/
 | 
			
		||||
							
								
								
									
										3
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -1,3 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "recommendations": ["Vue.volar"]
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
from flask import Flask, request, jsonify
 | 
			
		||||
from flask import Flask, request, jsonify, send_file
 | 
			
		||||
from flask_cors import CORS
 | 
			
		||||
import sqlite3
 | 
			
		||||
from pymongo import MongoClient
 | 
			
		||||
from datetime import datetime, timedelta
 | 
			
		||||
import matplotlib
 | 
			
		||||
matplotlib.use('Agg')
 | 
			
		||||
import matplotlib.pyplot as plt
 | 
			
		||||
@@ -8,73 +9,39 @@ import base64
 | 
			
		||||
import io
 | 
			
		||||
import bcrypt
 | 
			
		||||
import os
 | 
			
		||||
from datetime import datetime, timedelta
 | 
			
		||||
from bson import ObjectId
 | 
			
		||||
import json
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
app = Flask(__name__)
 | 
			
		||||
CORS(app)
 | 
			
		||||
 | 
			
		||||
# Configuração do SQLite
 | 
			
		||||
DB_PATH = 'ctrlcash.db'
 | 
			
		||||
# Configurações do MongoDB
 | 
			
		||||
MONGO_URI = os.getenv('MONGO_URI', 'mongodb://localhost:27017/')
 | 
			
		||||
DB_NAME = 'CtrlCash'
 | 
			
		||||
 | 
			
		||||
# Funções do Banco de Dados
 | 
			
		||||
def init_db():
 | 
			
		||||
    """Inicializa o banco de dados e cria as tabelas"""
 | 
			
		||||
    conn = sqlite3.connect(DB_PATH)
 | 
			
		||||
    cursor = conn.cursor()
 | 
			
		||||
    
 | 
			
		||||
    # Tabela de usuários
 | 
			
		||||
    cursor.execute('''
 | 
			
		||||
        CREATE TABLE IF NOT EXISTS users (
 | 
			
		||||
            id INTEGER PRIMARY KEY AUTOINCREMENT,
 | 
			
		||||
            email TEXT UNIQUE NOT NULL,
 | 
			
		||||
            password TEXT NOT NULL,
 | 
			
		||||
            name TEXT NOT NULL,
 | 
			
		||||
            monthly_income_goal REAL DEFAULT 5000,
 | 
			
		||||
            monthly_expense_limit REAL DEFAULT 2500,
 | 
			
		||||
            currency TEXT DEFAULT 'BRL',
 | 
			
		||||
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
 | 
			
		||||
        )
 | 
			
		||||
    ''')
 | 
			
		||||
    
 | 
			
		||||
    # Tabela de categorias
 | 
			
		||||
    cursor.execute('''
 | 
			
		||||
        CREATE TABLE IF NOT EXISTS categories (
 | 
			
		||||
            id INTEGER PRIMARY KEY AUTOINCREMENT,
 | 
			
		||||
            user_id INTEGER NOT NULL,
 | 
			
		||||
            name TEXT NOT NULL,
 | 
			
		||||
            type TEXT NOT NULL,
 | 
			
		||||
            color TEXT NOT NULL,
 | 
			
		||||
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
 | 
			
		||||
            FOREIGN KEY (user_id) REFERENCES users (id),
 | 
			
		||||
            UNIQUE(user_id, name, type)
 | 
			
		||||
        )
 | 
			
		||||
    ''')
 | 
			
		||||
    
 | 
			
		||||
    # Tabela de transações
 | 
			
		||||
    cursor.execute('''
 | 
			
		||||
        CREATE TABLE IF NOT EXISTS transactions (
 | 
			
		||||
            id INTEGER PRIMARY KEY AUTOINCREMENT,
 | 
			
		||||
            user_id INTEGER NOT NULL,
 | 
			
		||||
            amount REAL NOT NULL,
 | 
			
		||||
            description TEXT NOT NULL,
 | 
			
		||||
            category TEXT NOT NULL,
 | 
			
		||||
            type TEXT NOT NULL,
 | 
			
		||||
            date DATE NOT NULL,
 | 
			
		||||
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
 | 
			
		||||
            FOREIGN KEY (user_id) REFERENCES users (id)
 | 
			
		||||
        )
 | 
			
		||||
    ''')
 | 
			
		||||
    
 | 
			
		||||
    conn.commit()
 | 
			
		||||
    conn.close()
 | 
			
		||||
# Conexão com MongoDB
 | 
			
		||||
try:
 | 
			
		||||
    client = MongoClient(MONGO_URI)
 | 
			
		||||
    db = client[DB_NAME]
 | 
			
		||||
    print(f"✅ Conectado ao MongoDB: {DB_NAME}")
 | 
			
		||||
except Exception as e:
 | 
			
		||||
    print(f"❌ Erro ao conectar com MongoDB: {e}")
 | 
			
		||||
    exit(1)
 | 
			
		||||
 | 
			
		||||
def get_db_connection():
 | 
			
		||||
    """Cria uma conexão com o banco de dados"""
 | 
			
		||||
    conn = sqlite3.connect(DB_PATH)
 | 
			
		||||
    conn.row_factory = sqlite3.Row  # Para retornar dicionários
 | 
			
		||||
    return conn
 | 
			
		||||
# Collections
 | 
			
		||||
users_collection = db['users']
 | 
			
		||||
transactions_collection = db['transactions']
 | 
			
		||||
categories_collection = db['categories']
 | 
			
		||||
 | 
			
		||||
class JSONEncoder(json.JSONEncoder):
 | 
			
		||||
    def default(self, obj):
 | 
			
		||||
        if isinstance(obj, ObjectId):
 | 
			
		||||
            return str(obj)
 | 
			
		||||
        if isinstance(obj, datetime):
 | 
			
		||||
            return obj.isoformat()
 | 
			
		||||
        return super().default(obj)
 | 
			
		||||
 | 
			
		||||
app.json_encoder = JSONEncoder
 | 
			
		||||
 | 
			
		||||
# Categorias padrão
 | 
			
		||||
default_categories = [
 | 
			
		||||
@@ -100,23 +67,49 @@ def check_password(password, hashed):
 | 
			
		||||
 | 
			
		||||
def init_default_categories(user_id):
 | 
			
		||||
    """Insere categorias padrão para um usuário"""
 | 
			
		||||
    conn = get_db_connection()
 | 
			
		||||
    cursor = conn.cursor()
 | 
			
		||||
    
 | 
			
		||||
    categories = []
 | 
			
		||||
    for cat in default_categories:
 | 
			
		||||
        cursor.execute('''
 | 
			
		||||
            INSERT OR IGNORE INTO categories (user_id, name, type, color)
 | 
			
		||||
            VALUES (?, ?, ?, ?)
 | 
			
		||||
        ''', (user_id, cat['name'], cat['type'], cat['color']))
 | 
			
		||||
        categories.append({
 | 
			
		||||
            **cat,
 | 
			
		||||
            "user_id": ObjectId(user_id),
 | 
			
		||||
            "created_at": datetime.now()
 | 
			
		||||
        })
 | 
			
		||||
    if categories:
 | 
			
		||||
        categories_collection.insert_many(categories)
 | 
			
		||||
@app.route('/')
 | 
			
		||||
def serve_html_home():
 | 
			
		||||
    return send_file('../web/index.html')
 | 
			
		||||
 | 
			
		||||
@app.route('/dashboard')
 | 
			
		||||
def serve_html_dashboard():
 | 
			
		||||
    return send_file('../web/index.html')
 | 
			
		||||
 | 
			
		||||
@app.route('/login')
 | 
			
		||||
def serve_html_login():
 | 
			
		||||
    return send_file('../web/index.html')
 | 
			
		||||
 | 
			
		||||
@app.route('/cadastro')
 | 
			
		||||
def serve_html_cadastro():
 | 
			
		||||
    return send_file('../web/index.html')
 | 
			
		||||
 | 
			
		||||
@app.route('/transacoes')
 | 
			
		||||
def serve_html_transacoes():
 | 
			
		||||
    return send_file('../web/index.html')
 | 
			
		||||
 | 
			
		||||
@app.route('/configuracoes')
 | 
			
		||||
def serve_html_configuracoes():
 | 
			
		||||
    return send_file('../web/index.html')
 | 
			
		||||
 | 
			
		||||
@app.route('/about')
 | 
			
		||||
def serve_html_about():
 | 
			
		||||
    return send_file('../web/index.html')
 | 
			
		||||
    
 | 
			
		||||
    conn.commit()
 | 
			
		||||
    conn.close()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.route('/help')
 | 
			
		||||
def serve_html_help():
 | 
			
		||||
    return send_file('../web/index.html')
 | 
			
		||||
# ROTAS DE AUTENTICAÇÃO
 | 
			
		||||
@app.route('/api/auth/register', methods=['POST'])
 | 
			
		||||
def register():
 | 
			
		||||
def register(): 
 | 
			
		||||
    try:
 | 
			
		||||
        data = request.get_json()
 | 
			
		||||
        email = data.get('email', '').strip().lower()
 | 
			
		||||
@@ -129,40 +122,36 @@ def register():
 | 
			
		||||
        if len(password) < 6:
 | 
			
		||||
            return jsonify({"error": "A senha deve ter pelo menos 6 caracteres"}), 400
 | 
			
		||||
        
 | 
			
		||||
        conn = get_db_connection()
 | 
			
		||||
        cursor = conn.cursor()
 | 
			
		||||
        
 | 
			
		||||
        # Verificar se usuário já existe
 | 
			
		||||
        cursor.execute('SELECT id FROM users WHERE email = ?', (email,))
 | 
			
		||||
        if cursor.fetchone():
 | 
			
		||||
            conn.close()
 | 
			
		||||
        if users_collection.find_one({"email": email}):
 | 
			
		||||
            return jsonify({"error": "Email já cadastrado"}), 400
 | 
			
		||||
        
 | 
			
		||||
        # Criar usuário
 | 
			
		||||
        cursor.execute('''
 | 
			
		||||
            INSERT INTO users (email, password, name)
 | 
			
		||||
            VALUES (?, ?, ?)
 | 
			
		||||
        ''', (email, hash_password(password), name))
 | 
			
		||||
        user_data = {
 | 
			
		||||
            'email': email,
 | 
			
		||||
            'password': hash_password(password),
 | 
			
		||||
            'name': name,
 | 
			
		||||
            'created_at': datetime.now(),
 | 
			
		||||
            'profile': {
 | 
			
		||||
                'monthly_income_goal': 5000,
 | 
			
		||||
                'monthly_expense_limit': 2500,
 | 
			
		||||
                'currency': 'BRL'
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        user_id = cursor.lastrowid
 | 
			
		||||
        result = users_collection.insert_one(user_data)
 | 
			
		||||
        user_id = result.inserted_id
 | 
			
		||||
        
 | 
			
		||||
        conn.commit()
 | 
			
		||||
        conn.close()
 | 
			
		||||
        
 | 
			
		||||
        # Inicializar categorias e dados mock
 | 
			
		||||
        # Inicializar categorias
 | 
			
		||||
        init_default_categories(user_id)
 | 
			
		||||
        
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            "message": "Usuário criado com sucesso",
 | 
			
		||||
            "user": {
 | 
			
		||||
                "id": user_id,
 | 
			
		||||
                "id": str(user_id),
 | 
			
		||||
                "email": email,
 | 
			
		||||
                "name": name,
 | 
			
		||||
                "profile": {
 | 
			
		||||
                    "monthly_income_goal": 5000,
 | 
			
		||||
                    "monthly_expense_limit": 2500,
 | 
			
		||||
                    "currency": "BRL"
 | 
			
		||||
                }
 | 
			
		||||
                "profile": user_data['profile']
 | 
			
		||||
            }
 | 
			
		||||
        }), 201
 | 
			
		||||
        
 | 
			
		||||
@@ -180,16 +169,7 @@ def login():
 | 
			
		||||
        if not email or not password:
 | 
			
		||||
            return jsonify({"error": "Email e senha são obrigatórios"}), 400
 | 
			
		||||
        
 | 
			
		||||
        conn = get_db_connection()
 | 
			
		||||
        cursor = conn.cursor()
 | 
			
		||||
        
 | 
			
		||||
        cursor.execute('''
 | 
			
		||||
            SELECT id, email, password, name, monthly_income_goal, monthly_expense_limit, currency 
 | 
			
		||||
            FROM users WHERE email = ?
 | 
			
		||||
        ''', (email,))
 | 
			
		||||
        
 | 
			
		||||
        user = cursor.fetchone()
 | 
			
		||||
        conn.close()
 | 
			
		||||
        user = users_collection.find_one({"email": email})
 | 
			
		||||
        
 | 
			
		||||
        if not user or not check_password(password, user['password']):
 | 
			
		||||
            return jsonify({"error": "Credenciais inválidas"}), 401
 | 
			
		||||
@@ -197,14 +177,10 @@ def login():
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            "message": "Login realizado com sucesso",
 | 
			
		||||
            "user": {
 | 
			
		||||
                "id": user['id'],
 | 
			
		||||
                "id": str(user['_id']),
 | 
			
		||||
                "email": user['email'],
 | 
			
		||||
                "name": user['name'],
 | 
			
		||||
                "profile": {
 | 
			
		||||
                    "monthly_income_goal": user['monthly_income_goal'],
 | 
			
		||||
                    "monthly_expense_limit": user['monthly_expense_limit'],
 | 
			
		||||
                    "currency": user['currency']
 | 
			
		||||
                }
 | 
			
		||||
                "profile": user.get('profile', {})
 | 
			
		||||
            }
 | 
			
		||||
        }), 200
 | 
			
		||||
        
 | 
			
		||||
@@ -220,29 +196,16 @@ def get_transactions():
 | 
			
		||||
        if not user_id:
 | 
			
		||||
            return jsonify({"transactions": []}), 200
 | 
			
		||||
        
 | 
			
		||||
        conn = get_db_connection()
 | 
			
		||||
        cursor = conn.cursor()
 | 
			
		||||
        transactions = list(transactions_collection.find(
 | 
			
		||||
            {"user_id": ObjectId(user_id)}
 | 
			
		||||
        ).sort("date", -1))
 | 
			
		||||
        
 | 
			
		||||
        cursor.execute('''
 | 
			
		||||
            SELECT id, user_id, amount, description, category, type, date
 | 
			
		||||
            FROM transactions 
 | 
			
		||||
            WHERE user_id = ?
 | 
			
		||||
            ORDER BY date DESC
 | 
			
		||||
        ''', (user_id,))
 | 
			
		||||
        # Converter ObjectId para string
 | 
			
		||||
        for transaction in transactions:
 | 
			
		||||
            transaction['id'] = str(transaction['_id'])
 | 
			
		||||
            transaction['user_id'] = str(transaction['user_id'])
 | 
			
		||||
            del transaction['_id']
 | 
			
		||||
        
 | 
			
		||||
        transactions = []
 | 
			
		||||
        for row in cursor.fetchall():
 | 
			
		||||
            transactions.append({
 | 
			
		||||
                "id": row['id'],
 | 
			
		||||
                "user_id": row['user_id'],
 | 
			
		||||
                "amount": row['amount'],
 | 
			
		||||
                "description": row['description'],
 | 
			
		||||
                "category": row['category'],
 | 
			
		||||
                "type": row['type'],
 | 
			
		||||
                "date": row['date']
 | 
			
		||||
            })
 | 
			
		||||
        
 | 
			
		||||
        conn.close()
 | 
			
		||||
        return jsonify({"transactions": transactions}), 200
 | 
			
		||||
        
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
@@ -271,78 +234,49 @@ def add_transaction():
 | 
			
		||||
        if not description:
 | 
			
		||||
            return jsonify({"error": "Descrição é obrigatória"}), 400
 | 
			
		||||
        
 | 
			
		||||
        # ✅ CORREÇÃO: Usar a data EXATA que veio do frontend
 | 
			
		||||
        # Não converter para datetime, usar como string mesmo
 | 
			
		||||
        # Usar data exata do frontend
 | 
			
		||||
        if not date_str:
 | 
			
		||||
            date_str = datetime.now().strftime('%Y-%m-%d')
 | 
			
		||||
        
 | 
			
		||||
        # Validar formato da data (YYYY-MM-DD)
 | 
			
		||||
        try:
 | 
			
		||||
            datetime.strptime(date_str, '%Y-%m-%d')
 | 
			
		||||
        except ValueError:
 | 
			
		||||
            return jsonify({"error": "Formato de data inválido. Use YYYY-MM-DD"}), 400
 | 
			
		||||
        new_transaction = {
 | 
			
		||||
            "user_id": ObjectId(user_id),
 | 
			
		||||
            "amount": amount,
 | 
			
		||||
            "description": description,
 | 
			
		||||
            "category": category,
 | 
			
		||||
            "type": transaction_type,
 | 
			
		||||
            "date": date_str,
 | 
			
		||||
            "created_at": datetime.now()
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        conn = get_db_connection()
 | 
			
		||||
        cursor = conn.cursor()
 | 
			
		||||
        
 | 
			
		||||
        # ✅ CORREÇÃO: Inserir a data EXATA como string
 | 
			
		||||
        cursor.execute('''
 | 
			
		||||
            INSERT INTO transactions (user_id, amount, description, category, type, date)
 | 
			
		||||
            VALUES (?, ?, ?, ?, ?, ?)
 | 
			
		||||
        ''', (user_id, amount, description, category, transaction_type, date_str))
 | 
			
		||||
        
 | 
			
		||||
        transaction_id = cursor.lastrowid
 | 
			
		||||
        
 | 
			
		||||
        # Buscar transação criada
 | 
			
		||||
        cursor.execute('SELECT * FROM transactions WHERE id = ?', (transaction_id,))
 | 
			
		||||
        new_transaction = cursor.fetchone()
 | 
			
		||||
        
 | 
			
		||||
        conn.commit()
 | 
			
		||||
        conn.close()
 | 
			
		||||
        result = transactions_collection.insert_one(new_transaction)
 | 
			
		||||
        new_transaction['id'] = str(result.inserted_id)
 | 
			
		||||
        new_transaction['user_id'] = user_id
 | 
			
		||||
        del new_transaction['_id']
 | 
			
		||||
        
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            "message": "Transação adicionada com sucesso",
 | 
			
		||||
            "transaction": {
 | 
			
		||||
                "id": new_transaction['id'],
 | 
			
		||||
                "user_id": new_transaction['user_id'],
 | 
			
		||||
                "amount": new_transaction['amount'],
 | 
			
		||||
                "description": new_transaction['description'],
 | 
			
		||||
                "category": new_transaction['category'],
 | 
			
		||||
                "type": new_transaction['type'],
 | 
			
		||||
                "date": new_transaction['date']  # ✅ Já vem correto do banco
 | 
			
		||||
            }
 | 
			
		||||
            "transaction": new_transaction
 | 
			
		||||
        }), 201
 | 
			
		||||
        
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        print(f"Erro ao adicionar transação: {e}")
 | 
			
		||||
        return jsonify({"error": "Erro ao adicionar transação"}), 500
 | 
			
		||||
        
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        print(f"Erro ao adicionar transação: {e}")
 | 
			
		||||
        return jsonify({"error": "Erro ao adicionar transação"}), 500
 | 
			
		||||
 | 
			
		||||
@app.route('/api/transactions/<int:transaction_id>', methods=['DELETE'])
 | 
			
		||||
@app.route('/api/transactions/<transaction_id>', methods=['DELETE'])
 | 
			
		||||
def delete_transaction(transaction_id):
 | 
			
		||||
    try:
 | 
			
		||||
        user_id = request.args.get('user_id')
 | 
			
		||||
        if not user_id:
 | 
			
		||||
            return jsonify({"error": "User ID é obrigatório"}), 400
 | 
			
		||||
        
 | 
			
		||||
        conn = get_db_connection()
 | 
			
		||||
        cursor = conn.cursor()
 | 
			
		||||
        result = transactions_collection.delete_one({
 | 
			
		||||
            "_id": ObjectId(transaction_id),
 | 
			
		||||
            "user_id": ObjectId(user_id)
 | 
			
		||||
        })
 | 
			
		||||
        
 | 
			
		||||
        cursor.execute('''
 | 
			
		||||
            DELETE FROM transactions 
 | 
			
		||||
            WHERE id = ? AND user_id = ?
 | 
			
		||||
        ''', (transaction_id, user_id))
 | 
			
		||||
        
 | 
			
		||||
        if cursor.rowcount == 0:
 | 
			
		||||
            conn.close()
 | 
			
		||||
        if result.deleted_count == 0:
 | 
			
		||||
            return jsonify({"error": "Transação não encontrada"}), 404
 | 
			
		||||
        
 | 
			
		||||
        conn.commit()
 | 
			
		||||
        conn.close()
 | 
			
		||||
        
 | 
			
		||||
        return jsonify({"message": "Transação deletada com sucesso"}), 200
 | 
			
		||||
        
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
@@ -357,26 +291,11 @@ def get_categories():
 | 
			
		||||
        
 | 
			
		||||
        if user_id:
 | 
			
		||||
            # Buscar categorias do usuário
 | 
			
		||||
            conn = get_db_connection()
 | 
			
		||||
            cursor = conn.cursor()
 | 
			
		||||
            
 | 
			
		||||
            cursor.execute('''
 | 
			
		||||
                SELECT id, user_id, name, type, color
 | 
			
		||||
                FROM categories 
 | 
			
		||||
                WHERE user_id = ?
 | 
			
		||||
            ''', (user_id,))
 | 
			
		||||
            
 | 
			
		||||
            categories = []
 | 
			
		||||
            for row in cursor.fetchall():
 | 
			
		||||
                categories.append({
 | 
			
		||||
                    "id": row['id'],
 | 
			
		||||
                    "user_id": row['user_id'],
 | 
			
		||||
                    "name": row['name'],
 | 
			
		||||
                    "type": row['type'],
 | 
			
		||||
                    "color": row['color']
 | 
			
		||||
                })
 | 
			
		||||
            
 | 
			
		||||
            conn.close()
 | 
			
		||||
            categories = list(categories_collection.find({"user_id": ObjectId(user_id)}))
 | 
			
		||||
            for category in categories:
 | 
			
		||||
                category['id'] = str(category['_id'])
 | 
			
		||||
                category['user_id'] = str(category['user_id'])
 | 
			
		||||
                del category['_id']
 | 
			
		||||
        else:
 | 
			
		||||
            # Retornar categorias padrão
 | 
			
		||||
            categories = [{"id": i, **cat} for i, cat in enumerate(default_categories)]
 | 
			
		||||
@@ -405,39 +324,32 @@ def add_category():
 | 
			
		||||
        if not category_type or category_type not in ['income', 'expense']:
 | 
			
		||||
            return jsonify({"error": "Tipo de categoria inválido"}), 400
 | 
			
		||||
        
 | 
			
		||||
        conn = get_db_connection()
 | 
			
		||||
        cursor = conn.cursor()
 | 
			
		||||
        
 | 
			
		||||
        # Verificar se categoria já existe
 | 
			
		||||
        cursor.execute('''
 | 
			
		||||
            SELECT id FROM categories 
 | 
			
		||||
            WHERE user_id = ? AND name = ? AND type = ?
 | 
			
		||||
        ''', (user_id, name, category_type))
 | 
			
		||||
        existing_category = categories_collection.find_one({
 | 
			
		||||
            "user_id": ObjectId(user_id),
 | 
			
		||||
            "name": name,
 | 
			
		||||
            "type": category_type
 | 
			
		||||
        })
 | 
			
		||||
        
 | 
			
		||||
        if cursor.fetchone():
 | 
			
		||||
            conn.close()
 | 
			
		||||
        if existing_category:
 | 
			
		||||
            return jsonify({"error": "Categoria já existe"}), 400
 | 
			
		||||
        
 | 
			
		||||
        # Inserir nova categoria
 | 
			
		||||
        cursor.execute('''
 | 
			
		||||
            INSERT INTO categories (user_id, name, type, color)
 | 
			
		||||
            VALUES (?, ?, ?, ?)
 | 
			
		||||
        ''', (user_id, name, category_type, color))
 | 
			
		||||
        new_category = {
 | 
			
		||||
            "user_id": ObjectId(user_id),
 | 
			
		||||
            "name": name,
 | 
			
		||||
            "type": category_type,
 | 
			
		||||
            "color": color,
 | 
			
		||||
            "created_at": datetime.now()
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        category_id = cursor.lastrowid
 | 
			
		||||
        
 | 
			
		||||
        conn.commit()
 | 
			
		||||
        conn.close()
 | 
			
		||||
        result = categories_collection.insert_one(new_category)
 | 
			
		||||
        new_category['id'] = str(result.inserted_id)
 | 
			
		||||
        new_category['user_id'] = user_id
 | 
			
		||||
        del new_category['_id']
 | 
			
		||||
        
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            "message": "Categoria adicionada com sucesso",
 | 
			
		||||
            "category": {
 | 
			
		||||
                "id": category_id,
 | 
			
		||||
                "user_id": user_id,
 | 
			
		||||
                "name": name,
 | 
			
		||||
                "type": category_type,
 | 
			
		||||
                "color": color
 | 
			
		||||
            }
 | 
			
		||||
            "category": new_category
 | 
			
		||||
        }), 201
 | 
			
		||||
        
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
@@ -457,53 +369,45 @@ def get_dashboard_summary():
 | 
			
		||||
                "recent_transactions": []
 | 
			
		||||
            }), 200
 | 
			
		||||
        
 | 
			
		||||
        conn = get_db_connection()
 | 
			
		||||
        cursor = conn.cursor()
 | 
			
		||||
        
 | 
			
		||||
        # Calcular totais do mês atual
 | 
			
		||||
        first_day_of_month = datetime.now().replace(day=1).strftime('%Y-%m-%d')
 | 
			
		||||
        start_of_month = datetime.now().replace(day=1, hour=0, minute=0, second=0, microsecond=0)
 | 
			
		||||
        
 | 
			
		||||
        # Receitas do mês
 | 
			
		||||
        cursor.execute('''
 | 
			
		||||
            SELECT COALESCE(SUM(amount), 0) as total
 | 
			
		||||
            FROM transactions 
 | 
			
		||||
            WHERE user_id = ? AND type = 'income' AND date >= ?
 | 
			
		||||
        ''', (user_id, first_day_of_month))
 | 
			
		||||
        # Pipeline para receitas do mês
 | 
			
		||||
        pipeline_income = [
 | 
			
		||||
            {"$match": {
 | 
			
		||||
                "user_id": ObjectId(user_id),
 | 
			
		||||
                "type": "income",
 | 
			
		||||
                "date": {"$gte": start_of_month.strftime('%Y-%m-%d')}
 | 
			
		||||
            }},
 | 
			
		||||
            {"$group": {"_id": None, "total": {"$sum": "$amount"}}}
 | 
			
		||||
        ]
 | 
			
		||||
        
 | 
			
		||||
        total_income = cursor.fetchone()['total'] or 0
 | 
			
		||||
        # Pipeline para despesas do mês
 | 
			
		||||
        pipeline_expenses = [
 | 
			
		||||
            {"$match": {
 | 
			
		||||
                "user_id": ObjectId(user_id),
 | 
			
		||||
                "type": "expense",
 | 
			
		||||
                "date": {"$gte": start_of_month.strftime('%Y-%m-%d')}
 | 
			
		||||
            }},
 | 
			
		||||
            {"$group": {"_id": None, "total": {"$sum": "$amount"}}}
 | 
			
		||||
        ]
 | 
			
		||||
        
 | 
			
		||||
        # Despesas do mês
 | 
			
		||||
        cursor.execute('''
 | 
			
		||||
            SELECT COALESCE(SUM(amount), 0) as total
 | 
			
		||||
            FROM transactions 
 | 
			
		||||
            WHERE user_id = ? AND type = 'expense' AND date >= ?
 | 
			
		||||
        ''', (user_id, first_day_of_month))
 | 
			
		||||
        income_result = list(transactions_collection.aggregate(pipeline_income))
 | 
			
		||||
        expense_result = list(transactions_collection.aggregate(pipeline_expenses))
 | 
			
		||||
        
 | 
			
		||||
        total_expenses = cursor.fetchone()['total'] or 0
 | 
			
		||||
        total_income = income_result[0]['total'] if income_result else 0
 | 
			
		||||
        total_expenses = expense_result[0]['total'] if expense_result else 0
 | 
			
		||||
        balance = total_income - total_expenses
 | 
			
		||||
        
 | 
			
		||||
        # Últimas 5 transações
 | 
			
		||||
        cursor.execute('''
 | 
			
		||||
            SELECT id, user_id, amount, description, category, type, date
 | 
			
		||||
            FROM transactions 
 | 
			
		||||
            WHERE user_id = ?
 | 
			
		||||
            ORDER BY date DESC
 | 
			
		||||
            LIMIT 5
 | 
			
		||||
        ''', (user_id,))
 | 
			
		||||
        recent_transactions = list(transactions_collection.find(
 | 
			
		||||
            {"user_id": ObjectId(user_id)}
 | 
			
		||||
        ).sort("date", -1).limit(5))
 | 
			
		||||
        
 | 
			
		||||
        recent_transactions = []
 | 
			
		||||
        for row in cursor.fetchall():
 | 
			
		||||
            recent_transactions.append({
 | 
			
		||||
                "id": row['id'],
 | 
			
		||||
                "user_id": row['user_id'],
 | 
			
		||||
                "amount": row['amount'],
 | 
			
		||||
                "description": row['description'],
 | 
			
		||||
                "category": row['category'],
 | 
			
		||||
                "type": row['type'],
 | 
			
		||||
                "date": row['date']
 | 
			
		||||
            })
 | 
			
		||||
        
 | 
			
		||||
        conn.close()
 | 
			
		||||
        for transaction in recent_transactions:
 | 
			
		||||
            transaction['id'] = str(transaction['_id'])
 | 
			
		||||
            transaction['user_id'] = str(transaction['user_id'])
 | 
			
		||||
            del transaction['_id']
 | 
			
		||||
        
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            "total_income": round(total_income, 2),
 | 
			
		||||
@@ -530,38 +434,32 @@ def get_chart():
 | 
			
		||||
        if not user_id:
 | 
			
		||||
            return jsonify({"error": "User ID é obrigatório"}), 400
 | 
			
		||||
        
 | 
			
		||||
        conn = get_db_connection()
 | 
			
		||||
        cursor = conn.cursor()
 | 
			
		||||
        
 | 
			
		||||
        plt.figure(figsize=(10, 6))
 | 
			
		||||
        
 | 
			
		||||
        if chart_type == 'monthly':
 | 
			
		||||
            # Últimos 6 meses
 | 
			
		||||
            six_months_ago = (datetime.now() - timedelta(days=180)).strftime('%Y-%m-%d')
 | 
			
		||||
            six_months_ago = datetime.now() - timedelta(days=180)
 | 
			
		||||
            
 | 
			
		||||
            cursor.execute('''
 | 
			
		||||
                SELECT 
 | 
			
		||||
                    strftime('%Y-%m', date) as month,
 | 
			
		||||
                    SUM(CASE WHEN type = 'income' THEN amount ELSE 0 END) as income,
 | 
			
		||||
                    SUM(CASE WHEN type = 'expense' THEN amount ELSE 0 END) as expenses
 | 
			
		||||
                FROM transactions 
 | 
			
		||||
                WHERE user_id = ? AND date >= ?
 | 
			
		||||
                GROUP BY strftime('%Y-%m', date)
 | 
			
		||||
                ORDER BY month DESC
 | 
			
		||||
                LIMIT 6
 | 
			
		||||
            ''', (user_id, six_months_ago))
 | 
			
		||||
            pipeline = [
 | 
			
		||||
                {"$match": {
 | 
			
		||||
                    "user_id": ObjectId(user_id),
 | 
			
		||||
                    "date": {"$gte": six_months_ago.strftime('%Y-%m-%d')}
 | 
			
		||||
                }},
 | 
			
		||||
                {"$group": {
 | 
			
		||||
                    "_id": {"$substr": ["$date", 0, 7]},  # Extrair YYYY-MM
 | 
			
		||||
                    "income": {"$sum": {"$cond": [{"$eq": ["$type", "income"]}, "$amount", 0]}},
 | 
			
		||||
                    "expenses": {"$sum": {"$cond": [{"$eq": ["$type", "expense"]}, "$amount", 0]}}
 | 
			
		||||
                }},
 | 
			
		||||
                {"$sort": {"_id": 1}},
 | 
			
		||||
                {"$limit": 6}
 | 
			
		||||
            ]
 | 
			
		||||
            
 | 
			
		||||
            result = cursor.fetchall()
 | 
			
		||||
            result = list(transactions_collection.aggregate(pipeline))
 | 
			
		||||
            
 | 
			
		||||
            if result:
 | 
			
		||||
                months = [row['month'] for row in result]
 | 
			
		||||
                income = [row['income'] for row in result]
 | 
			
		||||
                expenses = [row['expenses'] for row in result]
 | 
			
		||||
                
 | 
			
		||||
                # Reverter para ordem cronológica
 | 
			
		||||
                months.reverse()
 | 
			
		||||
                income.reverse()
 | 
			
		||||
                expenses.reverse()
 | 
			
		||||
                months = [r['_id'] for r in result]
 | 
			
		||||
                income = [r['income'] for r in result]
 | 
			
		||||
                expenses = [r['expenses'] for r in result]
 | 
			
		||||
                
 | 
			
		||||
                x = range(len(months))
 | 
			
		||||
                width = 0.35
 | 
			
		||||
@@ -580,20 +478,25 @@ def get_chart():
 | 
			
		||||
            
 | 
			
		||||
        elif chart_type == 'categories':
 | 
			
		||||
            # Gastos por categoria (últimos 30 dias)
 | 
			
		||||
            thirty_days_ago = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d')
 | 
			
		||||
            thirty_days_ago = datetime.now() - timedelta(days=30)
 | 
			
		||||
            
 | 
			
		||||
            cursor.execute('''
 | 
			
		||||
                SELECT category, SUM(amount) as total
 | 
			
		||||
                FROM transactions 
 | 
			
		||||
                WHERE user_id = ? AND type = 'expense' AND date >= ?
 | 
			
		||||
                GROUP BY category
 | 
			
		||||
            ''', (user_id, thirty_days_ago))
 | 
			
		||||
            pipeline = [
 | 
			
		||||
                {"$match": {
 | 
			
		||||
                    "user_id": ObjectId(user_id),
 | 
			
		||||
                    "type": "expense",
 | 
			
		||||
                    "date": {"$gte": thirty_days_ago.strftime('%Y-%m-%d')}
 | 
			
		||||
                }},
 | 
			
		||||
                {"$group": {
 | 
			
		||||
                    "_id": "$category",
 | 
			
		||||
                    "total": {"$sum": "$amount"}
 | 
			
		||||
                }}
 | 
			
		||||
            ]
 | 
			
		||||
            
 | 
			
		||||
            result = cursor.fetchall()
 | 
			
		||||
            result = list(transactions_collection.aggregate(pipeline))
 | 
			
		||||
            
 | 
			
		||||
            if result:
 | 
			
		||||
                categories = [row['category'] for row in result]
 | 
			
		||||
                amounts = [row['total'] for row in result]
 | 
			
		||||
                categories = [r['_id'] for r in result]
 | 
			
		||||
                amounts = [r['total'] for r in result]
 | 
			
		||||
                
 | 
			
		||||
                plt.pie(amounts, labels=categories, autopct='%1.1f%%', startangle=90)
 | 
			
		||||
                plt.title('Distribuição de Gastos por Categoria (Últimos 30 dias)')
 | 
			
		||||
@@ -601,8 +504,6 @@ def get_chart():
 | 
			
		||||
                plt.text(0.5, 0.5, 'Sem dados de gastos', 
 | 
			
		||||
                        ha='center', va='center', transform=plt.gca().transAxes)
 | 
			
		||||
        
 | 
			
		||||
        conn.close()
 | 
			
		||||
        
 | 
			
		||||
        # Converter para base64
 | 
			
		||||
        buffer = io.BytesIO()
 | 
			
		||||
        plt.savefig(buffer, format='png', dpi=100, bbox_inches='tight')
 | 
			
		||||
@@ -626,30 +527,16 @@ def get_profile():
 | 
			
		||||
        if not user_id:
 | 
			
		||||
            return jsonify({"error": "User ID é obrigatório"}), 400
 | 
			
		||||
        
 | 
			
		||||
        conn = get_db_connection()
 | 
			
		||||
        cursor = conn.cursor()
 | 
			
		||||
        
 | 
			
		||||
        cursor.execute('''
 | 
			
		||||
            SELECT id, email, name, monthly_income_goal, monthly_expense_limit, currency
 | 
			
		||||
            FROM users WHERE id = ?
 | 
			
		||||
        ''', (user_id,))
 | 
			
		||||
        
 | 
			
		||||
        user = cursor.fetchone()
 | 
			
		||||
        conn.close()
 | 
			
		||||
        
 | 
			
		||||
        user = users_collection.find_one({"_id": ObjectId(user_id)})
 | 
			
		||||
        if not user:
 | 
			
		||||
            return jsonify({"error": "Usuário não encontrado"}), 404
 | 
			
		||||
        
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            "user": {
 | 
			
		||||
                "id": user['id'],
 | 
			
		||||
                "id": str(user['_id']),
 | 
			
		||||
                "name": user['name'],
 | 
			
		||||
                "email": user['email'],
 | 
			
		||||
                "profile": {
 | 
			
		||||
                    "monthly_income_goal": user['monthly_income_goal'],
 | 
			
		||||
                    "monthly_expense_limit": user['monthly_expense_limit'],
 | 
			
		||||
                    "currency": user['currency']
 | 
			
		||||
                }
 | 
			
		||||
                "profile": user.get('profile', {})
 | 
			
		||||
            }
 | 
			
		||||
        }), 200
 | 
			
		||||
        
 | 
			
		||||
@@ -672,29 +559,29 @@ def update_profile():
 | 
			
		||||
        if not name:
 | 
			
		||||
            return jsonify({"error": "Nome é obrigatório"}), 400
 | 
			
		||||
        
 | 
			
		||||
        conn = get_db_connection()
 | 
			
		||||
        cursor = conn.cursor()
 | 
			
		||||
        
 | 
			
		||||
        # Verificar se email já existe (para outro usuário)
 | 
			
		||||
        cursor.execute('SELECT id FROM users WHERE email = ? AND id != ?', (email, user_id))
 | 
			
		||||
        if cursor.fetchone():
 | 
			
		||||
            conn.close()
 | 
			
		||||
        existing_user = users_collection.find_one({
 | 
			
		||||
            "email": email,
 | 
			
		||||
            "_id": {"$ne": ObjectId(user_id)}
 | 
			
		||||
        })
 | 
			
		||||
        
 | 
			
		||||
        if existing_user:
 | 
			
		||||
            return jsonify({"error": "Email já está em uso"}), 400
 | 
			
		||||
        
 | 
			
		||||
        # Atualizar perfil
 | 
			
		||||
        cursor.execute('''
 | 
			
		||||
            UPDATE users 
 | 
			
		||||
            SET name = ?, email = ?, monthly_income_goal = ?, monthly_expense_limit = ?
 | 
			
		||||
            WHERE id = ?
 | 
			
		||||
        ''', (name, email, profile.get('monthly_income_goal', 5000), profile.get('monthly_expense_limit', 2500), user_id))
 | 
			
		||||
        update_data = {
 | 
			
		||||
            "name": name,
 | 
			
		||||
            "email": email,
 | 
			
		||||
            "profile": profile
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        if cursor.rowcount == 0:
 | 
			
		||||
            conn.close()
 | 
			
		||||
        result = users_collection.update_one(
 | 
			
		||||
            {"_id": ObjectId(user_id)},
 | 
			
		||||
            {"$set": update_data}
 | 
			
		||||
        )
 | 
			
		||||
        
 | 
			
		||||
        if result.modified_count == 0:
 | 
			
		||||
            return jsonify({"error": "Nenhuma alteração realizada"}), 400
 | 
			
		||||
        
 | 
			
		||||
        conn.commit()
 | 
			
		||||
        conn.close()
 | 
			
		||||
        
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            "message": "Perfil atualizado com sucesso",
 | 
			
		||||
            "user": {
 | 
			
		||||
@@ -720,27 +607,20 @@ def change_password():
 | 
			
		||||
        if not user_id:
 | 
			
		||||
            return jsonify({"error": "User ID é obrigatório"}), 400
 | 
			
		||||
        
 | 
			
		||||
        conn = get_db_connection()
 | 
			
		||||
        cursor = conn.cursor()
 | 
			
		||||
        
 | 
			
		||||
        cursor.execute('SELECT password FROM users WHERE id = ?', (user_id,))
 | 
			
		||||
        user = cursor.fetchone()
 | 
			
		||||
        
 | 
			
		||||
        user = users_collection.find_one({"_id": ObjectId(user_id)})
 | 
			
		||||
        if not user:
 | 
			
		||||
            conn.close()
 | 
			
		||||
            return jsonify({"error": "Usuário não encontrado"}), 404
 | 
			
		||||
        
 | 
			
		||||
        if not check_password(current_password, user['password']):
 | 
			
		||||
            conn.close()
 | 
			
		||||
            return jsonify({"error": "Senha atual incorreta"}), 400
 | 
			
		||||
        
 | 
			
		||||
        if len(new_password) < 6:
 | 
			
		||||
            conn.close()
 | 
			
		||||
            return jsonify({"error": "A nova senha deve ter pelo menos 6 caracteres"}), 400
 | 
			
		||||
        
 | 
			
		||||
        cursor.execute('UPDATE users SET password = ? WHERE id = ?', (hash_password(new_password), user_id))
 | 
			
		||||
        conn.commit()
 | 
			
		||||
        conn.close()
 | 
			
		||||
        users_collection.update_one(
 | 
			
		||||
            {"_id": ObjectId(user_id)},
 | 
			
		||||
            {"$set": {"password": hash_password(new_password)}}
 | 
			
		||||
        )
 | 
			
		||||
        
 | 
			
		||||
        return jsonify({"message": "Senha alterada com sucesso"}), 200
 | 
			
		||||
        
 | 
			
		||||
@@ -752,21 +632,17 @@ def change_password():
 | 
			
		||||
@app.route('/api/health', methods=['GET'])
 | 
			
		||||
def health_check():
 | 
			
		||||
    try:
 | 
			
		||||
        # Testar conexão com SQLite
 | 
			
		||||
        conn = get_db_connection()
 | 
			
		||||
        cursor = conn.cursor()
 | 
			
		||||
        cursor.execute('SELECT 1')
 | 
			
		||||
        conn.close()
 | 
			
		||||
        
 | 
			
		||||
        # Testar conexão com MongoDB
 | 
			
		||||
        client.admin.command('ismaster')
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            "status": "OK", 
 | 
			
		||||
            "message": "Backend e SQLite funcionando",
 | 
			
		||||
            "message": "Backend e MongoDB funcionando",
 | 
			
		||||
            "timestamp": datetime.now().isoformat()
 | 
			
		||||
        }), 200
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            "status": "ERROR",
 | 
			
		||||
            "message": f"Erro no SQLite: {e}"
 | 
			
		||||
            "message": f"Erro no MongoDB: {e}"
 | 
			
		||||
        }), 500
 | 
			
		||||
 | 
			
		||||
@app.route('/')
 | 
			
		||||
@@ -777,12 +653,8 @@ def index():
 | 
			
		||||
        "timestamp": datetime.now().isoformat()
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
# Inicializar o banco de dados quando o app iniciar
 | 
			
		||||
init_db()
 | 
			
		||||
print("✅ Banco de dados SQLite inicializado!")
 | 
			
		||||
 | 
			
		||||
if __name__ == '__main__':
 | 
			
		||||
    print("🚀 Iniciando CtrlCash API com SQLite...")
 | 
			
		||||
    print(f"💾 Database: {DB_PATH}")
 | 
			
		||||
    app.run(debug=False, host='0.0.0.0', port=5000)
 | 
			
		||||
 | 
			
		||||
    print("🚀 Iniciando CtrlCash API com MongoDB...")
 | 
			
		||||
    print(f"📊 MongoDB: {MONGO_URI}")
 | 
			
		||||
    print(f"💾 Database: {DB_NAME}")
 | 
			
		||||
    app.run(debug=False, host='0.0.0.0', port=5000)
 | 
			
		||||
@@ -1,8 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    "paths": {
 | 
			
		||||
      "@/*": ["./src/*"]
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "exclude": ["node_modules", "dist"]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										2561
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
							
								
								
									
										23
									
								
								package.json
									
									
									
									
									
								
							
							
						
						@@ -1,23 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "vue",
 | 
			
		||||
  "version": "0.0.0",
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "type": "module",
 | 
			
		||||
  "engines": {
 | 
			
		||||
    "node": "^20.19.0 || >=22.12.0"
 | 
			
		||||
  },
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "dev": "vite",
 | 
			
		||||
    "build": "vite build",
 | 
			
		||||
    "preview": "vite preview"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "vue": "^3.5.22",
 | 
			
		||||
    "vue-router": "^4.6.3"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@vitejs/plugin-vue": "^6.0.1",
 | 
			
		||||
    "vite": "^7.1.11",
 | 
			
		||||
    "vite-plugin-vue-devtools": "^8.0.3"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										6
									
								
								requirements.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,6 @@
 | 
			
		||||
Flask==3.1.2
 | 
			
		||||
flask-cors==6.0.1
 | 
			
		||||
Werkzeug==3.1.3 
 | 
			
		||||
pymongo==4.15.3
 | 
			
		||||
matplotlib==3.10.7
 | 
			
		||||
bcrypt==5.0.0
 | 
			
		||||
							
								
								
									
										10
									
								
								src/App.vue
									
									
									
									
									
								
							
							
						
						@@ -1,10 +0,0 @@
 | 
			
		||||
<script setup>
 | 
			
		||||
  
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <RouterView />
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,88 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <footer class="bg-dark-footer text-white pt-5 pb-3">
 | 
			
		||||
        <div class="container">
 | 
			
		||||
            <div class="row g-4">
 | 
			
		||||
                
 | 
			
		||||
                <div class="col-md-4 col-12">
 | 
			
		||||
                    <img src="@/assets/CtrlCash-white.png" alt="CtrlCash Logo" width="120" height="35" class="mb-3">
 | 
			
		||||
                    <p class="text-secondary-footer mt-2">
 | 
			
		||||
                        Seu dinheiro. Seu controle total.
 | 
			
		||||
                    </p>
 | 
			
		||||
                    <p class="text-secondary-footer small">
 | 
			
		||||
                        Um projeto acadêmico de finanças inovadoras.
 | 
			
		||||
                    </p>
 | 
			
		||||
                </div>
 | 
			
		||||
                
 | 
			
		||||
                <div class="col-md-2 col-6">
 | 
			
		||||
                    <h5 class="fw-bold text-white-footer mb-3">Navegue</h5>
 | 
			
		||||
                    <ul class="list-unstyled">
 | 
			
		||||
                        <li class="mb-2"><router-link to="/" class="text-secondary-footer text-decoration-none">Início</router-link></li>
 | 
			
		||||
                        <li class="mb-2"><router-link to="/about" class="text-secondary-footer text-decoration-none">Sobre Nós</router-link></li>
 | 
			
		||||
                        <li class="mb-2"><router-link to="/help" class="text-secondary-footer text-decoration-none">Ajuda</router-link></li>
 | 
			
		||||
                    </ul>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <div class="col-md-3 col-6">
 | 
			
		||||
                    <h5 class="fw-bold text-white-footer mb-3">Projeto</h5>
 | 
			
		||||
                    <ul class="list-unstyled">
 | 
			
		||||
                        <li class="mb-2"><a href="https://github.com/Caio1w/CtrlCash" target="_blank" class="text-secondary-footer text-decoration-none">Repósitorio GitHub</a></li>
 | 
			
		||||
                        <li class="mb-2"><a href="/termos" class="text-secondary-footer text-decoration-none">Termos de Uso</a></li>
 | 
			
		||||
                        <li class="mb-2"><a href="/privacidade" class="text-secondary-footer text-decoration-none">Política de Privacidade</a></li>
 | 
			
		||||
                        <li class="mb-2"><a href="/equipe" class="text-secondary-footer text-decoration-none">Equipe de Desenvolvimento</a></li>
 | 
			
		||||
                    </ul>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <div class="col-md-3 col-12">
 | 
			
		||||
                    <h5 class="fw-bold text-white-footer mb-3">Contato</h5>
 | 
			
		||||
                    <p class="text-secondary-footer mb-1">E-mail: contato@ctrlcash.com</p>
 | 
			
		||||
                    <p class="text-secondary-footer">Telefone: (XX) XXXX-XXXX</p>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
            </div>
 | 
			
		||||
            
 | 
			
		||||
            <hr class="my-4 border-secondary-footer">
 | 
			
		||||
            <div class="text-center">
 | 
			
		||||
                <p class="text-secondary-footer small mb-0">
 | 
			
		||||
                    © 2025 CtrlCash - Todos os direitos reservados. Protótipo desenvolvido para fins acadêmicos.
 | 
			
		||||
                </p>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
        </div>
 | 
			
		||||
    </footer>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
    
 | 
			
		||||
    .bg-dark-footer {
 | 
			
		||||
        background-color: #1A3B5E; 
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    
 | 
			
		||||
    .text-white-footer {
 | 
			
		||||
        color: #FFFFFF !important;
 | 
			
		||||
    }
 | 
			
		||||
    .text-secondary-footer {
 | 
			
		||||
        color: #C0CCDA !important; 
 | 
			
		||||
        transition: color 0.2s;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    
 | 
			
		||||
    .text-success-footer {
 | 
			
		||||
        color: #2ECC71 !important; 
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    .text-secondary-footer:hover {
 | 
			
		||||
        color: #FFFFFF !important;
 | 
			
		||||
    }
 | 
			
		||||
    .text-success-footer:hover {
 | 
			
		||||
        color: #34D399 !important;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .border-secondary-footer {
 | 
			
		||||
        border-color: rgba(255, 255, 255, 0.1) !important;
 | 
			
		||||
    }
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,185 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <nav class="navbar navbar-expand-lg navbar-dark bg-primary-dark shadow-lg sticky-top">
 | 
			
		||||
    <div class="container-fluid max-w-7xl mx-auto px-4">
 | 
			
		||||
      
 | 
			
		||||
      <!-- Logo e Branding -->
 | 
			
		||||
      <router-link to="/dashboard" class="navbar-brand d-flex align-items-center me-4">
 | 
			
		||||
        <img src="@/assets/CtrlCash-white.png" alt="CtrlCash Logo" width="120" class="d-inline-block logo-align">
 | 
			
		||||
      </router-link>
 | 
			
		||||
 | 
			
		||||
      <!-- Botão para Mobile (Hamburguer) -->
 | 
			
		||||
      <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
 | 
			
		||||
        <span class="navbar-toggler-icon"></span>
 | 
			
		||||
      </button>
 | 
			
		||||
 | 
			
		||||
      <div class="collapse navbar-collapse" id="navbarNav">
 | 
			
		||||
        
 | 
			
		||||
        <!-- Navegação Principal -->
 | 
			
		||||
        <ul class="navbar-nav me-auto mb-2 mb-lg-0">
 | 
			
		||||
          <li class="nav-item">
 | 
			
		||||
            <router-link to="/dashboard" :class="['nav-link', route.path === '/dashboard' ? 'active fw-bold text-white' : 'text-white-50', 'hover-success-feature']">Dashboard</router-link>
 | 
			
		||||
          </li>
 | 
			
		||||
          <li class="nav-item">
 | 
			
		||||
            <router-link to="/transacoes" :class="['nav-link', route.path === '/transacoes' ? 'active fw-bold text-white' : 'text-white-50', 'hover-success-feature']">Transações</router-link>
 | 
			
		||||
          </li>
 | 
			
		||||
          <li class="nav-item">
 | 
			
		||||
            <router-link to="/configuracoes" :class="['nav-link', route.path === '/configuracoes' ? 'active fw-bold text-white' : 'text-white-50', 'hover-success-feature']">Configurações</router-link>
 | 
			
		||||
          </li>
 | 
			
		||||
        </ul>
 | 
			
		||||
        
 | 
			
		||||
        <!-- Ícones de Ação e Usuário -->
 | 
			
		||||
        <div class="d-flex align-items-center ms-auto">
 | 
			
		||||
          
 | 
			
		||||
          <!-- Botão de Notificações -->
 | 
			
		||||
          <button class="btn btn-link text-white me-3 p-0" title="Notificações">
 | 
			
		||||
            <i class="bi bi-bell-fill fs-5 position-relative">
 | 
			
		||||
              <span v-if="unreadNotifications > 0" class="position-absolute top-0 start-100 translate-middle p-1 bg-danger border border-light rounded-circle">
 | 
			
		||||
                <small>{{ unreadNotifications > 9 ? '9+' : unreadNotifications }}</small>
 | 
			
		||||
              </span>
 | 
			
		||||
            </i>
 | 
			
		||||
          </button>
 | 
			
		||||
 | 
			
		||||
          <!-- Avatar e Nome do Usuário -->
 | 
			
		||||
          <div class="d-flex align-items-center me-3">
 | 
			
		||||
            <img 
 | 
			
		||||
              class="rounded-circle border border-white" 
 | 
			
		||||
              :src="userAvatar" 
 | 
			
		||||
              :alt="`Avatar de ${userName}`" 
 | 
			
		||||
              style="width: 36px; height: 36px; object-fit: cover;"
 | 
			
		||||
            >
 | 
			
		||||
            <span class="ms-2 d-none d-md-inline text-sm text-white">{{ userName }}</span>
 | 
			
		||||
          </div>
 | 
			
		||||
          
 | 
			
		||||
          <!-- Botão de Logout - Desktop -->
 | 
			
		||||
          <button @click="logout" class="btn btn-outline-light btn-sm d-none d-md-inline-flex align-items-center" title="Sair">
 | 
			
		||||
             <i class="bi bi-box-arrow-right me-1"></i>
 | 
			
		||||
             Sair
 | 
			
		||||
          </button>
 | 
			
		||||
 | 
			
		||||
          <!-- Botão de Logout - Mobile (dentro do menu) -->
 | 
			
		||||
          <div class="d-md-none">
 | 
			
		||||
            <button @click="logout" class="btn btn-outline-light btn-sm w-100 mt-2 d-flex align-items-center justify-content-center">
 | 
			
		||||
              <i class="bi bi-box-arrow-right me-2"></i>
 | 
			
		||||
              Sair
 | 
			
		||||
            </button>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </nav>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
    import { useRouter, useRoute } from 'vue-router';
 | 
			
		||||
    import { ref, onMounted, watch } from 'vue';
 | 
			
		||||
 | 
			
		||||
    const router = useRouter();
 | 
			
		||||
    const route = useRoute();
 | 
			
		||||
 | 
			
		||||
    // Estados reativos
 | 
			
		||||
    const userName = ref('Usuário');
 | 
			
		||||
    const userAvatar = ref('https://placehold.co/36x36/1A3B5E/FFFFFF?text=U');
 | 
			
		||||
    const unreadNotifications = ref(0);
 | 
			
		||||
 | 
			
		||||
    // Função para carregar dados do usuário
 | 
			
		||||
    const loadUserData = () => {
 | 
			
		||||
        try {
 | 
			
		||||
            const userData = localStorage.getItem('user');
 | 
			
		||||
            if (userData) {
 | 
			
		||||
                const user = JSON.parse(userData);
 | 
			
		||||
                userName.value = user.name || 'Usuário';
 | 
			
		||||
                
 | 
			
		||||
                // Gerar avatar dinâmico baseado nas iniciais do nome
 | 
			
		||||
                if (user.name) {
 | 
			
		||||
                    const initials = user.name
 | 
			
		||||
                        .split(' ')
 | 
			
		||||
                        .map(word => word[0])
 | 
			
		||||
                        .join('')
 | 
			
		||||
                        .toUpperCase()
 | 
			
		||||
                        .substring(0, 2);
 | 
			
		||||
                    
 | 
			
		||||
                    userAvatar.value = `https://placehold.co/36x36/1A3B5E/FFFFFF?text=${initials}`;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('Erro ao carregar dados do usuário:', error);
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const logout = () => {
 | 
			
		||||
        console.log("Usuário desconectado. Redirecionando para o login.");
 | 
			
		||||
        
 | 
			
		||||
        // Limpar dados de autenticação
 | 
			
		||||
        localStorage.removeItem('user');
 | 
			
		||||
        localStorage.removeItem('isAuthenticated');
 | 
			
		||||
        localStorage.removeItem('token');
 | 
			
		||||
        
 | 
			
		||||
        // Redirecionar para login
 | 
			
		||||
        router.push('/login');
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // Watcher para atualizar dados do usuário quando a rota mudar
 | 
			
		||||
    watch(route, () => {
 | 
			
		||||
        // Recarregar dados do usuário quando mudar de página
 | 
			
		||||
        loadUserData();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Carregar dados quando o componente for montado
 | 
			
		||||
    onMounted(() => {
 | 
			
		||||
        loadUserData();
 | 
			
		||||
        
 | 
			
		||||
        // Verificar se o usuário está autenticado
 | 
			
		||||
        const isAuthenticated = localStorage.getItem('isAuthenticated');
 | 
			
		||||
        const user = localStorage.getItem('user');
 | 
			
		||||
        
 | 
			
		||||
        if (!isAuthenticated || !user) {
 | 
			
		||||
            router.push('/login');
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
    /* Definição das cores customizadas com Bootstrap */
 | 
			
		||||
    .bg-primary-dark { background-color: #1A3B5E !important; }
 | 
			
		||||
    .text-primary-dark { color: #1A3B5E !important; }
 | 
			
		||||
    .text-success-feature { color: #2ECC71 !important; }
 | 
			
		||||
    .hover-success-feature:hover { color: #2ECC71 !important; }
 | 
			
		||||
 | 
			
		||||
    /* FORÇA O ALINHAMENTO VERTICAL DA IMAGEM E DO TEXTO */
 | 
			
		||||
    .navbar-brand {
 | 
			
		||||
        display: flex;
 | 
			
		||||
        align-items: center;
 | 
			
		||||
    }
 | 
			
		||||
    .logo-align {
 | 
			
		||||
        vertical-align: middle;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* Estilos para o botão de logout no mobile */
 | 
			
		||||
    @media (max-width: 767.98px) {
 | 
			
		||||
        .navbar-collapse {
 | 
			
		||||
            padding-bottom: 1rem;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        .btn-outline-light {
 | 
			
		||||
            margin-top: 0.5rem;
 | 
			
		||||
            border-color: rgba(255, 255, 255, 0.5);
 | 
			
		||||
            color: rgba(255, 255, 255, 0.8);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        .btn-outline-light:hover {
 | 
			
		||||
            background-color: rgba(255, 255, 255, 0.1);
 | 
			
		||||
            color: white;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* Estilos para a badge de notificações */
 | 
			
		||||
    .position-relative .bg-danger {
 | 
			
		||||
        font-size: 0.6rem;
 | 
			
		||||
        min-width: 18px;
 | 
			
		||||
        height: 18px;
 | 
			
		||||
        display: flex;
 | 
			
		||||
        align-items: center;
 | 
			
		||||
        justify-content: center;
 | 
			
		||||
    }
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,51 +0,0 @@
 | 
			
		||||
<script>
 | 
			
		||||
    // A tag <script> pode ficar vazia se não houver lógica (como imports de componentes ou métodos), 
 | 
			
		||||
    // mas é bom mantê-la para futuras expansões.
 | 
			
		||||
</script>
 | 
			
		||||
<template>
 | 
			
		||||
    <header>
 | 
			
		||||
        <nav class="navbar navbar-expand-lg px-3" :class="['bg-especial', 'navbar-dark']">
 | 
			
		||||
            <div class="container-fluid">
 | 
			
		||||
                
 | 
			
		||||
                <router-link class="navbar-brand p-0" to="/">
 | 
			
		||||
                    <img src="@/assets/CtrlCash-white.png" alt="CtrlCash Logo" width="140" height="40">
 | 
			
		||||
                </router-link>
 | 
			
		||||
 | 
			
		||||
                <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
 | 
			
		||||
                    <span class="navbar-toggler-icon"></span>
 | 
			
		||||
                </button>
 | 
			
		||||
 | 
			
		||||
                <div class="collapse navbar-collapse" id="navbarNav">
 | 
			
		||||
                    
 | 
			
		||||
                    <div class="navbar-nav ms-auto align-items-center">
 | 
			
		||||
                        
 | 
			
		||||
                        <router-link to="/about" class="nav-link">Sobre nós</router-link>
 | 
			
		||||
                        <router-link to="/help" class="nav-link me-3">Ajuda</router-link> 
 | 
			
		||||
                        
 | 
			
		||||
                        <router-link to="/cadastro" class="btn btn-success me-2">Abrir Conta</router-link>
 | 
			
		||||
                        <router-link to="/login" class="btn btn-light text-primary">Login</router-link>
 | 
			
		||||
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </nav>
 | 
			
		||||
    </header>
 | 
			
		||||
</template>
 | 
			
		||||
<style scoped>
 | 
			
		||||
    .bg-especial {
 | 
			
		||||
        background-color: #1A3B5E;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .btn-success {
 | 
			
		||||
        background-color: #2ECC71; 
 | 
			
		||||
        border-color: #2ECC71;
 | 
			
		||||
    }
 | 
			
		||||
    .btn-success:hover {
 | 
			
		||||
        background-color: #26a95f;
 | 
			
		||||
        border-color: #26a95f;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .text-primary {
 | 
			
		||||
        color: #1A3B5E !important;
 | 
			
		||||
    }
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,9 +0,0 @@
 | 
			
		||||
import { createApp } from 'vue'
 | 
			
		||||
import App from './App.vue'
 | 
			
		||||
import router from './router'
 | 
			
		||||
 | 
			
		||||
const app = createApp(App)
 | 
			
		||||
 | 
			
		||||
app.use(router)
 | 
			
		||||
 | 
			
		||||
app.mount('#app')
 | 
			
		||||
@@ -1,58 +0,0 @@
 | 
			
		||||
import { createRouter, createWebHistory } from 'vue-router'
 | 
			
		||||
import HomeView from '../views/HomeView.vue'
 | 
			
		||||
import UserDashboardView from '../views/UserDashboardView.vue'
 | 
			
		||||
import LoginView from '../views/LoginView.vue'
 | 
			
		||||
import CadastroView from '@/views/CadastroView.vue'
 | 
			
		||||
import TransacoesView from '@/views/TransacoesView.vue'
 | 
			
		||||
import ConfiguracoesView from '@/views/ConfiguracoesView.vue'
 | 
			
		||||
import SobreNosView from '@/views/SobreNosView.vue'
 | 
			
		||||
import AjudaView from '@/views/AjudaView.vue'
 | 
			
		||||
const router = createRouter({
 | 
			
		||||
  history: createWebHistory(import.meta.env.BASE_URL),
 | 
			
		||||
  routes: [
 | 
			
		||||
    {
 | 
			
		||||
    path: '/',
 | 
			
		||||
    name: 'home',
 | 
			
		||||
    component: HomeView
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: '/dashboard',
 | 
			
		||||
    name: 'dashboard',
 | 
			
		||||
    component: UserDashboardView
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: '/login',
 | 
			
		||||
    name: 'login',
 | 
			
		||||
    component: LoginView
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: '/cadastro',
 | 
			
		||||
    name: 'cadastro',
 | 
			
		||||
    component: CadastroView
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: '/transacoes',
 | 
			
		||||
    name: 'transacoes',
 | 
			
		||||
    component: TransacoesView
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: '/configuracoes',
 | 
			
		||||
    name: 'configuracoes',
 | 
			
		||||
    component: ConfiguracoesView
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: '/about',
 | 
			
		||||
    name: 'about',
 | 
			
		||||
    component: SobreNosView
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: '/help',
 | 
			
		||||
    name: 'help',
 | 
			
		||||
    component: AjudaView
 | 
			
		||||
  },
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
],
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default router
 | 
			
		||||
@@ -1,80 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="ajuda-page bg-light min-vh-100">
 | 
			
		||||
        
 | 
			
		||||
        <!-- Header Público (Links para Login/Cadastro) -->
 | 
			
		||||
        <HeaderPublic />
 | 
			
		||||
 | 
			
		||||
        <div class="container-fluid py-5 max-w-7xl mx-auto px-4">
 | 
			
		||||
            
 | 
			
		||||
            <h1 class="display-5 fw-bold text-primary-dark mb-4 text-center">Central de Ajuda e FAQ</h1>
 | 
			
		||||
            <p class="text-center text-muted mb-5">Encontre respostas rápidas para suas dúvidas mais comuns ou envie seu feedback.</p>
 | 
			
		||||
            
 | 
			
		||||
            <div class="row justify-content-center">
 | 
			
		||||
                <div class="col-lg-10">
 | 
			
		||||
                    
 | 
			
		||||
                    <!-- ACORDEÃO DE PERGUNTAS FREQUENTES -->
 | 
			
		||||
                    <div class="accordion" id="faqAccordion">
 | 
			
		||||
                        
 | 
			
		||||
                        <!-- Pergunta 1 -->
 | 
			
		||||
                        <div class="accordion-item shadow-sm mb-3 rounded-3">
 | 
			
		||||
                            <h2 class="accordion-header" id="headingOne">
 | 
			
		||||
                                <button class="accordion-button fw-bold text-primary-dark" type="button" data-bs-toggle="collapse" data-bs-target="#collapseOne" aria-expanded="true" aria-controls="collapseOne">
 | 
			
		||||
                                    Como faço para registrar uma nova transação?
 | 
			
		||||
                                </button>
 | 
			
		||||
                            </h2>
 | 
			
		||||
                            <div id="collapseOne" class="accordion-collapse collapse show" aria-labelledby="headingOne" data-bs-parent="#faqAccordion">
 | 
			
		||||
                                <div class="accordion-body text-muted small">
 | 
			
		||||
                                    No seu Dashboard, clique em "Transações" na barra de navegação. Na View de Transações, use o botão "Nova Transação" (verde) para adicionar Receitas ou Despesas. Certifique-se de categorizar corretamente para as análises futuras.
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    
 | 
			
		||||
 | 
			
		||||
                        <!-- Pergunta 2 -->
 | 
			
		||||
                        <div class="accordion-item shadow-sm mb-3 rounded-3">
 | 
			
		||||
                            <h2 class="accordion-header" id="headingThree">
 | 
			
		||||
                                <button class="accordion-button collapsed fw-bold text-primary-dark" type="button" data-bs-toggle="collapse" data-bs-target="#collapseThree" aria-expanded="false" aria-controls="collapseThree">
 | 
			
		||||
                                    Posso adicionar minhas próprias categorias?
 | 
			
		||||
                                </button>
 | 
			
		||||
                            </h2>
 | 
			
		||||
                            <div id="collapseThree" class="accordion-collapse collapse" aria-labelledby="headingThree" data-bs-parent="#faqAccordion">
 | 
			
		||||
                                <div class="accordion-body text-muted small">
 | 
			
		||||
                                    Sim! Vá para a View "Configurações" e selecione a aba "Gestão de Categorias". Lá você pode criar, editar ou remover categorias personalizadas para se adequar ao seu estilo de vida financeiro.
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    
 | 
			
		||||
                    
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <FooterPublic />
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
    import FooterPublic from '@/components/FooterPublic.vue'
 | 
			
		||||
    import HeaderPublic from '@/components/HeaderPublic.vue';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
    /* Estilos Customizados do CtrlCash */
 | 
			
		||||
    .bg-primary-dark { background-color: #1A3B5E !important; }
 | 
			
		||||
    .text-primary-dark { color: #1A3B5E !important; }
 | 
			
		||||
    .text-success-feature { color: #2ECC71 !important; }
 | 
			
		||||
    .btn-success-feature { background-color: #2ECC71 !important; border-color: #2ECC71 !important; color: white; }
 | 
			
		||||
    
 | 
			
		||||
    .max-w-7xl { max-width: 80rem; }
 | 
			
		||||
    .mx-auto { margin-left: auto !important; margin-right: auto !important; }
 | 
			
		||||
 | 
			
		||||
    .accordion-button:not(.collapsed) {
 | 
			
		||||
        color: white !important;
 | 
			
		||||
        background-color: #1A3B5E !important;
 | 
			
		||||
        box-shadow: none;
 | 
			
		||||
    }
 | 
			
		||||
    .accordion-button:focus {
 | 
			
		||||
        box-shadow: none;
 | 
			
		||||
        border-color: #1A3B5E;
 | 
			
		||||
    }
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,160 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <HeaderPublic />
 | 
			
		||||
    <div class="cadastro-container d-flex align-items-center justify-content-center min-vh-100 p-3">
 | 
			
		||||
        <div class="cadastro-card p-5 shadow-lg rounded-4 bg-white">
 | 
			
		||||
            
 | 
			
		||||
            <div class="text-center mb-4">
 | 
			
		||||
                <img src="@/assets/CtrlCash-blue.png" alt="CtrlCash Logo" width="150" class="mb-3">
 | 
			
		||||
                <h2 class="fw-bold text-primary-dark">Abra Sua Conta</h2>
 | 
			
		||||
                <p class="text-secondary-dark">Preencha os campos para iniciar seu controle financeiro.</p>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <form @submit.prevent="handleCadastro">
 | 
			
		||||
                <div class="mb-3">
 | 
			
		||||
                    <label for="nome" class="form-label fw-medium text-primary-dark">Nome Completo</label>
 | 
			
		||||
                    <div class="input-group">
 | 
			
		||||
                        <span class="input-group-text"><i class="bi bi-person-fill"></i></span>
 | 
			
		||||
                        <input type="text" class="form-control" id="nome" v-model="cadastroForm.name" required placeholder="Seu nome">
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <div class="mb-3">
 | 
			
		||||
                    <label for="email" class="form-label fw-medium text-primary-dark">E-mail</label>
 | 
			
		||||
                    <div class="input-group">
 | 
			
		||||
                        <span class="input-group-text"><i class="bi bi-envelope-fill"></i></span>
 | 
			
		||||
                        <input type="email" class="form-control" id="email" v-model="cadastroForm.email" required placeholder="seu.email@exemplo.com">
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                
 | 
			
		||||
                <div class="mb-3">
 | 
			
		||||
                    <label for="password" class="form-label fw-medium text-primary-dark">Crie Sua Senha</label>
 | 
			
		||||
                    <div class="input-group">
 | 
			
		||||
                        <span class="input-group-text"><i class="bi bi-lock-fill"></i></span>
 | 
			
		||||
                        <input type="password" class="form-control" id="password" v-model="cadastroForm.password" required placeholder="Mínimo 6 caracteres">
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                
 | 
			
		||||
                <div class="mb-4">
 | 
			
		||||
                    <label for="confirmPassword" class="form-label fw-medium text-primary-dark">Confirmar Senha</label>
 | 
			
		||||
                    <div class="input-group">
 | 
			
		||||
                        <span class="input-group-text"><i class="bi bi-lock-fill"></i></span>
 | 
			
		||||
                        <input type="password" class="form-control" id="confirmPassword" v-model="confirmPassword" required placeholder="Repita a senha">
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <!-- Mensagens -->
 | 
			
		||||
                <div v-if="error" class="alert alert-danger">
 | 
			
		||||
                    {{ error }}
 | 
			
		||||
                </div>
 | 
			
		||||
                <div v-if="success" class="alert alert-success">
 | 
			
		||||
                    {{ success }}
 | 
			
		||||
                </div>
 | 
			
		||||
                
 | 
			
		||||
                <button type="submit" class="btn btn-primary-feature w-100 fw-bold py-2 shadow-sm" :disabled="loading">
 | 
			
		||||
                    <span v-if="loading" class="spinner-border spinner-border-sm me-2"></span>
 | 
			
		||||
                    {{ loading ? 'Cadastrando...' : 'Criar Minha Conta CtrlCash' }}
 | 
			
		||||
                </button>
 | 
			
		||||
            </form>
 | 
			
		||||
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
    import { ref } from 'vue';
 | 
			
		||||
    import { useRouter } from 'vue-router';
 | 
			
		||||
    import HeaderPublic from '@/components/HeaderPublic.vue';
 | 
			
		||||
 | 
			
		||||
    const router = useRouter();
 | 
			
		||||
    const loading = ref(false);
 | 
			
		||||
    const error = ref('');
 | 
			
		||||
    const success = ref('');
 | 
			
		||||
 | 
			
		||||
    const cadastroForm = ref({
 | 
			
		||||
        name: '',
 | 
			
		||||
        email: '',
 | 
			
		||||
        password: ''
 | 
			
		||||
    });
 | 
			
		||||
    const confirmPassword = ref('');
 | 
			
		||||
 | 
			
		||||
    const handleCadastro = async () => {
 | 
			
		||||
        loading.value = true;
 | 
			
		||||
        error.value = '';
 | 
			
		||||
        success.value = '';
 | 
			
		||||
 | 
			
		||||
        // Validações
 | 
			
		||||
        if (cadastroForm.value.password !== confirmPassword.value) {
 | 
			
		||||
            error.value = "As senhas não coincidem!";
 | 
			
		||||
            loading.value = false;
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (cadastroForm.value.password.length < 6) {
 | 
			
		||||
            error.value = "A senha deve ter pelo menos 6 caracteres";
 | 
			
		||||
            loading.value = false;
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            const response = await fetch('/api/auth/register', {
 | 
			
		||||
                method: 'POST',
 | 
			
		||||
                headers: {
 | 
			
		||||
                    'Content-Type': 'application/json',
 | 
			
		||||
                },
 | 
			
		||||
                body: JSON.stringify(cadastroForm.value)
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const data = await response.json();
 | 
			
		||||
 | 
			
		||||
            if (response.ok) {
 | 
			
		||||
                success.value = 'Cadastro realizado com sucesso! Redirecionando...';
 | 
			
		||||
                
 | 
			
		||||
                // Auto-login após cadastro
 | 
			
		||||
                setTimeout(() => {
 | 
			
		||||
                    localStorage.setItem('user', JSON.stringify(data.user));
 | 
			
		||||
                    localStorage.setItem('isAuthenticated', 'true');
 | 
			
		||||
                    router.push('/dashboard');
 | 
			
		||||
                }, 2000);
 | 
			
		||||
            } else {
 | 
			
		||||
                error.value = data.error || 'Erro ao cadastrar';
 | 
			
		||||
            }
 | 
			
		||||
        } catch (err) {
 | 
			
		||||
            console.error('Erro:', err);
 | 
			
		||||
            error.value = 'Erro de conexão com o servidor';
 | 
			
		||||
        } finally {
 | 
			
		||||
            loading.value = false;
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
    .text-primary-dark { color: #1A3B5E !important; }
 | 
			
		||||
    .text-secondary-dark { color: #6c757d !important; }
 | 
			
		||||
    
 | 
			
		||||
    .cadastro-container { background-color: #F8F9FA; }
 | 
			
		||||
    .cadastro-card { max-width: 450px; width: 100%; }
 | 
			
		||||
    
 | 
			
		||||
    .btn-primary-feature {
 | 
			
		||||
        background-color: #1A3B5E;
 | 
			
		||||
        border-color: #1A3B5E;
 | 
			
		||||
        color: white;
 | 
			
		||||
        transition: background-color 0.2s;
 | 
			
		||||
    }
 | 
			
		||||
    .btn-primary-feature:hover:not(:disabled) {
 | 
			
		||||
        background-color: #29517b;
 | 
			
		||||
        border-color: #29517b;
 | 
			
		||||
    }
 | 
			
		||||
    .btn-primary-feature:disabled {
 | 
			
		||||
        opacity: 0.6;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .form-control:focus {
 | 
			
		||||
        border-color: #1A3B5E;
 | 
			
		||||
        box-shadow: 0 0 0 0.25rem rgba(26, 59, 94, 0.25);
 | 
			
		||||
    }
 | 
			
		||||
    .input-group-text {
 | 
			
		||||
        background-color: #e9ecef;
 | 
			
		||||
        border-right: none;
 | 
			
		||||
        color: #1A3B5E;
 | 
			
		||||
    }
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,588 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="config-page bg-light min-vh-100">
 | 
			
		||||
        
 | 
			
		||||
        <!-- Header da Aplicação -->
 | 
			
		||||
        <HeaderApp />
 | 
			
		||||
 | 
			
		||||
        <div class="container-fluid py-4 max-w-7xl mx-auto px-4">
 | 
			
		||||
            
 | 
			
		||||
            <h1 class="h3 fw-bold text-primary-dark mb-4">Configurações e Gestão da Conta</h1>
 | 
			
		||||
            
 | 
			
		||||
            <div class="row g-4">
 | 
			
		||||
                
 | 
			
		||||
                <!-- Coluna de Navegação Lateral -->
 | 
			
		||||
                <div class="col-lg-3">
 | 
			
		||||
                    <div class="list-group shadow-sm border-0 rounded-3">
 | 
			
		||||
                        <a href="#" 
 | 
			
		||||
                           @click.prevent="setActiveTab('perfil')" 
 | 
			
		||||
                           :class="['list-group-item list-group-item-action', {'active-feature': activeTab === 'perfil'}]">
 | 
			
		||||
                            <i class="bi bi-person-circle me-2"></i> Perfil e Dados Pessoais
 | 
			
		||||
                        </a>
 | 
			
		||||
                        <a href="#" 
 | 
			
		||||
                           @click.prevent="setActiveTab('categorias')" 
 | 
			
		||||
                           :class="['list-group-item list-group-item-action', {'active-feature': activeTab === 'categorias'}]">
 | 
			
		||||
                            <i class="bi bi-tags-fill me-2"></i> Gestão de Categorias
 | 
			
		||||
                        </a>
 | 
			
		||||
                        <a href="#" 
 | 
			
		||||
                           @click.prevent="setActiveTab('seguranca')" 
 | 
			
		||||
                           :class="['list-group-item list-group-item-action', {'active-feature': activeTab === 'seguranca'}]">
 | 
			
		||||
                            <i class="bi bi-lock-fill me-2"></i> Segurança e Senha
 | 
			
		||||
                        </a>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <!-- Área de Conteúdo (Abas) -->
 | 
			
		||||
                <div class="col-lg-9">
 | 
			
		||||
                    <div class="card shadow-sm border-0 p-4 h-100">
 | 
			
		||||
                        
 | 
			
		||||
                        <!-- Aba: Perfil e Dados Pessoais -->
 | 
			
		||||
                        <div v-if="activeTab === 'perfil'">
 | 
			
		||||
                            <h2 class="h4 fw-bold text-primary-dark mb-3">Meu Perfil</h2>
 | 
			
		||||
                            <p class="text-muted">Gerencie seu nome, e-mail e outras informações de contato.</p>
 | 
			
		||||
                            
 | 
			
		||||
                            <form @submit.prevent="updateProfile">
 | 
			
		||||
                                <div class="row">
 | 
			
		||||
                                    <div class="col-md-6 mb-3">
 | 
			
		||||
                                        <label class="form-label fw-medium">Nome Completo</label>
 | 
			
		||||
                                        <input type="text" class="form-control" v-model="profileForm.name" required>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                    <div class="col-md-6 mb-3">
 | 
			
		||||
                                        <label class="form-label fw-medium">E-mail</label>
 | 
			
		||||
                                        <input type="email" class="form-control" v-model="profileForm.email" required>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                </div>
 | 
			
		||||
 | 
			
		||||
                                <div class="row">
 | 
			
		||||
                                    <div class="col-md-6 mb-3">
 | 
			
		||||
                                        <label class="form-label fw-medium">Meta de Receita Mensal (R$)</label>
 | 
			
		||||
                                        <input type="number" class="form-control" v-model="profileForm.profile.monthly_income_goal" step="0.01" min="0">
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                    <div class="col-md-6 mb-3">
 | 
			
		||||
                                        <label class="form-label fw-medium">Limite de Despesas Mensal (R$)</label>
 | 
			
		||||
                                        <input type="number" class="form-control" v-model="profileForm.profile.monthly_expense_limit" step="0.01" min="0">
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                </div>
 | 
			
		||||
 | 
			
		||||
                                <!-- Mensagens -->
 | 
			
		||||
                                <div v-if="profileMessage" class="alert" :class="profileMessage.type === 'success' ? 'alert-success' : 'alert-danger'">
 | 
			
		||||
                                    {{ profileMessage.text }}
 | 
			
		||||
                                </div>
 | 
			
		||||
 | 
			
		||||
                                <button class="btn btn-primary-feature mt-2" :disabled="updatingProfile">
 | 
			
		||||
                                    <span v-if="updatingProfile" class="spinner-border spinner-border-sm me-2"></span>
 | 
			
		||||
                                    {{ updatingProfile ? 'Salvando...' : 'Salvar Alterações' }}
 | 
			
		||||
                                </button>
 | 
			
		||||
                            </form>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        
 | 
			
		||||
                        <!-- Aba: Gestão de Categorias -->
 | 
			
		||||
                        <div v-if="activeTab === 'categorias'">
 | 
			
		||||
                            <div class="d-flex justify-content-between align-items-center mb-3">
 | 
			
		||||
                                <h2 class="h4 fw-bold text-primary-dark mb-0">Categorias Financeiras</h2>
 | 
			
		||||
                                <button class="btn btn-success-feature" @click="showAddCategoryModal = true">
 | 
			
		||||
                                    <i class="bi bi-plus-circle me-2"></i> Nova Categoria
 | 
			
		||||
                                </button>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <p class="text-muted">Crie, edite ou remova categorias de receitas e despesas.</p>
 | 
			
		||||
                            
 | 
			
		||||
                            <!-- Loading State -->
 | 
			
		||||
                            <div v-if="loadingCategories" class="text-center py-4">
 | 
			
		||||
                                <div class="spinner-border text-primary-dark" role="status"></div>
 | 
			
		||||
                                <p class="text-muted mt-2">Carregando categorias...</p>
 | 
			
		||||
                            </div>
 | 
			
		||||
 | 
			
		||||
                            <!-- Lista de Categorias -->
 | 
			
		||||
                            <div v-else class="row">
 | 
			
		||||
                                <div class="col-md-6">
 | 
			
		||||
                                    <h5 class="fw-bold text-success mb-3">📈 Receitas</h5>
 | 
			
		||||
                                    <div class="list-group mb-4">
 | 
			
		||||
                                        <div 
 | 
			
		||||
                                            v-for="category in incomeCategories" 
 | 
			
		||||
                                            :key="category.id"
 | 
			
		||||
                                            class="list-group-item d-flex justify-content-between align-items-center"
 | 
			
		||||
                                        >
 | 
			
		||||
                                            <div class="d-flex align-items-center">
 | 
			
		||||
                                                <span 
 | 
			
		||||
                                                    class="badge me-2" 
 | 
			
		||||
                                                    :style="{ 
 | 
			
		||||
                                                        backgroundColor: category.color, 
 | 
			
		||||
                                                        width: '15px', 
 | 
			
		||||
                                                        height: '15px',
 | 
			
		||||
                                                        display: 'inline-block'
 | 
			
		||||
                                                    }"
 | 
			
		||||
                                                ></span>
 | 
			
		||||
                                                {{ category.name }}
 | 
			
		||||
                                            </div>
 | 
			
		||||
                                            <span class="badge bg-success">Receita</span>
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                
 | 
			
		||||
                                <div class="col-md-6">
 | 
			
		||||
                                    <h5 class="fw-bold text-danger mb-3">📉 Despesas</h5>
 | 
			
		||||
                                    <div class="list-group">
 | 
			
		||||
                                        <div 
 | 
			
		||||
                                            v-for="category in expenseCategories" 
 | 
			
		||||
                                            :key="category.id"
 | 
			
		||||
                                            class="list-group-item d-flex justify-content-between align-items-center"
 | 
			
		||||
                                        >
 | 
			
		||||
                                            <div class="d-flex align-items-center">
 | 
			
		||||
                                                <span 
 | 
			
		||||
                                                    class="badge me-2" 
 | 
			
		||||
                                                    :style="{ 
 | 
			
		||||
                                                        backgroundColor: category.color, 
 | 
			
		||||
                                                        width: '15px', 
 | 
			
		||||
                                                        height: '15px',
 | 
			
		||||
                                                        display: 'inline-block'
 | 
			
		||||
                                                    }"
 | 
			
		||||
                                                ></span>
 | 
			
		||||
                                                {{ category.name }}
 | 
			
		||||
                                            </div>
 | 
			
		||||
                                            <span class="badge bg-danger">Despesa</span>
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
 | 
			
		||||
                            <!-- Mensagem de Categorias -->
 | 
			
		||||
                            <div v-if="categoriesMessage" class="alert" :class="categoriesMessage.type === 'success' ? 'alert-success' : 'alert-danger'">
 | 
			
		||||
                                {{ categoriesMessage.text }}
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        
 | 
			
		||||
                        <!-- Aba: Segurança e Senha -->
 | 
			
		||||
                        <div v-if="activeTab === 'seguranca'">
 | 
			
		||||
                            <h2 class="h4 fw-bold text-primary-dark mb-3">Segurança da Conta</h2>
 | 
			
		||||
                            <p class="text-muted">Altere sua senha para manter sua conta segura.</p>
 | 
			
		||||
                            
 | 
			
		||||
                            <form @submit.prevent="changePassword">
 | 
			
		||||
                                <div class="mb-3">
 | 
			
		||||
                                    <label class="form-label fw-medium">Senha Atual</label>
 | 
			
		||||
                                    <input type="password" class="form-control" v-model="passwordForm.currentPassword" required>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <div class="mb-3">
 | 
			
		||||
                                    <label class="form-label fw-medium">Nova Senha</label>
 | 
			
		||||
                                    <input type="password" class="form-control" v-model="passwordForm.newPassword" required minlength="6">
 | 
			
		||||
                                    <small class="text-muted">Mínimo 6 caracteres</small>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <div class="mb-3">
 | 
			
		||||
                                    <label class="form-label fw-medium">Confirmar Nova Senha</label>
 | 
			
		||||
                                    <input type="password" class="form-control" v-model="passwordForm.confirmPassword" required>
 | 
			
		||||
                                </div>
 | 
			
		||||
 | 
			
		||||
                                <!-- Mensagens -->
 | 
			
		||||
                                <div v-if="passwordMessage" class="alert" :class="passwordMessage.type === 'success' ? 'alert-success' : 'alert-danger'">
 | 
			
		||||
                                    {{ passwordMessage.text }}
 | 
			
		||||
                                </div>
 | 
			
		||||
 | 
			
		||||
                                <button class="btn btn-warning text-white mt-2" :disabled="changingPassword">
 | 
			
		||||
                                    <span v-if="changingPassword" class="spinner-border spinner-border-sm me-2"></span>
 | 
			
		||||
                                    {{ changingPassword ? 'Alterando...' : 'Alterar Senha' }}
 | 
			
		||||
                                </button>
 | 
			
		||||
                            </form>
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <!-- Modal Adicionar Categoria -->
 | 
			
		||||
        <div class="modal fade" :class="{ 'show d-block': showAddCategoryModal }" tabindex="-1" v-if="showAddCategoryModal">
 | 
			
		||||
            <div class="modal-dialog modal-dialog-centered">
 | 
			
		||||
                <div class="modal-content">
 | 
			
		||||
                    <div class="modal-header bg-primary-dark text-white">
 | 
			
		||||
                        <h5 class="modal-title fw-bold">Nova Categoria</h5>
 | 
			
		||||
                        <button type="button" class="btn-close btn-close-white" @click="closeCategoryModal"></button>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="modal fade" :class="{ 'show d-block': showAddCategoryModal }" tabindex="-1" v-if="showAddCategoryModal">
 | 
			
		||||
        <div class="modal-dialog modal-dialog-centered">
 | 
			
		||||
            <div class="modal-content">
 | 
			
		||||
                <div class="modal-header bg-primary-dark text-white">
 | 
			
		||||
                    <h5 class="modal-title fw-bold">Nova Categoria</h5>
 | 
			
		||||
                    <button type="button" class="btn-close btn-close-white" @click="closeCategoryModal"></button>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="modal-body">
 | 
			
		||||
                    <form @submit.prevent="addCategory" id="categoryForm">
 | 
			
		||||
                        <div class="mb-3">
 | 
			
		||||
                            <label class="form-label fw-medium">Nome da Categoria</label>
 | 
			
		||||
                            <input 
 | 
			
		||||
                                type="text" 
 | 
			
		||||
                                class="form-control" 
 | 
			
		||||
                                v-model="newCategory.name"
 | 
			
		||||
                                placeholder="Ex: Investimentos, Lazer, Educação..."
 | 
			
		||||
                                required
 | 
			
		||||
                            >
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="mb-3">
 | 
			
		||||
                            <label class="form-label fw-medium">Tipo</label>
 | 
			
		||||
                            <select v-model="newCategory.type" class="form-select" required>
 | 
			
		||||
                                <option value="income">Receita</option>
 | 
			
		||||
                                <option value="expense">Despesa</option>
 | 
			
		||||
                            </select>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="mb-3">
 | 
			
		||||
                            <label class="form-label fw-medium">Cor</label>
 | 
			
		||||
                            <div class="d-flex align-items-center">
 | 
			
		||||
                                <input 
 | 
			
		||||
                                    type="color" 
 | 
			
		||||
                                    class="form-control form-control-color" 
 | 
			
		||||
                                    v-model="newCategory.color"
 | 
			
		||||
                                    required
 | 
			
		||||
                                >
 | 
			
		||||
                                <span class="ms-2 small text-muted">Escolha uma cor para identificar</span>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        <!-- Mensagens -->
 | 
			
		||||
                        <div v-if="categoryMessage" class="alert" :class="categoryMessage.type === 'success' ? 'alert-success' : 'alert-danger'">
 | 
			
		||||
                            {{ categoryMessage.text }}
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </form>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="modal-footer">
 | 
			
		||||
                    <button type="button" class="btn btn-secondary" @click="closeCategoryModal">Cancelar</button>
 | 
			
		||||
                    <button 
 | 
			
		||||
                        type="submit" 
 | 
			
		||||
                        class="btn btn-primary-dark-feature fw-bold" 
 | 
			
		||||
                        form="categoryForm"
 | 
			
		||||
                        :disabled="addingCategory"
 | 
			
		||||
                    >
 | 
			
		||||
                        <span v-if="addingCategory" class="spinner-border spinner-border-sm me-2"></span>
 | 
			
		||||
                        {{ addingCategory ? 'Adicionando...' : 'Adicionar Categoria' }}
 | 
			
		||||
                    </button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="modal-backdrop fade show" @click="closeCategoryModal"></div>
 | 
			
		||||
    </div>
 | 
			
		||||
                    <div class="modal-footer">
 | 
			
		||||
                        <button type="button" class="btn btn-secondary" @click="closeCategoryModal">Cancelar</button>
 | 
			
		||||
                        <button 
 | 
			
		||||
                            type="button" 
 | 
			
		||||
                            class="btn btn-primary-dark-feature fw-bold" 
 | 
			
		||||
                            @click="addCategory"
 | 
			
		||||
                            :disabled="addingCategory"
 | 
			
		||||
                        >
 | 
			
		||||
                            <span v-if="addingCategory" class="spinner-border spinner-border-sm me-2"></span>
 | 
			
		||||
                            {{ addingCategory ? 'Adicionando...' : 'Adicionar Categoria' }}
 | 
			
		||||
                        </button>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="modal-backdrop fade show" @click="closeCategoryModal"></div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
    import { ref, onMounted, computed } from 'vue';
 | 
			
		||||
    import { useRouter } from 'vue-router';
 | 
			
		||||
    import HeaderApp from '@/components/HeaderApp.vue';
 | 
			
		||||
 | 
			
		||||
    const router = useRouter();
 | 
			
		||||
 | 
			
		||||
    // Estados reativos
 | 
			
		||||
    const activeTab = ref('perfil');
 | 
			
		||||
    const loadingCategories = ref(false);
 | 
			
		||||
    const updatingProfile = ref(false);
 | 
			
		||||
    const changingPassword = ref(false);
 | 
			
		||||
    const addingCategory = ref(false);
 | 
			
		||||
    const showAddCategoryModal = ref(false);
 | 
			
		||||
 | 
			
		||||
    // Dados do usuário
 | 
			
		||||
    const userProfile = ref({
 | 
			
		||||
        name: '',
 | 
			
		||||
        email: '',
 | 
			
		||||
        profile: {
 | 
			
		||||
            monthly_income_goal: 5000,
 | 
			
		||||
            monthly_expense_limit: 2500,
 | 
			
		||||
            currency: 'BRL'
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const categories = ref([]);
 | 
			
		||||
 | 
			
		||||
    // Forms
 | 
			
		||||
    const profileForm = ref({
 | 
			
		||||
        name: '',
 | 
			
		||||
        email: '',
 | 
			
		||||
        profile: {
 | 
			
		||||
            monthly_income_goal: 5000,
 | 
			
		||||
            monthly_expense_limit: 2500,
 | 
			
		||||
            currency: 'BRL'
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const passwordForm = ref({
 | 
			
		||||
        currentPassword: '',
 | 
			
		||||
        newPassword: '',
 | 
			
		||||
        confirmPassword: ''
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const newCategory = ref({
 | 
			
		||||
        name: '',
 | 
			
		||||
        type: 'expense',
 | 
			
		||||
        color: '#6c757d'
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Mensagens
 | 
			
		||||
    const profileMessage = ref(null);
 | 
			
		||||
    const passwordMessage = ref(null);
 | 
			
		||||
    const categoriesMessage = ref(null);
 | 
			
		||||
    const categoryMessage = ref(null);
 | 
			
		||||
 | 
			
		||||
    // Computed
 | 
			
		||||
    const incomeCategories = computed(() => {
 | 
			
		||||
        return categories.value.filter(cat => cat.type === 'income');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const expenseCategories = computed(() => {
 | 
			
		||||
        return categories.value.filter(cat => cat.type === 'expense');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Métodos
 | 
			
		||||
    const setActiveTab = (tabName) => {
 | 
			
		||||
        activeTab.value = tabName;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const loadUserProfile = async () => {
 | 
			
		||||
        try {
 | 
			
		||||
            const user = JSON.parse(localStorage.getItem('user'));
 | 
			
		||||
            if (!user) {
 | 
			
		||||
                router.push('/login');
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const response = await fetch(`/api/user/profile?user_id=${user.id}`);
 | 
			
		||||
            if (response.ok) {
 | 
			
		||||
                const data = await response.json();
 | 
			
		||||
                userProfile.value = data.user;
 | 
			
		||||
                profileForm.value = { ...data.user };
 | 
			
		||||
                
 | 
			
		||||
                // Atualizar localStorage
 | 
			
		||||
                localStorage.setItem('user', JSON.stringify(data.user));
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('Erro ao carregar perfil:', error);
 | 
			
		||||
            showMessage(profileMessage, 'Erro ao carregar perfil', 'error');
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const loadCategories = async () => {
 | 
			
		||||
        loadingCategories.value = true;
 | 
			
		||||
        try {
 | 
			
		||||
            const user = JSON.parse(localStorage.getItem('user'));
 | 
			
		||||
            const response = await fetch(`/api/categories?user_id=${user.id}`);
 | 
			
		||||
            const data = await response.json();
 | 
			
		||||
            
 | 
			
		||||
            if (response.ok) {
 | 
			
		||||
                categories.value = data.categories;
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('Erro ao carregar categorias:', error);
 | 
			
		||||
            showMessage(categoriesMessage, 'Erro ao carregar categorias', 'error');
 | 
			
		||||
        } finally {
 | 
			
		||||
            loadingCategories.value = false;
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const updateProfile = async () => {
 | 
			
		||||
        updatingProfile.value = true;
 | 
			
		||||
        profileMessage.value = null;
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            const user = JSON.parse(localStorage.getItem('user'));
 | 
			
		||||
            const response = await fetch('/api/user/profile', {
 | 
			
		||||
                method: 'PUT',
 | 
			
		||||
                headers: {
 | 
			
		||||
                    'Content-Type': 'application/json',
 | 
			
		||||
                },
 | 
			
		||||
                body: JSON.stringify({
 | 
			
		||||
                    user_id: user.id,
 | 
			
		||||
                    name: profileForm.value.name,
 | 
			
		||||
                    email: profileForm.value.email,
 | 
			
		||||
                    profile: profileForm.value.profile
 | 
			
		||||
                })
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const data = await response.json();
 | 
			
		||||
 | 
			
		||||
            if (response.ok) {
 | 
			
		||||
                // Atualizar localStorage
 | 
			
		||||
                const updatedUser = { ...user, name: data.user.name, email: data.user.email, profile: data.user.profile };
 | 
			
		||||
                localStorage.setItem('user', JSON.stringify(updatedUser));
 | 
			
		||||
                
 | 
			
		||||
                showMessage(profileMessage, 'Perfil atualizado com sucesso!', 'success');
 | 
			
		||||
            } else {
 | 
			
		||||
                showMessage(profileMessage, data.error || 'Erro ao atualizar perfil', 'error');
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('Erro:', error);
 | 
			
		||||
            showMessage(profileMessage, 'Erro de conexão com o servidor', 'error');
 | 
			
		||||
        } finally {
 | 
			
		||||
            updatingProfile.value = false;
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const changePassword = async () => {
 | 
			
		||||
        changingPassword.value = true;
 | 
			
		||||
        passwordMessage.value = null;
 | 
			
		||||
 | 
			
		||||
        // Validações
 | 
			
		||||
        if (passwordForm.value.newPassword !== passwordForm.value.confirmPassword) {
 | 
			
		||||
            showMessage(passwordMessage, 'As senhas não coincidem', 'error');
 | 
			
		||||
            changingPassword.value = false;
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (passwordForm.value.newPassword.length < 6) {
 | 
			
		||||
            showMessage(passwordMessage, 'A senha deve ter pelo menos 6 caracteres', 'error');
 | 
			
		||||
            changingPassword.value = false;
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            const user = JSON.parse(localStorage.getItem('user'));
 | 
			
		||||
            const response = await fetch('/api/user/change-password', {
 | 
			
		||||
                method: 'PUT',
 | 
			
		||||
                headers: {
 | 
			
		||||
                    'Content-Type': 'application/json',
 | 
			
		||||
                },
 | 
			
		||||
                body: JSON.stringify({
 | 
			
		||||
                    user_id: user.id,
 | 
			
		||||
                    current_password: passwordForm.value.currentPassword,
 | 
			
		||||
                    new_password: passwordForm.value.newPassword
 | 
			
		||||
                })
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const data = await response.json();
 | 
			
		||||
 | 
			
		||||
            if (response.ok) {
 | 
			
		||||
                showMessage(passwordMessage, 'Senha alterada com sucesso!', 'success');
 | 
			
		||||
                passwordForm.value = {
 | 
			
		||||
                    currentPassword: '',
 | 
			
		||||
                    newPassword: '',
 | 
			
		||||
                    confirmPassword: ''
 | 
			
		||||
                };
 | 
			
		||||
            } else {
 | 
			
		||||
                showMessage(passwordMessage, data.error || 'Erro ao alterar senha', 'error');
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('Erro:', error);
 | 
			
		||||
            showMessage(passwordMessage, 'Erro de conexão com o servidor', 'error');
 | 
			
		||||
        } finally {
 | 
			
		||||
            changingPassword.value = false;
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const addCategory = async () => {
 | 
			
		||||
        addingCategory.value = true;
 | 
			
		||||
        categoryMessage.value = null;
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            const user = JSON.parse(localStorage.getItem('user'));
 | 
			
		||||
            const response = await fetch('/api/categories', {
 | 
			
		||||
                method: 'POST',
 | 
			
		||||
                headers: {
 | 
			
		||||
                    'Content-Type': 'application/json',
 | 
			
		||||
                },
 | 
			
		||||
                body: JSON.stringify({
 | 
			
		||||
                    user_id: user.id,
 | 
			
		||||
                    name: newCategory.value.name,
 | 
			
		||||
                    type: newCategory.value.type,
 | 
			
		||||
                    color: newCategory.value.color
 | 
			
		||||
                })
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const data = await response.json();
 | 
			
		||||
 | 
			
		||||
            if (response.ok) {
 | 
			
		||||
                showMessage(categoryMessage, 'Categoria adicionada com sucesso!', 'success');
 | 
			
		||||
                closeCategoryModal();
 | 
			
		||||
                loadCategories();
 | 
			
		||||
            } else {
 | 
			
		||||
                showMessage(categoryMessage, data.error || 'Erro ao adicionar categoria', 'error');
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('Erro:', error);
 | 
			
		||||
            showMessage(categoryMessage, 'Erro de conexão com o servidor', 'error');
 | 
			
		||||
        } finally {
 | 
			
		||||
            addingCategory.value = false;
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const closeCategoryModal = () => {
 | 
			
		||||
        showAddCategoryModal.value = false;
 | 
			
		||||
        newCategory.value = {
 | 
			
		||||
            name: '',
 | 
			
		||||
            type: 'expense',
 | 
			
		||||
            color: '#6c757d'
 | 
			
		||||
        };
 | 
			
		||||
        categoryMessage.value = null;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const showMessage = (messageRef, text, type) => {
 | 
			
		||||
        messageRef.value = { text, type };
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
            messageRef.value = null;
 | 
			
		||||
        }, 5000);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // Lifecycle
 | 
			
		||||
    onMounted(() => {
 | 
			
		||||
        loadUserProfile();
 | 
			
		||||
        loadCategories();
 | 
			
		||||
    });
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
    /* Estilos Customizados do CtrlCash */
 | 
			
		||||
    .bg-primary-dark { background-color: #1A3B5E !important; }
 | 
			
		||||
    .text-primary-dark { color: #1A3B5E !important; }
 | 
			
		||||
    .btn-primary-feature { 
 | 
			
		||||
        background-color: #1A3B5E !important; 
 | 
			
		||||
        border-color: #1A3B5E !important; 
 | 
			
		||||
        color: white; 
 | 
			
		||||
    }
 | 
			
		||||
    .btn-primary-feature:disabled {
 | 
			
		||||
        opacity: 0.6;
 | 
			
		||||
        cursor: not-allowed;
 | 
			
		||||
    }
 | 
			
		||||
    .btn-success-feature { 
 | 
			
		||||
        background-color: #2ECC71 !important; 
 | 
			
		||||
        border-color: #2ECC71 !important; 
 | 
			
		||||
        color: white; 
 | 
			
		||||
    }
 | 
			
		||||
    .active-feature { 
 | 
			
		||||
        background-color: #1A3B5E !important; 
 | 
			
		||||
        color: white !important; 
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .max-w-7xl { max-width: 80rem; }
 | 
			
		||||
    .mx-auto { margin-left: auto !important; margin-right: auto !important; }
 | 
			
		||||
 | 
			
		||||
    /* Modal backdrop */
 | 
			
		||||
    .modal-backdrop {
 | 
			
		||||
        opacity: 0.5;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* Form controls */
 | 
			
		||||
    .form-control:focus {
 | 
			
		||||
        border-color: #1A3B5E;
 | 
			
		||||
        box-shadow: 0 0 0 0.25rem rgba(26, 59, 94, 0.25);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* List group items */
 | 
			
		||||
    .list-group-item {
 | 
			
		||||
        border: 1px solid #dee2e6;
 | 
			
		||||
        margin-bottom: 5px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* Color picker */
 | 
			
		||||
    .form-control-color {
 | 
			
		||||
        width: 50px;
 | 
			
		||||
        height: 38px;
 | 
			
		||||
    }
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,154 +0,0 @@
 | 
			
		||||
<script setup>
 | 
			
		||||
    import HeaderPublic from '../components/HeaderPublic.vue'
 | 
			
		||||
    import FooterPublic from '../components/FooterPublic.vue'
 | 
			
		||||
</script>
 | 
			
		||||
<template>
 | 
			
		||||
    <HeaderPublic />
 | 
			
		||||
    
 | 
			
		||||
   
 | 
			
		||||
    <main class="main-no-gap"> 
 | 
			
		||||
        
 | 
			
		||||
        
 | 
			
		||||
        <div class="bg-especial">
 | 
			
		||||
            <section class="container p-3 row align-items-center m-auto">
 | 
			
		||||
                
 | 
			
		||||
                <div class="col-md-6 col-12 py-5">
 | 
			
		||||
                    <h1 class="text-white display-10 text fw-bold text-break">Seu dinheiro. <br> Seu controle total.</h1>
 | 
			
		||||
                    <p class="text-secondary fs-5">Organize, invista e alcance seus objetivos financeiros com segurança e facilidade. </p>
 | 
			
		||||
                    
 | 
			
		||||
                    <router-link to="/register" class="btn btn-success btn-lg mt-3 fw-bold me-3">Abra sua conta</router-link>
 | 
			
		||||
                    <router-link to="/features" class="btn btn-outline-light btn-lg mt-3">Conheça os recursos</router-link>
 | 
			
		||||
                </div>
 | 
			
		||||
                
 | 
			
		||||
                
 | 
			
		||||
                <div class="col-md-6 col-12 d-flex justify-content-center py-5">
 | 
			
		||||
                    <img src="@/assets/Hero.png" alt="Ilustração de controle financeiro do CtrlCash" class="img-fluid">
 | 
			
		||||
                </div>
 | 
			
		||||
            </section>
 | 
			
		||||
        </div>
 | 
			
		||||
        
 | 
			
		||||
       
 | 
			
		||||
        <section class="container py-5">
 | 
			
		||||
            
 | 
			
		||||
            <div class="row text-center mb-5">
 | 
			
		||||
                <div class="col-lg-8 mx-auto">
 | 
			
		||||
                    
 | 
			
		||||
                    <h2 class="fw-bold display-6 text-primary-dark">Concentre-se no que importa. Nós cuidamos do resto.</h2>
 | 
			
		||||
                    <p class="lead text-secondary-feature mt-3">Descubra as funcionalidades que dão o controle total da sua vida financeira na palma da sua mão.</p>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            <div class="row g-5"> 
 | 
			
		||||
                
 | 
			
		||||
               
 | 
			
		||||
                <div class="col-lg-4 col-md-6 col-12">
 | 
			
		||||
                    <div class="d-flex align-items-start">
 | 
			
		||||
                        <i class="bi bi-pie-chart-fill display-5 me-5 text-success-feature flex-shrink-0"></i>
 | 
			
		||||
                        <div>
 | 
			
		||||
                            <h4 class="fw-bold mb-1 text-dark-feature">Orçamento Inteligente</h4>
 | 
			
		||||
                            <p class="text-secondary-feature">Crie orçamentos por categoria, receba alertas e visualize seus gastos em tempo real, evitando surpresas no fim do mês.</p>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                
 | 
			
		||||
                <div class="col-lg-4 col-md-6 col-12">
 | 
			
		||||
                    <div class="d-flex align-items-start">
 | 
			
		||||
                        <i class="bi bi-rocket-takeoff-fill display-5 me-4 text-success-feature flex-shrink-0"></i>
 | 
			
		||||
                        <div>
 | 
			
		||||
                            <h4 class="fw-bold mb-1 text-dark-feature">Metas e Investimentos</h4>
 | 
			
		||||
                            <p class="text-secondary-feature">Defina seus objetivos (viagem, casa, carro) e o CtrlCash te ajuda a poupar, investir e acompanhar seu progresso.</p>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                
 | 
			
		||||
                <div class="col-lg-4 col-md-6 col-12">
 | 
			
		||||
                    <div class="d-flex align-items-start">
 | 
			
		||||
                        <i class="bi bi-lightning-charge-fill display-5 me-4 text-success-feature flex-shrink-0"></i>
 | 
			
		||||
                        <div>
 | 
			
		||||
                            <h4 class="fw-bold mb-1 text-dark-feature">PIX e Pagamentos Rápidos</h4>
 | 
			
		||||
                            <p class="text-secondary-feature">Envie e receba dinheiro em segundos. Pague contas e boletos com agilidade, tudo em um só lugar.</p>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
               
 | 
			
		||||
                <div class="col-lg-4 col-md-6 col-12">
 | 
			
		||||
                    <div class="d-flex align-items-start">
 | 
			
		||||
                        <i class="bi bi-clock-history display-5 me-4 text-success-feature flex-shrink-0"></i>
 | 
			
		||||
                        <div>
 | 
			
		||||
                            <h4 class="fw-bold mb-1 text-dark-feature">Histórico Automático</h4>
 | 
			
		||||
                            <p class="text-secondary-feature">Chega de planilhas. O app registra todas as transações, categoriza e gera relatórios visuais inteligentes para você.</p>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <!-- Recurso 5: Segurança e Alertas -->
 | 
			
		||||
                <div class="col-lg-4 col-md-6 col-12">
 | 
			
		||||
                    <div class="d-flex align-items-start">
 | 
			
		||||
                        <i class="bi bi-lock-fill display-5 me-4 text-success-feature flex-shrink-0"></i>
 | 
			
		||||
                        <div>
 | 
			
		||||
                            <h4 class="fw-bold mb-1 text-dark-feature">Segurança e Alertas</h4>
 | 
			
		||||
                            <p class="text-secondary-feature">Monitoramento 24h e notificações em tempo real. Sua segurança é prioridade máxima com tecnologia de ponta.</p>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
               
 | 
			
		||||
                <div class="col-lg-4 col-md-6 col-12">
 | 
			
		||||
                    <div class="d-flex align-items-start">
 | 
			
		||||
                        <i class="bi bi-credit-card-2-front-fill display-5 me-4 text-success-feature flex-shrink-0"></i>
 | 
			
		||||
                        <div>
 | 
			
		||||
                            <h4 class="fw-bold mb-1 text-dark-feature">Central de Cartões</h4>
 | 
			
		||||
                            <p class="text-secondary-feature">Controle seus limites, bloqueie e desbloqueie cartões virtuais e físicos diretamente pelo app, com total autonomia.</p>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
            </div>
 | 
			
		||||
        </section>
 | 
			
		||||
        
 | 
			
		||||
    </main>
 | 
			
		||||
    <FooterPublic />
 | 
			
		||||
</template>
 | 
			
		||||
<style scoped>
 | 
			
		||||
    
 | 
			
		||||
    .main-no-gap {
 | 
			
		||||
        margin-top: 0;
 | 
			
		||||
        padding-top: 0;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    
 | 
			
		||||
    .bg-especial {
 | 
			
		||||
        background-color: #122942; 
 | 
			
		||||
    }
 | 
			
		||||
    .btn-success {
 | 
			
		||||
        background-color: #2ECC71; 
 | 
			
		||||
        border-color: #2ECC71;
 | 
			
		||||
    }
 | 
			
		||||
    .btn-success:hover {
 | 
			
		||||
        background-color: #26a95f;
 | 
			
		||||
        border-color: #26a95f;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    .text-secondary {
 | 
			
		||||
        color: #D1D5DB !important; 
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
   
 | 
			
		||||
    .text-primary-dark {
 | 
			
		||||
        color: #1A3B5E !important; 
 | 
			
		||||
    }
 | 
			
		||||
    .text-success-feature {
 | 
			
		||||
        color: #2ECC71 !important;
 | 
			
		||||
    }
 | 
			
		||||
    .text-dark-feature {
 | 
			
		||||
        color: #343a40 !important; 
 | 
			
		||||
    }
 | 
			
		||||
    .text-secondary-feature {
 | 
			
		||||
        color: #6c757d !important; 
 | 
			
		||||
    }
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,127 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <HeaderPublic />
 | 
			
		||||
    <div class="login-container d-flex align-items-center justify-content-center min-vh-100 p-3">
 | 
			
		||||
        
 | 
			
		||||
        <div class="login-card p-5 shadow-lg rounded-4 bg-white">
 | 
			
		||||
           
 | 
			
		||||
            <div class="text-center mb-4">
 | 
			
		||||
                <img src="@/assets/CtrlCash-blue.png" alt="CtrlCash Logo" width="150" class="mb-3">
 | 
			
		||||
                <h2 class="fw-bold text-primary-dark">Login</h2>
 | 
			
		||||
                <p class="text-secondary-dark">Insira seus dados para continuar o controle.</p>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <form @submit.prevent="handleLogin">
 | 
			
		||||
                <div class="mb-3">
 | 
			
		||||
                    <label for="email" class="form-label fw-medium text-primary-dark">E-mail</label>
 | 
			
		||||
                    <div class="input-group">
 | 
			
		||||
                        <span class="input-group-text"><i class="bi bi-envelope-fill"></i></span>
 | 
			
		||||
                        <input type="email" class="form-control" id="email" v-model="loginForm.email" required placeholder="seu.email@exemplo.com">
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
               
 | 
			
		||||
                <div class="mb-4">
 | 
			
		||||
                    <label for="password" class="form-label fw-medium text-primary-dark">Senha</label>
 | 
			
		||||
                    <div class="input-group">
 | 
			
		||||
                        <span class="input-group-text"><i class="bi bi-lock-fill"></i></span>
 | 
			
		||||
                        <input type="password" class="form-control" id="password" v-model="loginForm.password" required placeholder="********">
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="text-end mt-2">
 | 
			
		||||
    
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <!-- Mensagem de erro -->
 | 
			
		||||
                <div v-if="error" class="alert alert-danger">
 | 
			
		||||
                    {{ error }}
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <button type="submit" class="btn btn-primary-feature w-100 fw-bold py-2 shadow-sm" :disabled="loading">
 | 
			
		||||
                    <span v-if="loading" class="spinner-border spinner-border-sm me-2"></span>
 | 
			
		||||
                    {{ loading ? 'Entrando...' : 'Acessar Minha Conta' }}
 | 
			
		||||
                </button>
 | 
			
		||||
            </form>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
    import { ref } from 'vue';
 | 
			
		||||
    import { useRouter } from 'vue-router';
 | 
			
		||||
    import HeaderPublic from '@/components/HeaderPublic.vue';
 | 
			
		||||
 | 
			
		||||
    const router = useRouter();
 | 
			
		||||
    const loading = ref(false);
 | 
			
		||||
    const error = ref('');
 | 
			
		||||
 | 
			
		||||
    const loginForm = ref({
 | 
			
		||||
        email: '',
 | 
			
		||||
        password: ''
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const handleLogin = async () => {
 | 
			
		||||
        loading.value = true;
 | 
			
		||||
        error.value = '';
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            const response = await fetch('/api/auth/login', {
 | 
			
		||||
                method: 'POST',
 | 
			
		||||
                headers: {
 | 
			
		||||
                    'Content-Type': 'application/json',
 | 
			
		||||
                },
 | 
			
		||||
                body: JSON.stringify(loginForm.value)
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const data = await response.json();
 | 
			
		||||
 | 
			
		||||
            if (response.ok) {
 | 
			
		||||
                // Salvar usuário no localStorage
 | 
			
		||||
                localStorage.setItem('user', JSON.stringify(data.user));
 | 
			
		||||
                localStorage.setItem('isAuthenticated', 'true');
 | 
			
		||||
                
 | 
			
		||||
                // Redirecionar para dashboard
 | 
			
		||||
                router.push('/dashboard');
 | 
			
		||||
            } else {
 | 
			
		||||
                error.value = data.error || 'Erro ao fazer login';
 | 
			
		||||
            }
 | 
			
		||||
        } catch (err) {
 | 
			
		||||
            console.error('Erro:', err);
 | 
			
		||||
            error.value = 'Erro de conexão com o servidor';
 | 
			
		||||
        } finally {
 | 
			
		||||
            loading.value = false;
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
    .text-primary-dark { color: #1A3B5E !important; }
 | 
			
		||||
    .text-secondary-dark { color: #6c757d !important; }
 | 
			
		||||
    
 | 
			
		||||
    .login-container { background-color: #F8F9FA; }
 | 
			
		||||
    .login-card { max-width: 420px; width: 100%; }
 | 
			
		||||
    
 | 
			
		||||
    .btn-primary-feature {
 | 
			
		||||
        background-color: #1A3B5E;
 | 
			
		||||
        border-color: #1A3B5E;
 | 
			
		||||
        color: white;
 | 
			
		||||
        transition: background-color 0.2s;
 | 
			
		||||
    }
 | 
			
		||||
    .btn-primary-feature:hover:not(:disabled) {
 | 
			
		||||
        background-color: #29517b;
 | 
			
		||||
        border-color: #29517b;
 | 
			
		||||
    }
 | 
			
		||||
    .btn-primary-feature:disabled {
 | 
			
		||||
        opacity: 0.6;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .form-control:focus {
 | 
			
		||||
        border-color: #1A3B5E;
 | 
			
		||||
        box-shadow: 0 0 0 0.25rem rgba(26, 59, 94, 0.25);
 | 
			
		||||
    }
 | 
			
		||||
    .input-group-text {
 | 
			
		||||
        background-color: #e9ecef;
 | 
			
		||||
        border-right: none;
 | 
			
		||||
        color: #1A3B5E;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .hover-link:hover { text-decoration: underline !important; }
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,58 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="sobre-nos-page bg-light min-vh-100">
 | 
			
		||||
        
 | 
			
		||||
        <!-- Header Público (Links para Login/Cadastro) -->
 | 
			
		||||
        <HeaderPublic />
 | 
			
		||||
 | 
			
		||||
        <div class="container-fluid py-5 max-w-7xl mx-auto px-4">
 | 
			
		||||
            
 | 
			
		||||
            <div class="row g-5 align-items-center">
 | 
			
		||||
                <!-- Seção de Texto (Sobre a Missão) -->
 | 
			
		||||
                <div class="col-lg-6">
 | 
			
		||||
                    <h1 class="display-5 fw-bold text-primary-dark mb-4">
 | 
			
		||||
                        Nossa Missão: <span class="text-success-feature">Empoderar</span> Suas Finanças.
 | 
			
		||||
                    </h1>
 | 
			
		||||
                    <p class="lead text-muted">
 | 
			
		||||
                        O CtrlCash nasceu da necessidade de simplificar a gestão financeira pessoal. Acreditamos que controlar suas finanças não deve ser um fardo, mas uma ferramenta para alcançar seus objetivos.
 | 
			
		||||
                    </p>
 | 
			
		||||
                    <p class="text-muted">
 | 
			
		||||
                        Nosso sistema oferece uma visão clara e organizada de onde seu dinheiro está indo, permitindo que você tome decisões informadas. Não se trata apenas de registrar gastos, mas de planejar o futuro.
 | 
			
		||||
                    </p>
 | 
			
		||||
                    
 | 
			
		||||
                    <router-link to="/cadastro" class="btn btn-lg btn-success-feature shadow-lg mt-4">
 | 
			
		||||
                        Comece a Controlar Sua Vida Financeira Hoje
 | 
			
		||||
                    </router-link>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <!-- Seção de Imagem/Visual -->
 | 
			
		||||
                <div class="col-lg-6 d-none d-lg-block">
 | 
			
		||||
                    <div class="p-5 bg-white shadow-lg rounded-5 text-center border">
 | 
			
		||||
                        <i class="bi bi-wallet2 display-1 text-primary-dark mb-3"></i>
 | 
			
		||||
                        <h3 class="fw-bold text-primary-dark">Controle Financeiro Descomplicado</h3>
 | 
			
		||||
                        <p class="text-muted">A plataforma ideal para quem busca clareza e inteligência nos gastos diários.</p>
 | 
			
		||||
                        <i class="bi bi-graph-up display-4 text-success-feature mt-3"></i>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        
 | 
			
		||||
        <!-- FOOTER SIMPLES -->
 | 
			
		||||
        <FooterPublic />
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
    import FooterPublic from '@/components/FooterPublic.vue'
 | 
			
		||||
    import HeaderPublic from '@/components/HeaderPublic.vue'
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
    /* Estilos Customizados do CtrlCash (Garantindo que o TXT funcione) */
 | 
			
		||||
    .bg-primary-dark { background-color: #1A3B5E !important; }
 | 
			
		||||
    .text-primary-dark { color: #1A3B5E !important; }
 | 
			
		||||
    .text-success-feature { color: #2ECC71 !important; }
 | 
			
		||||
    .btn-success-feature { background-color: #2ECC71 !important; border-color: #2ECC71 !important; color: white; }
 | 
			
		||||
    
 | 
			
		||||
    .max-w-7xl { max-width: 80rem; }
 | 
			
		||||
    .mx-auto { margin-left: auto !important; margin-right: auto !important; }
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,623 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="transacoes-page bg-light min-vh-100">
 | 
			
		||||
        
 | 
			
		||||
        <!-- Componente do Header -->
 | 
			
		||||
        <HeaderApp />
 | 
			
		||||
 | 
			
		||||
        <div class="container-fluid py-4 max-w-7xl mx-auto px-4">
 | 
			
		||||
            
 | 
			
		||||
            <div class="d-flex justify-content-between align-items-center mb-4">
 | 
			
		||||
                <h1 class="h3 fw-bold text-primary-dark">Minhas Transações</h1>
 | 
			
		||||
                
 | 
			
		||||
                <!-- Botão para abrir o Modal de Nova Transação -->
 | 
			
		||||
                <button 
 | 
			
		||||
                    type="button" 
 | 
			
		||||
                    class="btn btn-success-feature fw-bold shadow-sm d-flex align-items-center" 
 | 
			
		||||
                    @click="openAddModal"
 | 
			
		||||
                >
 | 
			
		||||
                    <i class="bi bi-plus-circle-fill me-2"></i>
 | 
			
		||||
                    Nova Transação
 | 
			
		||||
                </button>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <!-- Card de Filtros e Pesquisa -->
 | 
			
		||||
            <div class="card shadow-sm border-0 mb-4 p-3">
 | 
			
		||||
                <div class="row g-3 align-items-center">
 | 
			
		||||
                    
 | 
			
		||||
                    <!-- Filtro por Tipo -->
 | 
			
		||||
                    <div class="col-md-3">
 | 
			
		||||
                        <label for="filtroTipo" class="form-label small text-muted">Tipo</label>
 | 
			
		||||
                        <select id="filtroTipo" v-model="filters.type" class="form-select" @change="loadTransactions">
 | 
			
		||||
                            <option value="">Todos</option>
 | 
			
		||||
                            <option value="income">Receitas</option>
 | 
			
		||||
                            <option value="expense">Despesas</option>
 | 
			
		||||
                        </select>
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <!-- Filtro por Categoria -->
 | 
			
		||||
                    <div class="col-md-4">
 | 
			
		||||
                        <label for="filtroCategoria" class="form-label small text-muted">Categoria</label>
 | 
			
		||||
                        <select id="filtroCategoria" v-model="filters.category" class="form-select" @change="loadTransactions">
 | 
			
		||||
                            <option value="">Todas as categorias</option>
 | 
			
		||||
                            <option v-for="category in categories" :key="category.id" :value="category.name">
 | 
			
		||||
                                {{ category.name }}
 | 
			
		||||
                            </option>
 | 
			
		||||
                        </select>
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <!-- Filtro por Período -->
 | 
			
		||||
                    <div class="col-md-5">
 | 
			
		||||
                        <label for="filtroPeriodo" class="form-label small text-muted">Período</label>
 | 
			
		||||
                        <div class="input-group">
 | 
			
		||||
                            <input type="date" v-model="filters.startDate" class="form-control" @change="loadTransactions">
 | 
			
		||||
                            <span class="input-group-text">a</span>
 | 
			
		||||
                            <input type="date" v-model="filters.endDate" class="form-control" @change="loadTransactions">
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <!-- Tabela de Transações -->
 | 
			
		||||
            <div class="card shadow-sm border-0">
 | 
			
		||||
                <div class="card-body p-0">
 | 
			
		||||
                    
 | 
			
		||||
                    <!-- Loading State -->
 | 
			
		||||
                    <div v-if="loading" class="text-center py-5">
 | 
			
		||||
                        <div class="spinner-border text-primary-dark" role="status">
 | 
			
		||||
                            <span class="visually-hidden">Carregando...</span>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <p class="text-muted mt-2">Carregando transações...</p>
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <!-- Tabela com Dados -->
 | 
			
		||||
                    <div v-else class="table-responsive">
 | 
			
		||||
                        <table class="table table-hover mb-0">
 | 
			
		||||
                            <thead class="bg-light">
 | 
			
		||||
                                <tr>
 | 
			
		||||
                                    <th scope="col" class="text-primary-dark fw-bold">Descrição</th>
 | 
			
		||||
                                    <th scope="col" class="text-primary-dark fw-bold">Categoria</th>
 | 
			
		||||
                                    <th scope="col" class="text-primary-dark fw-bold text-center">Tipo</th>
 | 
			
		||||
                                    <th scope="col" class="text-primary-dark fw-bold text-end">Valor</th>
 | 
			
		||||
                                    <th scope="col" class="text-primary-dark fw-bold">Data</th>
 | 
			
		||||
                                    <th scope="col" class="text-primary-dark fw-bold text-center">Ações</th>
 | 
			
		||||
                                </tr>
 | 
			
		||||
                            </thead>
 | 
			
		||||
                            <tbody>
 | 
			
		||||
                                <!-- Transações Dinâmicas -->
 | 
			
		||||
                                <tr v-for="transaction in transactions" :key="transaction.id">
 | 
			
		||||
                                    <td>{{ transaction.description }}</td>
 | 
			
		||||
                                    <td>
 | 
			
		||||
                                        <span class="badge" :style="{ 
 | 
			
		||||
                                            backgroundColor: getCategoryColor(transaction.category),
 | 
			
		||||
                                            color: 'white'
 | 
			
		||||
                                        }">
 | 
			
		||||
                                            {{ transaction.category }}
 | 
			
		||||
                                        </span>
 | 
			
		||||
                                    </td>
 | 
			
		||||
                                    <td class="text-center">
 | 
			
		||||
                                        <span 
 | 
			
		||||
                                            class="badge"
 | 
			
		||||
                                            :class="transaction.type === 'income' ? 'bg-success-feature' : 'bg-danger'"
 | 
			
		||||
                                        >
 | 
			
		||||
                                            <i :class="transaction.type === 'income' ? 'bi bi-arrow-up' : 'bi bi-arrow-down'"></i>
 | 
			
		||||
                                            {{ transaction.type === 'income' ? 'Receita' : 'Despesa' }}
 | 
			
		||||
                                        </span>
 | 
			
		||||
                                    </td>
 | 
			
		||||
                                    <td class="text-end fw-bold" :class="transaction.type === 'income' ? 'text-success-feature' : 'text-danger'">
 | 
			
		||||
                                        {{ formatCurrency(transaction.amount) }}
 | 
			
		||||
                                    </td>
 | 
			
		||||
                                    <td>{{ formatDate(transaction.date) }}</td>
 | 
			
		||||
                                    <td class="text-center">
 | 
			
		||||
                                        <!-- ✅ BOTÃO DE DELETAR VISÍVEL -->
 | 
			
		||||
                                        <button 
 | 
			
		||||
                                            class="btn btn-sm btn-outline-danger" 
 | 
			
		||||
                                            title="Excluir transação"
 | 
			
		||||
                                            @click="deleteTransaction(transaction.id)"
 | 
			
		||||
                                        >
 | 
			
		||||
                                            <i class="bi bi-trash"></i>
 | 
			
		||||
                                        </button>
 | 
			
		||||
                                    </td>
 | 
			
		||||
                                </tr>
 | 
			
		||||
 | 
			
		||||
                                <!-- Estado Vazio -->
 | 
			
		||||
                                <tr v-if="transactions.length === 0 && !loading">
 | 
			
		||||
                                    <td colspan="6" class="text-center py-5 text-muted">
 | 
			
		||||
                                        <i class="bi bi-receipt fs-1 opacity-50 d-block mb-2"></i>
 | 
			
		||||
                                        Nenhuma transação encontrada
 | 
			
		||||
                                        <br>
 | 
			
		||||
                                        <small>Clique em "Nova Transação" para adicionar sua primeira transação</small>
 | 
			
		||||
                                    </td>
 | 
			
		||||
                                </tr>
 | 
			
		||||
                            </tbody>
 | 
			
		||||
                        </table>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                
 | 
			
		||||
                <!-- Footer com Contador -->
 | 
			
		||||
                <div class="card-footer bg-white text-center" v-if="transactions.length > 0">
 | 
			
		||||
                    <small class="text-muted">Exibindo {{ transactions.length }} transações</small>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <!-- Modal de Nova Transação -->
 | 
			
		||||
        <div v-if="showAddModal" class="modal-overlay">
 | 
			
		||||
            <div class="modal-container">
 | 
			
		||||
                <div class="modal-content">
 | 
			
		||||
                    <div class="modal-header bg-primary-dark text-white">
 | 
			
		||||
                        <h5 class="modal-title fw-bold">
 | 
			
		||||
                            Adicionar Nova Transação
 | 
			
		||||
                        </h5>
 | 
			
		||||
                        <button type="button" class="btn-close btn-close-white" @click="closeModal"></button>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="modal-body">
 | 
			
		||||
                        <form @submit.prevent="saveTransaction">
 | 
			
		||||
                            
 | 
			
		||||
                            <!-- Tipo de Transação (Receita/Despesa) -->
 | 
			
		||||
                            <div class="mb-3">
 | 
			
		||||
                                <label class="form-label fw-medium text-primary-dark">Tipo</label>
 | 
			
		||||
                                <div class="btn-group w-100" role="group">
 | 
			
		||||
                                    <input type="radio" class="btn-check" v-model="newTransaction.type" id="tipoReceita" value="income">
 | 
			
		||||
                                    <label class="btn btn-outline-success-feature fw-bold" for="tipoReceita" 
 | 
			
		||||
                                           :class="{ 'active': newTransaction.type === 'income' }">
 | 
			
		||||
                                        Receita
 | 
			
		||||
                                    </label>
 | 
			
		||||
 | 
			
		||||
                                    <input type="radio" class="btn-check" v-model="newTransaction.type" id="tipoDespesa" value="expense">
 | 
			
		||||
                                    <label class="btn btn-outline-danger fw-bold" for="tipoDespesa"
 | 
			
		||||
                                           :class="{ 'active': newTransaction.type === 'expense' }">
 | 
			
		||||
                                        Despesa
 | 
			
		||||
                                    </label>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            
 | 
			
		||||
                            <!-- Descrição -->
 | 
			
		||||
                            <div class="mb-3">
 | 
			
		||||
                                <label for="descricao" class="form-label fw-medium text-primary-dark">Descrição</label>
 | 
			
		||||
                                <input 
 | 
			
		||||
                                    type="text" 
 | 
			
		||||
                                    class="form-control" 
 | 
			
		||||
                                    id="descricao" 
 | 
			
		||||
                                    v-model="newTransaction.description"
 | 
			
		||||
                                    placeholder="Ex: Salário, Aluguel, Supermercado"
 | 
			
		||||
                                    required
 | 
			
		||||
                                >
 | 
			
		||||
                            </div>
 | 
			
		||||
                            
 | 
			
		||||
                            <!-- Valor e Data -->
 | 
			
		||||
                            <div class="row">
 | 
			
		||||
                                <div class="col-md-6 mb-3">
 | 
			
		||||
                                    <label for="valor" class="form-label fw-medium text-primary-dark">Valor (R$)</label>
 | 
			
		||||
                                    <input 
 | 
			
		||||
                                        type="number" 
 | 
			
		||||
                                        step="0.01" 
 | 
			
		||||
                                        min="0.01"
 | 
			
		||||
                                        class="form-control" 
 | 
			
		||||
                                        id="valor" 
 | 
			
		||||
                                        v-model="newTransaction.amount"
 | 
			
		||||
                                        placeholder="0.00"
 | 
			
		||||
                                        required
 | 
			
		||||
                                    >
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <div class="col-md-6 mb-3">
 | 
			
		||||
                                    <label for="data" class="form-label fw-medium text-primary-dark">Data</label>
 | 
			
		||||
                                    <input 
 | 
			
		||||
                                        type="date" 
 | 
			
		||||
                                        class="form-control" 
 | 
			
		||||
                                        id="data" 
 | 
			
		||||
                                        v-model="newTransaction.date"
 | 
			
		||||
                                        required
 | 
			
		||||
                                    >
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
 | 
			
		||||
                            <!-- Categoria -->
 | 
			
		||||
                            <div class="mb-3">
 | 
			
		||||
                                <label for="categoria" class="form-label fw-medium text-primary-dark">Categoria</label>
 | 
			
		||||
                                <select 
 | 
			
		||||
                                    id="categoria" 
 | 
			
		||||
                                    class="form-select" 
 | 
			
		||||
                                    v-model="newTransaction.category"
 | 
			
		||||
                                    required
 | 
			
		||||
                                >
 | 
			
		||||
                                    <option value="">Selecione uma categoria...</option>
 | 
			
		||||
                                    <option 
 | 
			
		||||
                                        v-for="category in filteredCategories" 
 | 
			
		||||
                                        :key="category.id" 
 | 
			
		||||
                                        :value="category.name"
 | 
			
		||||
                                    >
 | 
			
		||||
                                        {{ category.name }}
 | 
			
		||||
                                    </option>
 | 
			
		||||
                                </select>
 | 
			
		||||
                            </div>
 | 
			
		||||
 | 
			
		||||
                            <!-- Mensagens de Erro/Sucesso -->
 | 
			
		||||
                            <div v-if="message" class="alert" :class="message.type === 'success' ? 'alert-success' : 'alert-danger'">
 | 
			
		||||
                                {{ message.text }}
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </form>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="modal-footer">
 | 
			
		||||
                        <button type="button" class="btn btn-secondary" @click="closeModal">Cancelar</button>
 | 
			
		||||
                        <button 
 | 
			
		||||
                            type="button" 
 | 
			
		||||
                            class="btn btn-primary-dark-feature fw-bold" 
 | 
			
		||||
                            @click="saveTransaction"
 | 
			
		||||
                            :disabled="saving"
 | 
			
		||||
                        >
 | 
			
		||||
                            <span v-if="saving" class="spinner-border spinner-border-sm me-2"></span>
 | 
			
		||||
                            {{ saving ? 'Salvando...' : 'Salvar Transação' }}
 | 
			
		||||
                        </button>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
    import { ref, onMounted, computed } from 'vue';
 | 
			
		||||
    import { useRouter } from 'vue-router';
 | 
			
		||||
    import HeaderApp from '@/components/HeaderApp.vue';
 | 
			
		||||
 | 
			
		||||
    const router = useRouter();
 | 
			
		||||
 | 
			
		||||
    // Estados reativos
 | 
			
		||||
    const transactions = ref([]);
 | 
			
		||||
    const categories = ref([]);
 | 
			
		||||
    const loading = ref(false);
 | 
			
		||||
    const saving = ref(false);
 | 
			
		||||
    const showAddModal = ref(false);
 | 
			
		||||
 | 
			
		||||
    const filters = ref({
 | 
			
		||||
        type: '',
 | 
			
		||||
        category: '',
 | 
			
		||||
        startDate: '',
 | 
			
		||||
        endDate: ''
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const newTransaction = ref({
 | 
			
		||||
        type: 'expense',
 | 
			
		||||
        description: '',
 | 
			
		||||
        amount: '',
 | 
			
		||||
        category: '',
 | 
			
		||||
        date: new Date().toISOString().split('T')[0]
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const message = ref(null);
 | 
			
		||||
 | 
			
		||||
    // Computed
 | 
			
		||||
    const filteredCategories = computed(() => {
 | 
			
		||||
        return categories.value.filter(cat => cat.type === newTransaction.value.type);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Métodos
 | 
			
		||||
    const loadTransactions = async () => {
 | 
			
		||||
        loading.value = true;
 | 
			
		||||
        try {
 | 
			
		||||
            const user = JSON.parse(localStorage.getItem('user'));
 | 
			
		||||
            if (!user) {
 | 
			
		||||
                router.push('/login');
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const response = await fetch(`/api/transactions?user_id=${user.id}`);
 | 
			
		||||
            const data = await response.json();
 | 
			
		||||
            
 | 
			
		||||
            if (response.ok) {
 | 
			
		||||
                let filtered = data.transactions;
 | 
			
		||||
 | 
			
		||||
                // Aplicar filtros
 | 
			
		||||
                if (filters.value.type) {
 | 
			
		||||
                    filtered = filtered.filter(t => t.type === filters.value.type);
 | 
			
		||||
                }
 | 
			
		||||
                if (filters.value.category) {
 | 
			
		||||
                    filtered = filtered.filter(t => t.category === filters.value.category);
 | 
			
		||||
                }
 | 
			
		||||
                if (filters.value.startDate) {
 | 
			
		||||
                    filtered = filtered.filter(t => t.date >= filters.value.startDate);
 | 
			
		||||
                }
 | 
			
		||||
                if (filters.value.endDate) {
 | 
			
		||||
                    filtered = filtered.filter(t => t.date <= filters.value.endDate);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                transactions.value = filtered;
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('Erro ao carregar transações:', error);
 | 
			
		||||
            showMessage('Erro ao carregar transações', 'error');
 | 
			
		||||
        } finally {
 | 
			
		||||
            loading.value = false;
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const loadCategories = async () => {
 | 
			
		||||
        try {
 | 
			
		||||
            const user = JSON.parse(localStorage.getItem('user'));
 | 
			
		||||
            const response = await fetch(`/api/categories?user_id=${user.id}`);
 | 
			
		||||
            const data = await response.json();
 | 
			
		||||
            
 | 
			
		||||
            if (response.ok) {
 | 
			
		||||
                categories.value = data.categories;
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('Erro ao carregar categorias:', error);
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const saveTransaction = async () => {
 | 
			
		||||
        saving.value = true;
 | 
			
		||||
        message.value = null;
 | 
			
		||||
 | 
			
		||||
        // Validações
 | 
			
		||||
        if (!newTransaction.value.description.trim()) {
 | 
			
		||||
            showMessage('Descrição é obrigatória', 'error');
 | 
			
		||||
            saving.value = false;
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!newTransaction.value.amount || parseFloat(newTransaction.value.amount) <= 0) {
 | 
			
		||||
            showMessage('Valor deve ser maior que zero', 'error');
 | 
			
		||||
            saving.value = false;
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!newTransaction.value.category) {
 | 
			
		||||
            showMessage('Selecione uma categoria', 'error');
 | 
			
		||||
            saving.value = false;
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            const user = JSON.parse(localStorage.getItem('user'));
 | 
			
		||||
            
 | 
			
		||||
            const transactionData = {
 | 
			
		||||
                user_id: user.id,
 | 
			
		||||
                amount: parseFloat(newTransaction.value.amount),
 | 
			
		||||
                description: newTransaction.value.description.trim(),
 | 
			
		||||
                category: newTransaction.value.category,
 | 
			
		||||
                type: newTransaction.value.type,
 | 
			
		||||
                date: newTransaction.value.date
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            const response = await fetch('/api/transactions', {
 | 
			
		||||
                method: 'POST',
 | 
			
		||||
                headers: {
 | 
			
		||||
                    'Content-Type': 'application/json',
 | 
			
		||||
                },
 | 
			
		||||
                body: JSON.stringify(transactionData)
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const data = await response.json();
 | 
			
		||||
 | 
			
		||||
            if (response.ok) {
 | 
			
		||||
                showMessage('Transação salva com sucesso!', 'success');
 | 
			
		||||
                closeModal();
 | 
			
		||||
                loadTransactions();
 | 
			
		||||
            } else {
 | 
			
		||||
                showMessage(data.error || 'Erro ao salvar transação', 'error');
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('Erro:', error);
 | 
			
		||||
            showMessage('Erro de conexão com o servidor', 'error');
 | 
			
		||||
        } finally {
 | 
			
		||||
            saving.value = false;
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const deleteTransaction = async (transactionId) => {
 | 
			
		||||
        if (!confirm('Tem certeza que deseja excluir esta transação?')) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            const user = JSON.parse(localStorage.getItem('user'));
 | 
			
		||||
            const response = await fetch(`/api/transactions/${transactionId}?user_id=${user.id}`, {
 | 
			
		||||
                method: 'DELETE'
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (response.ok) {
 | 
			
		||||
                showMessage('Transação excluída com sucesso!', 'success');
 | 
			
		||||
                loadTransactions();
 | 
			
		||||
            } else {
 | 
			
		||||
                const data = await response.json();
 | 
			
		||||
                showMessage(data.error || 'Erro ao excluir transação', 'error');
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('Erro:', error);
 | 
			
		||||
            showMessage('Erro de conexão com o servidor', 'error');
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const openAddModal = () => {
 | 
			
		||||
        showAddModal.value = true;
 | 
			
		||||
        resetNewTransaction();
 | 
			
		||||
        message.value = null;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const closeModal = () => {
 | 
			
		||||
        showAddModal.value = false;
 | 
			
		||||
        resetNewTransaction();
 | 
			
		||||
        message.value = null;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const resetNewTransaction = () => {
 | 
			
		||||
        newTransaction.value = {
 | 
			
		||||
            type: 'expense',
 | 
			
		||||
            description: '',
 | 
			
		||||
            amount: '',
 | 
			
		||||
            category: '',
 | 
			
		||||
            date: new Date().toISOString().split('T')[0]
 | 
			
		||||
        };
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const showMessage = (text, type) => {
 | 
			
		||||
        message.value = { text, type };
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
            message.value = null;
 | 
			
		||||
        }, 5000);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const formatCurrency = (value) => {
 | 
			
		||||
        return new Intl.NumberFormat('pt-BR', {
 | 
			
		||||
            style: 'currency',
 | 
			
		||||
            currency: 'BRL'
 | 
			
		||||
        }).format(value || 0);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const formatDate = (dateString) => {
 | 
			
		||||
        if (!dateString) return '-';
 | 
			
		||||
        
 | 
			
		||||
        // Converter YYYY-MM-DD para DD/MM/YYYY
 | 
			
		||||
        if (dateString.match(/^\d{4}-\d{2}-\d{2}$/)) {
 | 
			
		||||
            const [year, month, day] = dateString.split('-');
 | 
			
		||||
            return `${day.padStart(2, '0')}/${month.padStart(2, '0')}/${year}`;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        return dateString;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const getCategoryColor = (categoryName) => {
 | 
			
		||||
        const category = categories.value.find(cat => cat.name === categoryName);
 | 
			
		||||
        return category ? category.color : '#6c757d';
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // Lifecycle
 | 
			
		||||
    onMounted(() => {
 | 
			
		||||
        loadTransactions();
 | 
			
		||||
        loadCategories();
 | 
			
		||||
    });
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
    /* Definição das cores customizadas para o Bootstrap */
 | 
			
		||||
    .bg-primary-dark { background-color: #1A3B5E !important; }
 | 
			
		||||
    .text-primary-dark { color: #1A3B5E !important; }
 | 
			
		||||
    
 | 
			
		||||
    .bg-success-feature { background-color: #2ECC71 !important; }
 | 
			
		||||
    .text-success-feature { color: #2ECC71 !important; }
 | 
			
		||||
    
 | 
			
		||||
    .btn-outline-success-feature {
 | 
			
		||||
        --bs-btn-color: #2ECC71;
 | 
			
		||||
        --bs-btn-border-color: #2ECC71;
 | 
			
		||||
        --bs-btn-hover-bg: #2ECC71;
 | 
			
		||||
        --bs-btn-hover-border-color: #2ECC71;
 | 
			
		||||
        --bs-btn-active-bg: #2ECC71;
 | 
			
		||||
        --bs-btn-active-border-color: #2ECC71;
 | 
			
		||||
        --bs-btn-active-color: white;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .btn-primary-dark-feature {
 | 
			
		||||
        background-color: #1A3B5E;
 | 
			
		||||
        color: white;
 | 
			
		||||
        border-color: #1A3B5E;
 | 
			
		||||
    }
 | 
			
		||||
    .btn-primary-dark-feature:hover:not(:disabled) {
 | 
			
		||||
        background-color: #122841;
 | 
			
		||||
        border-color: #122841;
 | 
			
		||||
        color: white;
 | 
			
		||||
    }
 | 
			
		||||
    .btn-primary-dark-feature:disabled {
 | 
			
		||||
        opacity: 0.6;
 | 
			
		||||
        cursor: not-allowed;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* Utilitário para limitar largura */
 | 
			
		||||
    .max-w-7xl {
 | 
			
		||||
        max-width: 80rem;
 | 
			
		||||
    }
 | 
			
		||||
    .mx-auto {
 | 
			
		||||
        margin-left: auto !important;
 | 
			
		||||
        margin-right: auto !important;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* Modal Customizado */
 | 
			
		||||
    .modal-overlay {
 | 
			
		||||
        position: fixed;
 | 
			
		||||
        top: 0;
 | 
			
		||||
        left: 0;
 | 
			
		||||
        right: 0;
 | 
			
		||||
        bottom: 0;
 | 
			
		||||
        background-color: rgba(0, 0, 0, 0.5);
 | 
			
		||||
        display: flex;
 | 
			
		||||
        justify-content: center;
 | 
			
		||||
        align-items: center;
 | 
			
		||||
        z-index: 1000;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .modal-container {
 | 
			
		||||
        width: 100%;
 | 
			
		||||
        max-width: 500px;
 | 
			
		||||
        margin: 20px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .modal-content {
 | 
			
		||||
        background: white;
 | 
			
		||||
        border-radius: 8px;
 | 
			
		||||
        box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
 | 
			
		||||
        animation: modalAppear 0.3s ease-out;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @keyframes modalAppear {
 | 
			
		||||
        from {
 | 
			
		||||
            opacity: 0;
 | 
			
		||||
            transform: translateY(-20px);
 | 
			
		||||
        }
 | 
			
		||||
        to {
 | 
			
		||||
            opacity: 1;
 | 
			
		||||
            transform: translateY(0);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .modal-header {
 | 
			
		||||
        border-bottom: 1px solid #dee2e6;
 | 
			
		||||
        padding: 1rem 1.5rem;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .modal-body {
 | 
			
		||||
        padding: 1.5rem;
 | 
			
		||||
        max-height: 70vh;
 | 
			
		||||
        overflow-y: auto;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .modal-footer {
 | 
			
		||||
        border-top: 1px solid #dee2e6;
 | 
			
		||||
        padding: 1rem 1.5rem;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* Estilo para botões ativos */
 | 
			
		||||
    .btn-group .btn.active {
 | 
			
		||||
        background-color: inherit;
 | 
			
		||||
        border-color: inherit;
 | 
			
		||||
        color: inherit;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .btn-outline-success-feature.active {
 | 
			
		||||
        background-color: #2ECC71;
 | 
			
		||||
        color: white;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .btn-outline-danger.active {
 | 
			
		||||
        background-color: #dc3545;
 | 
			
		||||
        color: white;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* Estilos para badges */
 | 
			
		||||
    .badge {
 | 
			
		||||
        font-size: 0.75em;
 | 
			
		||||
        padding: 0.35em 0.65em;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* Botão de deletar */
 | 
			
		||||
    .btn-outline-danger {
 | 
			
		||||
        border-color: #dc3545;
 | 
			
		||||
        color: #dc3545;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .btn-outline-danger:hover {
 | 
			
		||||
        background-color: #dc3545;
 | 
			
		||||
        color: white;
 | 
			
		||||
    }
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,239 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="dashboard-page bg-light min-vh-100">
 | 
			
		||||
        <HeaderApp />
 | 
			
		||||
        <div class="container-fluid py-4 max-w-7xl mx-auto px-4">
 | 
			
		||||
            
 | 
			
		||||
            <h1 class="h3 fw-bold text-primary-dark mb-4">Visão Geral</h1>
 | 
			
		||||
 | 
			
		||||
            <!-- Sumário (Cards de Resumo) -->
 | 
			
		||||
            <div class="row g-4 mb-5">
 | 
			
		||||
                <div class="col-lg-4 col-md-6 col-sm-12">
 | 
			
		||||
                    <div class="card shadow-sm border-0 h-100 p-3">
 | 
			
		||||
                        <div class="card-body">
 | 
			
		||||
                            <h5 class="card-title text-muted mb-3">Saldo Total</h5>
 | 
			
		||||
                            <div class="d-flex justify-content-between align-items-center">
 | 
			
		||||
                                <p class="card-text fs-3 fw-bold text-primary-dark">{{ formatCurrency(summary.balance) }}</p>
 | 
			
		||||
                                <i class="bi bi-wallet2 fs-2 text-primary-dark opacity-75"></i>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <small :class="summary.balance >= 0 ? 'text-success-feature' : 'text-danger'" class="fw-medium">
 | 
			
		||||
                                {{ getBalanceTrend() }}
 | 
			
		||||
                            </small>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <div class="col-lg-4 col-md-6 col-sm-12">
 | 
			
		||||
                    <div class="card shadow-sm border-0 h-100 p-3">
 | 
			
		||||
                        <div class="card-body">
 | 
			
		||||
                            <h5 class="card-title text-muted mb-3">Receitas do Mês</h5>
 | 
			
		||||
                            <div class="d-flex justify-content-between align-items-center">
 | 
			
		||||
                                <p class="card-text fs-3 fw-bold text-success-feature">{{ formatCurrency(summary.total_income) }}</p>
 | 
			
		||||
                                <i class="bi bi-arrow-up-circle-fill fs-2 text-success-feature opacity-75"></i>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <small class="text-muted">{{ getIncomeProgress() }}</small>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <div class="col-lg-4 col-md-12 col-sm-12">
 | 
			
		||||
                    <div class="card shadow-sm border-0 h-100 p-3">
 | 
			
		||||
                        <div class="card-body">
 | 
			
		||||
                            <h5 class="card-title text-muted mb-3">Despesas do Mês</h5>
 | 
			
		||||
                            <div class="d-flex justify-content-between align-items-center">
 | 
			
		||||
                                <p class="card-text fs-3 fw-bold text-danger">{{ formatCurrency(summary.total_expenses) }}</p>
 | 
			
		||||
                                <i class="bi bi-arrow-down-circle-fill fs-2 text-danger opacity-75"></i>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <small class="text-muted">{{ getExpenseLimit() }}</small>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <!-- Gráfico e Atividade Recente -->
 | 
			
		||||
            <div class="row g-4">
 | 
			
		||||
                <div class="col-lg-8">
 | 
			
		||||
                    <div class="card shadow-sm border-0 h-100 p-4">
 | 
			
		||||
                        <div class="d-flex justify-content-between align-items-center mb-3">
 | 
			
		||||
                            <h4 class="fw-bold text-primary-dark mb-0">Evolução Financeira</h4>
 | 
			
		||||
                            <div class="btn-group btn-group-sm">
 | 
			
		||||
                                <button @click="loadChartData('monthly')" class="btn btn-outline-primary" :class="{ 'active': chartType === 'monthly' }">
 | 
			
		||||
                                    Mensal
 | 
			
		||||
                                </button>
 | 
			
		||||
                                <button @click="loadChartData('categories')" class="btn btn-outline-primary" :class="{ 'active': chartType === 'categories' }">
 | 
			
		||||
                                    Categorias
 | 
			
		||||
                                </button>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        
 | 
			
		||||
                        <div v-if="chartImage" class="chart-container rounded-3 text-center">
 | 
			
		||||
                            <img :src="chartImage" alt="Gráfico Financeiro" class="img-fluid rounded" style="max-height: 400px;">
 | 
			
		||||
                        </div>
 | 
			
		||||
                        
 | 
			
		||||
                        <div v-else class="matplotlib-placeholder bg-light-subtle rounded-3 border border-dashed p-5 text-center d-flex flex-column align-items-center justify-content-center" style="min-height: 400px;">
 | 
			
		||||
                            <div v-if="loadingChart" class="spinner-border text-primary-dark mb-3" role="status"></div>
 | 
			
		||||
                            <i class="bi bi-bar-chart-fill fs-1 text-primary-dark opacity-25 mb-2"></i>
 | 
			
		||||
                            <p class="text-primary-dark opacity-50 mb-0">
 | 
			
		||||
                                {{ loadingChart ? 'Carregando visualização...' : 'Selecione um tipo de gráfico' }}
 | 
			
		||||
                            </p>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <div class="col-lg-4">
 | 
			
		||||
                    <div class="card shadow-sm border-0 h-100 p-4">
 | 
			
		||||
                        <h4 class="fw-bold text-primary-dark mb-3">Atividade Recente</h4>
 | 
			
		||||
                        
 | 
			
		||||
                        <div v-if="loadingTransactions" class="text-center py-4">
 | 
			
		||||
                            <div class="spinner-border text-primary-dark" role="status"></div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        
 | 
			
		||||
                        <ul v-else class="list-group list-group-flush">
 | 
			
		||||
                            <li v-for="transaction in summary.recent_transactions" :key="transaction.id" class="list-group-item d-flex justify-content-between align-items-center">
 | 
			
		||||
                                <div class="transaction-info">
 | 
			
		||||
                                    <div class="fw-medium">{{ transaction.description }}</div>
 | 
			
		||||
                                    <small class="text-muted">
 | 
			
		||||
                                        {{ formatDate(transaction.date) }} • {{ transaction.category }}
 | 
			
		||||
                                    </small>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <span :class="transaction.type === 'income' ? 'badge bg-success-feature' : 'badge bg-danger'">
 | 
			
		||||
                                    {{ transaction.type === 'income' ? '+' : '-' }}{{ formatCurrency(transaction.amount) }}
 | 
			
		||||
                                </span>
 | 
			
		||||
                            </li>
 | 
			
		||||
                            
 | 
			
		||||
                            <li v-if="summary.recent_transactions && summary.recent_transactions.length === 0" class="list-group-item text-center text-muted py-4">
 | 
			
		||||
                                <i class="bi bi-receipt fs-4 opacity-50 d-block mb-2"></i>
 | 
			
		||||
                                Nenhuma transação recente
 | 
			
		||||
                            </li>
 | 
			
		||||
                            
 | 
			
		||||
                            <li class="list-group-item text-center pt-3 border-0">
 | 
			
		||||
                                <router-link to="/transacoes" class="btn btn-link btn-sm text-primary-dark fw-medium">
 | 
			
		||||
                                    Ver todas as transações
 | 
			
		||||
                                </router-link>
 | 
			
		||||
                            </li>
 | 
			
		||||
                        </ul>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
    import { ref, onMounted } from 'vue';
 | 
			
		||||
    import { useRouter } from 'vue-router';
 | 
			
		||||
    import HeaderApp from '@/components/HeaderApp.vue';
 | 
			
		||||
 | 
			
		||||
    const router = useRouter();
 | 
			
		||||
    const summary = ref({
 | 
			
		||||
        total_income: 0,
 | 
			
		||||
        total_expenses: 0,
 | 
			
		||||
        balance: 0,
 | 
			
		||||
        recent_transactions: []
 | 
			
		||||
    });
 | 
			
		||||
    const chartImage = ref('');
 | 
			
		||||
    const chartType = ref('monthly');
 | 
			
		||||
    const loadingChart = ref(false);
 | 
			
		||||
    const loadingTransactions = ref(false);
 | 
			
		||||
 | 
			
		||||
    const loadDashboardData = async () => {
 | 
			
		||||
        loadingTransactions.value = true;
 | 
			
		||||
        try {
 | 
			
		||||
            const user = JSON.parse(localStorage.getItem('user'));
 | 
			
		||||
            if (!user) {
 | 
			
		||||
                router.push('/login');
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const response = await fetch(`/api/dashboard/summary?user_id=${user.id}`);
 | 
			
		||||
            if (response.ok) {
 | 
			
		||||
                const data = await response.json();
 | 
			
		||||
                summary.value = data;
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('Erro ao carregar dashboard:', error);
 | 
			
		||||
        } finally {
 | 
			
		||||
            loadingTransactions.value = false;
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const loadChartData = async (type = 'monthly') => {
 | 
			
		||||
        loadingChart.value = true;
 | 
			
		||||
        chartType.value = type;
 | 
			
		||||
        try {
 | 
			
		||||
            const user = JSON.parse(localStorage.getItem('user'));
 | 
			
		||||
            const response = await fetch(`/api/dashboard/chart?user_id=${user.id}&type=${type}`);
 | 
			
		||||
            if (response.ok) {
 | 
			
		||||
                const data = await response.json();
 | 
			
		||||
                chartImage.value = data.chart;
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('Erro ao carregar gráfico:', error);
 | 
			
		||||
        } finally {
 | 
			
		||||
            loadingChart.value = false;
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const formatCurrency = (value) => {
 | 
			
		||||
        return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(value || 0);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const formatDate = (dateString) => {
 | 
			
		||||
        if (!dateString) return '-';
 | 
			
		||||
        
 | 
			
		||||
        // ✅ MESMA CORREÇÃO: Converter YYYY-MM-DD para DD/MM/YYYY
 | 
			
		||||
        if (dateString.match(/^\d{4}-\d{2}-\d{2}$/)) {
 | 
			
		||||
            const [year, month, day] = dateString.split('-');
 | 
			
		||||
            return `${day.padStart(2, '0')}/${month.padStart(2, '0')}/${year}`;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // Se já está formatada, retorna como está
 | 
			
		||||
        return dateString;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const getBalanceTrend = () => {
 | 
			
		||||
        const balance = summary.value.balance;
 | 
			
		||||
        if (balance > 0) return 'Saldo positivo';
 | 
			
		||||
        if (balance < 0) return 'Saldo negativo';
 | 
			
		||||
        return 'Saldo zerado';
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const getIncomeProgress = () => {
 | 
			
		||||
        const income = summary.value.total_income;
 | 
			
		||||
        if (income === 0) return 'Sem receitas este mês';
 | 
			
		||||
        const target = 5000;
 | 
			
		||||
        const progress = (income / target) * 100;
 | 
			
		||||
        return `Meta: ${Math.min(progress, 100).toFixed(0)}%`;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const getExpenseLimit = () => {
 | 
			
		||||
        const expenses = summary.value.total_expenses;
 | 
			
		||||
        const limit = 2500;
 | 
			
		||||
        const remaining = limit - expenses;
 | 
			
		||||
        return remaining >= 0 ? `Limite restante: ${formatCurrency(remaining)}` : `Excedido: ${formatCurrency(Math.abs(remaining))}`;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    onMounted(() => {
 | 
			
		||||
        loadDashboardData();
 | 
			
		||||
        setTimeout(() => loadChartData('monthly'), 500);
 | 
			
		||||
    });
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
    .bg-primary-dark { background-color: #1A3B5E !important; }
 | 
			
		||||
    .text-primary-dark { color: #1A3B5E !important; }
 | 
			
		||||
    .bg-success-feature { background-color: #2ECC71 !important; }
 | 
			
		||||
    .text-success-feature { color: #2ECC71 !important; }
 | 
			
		||||
 | 
			
		||||
    .max-w-7xl { max-width: 80rem; }
 | 
			
		||||
    .mx-auto { margin-left: auto !important; margin-right: auto !important; }
 | 
			
		||||
 | 
			
		||||
    .btn-outline-primary.active {
 | 
			
		||||
        background-color: #1A3B5E;
 | 
			
		||||
        border-color: #1A3B5E;
 | 
			
		||||
        color: white;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .transaction-info { flex: 1; text-align: left; }
 | 
			
		||||
    .chart-container { background-color: white; border: 1px solid #e9ecef; }
 | 
			
		||||
    .border-dashed { border-style: dashed !important; }
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,18 +0,0 @@
 | 
			
		||||
import { fileURLToPath, URL } from 'node:url'
 | 
			
		||||
 | 
			
		||||
import { defineConfig } from 'vite'
 | 
			
		||||
import vue from '@vitejs/plugin-vue'
 | 
			
		||||
import vueDevTools from 'vite-plugin-vue-devtools'
 | 
			
		||||
 | 
			
		||||
// https://vite.dev/config/
 | 
			
		||||
export default defineConfig({
 | 
			
		||||
  plugins: [
 | 
			
		||||
    vue(),
 | 
			
		||||
    vueDevTools(),
 | 
			
		||||
  ],
 | 
			
		||||
  resolve: {
 | 
			
		||||
    alias: {
 | 
			
		||||
      '@': fileURLToPath(new URL('./src', import.meta.url))
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB  | 
| 
		 Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB  | 
| 
		 Before Width: | Height: | Size: 192 KiB After Width: | Height: | Size: 192 KiB  | 
| 
		 Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB  | 
							
								
								
									
										1
									
								
								web/assets/index-DvM4Z6IE.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										25
									
								
								web/assets/index-_Uo-jrdC.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -2,15 +2,16 @@
 | 
			
		||||
<html lang="">
 | 
			
		||||
  <head>
 | 
			
		||||
    <meta charset="UTF-8">
 | 
			
		||||
    <link rel="icon" href="./src/assets/icons/facivon.ico">
 | 
			
		||||
    <link rel="icon" href="/assets/facivon-DRlDKSIp.ico">
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
			
		||||
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
 | 
			
		||||
    <title>CtrlCash</title>
 | 
			
		||||
    
 | 
			
		||||
    <script type="module" crossorigin src="/assets/index-_Uo-jrdC.js"></script>
 | 
			
		||||
    <link rel="stylesheet" crossorigin href="/assets/index-DvM4Z6IE.css">
 | 
			
		||||
  </head>
 | 
			
		||||
  <body>
 | 
			
		||||
    <div id="app"></div>
 | 
			
		||||
    <script type="module" src="/src/main.js"></script>
 | 
			
		||||
  </body>
 | 
			
		||||
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
 | 
			
		||||
</html>
 | 
			
		||||