Compare commits

..

9 Commits

Author SHA1 Message Date
Caio1w
96f20dc7e5 Corrgi as coisas 2025-10-29 22:21:53 -03:00
Caio1w
2f3441d435 Terminei sem erros 2025-10-29 22:09:13 -03:00
Caio1w
89cac2cf8e terminei 2025-10-29 22:08:04 -03:00
Caio1w
afeaf36479 servir a index.html a rotas 2025-10-29 21:48:22 -03:00
Caio1w
aac7fa0317 pÃagina compilada atualizada com os footers corretos da vue 2025-10-29 20:05:23 -03:00
Caio1w
880c892a70 Acidentalmente coloquei a pasta node_modules na main 2025-10-29 19:58:20 -03:00
Caio1w
4a14f533d2 Criada a API do site 2025-10-29 19:56:41 -03:00
Caio1w
f201c8edbd paginas adicionadas 2025-10-28 20:42:28 -03:00
Caio1w
33334980a6 Atualizei a estrutura de pasta 2025-10-28 17:52:51 -03:00
10 changed files with 708 additions and 13 deletions

660
app/app.py Normal file
View 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)

View File

@@ -1,12 +0,0 @@
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello_world():
return "<p>Hello, World!</p>"
if __name__ == "__main__":
app.run()

View File

@@ -1,2 +1,6 @@
Flask==3.1.2 Flask==3.1.2
flask-cors==6.0.1
Werkzeug==3.1.3 Werkzeug==3.1.3
pymongo==4.15.3
matplotlib==3.10.7
bcrypt==5.0.0

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

17
web/index.html Normal file
View 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>