Compare commits
	
		
			13 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					96f20dc7e5 | ||
| 
						 | 
					2f3441d435 | ||
| 
						 | 
					89cac2cf8e | ||
| 
						 | 
					afeaf36479 | ||
| 
						 | 
					aac7fa0317 | ||
| 
						 | 
					880c892a70 | ||
| 
						 | 
					4a14f533d2 | ||
| 
						 | 
					f201c8edbd | ||
| 
						 | 
					33334980a6 | ||
| 
						 | 
					746c34879e | ||
| 
						 | 
					f599424ae9 | ||
| 
						 | 
					f3a05d4bb4 | ||
| 
						 | 
					eb43b636c0 | 
							
								
								
									
										660
									
								
								app/app.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										660
									
								
								app/app.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,660 @@
 | 
				
			|||||||
 | 
					from flask import Flask, request, jsonify, send_file
 | 
				
			||||||
 | 
					from flask_cors import CORS
 | 
				
			||||||
 | 
					from pymongo import MongoClient
 | 
				
			||||||
 | 
					from datetime import datetime, timedelta
 | 
				
			||||||
 | 
					import matplotlib
 | 
				
			||||||
 | 
					matplotlib.use('Agg')
 | 
				
			||||||
 | 
					import matplotlib.pyplot as plt
 | 
				
			||||||
 | 
					import base64
 | 
				
			||||||
 | 
					import io
 | 
				
			||||||
 | 
					import bcrypt
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					from bson import ObjectId
 | 
				
			||||||
 | 
					import json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					app = Flask(__name__)
 | 
				
			||||||
 | 
					CORS(app)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Configurações do MongoDB
 | 
				
			||||||
 | 
					MONGO_URI = os.getenv('MONGO_URI', 'mongodb://localhost:27017/')
 | 
				
			||||||
 | 
					DB_NAME = 'CtrlCash'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Conexão com MongoDB
 | 
				
			||||||
 | 
					try:
 | 
				
			||||||
 | 
					    client = MongoClient(MONGO_URI)
 | 
				
			||||||
 | 
					    db = client[DB_NAME]
 | 
				
			||||||
 | 
					    print(f"✅ Conectado ao MongoDB: {DB_NAME}")
 | 
				
			||||||
 | 
					except Exception as e:
 | 
				
			||||||
 | 
					    print(f"❌ Erro ao conectar com MongoDB: {e}")
 | 
				
			||||||
 | 
					    exit(1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Collections
 | 
				
			||||||
 | 
					users_collection = db['users']
 | 
				
			||||||
 | 
					transactions_collection = db['transactions']
 | 
				
			||||||
 | 
					categories_collection = db['categories']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class JSONEncoder(json.JSONEncoder):
 | 
				
			||||||
 | 
					    def default(self, obj):
 | 
				
			||||||
 | 
					        if isinstance(obj, ObjectId):
 | 
				
			||||||
 | 
					            return str(obj)
 | 
				
			||||||
 | 
					        if isinstance(obj, datetime):
 | 
				
			||||||
 | 
					            return obj.isoformat()
 | 
				
			||||||
 | 
					        return super().default(obj)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					app.json_encoder = JSONEncoder
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Categorias padrão
 | 
				
			||||||
 | 
					default_categories = [
 | 
				
			||||||
 | 
					    {"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"""
 | 
				
			||||||
 | 
					    categories = []
 | 
				
			||||||
 | 
					    for cat in default_categories:
 | 
				
			||||||
 | 
					        categories.append({
 | 
				
			||||||
 | 
					            **cat,
 | 
				
			||||||
 | 
					            "user_id": ObjectId(user_id),
 | 
				
			||||||
 | 
					            "created_at": datetime.now()
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    if categories:
 | 
				
			||||||
 | 
					        categories_collection.insert_many(categories)
 | 
				
			||||||
 | 
					@app.route('/')
 | 
				
			||||||
 | 
					def serve_html_home():
 | 
				
			||||||
 | 
					    return send_file('../web/index.html')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@app.route('/dashboard')
 | 
				
			||||||
 | 
					def serve_html_dashboard():
 | 
				
			||||||
 | 
					    return send_file('../web/index.html')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@app.route('/login')
 | 
				
			||||||
 | 
					def serve_html_login():
 | 
				
			||||||
 | 
					    return send_file('../web/index.html')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@app.route('/cadastro')
 | 
				
			||||||
 | 
					def serve_html_cadastro():
 | 
				
			||||||
 | 
					    return send_file('../web/index.html')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@app.route('/transacoes')
 | 
				
			||||||
 | 
					def serve_html_transacoes():
 | 
				
			||||||
 | 
					    return send_file('../web/index.html')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@app.route('/configuracoes')
 | 
				
			||||||
 | 
					def serve_html_configuracoes():
 | 
				
			||||||
 | 
					    return send_file('../web/index.html')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@app.route('/about')
 | 
				
			||||||
 | 
					def serve_html_about():
 | 
				
			||||||
 | 
					    return send_file('../web/index.html')
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					@app.route('/help')
 | 
				
			||||||
 | 
					def serve_html_help():
 | 
				
			||||||
 | 
					    return send_file('../web/index.html')
 | 
				
			||||||
 | 
					# ROTAS DE AUTENTICAÇÃO
 | 
				
			||||||
 | 
					@app.route('/api/auth/register', methods=['POST'])
 | 
				
			||||||
 | 
					def register(): 
 | 
				
			||||||
 | 
					    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
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Verificar se usuário já existe
 | 
				
			||||||
 | 
					        if users_collection.find_one({"email": email}):
 | 
				
			||||||
 | 
					            return jsonify({"error": "Email já cadastrado"}), 400
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Criar usuário
 | 
				
			||||||
 | 
					        user_data = {
 | 
				
			||||||
 | 
					            'email': email,
 | 
				
			||||||
 | 
					            'password': hash_password(password),
 | 
				
			||||||
 | 
					            'name': name,
 | 
				
			||||||
 | 
					            'created_at': datetime.now(),
 | 
				
			||||||
 | 
					            'profile': {
 | 
				
			||||||
 | 
					                'monthly_income_goal': 5000,
 | 
				
			||||||
 | 
					                'monthly_expense_limit': 2500,
 | 
				
			||||||
 | 
					                'currency': 'BRL'
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        result = users_collection.insert_one(user_data)
 | 
				
			||||||
 | 
					        user_id = result.inserted_id
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Inicializar categorias
 | 
				
			||||||
 | 
					        init_default_categories(user_id)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        return jsonify({
 | 
				
			||||||
 | 
					            "message": "Usuário criado com sucesso",
 | 
				
			||||||
 | 
					            "user": {
 | 
				
			||||||
 | 
					                "id": str(user_id),
 | 
				
			||||||
 | 
					                "email": email,
 | 
				
			||||||
 | 
					                "name": name,
 | 
				
			||||||
 | 
					                "profile": user_data['profile']
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }), 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
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        user = users_collection.find_one({"email": email})
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        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": str(user['_id']),
 | 
				
			||||||
 | 
					                "email": user['email'],
 | 
				
			||||||
 | 
					                "name": user['name'],
 | 
				
			||||||
 | 
					                "profile": user.get('profile', {})
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }), 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
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        transactions = list(transactions_collection.find(
 | 
				
			||||||
 | 
					            {"user_id": ObjectId(user_id)}
 | 
				
			||||||
 | 
					        ).sort("date", -1))
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Converter ObjectId para string
 | 
				
			||||||
 | 
					        for transaction in transactions:
 | 
				
			||||||
 | 
					            transaction['id'] = str(transaction['_id'])
 | 
				
			||||||
 | 
					            transaction['user_id'] = str(transaction['user_id'])
 | 
				
			||||||
 | 
					            del transaction['_id']
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        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
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Usar data exata do frontend
 | 
				
			||||||
 | 
					        if not date_str:
 | 
				
			||||||
 | 
					            date_str = datetime.now().strftime('%Y-%m-%d')
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        new_transaction = {
 | 
				
			||||||
 | 
					            "user_id": ObjectId(user_id),
 | 
				
			||||||
 | 
					            "amount": amount,
 | 
				
			||||||
 | 
					            "description": description,
 | 
				
			||||||
 | 
					            "category": category,
 | 
				
			||||||
 | 
					            "type": transaction_type,
 | 
				
			||||||
 | 
					            "date": date_str,
 | 
				
			||||||
 | 
					            "created_at": datetime.now()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        result = transactions_collection.insert_one(new_transaction)
 | 
				
			||||||
 | 
					        new_transaction['id'] = str(result.inserted_id)
 | 
				
			||||||
 | 
					        new_transaction['user_id'] = user_id
 | 
				
			||||||
 | 
					        del new_transaction['_id']
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        return jsonify({
 | 
				
			||||||
 | 
					            "message": "Transação adicionada com sucesso",
 | 
				
			||||||
 | 
					            "transaction": new_transaction
 | 
				
			||||||
 | 
					        }), 201
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					    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/<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
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        result = transactions_collection.delete_one({
 | 
				
			||||||
 | 
					            "_id": ObjectId(transaction_id),
 | 
				
			||||||
 | 
					            "user_id": ObjectId(user_id)
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if result.deleted_count == 0:
 | 
				
			||||||
 | 
					            return jsonify({"error": "Transação não encontrada"}), 404
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        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
 | 
				
			||||||
 | 
					            categories = list(categories_collection.find({"user_id": ObjectId(user_id)}))
 | 
				
			||||||
 | 
					            for category in categories:
 | 
				
			||||||
 | 
					                category['id'] = str(category['_id'])
 | 
				
			||||||
 | 
					                category['user_id'] = str(category['user_id'])
 | 
				
			||||||
 | 
					                del category['_id']
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            # Retornar categorias padrão
 | 
				
			||||||
 | 
					            categories = [{"id": i, **cat} for i, cat in enumerate(default_categories)]
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        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
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Verificar se categoria já existe
 | 
				
			||||||
 | 
					        existing_category = categories_collection.find_one({
 | 
				
			||||||
 | 
					            "user_id": ObjectId(user_id),
 | 
				
			||||||
 | 
					            "name": name,
 | 
				
			||||||
 | 
					            "type": category_type
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if existing_category:
 | 
				
			||||||
 | 
					            return jsonify({"error": "Categoria já existe"}), 400
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        new_category = {
 | 
				
			||||||
 | 
					            "user_id": ObjectId(user_id),
 | 
				
			||||||
 | 
					            "name": name,
 | 
				
			||||||
 | 
					            "type": category_type,
 | 
				
			||||||
 | 
					            "color": color,
 | 
				
			||||||
 | 
					            "created_at": datetime.now()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        result = categories_collection.insert_one(new_category)
 | 
				
			||||||
 | 
					        new_category['id'] = str(result.inserted_id)
 | 
				
			||||||
 | 
					        new_category['user_id'] = user_id
 | 
				
			||||||
 | 
					        del new_category['_id']
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        return jsonify({
 | 
				
			||||||
 | 
					            "message": "Categoria adicionada com sucesso",
 | 
				
			||||||
 | 
					            "category": new_category
 | 
				
			||||||
 | 
					        }), 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
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Calcular totais do mês atual
 | 
				
			||||||
 | 
					        start_of_month = datetime.now().replace(day=1, hour=0, minute=0, second=0, microsecond=0)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Pipeline para receitas do mês
 | 
				
			||||||
 | 
					        pipeline_income = [
 | 
				
			||||||
 | 
					            {"$match": {
 | 
				
			||||||
 | 
					                "user_id": ObjectId(user_id),
 | 
				
			||||||
 | 
					                "type": "income",
 | 
				
			||||||
 | 
					                "date": {"$gte": start_of_month.strftime('%Y-%m-%d')}
 | 
				
			||||||
 | 
					            }},
 | 
				
			||||||
 | 
					            {"$group": {"_id": None, "total": {"$sum": "$amount"}}}
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Pipeline para despesas do mês
 | 
				
			||||||
 | 
					        pipeline_expenses = [
 | 
				
			||||||
 | 
					            {"$match": {
 | 
				
			||||||
 | 
					                "user_id": ObjectId(user_id),
 | 
				
			||||||
 | 
					                "type": "expense",
 | 
				
			||||||
 | 
					                "date": {"$gte": start_of_month.strftime('%Y-%m-%d')}
 | 
				
			||||||
 | 
					            }},
 | 
				
			||||||
 | 
					            {"$group": {"_id": None, "total": {"$sum": "$amount"}}}
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        income_result = list(transactions_collection.aggregate(pipeline_income))
 | 
				
			||||||
 | 
					        expense_result = list(transactions_collection.aggregate(pipeline_expenses))
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        total_income = income_result[0]['total'] if income_result else 0
 | 
				
			||||||
 | 
					        total_expenses = expense_result[0]['total'] if expense_result else 0
 | 
				
			||||||
 | 
					        balance = total_income - total_expenses
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Últimas 5 transações
 | 
				
			||||||
 | 
					        recent_transactions = list(transactions_collection.find(
 | 
				
			||||||
 | 
					            {"user_id": ObjectId(user_id)}
 | 
				
			||||||
 | 
					        ).sort("date", -1).limit(5))
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        for transaction in recent_transactions:
 | 
				
			||||||
 | 
					            transaction['id'] = str(transaction['_id'])
 | 
				
			||||||
 | 
					            transaction['user_id'] = str(transaction['user_id'])
 | 
				
			||||||
 | 
					            del transaction['_id']
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        return jsonify({
 | 
				
			||||||
 | 
					            "total_income": round(total_income, 2),
 | 
				
			||||||
 | 
					            "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
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        plt.figure(figsize=(10, 6))
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if chart_type == 'monthly':
 | 
				
			||||||
 | 
					            # Últimos 6 meses
 | 
				
			||||||
 | 
					            six_months_ago = datetime.now() - timedelta(days=180)
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            pipeline = [
 | 
				
			||||||
 | 
					                {"$match": {
 | 
				
			||||||
 | 
					                    "user_id": ObjectId(user_id),
 | 
				
			||||||
 | 
					                    "date": {"$gte": six_months_ago.strftime('%Y-%m-%d')}
 | 
				
			||||||
 | 
					                }},
 | 
				
			||||||
 | 
					                {"$group": {
 | 
				
			||||||
 | 
					                    "_id": {"$substr": ["$date", 0, 7]},  # Extrair YYYY-MM
 | 
				
			||||||
 | 
					                    "income": {"$sum": {"$cond": [{"$eq": ["$type", "income"]}, "$amount", 0]}},
 | 
				
			||||||
 | 
					                    "expenses": {"$sum": {"$cond": [{"$eq": ["$type", "expense"]}, "$amount", 0]}}
 | 
				
			||||||
 | 
					                }},
 | 
				
			||||||
 | 
					                {"$sort": {"_id": 1}},
 | 
				
			||||||
 | 
					                {"$limit": 6}
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            result = list(transactions_collection.aggregate(pipeline))
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            if result:
 | 
				
			||||||
 | 
					                months = [r['_id'] for r in result]
 | 
				
			||||||
 | 
					                income = [r['income'] for r in result]
 | 
				
			||||||
 | 
					                expenses = [r['expenses'] for r in result]
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                x = range(len(months))
 | 
				
			||||||
 | 
					                width = 0.35
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                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)
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            pipeline = [
 | 
				
			||||||
 | 
					                {"$match": {
 | 
				
			||||||
 | 
					                    "user_id": ObjectId(user_id),
 | 
				
			||||||
 | 
					                    "type": "expense",
 | 
				
			||||||
 | 
					                    "date": {"$gte": thirty_days_ago.strftime('%Y-%m-%d')}
 | 
				
			||||||
 | 
					                }},
 | 
				
			||||||
 | 
					                {"$group": {
 | 
				
			||||||
 | 
					                    "_id": "$category",
 | 
				
			||||||
 | 
					                    "total": {"$sum": "$amount"}
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            result = list(transactions_collection.aggregate(pipeline))
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            if result:
 | 
				
			||||||
 | 
					                categories = [r['_id'] for r in result]
 | 
				
			||||||
 | 
					                amounts = [r['total'] for r in result]
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                plt.pie(amounts, labels=categories, autopct='%1.1f%%', startangle=90)
 | 
				
			||||||
 | 
					                plt.title('Distribuição de Gastos por Categoria (Últimos 30 dias)')
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                plt.text(0.5, 0.5, 'Sem dados de gastos', 
 | 
				
			||||||
 | 
					                        ha='center', va='center', transform=plt.gca().transAxes)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # 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
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        user = users_collection.find_one({"_id": ObjectId(user_id)})
 | 
				
			||||||
 | 
					        if not user:
 | 
				
			||||||
 | 
					            return jsonify({"error": "Usuário não encontrado"}), 404
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        return jsonify({
 | 
				
			||||||
 | 
					            "user": {
 | 
				
			||||||
 | 
					                "id": str(user['_id']),
 | 
				
			||||||
 | 
					                "name": user['name'],
 | 
				
			||||||
 | 
					                "email": user['email'],
 | 
				
			||||||
 | 
					                "profile": user.get('profile', {})
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }), 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
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Verificar se email já existe (para outro usuário)
 | 
				
			||||||
 | 
					        existing_user = users_collection.find_one({
 | 
				
			||||||
 | 
					            "email": email,
 | 
				
			||||||
 | 
					            "_id": {"$ne": ObjectId(user_id)}
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if existing_user:
 | 
				
			||||||
 | 
					            return jsonify({"error": "Email já está em uso"}), 400
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        update_data = {
 | 
				
			||||||
 | 
					            "name": name,
 | 
				
			||||||
 | 
					            "email": email,
 | 
				
			||||||
 | 
					            "profile": profile
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        result = users_collection.update_one(
 | 
				
			||||||
 | 
					            {"_id": ObjectId(user_id)},
 | 
				
			||||||
 | 
					            {"$set": update_data}
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if result.modified_count == 0:
 | 
				
			||||||
 | 
					            return jsonify({"error": "Nenhuma alteração realizada"}), 400
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        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
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        user = users_collection.find_one({"_id": ObjectId(user_id)})
 | 
				
			||||||
 | 
					        if not user:
 | 
				
			||||||
 | 
					            return jsonify({"error": "Usuário não encontrado"}), 404
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if not check_password(current_password, user['password']):
 | 
				
			||||||
 | 
					            return jsonify({"error": "Senha atual incorreta"}), 400
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if len(new_password) < 6:
 | 
				
			||||||
 | 
					            return jsonify({"error": "A nova senha deve ter pelo menos 6 caracteres"}), 400
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        users_collection.update_one(
 | 
				
			||||||
 | 
					            {"_id": ObjectId(user_id)},
 | 
				
			||||||
 | 
					            {"$set": {"password": hash_password(new_password)}}
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        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 MongoDB
 | 
				
			||||||
 | 
					        client.admin.command('ismaster')
 | 
				
			||||||
 | 
					        return jsonify({
 | 
				
			||||||
 | 
					            "status": "OK", 
 | 
				
			||||||
 | 
					            "message": "Backend e MongoDB funcionando",
 | 
				
			||||||
 | 
					            "timestamp": datetime.now().isoformat()
 | 
				
			||||||
 | 
					        }), 200
 | 
				
			||||||
 | 
					    except Exception as e:
 | 
				
			||||||
 | 
					        return jsonify({
 | 
				
			||||||
 | 
					            "status": "ERROR",
 | 
				
			||||||
 | 
					            "message": f"Erro no MongoDB: {e}"
 | 
				
			||||||
 | 
					        }), 500
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@app.route('/')
 | 
				
			||||||
 | 
					def index():
 | 
				
			||||||
 | 
					    return jsonify({
 | 
				
			||||||
 | 
					        "message": "CtrlCash API está funcionando!",
 | 
				
			||||||
 | 
					        "version": "1.0.0",
 | 
				
			||||||
 | 
					        "timestamp": datetime.now().isoformat()
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if __name__ == '__main__':
 | 
				
			||||||
 | 
					    print("🚀 Iniciando CtrlCash API com MongoDB...")
 | 
				
			||||||
 | 
					    print(f"📊 MongoDB: {MONGO_URI}")
 | 
				
			||||||
 | 
					    print(f"💾 Database: {DB_NAME}")
 | 
				
			||||||
 | 
					    app.run(debug=False, host='0.0.0.0', port=5000)
 | 
				
			||||||
							
								
								
									
										6
									
								
								requirements.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								requirements.txt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					Flask==3.1.2
 | 
				
			||||||
 | 
					flask-cors==6.0.1
 | 
				
			||||||
 | 
					Werkzeug==3.1.3 
 | 
				
			||||||
 | 
					pymongo==4.15.3
 | 
				
			||||||
 | 
					matplotlib==3.10.7
 | 
				
			||||||
 | 
					bcrypt==5.0.0
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								web/assets/CtrlCash-blue-DUaQbcwD.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								web/assets/CtrlCash-blue-DUaQbcwD.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 14 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								web/assets/CtrlCash-white-CJM1Egrh.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								web/assets/CtrlCash-white-CJM1Egrh.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 12 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								web/assets/Hero-C0gOcyj1.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								web/assets/Hero-C0gOcyj1.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 192 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								web/assets/facivon-DRlDKSIp.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								web/assets/facivon-DRlDKSIp.ico
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 30 KiB  | 
							
								
								
									
										1
									
								
								web/assets/index-DvM4Z6IE.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								web/assets/index-DvM4Z6IE.css
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										25
									
								
								web/assets/index-_Uo-jrdC.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								web/assets/index-_Uo-jrdC.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										17
									
								
								web/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								web/index.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html lang="">
 | 
				
			||||||
 | 
					  <head>
 | 
				
			||||||
 | 
					    <meta charset="UTF-8">
 | 
				
			||||||
 | 
					    <link rel="icon" href="/assets/facivon-DRlDKSIp.ico">
 | 
				
			||||||
 | 
					    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
				
			||||||
 | 
					    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
 | 
				
			||||||
 | 
					    <title>CtrlCash</title>
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    <script type="module" crossorigin src="/assets/index-_Uo-jrdC.js"></script>
 | 
				
			||||||
 | 
					    <link rel="stylesheet" crossorigin href="/assets/index-DvM4Z6IE.css">
 | 
				
			||||||
 | 
					  </head>
 | 
				
			||||||
 | 
					  <body>
 | 
				
			||||||
 | 
					    <div id="app"></div>
 | 
				
			||||||
 | 
					  </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>
 | 
				
			||||||
		Reference in New Issue
	
	Block a user