refactor: Implementa arquitetura MVC limpa

- Separa modelos em entidades individuais
- Cria camada de serviços para acesso a dados
- Implementa controladores para lógica de negócio
- Organiza rotas em blueprints por funcionalidade
- Adiciona documentação de arquitetura no README
- Cria script para preparação da estrutura

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
LS
2025-04-22 16:35:08 -03:00
parent e6057cd566
commit 62aaec3fbe
22 changed files with 2083 additions and 101 deletions

61
routes/auth.py Normal file
View File

@@ -0,0 +1,61 @@
from flask import Blueprint, render_template, redirect, url_for, request, jsonify
from flask_login import login_required, current_user
from controllers.auth_controller import AuthController
from services.database_service import DatabaseService
from models.entities.usuario import Usuario
auth_bp = Blueprint('auth', __name__)
@auth_bp.route("/login", methods=["GET", "POST"])
def login():
"""Rota de login"""
if request.method == "POST":
# Processar o login através do controlador
if AuthController.login():
# Redirecionar para home em caso de sucesso
return redirect(url_for("home"))
# GET ou falha no login, renderizar template
return render_template("login.html")
@auth_bp.route("/logout")
@login_required
def logout():
"""Rota de logout"""
AuthController.logout()
return redirect(url_for('auth.login'))
@auth_bp.route("/alterar_senha", methods=["GET", "POST"])
@login_required
def alterar_senha():
"""Rota para alterar a senha do usuário"""
if request.method == "POST":
senha_atual = request.form.get("senha_atual")
nova_senha = request.form.get("nova_senha")
confirmar_senha = request.form.get("confirmar_senha")
if AuthController.alterar_senha(current_user.id, senha_atual, nova_senha, confirmar_senha):
return redirect(url_for("home"))
return render_template("alterar_senha.html")
@auth_bp.route("/qr/<token>")
def get_qr_code(token):
"""Rota para exibir QR code para configuração 2FA"""
db = DatabaseService.get_db_connection()
try:
user = db.query(Usuario).filter_by(username='admin').first()
if not user:
flash('Usuário não encontrado', 'error')
return redirect(url_for('auth.login'))
qr_uri = user.get_otp_uri()
return render_template('mostrar_qr_code.html', qr_uri=qr_uri)
finally:
db.close()
@auth_bp.route('/check_session')
def check_session():
"""Rota para verificar status da sessão via AJAX"""
return jsonify(AuthController.check_session())

123
routes/cota.py Normal file
View File

@@ -0,0 +1,123 @@
from flask import Blueprint, render_template, redirect, url_for, request, jsonify, flash
from flask_login import login_required
from models.entities.cota_mensal import CotaMensal
from services.cota_service import CotaService
from services.militante_service import MilitanteService
from functions.decorators import require_permission
from utils.date_utils import validar_data, converter_data
cota_bp = Blueprint('cota', __name__, url_prefix='/cotas')
@cota_bp.route("/")
@login_required
@require_permission('gerenciar_cotas')
def listar():
"""Lista todas as cotas mensais"""
cotas = CotaService.listar_cotas()
# Calcular status de cada cota
for cota in cotas:
if cota.pago:
cota.status = "paga"
elif cota.data_vencimento < datetime.now().date():
cota.status = "atrasada"
else:
cota.status = "pendente"
militantes = MilitanteService.listar_militantes()
return render_template("listar_cotas.html", cotas=cotas, militantes=militantes)
@cota_bp.route("/novo", methods=["GET", "POST"])
@login_required
@require_permission('gerenciar_cotas')
def nova():
"""Cria uma nova cota"""
if request.method == "POST":
militante_id = request.form.get("militante_id")
valor_antigo = float(request.form.get("valor_antigo"))
valor_novo = float(request.form.get("valor_novo"))
data_alteracao = converter_data(request.form.get("data_alteracao"))
data_vencimento = converter_data(request.form.get("data_vencimento"))
# Validar datas
if not validar_data(data_alteracao) or not validar_data(data_vencimento):
flash('Datas inválidas', 'danger')
return redirect(url_for('cota.nova'))
result = CotaService.criar_cota(militante_id, valor_antigo, valor_novo,
data_alteracao, data_vencimento)
if result:
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({
'status': 'success',
'message': 'Cota cadastrada com sucesso!'
})
flash('Cota cadastrada com sucesso!', 'success')
return redirect(url_for('cota.listar'))
else:
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({
'status': 'error',
'message': 'Erro ao cadastrar cota. Verifique os dados e tente novamente.'
}), 400
flash('Erro ao cadastrar cota', 'danger')
return redirect(url_for('cota.nova'))
# GET
militantes = MilitanteService.listar_militantes()
return render_template("nova_cota.html", militantes=militantes)
@cota_bp.route('/editar/<int:id>', methods=['GET', 'POST'])
@login_required
@require_permission('gerenciar_cotas')
def editar(id):
"""Edita uma cota existente"""
cota = CotaService.buscar_cota(id)
if not cota:
flash('Cota não encontrada', 'danger')
return redirect(url_for('cota.listar'))
if request.method == 'POST':
militante_id = int(request.form['militante_id'])
valor_antigo = float(request.form['valor_antigo'])
valor_novo = float(request.form['valor_novo'])
data_alteracao = converter_data(request.form['data_alteracao'])
data_vencimento = converter_data(request.form['data_vencimento'])
pago = request.form.get('pago', '').lower() == 'true'
if CotaService.atualizar_cota(id, militante_id, valor_antigo, valor_novo,
data_alteracao, data_vencimento, pago):
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({
'status': 'success',
'message': 'Cota atualizada com sucesso!'
})
flash('Cota atualizada com sucesso!', 'success')
return redirect(url_for('cota.listar'))
else:
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({
'status': 'error',
'message': 'Erro ao atualizar cota'
}), 400
flash('Erro ao atualizar cota', 'danger')
return redirect(url_for('cota.editar', id=id))
return render_template('editar_cota.html', cota=cota)
@cota_bp.route('/excluir/<int:id>', methods=['POST'])
@login_required
@require_permission('gerenciar_cotas')
def excluir(id):
"""Exclui uma cota existente"""
if CotaService.excluir_cota(id):
flash('Cota excluída com sucesso!', 'success')
else:
flash('Erro ao excluir cota', 'danger')
return redirect(url_for('cota.listar'))

41
routes/main.py Normal file
View File

@@ -0,0 +1,41 @@
from flask import Blueprint, render_template, redirect, url_for
from flask_login import login_required
from functions.decorators import require_login
from controllers.home_controller import HomeController
main_bp = Blueprint('main', __name__)
@main_bp.route("/")
@require_login
def index():
"""Rota principal - redireciona para home se autenticado"""
return redirect(url_for('main.home'))
@main_bp.route("/home")
@require_login
def home():
"""Página inicial do sistema com dashboard"""
dashboard_data = HomeController.dashboard()
return render_template('home.html',
nome_usuario=dashboard_data['nome_usuario'],
data_atual=dashboard_data['data_atual'],
total_militantes=dashboard_data['total_militantes'],
total_cotas=dashboard_data['total_cotas'],
total_materiais=dashboard_data['total_materiais'],
total_assinaturas=dashboard_data['total_assinaturas'],
ultimos_militantes=dashboard_data['ultimos_militantes'],
ultimos_pagamentos=dashboard_data['ultimos_pagamentos'],
tipos_pagamento=dashboard_data['tipos_pagamento'],
Militante=None) # Militante class for constants
@main_bp.route("/api/setores/<int:cr_id>")
@require_login
def get_setores(cr_id):
"""API para listar setores por comitê regional"""
from services.setor_service import SetorService
setores = SetorService.listar_setores_por_cr(cr_id)
return jsonify({
'setores': [{'id': s.id, 'nome': s.nome} for s in setores]
})

50
routes/militante.py Normal file
View File

@@ -0,0 +1,50 @@
from flask import Blueprint, render_template, redirect, url_for, request, jsonify
from flask_login import login_required
from controllers.militante_controller import MilitanteController
from services.celula_service import CelulaService
from functions.decorators import require_permission, require_role
militante_bp = Blueprint('militante', __name__, url_prefix='/militantes')
@militante_bp.route("/")
@login_required
@require_permission('gerenciar_militantes')
def listar():
"""Lista todos os militantes"""
militantes = MilitanteController.listar_militantes()
celulas = CelulaService.listar_celulas()
return render_template('listar_militantes.html',
militantes=militantes,
celulas=celulas,
Militante=None) # Militante class for constants
@militante_bp.route("/criar", methods=["POST"])
@login_required
@require_permission('gerenciar_militantes')
def criar():
"""Cria um novo militante"""
return MilitanteController.criar_militante(request.form)
@militante_bp.route("/editar/<int:militante_id>", methods=["POST"])
@login_required
@require_permission('gerenciar_militantes')
def editar(militante_id):
"""Edita um militante existente"""
return MilitanteController.atualizar_militante(militante_id, request.form)
@militante_bp.route("/excluir/<int:militante_id>", methods=["POST"])
@login_required
@require_permission('gerenciar_militantes')
def excluir(militante_id):
"""Exclui um militante"""
if MilitanteController.excluir_militante(militante_id):
return redirect(url_for('militante.listar'))
return redirect(url_for('militante.listar'))
@militante_bp.route("/dados/<int:militante_id>")
@login_required
@require_permission('gerenciar_militantes')
def dados(militante_id):
"""Busca os dados de um militante específico"""
return MilitanteController.buscar_dados_militante(militante_id)

116
routes/pagamento.py Normal file
View File

@@ -0,0 +1,116 @@
from flask import Blueprint, render_template, redirect, url_for, request, jsonify, flash
from flask_login import login_required
from services.pagamento_service import PagamentoService
from services.militante_service import MilitanteService
from services.tipo_pagamento_service import TipoPagamentoService
from functions.decorators import require_permission
from utils.date_utils import validar_data, converter_data
pagamento_bp = Blueprint('pagamento', __name__, url_prefix='/pagamentos')
@pagamento_bp.route("/")
@login_required
@require_permission('gerenciar_pagamentos')
def listar():
"""Lista todos os pagamentos"""
pagamentos = PagamentoService.listar_pagamentos()
militantes = MilitanteService.listar_militantes()
tipos_pagamento = TipoPagamentoService.listar_tipos_pagamento()
return render_template("listar_pagamentos.html",
pagamentos=pagamentos,
militantes=militantes,
tipos_pagamento=tipos_pagamento)
@pagamento_bp.route("/novo", methods=["GET", "POST"])
@login_required
@require_permission('gerenciar_pagamentos')
def novo():
"""Cria um novo pagamento"""
if request.method == "POST":
militante_id = request.form.get("militante_id")
tipo_pagamento_id = request.form.get("tipo_pagamento_id")
valor = float(request.form.get("valor"))
data_pagamento = converter_data(request.form.get("data_pagamento"))
if not validar_data(data_pagamento):
flash('Data de pagamento inválida ou futura', 'danger')
return redirect(url_for('pagamento.novo'))
if PagamentoService.criar_pagamento(militante_id, tipo_pagamento_id, valor, data_pagamento):
flash('Pagamento cadastrado com sucesso!', 'success')
return redirect(url_for('pagamento.listar'))
else:
flash('Erro ao cadastrar pagamento', 'danger')
return redirect(url_for('pagamento.novo'))
# GET
militantes = MilitanteService.listar_militantes()
tipos_pagamento = TipoPagamentoService.listar_tipos_pagamento()
return render_template("novo_pagamento.html",
militantes=militantes,
tipos_pagamento=tipos_pagamento)
@pagamento_bp.route("/adicionar", methods=["POST"])
@login_required
@require_permission('gerenciar_pagamentos')
def adicionar():
"""Adiciona um novo pagamento (via AJAX)"""
militante_id = request.form.get("militante_id")
tipo_pagamento = request.form.get("tipo_pagamento")
valor = float(request.form.get("valor"))
data_pagamento = converter_data(request.form.get("data_pagamento"))
if PagamentoService.criar_pagamento_simples(militante_id, tipo_pagamento, valor, data_pagamento):
return jsonify({
'status': 'success',
'message': 'Pagamento adicionado com sucesso!'
})
else:
return jsonify({
'status': 'error',
'message': 'Erro ao adicionar pagamento'
}), 400
@pagamento_bp.route('/editar/<int:id>', methods=['GET', 'POST'])
@login_required
@require_permission('gerenciar_pagamentos')
def editar(id):
"""Edita um pagamento existente"""
pagamento = PagamentoService.buscar_pagamento(id)
if not pagamento:
flash('Pagamento não encontrado', 'danger')
return redirect(url_for('pagamento.listar'))
if request.method == 'POST':
militante_id = int(request.form['militante_id'])
tipo_pagamento_id = int(request.form['tipo_pagamento_id'])
valor = float(request.form['valor'])
data_pagamento = converter_data(request.form['data_pagamento'])
if PagamentoService.atualizar_pagamento(id, militante_id, tipo_pagamento_id, valor, data_pagamento):
flash('Pagamento atualizado com sucesso!', 'success')
return redirect(url_for('pagamento.listar'))
else:
flash('Erro ao atualizar pagamento', 'danger')
return redirect(url_for('pagamento.editar', id=id))
militantes = MilitanteService.listar_militantes()
tipos_pagamento = TipoPagamentoService.listar_tipos_pagamento()
return render_template('editar_pagamento.html',
pagamento=pagamento,
militantes=militantes,
tipos_pagamento=tipos_pagamento)
@pagamento_bp.route('/excluir/<int:id>', methods=['POST'])
@login_required
@require_permission('gerenciar_pagamentos')
def excluir(id):
"""Exclui um pagamento existente"""
if PagamentoService.excluir_pagamento(id):
flash('Pagamento excluído com sucesso!', 'success')
else:
flash('Erro ao excluir pagamento', 'danger')
return redirect(url_for('pagamento.listar'))

149
routes/relatorio.py Normal file
View File

@@ -0,0 +1,149 @@
from flask import Blueprint, render_template, redirect, url_for, request, jsonify, flash
from flask_login import login_required
from datetime import date
from services.relatorio_service import RelatorioService
from services.setor_service import SetorService
from services.comite_service import ComiteService
from functions.decorators import require_permission
from utils.date_utils import validar_data, converter_data
relatorio_bp = Blueprint('relatorio', __name__, url_prefix='/relatorios')
# Rotas para relatórios de cotas
@relatorio_bp.route("/cotas")
@login_required
@require_permission('visualizar_relatorios')
def listar_cotas():
"""Lista todos os relatórios de cotas"""
relatorios = RelatorioService.listar_relatorios_cotas()
return render_template("listar_relatorios_cotas.html", relatorios=relatorios)
@relatorio_bp.route("/cotas/novo", methods=["GET", "POST"])
@login_required
@require_permission('gerar_relatorios')
def novo_relatorio_cotas():
"""Cria um novo relatório de cotas"""
if request.method == "POST":
setor_id = request.form.get("setor_id")
comite_id = request.form.get("comite_id")
total_cotas = float(request.form.get("total_cotas"))
data_relatorio = request.form.get("data_relatorio")
# Validar data
if not validar_data(data_relatorio):
flash('Data do relatório inválida', 'danger')
return render_template("novo_relatorio_cotas.html")
# Converter data
data_relatorio = converter_data(data_relatorio)
# Validar data futura
if data_relatorio > date.today():
flash('A data do relatório não pode ser futura', 'danger')
return render_template("novo_relatorio_cotas.html")
if RelatorioService.criar_relatorio_cotas(setor_id, comite_id, total_cotas, data_relatorio):
flash('Relatório de cotas cadastrado com sucesso!', 'success')
return redirect(url_for('relatorio.listar_cotas'))
else:
flash('Erro ao cadastrar relatório de cotas', 'danger')
return render_template("novo_relatorio_cotas.html")
# GET
setores = SetorService.listar_setores()
comites = ComiteService.listar_comites()
return render_template("novo_relatorio_cotas.html",
setores=setores,
comites=comites,
hoje=date.today().strftime('%Y-%m-%d'))
@relatorio_bp.route('/cotas/editar/<int:id>', methods=['GET', 'POST'])
@login_required
@require_permission('gerar_relatorios')
def editar_relatorio_cotas(id):
"""Edita um relatório de cotas existente"""
relatorio = RelatorioService.buscar_relatorio_cotas(id)
if not relatorio:
flash('Relatório não encontrado', 'danger')
return redirect(url_for('relatorio.listar_cotas'))
if request.method == 'POST':
setor_id = int(request.form['setor_id']) if request.form['setor_id'] else None
comite_id = int(request.form['comite_id']) if request.form['comite_id'] else None
total_cotas = float(request.form['total_cotas'])
data_relatorio = converter_data(request.form['data_relatorio'])
if RelatorioService.atualizar_relatorio_cotas(id, setor_id, comite_id, total_cotas, data_relatorio):
flash('Relatório atualizado com sucesso!', 'success')
return redirect(url_for('relatorio.listar_cotas'))
else:
flash('Erro ao atualizar relatório', 'danger')
return redirect(url_for('relatorio.editar_relatorio_cotas', id=id))
setores = SetorService.listar_setores()
comites = ComiteService.listar_comites()
return render_template('editar_relatorio_cotas.html',
relatorio=relatorio,
setores=setores,
comites=comites)
@relatorio_bp.route('/cotas/excluir/<int:id>', methods=['POST'])
@login_required
@require_permission('gerar_relatorios')
def excluir_relatorio_cotas(id):
"""Exclui um relatório de cotas existente"""
if RelatorioService.excluir_relatorio_cotas(id):
flash('Relatório excluído com sucesso!', 'success')
else:
flash('Erro ao excluir relatório', 'danger')
return redirect(url_for('relatorio.listar_cotas'))
# Rotas para relatórios de vendas
@relatorio_bp.route("/vendas")
@login_required
@require_permission('visualizar_relatorios')
def listar_vendas():
"""Lista todos os relatórios de vendas"""
relatorios = RelatorioService.listar_relatorios_vendas()
return render_template("listar_relatorios_vendas.html", relatorios=relatorios)
@relatorio_bp.route("/vendas/novo", methods=["GET", "POST"])
@login_required
@require_permission('gerar_relatorios')
def novo_relatorio_vendas():
"""Cria um novo relatório de vendas"""
if request.method == "POST":
setor_id = request.form.get("setor_id")
comite_id = request.form.get("comite_id")
total_vendas = float(request.form.get("total_vendas"))
data_relatorio = request.form.get("data_relatorio")
# Validar data
if not validar_data(data_relatorio):
flash('Data do relatório inválida', 'danger')
return render_template("novo_relatorio_vendas.html")
# Converter data
data_relatorio = converter_data(data_relatorio)
# Validar data futura
if data_relatorio > date.today():
flash('A data do relatório não pode ser futura', 'danger')
return render_template("novo_relatorio_vendas.html")
if RelatorioService.criar_relatorio_vendas(setor_id, comite_id, total_vendas, data_relatorio):
flash('Relatório de vendas cadastrado com sucesso!', 'success')
return redirect(url_for('relatorio.listar_vendas'))
else:
flash('Erro ao cadastrar relatório de vendas', 'danger')
return render_template("novo_relatorio_vendas.html")
# GET
setores = SetorService.listar_setores()
comites = ComiteService.listar_comites()
return render_template("novo_relatorio_vendas.html",
setores=setores,
comites=comites,
hoje=date.today().strftime('%Y-%m-%d'))