Compare commits
	
		
			10 Commits
		
	
	
		
			f599424ae9
			...
			vue
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					3b2a5cb723 | ||
| 
						 | 
					5746e91796 | ||
| 
						 | 
					f2a26ef662 | ||
| 
						 | 
					18c36f366d | ||
| 
						 | 
					db0d1d4a3a | ||
| 
						 | 
					c126b0c550 | ||
| 
						 | 
					a9db545049 | ||
| 
						 | 
					d532c07790 | ||
| 
						 | 
					369179da55 | ||
| 
						 | 
					7a70cc4a87 | 
							
								
								
									
										36
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,36 @@
 | 
			
		||||
# 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
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
{
 | 
			
		||||
  "recommendations": ["Vue.volar"]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										788
									
								
								app.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										788
									
								
								app.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,788 @@
 | 
			
		||||
from flask import Flask, request, jsonify
 | 
			
		||||
from flask_cors import CORS
 | 
			
		||||
import sqlite3
 | 
			
		||||
import matplotlib
 | 
			
		||||
matplotlib.use('Agg')
 | 
			
		||||
import matplotlib.pyplot as plt
 | 
			
		||||
import base64
 | 
			
		||||
import io
 | 
			
		||||
import bcrypt
 | 
			
		||||
import os
 | 
			
		||||
from datetime import datetime, timedelta
 | 
			
		||||
import json
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
app = Flask(__name__)
 | 
			
		||||
CORS(app)
 | 
			
		||||
 | 
			
		||||
# Configuração do SQLite
 | 
			
		||||
DB_PATH = 'ctrlcash.db'
 | 
			
		||||
 | 
			
		||||
# 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()
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
# Categorias padrão
 | 
			
		||||
default_categories = [
 | 
			
		||||
    {"name": "Alimentação", "type": "expense", "color": "#FF6B6B"},
 | 
			
		||||
    {"name": "Transporte", "type": "expense", "color": "#4ECDC4"},
 | 
			
		||||
    {"name": "Moradia", "type": "expense", "color": "#45B7D1"},
 | 
			
		||||
    {"name": "Saúde", "type": "expense", "color": "#96CEB4"},
 | 
			
		||||
    {"name": "Educação", "type": "expense", "color": "#FFEAA7"},
 | 
			
		||||
    {"name": "Lazer", "type": "expense", "color": "#DDA0DD"},
 | 
			
		||||
    {"name": "Salário", "type": "income", "color": "#98D8C8"},
 | 
			
		||||
    {"name": "Investimentos", "type": "income", "color": "#F7DC6F"},
 | 
			
		||||
    {"name": "Freelance", "type": "income", "color": "#BB8FCE"},
 | 
			
		||||
    {"name": "Outros", "type": "income", "color": "#95a5a6"},
 | 
			
		||||
    {"name": "Outros", "type": "expense", "color": "#95a5a6"}
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
# Funções auxiliares
 | 
			
		||||
def hash_password(password):
 | 
			
		||||
    return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
 | 
			
		||||
 | 
			
		||||
def check_password(password, hashed):
 | 
			
		||||
    return bcrypt.checkpw(password.encode('utf-8'), hashed.encode('utf-8'))
 | 
			
		||||
 | 
			
		||||
def init_default_categories(user_id):
 | 
			
		||||
    """Insere categorias padrão para um usuário"""
 | 
			
		||||
    conn = get_db_connection()
 | 
			
		||||
    cursor = conn.cursor()
 | 
			
		||||
    
 | 
			
		||||
    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']))
 | 
			
		||||
    
 | 
			
		||||
    conn.commit()
 | 
			
		||||
    conn.close()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# ROTAS DE AUTENTICAÇÃO
 | 
			
		||||
@app.route('/api/auth/register', methods=['POST'])
 | 
			
		||||
def register():
 | 
			
		||||
    try:
 | 
			
		||||
        data = request.get_json()
 | 
			
		||||
        email = data.get('email', '').strip().lower()
 | 
			
		||||
        password = data.get('password', '')
 | 
			
		||||
        name = data.get('name', '').strip()
 | 
			
		||||
        
 | 
			
		||||
        if not email or not password or not name:
 | 
			
		||||
            return jsonify({"error": "Todos os campos são obrigatórios"}), 400
 | 
			
		||||
        
 | 
			
		||||
        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()
 | 
			
		||||
            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_id = cursor.lastrowid
 | 
			
		||||
        
 | 
			
		||||
        conn.commit()
 | 
			
		||||
        conn.close()
 | 
			
		||||
        
 | 
			
		||||
        # Inicializar categorias e dados mock
 | 
			
		||||
        init_default_categories(user_id)
 | 
			
		||||
        
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            "message": "Usuário criado com sucesso",
 | 
			
		||||
            "user": {
 | 
			
		||||
                "id": user_id,
 | 
			
		||||
                "email": email,
 | 
			
		||||
                "name": name,
 | 
			
		||||
                "profile": {
 | 
			
		||||
                    "monthly_income_goal": 5000,
 | 
			
		||||
                    "monthly_expense_limit": 2500,
 | 
			
		||||
                    "currency": "BRL"
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }), 201
 | 
			
		||||
        
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        print(f"Erro no registro: {e}")
 | 
			
		||||
        return jsonify({"error": "Erro interno do servidor"}), 500
 | 
			
		||||
 | 
			
		||||
@app.route('/api/auth/login', methods=['POST'])
 | 
			
		||||
def login():
 | 
			
		||||
    try:
 | 
			
		||||
        data = request.get_json()
 | 
			
		||||
        email = data.get('email', '').strip().lower()
 | 
			
		||||
        password = data.get('password', '')
 | 
			
		||||
        
 | 
			
		||||
        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()
 | 
			
		||||
        
 | 
			
		||||
        if not user or not check_password(password, user['password']):
 | 
			
		||||
            return jsonify({"error": "Credenciais inválidas"}), 401
 | 
			
		||||
        
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            "message": "Login realizado com sucesso",
 | 
			
		||||
            "user": {
 | 
			
		||||
                "id": 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']
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }), 200
 | 
			
		||||
        
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        print(f"Erro no login: {e}")
 | 
			
		||||
        return jsonify({"error": "Erro interno do servidor"}), 500
 | 
			
		||||
 | 
			
		||||
# ROTAS DE TRANSAÇÕES
 | 
			
		||||
@app.route('/api/transactions', methods=['GET'])
 | 
			
		||||
def get_transactions():
 | 
			
		||||
    try:
 | 
			
		||||
        user_id = request.args.get('user_id')
 | 
			
		||||
        if not user_id:
 | 
			
		||||
            return jsonify({"transactions": []}), 200
 | 
			
		||||
        
 | 
			
		||||
        conn = get_db_connection()
 | 
			
		||||
        cursor = conn.cursor()
 | 
			
		||||
        
 | 
			
		||||
        cursor.execute('''
 | 
			
		||||
            SELECT id, user_id, amount, description, category, type, date
 | 
			
		||||
            FROM transactions 
 | 
			
		||||
            WHERE user_id = ?
 | 
			
		||||
            ORDER BY date DESC
 | 
			
		||||
        ''', (user_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:
 | 
			
		||||
        print(f"Erro ao buscar transações: {e}")
 | 
			
		||||
        return jsonify({"error": "Erro ao buscar transações"}), 500
 | 
			
		||||
 | 
			
		||||
@app.route('/api/transactions', methods=['POST'])
 | 
			
		||||
def add_transaction():
 | 
			
		||||
    try:
 | 
			
		||||
        data = request.get_json()
 | 
			
		||||
        user_id = data.get('user_id')
 | 
			
		||||
        
 | 
			
		||||
        if not user_id:
 | 
			
		||||
            return jsonify({"error": "User ID é obrigatório"}), 400
 | 
			
		||||
        
 | 
			
		||||
        # Validar dados
 | 
			
		||||
        amount = float(data.get('amount', 0))
 | 
			
		||||
        description = data.get('description', '').strip()
 | 
			
		||||
        category = data.get('category', 'Outros')
 | 
			
		||||
        transaction_type = data.get('type', 'expense')
 | 
			
		||||
        date_str = data.get('date', '')
 | 
			
		||||
        
 | 
			
		||||
        if amount <= 0:
 | 
			
		||||
            return jsonify({"error": "Valor deve ser maior que zero"}), 400
 | 
			
		||||
        
 | 
			
		||||
        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
 | 
			
		||||
        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
 | 
			
		||||
        
 | 
			
		||||
        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()
 | 
			
		||||
        
 | 
			
		||||
        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
 | 
			
		||||
            }
 | 
			
		||||
        }), 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'])
 | 
			
		||||
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()
 | 
			
		||||
        
 | 
			
		||||
        cursor.execute('''
 | 
			
		||||
            DELETE FROM transactions 
 | 
			
		||||
            WHERE id = ? AND user_id = ?
 | 
			
		||||
        ''', (transaction_id, user_id))
 | 
			
		||||
        
 | 
			
		||||
        if cursor.rowcount == 0:
 | 
			
		||||
            conn.close()
 | 
			
		||||
            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:
 | 
			
		||||
        print(f"Erro ao deletar transação: {e}")
 | 
			
		||||
        return jsonify({"error": "Erro ao deletar transação"}), 500
 | 
			
		||||
 | 
			
		||||
# ROTAS DE CATEGORIAS
 | 
			
		||||
@app.route('/api/categories', methods=['GET'])
 | 
			
		||||
def get_categories():
 | 
			
		||||
    try:
 | 
			
		||||
        user_id = request.args.get('user_id')
 | 
			
		||||
        
 | 
			
		||||
        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()
 | 
			
		||||
        else:
 | 
			
		||||
            # Retornar categorias padrão
 | 
			
		||||
            categories = [{"id": i, **cat} for i, cat in enumerate(default_categories)]
 | 
			
		||||
        
 | 
			
		||||
        return jsonify({"categories": categories}), 200
 | 
			
		||||
        
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        print(f"Erro ao buscar categorias: {e}")
 | 
			
		||||
        return jsonify({"categories": default_categories}), 200
 | 
			
		||||
 | 
			
		||||
@app.route('/api/categories', methods=['POST'])
 | 
			
		||||
def add_category():
 | 
			
		||||
    try:
 | 
			
		||||
        data = request.get_json()
 | 
			
		||||
        user_id = data.get('user_id')
 | 
			
		||||
        name = data.get('name', '').strip()
 | 
			
		||||
        category_type = data.get('type')
 | 
			
		||||
        color = data.get('color', '#6c757d')
 | 
			
		||||
        
 | 
			
		||||
        if not user_id:
 | 
			
		||||
            return jsonify({"error": "User ID é obrigatório"}), 400
 | 
			
		||||
        
 | 
			
		||||
        if not name:
 | 
			
		||||
            return jsonify({"error": "Nome da categoria é obrigatório"}), 400
 | 
			
		||||
        
 | 
			
		||||
        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))
 | 
			
		||||
        
 | 
			
		||||
        if cursor.fetchone():
 | 
			
		||||
            conn.close()
 | 
			
		||||
            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))
 | 
			
		||||
        
 | 
			
		||||
        category_id = cursor.lastrowid
 | 
			
		||||
        
 | 
			
		||||
        conn.commit()
 | 
			
		||||
        conn.close()
 | 
			
		||||
        
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            "message": "Categoria adicionada com sucesso",
 | 
			
		||||
            "category": {
 | 
			
		||||
                "id": category_id,
 | 
			
		||||
                "user_id": user_id,
 | 
			
		||||
                "name": name,
 | 
			
		||||
                "type": category_type,
 | 
			
		||||
                "color": color
 | 
			
		||||
            }
 | 
			
		||||
        }), 201
 | 
			
		||||
        
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        print(f"Erro ao adicionar categoria: {e}")
 | 
			
		||||
        return jsonify({"error": "Erro ao adicionar categoria"}), 500
 | 
			
		||||
 | 
			
		||||
# ROTAS DO DASHBOARD
 | 
			
		||||
@app.route('/api/dashboard/summary', methods=['GET'])
 | 
			
		||||
def get_dashboard_summary():
 | 
			
		||||
    try:
 | 
			
		||||
        user_id = request.args.get('user_id')
 | 
			
		||||
        if not user_id:
 | 
			
		||||
            return jsonify({
 | 
			
		||||
                "total_income": 0,
 | 
			
		||||
                "total_expenses": 0,
 | 
			
		||||
                "balance": 0,
 | 
			
		||||
                "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')
 | 
			
		||||
        
 | 
			
		||||
        # 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))
 | 
			
		||||
        
 | 
			
		||||
        total_income = cursor.fetchone()['total'] or 0
 | 
			
		||||
        
 | 
			
		||||
        # 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))
 | 
			
		||||
        
 | 
			
		||||
        total_expenses = cursor.fetchone()['total'] or 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 = []
 | 
			
		||||
        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()
 | 
			
		||||
        
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            "total_income": round(total_income, 2),
 | 
			
		||||
            "total_expenses": round(total_expenses, 2),
 | 
			
		||||
            "balance": round(balance, 2),
 | 
			
		||||
            "recent_transactions": recent_transactions
 | 
			
		||||
        }), 200
 | 
			
		||||
        
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        print(f"Erro no dashboard: {e}")
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            "total_income": 0,
 | 
			
		||||
            "total_expenses": 0,
 | 
			
		||||
            "balance": 0,
 | 
			
		||||
            "recent_transactions": []
 | 
			
		||||
        }), 200
 | 
			
		||||
 | 
			
		||||
@app.route('/api/dashboard/chart')
 | 
			
		||||
def get_chart():
 | 
			
		||||
    try:
 | 
			
		||||
        user_id = request.args.get('user_id')
 | 
			
		||||
        chart_type = request.args.get('type', 'monthly')
 | 
			
		||||
        
 | 
			
		||||
        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')
 | 
			
		||||
            
 | 
			
		||||
            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))
 | 
			
		||||
            
 | 
			
		||||
            result = cursor.fetchall()
 | 
			
		||||
            
 | 
			
		||||
            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()
 | 
			
		||||
                
 | 
			
		||||
                x = range(len(months))
 | 
			
		||||
                width = 0.35
 | 
			
		||||
                
 | 
			
		||||
                plt.bar([i - width/2 for i in x], income, width, label='Receitas', color='#28a745')
 | 
			
		||||
                plt.bar([i + width/2 for i in x], expenses, width, label='Despesas', color='#dc3545')
 | 
			
		||||
                plt.xlabel('Mês')
 | 
			
		||||
                plt.ylabel('Valor (R$)')
 | 
			
		||||
                plt.title('Receitas vs Despesas - Últimos 6 Meses')
 | 
			
		||||
                plt.xticks(x, months, rotation=45)
 | 
			
		||||
                plt.legend()
 | 
			
		||||
                plt.tight_layout()
 | 
			
		||||
            else:
 | 
			
		||||
                plt.text(0.5, 0.5, 'Sem dados para exibir', 
 | 
			
		||||
                        ha='center', va='center', transform=plt.gca().transAxes)
 | 
			
		||||
            
 | 
			
		||||
        elif chart_type == 'categories':
 | 
			
		||||
            # Gastos por categoria (últimos 30 dias)
 | 
			
		||||
            thirty_days_ago = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d')
 | 
			
		||||
            
 | 
			
		||||
            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))
 | 
			
		||||
            
 | 
			
		||||
            result = cursor.fetchall()
 | 
			
		||||
            
 | 
			
		||||
            if result:
 | 
			
		||||
                categories = [row['category'] for row in result]
 | 
			
		||||
                amounts = [row['total'] for row in result]
 | 
			
		||||
                
 | 
			
		||||
                plt.pie(amounts, labels=categories, autopct='%1.1f%%', startangle=90)
 | 
			
		||||
                plt.title('Distribuição de Gastos por Categoria (Últimos 30 dias)')
 | 
			
		||||
            else:
 | 
			
		||||
                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')
 | 
			
		||||
        buffer.seek(0)
 | 
			
		||||
        image_base64 = base64.b64encode(buffer.getvalue()).decode()
 | 
			
		||||
        plt.close()
 | 
			
		||||
        
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            "chart": f"data:image/png;base64,{image_base64}"
 | 
			
		||||
        }), 200
 | 
			
		||||
        
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        print(f"Erro ao gerar gráfico: {e}")
 | 
			
		||||
        return jsonify({"error": "Erro ao gerar gráfico"}), 500
 | 
			
		||||
 | 
			
		||||
# ROTAS DE PERFIL
 | 
			
		||||
@app.route('/api/user/profile', methods=['GET'])
 | 
			
		||||
def get_profile():
 | 
			
		||||
    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()
 | 
			
		||||
        
 | 
			
		||||
        cursor.execute('''
 | 
			
		||||
            SELECT id, email, name, monthly_income_goal, monthly_expense_limit, currency
 | 
			
		||||
            FROM users WHERE id = ?
 | 
			
		||||
        ''', (user_id,))
 | 
			
		||||
        
 | 
			
		||||
        user = cursor.fetchone()
 | 
			
		||||
        conn.close()
 | 
			
		||||
        
 | 
			
		||||
        if not user:
 | 
			
		||||
            return jsonify({"error": "Usuário não encontrado"}), 404
 | 
			
		||||
        
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            "user": {
 | 
			
		||||
                "id": 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']
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }), 200
 | 
			
		||||
        
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        print(f"Erro ao buscar perfil: {e}")
 | 
			
		||||
        return jsonify({"error": "Erro ao buscar perfil"}), 500
 | 
			
		||||
 | 
			
		||||
@app.route('/api/user/profile', methods=['PUT'])
 | 
			
		||||
def update_profile():
 | 
			
		||||
    try:
 | 
			
		||||
        data = request.get_json()
 | 
			
		||||
        user_id = data.get('user_id')
 | 
			
		||||
        name = data.get('name', '').strip()
 | 
			
		||||
        email = data.get('email', '').strip().lower()
 | 
			
		||||
        profile = data.get('profile', {})
 | 
			
		||||
        
 | 
			
		||||
        if not user_id:
 | 
			
		||||
            return jsonify({"error": "User ID é obrigatório"}), 400
 | 
			
		||||
        
 | 
			
		||||
        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()
 | 
			
		||||
            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))
 | 
			
		||||
        
 | 
			
		||||
        if cursor.rowcount == 0:
 | 
			
		||||
            conn.close()
 | 
			
		||||
            return jsonify({"error": "Nenhuma alteração realizada"}), 400
 | 
			
		||||
        
 | 
			
		||||
        conn.commit()
 | 
			
		||||
        conn.close()
 | 
			
		||||
        
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            "message": "Perfil atualizado com sucesso",
 | 
			
		||||
            "user": {
 | 
			
		||||
                "id": user_id,
 | 
			
		||||
                "name": name,
 | 
			
		||||
                "email": email,
 | 
			
		||||
                "profile": profile
 | 
			
		||||
            }
 | 
			
		||||
        }), 200
 | 
			
		||||
        
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        print(f"Erro ao atualizar perfil: {e}")
 | 
			
		||||
        return jsonify({"error": "Erro ao atualizar perfil"}), 500
 | 
			
		||||
 | 
			
		||||
@app.route('/api/user/change-password', methods=['PUT'])
 | 
			
		||||
def change_password():
 | 
			
		||||
    try:
 | 
			
		||||
        data = request.get_json()
 | 
			
		||||
        user_id = data.get('user_id')
 | 
			
		||||
        current_password = data.get('current_password')
 | 
			
		||||
        new_password = data.get('new_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()
 | 
			
		||||
        
 | 
			
		||||
        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()
 | 
			
		||||
        
 | 
			
		||||
        return jsonify({"message": "Senha alterada com sucesso"}), 200
 | 
			
		||||
        
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        print(f"Erro ao alterar senha: {e}")
 | 
			
		||||
        return jsonify({"error": "Erro ao alterar senha"}), 500
 | 
			
		||||
 | 
			
		||||
# Rota health check
 | 
			
		||||
@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()
 | 
			
		||||
        
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            "status": "OK", 
 | 
			
		||||
            "message": "Backend e SQLite funcionando",
 | 
			
		||||
            "timestamp": datetime.now().isoformat()
 | 
			
		||||
        }), 200
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            "status": "ERROR",
 | 
			
		||||
            "message": f"Erro no SQLite: {e}"
 | 
			
		||||
        }), 500
 | 
			
		||||
 | 
			
		||||
@app.route('/')
 | 
			
		||||
def index():
 | 
			
		||||
    return jsonify({
 | 
			
		||||
        "message": "CtrlCash API está funcionando!",
 | 
			
		||||
        "version": "1.0.0",
 | 
			
		||||
        "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)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										16
									
								
								index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								index.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="">
 | 
			
		||||
  <head>
 | 
			
		||||
    <meta charset="UTF-8">
 | 
			
		||||
    <link rel="icon" href="./src/assets/icons/facivon.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>
 | 
			
		||||
    
 | 
			
		||||
  </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>
 | 
			
		||||
							
								
								
									
										8
									
								
								jsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								jsconfig.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
			
		||||
{
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    "paths": {
 | 
			
		||||
      "@/*": ["./src/*"]
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "exclude": ["node_modules", "dist"]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										2561
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										2561
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										23
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
{
 | 
			
		||||
  "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"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										10
									
								
								src/App.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/App.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
<script setup>
 | 
			
		||||
  
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <RouterView />
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								src/assets/CtrlCash-blue.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/assets/CtrlCash-blue.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 14 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/CtrlCash-white.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/assets/CtrlCash-white.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 12 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/Hero.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/assets/Hero.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 192 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/icons/facivon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/assets/icons/facivon.ico
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 30 KiB  | 
							
								
								
									
										88
									
								
								src/components/FooterPublic.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								src/components/FooterPublic.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,88 @@
 | 
			
		||||
<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>
 | 
			
		||||
							
								
								
									
										185
									
								
								src/components/HeaderApp.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										185
									
								
								src/components/HeaderApp.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,185 @@
 | 
			
		||||
<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>
 | 
			
		||||
							
								
								
									
										51
									
								
								src/components/HeaderPublic.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/components/HeaderPublic.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,51 @@
 | 
			
		||||
<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>
 | 
			
		||||
							
								
								
									
										9
									
								
								src/main.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/main.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
import { createApp } from 'vue'
 | 
			
		||||
import App from './App.vue'
 | 
			
		||||
import router from './router'
 | 
			
		||||
 | 
			
		||||
const app = createApp(App)
 | 
			
		||||
 | 
			
		||||
app.use(router)
 | 
			
		||||
 | 
			
		||||
app.mount('#app')
 | 
			
		||||
							
								
								
									
										58
									
								
								src/router/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								src/router/index.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,58 @@
 | 
			
		||||
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
 | 
			
		||||
							
								
								
									
										80
									
								
								src/views/AjudaView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								src/views/AjudaView.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,80 @@
 | 
			
		||||
<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>
 | 
			
		||||
							
								
								
									
										160
									
								
								src/views/CadastroView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										160
									
								
								src/views/CadastroView.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,160 @@
 | 
			
		||||
<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>
 | 
			
		||||
							
								
								
									
										588
									
								
								src/views/ConfiguracoesView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										588
									
								
								src/views/ConfiguracoesView.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,588 @@
 | 
			
		||||
<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>
 | 
			
		||||
							
								
								
									
										154
									
								
								src/views/HomeView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										154
									
								
								src/views/HomeView.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,154 @@
 | 
			
		||||
<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>
 | 
			
		||||
							
								
								
									
										127
									
								
								src/views/LoginView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								src/views/LoginView.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,127 @@
 | 
			
		||||
<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>
 | 
			
		||||
							
								
								
									
										58
									
								
								src/views/SobreNosView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								src/views/SobreNosView.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,58 @@
 | 
			
		||||
<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>
 | 
			
		||||
							
								
								
									
										623
									
								
								src/views/TransacoesView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										623
									
								
								src/views/TransacoesView.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,623 @@
 | 
			
		||||
<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>
 | 
			
		||||
							
								
								
									
										239
									
								
								src/views/UserDashboardView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										239
									
								
								src/views/UserDashboardView.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,239 @@
 | 
			
		||||
<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>
 | 
			
		||||
							
								
								
									
										18
									
								
								vite.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								vite.config.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
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))
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
		Reference in New Issue
	
	Block a user