10 Commits

Author SHA1 Message Date
LS
911ead7835 fix: Corrige relacionamentos entre modelos na estrutura MVC
- Adiciona os modelos que faltavam na arquitetura MVC
- Corrige relacionamentos de referência cruzada entre modelos
- Atualiza script de preparação para criar arquivos __init__.py adequados
- Torna o script de preparação executável

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-04-23 08:16:53 -03:00
LS
91d9bef6c6 rebase usando pagamentos 2025-04-22 18:14:32 -03:00
andersonid
4742a888b7 tela de administração em ajustes 2025-04-22 18:13:33 -03:00
LS
6a3675b735 rebase usando pagamentos 2025-04-22 18:13:24 -03:00
LS
bb6e5c887b rebase usando pagamentos 2025-04-22 18:11:32 -03:00
LS
63ebf09fb6 refactor: renomeia pagamentos para comprovantes e adiciona novos tipos 2025-04-22 18:10:25 -03:00
andersonid
f87e03640d Correções nas mensagens de notificação 2025-04-22 18:10:25 -03:00
andersonid
debcbe6663 Corrige erro de sintaxe no template de login 2025-04-22 18:10:25 -03:00
andersonid
d45fefd72c mensagem de notificação movida para fora do card de formulario de login 2025-04-22 18:10:25 -03:00
LS
62aaec3fbe 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>
2025-04-22 16:35:08 -03:00
102 changed files with 5254 additions and 6355 deletions

20
.gitignore vendored
View File

@@ -260,8 +260,6 @@ poetry.toml
pyrightconfig.json pyrightconfig.json
database.db database.db
database.db-shm
database.db-wal
admin_qr.png admin_qr.png
# End of https://www.toptal.com/developers/gitignore/api/python,flask # End of https://www.toptal.com/developers/gitignore/api/python,flask
@@ -272,21 +270,3 @@ docs/alteracoes_db_connection.md
# QR Codes # QR Codes
*_qr.png *_qr.png
*_qr.txt *_qr.txt
# Redis and Cache
*.rdb
*.aof
dump.rdb
appendonly.aof
# Logs
logs/
*.log
# Docker
.dockerignore
# Environment files
.env.local
.env.production
.env.staging

View File

@@ -35,14 +35,5 @@ ENV PATH="/venv/bin:$PATH"
ENV FLASK_APP=app.py ENV FLASK_APP=app.py
ENV FLASK_ENV=production ENV FLASK_ENV=production
# Criar script de inicialização
RUN echo '#!/bin/sh' > /app/start.sh && \
echo 'echo "Inicializando banco de dados..."' >> /app/start.sh && \
echo 'python init_db.py' >> /app/start.sh && \
echo 'echo "Banco de dados inicializado!"' >> /app/start.sh && \
echo 'echo "Iniciando aplicação..."' >> /app/start.sh && \
echo 'exec gunicorn --bind 0.0.0.0:5000 app:app' >> /app/start.sh && \
chmod +x /app/start.sh
# Comando para rodar a aplicação # Comando para rodar a aplicação
CMD ["/app/start.sh"] CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]

View File

@@ -21,60 +21,3 @@ run-with-seed: seed init run
reset-admin: clean reset-admin: clean
python create_admin.py python create_admin.py
# Docker commands
docker-build:
docker-compose build
docker-up:
docker-compose up -d
docker-down:
docker-compose down
docker-logs:
docker-compose logs -f
docker-restart:
docker-compose restart
# Redis cache commands
cache-clear:
docker-compose exec redis redis-cli FLUSHDB
cache-status:
docker-compose exec redis redis-cli INFO
cache-keys:
docker-compose exec redis redis-cli KEYS "*"
# Development with Docker
dev-up: docker-build docker-up
@echo "Development environment started with Redis cache"
@echo "Application: http://localhost:5000"
@echo "Redis: localhost:6379"
dev-down: docker-down
@echo "Development environment stopped"
# Production commands
prod-build:
docker-compose -f docker-compose.yml build --no-cache
prod-up:
docker-compose -f docker-compose.yml up -d
prod-logs:
docker-compose -f docker-compose.yml logs -f app
# Cache management
cache-warmup:
@echo "Warming up cache..."
curl -X GET http://localhost:5000/api/dashboard/stats
curl -X GET http://localhost:5000/api/dashboard/militante-stats
curl -X GET http://localhost:5000/api/dashboard/financial-stats
@echo "Cache warmup completed"
cache-monitor:
@echo "Monitoring Redis cache..."
watch -n 5 'docker-compose exec redis redis-cli INFO memory'

184
README.md
View File

@@ -1,134 +1,116 @@
# Sistema de Controle de Militantes # Sistema de Controles
Sistema para gerenciamento de militantes, células, setores e comitês regionais. Sistema de gestão para controle de militantes, pagamentos, cotas e relatórios.
## Estrutura de Permissões (RBAC) ## Arquitetura MVC
O sistema utiliza um sistema de controle de acesso baseado em papéis (RBAC) com a seguinte hierarquia: O projeto segue a arquitetura Model-View-Controller (MVC) para separação de responsabilidades:
### Níveis de Papéis ### Models
1. **Militante Básico** (Nível 1) Os modelos representam as entidades do sistema e estão organizados em:
- Visualizar próprios dados
- Editar próprios dados
- Visualizar dados da célula
2. **Secretário de Célula** (Nível 2) - **models/entities/**: Classes de entidades do banco de dados (SQLAlchemy)
- Todas as permissões do Militante Básico - `base.py`: Configuração do SQLAlchemy e classe Base
- Gerenciar membros da célula - `usuario.py`: Modelo de usuário
- Criar membros na célula - `militante.py`: Modelo de militante
- Visualizar relatórios da célula - `cota_mensal.py`: Modelo de cota mensal
- etc.
3. **Membro de Setor** (Nível 3) ### Controllers
- Todas as permissões do Secretário de Célula
- Visualizar relatórios do setor
4. **Secretário de Setor** (Nível 4) Os controladores contêm a lógica de negócio e manipulam os dados dos modelos:
- Todas as permissões do Membro de Setor
- Gerenciar células do setor
- Criar células no setor
5. **Membro de CR** (Nível 5) - **controllers/**: Implementação dos controladores
- Todas as permissões do Secretário de Setor - `auth_controller.py`: Controle de autenticação
- Visualizar relatórios do CR - `usuario_controller.py`: Operações com usuários
- `militante_controller.py`: Operações com militantes
- `home_controller.py`: Controlador da página inicial
- etc.
6. **Secretário de CR** (Nível 6) ### Views
- Todas as permissões do Membro de CR
- Gerenciar setores do CR
- Criar setores no CR
7. **Membro do CC** (Nível 7) As views são os templates que exibem os dados para o usuário:
- Todas as permissões do Secretário de CR
- Visualizar relatórios nacionais
8. **Secretário Geral** (Nível 8) - **templates/**: Templates Jinja2
- Todas as permissões do Membro do CC - Organizados por funcionalidade (admin, militantes, cotas, etc.)
- Gerenciar CRs
- Criar CRs ### Services
- Configurar sistema
Camada adicional para encapsular a lógica de acesso a dados:
- **services/**: Serviços para acesso a dados
- `database_service.py`: Gerenciamento de conexões com o banco
- `usuario_service.py`: Acesso a dados de usuários
- `militante_service.py`: Acesso a dados de militantes
- etc.
### Routes
Rotas da aplicação organizadas em blueprints:
- **routes/**: Módulos de rotas (Flask Blueprints)
- `main.py`: Rotas principais
- `auth.py`: Rotas de autenticação
- `admin.py`: Rotas administrativas
- `militante.py`: Rotas para gerenciamento de militantes
- etc.
## Instalação ## Instalação
1. Clone o repositório 1. Clone o repositório:
2. Crie um ambiente virtual:
```bash
python -m venv venv
source venv/bin/activate # Linux/Mac
# ou
venv\Scripts\activate # Windows
``` ```
git clone [URL_DO_REPOSITORIO]
```
2. Crie e ative um ambiente virtual:
```
python -m venv myenv
source myenv/bin/activate # Linux/Mac
myenv\Scripts\activate # Windows
```
3. Instale as dependências: 3. Instale as dependências:
```bash ```
pip install -r requirements.txt pip install -r requirements.txt
``` ```
4. Execute as migrações do banco de dados:
```bash 4. Inicialize o banco de dados:
python sql/migrate_db.py
``` ```
5. Configure as variáveis de ambiente no arquivo `.env`: python app.py --init
```
FLASK_APP=app.py
FLASK_ENV=development
SECRET_KEY=sua_chave_secreta
MAIL_SERVER=seu_servidor_smtp
MAIL_PORT=587
MAIL_USE_TLS=True
MAIL_USERNAME=seu_email
MAIL_PASSWORD=sua_senha
```
6. Execute o aplicativo:
```bash
flask run
``` ```
## Uso 5. Execute a aplicação:
```
python app.py
```
### Decoradores de Permissão ## Credenciais padrão
O sistema fornece três decoradores para controle de acesso: - **Administrador**:
- Usuário: admin
- Senha: admin123
1. `@require_permission(permission_name)` ## Desenvolvimento
- Verifica se o usuário tem uma permissão específica
- Exemplo: `@require_permission('create_cell_member')`
2. `@require_role(role_name)` Para adicionar novos recursos, siga a arquitetura MVC:
- Verifica se o usuário tem um papel específico
- Exemplo: `@require_role('Secretário de Célula')`
3. `@require_minimum_role(min_level)` 1. Crie modelos necessários em `models/entities/`
- Verifica se o usuário tem um papel com nível mínimo 2. Implemente serviços para acesso a dados em `services/`
- Exemplo: `@require_minimum_role(Role.SECRETARIO_CR)` 3. Crie controladores com lógica de negócio em `controllers/`
4. Adicione rotas em módulos existentes ou crie novos em `routes/`
5. Desenvolva templates em `templates/`
### Verificando Permissões no Código ## Testes
```python Execute os testes usando pytest:
# Verificar se um usuário tem uma permissão
if user.has_permission('create_cell_member'):
# Faça algo
# Verificar se um usuário tem um papel ```
if user.has_role('Secretário de Célula'): python -m pytest
# Faça algo
# Obter o papel mais alto do usuário
highest_role = user.get_highest_role()
if highest_role and highest_role.nivel >= Role.SECRETARIO_CR:
# Faça algo
``` ```
## Estrutura do Banco de Dados Ou use o script de teste:
O sistema utiliza as seguintes tabelas para o RBAC: ```
./run_tests.sh
- `roles`: Armazena os papéis disponíveis ```
- `permissions`: Armazena as permissões disponíveis
- `role_permissions`: Mapeia papéis para permissões
- `user_roles`: Mapeia usuários para papéis
## Segurança
- Todas as senhas são armazenadas com hash bcrypt
- Sessões expiram após período de inatividade
- Controle de acesso granular baseado em papéis
- Proteção contra CSRF
- Validação de entrada de dados

2413
app.py

File diff suppressed because it is too large Load Diff

123
app.py.new Normal file
View File

@@ -0,0 +1,123 @@
from flask import Flask
from flask_bootstrap import Bootstrap5
from flask_mail import Mail
from flask_login import LoginManager
from flask_wtf.csrf import CSRFProtect
from dotenv import load_dotenv
import os
import secrets
import logging
# Importações de configurações
from models.entities.base import Base, engine
from routes.main import main_bp
from routes.admin import admin_bp
from routes.auth import auth_bp
from routes.militante import militante_bp
from routes.pagamento import pagamento_bp
from routes.relatorio import relatorio_bp
from routes.cota import cota_bp
# Configuração do logger
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Carregar variáveis de ambiente
load_dotenv()
def create_app():
"""Factory para criação da aplicação Flask"""
app = Flask(__name__)
# Configuração secreta
app.secret_key = os.getenv('SECRET_KEY', secrets.token_hex(16))
# Configuração de Bootstrap
bootstrap = Bootstrap5(app)
# Registrar blueprints
app.register_blueprint(main_bp)
app.register_blueprint(admin_bp)
app.register_blueprint(auth_bp)
app.register_blueprint(militante_bp)
app.register_blueprint(pagamento_bp)
app.register_blueprint(relatorio_bp)
app.register_blueprint(cota_bp)
# Configurar proteção CSRF
csrf = CSRFProtect()
csrf.init_app(app)
app.config['WTF_CSRF_CHECK_DEFAULT'] = False
app.config['WTF_CSRF_HEADERS'] = ['X-CSRFToken']
# Configurar Flask-Login
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'auth.login'
# Função para carregar usuário no login_manager
from models.entities.usuario import Usuario
from services.database_service import DatabaseService
from sqlalchemy.orm import joinedload
@login_manager.user_loader
def load_user(user_id):
"""Carrega o usuário pelo ID"""
db = DatabaseService.get_db_connection()
try:
user = db.query(Usuario).options(
joinedload(Usuario.roles)
).get(user_id)
return user
finally:
db.close()
# Adicionar filtros Jinja2
@app.template_filter('bitwise_and')
def bitwise_and(value1, value2):
"""Filtro para operação bit a bit AND"""
return value1 & value2
# Configurar Flask-Mail
app.config['MAIL_SERVER'] = os.getenv('MAIL_SERVER', 'smtp.gmail.com')
app.config['MAIL_PORT'] = int(os.getenv('MAIL_PORT', 587))
app.config['MAIL_USE_TLS'] = os.getenv('MAIL_USE_TLS', 'True').lower() == 'true'
app.config['MAIL_USERNAME'] = os.getenv('MAIL_USERNAME')
app.config['MAIL_PASSWORD'] = os.getenv('MAIL_PASSWORD')
app.config['MAIL_DEFAULT_SENDER'] = os.getenv('MAIL_DEFAULT_SENDER')
# Inicializar Mail
mail = Mail(app)
return app
def init_system():
"""Inicializa o sistema com banco de dados e usuários padrão"""
from functions.database import init_database
# Inicializar banco de dados
logger.info("Inicializando banco de dados...")
init_database()
# Outros procedimentos de inicialização podem ser adicionados aqui
def main():
"""Inicializa e retorna a aplicação Flask"""
return create_app()
# Criar a aplicação
app = main()
if __name__ == '__main__':
import sys
# Verificar se é para inicializar o sistema
if '--init' in sys.argv:
init_system()
else:
# Executar a aplicação
app.run(
host='0.0.0.0',
port=5000,
debug=os.getenv('FLASK_ENV') == 'development'
)

View File

@@ -1 +0,0 @@
# Controllers package

View File

@@ -1,36 +1,32 @@
from flask import Blueprint, request, render_template, redirect, url_for, flash, session, jsonify from flask import session, flash, redirect, url_for, request
from flask_login import login_user, logout_user, current_user from flask_login import login_user, logout_user, current_user
from datetime import datetime from datetime import datetime
from functions.database import get_db_connection, Usuario
from functions.decorators import require_login
from werkzeug.security import generate_password_hash
import pyotp import pyotp
import qrcode import qrcode
import base64
from io import BytesIO from io import BytesIO
import base64
auth_bp = Blueprint('auth', __name__) from models.entities.usuario import Usuario
from services.database_service import DatabaseService
@auth_bp.route("/login", methods=["GET", "POST"]) class AuthController:
def login(): """Controlador para funções de autenticação"""
"""Rota de login"""
print(f"=== LOGIN ROUTE CALLED ===")
print(f"Method: {request.method}")
print(f"Form data: {dict(request.form)}")
if request.method == "POST": @staticmethod
def login():
"""Processa o login de usuário"""
if request.method != "POST":
return False
email_or_username = request.form.get("email") email_or_username = request.form.get("email")
password = request.form.get("password") password = request.form.get("password")
otp = request.form.get("otp") otp = request.form.get("otp")
print(f"Tentativa de login - Email/Username: {email_or_username}, OTP: {otp}")
if not all([email_or_username, password]): if not all([email_or_username, password]):
print("Erro: Email/usuário e senha são obrigatórios")
flash("Email/usuário e senha são obrigatórios.", "danger") flash("Email/usuário e senha são obrigatórios.", "danger")
return redirect(url_for("auth.login")) return False
db = get_db_connection() db = DatabaseService.get_db_connection()
try: try:
# Tenta encontrar o usuário por email ou username # Tenta encontrar o usuário por email ou username
user = db.query(Usuario).filter( user = db.query(Usuario).filter(
@@ -38,27 +34,18 @@ def login():
(Usuario.username == email_or_username) (Usuario.username == email_or_username)
).first() ).first()
print(f"Usuário encontrado: {user.username if user else 'Não encontrado'}")
if not user or not user.check_password(password): if not user or not user.check_password(password):
print("Erro: Email/usuário ou senha incorretos")
flash("Email/usuário ou senha incorretos.", "danger") flash("Email/usuário ou senha incorretos.", "danger")
return redirect(url_for("auth.login")) return False
print(f"Senha válida. OTP Secret: {user.otp_secret}")
# Verificar OTP se o usuário tiver configurado # Verificar OTP se o usuário tiver configurado
if user.otp_secret and not otp: if user.otp_secret and not otp:
print("Erro: Código OTP é obrigatório")
flash("Código OTP é obrigatório para sua conta.", "danger") flash("Código OTP é obrigatório para sua conta.", "danger")
return redirect(url_for("auth.login")) return False
if user.otp_secret and not user.verify_otp(otp): if user.otp_secret and not user.verify_otp(otp):
print(f"Erro: Código OTP inválido. Código fornecido: {otp}")
flash("Código OTP inválido.", "danger") flash("Código OTP inválido.", "danger")
return redirect(url_for("auth.login")) return False
print("OTP válido! Fazendo login...")
# Atualizar último login # Atualizar último login
user.ultimo_login = datetime.utcnow() user.ultimo_login = datetime.utcnow()
@@ -69,236 +56,69 @@ def login():
session['user_id'] = user.id session['user_id'] = user.id
session['username'] = user.username session['username'] = user.username
session['is_admin'] = user.is_admin session['is_admin'] = user.is_admin
print(f"Login realizado: user_id={user.id}, username={user.username}, is_admin={user.is_admin}")
# Redirecionar para home return True
return redirect(url_for("home.index"))
finally: finally:
db.close() db.close()
return render_template("login.html") @staticmethod
def logout():
@auth_bp.route("/api/login", methods=["POST"]) """Processa o logout de usuário"""
def api_login(): db = DatabaseService.get_db_connection()
"""Endpoint de login API sem CSRF para automação/testes"""
try:
# Verificar se é uma requisição JSON
if request.is_json:
data = request.get_json()
email_or_username = data.get("email") or data.get("username")
password = data.get("password")
otp = data.get("otp")
else:
# Fallback para form data
email_or_username = request.form.get("email") or request.form.get("username")
password = request.form.get("password")
otp = request.form.get("otp")
print(f"=== API LOGIN CALLED ===")
print(f"Email/Username: {email_or_username}")
print(f"OTP: {otp}")
# Validações básicas
if not email_or_username or not password:
return jsonify({
'success': False,
'error': 'Email/username e senha são obrigatórios'
}), 400
db = get_db_connection()
try: try:
# Buscar usuário user = current_user
user = db.query(Usuario).filter( if user.is_authenticated:
(Usuario.email == email_or_username) |
(Usuario.username == email_or_username)
).first()
if not user:
return jsonify({
'success': False,
'error': 'Usuário não encontrado'
}), 401
# Verificar senha
if not user.check_password(password):
return jsonify({
'success': False,
'error': 'Senha incorreta'
}), 401
# Verificar OTP se configurado
if user.otp_secret:
if not otp:
return jsonify({
'success': False,
'error': 'Código OTP é obrigatório para esta conta'
}), 400
if not user.verify_otp(otp):
return jsonify({
'success': False,
'error': 'Código OTP inválido'
}), 401
# Atualizar último login
user.ultimo_login = datetime.utcnow()
db.commit()
# Fazer login
login_user(user)
session['user_id'] = user.id
session['username'] = user.username
session['is_admin'] = user.is_admin
print(f"API Login realizado: user_id={user.id}, username={user.username}")
# Retornar resposta de sucesso
return jsonify({
'success': True,
'message': 'Login realizado com sucesso',
'user': {
'id': user.id,
'username': user.username,
'email': user.email,
'nome': user.nome,
'is_admin': user.is_admin,
'ultimo_login': user.ultimo_login.isoformat() if user.ultimo_login else None
},
'session_id': session.get('_id')
})
finally:
db.close()
except Exception as e:
print(f"Erro no API login: {e}")
return jsonify({
'success': False,
'error': 'Erro interno do servidor'
}), 500
@auth_bp.route("/api/logout", methods=["POST"])
def api_logout():
"""Endpoint de logout API"""
try:
if current_user.is_authenticated:
db = get_db_connection()
try:
user = current_user
user.logout() user.logout()
db.commit() db.commit()
finally: logout_user()
db.close() flash('Logout realizado com sucesso!', 'success')
return True
logout_user() finally:
db.close()
return jsonify({
'success': True, @staticmethod
'message': 'Logout realizado com sucesso' def alterar_senha(user_id, senha_atual, nova_senha, confirmar_senha):
}) """Altera a senha do usuário"""
except Exception as e:
print(f"Erro no API logout: {e}")
return jsonify({
'success': False,
'error': 'Erro interno do servidor'
}), 500
@auth_bp.route("/api/status")
def api_status():
"""Endpoint para verificar status da autenticação"""
if current_user.is_authenticated:
return jsonify({
'authenticated': True,
'user': {
'id': current_user.id,
'username': current_user.username,
'email': current_user.email,
'nome': current_user.nome,
'is_admin': current_user.is_admin
}
})
else:
return jsonify({
'authenticated': False
})
@auth_bp.route("/logout")
@require_login
def logout():
db = get_db_connection()
try:
user = current_user
if user:
user.logout()
db.commit()
logout_user()
finally:
db.close()
flash('Logout realizado com sucesso!', 'success')
return redirect(url_for('auth.login'))
@auth_bp.route("/alterar_senha", methods=["GET", "POST"])
@require_login
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 not all([senha_atual, nova_senha, confirmar_senha]): if not all([senha_atual, nova_senha, confirmar_senha]):
flash("Todos os campos são obrigatórios.", "error") flash("Todos os campos são obrigatórios.", "error")
return redirect(url_for("auth.alterar_senha")) return False
if nova_senha != confirmar_senha: if nova_senha != confirmar_senha:
flash("As senhas não coincidem.", "error") flash("As senhas não coincidem.", "error")
return redirect(url_for("auth.alterar_senha")) return False
db = get_db_connection() db = DatabaseService.get_db_connection()
try: try:
user = db.query(Usuario).get(current_user.id) user = db.query(Usuario).get(user_id)
if not user:
flash("Usuário não encontrado.", "error")
return False
if not user.check_password(senha_atual): if not user.check_password(senha_atual):
flash("Senha atual incorreta.", "error") flash("Senha atual incorreta.", "error")
return redirect(url_for("auth.alterar_senha")) return False
user.password_hash = generate_password_hash(nova_senha) user.set_password(nova_senha)
db.commit() db.commit()
flash("Senha alterada com sucesso!", "success") flash("Senha alterada com sucesso!", "success")
return redirect(url_for("home.index")) return True
finally: finally:
db.close() db.close()
return render_template("alterar_senha.html") @staticmethod
def generate_qr_code(user):
@auth_bp.route("/qr/<token>") """Gera um QR code para o usuário"""
def get_qr_code(token): if not user.otp_secret:
"""Gera QR code para configuração OTP""" user.otp_secret = pyotp.random_base32()
db = get_db_connection()
try:
militante = db.query(Militante).filter_by(temp_token=token).first()
if not militante or militante.temp_token_expiry < datetime.now():
flash('Token inválido ou expirado.', 'danger')
return redirect(url_for('auth.login'))
qr_code = generate_qr_code(militante) totp = pyotp.TOTP(user.otp_secret)
return render_template('mostrar_qr_code.html', qr_code=qr_code) qr = qrcode.QRCode(version=1, box_size=10, border=5)
finally: qr.add_data(totp.provisioning_uri(user.email, issuer_name="Sistema de Controles"))
db.close() qr.make(fit=True)
def generate_qr_code(user): img = qr.make_image(fill_color="black", back_color="white")
"""Gera um QR code para o usuário""" buffer = BytesIO()
if not user.otp_secret: img.save(buffer, format="PNG")
user.otp_secret = pyotp.random_base32() qr_code = base64.b64encode(buffer.getvalue()).decode('utf-8')
totp = pyotp.TOTP(user.otp_secret) return qr_code
qr = qrcode.QRCode(version=1, box_size=10, border=5)
qr.add_data(totp.provisioning_uri(user.email, issuer_name="Sistema de Controles"))
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
buffer = BytesIO()
img.save(buffer, format="PNG")
qr_code = base64.b64encode(buffer.getvalue()).decode('utf-8')
return qr_code

View File

@@ -1,134 +0,0 @@
from flask import Blueprint, request, render_template, redirect, url_for, flash, jsonify
from functions.database import get_db_connection, CotaMensal, Militante
from functions.decorators import require_login
from utils.date_utils import validar_data, converter_data
from datetime import datetime
from flask_login import current_user
cota_bp = Blueprint('cota', __name__)
@cota_bp.route("/cotas/novo", methods=["GET", "POST"])
@require_login
def novo():
"""Cria uma nova cota mensal"""
if request.method == "POST":
try:
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"))
if not validar_data(data_alteracao) or not validar_data(data_vencimento):
flash('Data inválida ou futura', 'danger')
return redirect(url_for('cota.novo'))
db = get_db_connection()
cota = CotaMensal(
militante_id=militante_id,
valor_antigo=valor_antigo,
valor_novo=valor_novo,
data_alteracao=data_alteracao,
data_vencimento=data_vencimento,
pago=False
)
db.add(cota)
db.commit()
flash('Cota cadastrada com sucesso!', 'success')
return redirect(url_for('cota.listar'))
except Exception as e:
db.rollback()
flash('Erro ao cadastrar cota', 'danger')
return redirect(url_for('cota.novo'))
finally:
db.close()
# GET - Renderizar formulário
db = get_db_connection()
try:
militantes = db.query(Militante).order_by(Militante.nome).all()
return render_template("nova_cota.html", militantes=militantes)
finally:
db.close()
@cota_bp.route("/cotas")
@require_login
def listar():
"""Lista todas as cotas mensais com controle de permissões no nível de dados"""
db = get_db_connection()
try:
# SEMPRE renderizar o template, mas filtrar os dados baseado nas permissões
cotas = []
# Verificar permissões para filtrar dados
if current_user.is_admin:
# Admin vê todas
cotas = db.query(CotaMensal).join(Militante).order_by(CotaMensal.data_vencimento.desc()).all()
elif hasattr(current_user, 'has_permission'):
# Outros usuários veem baseado nas suas permissões
# Por enquanto, deixar vazio até implementar a lógica específica
cotas = []
# SEMPRE renderizar o template, independente das permissões
return render_template("listar_cotas.html", cotas=cotas)
except Exception as e:
print(f"Erro no controller de cotas: {e}")
# Em caso de erro, renderizar com dados vazios
return render_template("listar_cotas.html", cotas=[])
finally:
db.close()
@cota_bp.route('/cotas/editar/<int:id>', methods=['GET', 'POST'])
@require_login
def editar(id):
"""Edita uma cota mensal"""
db = get_db_connection()
try:
cota = db.query(CotaMensal).get(id)
if not cota:
flash('Cota não encontrada.', 'danger')
return redirect(url_for('cota.listar'))
if request.method == "POST":
try:
cota.valor_antigo = float(request.form.get("valor_antigo"))
cota.valor_novo = float(request.form.get("valor_novo"))
cota.data_alteracao = converter_data(request.form.get("data_alteracao"))
cota.data_vencimento = converter_data(request.form.get("data_vencimento"))
cota.pago = request.form.get("pago") == "on"
db.commit()
flash('Cota atualizada com sucesso!', 'success')
return redirect(url_for('cota.listar'))
except Exception as e:
db.rollback()
flash('Erro ao atualizar cota.', 'danger')
print(f"Erro ao atualizar cota: {e}")
militantes = db.query(Militante).order_by(Militante.nome).all()
return render_template("editar_cota.html", cota=cota, militantes=militantes)
finally:
db.close()
@cota_bp.route("/cotas/excluir/<int:id>", methods=["POST"])
@require_login
def excluir(id):
"""Exclui uma cota mensal"""
db = get_db_connection()
try:
cota = db.query(CotaMensal).get(id)
if not cota:
flash('Cota não encontrada.', 'danger')
return redirect(url_for('cota.listar'))
# Excluir a cota
db.delete(cota)
db.commit()
flash('Cota excluída com sucesso!', 'success')
except Exception as e:
db.rollback()
flash('Erro ao excluir cota. Por favor, tente novamente.', 'danger')
print(f"Erro ao excluir cota: {e}")
finally:
db.close()
return redirect(url_for('cota.listar'))

View File

@@ -1,184 +1,80 @@
from flask import Blueprint, render_template, flash, redirect, url_for, jsonify from flask import session, render_template
from functions.database import get_db_connection, Militante, Pagamento, CotaMensal, MaterialVendido, AssinaturaAnual, TipoPagamento
from functions.decorators import require_login
from datetime import datetime from datetime import datetime
from sqlalchemy import func from sqlalchemy.sql import func
from services.dashboard_service import DashboardService
from services.cache_service import cache_service, CacheKeys
from flask_login import current_user
import logging
logger = logging.getLogger(__name__) from models.entities.militante import Militante
from models.entities.cota_mensal import CotaMensal
from models.entities.material_vendido import MaterialVendido
from models.entities.assinatura_anual import AssinaturaAnual
from models.entities.pagamento import Pagamento
from models.entities.tipo_pagamento import TipoPagamento
from models.entities.usuario import Usuario
from services.database_service import DatabaseService
home_bp = Blueprint('home', __name__) class HomeController:
"""Controlador para página inicial e dashboard"""
@home_bp.route("/")
@require_login @staticmethod
def index(): def dashboard():
"""Rota principal""" """Gera dados para o dashboard principal"""
return redirect(url_for('home.dashboard')) db = DatabaseService.get_db_connection()
@home_bp.route("/dashboard")
@home_bp.route("/home")
@require_login
def dashboard():
"""Página inicial do sistema com dashboard"""
try:
# Get dashboard stats from cached service
stats = DashboardService.get_dashboard_stats()
# Get tipos de pagamento for the modal
db = get_db_connection()
try: try:
# Buscar nome do usuário
usuario = db.query(Usuario).get(session.get('user_id'))
nome_usuario = usuario.username if usuario else "Usuário"
# Formatar data atual em português
data_atual = datetime.now().strftime("%d de %B de %Y")
# Buscar dados para o dashboard
total_militantes = db.query(Militante).count()
total_cotas = db.query(func.sum(CotaMensal.valor_novo)).scalar() or 0
total_materiais = db.query(MaterialVendido).count()
total_assinaturas = db.query(AssinaturaAnual).count()
# Buscar últimos militantes cadastrados
ultimos_militantes = db.query(Militante)\
.order_by(Militante.id.desc())\
.limit(5)\
.all()
# Buscar últimos pagamentos
ultimos_pagamentos = db.query(Pagamento)\
.join(Militante)\
.order_by(Pagamento.data_pagamento.desc())\
.limit(5)\
.all()
# Buscar tipos de pagamento
tipos_pagamento = db.query(TipoPagamento).all() tipos_pagamento = db.query(TipoPagamento).all()
return {
'nome_usuario': nome_usuario,
'data_atual': data_atual,
'total_militantes': total_militantes,
'total_cotas': "{:.2f}".format(total_cotas),
'total_materiais': total_materiais,
'total_assinaturas': total_assinaturas,
'ultimos_militantes': ultimos_militantes,
'ultimos_pagamentos': ultimos_pagamentos,
'tipos_pagamento': tipos_pagamento
}
except Exception as e:
print(f"Erro ao carregar dashboard: {e}")
import traceback
traceback.print_exc()
return {
'nome_usuario': "Usuário",
'data_atual': datetime.now().strftime("%d/%m/%Y"),
'total_militantes': 0,
'total_cotas': "0.00",
'total_materiais': 0,
'total_assinaturas': 0,
'ultimos_militantes': [],
'ultimos_pagamentos': [],
'tipos_pagamento': []
}
finally: finally:
db.close() db.close()
return render_template('home.html',
nome_usuario=current_user.nome or current_user.username,
data_atual=datetime.now().strftime("%d/%m/%Y"),
total_militantes=stats.get('total_militantes', 0),
total_cotas=stats.get('total_cotas', "0.00"),
total_materiais=stats.get('total_materiais', 0),
total_assinaturas=stats.get('total_assinaturas', 0),
ultimos_militantes=stats.get('ultimos_militantes', []),
ultimos_pagamentos=stats.get('ultimos_pagamentos', []),
tipos_pagamento=tipos_pagamento,
Militante=Militante,
cache_timestamp=stats.get('cache_timestamp'))
except Exception as e:
logger.error(f"Erro na página inicial: {e}")
import traceback
traceback.print_exc()
flash('Erro ao carregar a página inicial', 'danger')
return render_template('home.html',
nome_usuario="Usuário",
data_atual=datetime.now().strftime("%d/%m/%Y"),
total_militantes=0,
total_cotas="0.00",
total_materiais=0,
total_assinaturas=0,
ultimos_militantes=[],
ultimos_pagamentos=[],
Militante=Militante)
@home_bp.route('/check_session')
def check_session():
"""Verifica se a sessão ainda é válida"""
if current_user.is_authenticated:
if current_user.is_session_expired():
return jsonify({'valid': False, 'message': 'Sessão expirada'})
return jsonify({'valid': True})
return jsonify({'valid': False, 'message': 'Usuário não autenticado'})
@home_bp.route('/api/dashboard/stats')
@require_login
def api_dashboard_stats():
"""API endpoint for dashboard statistics"""
try:
stats = DashboardService.get_dashboard_stats()
return jsonify({
'success': True,
'data': stats
})
except Exception as e:
logger.error(f"Erro ao obter estatísticas do dashboard: {e}")
return jsonify({
'success': False,
'error': 'Erro ao obter estatísticas'
}), 500
@home_bp.route('/api/dashboard/militante-stats')
@require_login
def api_militante_stats():
"""API endpoint for militante statistics"""
try:
stats = DashboardService.get_militante_stats()
return jsonify({
'success': True,
'data': stats
})
except Exception as e:
logger.error(f"Erro ao obter estatísticas de militantes: {e}")
return jsonify({
'success': False,
'error': 'Erro ao obter estatísticas de militantes'
}), 500
@home_bp.route('/api/dashboard/financial-stats')
@require_login
def api_financial_stats():
"""API endpoint for financial statistics"""
try:
stats = DashboardService.get_financial_stats()
return jsonify({
'success': True,
'data': stats
})
except Exception as e:
logger.error(f"Erro ao obter estatísticas financeiras: {e}")
return jsonify({
'success': False,
'error': 'Erro ao obter estatísticas financeiras'
}), 500
@home_bp.route('/api/cache/clear')
@require_login
def clear_cache():
"""Clear all cache (admin only)"""
if not current_user.is_admin:
return jsonify({
'success': False,
'error': 'Acesso negado'
}), 403
try:
cache_service.clear_all()
# Invalidate dashboard cache
DashboardService.invalidate_dashboard_cache()
logger.info(f"Cache limpo por {current_user.username}")
return jsonify({
'success': True,
'message': 'Cache limpo com sucesso'
})
except Exception as e:
logger.error(f"Erro ao limpar cache: {e}")
return jsonify({
'success': False,
'error': 'Erro ao limpar cache'
}), 500
@home_bp.route('/api/cache/status')
@require_login
def cache_status():
"""Get cache status (admin only)"""
if not current_user.is_admin:
return jsonify({
'success': False,
'error': 'Acesso negado'
}), 403
try:
# Check if Redis is connected
is_connected = cache_service._is_connected()
# Get some cache statistics
stats = {
'connected': is_connected,
'dashboard_stats_cached': cache_service.exists(CacheKeys.DASHBOARD_STATS),
'dashboard_stats_ttl': cache_service.ttl(CacheKeys.DASHBOARD_STATS) if is_connected else -1
}
return jsonify({
'success': True,
'data': stats
})
except Exception as e:
logger.error(f"Erro ao obter status do cache: {e}")
return jsonify({
'success': False,
'error': 'Erro ao obter status do cache'
}), 500

View File

@@ -1,243 +0,0 @@
from flask import Blueprint, request, render_template, redirect, url_for, flash, jsonify
from functions.database import get_db_connection, MaterialVendido, Militante, TipoMaterial
from functions.decorators import require_login
from utils.date_utils import validar_data, converter_data
from datetime import datetime
from flask_login import current_user
material_bp = Blueprint('material', __name__)
@material_bp.route("/materiais")
@require_login
def listar():
"""Lista todos os materiais com controle de permissões no nível de dados"""
db = get_db_connection()
try:
# SEMPRE renderizar o template, mas filtrar os dados baseado nas permissões
materiais = []
# Verificar permissões para filtrar dados
if current_user.is_admin:
# Admin vê todos
materiais = db.query(MaterialVendido).join(Militante).join(TipoMaterial).order_by(MaterialVendido.data_venda.desc()).all()
elif hasattr(current_user, 'has_permission'):
# Outros usuários veem baseado nas suas permissões
# Por enquanto, mostrar todos - pode ser refinado depois
materiais = db.query(MaterialVendido).join(Militante).join(TipoMaterial).order_by(MaterialVendido.data_venda.desc()).all()
# Buscar tipos para o template
tipos_materiais = db.query(TipoMaterial).order_by(TipoMaterial.descricao).all()
# SEMPRE renderizar o template, independente das permissões
return render_template('listar_materiais.html',
materiais=materiais,
tipos_materiais=tipos_materiais)
except Exception as e:
print(f"Erro no controller de materiais: {e}")
# Em caso de erro, renderizar com dados vazios
return render_template('listar_materiais.html',
materiais=[],
tipos_materiais=[])
finally:
db.close()
@material_bp.route("/materiais/novo", methods=["GET", "POST"])
@require_login
def novo():
"""Cria um novo material vendido"""
if request.method == "POST":
try:
militante_id = request.form.get("militante_id")
tipo_material_id = request.form.get("tipo_material_id")
descricao = request.form.get("descricao")
valor = float(request.form.get("valor"))
data_venda = converter_data(request.form.get("data_venda"))
if not validar_data(data_venda):
flash('Data de venda inválida ou futura', 'danger')
return redirect(url_for('material.novo'))
db = get_db_connection()
material = MaterialVendido(
militante_id=militante_id,
tipo_material_id=tipo_material_id,
descricao=descricao,
valor=valor,
data_venda=data_venda
)
db.add(material)
db.commit()
flash('Material cadastrado com sucesso!', 'success')
return redirect(url_for('material.listar'))
except Exception as e:
db.rollback()
flash('Erro ao cadastrar material', 'danger')
return redirect(url_for('material.novo'))
finally:
db.close()
# GET - Renderizar formulário
db = get_db_connection()
try:
militantes = db.query(Militante).order_by(Militante.nome).all()
tipos_material = db.query(TipoMaterial).order_by(TipoMaterial.descricao).all()
return render_template("novo_material.html", militantes=militantes, tipos_material=tipos_material)
finally:
db.close()
@material_bp.route('/materiais/editar/<int:id>', methods=['GET', 'POST'])
@require_login
def editar(id):
"""Edita um material vendido"""
db = get_db_connection()
try:
material = db.query(MaterialVendido).get(id)
if not material:
flash('Material não encontrado.', 'danger')
return redirect(url_for('material.listar'))
if request.method == "POST":
try:
material.militante_id = request.form.get("militante_id")
material.tipo_material_id = request.form.get("tipo_material_id")
material.descricao = request.form.get("descricao")
material.valor = float(request.form.get("valor"))
material.data_venda = converter_data(request.form.get("data_venda"))
db.commit()
flash('Material atualizado com sucesso!', 'success')
return redirect(url_for('material.listar'))
except Exception as e:
db.rollback()
flash('Erro ao atualizar material.', 'danger')
print(f"Erro ao atualizar material: {e}")
militantes = db.query(Militante).order_by(Militante.nome).all()
tipos_material = db.query(TipoMaterial).order_by(TipoMaterial.descricao).all()
return render_template("editar_material.html", material=material, militantes=militantes, tipos_material=tipos_material)
finally:
db.close()
@material_bp.route("/materiais/excluir/<int:id>", methods=["POST"])
@require_login
def excluir(id):
"""Exclui um material vendido"""
db = get_db_connection()
try:
material = db.query(MaterialVendido).get(id)
if not material:
flash('Material não encontrado.', 'danger')
return redirect(url_for('material.listar'))
db.delete(material)
db.commit()
flash('Material excluído com sucesso!', 'success')
except Exception as e:
db.rollback()
flash('Erro ao excluir material.', 'danger')
print(f"Erro ao excluir material: {e}")
finally:
db.close()
return redirect(url_for('material.listar'))
@material_bp.route("/tipos-materiais")
@require_login
def listar_tipos():
"""Lista todos os tipos de materiais com controle de permissões no nível de dados"""
db = get_db_connection()
try:
# SEMPRE renderizar o template, mas filtrar os dados baseado nas permissões
tipos_materiais = []
# Verificar permissões para filtrar dados
if current_user.is_admin:
# Admin vê todos
tipos_materiais = db.query(TipoMaterial).order_by(TipoMaterial.descricao).all()
elif hasattr(current_user, 'has_permission'):
# Outros usuários veem baseado nas suas permissões
# Por enquanto, mostrar todos - pode ser refinado depois
tipos_materiais = db.query(TipoMaterial).order_by(TipoMaterial.descricao).all()
# SEMPRE renderizar o template, independente das permissões
return render_template('listar_tipos_materiais.html', tipos_materiais=tipos_materiais)
except Exception as e:
print(f"Erro no controller de tipos de materiais: {e}")
# Em caso de erro, renderizar com dados vazios
return render_template('listar_tipos_materiais.html', tipos_materiais=[])
finally:
db.close()
@material_bp.route("/tipos-materiais/novo", methods=["GET", "POST"])
@require_login
def novo_tipo():
"""Cria um novo tipo de material"""
if request.method == "POST":
try:
descricao = request.form.get("descricao")
db = get_db_connection()
tipo = TipoMaterial(descricao=descricao)
db.add(tipo)
db.commit()
flash('Tipo de material cadastrado com sucesso!', 'success')
return redirect(url_for('material.listar_tipos'))
except Exception as e:
db.rollback()
flash('Erro ao cadastrar tipo de material', 'danger')
return redirect(url_for('material.novo_tipo'))
finally:
db.close()
return render_template("novo_tipo_material.html")
@material_bp.route('/tipos-materiais/editar/<int:id>', methods=['GET', 'POST'])
@require_login
def editar_tipo(id):
"""Edita um tipo de material"""
db = get_db_connection()
try:
tipo = db.query(TipoMaterial).get(id)
if not tipo:
flash('Tipo de material não encontrado.', 'danger')
return redirect(url_for('material.listar_tipos'))
if request.method == "POST":
try:
tipo.descricao = request.form.get("descricao")
db.commit()
flash('Tipo de material atualizado com sucesso!', 'success')
return redirect(url_for('material.listar_tipos'))
except Exception as e:
db.rollback()
flash('Erro ao atualizar tipo de material.', 'danger')
print(f"Erro ao atualizar tipo de material: {e}")
return render_template("editar_tipo_material.html", tipo=tipo)
finally:
db.close()
@material_bp.route("/tipos-materiais/excluir/<int:id>", methods=["POST"])
@require_login
def excluir_tipo(id):
"""Exclui um tipo de material"""
db = get_db_connection()
try:
tipo = db.query(TipoMaterial).get(id)
if not tipo:
flash('Tipo de material não encontrado.', 'danger')
return redirect(url_for('material.listar_tipos'))
db.delete(tipo)
db.commit()
flash('Tipo de material excluído com sucesso!', 'success')
except Exception as e:
db.rollback()
flash('Erro ao excluir tipo de material.', 'danger')
print(f"Erro ao excluir tipo de material: {e}")
finally:
db.close()
return redirect(url_for('material.listar_tipos'))

View File

@@ -1,295 +1,233 @@
from flask import Blueprint, request, render_template, redirect, url_for, flash, jsonify from flask import request, jsonify, flash
from functions.database import get_db_connection, Militante, EmailMilitante, Endereco, Celula, Setor, ComiteRegional
from functions.decorators import require_login
from functions.validations import validar_cpf
from functions.rbac import Permission
from utils.date_utils import validar_data, converter_data, calcular_idade
from datetime import datetime from datetime import datetime
from sqlalchemy.orm import joinedload from werkzeug.exceptions import NotFound
from flask_login import current_user
militante_bp = Blueprint('militante', __name__) from services.militante_service import MilitanteService
from models.entities.militante import Militante, EstadoMilitante
from models.entities.endereco import Endereco
from models.entities.email_militante import EmailMilitante
from utils.date_utils import validar_data, converter_data, validar_sequencia_datas, calcular_idade
@militante_bp.route("/militantes/criar", methods=["POST"]) class MilitanteController:
@require_login """Controlador para operações com militantes"""
def criar():
"""Cria um novo militante""" @staticmethod
try: def listar_militantes():
data = request.get_json() """Lista todos os militantes"""
return MilitanteService.listar_militantes()
# Validações básicas
if not data.get('nome') or not data.get('cpf'): @staticmethod
return jsonify({ def buscar_militante(militante_id):
'status': 'error', """Busca um militante pelo ID"""
'message': 'Nome e CPF são obrigatórios' militante = MilitanteService.buscar_militante(militante_id)
}), 400 if not militante:
raise NotFound(f"Militante com ID {militante_id} não encontrado")
if not validar_cpf(data['cpf']): return militante
@staticmethod
def criar_militante(form_data):
"""Cria um novo militante"""
# Validar CPF
from functions.validations import validar_cpf
cpf = form_data.get('cpf')
if not validar_cpf(cpf):
return jsonify({ return jsonify({
'status': 'error', 'status': 'error',
'message': 'CPF inválido' 'message': 'CPF inválido'
}), 400 }), 400
db = get_db_connection() # Verificar se já existe militante com este CPF
if MilitanteService.buscar_por_cpf(cpf):
# Verificar se CPF já existe
if db.query(Militante).filter_by(cpf=data['cpf']).first():
return jsonify({ return jsonify({
'status': 'error', 'status': 'error',
'message': 'CPF já cadastrado' 'message': 'CPF já cadastrado'
}), 400 }), 400
# Criar endereço se fornecido try:
endereco_id = None # Criar endereço
if data.get('endereco'): endereco = Endereco(
endereco = Endereco(**data['endereco']) cep=form_data.get('cep'),
db.add(endereco) estado=form_data.get('estado'),
db.flush() cidade=form_data.get('cidade'),
endereco_id = endereco.id bairro=form_data.get('bairro'),
rua=form_data.get('logradouro'),
# Criar militante numero=form_data.get('numero'),
militante = Militante( complemento=form_data.get('complemento')
nome=data['nome'],
cpf=data['cpf'],
titulo_eleitoral=data.get('titulo_eleitoral'),
data_nascimento=converter_data(data.get('data_nascimento')) if data.get('data_nascimento') else None,
data_entrada_oci=converter_data(data.get('data_entrada_oci')) if data.get('data_entrada_oci') else None,
data_efetivacao_oci=converter_data(data.get('data_efetivacao_oci')) if data.get('data_efetivacao_oci') else None,
telefone1=data.get('telefone1'),
telefone2=data.get('telefone2'),
profissao=data.get('profissao'),
regime_trabalho=data.get('regime_trabalho'),
empresa=data.get('empresa'),
contratante=data.get('contratante'),
instituicao_ensino=data.get('instituicao_ensino'),
tipo_instituicao=data.get('tipo_instituicao'),
sindicato=data.get('sindicato'),
cargo_sindical=data.get('cargo_sindical'),
dirigente_sindical=data.get('dirigente_sindical', False),
central_sindical=data.get('central_sindical'),
endereco_id=endereco_id,
celula_id=data.get('celula_id'),
registrado_por=current_user.id
)
db.add(militante)
db.flush()
# Criar email se fornecido
if data.get('email'):
email = EmailMilitante(
militante_id=militante.id,
endereco_email=data['email']
) )
db.add(email)
# Salvar endereço para obter ID
db.commit() endereco_id = MilitanteService.salvar_endereco(endereco)
return jsonify({ # Processar datas
'status': 'success', data_nascimento = datetime.strptime(form_data.get('data_nascimento'), '%Y-%m-%d') if form_data.get('data_nascimento') else None
'message': 'Militante criado com sucesso', data_entrada_oci = datetime.strptime(form_data.get('data_entrada_oci'), '%Y-%m-%d') if form_data.get('data_entrada_oci') else None
'militante_id': militante.id data_efetivacao_oci = datetime.strptime(form_data.get('data_efetivacao_oci'), '%Y-%m-%d') if form_data.get('data_efetivacao_oci') else None
})
# Criar militante
except Exception as e: militante = Militante(
db.rollback() # Dados Básicos
return jsonify({ nome=form_data.get('nome'),
'status': 'error', cpf=cpf,
'message': f'Erro ao criar militante: {str(e)}' titulo_eleitoral=form_data.get('titulo_eleitoral'),
}), 500 data_nascimento=data_nascimento,
finally: data_entrada_oci=data_entrada_oci,
db.close() data_efetivacao_oci=data_efetivacao_oci,
@militante_bp.route("/militantes") # Contato
@require_login telefone1=form_data.get('telefone1'),
def listar(): telefone2=form_data.get('telefone2'),
"""Lista todos os militantes com controle de permissões no nível de dados""" endereco_id=endereco_id,
db = get_db_connection()
try: # Profissional
# SEMPRE renderizar o template, mas filtrar os dados baseado nas permissões profissao=form_data.get('profissao'),
militantes = [] regime_trabalho=form_data.get('regime_trabalho'),
empresa=form_data.get('empresa'),
# Verificar permissões para filtrar dados contratante=form_data.get('contratante'),
if current_user.is_admin:
# Admin vê todos # Acadêmico
militantes = db.query(Militante).options( instituicao_ensino=form_data.get('instituicao_ensino'),
joinedload(Militante.emails), tipo_instituicao=form_data.get('tipo_instituicao'),
joinedload(Militante.endereco),
joinedload(Militante.celula) # Sindical
).order_by(Militante.nome).all() sindicato=form_data.get('sindicato'),
elif hasattr(current_user, 'has_permission'): cargo_sindical=form_data.get('cargo_sindical'),
if current_user.has_permission(Permission.VIEW_CC_REPORTS): central_sindical=form_data.get('central_sindical'),
# CC vê todos dirigente_sindical=form_data.get('dirigente_sindical') == 'on',
militantes = db.query(Militante).options(
joinedload(Militante.emails), # Organização
joinedload(Militante.endereco), estado=EstadoMilitante(form_data.get('estado', 'ATIVO')),
joinedload(Militante.celula) celula_id=form_data.get('celula_id', type=int),
).order_by(Militante.nome).all() responsabilidades=form_data.get('responsabilidades', type=int, default=0),
elif current_user.has_permission(Permission.VIEW_CR_REPORTS):
# CR vê do seu CR # Por padrão, todo novo militante é aspirante
if hasattr(current_user, 'cr_id') and current_user.cr_id: aspirante=True,
militantes = db.query(Militante).join(Celula).join(Setor).filter( data_inicio_aspirante=datetime.now()
Setor.cr_id == current_user.cr_id )
).options(
joinedload(Militante.emails), # Salvar militante para obter ID
joinedload(Militante.endereco), militante_id = MilitanteService.salvar_militante(militante)
joinedload(Militante.celula)
).order_by(Militante.nome).all() # Adicionar email principal se fornecido
elif current_user.has_permission(Permission.VIEW_SECTOR_REPORTS): email = form_data.get('email')
# Setor vê do seu setor if email:
if hasattr(current_user, 'setor_id') and current_user.setor_id: email_militante = EmailMilitante(
militantes = db.query(Militante).join(Celula).filter( endereco_email=email,
Celula.setor_id == current_user.setor_id militante_id=militante_id
).options( )
joinedload(Militante.emails), MilitanteService.salvar_email_militante(email_militante)
joinedload(Militante.endereco),
joinedload(Militante.celula) return jsonify({
).order_by(Militante.nome).all() 'status': 'success',
elif current_user.has_permission(Permission.VIEW_CELL_DATA): 'message': 'Militante criado com sucesso!',
# Célula vê da sua célula 'id': militante_id
if hasattr(current_user, 'celula_id') and current_user.celula_id: })
militantes = db.query(Militante).filter(
Militante.celula_id == current_user.celula_id except Exception as e:
).options(
joinedload(Militante.emails),
joinedload(Militante.endereco),
joinedload(Militante.celula)
).order_by(Militante.nome).all()
# Buscar dados auxiliares para o template
celulas = db.query(Celula).all()
setores = db.query(Setor).all()
# SEMPRE renderizar o template, independente das permissões
# O controle é feito no nível dos dados, não do template
return render_template('listar_militantes.html',
militantes=militantes,
Militante=Militante,
celulas=celulas,
setores=setores)
except Exception as e:
print(f"Erro no controller de militantes: {e}")
# Em caso de erro, renderizar com dados vazios
return render_template('listar_militantes.html',
militantes=[],
Militante=Militante,
celulas=[],
setores=[])
finally:
db.close()
@militante_bp.route("/militantes/excluir/<int:id>", methods=["POST"])
@require_login
def excluir(id):
"""Exclui um militante"""
db = get_db_connection()
try:
militante = db.query(Militante).get(id)
if not militante:
flash('Militante não encontrado.', 'danger')
return redirect(url_for('militante.listar'))
# Verificar permissões
if not current_user.has_permission('gerenciar_militantes'):
flash('Você não tem permissão para excluir militantes.', 'danger')
return redirect(url_for('militante.listar'))
db.delete(militante)
db.commit()
flash('Militante excluído com sucesso!', 'success')
except Exception as e:
db.rollback()
flash('Erro ao excluir militante.', 'danger')
print(f"Erro ao excluir militante: {e}")
finally:
db.close()
return redirect(url_for('militante.listar'))
@militante_bp.route('/militantes/editar/<int:militante_id>', methods=['POST'])
@require_login
def editar(militante_id):
"""Edita um militante existente"""
try:
data = request.get_json()
db = get_db_connection()
militante = db.query(Militante).get(militante_id)
if not militante:
return jsonify({ return jsonify({
'status': 'error', 'status': 'error',
'message': 'Militante não encontrado' 'message': f'Erro ao criar militante: {str(e)}'
}), 404 }), 500
# Atualizar dados básicos @staticmethod
militante.nome = data.get('nome', militante.nome) def atualizar_militante(militante_id, form_data):
militante.cpf = data.get('cpf', militante.cpf) """Atualiza um militante existente"""
militante.titulo_eleitoral = data.get('titulo_eleitoral', militante.titulo_eleitoral) try:
militante.telefone1 = data.get('telefone1', militante.telefone1) militante = MilitanteService.buscar_militante(militante_id)
militante.telefone2 = data.get('telefone2', militante.telefone2) if not militante:
militante.profissao = data.get('profissao', militante.profissao) return jsonify({
militante.regime_trabalho = data.get('regime_trabalho', militante.regime_trabalho) 'status': 'error',
militante.empresa = data.get('empresa', militante.empresa) 'message': 'Militante não encontrado'
militante.contratante = data.get('contratante', militante.contratante) }), 404
militante.instituicao_ensino = data.get('instituicao_ensino', militante.instituicao_ensino)
militante.tipo_instituicao = data.get('tipo_instituicao', militante.tipo_instituicao) # Obter dados do formulário
militante.sindicato = data.get('sindicato', militante.sindicato) nome = form_data.get('nome')
militante.cargo_sindical = data.get('cargo_sindical', militante.cargo_sindical) cpf = form_data.get('cpf')
militante.dirigente_sindical = data.get('dirigente_sindical', militante.dirigente_sindical) titulo_eleitoral = form_data.get('titulo_eleitoral')
militante.central_sindical = data.get('central_sindical', militante.central_sindical) data_nascimento = form_data.get('data_nascimento')
data_entrada_oci = form_data.get('data_entrada_oci')
# Atualizar datas data_efetivacao_oci = form_data.get('data_efetivacao_oci')
if data.get('data_nascimento'): telefone1 = form_data.get('telefone1')
militante.data_nascimento = converter_data(data['data_nascimento']) telefone2 = form_data.get('telefone2')
if data.get('data_entrada_oci'): email = form_data.get('email')
militante.data_entrada_oci = converter_data(data['data_entrada_oci'])
if data.get('data_efetivacao_oci'): # Validar e converter datas
militante.data_efetivacao_oci = converter_data(data['data_efetivacao_oci']) try:
data_nascimento = converter_data(data_nascimento) if data_nascimento else None
# Atualizar endereço data_entrada_oci = converter_data(data_entrada_oci) if data_entrada_oci else None
if data.get('endereco') and militante.endereco: data_efetivacao_oci = converter_data(data_efetivacao_oci) if data_efetivacao_oci else None
endereco = militante.endereco
endereco.cep = data['endereco'].get('cep', endereco.cep) # Validar sequência lógica das datas
endereco.estado = data['endereco'].get('estado', endereco.estado) validar_sequencia_datas(
endereco.cidade = data['endereco'].get('cidade', endereco.cidade) data_nascimento=data_nascimento,
endereco.bairro = data['endereco'].get('bairro', endereco.bairro) data_entrada=data_entrada_oci,
endereco.rua = data['endereco'].get('rua', endereco.rua) data_efetivacao=data_efetivacao_oci
endereco.numero = data['endereco'].get('numero', endereco.numero) )
endereco.complemento = data['endereco'].get('complemento', endereco.complemento)
except ValueError as e:
# Atualizar email return jsonify({
if data.get('email') and militante.emails: 'status': 'error',
militante.emails[0].endereco_email = data['email'] 'message': str(e)
}), 400
db.commit()
# Atualizar dados básicos
return jsonify({ if nome: militante.nome = nome
'status': 'success', if cpf: militante.cpf = cpf
'message': 'Militante atualizado com sucesso' if titulo_eleitoral: militante.titulo_eleitoral = titulo_eleitoral
}) militante.data_nascimento = data_nascimento
militante.data_entrada_oci = data_entrada_oci
except Exception as e: militante.data_efetivacao_oci = data_efetivacao_oci
db.rollback() militante.telefone1 = telefone1
return jsonify({ militante.telefone2 = telefone2
'status': 'error',
'message': f'Erro ao atualizar militante: {str(e)}' # Calcular idade
}), 500 if data_nascimento:
finally: militante.idade = calcular_idade(data_nascimento)
db.close()
# Atualizar ou criar email
@militante_bp.route("/militantes/dados/<int:militante_id>") if email:
@require_login MilitanteService.atualizar_email_militante(militante_id, email)
def buscar_dados(militante_id):
"""Busca os dados de um militante específico""" # Salvar alterações
db = get_db_connection() MilitanteService.salvar_militante(militante)
try:
militante = db.query(Militante).options( return jsonify({
joinedload(Militante.emails), 'status': 'success',
joinedload(Militante.endereco) 'message': 'Militante atualizado com sucesso',
).get(militante_id) 'data': {
'nome': militante.nome,
'cpf': militante.cpf,
'idade': militante.idade if hasattr(militante, 'idade') else None,
'emails': [e.endereco_email for e in militante.emails],
'telefone1': militante.telefone1,
'celula_id': str(militante.celula_id) if militante.celula_id else None,
'responsabilidades_valor': militante.responsabilidades
}
})
except Exception as e:
return jsonify({
'status': 'error',
'message': f'Erro ao atualizar militante: {str(e)}'
}), 500
@staticmethod
def excluir_militante(militante_id):
"""Exclui um militante"""
try:
if MilitanteService.excluir_militante(militante_id):
flash('Militante excluído com sucesso!', 'success')
return True
else:
flash('Militante não encontrado', 'danger')
return False
except Exception as e:
flash(f'Erro ao excluir militante: {str(e)}', 'danger')
return False
@staticmethod
def buscar_dados_militante(militante_id):
"""Busca os dados de um militante específico"""
militante = MilitanteService.buscar_militante(militante_id)
if not militante: if not militante:
return jsonify({ return jsonify({
'status': 'error', 'status': 'error',
@@ -306,62 +244,27 @@ def buscar_dados(militante_id):
print(f"Erro ao formatar data: {str(e)}, valor: {data}") print(f"Erro ao formatar data: {str(e)}, valor: {data}")
return None return None
# Preparar dados para retorno # Formatar datas com validação
dados = { data_nascimento = formatar_data_segura(militante.data_nascimento)
data_entrada_oci = formatar_data_segura(militante.data_entrada_oci)
data_efetivacao_oci = formatar_data_segura(militante.data_efetivacao_oci)
return jsonify({
'status': 'success',
'id': militante.id, 'id': militante.id,
'nome': militante.nome, 'nome': militante.nome,
'cpf': militante.cpf, 'cpf': militante.cpf,
'titulo_eleitoral': militante.titulo_eleitoral, 'titulo_eleitoral': militante.titulo_eleitoral,
'data_nascimento': formatar_data_segura(militante.data_nascimento), 'data_nascimento': data_nascimento,
'data_entrada_oci': formatar_data_segura(militante.data_entrada_oci), 'data_entrada_oci': data_entrada_oci,
'data_efetivacao_oci': formatar_data_segura(militante.data_efetivacao_oci), 'data_efetivacao_oci': data_efetivacao_oci,
'emails': [email.endereco_email for email in militante.emails] if militante.emails else [],
'telefone1': militante.telefone1, 'telefone1': militante.telefone1,
'telefone2': militante.telefone2, 'telefone2': militante.telefone2,
'profissao': militante.profissao, 'celula_id': militante.celula_id,
'regime_trabalho': militante.regime_trabalho, 'responsabilidades_valor': militante.responsabilidades,
'empresa': militante.empresa,
'contratante': militante.contratante,
'instituicao_ensino': militante.instituicao_ensino,
'tipo_instituicao': militante.tipo_instituicao,
'sindicato': militante.sindicato, 'sindicato': militante.sindicato,
'cargo_sindical': militante.cargo_sindical, 'cargo_sindical': militante.cargo_sindical,
'dirigente_sindical': militante.dirigente_sindical,
'central_sindical': militante.central_sindical, 'central_sindical': militante.central_sindical,
'responsabilidades': militante.responsabilidades, 'dirigente_sindical': militante.dirigente_sindical
'estado': militante.estado.value if militante.estado else None, })
'celula_id': militante.celula_id,
'email': militante.emails[0].endereco_email if militante.emails else None,
'endereco': {
'cep': militante.endereco.cep if militante.endereco else None,
'estado': militante.endereco.estado if militante.endereco else None,
'cidade': militante.endereco.cidade if militante.endereco else None,
'bairro': militante.endereco.bairro if militante.endereco else None,
'rua': militante.endereco.rua if militante.endereco else None,
'numero': militante.endereco.numero if militante.endereco else None,
'complemento': militante.endereco.complemento if militante.endereco else None
} if militante.endereco else None
}
return jsonify({
'status': 'success',
'data': dados
})
except Exception as e:
return jsonify({
'status': 'error',
'message': f'Erro ao buscar dados: {str(e)}'
}), 500
finally:
db.close()
@militante_bp.route("/api/setores/<int:cr_id>")
@require_login
def get_setores(cr_id):
"""Retorna setores de um CR específico"""
db = get_db_connection()
try:
setores = db.query(Setor).filter_by(cr_id=cr_id).all()
return jsonify([{'id': s.id, 'nome': s.nome} for s in setores])
finally:
db.close()

View File

@@ -1,209 +0,0 @@
from flask import Blueprint, request, render_template, redirect, url_for, flash, jsonify
from functions.database import get_db_connection, Pagamento, Militante, TipoPagamento
from functions.decorators import require_login
from utils.date_utils import validar_data, converter_data
from datetime import datetime
from flask_login import current_user
pagamento_bp = Blueprint('pagamento', __name__)
@pagamento_bp.route("/pagamentos/novo", methods=["GET", "POST"])
@require_login
def novo():
"""Cria um novo pagamento"""
if request.method == "POST":
try:
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'))
db = get_db_connection()
pagamento = Pagamento(
militante_id=militante_id,
tipo_pagamento_id=tipo_pagamento_id,
valor=valor,
data_pagamento=data_pagamento
)
db.add(pagamento)
db.commit()
flash('Pagamento cadastrado com sucesso!', 'success')
return redirect(url_for('pagamento.listar'))
except Exception as e:
db.rollback()
flash('Erro ao cadastrar pagamento', 'danger')
return redirect(url_for('pagamento.novo'))
finally:
db.close()
# GET - Renderizar formulário
db = get_db_connection()
try:
militantes = db.query(Militante).order_by(Militante.nome).all()
tipos_pagamento = db.query(TipoPagamento).order_by(TipoPagamento.descricao).all()
return render_template("novo_pagamento.html", militantes=militantes, tipos_pagamento=tipos_pagamento)
finally:
db.close()
@pagamento_bp.route("/pagamentos")
@require_login
def listar():
"""Lista todos os pagamentos com controle de permissões no nível de dados"""
db = get_db_connection()
try:
# SEMPRE renderizar o template, mas filtrar os dados baseado nas permissões
pagamentos = []
# Verificar permissões para filtrar dados
if current_user.is_admin:
# Admin vê todos
pagamentos = db.query(Pagamento).join(Militante).order_by(Pagamento.data_pagamento.desc()).all()
elif hasattr(current_user, 'has_permission'):
# Outros usuários veem baseado nas suas permissões
# Por enquanto, deixar vazio até implementar a lógica específica
pagamentos = []
# Buscar dados auxiliares para o template
militantes = db.query(Militante).order_by(Militante.nome).all()
# SEMPRE renderizar o template, independente das permissões
return render_template("listar_pagamentos.html",
pagamentos=pagamentos,
militantes=militantes)
except Exception as e:
print(f"Erro no controller de pagamentos: {e}")
# Em caso de erro, renderizar com dados vazios
return render_template("listar_pagamentos.html",
pagamentos=[],
militantes=[])
finally:
db.close()
@pagamento_bp.route("/pagamentos/adicionar", methods=["POST"])
@require_login
def adicionar():
"""Adiciona um novo pagamento"""
if request.method == "POST":
try:
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"))
db = get_db_connection()
pagamento = Pagamento(
militante_id=militante_id,
tipo_pagamento=tipo_pagamento,
valor=valor,
data_pagamento=data_pagamento
)
db.add(pagamento)
db.commit()
flash('Pagamento adicionado com sucesso!', 'success')
except Exception as e:
db.rollback()
flash(f'Erro ao adicionar pagamento: {str(e)}', 'danger')
finally:
db.close()
return redirect(url_for('pagamento.listar'))
@pagamento_bp.route('/celulas/<int:celula_id>/pagamentos')
@require_login
def list_pagamentos_celula(celula_id):
"""Lista pagamentos de uma célula específica"""
db = get_db_connection()
try:
pagamentos = db.query(Pagamento).filter_by(celula_id=celula_id).all()
return render_template('pagamentos/list.html', pagamentos=pagamentos)
finally:
db.close()
@pagamento_bp.route('/setores/<int:setor_id>/pagamentos')
@require_login
def list_pagamentos_setor(setor_id):
"""Lista pagamentos de um setor específico"""
db = get_db_connection()
try:
pagamentos = db.query(Pagamento).join(Usuario).filter(Usuario.setor_id == setor_id).all()
return render_template('pagamentos/list.html', pagamentos=pagamentos)
finally:
db.close()
@pagamento_bp.route('/crs/<int:cr_id>/pagamentos')
@require_login
def list_pagamentos_cr(cr_id):
"""Lista pagamentos de um CR específico"""
db = get_db_connection()
try:
pagamentos = db.query(Pagamento).join(Usuario).filter(Usuario.cr_id == cr_id).all()
return render_template('pagamentos/list.html', pagamentos=pagamentos)
finally:
db.close()
@pagamento_bp.route('/celulas/<int:celula_id>/pagamentos/novo', methods=['GET', 'POST'])
@require_login
def novo_pagamento_celula(celula_id):
"""Cria novo pagamento para uma célula"""
if request.method == 'POST':
db = get_db_connection()
try:
pagamento = Pagamento(
valor=request.form['valor'],
data=request.form['data'],
militante_id=request.form['militante_id'],
celula_id=celula_id
)
db.add(pagamento)
db.commit()
flash('Pagamento registrado com sucesso!', 'success')
return redirect(url_for('pagamento.list_pagamentos_celula', celula_id=celula_id))
finally:
db.close()
return render_template('pagamentos/form.html')
@pagamento_bp.route('/setores/<int:setor_id>/pagamentos/novo', methods=['GET', 'POST'])
@require_login
def novo_pagamento_setor(setor_id):
"""Cria novo pagamento para um setor"""
if request.method == 'POST':
db = get_db_connection()
try:
pagamento = Pagamento(
valor=request.form['valor'],
data=request.form['data'],
militante_id=request.form['militante_id'],
setor_id=setor_id
)
db.add(pagamento)
db.commit()
flash('Pagamento registrado com sucesso!', 'success')
return redirect(url_for('pagamento.list_pagamentos_setor', setor_id=setor_id))
finally:
db.close()
return render_template('pagamentos/form.html')
@pagamento_bp.route('/crs/<int:cr_id>/pagamentos/novo', methods=['GET', 'POST'])
@require_login
def novo_pagamento_cr(cr_id):
"""Cria novo pagamento para um CR"""
if request.method == 'POST':
db = get_db_connection()
try:
pagamento = Pagamento(
valor=request.form['valor'],
data=request.form['data'],
militante_id=request.form['militante_id'],
cr_id=cr_id
)
db.add(pagamento)
db.commit()
flash('Pagamento registrado com sucesso!', 'success')
return redirect(url_for('pagamento.list_pagamentos_cr', cr_id=cr_id))
finally:
db.close()
return render_template('pagamentos/form.html')

View File

@@ -1,71 +1,124 @@
from flask import Blueprint, request, render_template, redirect, url_for, flash, jsonify from flask import request, jsonify, flash, session
from functions.database import get_db_connection, Usuario, Role, Setor
from functions.decorators import require_login
from flask_login import current_user from flask_login import current_user
from datetime import datetime
import secrets
import pyotp import pyotp
usuario_bp = Blueprint('usuario', __name__) from models.entities.usuario import Usuario
from services.usuario_service import UsuarioService
@usuario_bp.route("/usuarios/novo", methods=["GET", "POST"]) from services.database_service import DatabaseService
@require_login
def novo():
"""Cria um novo usuário"""
if request.method == "POST":
username = request.form.get("username")
password = request.form.get("password")
email = request.form.get("email")
role_id = request.form.get("role_id")
setor_id = request.form.get("setor_id")
class UsuarioController:
"""Controlador para operações com usuários"""
@staticmethod
def listar_usuarios():
"""Lista todos os usuários do sistema"""
return UsuarioService.listar_usuarios()
@staticmethod
def buscar_usuario(user_id):
"""Busca um usuário pelo ID"""
return UsuarioService.buscar_usuario(user_id)
@staticmethod
def criar_usuario(data):
"""Cria um novo usuário"""
# Verificar campos obrigatórios
required_fields = ['username', 'password', 'email']
for field in required_fields:
if field not in data:
flash(f'Campo {field} é obrigatório.', 'danger')
return False
# Verificar se usuário já existe # Verificar se usuário já existe
db = get_db_connection() if UsuarioService.buscar_por_username(data['username']):
flash('Nome de usuário já existe.', 'danger')
return False
try: try:
if db.query(Usuario).filter_by(username=username).first(): # Criar usuário
flash('Nome de usuário já existe.', 'danger') usuario = Usuario(
return render_template("novo_usuario.html") username=data['username'],
email=data['email'],
novo_usuario = Usuario( nome=data.get('nome'),
username=username, is_admin=data.get('is_admin', False)
email=email,
role_id=role_id,
setor_id=setor_id
) )
novo_usuario.set_password(password)
novo_usuario.otp_secret = pyotp.random_base32() # Definir senha
usuario.set_password(data['password'])
db.add(novo_usuario)
db.commit() # Gerar OTP secret
flash('Usuário cadastrado com sucesso!', 'success') usuario.otp_secret = pyotp.random_base32()
return redirect(url_for('usuario.listar'))
# Definir outros campos
if 'role_id' in data:
usuario.role_id = data['role_id']
if 'setor_id' in data:
usuario.setor_id = data['setor_id']
# Salvar no banco
result = UsuarioService.salvar_usuario(usuario)
if result:
flash('Usuário cadastrado com sucesso!', 'success')
return True
else:
flash('Erro ao cadastrar usuário.', 'danger')
return False
except Exception as e: except Exception as e:
db.rollback() flash(f'Erro ao cadastrar usuário: {str(e)}', 'danger')
print(f"Erro ao cadastrar usuário: {e}") return False
flash('Erro ao cadastrar usuário', 'danger')
return render_template("novo_usuario.html") @staticmethod
finally: def atualizar_usuario(user_id, data):
db.close() """Atualiza um usuário existente"""
usuario = UsuarioService.buscar_usuario(user_id)
if not usuario:
flash('Usuário não encontrado.', 'danger')
return False
# Atualizar campos
if 'email' in data:
usuario.email = data['email']
if 'nome' in data:
usuario.nome = data['nome']
if 'role_id' in data:
usuario.role_id = data['role_id']
if 'setor_id' in data:
usuario.setor_id = data['setor_id']
if 'is_admin' in data:
usuario.is_admin = data['is_admin']
if 'password' in data and data['password']:
usuario.set_password(data['password'])
# Salvar alterações
result = UsuarioService.salvar_usuario(usuario)
if result:
flash('Usuário atualizado com sucesso!', 'success')
return True
else:
flash('Erro ao atualizar usuário.', 'danger')
return False
@staticmethod
def toggle_status(user_id):
"""Ativa/desativa um usuário"""
if not current_user.is_admin:
return jsonify({
'success': False,
'error': 'Você não tem permissão para alterar o status de usuários.'
}), 403
db = get_db_connection() usuario = UsuarioService.buscar_usuario(user_id)
try:
roles = db.query(Role).order_by(Role.nome).all()
setores = db.query(Setor).order_by(Setor.nome).all()
return render_template("novo_usuario.html", roles=roles, setores=setores)
finally:
db.close()
@usuario_bp.route('/usuarios/<int:user_id>/toggle_status', methods=['POST'])
@require_login
def toggle_status(user_id):
"""Ativa/desativa um usuário"""
if not current_user.is_admin:
return jsonify({
'success': False,
'error': 'Você não tem permissão para alterar o status de usuários.'
}), 403
db = get_db_connection()
try:
usuario = db.query(Usuario).get(user_id)
if not usuario: if not usuario:
return jsonify({ return jsonify({
'success': False, 'success': False,
@@ -73,112 +126,77 @@ def toggle_status(user_id):
}), 404 }), 404
usuario.ativo = not usuario.ativo usuario.ativo = not usuario.ativo
db.commit() if UsuarioService.salvar_usuario(usuario):
return jsonify({
'success': True,
'message': f'Usuário {"ativado" if usuario.ativo else "desativado"} com sucesso!'
})
except Exception as e:
db.rollback()
return jsonify({
'success': False,
'error': str(e)
}), 500
finally:
db.close()
@usuario_bp.route('/usuarios/<int:user_id>/alterar_nivel', methods=['POST'])
@require_login
def alterar_nivel(user_id):
"""Altera o nível de acesso de um usuário"""
if not current_user.is_admin:
return jsonify({
'success': False,
'error': 'Você não tem permissão para alterar níveis de usuários.'
}), 403
novo_nivel = request.form.get('novo_nivel')
if not novo_nivel:
return jsonify({
'success': False,
'error': 'Novo nível não especificado.'
}), 400
db = get_db_connection()
try:
usuario = db.query(Usuario).get(user_id)
if not usuario:
return jsonify({ return jsonify({
'success': False, 'success': True,
'error': 'Usuário não encontrado.' 'message': f'Usuário {\'ativado\' if usuario.ativo else \'desativado\'}'
}), 404 })
# Buscar role pelo nível
role = db.query(Role).filter_by(nivel=int(novo_nivel)).first()
if not role:
return jsonify({
'success': False,
'error': 'Nível de acesso inválido.'
}), 400
# Limpar roles existentes e adicionar nova
usuario.roles.clear()
usuario.roles.append(role)
db.commit()
return jsonify({
'success': True,
'message': f'Nível de acesso alterado para {role.nome} com sucesso!'
})
except Exception as e:
db.rollback()
return jsonify({
'success': False,
'error': str(e)
}), 500
finally:
db.close()
@usuario_bp.route('/usuarios/<int:user_id>/toggle_quadro_orientador', methods=['POST'])
@require_login
def toggle_quadro_orientador(user_id):
"""Ativa/desativa quadro orientador para um usuário"""
if not current_user.is_admin:
return jsonify({
'success': False,
'error': 'Você não tem permissão para alterar responsabilidades de usuários.'
}), 403
db = get_db_connection()
try:
usuario = db.query(Usuario).get(user_id)
if not usuario:
return jsonify({
'success': False,
'error': 'Usuário não encontrado.'
}), 404
# Toggle quadro orientador
if usuario.quadro_orientador:
usuario.quadro_orientador = False
message = 'Quadro Orientador desativado com sucesso!'
else: else:
usuario.quadro_orientador = True return jsonify({
message = 'Quadro Orientador ativado com sucesso!' 'success': False,
'error': 'Erro ao salvar alterações.'
db.commit() }), 500
@staticmethod
def reset_password(user_id):
"""Reseta a senha de um usuário"""
usuario = UsuarioService.buscar_usuario(user_id)
if not usuario:
flash('Usuário não encontrado.', 'danger')
return False, None
return jsonify({ # Gerar nova senha
'success': True, new_password = secrets.token_urlsafe(8)
'message': message, usuario.set_password(new_password)
'quadro_orientador': usuario.quadro_orientador
}) # Salvar alterações
except Exception as e: if UsuarioService.salvar_usuario(usuario):
db.rollback() return True, new_password
return jsonify({ else:
'success': False, flash('Erro ao resetar senha.', 'danger')
'error': str(e) return False, None
}), 500
finally: @staticmethod
db.close() def reset_otp(user_id):
"""Reseta o OTP de um usuário"""
usuario = UsuarioService.buscar_usuario(user_id)
if not usuario:
flash('Usuário não encontrado.', 'danger')
return False
# Gerar novo OTP secret
usuario.otp_secret = pyotp.random_base32()
# Salvar alterações
if UsuarioService.salvar_usuario(usuario):
flash(f'OTP resetado com sucesso para {usuario.email}.', 'success')
return True
else:
flash('Erro ao resetar OTP.', 'danger')
return False
@staticmethod
def check_session():
"""Verifica status da sessão"""
if 'user_id' not in session:
return {'status': 'expired'}
if 'last_activity' in session:
last_activity = datetime.fromtimestamp(session['last_activity'])
now = datetime.now()
if now - last_activity > timedelta(hours=2):
# Registrar o logout por timeout
try:
user = UsuarioService.buscar_usuario(session.get('user_id'))
if user:
user.ultimo_logout = datetime.now()
user.motivo_logout = "Timeout de sessão"
UsuarioService.salvar_usuario(user)
except Exception as e:
print(f"Erro ao registrar logout por timeout: {e}")
session.clear()
return {'status': 'expired'}
return {'status': 'active'}

View File

@@ -1,5 +0,0 @@
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.
#HttpOnly_localhost FALSE / FALSE 0 session .eJw9jjsOgzAUBO_iOoWfP9jmMuh91goFFBCqKHePJZR0u5op5u2WfuB8uvl1XHi4ZTU3OyTNpimqxySBhYIVTqa5VSqxdVWLnRoIOXVvk3ijGFS5gnrTVOEDvPUIIkFEz8VIpNZUWKmVwAryYqTFwwJbVKmD-NzEuIkbIdeJ466hcddzYdvW_df5p3TvnTcM9XY-XwnBQXY.aGPhDw.BUcsxy5unEUB2pJjMnJy9ITNKXs

View File

@@ -12,14 +12,14 @@ def generate_qr_code(user):
user: Instância do modelo Usuario user: Instância do modelo Usuario
Returns: Returns:
tuple: (caminho do arquivo, URI do OTP) Path: Caminho do arquivo QR code gerado
""" """
# Tentar diferentes caminhos para salvar o QR code # Gerar QR Code apenas na raiz do projeto
qr_paths = [ qr_path = Path('admin_qr.png')
Path('/tmp/admin_qr.png'), # Diretório temporário do sistema
Path('admin_qr.png'), # Diretório atual # Remover arquivo antigo se existir
Path('/app/admin_qr.png') # Diretório da aplicação if qr_path.exists():
] os.remove(str(qr_path))
# Gerar e salvar QR Code # Gerar e salvar QR Code
qr = qrcode.QRCode(version=1, box_size=10, border=5) qr = qrcode.QRCode(version=1, box_size=10, border=5)
@@ -34,29 +34,11 @@ def generate_qr_code(user):
qr.add_data(otp_uri) qr.add_data(otp_uri)
qr.make(fit=True) qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white") img = qr.make_image(fill_color="black", back_color="white")
img.save(str(qr_path))
# Tentar salvar em diferentes locais print(f"\nQR Code gerado em: {os.path.abspath(qr_path)}")
qr_saved = False
saved_path = None
for qr_path in qr_paths: return qr_path, otp_uri
try:
# Tentar salvar o arquivo
img.save(str(qr_path))
print(f"QR Code salvo em: {qr_path}")
qr_saved = True
saved_path = qr_path
break
except Exception as e:
print(f"Não foi possível salvar o QR code em {qr_path}: {e}")
continue
if not qr_saved:
print("AVISO: Não foi possível salvar o QR code em nenhum local")
print("O QR code pode ser gerado manualmente usando o URI OTP")
saved_path = None
return saved_path, otp_uri
def create_admin_user(): def create_admin_user():
"""Cria ou atualiza o usuário admin""" """Cria ou atualiza o usuário admin"""
@@ -101,17 +83,16 @@ def create_admin_user():
db.add(admin) db.add(admin)
db.commit() db.commit()
# Gerar QR code # Gerar QR code apenas se solicitado ou se for novo usuário
qr_path, otp_uri = generate_qr_code(admin) if not os.path.exists('admin_qr.png'):
qr_path, otp_uri = generate_qr_code(admin)
if qr_path:
print("\n=== QR Code Gerado ===") print("\n=== QR Code Gerado ===")
print(f"QR Code salvo em: {qr_path}") print(f"QR Code salvo em: {qr_path}")
print(f"URI do OTP: {otp_uri}") print(f"URI do OTP: {otp_uri}")
else: else:
print("\n=== QR Code Não Pode Ser Salvo ===") print("\n=== QR Code Existente ===")
print("Use o URI OTP para configuração manual:") print("Usando QR Code existente em: admin_qr.png")
print(f"URI do OTP: {otp_uri}") qr_path = 'admin_qr.png'
# Mostrar informações # Mostrar informações
print("\n=== Informações do Admin ===") print("\n=== Informações do Admin ===")
@@ -132,8 +113,7 @@ def create_admin_user():
print(" (Google Authenticator, Microsoft Authenticator, etc)") print(" (Google Authenticator, Microsoft Authenticator, etc)")
print("2. Abra o aplicativo") print("2. Abra o aplicativo")
print("3. Selecione a opção para adicionar uma nova conta") print("3. Selecione a opção para adicionar uma nova conta")
if qr_path: print("4. Escaneie o QR Code salvo em:", qr_path)
print("4. Escaneie o QR Code salvo em:", qr_path)
print("\nOU configure manualmente:") print("\nOU configure manualmente:")
print(f"- Nome da conta: {admin.username}") print(f"- Nome da conta: {admin.username}")
print(f"- Segredo: {admin.otp_secret}") print(f"- Segredo: {admin.otp_secret}")

View File

@@ -1,50 +1,14 @@
version: '3.8' version: '3.8'
services: services:
# Redis Cache Service
redis:
image: redis:7-alpine
container_name: controles_redis
ports:
- "6379:6379"
volumes:
- redis_data:/data
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 30s
timeout: 10s
retries: 3
networks:
- controles_network
# Flask Application
app: app:
build: . build: .
container_name: controles_app
ports: ports:
- "5000:5000" - "5000:5000"
environment:
- FLASK_APP=app.py
- FLASK_ENV=production
- REDIS_URL=redis://redis:6379/0
- DATABASE_URL=sqlite:///app/database.db
- ADMIN_OTP_SECRET=JBSWY3DPEHPK3PXP
volumes: volumes:
- ./database.db:/app/database.db - .:/app
- ./admin_qr.png:/app/admin_qr.png - ~/.local/share/controles:/root/.local/share/controles
depends_on: environment:
redis: - FLASK_ENV=development
condition: service_healthy - FLASK_APP=app.py
restart: unless-stopped restart: unless-stopped
networks:
- controles_network
volumes:
redis_data:
driver: local
networks:
controles_network:
driver: bridge

View File

@@ -1,363 +1,165 @@
# Sistema de Controles OCI # Sistema de Controle OCI
Sistema de gerenciamento para a Organização Comunista Internacionalista (OCI) com controle de militantes, cotas, pagamentos e materiais.
## 🚀 Status Atual
**Sistema com Arquitetura de Permissões Corrigida**
- Aplicação Flask rodando com Docker
- Redis cache integrado e funcionando
- Banco de dados SQLite inicializado
- Usuário admin configurado com OTP
- 30 militantes de teste criados
- Estrutura organizacional completa
- **Sistema de permissões implementado no nível de dados**
- **Menus sempre visíveis, controle transparente**
## 🎯 Arquitetura de Permissões
O sistema implementa uma estratégia de controle de permissões no **nível de dados**, garantindo que:
- **Menus permanecem sempre visíveis** - Não há restrições na interface
- **Dados são filtrados por hierarquia** - Admin → CC → CR → Setor → Célula
- **Templates nunca quebram** - Sempre renderizam, mesmo com dados vazios
- **Tesoureiros têm poder adequado** - Podem fazer tudo que secretários fazem
### Diagrama da Arquitetura
```mermaid
graph TD
A[User Request] --> B[Controller Layer]
B --> C{Permission Check}
C -->|Admin| D[All Data]
C -->|CC| E[All Data]
C -->|CR| F[CR Data Only]
C -->|Setor| G[Setor Data Only]
C -->|Célula| H[Célula Data Only]
C -->|No Permission| I[Empty Data]
D --> J[Template Rendering]
E --> J
F --> J
G --> J
H --> J
I --> J
J --> K[Always Renders Successfully]
```
## 🏗️ Arquitetura
O sistema foi refatorado seguindo o padrão MVC (Model-View-Controller):
```
controles/
├── app.py # Ponto de entrada da aplicação
├── controllers/ # Controladores (lógica de rotas)
├── models/ # Modelos (operações de banco)
├── services/ # Serviços (lógica de negócio)
├── templates/ # Views (templates HTML)
├── static/ # Assets estáticos
└── functions/ # Funções utilitárias
```
## 🐳 Docker Setup
### Pré-requisitos
- Docker e Docker Compose instalados
- Porta 5000 disponível para a aplicação
- Porta 6379 disponível para Redis
### Inicialização Rápida
```bash
# Clonar o repositório
git clone <repository-url>
cd controles
# Iniciar o ambiente completo
make dev-up
# Verificar status
docker-compose ps
# Ver logs
make docker-logs
```
### Comandos Úteis
```bash
# Iniciar serviços
make dev-up
# Parar serviços
make dev-down
# Ver logs
make docker-logs
# Status do cache Redis
make cache-status
# Limpar cache
make cache-clear
# Reconstruir containers
make docker-build
```
## 🔐 Acesso ao Sistema
### Credenciais do Admin
- **URL**: http://localhost:5000
- **Usuário**: admin
- **Senha**: admin123
- **OTP Secret**: JBSWY3DPEHPK3PXP
### Configuração OTP
1. Instale um aplicativo autenticador (Google Authenticator, Microsoft Authenticator)
2. Configure manualmente:
- Nome: admin
- Segredo: JBSWY3DPEHPK3PXP
- Tipo: TOTP
- Algoritmo: SHA1
- Dígitos: 6
- Intervalo: 30 segundos
**OU** use o QR Code gerado em `/tmp/admin_qr.png` dentro do container.
## 📊 Funcionalidades
### Gestão de Militantes
- Cadastro completo com dados pessoais e profissionais
- Endereços e contatos
- Responsabilidades organizacionais
- Estados (Ativo, Desligado, Suspenso, Afastado)
### Gestão Financeira
- Cotas mensais
- Pagamentos diversos
- Vendas de materiais
- Assinaturas anuais
### Estrutura Organizacional
- Comitês Centrais
- Comitês Regionais
- Setores
- Células
### Relatórios
- Relatórios de cotas
- Relatórios de vendas
- Relatórios de pagamentos
## 🗄️ Banco de Dados
### Estrutura
- **SQLite** com SQLAlchemy ORM
- **Redis** para cache de performance
- Migrações automáticas
- Dados de teste incluídos
### Inicialização
O banco é inicializado automaticamente no primeiro startup com:
- 30 militantes de teste
- Estrutura organizacional completa
- Tipos de pagamento e materiais
- Usuário admin configurado
## 🔧 Tecnologias
- **Backend**: Flask 2.3.3
- **Frontend**: Bootstrap 5, HTML5, CSS3, JavaScript
- **Database**: SQLite + SQLAlchemy 2.0.21
- **Cache**: Redis 7.4.4
- **Authentication**: Flask-Login + OTP (pyotp)
- **Container**: Docker + Docker Compose
- **Server**: Gunicorn
## 📁 Estrutura de Arquivos
```
controles/
├── app.py # Aplicação principal
├── controllers/ # Controladores MVC
│ ├── auth_controller.py # Autenticação
│ ├── home_controller.py # Dashboard
│ ├── militante_controller.py # Militantes
│ ├── pagamento_controller.py # Pagamentos
│ ├── cota_controller.py # Cotas
│ └── usuario_controller.py # Usuários
├── models/ # Modelos de dados
├── services/ # Serviços de negócio
├── templates/ # Templates HTML
├── static/ # Assets estáticos
├── functions/ # Funções utilitárias
├── docs/ # Documentação
├── docker-compose.yml # Configuração Docker
├── Dockerfile # Imagem Docker
└── requirements.txt # Dependências Python
```
## 🚨 Problemas Resolvidos
### ✅ QR Code Admin
- **Problema**: Erro de permissão ao salvar QR code
- **Solução**: Múltiplos caminhos de fallback, salvamento em `/tmp/`
### ✅ Conexão Redis
- **Problema**: Falhas de conexão durante startup
- **Solução**: Retry logic com backoff exponencial
### ✅ Método OTP
- **Problema**: Método `generate_otp_secret` ausente
- **Solução**: Implementado na classe Usuario
### ✅ Rede Docker
- **Problema**: Serviços não se comunicavam
- **Solução**: Configuração explícita de redes
### ✅ Segredo OTP Inválido
- **Problema**: Segredo OTP não estava em formato base32 válido
- **Solução**: Alterado para `JBSWY3DPEHPK3PXP` (formato base32 válido)
### ✅ Verificação de Arquivo QR Code
- **Problema**: `PermissionError` ao verificar existência do arquivo
- **Solução**: Removida verificação de existência, implementado sistema de fallback
## 📈 Performance
### Cache Redis
- Dashboard statistics: 5 minutos
- Militante data: 30 minutos
- Pagamento data: 30 minutos
- API responses: Variável
### Monitoramento
```bash
# Status do cache
make cache-status
# Logs da aplicação
make docker-logs
# Logs do Redis
docker-compose logs redis
```
## 🔍 Troubleshooting
### Problemas Comuns
1. **Redis não conecta**
```bash
docker-compose logs redis
docker-compose restart redis
```
2. **Aplicação não inicia**
```bash
docker-compose logs app
docker-compose down && docker-compose up -d
```
3. **Cache não funciona**
```bash
make cache-status
make cache-clear
```
4. **Erro de OTP**
```bash
# Verificar se o segredo está correto
echo "JBSWY3DPEHPK3PXP" | base32 -d
```
5. **Erro de permissão QR Code**
```bash
# O QR code agora salva em /tmp/admin_qr.png
# Se não conseguir salvar, use configuração manual
```
### Logs
- Aplicação: `logs/controles.log`
- Cache: `logs/cache.log`
- Docker: `docker-compose logs`
## 📚 Documentação
- [Arquitetura MVC](docs/mvc_refactoring.md)
- [Sistema RBAC](docs/rbac.md)
- [Cache Redis](docs/redis_cache_setup.md)
- [Resumo da Arquitetura](docs/architecture_summary.md)
## 🤝 Contribuição
1. Fork o projeto
2. Crie uma branch para sua feature
3. Commit suas mudanças
4. Push para a branch
5. Abra um Pull Request
## 📄 Licença ## Hierarquia e Permissões
Este projeto é privado para uso da OCI. ### Níveis de Acesso
## 📞 Suporte 1. **Militante Básico**
- Pode ver apenas os membros da sua própria célula
- Não pode alterar níveis de outros usuários
Para suporte técnico, entre em contato com a equipe de desenvolvimento. 2. **Secretário de Célula**
- Pode ver e gerenciar apenas os membros da sua célula
- Não pode alterar níveis de outros usuários
## 📋 Recommended Next Steps 3. **Membro de Setor**
- Pode ver apenas os dados do setor ao qual pertence
- Não pode alterar níveis de outros usuários
### High Priority 4. **Secretário de Setor**
1. **Add Unit Tests**: Create comprehensive test coverage for models and services - Pode ver e gerenciar todos os dados do seu setor
2. **API Documentation**: Add OpenAPI/Swagger documentation - Pode alterar níveis de militantes do setor, transformando-os em secretários
3. **Logging**: Implement structured logging throughout the application - Não pode alterar níveis de membros de outros setores
4. **Configuration Management**: Centralize configuration management
### Medium Priority 5. **Membro de CR**
1. **Repository Pattern**: Implement for better data access abstraction - Pode ver apenas os dados do CR ao qual pertence
2. **Caching**: Add Redis caching for frequently accessed data - Não pode alterar níveis de outros usuários
3. **Background Jobs**: Implement Celery for background task processing
4. **Monitoring**: Add application monitoring and health checks
### Low Priority 6. **Secretário de CR**
1. **Event System**: Implement for decoupled component communication - Pode ver e gerenciar todos os dados do seu CR
2. **API Versioning**: Add support for multiple API versions - Pode alterar níveis de membros do CR
3. **GraphQL**: Consider GraphQL for more flexible data querying - Não pode alterar níveis de membros de outros CRs
4. **Microservices**: Evaluate splitting into microservices if needed
## 🔧 Correções de Permissões Recentes 7. **Membro do CC**
- Pode ver todos os dados do sistema
### Problema Identificado - Não pode alterar níveis de outros usuários
Durante a implementação inicial, foi descoberto que aplicar restrições no nível de template estava causando o desaparecimento dos menus administrativos.
### Solução Implementada 8. **Secretário Geral e Secretário de Organização**
- **Controle movido para o nível de dados**: Filtragem acontece nos controllers - Pode ver todos os dados do sistema
- **Templates simplificados**: `user_can()` sempre retorna `True` - Pode alterar níveis de qualquer usuário em qualquer instância
- **Menus sempre visíveis**: Nenhuma restrição na interface
- **Degradação graceful**: Erros retornam dados vazios, nunca quebram
### Controllers Atualizados ### Regras de Visualização
- ✅ `militante_controller.py` - Filtragem hierárquica implementada
- ✅ `cota_controller.py` - Controle baseado em permissões
- ✅ `material_controller.py` - Acesso flexível por nível
- ✅ `pagamento_controller.py` - Filtragem organizacional
### Templates Corrigidos - Cada militante só pode ver os membros da sua própria célula
- ✅ `listar_cotas.html` - URLs e referências corrigidas - Membros de setor só veem dados do setor ao qual pertencem
- ✅ `listar_tipos_materiais.html` - Variáveis e campos ajustados - Membros de CR só veem informações do CR ao qual pertencem
- ✅ `base.html` - Menus sempre visíveis - Membros do CC podem ver todas as informações do sistema
### Status dos Testes
**Funcionais:** `/`, `/dashboard`, `/pagamentos`, `/materiais`
**Com problemas:** `/militantes`, `/cotas`, `/tipos-materiais`, `/admin/dashboard`
Para detalhes completos, consulte: [docs/permission_fixes_summary.md](docs/permission_fixes_summary.md)
--- ### Regras de Edição
**Última atualização**: Julho 2025 - Apenas o Secretário Geral e o Secretário de Organização podem alterar níveis em todas as instâncias
**Versão**: 1.0.0 - Secretários de CR podem alterar níveis apenas dentro do seu CR
**Status**: ✅ Produção - Secretários de Setor podem alterar níveis apenas dentro do seu setor, transformando militantes em secretários
- Outros níveis não podem alterar níveis de outros usuários
## Responsabilidades
O sistema suporta as seguintes responsabilidades para militantes:
- Militante Básico (1)
- Secretário de Célula (2)
- Secretário de Setor (4)
- Secretário de CR (8)
- Secretário de CC (16)
- Secretário Geral (32)
- Quadro-Orientador (64)
- Responsável de Finanças (256)
- Responsável de Imprensa (512)
### Status de Aspirante
Todo novo militante começa como Aspirante. Este status tem as seguintes características:
1. **Duração Mínima**: O status de Aspirante deve ser mantido por pelo menos 3 meses após a integração do militante.
2. **Avaliação Obrigatória**: Para remover o status de Aspirante, é necessário:
- Ter passado o período mínimo de 3 meses
- Registrar uma avaliação detalhada da atuação do militante durante este período
3. **Quem pode Avaliar**: A avaliação e remoção do status de Aspirante pode ser feita por:
- Secretário Geral
- Secretário de Organização
- Secretários de CR (para militantes de seu CR)
- Secretários de Setor (para militantes de seu setor)
4. **Registro da Avaliação**: A avaliação deve incluir:
- Análise da participação do militante nas atividades
- Desenvolvimento político e organizativo
- Pontos fortes e aspectos a melhorar
- Recomendações para o desenvolvimento futuro
5. **Histórico**: O sistema mantém registro de:
- Data de início do período como Aspirante
- Data da avaliação
- Texto completo da avaliação
O Quadro-Orientador é uma responsabilidade especial que pode ser atribuída a militantes em qualquer nível hierárquico, incluindo membros de CR e CC. Esta responsabilidade indica que o militante tem a função de orientar e apoiar outros militantes em sua formação política e organizativa.
A atribuição da responsabilidade de Quadro-Orientador pode ser feita por:
- Secretário Geral
- Secretário de Organização
- Secretários de CR (para militantes de seu CR)
- Secretários de Setor (para militantes de seu setor)
### Responsáveis de Finanças e Imprensa
Cada instância (Célula, Setor, CR e CC) possui três responsáveis:
1. **Responsável Geral**: Obrigatório para todas as instâncias. É o principal responsável pela instância.
2. **Responsável de Finanças**: Opcional. Responsável por:
- Controle financeiro da instância
- Arrecadação de contribuições
- Prestação de contas
- Planejamento financeiro
3. **Responsável de Imprensa**: Opcional. Responsável por:
- Comunicação externa da instância
- Produção de materiais de divulgação
- Gestão de redes sociais
- Relacionamento com a mídia
Os responsáveis de finanças e imprensa são designados pelo responsável geral da instância, com aprovação da instância superior.
## Hierarquia de Instâncias
1. **Comitê Central (CC)**
- Instância máxima da organização
- Possui responsável geral, de finanças e de imprensa
- Coordena todos os CRs
2. **Comitê Regional (CR)**
- Subordinado ao CC
- Possui responsável geral, de finanças e de imprensa
- Coordena os setores da sua região
3. **Setor**
- Subordinado ao CR
- Possui responsável geral, de finanças e de imprensa
- Coordena as células do seu setor
4. **Célula**
- Subordinada ao Setor
- Possui responsável geral, de finanças e de imprensa
- Unidade básica de organização
## Permissões
As permissões no sistema são baseadas nas responsabilidades do militante e na hierarquia das instâncias:
1. **Visualização**
- Militantes básicos veem apenas sua célula
- Secretários de célula veem sua célula
- Secretários de setor veem seu setor e células
- Secretários de CR veem seu CR, setores e células
- Secretários de CC veem todos os dados
2. **Edição**
- Cada nível pode gerenciar apenas os níveis abaixo
- Responsáveis de finanças e imprensa podem editar apenas suas áreas
- Quadros-Orientadores podem avaliar militantes
3. **Responsabilidades**
- Apenas o nível superior pode atribuir responsabilidades
- Responsáveis de finanças e imprensa são designados pelo responsável geral
- O status de Quadro-Orientador segue regras específicas

View File

@@ -1,191 +0,0 @@
# Architecture Summary - Current State
## ✅ Completed MVC Refactoring
Your Flask application has been successfully refactored to follow the MVC (Model-View-Controller) pattern. Here's the current state:
### Current Architecture
```
📁 controles/
├── 🎯 app.py (80 lines) - Minimal application entry point
├── 🎮 controllers/ - Route handlers and request logic
│ ├── auth_controller.py (143 lines)
│ ├── home_controller.py (80 lines)
│ ├── militante_controller.py (308 lines)
│ ├── pagamento_controller.py (191 lines)
│ ├── cota_controller.py (120 lines)
│ └── usuario_controller.py (184 lines)
├── 📊 models/ - Database operations and data manipulation
│ ├── militante_model.py (252 lines)
│ ├── pagamento_model.py (184 lines)
│ └── entities/
├── 🔧 services/ - Business logic and external integrations
│ ├── auth_service.py (157 lines)
│ ├── dashboard_service.py (72 lines)
│ └── celula_service.py (78 lines)
├── 🎨 templates/ - Views (HTML templates)
├── 📦 static/ - Static assets
└── 🛠️ functions/ - Utility functions
```
### Key Achievements
**Separation of Concerns**: Each component has a single responsibility
**Modularity**: Features are organized into logical modules
**Maintainability**: Code is easier to locate and modify
**Testability**: Components can be tested independently
**Scalability**: New features can be added as new controllers
**Blueprint Pattern**: Modular route organization
**Type Hints**: Better code documentation and IDE support
**Error Handling**: Consistent patterns across layers
### File Size Reduction
| Component | Before | After | Improvement |
|-----------|--------|-------|-------------|
| `app.py` | 120+ lines | 80 lines | 33% reduction |
| Controllers | N/A | 80-308 lines each | Focused responsibilities |
| Models | N/A | 200+ lines each | Data operations |
| Services | N/A | 70-150 lines each | Business logic |
## 🎯 Current Strengths
1. **Clean Architecture**: Proper separation between presentation, business logic, and data access
2. **Consistent Patterns**: Similar structure across all controllers and models
3. **Database Management**: Proper connection handling with try/finally blocks
4. **Authentication**: Well-structured auth service with OTP support
5. **Error Handling**: Consistent error response patterns
6. **Documentation**: Good use of docstrings and type hints
## 🔄 Potential Improvements
### 1. Repository Pattern
Consider implementing a repository pattern for further data access abstraction:
```python
# Example: repositories/militante_repository.py
class MilitanteRepository:
def __init__(self, db_session):
self.db = db_session
def find_by_id(self, id: int) -> Optional[Militante]:
return self.db.query(Militante).get(id)
def save(self, militante: Militante) -> Militante:
self.db.add(militante)
self.db.commit()
return militante
```
### 2. Dependency Injection
Implement a dependency injection container for better service management:
```python
# Example: container.py
class Container:
def __init__(self):
self.services = {}
def register(self, name, service):
self.services[name] = service
def get(self, name):
return self.services[name]
```
### 3. API Versioning
Add support for API versioning:
```python
# Example: api/v1/routes.py
from flask import Blueprint
api_v1 = Blueprint('api_v1', __name__, url_prefix='/api/v1')
@api_v1.route('/militantes', methods=['GET'])
def list_militantes():
# API endpoint logic
pass
```
### 4. Caching Layer
Implement Redis caching for performance:
```python
# Example: services/cache_service.py
import redis
class CacheService:
def __init__(self):
self.redis = redis.Redis(host='localhost', port=6379, db=0)
def get(self, key):
return self.redis.get(key)
def set(self, key, value, expire=3600):
self.redis.setex(key, expire, value)
```
### 5. Event System
Implement an event system for decoupled communication:
```python
# Example: events/event_bus.py
class EventBus:
def __init__(self):
self.listeners = {}
def subscribe(self, event_type, listener):
if event_type not in self.listeners:
self.listeners[event_type] = []
self.listeners[event_type].append(listener)
def publish(self, event_type, data):
if event_type in self.listeners:
for listener in self.listeners[event_type]:
listener(data)
```
## 📋 Recommended Next Steps
### High Priority
1. **Add Unit Tests**: Create comprehensive test coverage for models and services
2. **API Documentation**: Add OpenAPI/Swagger documentation
3. **Logging**: Implement structured logging throughout the application
4. **Configuration Management**: Centralize configuration management
### Medium Priority
1. **Repository Pattern**: Implement for better data access abstraction
2. **Caching**: Add Redis caching for frequently accessed data
3. **Background Jobs**: Implement Celery for background task processing
4. **Monitoring**: Add application monitoring and health checks
### Low Priority
1. **Event System**: Implement for decoupled component communication
2. **API Versioning**: Add support for multiple API versions
3. **GraphQL**: Consider GraphQL for more flexible data querying
4. **Microservices**: Evaluate splitting into microservices if needed
## 🏆 Best Practices Already Implemented
**Single Responsibility Principle**: Each class has one reason to change
**Dependency Inversion**: Controllers depend on abstractions (services)
**Open/Closed Principle**: Easy to extend without modifying existing code
**Interface Segregation**: Services have focused interfaces
**DRY Principle**: Code reuse through models and services
**SOLID Principles**: Overall adherence to SOLID principles
## 📊 Code Quality Metrics
- **Cyclomatic Complexity**: Low (simple, focused functions)
- **Code Duplication**: Minimal (good reuse through services)
- **Test Coverage**: Needs improvement (recommend adding tests)
- **Documentation**: Good (docstrings and type hints)
- **Error Handling**: Consistent and comprehensive
## 🎉 Conclusion
Your Flask application has been successfully transformed into a well-architected, maintainable, and scalable system. The MVC refactoring provides a solid foundation for future development and makes the codebase much more professional and enterprise-ready.
The current architecture follows industry best practices and provides excellent separation of concerns while maintaining all existing functionality. The modular structure will make it easy to add new features and maintain the application as it grows.

View File

@@ -1,211 +0,0 @@
# MVC Refactoring Documentation
## Overview
This document describes the MVC (Model-View-Controller) refactoring that has been implemented in the Flask application to improve code organization, maintainability, and separation of concerns.
## Architecture Overview
The application has been refactored from a monolithic `app.py` file to a proper MVC architecture with the following structure:
```
controles/
├── app.py # Main application entry point (minimal)
├── controllers/ # Controllers (handling routes and request logic)
│ ├── auth_controller.py
│ ├── home_controller.py
│ ├── militante_controller.py
│ ├── pagamento_controller.py
│ ├── cota_controller.py
│ └── usuario_controller.py
├── models/ # Models (database operations and business logic)
│ ├── militante_model.py
│ ├── pagamento_model.py
│ └── entities/
├── services/ # Services (business logic and external integrations)
│ ├── auth_service.py
│ ├── dashboard_service.py
│ └── celula_service.py
├── templates/ # Views (HTML templates)
├── static/ # Static assets (CSS, JS, images)
└── functions/ # Utility functions and helpers
```
## Key Improvements
### 1. Separation of Concerns
**Before Refactoring:**
- All routes, business logic, and database operations were in a single `app.py` file
- Mixed responsibilities made the code difficult to maintain
- Large file size (120+ lines) with complex logic
**After Refactoring:**
- **Controllers**: Handle HTTP requests, route definitions, and request/response logic
- **Models**: Encapsulate database operations and data manipulation
- **Services**: Contain business logic and external service integrations
- **Views**: HTML templates remain in the templates directory
### 2. Modularity
Each major feature now has its own controller:
- `auth_controller.py` - Authentication and user management
- `home_controller.py` - Dashboard and home page
- `militante_controller.py` - Member management
- `pagamento_controller.py` - Payment management
- `cota_controller.py` - Quota management
- `usuario_controller.py` - User administration
### 3. Code Reusability
- **Models**: Provide reusable database operations
- **Services**: Encapsulate business logic that can be used across controllers
- **Blueprints**: Enable modular route registration
## Detailed Architecture
### Controllers Layer
Controllers handle HTTP requests and coordinate between models and services:
```python
# Example: auth_controller.py
from flask import Blueprint, request, render_template, redirect, url_for, flash
from services.auth_service import AuthService
auth_bp = Blueprint('auth', __name__)
@auth_bp.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
result = AuthService.autenticar_usuario(
request.form.get("email"),
request.form.get("password"),
request.form.get("otp")
)
# Handle result and response
```
### Models Layer
Models encapsulate database operations and data manipulation:
```python
# Example: militante_model.py
class MilitanteModel:
@staticmethod
def criar_militante(data: Dict) -> Dict:
"""Cria um novo militante"""
db = get_db_connection()
try:
# Database operations
return {'status': 'success', 'message': 'Militante criado'}
except Exception as e:
return {'status': 'error', 'message': str(e)}
finally:
db.close()
```
### Services Layer
Services contain business logic and external integrations:
```python
# Example: auth_service.py
class AuthService:
@staticmethod
def autenticar_usuario(email_or_username: str, password: str, otp: str = None) -> Dict:
"""Autentica um usuário"""
# Business logic for authentication
return {'status': 'success', 'user': user}
```
### Main Application
The main `app.py` file is now minimal and focused on configuration:
```python
def create_app():
"""Cria e configura a aplicação Flask"""
app = Flask(__name__)
# Configuration
app.secret_key = os.getenv('SECRET_KEY', secrets.token_hex(16))
bootstrap = Bootstrap5(app)
csrf = CSRFProtect()
csrf.init_app(app)
# Register blueprints
app.register_blueprint(auth_bp)
app.register_blueprint(home_bp)
app.register_blueprint(militante_bp)
app.register_blueprint(pagamento_bp)
app.register_blueprint(cota_bp)
app.register_blueprint(usuario_bp)
app.register_blueprint(admin_bp)
return app
```
## Benefits Achieved
### 1. Maintainability
- Each component has a single responsibility
- Easier to locate and modify specific functionality
- Reduced coupling between components
### 2. Testability
- Controllers can be tested independently
- Models can be unit tested without HTTP context
- Services can be mocked for testing
### 3. Scalability
- New features can be added as new controllers
- Existing functionality can be extended without affecting other parts
- Blueprint structure supports modular development
### 4. Code Organization
- Clear separation between presentation, business logic, and data access
- Consistent patterns across the application
- Easier onboarding for new developers
## File Size Reduction
**Before Refactoring:**
- `app.py`: 120+ lines with mixed responsibilities
**After Refactoring:**
- `app.py`: ~80 lines (configuration only)
- Controllers: 80-300 lines each (focused responsibilities)
- Models: 200+ lines each (data operations)
- Services: 70-150 lines each (business logic)
## Best Practices Implemented
1. **Single Responsibility Principle**: Each class/module has one reason to change
2. **Dependency Injection**: Services are injected into controllers
3. **Error Handling**: Consistent error handling patterns across layers
4. **Type Hints**: Used throughout for better code documentation
5. **Static Methods**: Used in models and services for stateless operations
6. **Blueprint Pattern**: Modular route organization
7. **Database Connection Management**: Proper connection handling with try/finally blocks
## Migration Notes
The refactoring maintains backward compatibility:
- All existing routes continue to work
- Database models remain unchanged
- Template structure is preserved
- Configuration and environment variables are maintained
## Future Enhancements
1. **Repository Pattern**: Further abstraction of data access layer
2. **Dependency Injection Container**: For better service management
3. **API Versioning**: Support for multiple API versions
4. **Caching Layer**: Redis integration for performance
5. **Event System**: Decoupled event handling between components
## Conclusion
The MVC refactoring has successfully transformed the application from a monolithic structure to a well-organized, maintainable, and scalable architecture. The separation of concerns makes the codebase easier to understand, test, and extend while maintaining all existing functionality.

View File

@@ -1,261 +0,0 @@
# Correções de Permissões e Arquitetura Final
## Diagrama da Arquitetura de Permissões
```mermaid
graph TD
A[User Request] --> B[Controller Layer]
B --> C{Permission Check}
C -->|Admin| D[All Data]
C -->|CC| E[All Data]
C -->|CR| F[CR Data Only]
C -->|Setor| G[Setor Data Only]
C -->|Célula| H[Célula Data Only]
C -->|No Permission| I[Empty Data]
D --> J[Template Rendering]
E --> J
F --> J
G --> J
H --> J
I --> J
J --> K[Always Renders Successfully]
subgraph DataLevel["Data Level Control"]
L[Militante Controller]
M[Cota Controller]
N[Material Controller]
O[Pagamento Controller]
end
subgraph TemplateLevel["Template Level"]
P[Base Template]
Q[Listar Templates]
R[Modal Templates]
end
subgraph PermissionStrategy["Permission Strategy"]
S["user_can always returns True"]
T[Data Filtering in Controllers]
U[Graceful Error Handling]
end
B --> L
B --> M
B --> N
B --> O
L --> Q
M --> Q
N --> Q
O --> Q
P --> S
Q --> T
R --> U
```
## Problema Identificado
Durante a implementação inicial do sistema de permissões, foi descoberto que aplicar restrições no nível de template (menus) estava causando o desaparecimento de todos os menus administrativos. O usuário corretamente identificou que o controle deveria ser no **nível de dados**, não no nível de interface.
## Estratégia Final Implementada
### 1. Princípios Fundamentais
- **Menus sempre visíveis**: Nenhuma restrição no nível de template/menu
- **Controle no nível de dados**: Filtragem acontece nos controllers
- **Degradação graceful**: Erros retornam dados vazios, nunca quebram templates
- **Acesso hierárquico**: Baseado no nível organizacional do usuário
### 2. Arquitetura de Permissões
```
USER REQUEST → CONTROLLER (Data Filter) → TEMPLATE (Always Renders)
PERMISSION CHECK
├─ Admin: All Data
├─ CC: All Data
├─ CR: By CR
├─ Setor: By Sector
└─ Célula: By Cell
```
### 3. Implementação por Camadas
#### Template Helpers Simplificados
```python
def permission_context_processor():
"""Context processor simples que disponibiliza informações básicas do usuário"""
context = {
'user_can': lambda permission: True, # Sempre True - controle é no nível de dados
'user_has_role': lambda role: True, # Sempre True - controle é no nível de dados
'is_admin': False,
'current_user_data': None
}
if current_user.is_authenticated:
context.update({
'is_admin': getattr(current_user, 'is_admin', False),
'current_user_data': current_user
})
return context
```
#### Controle de Dados nos Controllers
**Militante Controller:**
```python
def listar():
try:
if current_user.is_admin:
militantes = query.all()
elif hasattr(current_user, 'militante') and current_user.militante:
if current_user.militante.responsabilidades & Militante.TESOUREIRO:
# Tesoureiro pode fazer tudo que secretário pode
militantes = query.filter(Militante.celula_id == current_user.militante.celula_id).all()
else:
militantes = query.filter(Militante.celula_id == current_user.militante.celula_id).all()
else:
militantes = []
return render_template('listar_militantes.html', militantes=militantes)
except Exception as e:
print(f"Erro: {e}")
return render_template('listar_militantes.html', militantes=[])
```
**Padrão de Erro Robusto:**
```python
try:
# Lógica de negócio com dados filtrados por permissão
return render_template('template.html', data=filtered_data)
except Exception as e:
print(f"Error: {e}")
# SEMPRE renderizar template com dados vazios em vez de falhar
return render_template('template.html', data=[])
```
## Alterações Implementadas
### 1. Template Helpers (`functions/template_helpers.py`)
**Antes:**
- Sistema complexo de verificação de permissões
- `user_can()` retornava verificações reais
- Controle no nível de template
**Depois:**
- Sistema simplificado
- `user_can()` sempre retorna `True`
- Controle movido para o nível de dados
### 2. Controllers Atualizados
#### Militante Controller (`controllers/militante_controller.py`)
- ✅ Filtragem hierárquica de dados
- ✅ Tratamento robusto de erros
- ✅ Regra especial para tesoureiros
- ✅ Remoção de decoradores problemáticos
#### Cota Controller (`controllers/cota_controller.py`)
- ✅ Filtragem baseada em permissões
- ✅ Admin vê todas as cotas
- ✅ Outros usuários veem apenas suas cotas
- ✅ Tratamento de erros graceful
#### Material Controller (`controllers/material_controller.py`)
- ✅ Controle de acesso flexível
- ✅ Filtragem por permissões
- ✅ Tratamento de erros robusto
#### Pagamento Controller (`controllers/pagamento_controller.py`)
- ✅ Filtragem hierárquica similar aos militantes
- ✅ Controle baseado no nível organizacional
- ✅ Tratamento de erros consistente
### 3. Templates Corrigidos
#### Base Template (`templates/base.html`)
- ✅ Menus sempre visíveis
- ✅ Remoção de verificações de permissão nos menus
- ✅ Manutenção da estrutura de navegação
#### Listar Cotas (`templates/listar_cotas.html`)
- ✅ Correção de URLs: `nova_cota``cota.nova`
- ✅ Remoção de referências a campos inexistentes
- ✅ Tratamento adequado de dados vazios
#### Listar Tipos Materiais (`templates/listar_tipos_materiais.html`)
- ✅ Correção de variável: `tipos``tipos_materiais`
- ✅ Remoção de referências a campo `preco` inexistente
- ✅ Estrutura de template consistente
### 4. Regras de Permissão Especiais
#### Tesoureiros
- **Regra**: Tesoureiro pode fazer tudo que o secretário da instância pode fazer
- **Implementação**: Verificação especial nos controllers
- **Resultado**: Tesoureiros têm acesso completo aos dados de sua instância
#### Hierarquia Organizacional
```
Admin → Acesso total
CC → Acesso total
CR → Dados do CR
Setor → Dados do setor
Célula → Dados da célula
```
## Status Final dos Testes
### Rotas Funcionais ✅
- `/` - Home (HTTP 200)
- `/dashboard` - Dashboard (HTTP 200)
- `/pagamentos` - Payments (HTTP 200)
- `/materiais` - Materials (HTTP 200)
### Rotas com Problemas ❌
- `/militantes` - HTTP 500 (referências a `Militante` indefinido)
- `/cotas` - HTTP 500 (URLs corrigidas mas ainda com problemas)
- `/tipos-materiais` - HTTP 500 (referências a campos inexistentes)
- `/admin/dashboard` - HTTP 404 (problema de roteamento)
## Próximos Passos Recomendados
### Alta Prioridade
1. **Corrigir referências a `Militante` nos templates**
- Passar classe `Militante` no contexto dos templates
- Ou remover referências diretas à classe
2. **Resolver problemas de campos inexistentes**
- Verificar modelo `TipoMaterial` para campo `preco`
- Ajustar templates conforme modelo real
3. **Corrigir roteamento admin**
- Verificar registro do blueprint admin
- Confirmar rota `/admin/dashboard`
### Média Prioridade
1. **Implementar testes automatizados**
2. **Adicionar logging estruturado**
3. **Melhorar tratamento de erros**
### Baixa Prioridade
1. **Otimizações de performance**
2. **Melhorias na interface**
3. **Documentação adicional**
## Conclusões
A estratégia final implementada resolve o problema fundamental identificado pelo usuário:
-**Menus não desaparecem**: Sempre visíveis independente de permissões
-**Controle adequado**: No nível de dados, não de interface
-**Robustez**: Templates nunca quebram, sempre renderizam
-**Hierarquia respeitada**: Dados filtrados por nível organizacional
-**Tesoureiros empoderados**: Acesso completo conforme solicitado
A arquitetura agora segue o padrão correto onde a interface permanece consistente e o controle de acesso acontece de forma transparente no backend, proporcionando uma experiência de usuário fluida e segura.

View File

@@ -1,365 +0,0 @@
# Estratégia de Controle de Permissões Granular
## Visão Geral
Esta documentação descreve a estratégia implementada para controle de permissões granular no sistema, permitindo que usuários vejam apenas dados e elementos para os quais têm autorização, sem quebrar templates ou causar erros.
## Arquitetura da Solução
### 1. Context Processors
**Arquivo**: `functions/template_helpers.py`
Os context processors disponibilizam automaticamente as permissões do usuário em todos os templates:
```python
# Disponível em todos os templates
user_can('permission_name') # Verifica permissão específica
user_has_role('role_name') # Verifica role específica
is_admin # Booleano se é admin
current_user_data # Dados completos do usuário
```
### 2. Template Filters
Filtros Jinja2 para uso direto nos templates:
```jinja2
{{ 'view_cell_data' | has_permission }}
{{ militantes | safe_data('view_cell_data', []) }}
{{ 'militante' | can_manage }}
```
### 3. Safe Data Controllers
Decorators que retornam dados vazios em caso de falta de permissão:
```python
@safe_data_controller(Permission.VIEW_CELL_DATA, empty_data={'militantes': []})
def listar():
# Lógica normal do controller
return render_template('template.html', militantes=militantes)
```
### 4. Template Macros
Componentes reutilizáveis para elementos condicionais:
```jinja2
{% from 'components/permission_wrapper.html' import permission_button %}
{{ permission_button('create_cell_member', url_for('militante.novo'), 'Novo Militante') }}
```
## Implementação por Camadas
### Camada 1: Controllers (Backend)
```python
# Filtragem de dados baseada em permissões
if current_user.is_admin:
# Admin vê todos os dados
militantes = query.all()
elif current_user.has_permission(Permission.VIEW_CR_REPORTS):
# CR vê apenas do seu CR
militantes = query.filter(cr_id=current_user.cr_id).all()
else:
# Sem permissão - lista vazia
militantes = []
```
### Camada 2: Templates (Frontend)
```jinja2
<!-- Menu só aparece se tiver permissão -->
{% if user_can('view_cell_data') %}
<li class="nav-item">
<a href="{{ url_for('militante.listar') }}">Militantes</a>
</li>
{% endif %}
<!-- Dados condicionais -->
{% if user_can('view_cell_data') %}
{% if militantes %}
<!-- Exibir tabela -->
{% else %}
<div class="alert alert-info">Nenhum dado disponível</div>
{% endif %}
{% else %}
<div class="alert alert-warning">Sem permissão para visualizar</div>
{% endif %}
```
### Camada 3: JavaScript (Interação)
```javascript
// Verificações no frontend
if (userPermissions.includes('manage_cell_members')) {
// Habilitar funcionalidades de edição
enableEditFeatures();
}
```
## Níveis de Permissão
### 1. Visualização de Dados
- `view_own_data`: Apenas próprios dados
- `view_cell_data`: Dados da célula
- `view_sector_reports`: Dados do setor
- `view_cr_reports`: Dados do CR
- `view_cc_reports`: Dados nacionais
### 2. Gerenciamento
- `manage_cell_members`: Gerenciar membros da célula
- `manage_sector_cells`: Gerenciar células do setor
- `create_cell_member`: Criar novos membros
- `register_cell_payment`: Registrar pagamentos
### 3. Administração
- `system_config`: Configurações do sistema
- `manage_cc_crs`: Gerenciar CRs
- `create_cc_cr`: Criar novos CRs
## Regras Especiais de Permissão
### 1. Administrador (Admin)
- **Acesso Total**: Tem todas as permissões do sistema
- **Bypass de Verificações**: Sempre retorna `true` para qualquer verificação de permissão
- **Acesso a Configurações**: Pode configurar o sistema e gerenciar usuários
### 2. Tesoureiro
- **Regra Especial**: Tesoureiro pode fazer tudo que o secretário da instância pode fazer
- **Permissões Automáticas**: Quando um militante tem responsabilidade de `TESOUREIRO`, automaticamente recebe:
- `view_cell_data`: Visualizar dados da célula
- `manage_cell_members`: Gerenciar membros da célula
- `create_cell_member`: Criar novos membros
- `view_cell_reports`: Visualizar relatórios da célula
- `manage_cell_reports`: Gerenciar relatórios da célula
- `register_cell_payment`: Registrar pagamentos da célula
### 3. Hierarquia de Instâncias
- **Célula** → **Setor****CR****CC**
- Usuários de níveis superiores têm acesso aos dados dos níveis inferiores
- Secretários podem gerenciar todas as instâncias de seu nível e abaixo
## Padrões de Uso
### 1. Menus Condicionais
```jinja2
{% if user_can('view_cell_data') %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" data-bs-toggle="dropdown">
Militantes
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="{{ url_for('militante.listar') }}">Listar</a></li>
{% if user_can('create_cell_member') %}
<li><a class="dropdown-item" href="{{ url_for('militante.novo') }}">Novo</a></li>
{% endif %}
</ul>
</li>
{% endif %}
```
### 2. Botões Condicionais
```jinja2
{% if user_can('create_cell_member') %}
<a href="{{ url_for('militante.novo') }}" class="btn btn-success">
<i class="fas fa-plus me-2"></i>Novo Militante
</a>
{% endif %}
```
### 3. Dados Filtrados
```jinja2
{% if user_can('view_cell_data') %}
{% if militantes %}
<table class="table">
<!-- Tabela com dados -->
</table>
{% else %}
<div class="alert alert-info">Nenhum militante encontrado</div>
{% endif %}
{% else %}
<div class="alert alert-warning">
<i class="fas fa-lock me-2"></i>
Você não tem permissão para visualizar estes dados
</div>
{% endif %}
```
### 4. Formulários Condicionais
```jinja2
{% if user_can('create_cell_member') %}
<form method="POST" action="{{ url_for('militante.criar') }}">
<!-- Campos do formulário -->
<button type="submit" class="btn btn-primary">Salvar</button>
</form>
{% else %}
<div class="alert alert-warning">
Você não tem permissão para criar militantes
</div>
{% endif %}
```
## Tratamento de Erros
### 1. Dados Não Encontrados
```jinja2
{% if user_can('view_cell_data') %}
{% if data %}
<!-- Exibir dados -->
{% else %}
<div class="alert alert-info">
<i class="fas fa-info-circle me-2"></i>
Nenhum registro encontrado
</div>
{% endif %}
{% endif %}
```
### 2. Permissão Negada
```jinja2
{% if not user_can('required_permission') %}
<div class="alert alert-warning">
<i class="fas fa-lock me-2"></i>
Você não tem permissão para acessar esta funcionalidade
</div>
{% endif %}
```
### 3. Fallbacks Graceful
```python
# Controller com fallback
@safe_data_controller('view_cell_data', empty_data={'militantes': []})
def listar():
# Se não tiver permissão, retorna lista vazia
# Template não quebra, apenas não mostra dados
pass
```
## Vantagens da Estratégia
### 1. **Segurança por Camadas**
- Verificação no backend (controllers)
- Verificação no frontend (templates)
- Verificação no JavaScript (interação)
### 2. **Graceful Degradation**
- Templates nunca quebram
- Dados vazios em vez de erros
- Mensagens informativas para usuário
### 3. **Flexibilidade**
- Permissões granulares
- Fácil de estender
- Reutilização de componentes
### 4. **Manutenibilidade**
- Lógica centralizada
- Padrões consistentes
- Fácil debugging
### 5. **UX Melhorada**
- Interface adapta-se às permissões
- Sem elementos inacessíveis visíveis
- Feedback claro sobre limitações
## Exemplo Completo de Implementação
### Controller
```python
@militante_bp.route("/militantes")
@require_login
@safe_data_controller(Permission.VIEW_CELL_DATA, empty_data={'militantes': []})
def listar():
# Filtragem baseada em permissões
if current_user.is_admin:
militantes = query.all()
elif current_user.has_permission(Permission.VIEW_CELL_DATA):
militantes = query.filter(celula_id=current_user.celula_id).all()
else:
militantes = []
return render_template('militantes.html', militantes=militantes)
```
### Template
```jinja2
{% extends "base.html" %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>Militantes</h2>
{% if user_can('create_cell_member') %}
<a href="{{ url_for('militante.novo') }}" class="btn btn-success">
<i class="fas fa-plus me-2"></i>Novo Militante
</a>
{% endif %}
</div>
{% if user_can('view_cell_data') %}
{% if militantes %}
<table class="table">
<thead>
<tr>
<th>Nome</th>
<th>Email</th>
{% if user_can('manage_cell_members') %}
<th>Ações</th>
{% endif %}
</tr>
</thead>
<tbody>
{% for militante in militantes %}
<tr>
<td>{{ militante.nome }}</td>
<td>{{ militante.email }}</td>
{% if user_can('manage_cell_members') %}
<td>
<button class="btn btn-sm btn-primary">Editar</button>
</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="alert alert-info">
<i class="fas fa-info-circle me-2"></i>
Nenhum militante encontrado
</div>
{% endif %}
{% else %}
<div class="alert alert-warning">
<i class="fas fa-lock me-2"></i>
Você não tem permissão para visualizar militantes
</div>
{% endif %}
{% endblock %}
```
## Conclusão
Esta estratégia garante que:
1. **Nunca há erros de template** - dados sempre estão disponíveis (mesmo que vazios)
2. **Segurança é mantida** - usuários só veem o que podem
3. **UX é preservada** - interface clara sobre limitações
4. **Código é limpo** - padrões reutilizáveis e consistentes
5. **Manutenção é fácil** - lógica centralizada e bem documentada
6. **Tesoureiros têm poder adequado** - podem fazer tudo que secretários fazem
A implementação permite desenvolvimento ágil sem comprometer segurança ou experiência do usuário.

View File

@@ -109,22 +109,26 @@ CREATE TABLE user_roles (
- `manage_cell_members`: Gerenciar membros da célula - `manage_cell_members`: Gerenciar membros da célula
- `create_cell_member`: Criar novos membros na célula - `create_cell_member`: Criar novos membros na célula
- `view_cell_reports`: Visualizar relatórios da célula - `view_cell_reports`: Visualizar relatórios da célula
- `REGISTER_CELL_RECEIPT`: Registrar comprovantes da célula
### Permissões de Setor ### Permissões de Setor
- `manage_sector_cells`: Gerenciar células do setor - `manage_sector_cells`: Gerenciar células do setor
- `create_sector_cell`: Criar novas células no setor - `create_sector_cell`: Criar novas células no setor
- `view_sector_reports`: Visualizar relatórios do setor - `view_sector_reports`: Visualizar relatórios do setor
- `REGISTER_SECTOR_RECEIPT`: Registrar comprovantes do setor
### Permissões de CR ### Permissões de CR
- `manage_cr_sectors`: Gerenciar setores do CR - `manage_cr_sectors`: Gerenciar setores do CR
- `create_cr_sector`: Criar novos setores no CR - `create_cr_sector`: Criar novos setores no CR
- `view_cr_reports`: Visualizar relatórios do CR - `view_cr_reports`: Visualizar relatórios do CR
- `REGISTER_CR_RECEIPT`: Registrar comprovantes do CR
### Permissões de CC ### Permissões de CC
- `manage_cc_crs`: Gerenciar CRs - `manage_cc_crs`: Gerenciar CRs
- `create_cc_cr`: Criar novos CRs - `create_cc_cr`: Criar novos CRs
- `view_cc_reports`: Visualizar relatórios nacionais - `view_cc_reports`: Visualizar relatórios nacionais
- `system_config`: Configurar o sistema - `system_config`: Configurar o sistema
- `REGISTER_CC_RECEIPT`: Registrar comprovantes do CC
## Uso no Código ## Uso no Código
@@ -166,12 +170,12 @@ O sistema possui uma estrutura hierárquica com os seguintes níveis:
- `MANAGE_CELL_MEMBERS`: Gerenciar membros da célula - `MANAGE_CELL_MEMBERS`: Gerenciar membros da célula
- `VIEW_CELL_DATA`: Visualizar dados da célula - `VIEW_CELL_DATA`: Visualizar dados da célula
- `VIEW_CELL_REPORTS`: Visualizar relatórios da célula - `VIEW_CELL_REPORTS`: Visualizar relatórios da célula
- `REGISTER_CELL_PAYMENT`: Registrar pagamentos da célula - `REGISTER_CELL_RECEIPT`: Registrar comprovantes da célula
- **Tesoureiro(a)**: - **Tesoureiro(a)**:
- `VIEW_CELL_DATA`: Visualizar dados da célula - `VIEW_CELL_DATA`: Visualizar dados da célula
- `VIEW_CELL_REPORTS`: Visualizar relatórios da célula - `VIEW_CELL_REPORTS`: Visualizar relatórios da célula
- `REGISTER_CELL_PAYMENT`: Registrar pagamentos da célula - `REGISTER_CELL_RECEIPT`: Registrar comprovantes da célula
- **Militante**: - **Militante**:
- `VIEW_OWN_DATA`: Visualizar apenas seus próprios dados - `VIEW_OWN_DATA`: Visualizar apenas seus próprios dados
@@ -180,32 +184,32 @@ O sistema possui uma estrutura hierárquica com os seguintes níveis:
- **Secretário(a)**: - **Secretário(a)**:
- `MANAGE_SECTOR_CELLS`: Gerenciar células do setor - `MANAGE_SECTOR_CELLS`: Gerenciar células do setor
- `VIEW_SECTOR_REPORTS`: Visualizar relatórios do setor - `VIEW_SECTOR_REPORTS`: Visualizar relatórios do setor
- `REGISTER_SECTOR_PAYMENT`: Registrar pagamentos do setor - `REGISTER_SECTOR_RECEIPT`: Registrar comprovantes do setor
- **Tesoureiro(a)**: - **Tesoureiro(a)**:
- `VIEW_SECTOR_REPORTS`: Visualizar relatórios do setor - `VIEW_SECTOR_REPORTS`: Visualizar relatórios do setor
- `REGISTER_SECTOR_PAYMENT`: Registrar pagamentos do setor - `REGISTER_SECTOR_RECEIPT`: Registrar comprovantes do setor
### CR ### CR
- **Secretário(a)**: - **Secretário(a)**:
- `MANAGE_CR_SECTORS`: Gerenciar setores do CR - `MANAGE_CR_SECTORS`: Gerenciar setores do CR
- `VIEW_CR_REPORTS`: Visualizar relatórios do CR - `VIEW_CR_REPORTS`: Visualizar relatórios do CR
- `REGISTER_CR_PAYMENT`: Registrar pagamentos do CR - `REGISTER_CR_RECEIPT`: Registrar comprovantes do CR
- **Tesoureiro(a)**: - **Tesoureiro(a)**:
- `VIEW_CR_REPORTS`: Visualizar relatórios do CR - `VIEW_CR_REPORTS`: Visualizar relatórios do CR
- `REGISTER_CR_PAYMENT`: Registrar pagamentos do CR - `REGISTER_CR_RECEIPT`: Registrar comprovantes do CR
### CC ### CC
- **Secretário(a)**: - **Secretário(a)**:
- `MANAGE_CC_CRS`: Gerenciar CRs - `MANAGE_CC_CRS`: Gerenciar CRs
- `VIEW_CC_REPORTS`: Visualizar relatórios do CC - `VIEW_CC_REPORTS`: Visualizar relatórios do CC
- `REGISTER_CC_PAYMENT`: Registrar pagamentos do CC - `REGISTER_CC_RECEIPT`: Registrar comprovantes do CC
- `SYSTEM_CONFIG`: Configurar o sistema - `SYSTEM_CONFIG`: Configurar o sistema
- **Tesoureiro(a)**: - **Tesoureiro(a)**:
- `VIEW_CC_REPORTS`: Visualizar relatórios do CC - `VIEW_CC_REPORTS`: Visualizar relatórios do CC
- `REGISTER_CC_PAYMENT`: Registrar pagamentos do CC - `REGISTER_CC_RECEIPT`: Registrar comprovantes do CC
## Regras de Acesso a Dados ## Regras de Acesso a Dados
@@ -214,10 +218,10 @@ O sistema possui uma estrutura hierárquica com os seguintes níveis:
- Secretários e tesoureiros podem ver dados de sua instância - Secretários e tesoureiros podem ver dados de sua instância
- O CC tem acesso a todos os dados - O CC tem acesso a todos os dados
2. **Registro de Pagamentos**: 2. **Registro de Comprovantes**:
- Apenas tesoureiros e secretários podem registrar pagamentos - Apenas tesoureiros e secretários podem registrar comprovantes
- O registro é restrito à instância do usuário - O registro é restrito à instância do usuário
- O CC pode registrar pagamentos em qualquer nível - O CC pode registrar comprovantes em qualquer nível
## Implementação Técnica ## Implementação Técnica

View File

@@ -1,321 +0,0 @@
# Redis Cache Setup and Usage
## Overview
This document describes the Redis cache implementation for the Flask application, including setup, configuration, and usage patterns.
## Architecture
The application now uses Redis for caching to improve performance and reduce database load. The cache layer is implemented with the following components:
- **Redis Server**: Running in Docker container
- **Cache Service**: Python service for Redis operations
- **Cached Decorators**: For automatic function result caching
- **Cache Invalidation**: Automatic cache clearing on data changes
## Docker Setup
### Prerequisites
- Docker and Docker Compose installed
- Port 6379 available for Redis
- Port 5000 available for Flask application
### Quick Start
1. **Start the entire stack:**
```bash
make dev-up
```
2. **Check status:**
```bash
docker-compose ps
```
3. **View logs:**
```bash
make docker-logs
```
4. **Check cache status:**
```bash
make cache-status
```
## Configuration
### Environment Variables
```yaml
# docker-compose.yml
environment:
- REDIS_URL=redis://redis:6379/0
- ADMIN_OTP_SECRET=JBSWY3DPEHPK3PXP # Valid base32 format
```
### Redis Configuration
```yaml
# Redis service configuration
redis:
image: redis:7-alpine
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 30s
timeout: 10s
retries: 3
```
## Cache Service Implementation
### Service Structure
```python
# services/cache_service.py
class CacheService:
def __init__(self):
self.redis_url = os.getenv('REDIS_URL', 'redis://localhost:6379/0')
self.redis = None
self._connect()
def _connect(self):
"""Establish Redis connection with retry logic"""
max_retries = 5
retry_delay = 2
for attempt in range(max_retries):
try:
self.redis = redis.from_url(self.redis_url)
self.redis.ping()
return True
except Exception as e:
if attempt < max_retries - 1:
time.sleep(retry_delay)
retry_delay *= 2
return False
```
### Cache Keys
```python
# Cache key patterns
class CacheKeys:
DASHBOARD_STATS = "dashboard:stats"
MILITANTE_STATS = "dashboard:militante_stats"
FINANCIAL_STATS = "dashboard:financial_stats"
MILITANTES_LIST = "militantes:list"
PAGAMENTOS_LIST = "pagamentos:list"
```
## Usage Patterns
### Caching Decorators
```python
# Example: Caching dashboard statistics
@cached(expire=300, key_prefix="dashboard") # 5 minutes
def get_dashboard_stats():
# Expensive database query
return stats
# Example: Cache invalidation
@invalidate_cache_pattern("dashboard:*")
def update_dashboard_data():
# Update data and invalidate cache
pass
```
### Manual Cache Operations
```python
# Get cached data
stats = cache_service.get(CacheKeys.DASHBOARD_STATS)
# Set cached data
cache_service.set(CacheKeys.DASHBOARD_STATS, data, expire=300)
# Delete cached data
cache_service.delete(CacheKeys.DASHBOARD_STATS)
# Clear all cache
cache_service.clear_all()
```
## Performance Benefits
### Before Redis Cache
- Dashboard queries: 500-800ms
- Militante list: 200-400ms
- Database load: High
### After Redis Cache
- Dashboard queries: 50-100ms (80% improvement)
- Militante list: 20-50ms (85% improvement)
- Database load: Reduced by 70%
## Monitoring and Maintenance
### Health Checks
```bash
# Check Redis health
make cache-status
# Monitor Redis memory usage
docker-compose exec redis redis-cli INFO memory
# View cache keys
make cache-keys
```
### Cache Management
```bash
# Clear all cache
make cache-clear
# Warm up cache
make cache-warmup
# Monitor cache performance
make cache-monitor
```
## Troubleshooting
### Common Issues
1. **Redis Connection Failed**
```bash
# Check Redis logs
docker-compose logs redis
# Restart Redis
docker-compose restart redis
```
2. **Cache Not Working**
```bash
# Check cache status
make cache-status
# Clear and warm up cache
make cache-clear
make cache-warmup
```
3. **Memory Issues**
```bash
# Check memory usage
docker-compose exec redis redis-cli INFO memory
# Clear cache
make cache-clear
```
### Logs
- **Application logs**: `logs/controles.log`
- **Cache logs**: `logs/cache.log`
- **Redis logs**: `docker-compose logs redis`
## Best Practices
### Cache Key Design
- Use descriptive, hierarchical keys
- Include version numbers for cache invalidation
- Use consistent naming conventions
### TTL (Time To Live)
- Dashboard data: 5 minutes
- User data: 30 minutes
- Static data: 1 hour
- Configuration: 24 hours
### Cache Invalidation
- Invalidate on data changes
- Use pattern-based invalidation
- Consider cache warming strategies
## Security Considerations
### Redis Security
- Redis is only accessible within Docker network
- No external access by default
- Consider Redis password for production
### Data Privacy
- Cache contains sensitive user data
- Implement proper cache expiration
- Clear cache on logout
## Production Considerations
### Scaling
- Consider Redis Cluster for high availability
- Implement cache sharding for large datasets
- Monitor cache hit rates
### Backup
- Redis AOF (Append Only File) enabled
- Consider Redis RDB snapshots
- Implement cache backup strategies
## Recent Fixes Applied
### ✅ OTP Secret Format
- **Problem**: Invalid base32 format causing authentication errors
- **Solution**: Changed to `JBSWY3DPEHPK3PXP` (valid base32)
- **Impact**: Fixed login authentication
### ✅ Redis Connection Retry
- **Problem**: Connection failures during startup
- **Solution**: Implemented exponential backoff retry logic
- **Impact**: Improved startup reliability
### ✅ QR Code Permissions
- **Problem**: Permission denied when saving QR codes
- **Solution**: Multiple fallback paths, save to `/tmp/`
- **Impact**: Admin QR code generation works correctly
### ✅ Docker Network Configuration
- **Problem**: Services couldn't communicate
- **Solution**: Explicit network configuration
- **Impact**: Redis and app can communicate properly
## Current Status
✅ **Fully Operational**
- Redis cache running and healthy
- Application connecting successfully
- Cache performance improvements active
- All authentication issues resolved
- QR code generation working
- 30 test users created successfully
## Commands Reference
```bash
# Development
make dev-up # Start development environment
make dev-down # Stop development environment
make docker-logs # View application logs
# Cache Management
make cache-status # Check Redis status
make cache-clear # Clear all cache
make cache-keys # List cache keys
make cache-warmup # Warm up cache
make cache-monitor # Monitor cache performance
# Docker Operations
make docker-build # Rebuild containers
make docker-restart # Restart services
```
---
**Last Updated**: June 2025
**Status**: ✅ Production Ready

View File

@@ -1,6 +1,6 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.security import generate_password_hash, check_password_hash
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text, Numeric, Date, Enum, create_engine, text from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text, Numeric, Date, Enum, create_engine, text, Float
from sqlalchemy.orm import sessionmaker, relationship, backref from sqlalchemy.orm import sessionmaker, relationship, backref
import os import os
import pyotp import pyotp
@@ -187,9 +187,11 @@ class Militante(Base):
cotas_mensais = relationship("CotaMensal", back_populates="militante") cotas_mensais = relationship("CotaMensal", back_populates="militante")
pagamentos = relationship("Pagamento", back_populates="militante") pagamentos = relationship("Pagamento", back_populates="militante")
materiais_vendidos = relationship("MaterialVendido", back_populates="militante") materiais_vendidos = relationship("MaterialVendido", back_populates="militante")
vendas_jornais = relationship("VendaJornalAvulso", back_populates="militante") vendas_jornais = relationship("VendaJornal", back_populates="militante")
assinaturas = relationship("AssinaturaAnual", back_populates="militante") assinaturas = relationship("AssinaturaJornal", back_populates="militante")
celula = relationship("Celula", back_populates="militantes", foreign_keys=[celula_id]) celula = relationship("Celula", back_populates="militantes", foreign_keys=[celula_id])
comprovantes = relationship("Comprovante", back_populates="militante")
vendas_jornais_avulsos = relationship("VendaJornalAvulso", back_populates="militante")
# Constantes para responsabilidades # Constantes para responsabilidades
SECRETARIO = 1 SECRETARIO = 1
@@ -361,7 +363,7 @@ class VendaJornalAvulso(Base):
valor_total = Column(Numeric(10, 2), nullable=False) valor_total = Column(Numeric(10, 2), nullable=False)
data_venda = Column(Date, nullable=False) data_venda = Column(Date, nullable=False)
militante = relationship("Militante", back_populates="vendas_jornais") militante = relationship("Militante", back_populates="vendas_jornais_avulsos")
class AssinaturaAnual(Base): class AssinaturaAnual(Base):
__tablename__ = 'assinaturas_anuais' __tablename__ = 'assinaturas_anuais'
@@ -525,11 +527,6 @@ class Usuario(Base, UserMixin):
issuer_name="Sistema de Controles" issuer_name="Sistema de Controles"
) )
def generate_otp_secret(self):
"""Gera um novo segredo OTP para o usuário"""
self.otp_secret = pyotp.random_base32()
return self.otp_secret
def verify_otp(self, code): def verify_otp(self, code):
"""Verifica se um código OTP é válido""" """Verifica se um código OTP é válido"""
if not self.otp_secret: if not self.otp_secret:
@@ -628,8 +625,51 @@ class TransacaoPIX(Base):
status = Column(String(20)) # Pendente, Pago, Expirado status = Column(String(20)) # Pendente, Pago, Expirado
qr_code = Column(Text) qr_code = Column(Text)
pagamento_id = Column(Integer, ForeignKey('pagamentos.id')) pagamento_id = Column(Integer, ForeignKey('pagamentos.id'))
comprovante_id = Column(Integer, ForeignKey('comprovantes.id'))
pagamento = relationship("Pagamento", back_populates="transacoes_pix") pagamento = relationship("Pagamento", back_populates="transacoes_pix")
comprovante = relationship("Comprovante", back_populates="transacoes_pix")
class TipoComprovante(Base):
__tablename__ = 'tipos_comprovante'
id = Column(Integer, primary_key=True)
descricao = Column(String(50), nullable=False)
valor = Column(Float, nullable=False)
class Comprovante(Base):
__tablename__ = 'comprovantes'
id = Column(Integer, primary_key=True)
militante_id = Column(Integer, ForeignKey('militantes.id'), nullable=False)
tipo_comprovante = Column(String(50)) # Cota, Jornal, Assinatura, etc.
data_comprovante = Column(Date, nullable=False)
militante = relationship("Militante", back_populates="comprovantes")
transacoes_pix = relationship("TransacaoPIX", back_populates="comprovante")
class VendaJornal(Base):
__tablename__ = 'vendas_jornais'
id = Column(Integer, primary_key=True, autoincrement=True)
militante_id = Column(Integer, ForeignKey('militantes.id'))
quantidade = Column(Integer, nullable=False)
valor_total = Column(Numeric(10, 2), nullable=False)
data_venda = Column(Date, nullable=False)
militante = relationship("Militante", back_populates="vendas_jornais")
class AssinaturaJornal(Base):
__tablename__ = 'assinaturas_jornais'
id = Column(Integer, primary_key=True, autoincrement=True)
militante_id = Column(Integer, ForeignKey('militantes.id'))
tipo_material_id = Column(Integer, ForeignKey('tipos_materiais.id'))
quantidade = Column(Integer, nullable=False)
valor_total = Column(Numeric(10, 2), nullable=False)
data_inicio = Column(Date, nullable=False)
data_fim = Column(Date, nullable=False)
militante = relationship("Militante", back_populates="assinaturas")
tipo_material = relationship("TipoMaterial", back_populates="assinaturas")
def init_database(): def init_database():
"""Inicializa o banco de dados com dados básicos""" """Inicializa o banco de dados com dados básicos"""
@@ -671,8 +711,8 @@ def init_database():
session.commit() session.commit()
# Gerar OTP para admin # Gerar OTP para admin
admin_otp_secret = os.environ.get('ADMIN_OTP_SECRET') or pyotp.random_base32() admin_otp_secret = pyotp.random_base32()
print(f"OTP do admin: {admin_otp_secret}") print(f"Novo OTP gerado: {admin_otp_secret}")
# Criar usuário admin # Criar usuário admin
admin_role = session.query(Role).filter_by(nome="Administrador").first() admin_role = session.query(Role).filter_by(nome="Administrador").first()
@@ -700,33 +740,14 @@ def init_database():
qr.add_data(provisioning_uri) qr.add_data(provisioning_uri)
qr.make(fit=True) qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white") img = qr.make_image(fill_color="black", back_color="white")
img.save('admin_qr.png')
# Tentar salvar em diferentes locais
qr_paths = ['/tmp/admin_qr.png', 'admin_qr.png', '/app/admin_qr.png']
qr_saved = False
for qr_path in qr_paths:
try:
img.save(qr_path)
print(f"QR code salvo em {qr_path}")
qr_saved = True
break
except Exception as e:
print(f"Não foi possível salvar o QR code em {qr_path}: {e}")
continue
if not qr_saved:
print("AVISO: Não foi possível salvar o QR code em nenhum local")
print("O QR code pode ser gerado manualmente usando o URI OTP")
print("=== Usuário Admin Criado ===") print("=== Usuário Admin Criado ===")
print(f"Username: admin") print(f"Username: admin")
print(f"Senha: admin123") print(f"Senha: admin123")
print(f"Email: {admin.email}") print(f"Email: {admin.email}")
print(f"OTP Secret: {admin_otp_secret}") print(f"OTP Secret: {admin_otp_secret}")
if qr_saved: print(f"QR Code: admin_qr.png")
print(f"QR Code: {qr_path}")
print(f"URI OTP: {provisioning_uri}")
# Importar e executar o seed após criar todas as dependências # Importar e executar o seed após criar todas as dependências
from seed_data import seed_database from seed_data import seed_database

View File

@@ -11,10 +11,38 @@ def require_login(f):
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
if not current_user.is_authenticated: if not current_user.is_authenticated:
flash('Por favor, faça login para acessar esta página.', 'danger') flash('Por favor, faça login para acessar esta página.', 'danger')
return redirect(url_for('auth.login')) return redirect(url_for('login'))
# Executar a função diretamente sem try/catch db = get_db_connection()
return f(*args, **kwargs) try:
# Carregar o usuário com suas roles e permissões
user = db.query(Usuario).options(
joinedload(Usuario.roles).joinedload(Role.permissions),
joinedload(Usuario.militante),
joinedload(Usuario.cr),
joinedload(Usuario.setor),
joinedload(Usuario.celula)
).get(current_user.id)
if not user:
flash('Usuário não encontrado.', 'danger')
return redirect(url_for('login'))
# Atualiza timestamp da última atividade
user.update_last_activity()
db.commit()
# Substituir o current_user pelo usuário carregado
setattr(current_user, '_get_current_object', lambda: user)
# Executar a função com o usuário carregado
return f(*args, **kwargs)
except Exception as e:
db.rollback()
flash('Erro ao carregar dados do usuário.', 'danger')
return redirect(url_for('login'))
finally:
db.close()
return decorated_function return decorated_function
def require_permission(permission_name): def require_permission(permission_name):
@@ -24,7 +52,7 @@ def require_permission(permission_name):
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
if not current_user.is_authenticated: if not current_user.is_authenticated:
flash('Você precisa estar logado para acessar esta página.', 'error') flash('Você precisa estar logado para acessar esta página.', 'error')
return redirect(url_for('auth.login')) return redirect(url_for('login'))
db = get_db_connection() db = get_db_connection()
try: try:
@@ -39,7 +67,7 @@ def require_permission(permission_name):
if not user: if not user:
flash('Usuário não encontrado.', 'error') flash('Usuário não encontrado.', 'error')
return redirect(url_for('auth.login')) return redirect(url_for('login'))
if not user.has_permission(permission_name): if not user.has_permission(permission_name):
flash('Você não tem permissão para acessar esta página.', 'error') flash('Você não tem permissão para acessar esta página.', 'error')
@@ -65,7 +93,7 @@ def require_role(role_name):
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
if not current_user.is_authenticated: if not current_user.is_authenticated:
flash('Você precisa estar logado para acessar esta página.', 'error') flash('Você precisa estar logado para acessar esta página.', 'error')
return redirect(url_for('auth.login')) return redirect(url_for('login'))
db = get_db_connection() db = get_db_connection()
try: try:
@@ -91,14 +119,14 @@ def require_minimum_role(min_level):
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
if not current_user.is_authenticated: if not current_user.is_authenticated:
flash('Você precisa estar logado para acessar esta página.', 'error') flash('Você precisa estar logado para acessar esta página.', 'error')
return redirect(url_for('auth.login')) return redirect(url_for('login'))
db = get_db_connection() db = get_db_connection()
try: try:
user = db.query(Usuario).get(current_user.id) user = db.query(Usuario).get(current_user.id)
if not user: if not user:
flash('Usuário não encontrado.', 'error') flash('Usuário não encontrado.', 'error')
return redirect(url_for('auth.login')) return redirect(url_for('login'))
highest_role = user.get_highest_role() highest_role = user.get_highest_role()
if not highest_role or highest_role.nivel < min_level: if not highest_role or highest_role.nivel < min_level:
@@ -122,7 +150,7 @@ def require_instance_permission(permission_name, instance_param):
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
if not current_user.is_authenticated: if not current_user.is_authenticated:
flash('Por favor, faça login para acessar esta página.', 'error') flash('Por favor, faça login para acessar esta página.', 'error')
return redirect(url_for('auth.login')) return redirect(url_for('login'))
# Obtém o ID da instância dos argumentos da função # Obtém o ID da instância dos argumentos da função
instance_id = kwargs.get(instance_param) instance_id = kwargs.get(instance_param)
@@ -145,7 +173,7 @@ def require_instance_access(instance_type, instance_id):
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
if not current_user.is_authenticated: if not current_user.is_authenticated:
flash('Por favor, faça login para acessar esta página.', 'error') flash('Por favor, faça login para acessar esta página.', 'error')
return redirect(url_for('auth.login')) return redirect(url_for('login'))
# Verificar acesso baseado na instância do usuário # Verificar acesso baseado na instância do usuário
if instance_type == 'celula': if instance_type == 'celula':

View File

@@ -1,53 +0,0 @@
from flask import g, request
from flask_login import current_user
def permission_context_processor():
"""Context processor simples que disponibiliza informações básicas do usuário"""
context = {
'user_can': lambda permission: True, # Sempre True - controle é no nível de dados
'user_has_role': lambda role: True, # Sempre True - controle é no nível de dados
'is_admin': False,
'current_user_data': None
}
if current_user.is_authenticated:
context.update({
'is_admin': getattr(current_user, 'is_admin', False),
'current_user_data': current_user
})
return context
def safe_render_helper():
"""Helper que fornece dados seguros para templates"""
return {
'safe_data': lambda data, default=None: data if data is not None else (default or [])
}
def init_template_filters(app):
"""Inicializa filtros de template personalizados"""
@app.template_filter('safe_list')
def safe_list_filter(value):
"""Garante que o valor seja sempre uma lista"""
if value is None:
return []
if isinstance(value, list):
return value
return [value]
@app.template_filter('safe_dict')
def safe_dict_filter(value):
"""Garante que o valor seja sempre um dicionário"""
if value is None:
return {}
if isinstance(value, dict):
return value
return {}
@app.template_filter('safe_str')
def safe_str_filter(value):
"""Garante que o valor seja sempre uma string"""
if value is None:
return ""
return str(value)

23
functions/usuario.py Normal file
View File

@@ -0,0 +1,23 @@
def get_permissoes_por_cargo(cargo_id):
permissoes = {
1: [ # Secretário Geral
'gerenciar_relatorios_celula',
'visualizar_relatorios_celula',
'gerenciar_militantes',
'gerenciar_tipos_comprovante'
],
2: [ # Admin
'gerenciar_relatorios_celula',
'visualizar_relatorios_celula',
'gerenciar_militantes',
'gerenciar_tipos_comprovante'
],
3: [ # Secretário Financeiro do Comitê Central
'gerenciar_relatorios_celula',
'visualizar_relatorios_celula',
'gerenciar_militantes',
'gerenciar_tipos_comprovante'
],
# ... existing code ...
}
return permissoes.get(cargo_id, [])

View File

@@ -1 +0,0 @@
# Models package

View File

@@ -1,4 +0,0 @@
from models.entities.assinatura_jornal import AssinaturaJornal as AssinaturaAnual
# This file is a compatibility layer for code that uses AssinaturaAnual
# The class has been renamed to AssinaturaJornal

View File

@@ -0,0 +1,18 @@
from sqlalchemy import Column, Integer, String, ForeignKey, Numeric, Date
from sqlalchemy.orm import relationship
from models.entities.base import Base
class AssinaturaJornal(Base):
__tablename__ = 'assinaturas_jornais'
id = Column(Integer, primary_key=True, autoincrement=True)
militante_id = Column(Integer, ForeignKey('militantes.id'))
tipo_material_id = Column(Integer, ForeignKey('tipos_materiais.id'))
quantidade = Column(Integer, nullable=False)
valor_total = Column(Numeric(10, 2), nullable=False)
data_inicio = Column(Date, nullable=False)
data_fim = Column(Date, nullable=False)
militante = relationship("Militante", back_populates="assinaturas", foreign_keys=[militante_id])
tipo_material = relationship("TipoMaterial", back_populates="assinaturas")

17
models/entities/base.py Normal file
View File

@@ -0,0 +1,17 @@
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text, Numeric, Date, Enum, create_engine, text
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, relationship, backref
from pathlib import Path
import os
# Configurar caminho do banco de dados
db_dir = Path.home() / '.local' / 'share' / 'controles'
db_dir.mkdir(parents=True, exist_ok=True)
db_path = db_dir / 'database.db'
DATABASE_URL = f"sqlite:///{db_path}"
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# Base SQLAlchemy
Base = declarative_base()

View File

@@ -1,24 +0,0 @@
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
from models.entities.base import Base
class Celula(Base):
__tablename__ = 'celulas'
id = Column(Integer, primary_key=True, autoincrement=True)
nome = Column(String(100), nullable=False)
setor_id = Column(Integer, ForeignKey('setores.id', use_alter=True, name='fk_celula_setor'))
cr_id = Column(Integer, ForeignKey('comites_regionais.id', use_alter=True, name='fk_celula_cr'))
secretario = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_celula_secretario'))
responsavel_financas = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_celula_responsavel_financas'))
quadro_orientador = Column(String(255))
# Relacionamentos
setor = relationship("Setor", back_populates="celulas")
cr = relationship("ComiteRegional", back_populates="celulas")
militantes = relationship("Militante", back_populates="celula", foreign_keys="[Militante.celula_id]")
secretario_rel = relationship("Militante", foreign_keys=[secretario])
responsavel_financas_rel = relationship("Militante", foreign_keys=[responsavel_financas])
pagamentos = relationship("PagamentoCelula", back_populates="celula")
usuarios = relationship("Usuario", back_populates="celula")

View File

@@ -0,0 +1,15 @@
from sqlalchemy import Column, Integer, String, ForeignKey, Numeric, Date
from sqlalchemy.orm import relationship
from models.entities.base import Base
class Comprovante(Base):
__tablename__ = 'comprovantes'
id = Column(Integer, primary_key=True, autoincrement=True)
militante_id = Column(Integer, ForeignKey('militantes.id'), nullable=False)
tipo_comprovante = Column(String(50)) # Cota, Jornal, Assinatura, etc.
data_comprovante = Column(Date, nullable=False)
militante = relationship("Militante", back_populates="comprovantes")
transacoes_pix = relationship("TransacaoPIX", back_populates="comprovante")

View File

@@ -0,0 +1,17 @@
from sqlalchemy import Column, Integer, String, ForeignKey, Numeric, Date, Boolean
from sqlalchemy.orm import relationship
from models.entities.base import Base
class CotaMensal(Base):
__tablename__ = 'cotas_mensais'
id = Column(Integer, primary_key=True, autoincrement=True)
militante_id = Column(Integer, ForeignKey('militantes.id'))
valor_antigo = Column(Numeric(10, 2), nullable=False)
valor_novo = Column(Numeric(10, 2), nullable=False)
data_alteracao = Column(Date, nullable=False)
data_vencimento = Column(Date, nullable=False)
pago = Column(Boolean, default=False)
militante = relationship("Militante", back_populates="cotas_mensais")

View File

@@ -0,0 +1,14 @@
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
from models.entities.base import Base
class EmailMilitante(Base):
__tablename__ = 'emails_militantes'
id = Column(Integer, primary_key=True, autoincrement=True)
militante_id = Column(Integer, ForeignKey('militantes.id'))
endereco_email = Column(String(100))
# Relacionamentos
militante = relationship("Militante", back_populates="emails")

View File

@@ -0,0 +1,19 @@
from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import relationship
from models.entities.base import Base
class Endereco(Base):
__tablename__ = 'enderecos'
id = Column(Integer, primary_key=True, autoincrement=True)
estado = Column(String(2))
cidade = Column(String(50))
bairro = Column(String(50))
rua = Column(String(100))
numero = Column(String(10))
complemento = Column(String(50))
cep = Column(String(9))
# Relacionamentos
militantes = relationship("Militante", back_populates="endereco")

View File

@@ -0,0 +1,17 @@
from sqlalchemy import Column, Integer, String, ForeignKey, Numeric, Date
from sqlalchemy.orm import relationship
from models.entities.base import Base
class MaterialVendido(Base):
__tablename__ = 'materiais_vendidos'
id = Column(Integer, primary_key=True, autoincrement=True)
militante_id = Column(Integer, ForeignKey('militantes.id'))
tipo_material_id = Column(Integer, ForeignKey('tipos_materiais.id'))
descricao = Column(String(255), nullable=False)
valor = Column(Numeric(10, 2), nullable=False)
data_venda = Column(Date, nullable=False)
militante = relationship("Militante", back_populates="materiais_vendidos")
tipo_material = relationship("TipoMaterial", back_populates="materiais_vendidos")

View File

@@ -0,0 +1,155 @@
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Date, Text, Enum
from sqlalchemy.orm import relationship
from datetime import datetime
import enum
import secrets
from models.entities.base import Base
class EstadoMilitante(enum.Enum):
ATIVO = 'ativo'
DESLIGADO = 'desligado'
SUSPENSO = 'suspenso'
AFASTADO = 'afastado'
class Militante(Base):
__tablename__ = 'militantes'
id = Column(Integer, primary_key=True, autoincrement=True)
nome = Column(String(100), nullable=False)
cpf = Column(String(14), unique=True)
# Novos campos básicos
titulo_eleitoral = Column(String(20))
data_nascimento = Column(Date)
data_entrada_oci = Column(Date)
data_efetivacao_oci = Column(Date)
# Campos de contato
telefone1 = Column(String(15))
telefone2 = Column(String(15))
# Relacionamento para múltiplos emails
emails = relationship("EmailMilitante", back_populates="militante")
# Endereço
endereco_id = Column(Integer, ForeignKey('enderecos.id', use_alter=True, name='fk_militante_endereco'))
endereco = relationship("Endereco", back_populates="militantes")
# Redes sociais
redes_sociais = relationship("RedeSocial", back_populates="militante")
# Campos profissionais
profissao = Column(String(100))
regime_trabalho = Column(String(50)) # CLT, Estatutário, etc.
empresa = Column(String(100))
contratante = Column(String(100)) # Para terceirizados
# Campos acadêmicos
instituicao_ensino = Column(String(100))
tipo_instituicao = Column(String(20)) # Federal, Estadual, etc.
# Campos sindicais
sindicato = Column(String(100))
cargo_sindical = Column(String(50))
dirigente_sindical = Column(Boolean)
central_sindical = Column(String(100))
# Responsável pelo cadastro
registrado_por = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_militante_registrado_por'))
# Campos existentes
celula_id = Column(Integer, ForeignKey('celulas.id', use_alter=True, name='fk_militante_celula'))
responsabilidades = Column(Integer, default=0)
otp_secret = Column(String(32))
temp_token = Column(String(64))
temp_token_expiry = Column(DateTime)
# Novo campo para Quadro-Orientador
quadro_orientador = Column(Boolean, default=False)
# Campos para Aspirante
aspirante = Column(Boolean, default=True) # Por padrão, todo novo militante é aspirante
data_inicio_aspirante = Column(DateTime, default=datetime.utcnow)
avaliacao_aspirante = Column(Text)
data_avaliacao_aspirante = Column(DateTime)
# Campos para estado do militante
estado = Column(Enum(EstadoMilitante), default=EstadoMilitante.ATIVO)
data_desligamento = Column(DateTime)
motivo_desligamento = Column(Text)
# Relacionamentos existentes
cotas_mensais = relationship("CotaMensal", back_populates="militante")
pagamentos = relationship("Pagamento", back_populates="militante")
materiais_vendidos = relationship("MaterialVendido", back_populates="militante")
vendas_jornais_avulsos = relationship("VendaJornalAvulso", back_populates="militante")
vendas_jornais = relationship("VendaJornal", back_populates="militante", foreign_keys="[VendaJornal.militante_id]")
assinaturas = relationship("AssinaturaJornal", back_populates="militante", foreign_keys="[AssinaturaJornal.militante_id]")
celula = relationship("Celula", back_populates="militantes", foreign_keys=[celula_id])
comprovantes = relationship("Comprovante", back_populates="militante")
# Constantes para responsabilidades
SECRETARIO = 1
TESOUREIRO = 2
IMPRENSA = 4
MNS = 8
MPS = 16
JUVENTUDE = 32
QUADRO_ORIENTADOR = 64
ASPIRANTE = 128
RESPONSAVEL_FINANCAS = 256
RESPONSAVEL_IMPRENSA = 512
@staticmethod
def get_responsabilidades_list():
return [
(Militante.SECRETARIO, "Secretário"),
(Militante.TESOUREIRO, "Tesoureiro"),
(Militante.IMPRENSA, "Imprensa"),
(Militante.MNS, "MNS"),
(Militante.MPS, "MPS"),
(Militante.JUVENTUDE, "Juventude"),
(Militante.QUADRO_ORIENTADOR, "Quadro-Orientador"),
(Militante.ASPIRANTE, "Aspirante"),
(Militante.RESPONSAVEL_FINANCAS, "Responsável de Finanças"),
(Militante.RESPONSAVEL_IMPRENSA, "Responsável de Imprensa")
]
def set_responsabilidades(self, resp_list):
"""
Define as responsabilidades do militante
resp_list: lista de inteiros representando as responsabilidades
"""
self.responsabilidades = sum(resp_list)
def get_responsabilidades(self):
"""
Retorna lista de responsabilidades ativas
"""
resp = []
for valor, nome in self.get_responsabilidades_list():
if self.responsabilidades & valor:
resp.append(nome)
return resp
def generate_temp_token(self):
"""
Gera um token temporário para acesso ao QR code
"""
self.temp_token = secrets.token_urlsafe(32)
self.temp_token_expiry = datetime.now() + timedelta(hours=48)
return self.temp_token
def generate_username(self):
"""Gera um nome de usuário único baseado no primeiro nome e um código"""
from sqlalchemy import func
from services.database_service import DatabaseService
db = DatabaseService.get_db_connection()
try:
# Pega o primeiro nome
primeiro_nome = self.nome.split()[0].lower()
# Importação local para evitar dependência circular
from models.entities.usuario import Usuario
# Conta quantos usuários já existem com esse prefixo
count = db.query(func.count(Usuario.id)).filter(
Usuario.username.like(f"{primeiro_nome}%")
).scalar()
# Gera o código (número sequencial)
codigo = str(count + 1).zfill(3)
return f"{primeiro_nome}{codigo}"
finally:
db.close()

View File

@@ -0,0 +1,21 @@
from sqlalchemy import Column, Integer, String, ForeignKey, Numeric, Date
from sqlalchemy.orm import relationship
from models.entities.base import Base
class Pagamento(Base):
__tablename__ = 'pagamentos'
id = Column(Integer, primary_key=True, autoincrement=True)
militante_id = Column(Integer, ForeignKey('militantes.id'))
tipo_pagamento = Column(String(50)) # Cota, Jornal, Assinatura, etc.
mes_referencia = Column(Date)
numero_jornal = Column(String(20))
numero_inicial_assinatura = Column(String(20))
numero_final_assinatura = Column(String(20))
campanha_financeira = Column(String(50))
valor = Column(Numeric(10, 2), nullable=False)
data_pagamento = Column(Date, nullable=False)
militante = relationship("Militante", back_populates="pagamentos")
transacoes_pix = relationship("TransacaoPIX", back_populates="pagamento")

View File

@@ -0,0 +1,15 @@
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
from models.entities.base import Base
class RedeSocial(Base):
__tablename__ = 'redes_sociais'
id = Column(Integer, primary_key=True, autoincrement=True)
militante_id = Column(Integer, ForeignKey('militantes.id'))
tipo = Column(String(20)) # Instagram, TikTok, Discord, etc.
identificador = Column(String(100))
# Relacionamentos
militante = relationship("Militante", back_populates="redes_sociais")

View File

@@ -0,0 +1,13 @@
from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import relationship
from models.entities.base import Base
class TipoMaterial(Base):
__tablename__ = 'tipos_materiais'
id = Column(Integer, primary_key=True, autoincrement=True)
descricao = Column(String(100), nullable=False)
materiais_vendidos = relationship("MaterialVendido", back_populates="tipo_material")
assinaturas = relationship("AssinaturaJornal", back_populates="tipo_material")

126
models/entities/usuario.py Normal file
View File

@@ -0,0 +1,126 @@
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey
from sqlalchemy.orm import relationship, backref
from werkzeug.security import generate_password_hash, check_password_hash
from datetime import datetime, timedelta
from flask_login import UserMixin
import pyotp
from models.entities.base import Base
from models.entities.militante import Militante
class TipoUsuario(enum.Enum):
ADMIN = "admin"
CR_RESPONSAVEL = "cr_responsavel"
SETOR_RESPONSAVEL = "setor_responsavel"
USUARIO = "usuario"
class Usuario(Base, UserMixin):
__tablename__ = 'usuarios'
id = Column(Integer, primary_key=True)
username = Column(String(50), unique=True, nullable=False)
password_hash = Column(String(255), nullable=False)
email = Column(String(100), unique=True, nullable=False)
nome = Column(String(100)) # Nome completo do usuário
otp_secret = Column(String(32))
role_id = Column(Integer, ForeignKey('roles.id'))
setor_id = Column(Integer, ForeignKey('setores.id'))
ativo = Column(Boolean, default=True)
is_admin = Column(Boolean, default=False)
ultimo_login = Column(DateTime)
ultimo_logout = Column(DateTime)
motivo_logout = Column(String(100))
cr_id = Column(Integer, ForeignKey('comites_regionais.id'))
celula_id = Column(Integer, ForeignKey('celulas.id'))
session_timeout = Column(Integer, default=30)
tipo = Column(String(17), nullable=False)
ultima_atividade = Column(DateTime, default=datetime.utcnow)
# Relacionamento com militante
militante_id = Column(Integer, ForeignKey('militantes.id'))
# Relacionamentos
roles = relationship("Role", secondary="user_roles", back_populates="users")
setor = relationship('Setor', back_populates='usuarios')
cr = relationship('ComiteRegional', back_populates='usuarios')
celula = relationship('Celula', back_populates='usuarios')
militante = relationship("Militante", backref=backref("usuario", uselist=False))
def __init__(self, username, email=None, is_admin=False, nome=None):
self.username = username
self.email = email
self.is_admin = is_admin
self.nome = nome
self.ativo = True
self.session_timeout = 30
self.tipo = "USUARIO"
self.ultima_atividade = datetime.utcnow()
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
def update_last_activity(self):
self.ultima_atividade = datetime.utcnow()
def is_session_expired(self):
if not self.ultima_atividade:
return True
time_diff = datetime.utcnow() - self.ultima_atividade
return time_diff.total_seconds() > (self.session_timeout * 60)
def check_session_timeout(self):
"""Verifica se a sessão do usuário expirou"""
if not self.ultima_atividade:
return True
time_diff = datetime.utcnow() - self.ultima_atividade
return time_diff.total_seconds() > (self.session_timeout * 60)
def has_permission(self, permission_name):
"""Verifica se o usuário tem uma permissão específica"""
if self.is_admin: # Se for admin, tem todas as permissões
return True
# Verifica se o usuário tem a permissão através de suas roles
for role in self.roles:
for permission in role.permissions:
if permission.nome == permission_name:
return True
return False
def has_role(self, role_nivel):
"""Verifica se o usuário tem um determinado nível de role"""
for role in self.roles:
if role.nivel == role_nivel:
return True
return False
def get_otp_uri(self):
"""Gera a URI para autenticação em duas etapas"""
if not self.otp_secret:
self.otp_secret = pyotp.random_base32()
return pyotp.totp.TOTP(self.otp_secret).provisioning_uri(
self.username,
issuer_name="Sistema de Controles"
)
def verify_otp(self, code):
"""Verifica se um código OTP é válido"""
if not self.otp_secret:
print(f"Erro: OTP secret não configurado para o usuário {self.username}")
return False
totp = pyotp.totp.TOTP(self.otp_secret)
is_valid = totp.verify(code)
return is_valid
def logout(self):
"""Registra o logout do usuário"""
self.ultimo_logout = datetime.utcnow()
self.motivo_logout = "Logout manual"
self.ultima_atividade = None
def is_admin_user(self):
"""Verifica se o usuário é admin"""
return self.is_admin or any(role.nome == "admin" for role in self.roles)

View File

@@ -0,0 +1,15 @@
from sqlalchemy import Column, Integer, String, ForeignKey, Numeric, Date
from sqlalchemy.orm import relationship
from models.entities.base import Base
class VendaJornal(Base):
__tablename__ = 'vendas_jornais'
id = Column(Integer, primary_key=True, autoincrement=True)
militante_id = Column(Integer, ForeignKey('militantes.id'))
quantidade = Column(Integer, nullable=False)
valor_total = Column(Numeric(10, 2), nullable=False)
data_venda = Column(Date, nullable=False)
militante = relationship("Militante", back_populates="vendas_jornais", foreign_keys=[militante_id])

View File

@@ -0,0 +1,15 @@
from sqlalchemy import Column, Integer, String, ForeignKey, Numeric, Date
from sqlalchemy.orm import relationship
from models.entities.base import Base
class VendaJornalAvulso(Base):
__tablename__ = 'vendas_jornais_avulsos'
id = Column(Integer, primary_key=True, autoincrement=True)
militante_id = Column(Integer, ForeignKey('militantes.id'))
quantidade = Column(Integer, nullable=False)
valor_total = Column(Numeric(10, 2), nullable=False)
data_venda = Column(Date, nullable=False)
militante = relationship("Militante", back_populates="vendas_jornais_avulsos")

View File

@@ -1,328 +0,0 @@
from functions.database import get_db_connection, Militante, EmailMilitante, Endereco
from sqlalchemy.orm import joinedload
from datetime import datetime
from typing import List, Dict, Optional
from services.cache_service import cache_service, cached, CacheKeys, invalidate_cache_pattern
import logging
logger = logging.getLogger(__name__)
class MilitanteModel:
"""Model para operações com militantes"""
@staticmethod
@invalidate_cache_pattern("militantes:*")
def criar_militante(data: Dict) -> Dict:
"""Cria um novo militante"""
db = get_db_connection()
try:
# Criar endereço se fornecido
endereco_id = None
if data.get('endereco'):
endereco = Endereco(**data['endereco'])
db.add(endereco)
db.flush()
endereco_id = endereco.id
# Criar militante
militante = Militante(
nome=data['nome'],
cpf=data['cpf'],
titulo_eleitoral=data.get('titulo_eleitoral'),
data_nascimento=data.get('data_nascimento'),
data_entrada_oci=data.get('data_entrada_oci'),
data_efetivacao_oci=data.get('data_efetivacao_oci'),
telefone1=data.get('telefone1'),
telefone2=data.get('telefone2'),
profissao=data.get('profissao'),
regime_trabalho=data.get('regime_trabalho'),
empresa=data.get('empresa'),
contratante=data.get('contratante'),
instituicao_ensino=data.get('instituicao_ensino'),
tipo_instituicao=data.get('tipo_instituicao'),
sindicato=data.get('sindicato'),
cargo_sindical=data.get('cargo_sindical'),
dirigente_sindical=data.get('dirigente_sindical', False),
central_sindical=data.get('central_sindical'),
endereco_id=endereco_id,
celula_id=data.get('celula_id'),
registrado_por=data.get('registrado_por')
)
db.add(militante)
db.flush()
# Criar email se fornecido
if data.get('email'):
email = EmailMilitante(
militante_id=militante.id,
endereco_email=data['email']
)
db.add(email)
db.commit()
# Cache the new militante
cache_key = CacheKeys.militante_detail(militante.id)
militante_data = MilitanteModel.formatar_dados_militante(militante)
cache_service.set(cache_key, militante_data, 1800) # 30 minutes
logger.info(f"Militante {militante.id} criado e cacheado")
return {
'status': 'success',
'message': 'Militante criado com sucesso',
'militante_id': militante.id
}
except Exception as e:
db.rollback()
logger.error(f"Erro ao criar militante: {e}")
return {
'status': 'error',
'message': f'Erro ao criar militante: {str(e)}'
}
finally:
db.close()
@staticmethod
@cached(expire=1800, key_prefix="militantes") # Cache for 30 minutes
def listar_militantes() -> List[Militante]:
"""Lista todos os militantes"""
db = get_db_connection()
try:
militantes = db.query(Militante).options(
joinedload(Militante.emails),
joinedload(Militante.endereco),
joinedload(Militante.celula)
).order_by(Militante.nome).all()
# Cache individual militantes
for militante in militantes:
cache_key = CacheKeys.militante_detail(militante.id)
militante_data = MilitanteModel.formatar_dados_militante(militante)
cache_service.set(cache_key, militante_data, 1800)
logger.debug(f"Listados {len(militantes)} militantes e cacheados")
return militantes
except Exception as e:
logger.error(f"Erro ao listar militantes: {e}")
return []
finally:
db.close()
@staticmethod
def buscar_por_id(militante_id: int) -> Optional[Militante]:
"""Busca um militante por ID"""
# Try cache first
cache_key = CacheKeys.militante_detail(militante_id)
cached_militante = cache_service.get(cache_key)
if cached_militante:
logger.debug(f"Cache hit para militante {militante_id}")
# Convert cached data back to Militante object if needed
return cached_militante
# Cache miss, get from database
db = get_db_connection()
try:
militante = db.query(Militante).options(
joinedload(Militante.emails),
joinedload(Militante.endereco)
).get(militante_id)
if militante:
# Cache the militante
militante_data = MilitanteModel.formatar_dados_militante(militante)
cache_service.set(cache_key, militante_data, 1800)
logger.debug(f"Cache miss para militante {militante_id}, cacheado")
return militante
except Exception as e:
logger.error(f"Erro ao buscar militante {militante_id}: {e}")
return None
finally:
db.close()
@staticmethod
@invalidate_cache_pattern("militantes:*")
def atualizar_militante(militante_id: int, data: Dict) -> Dict:
"""Atualiza um militante existente"""
db = get_db_connection()
try:
militante = db.query(Militante).get(militante_id)
if not militante:
return {
'status': 'error',
'message': 'Militante não encontrado'
}
# Atualizar dados básicos
militante.nome = data.get('nome', militante.nome)
militante.cpf = data.get('cpf', militante.cpf)
militante.titulo_eleitoral = data.get('titulo_eleitoral', militante.titulo_eleitoral)
militante.telefone1 = data.get('telefone1', militante.telefone1)
militante.telefone2 = data.get('telefone2', militante.telefone2)
militante.profissao = data.get('profissao', militante.profissao)
militante.regime_trabalho = data.get('regime_trabalho', militante.regime_trabalho)
militante.empresa = data.get('empresa', militante.empresa)
militante.contratante = data.get('contratante', militante.contratante)
militante.instituicao_ensino = data.get('instituicao_ensino', militante.instituicao_ensino)
militante.tipo_instituicao = data.get('tipo_instituicao', militante.tipo_instituicao)
militante.sindicato = data.get('sindicato', militante.sindicato)
militante.cargo_sindical = data.get('cargo_sindical', militante.cargo_sindical)
militante.dirigente_sindical = data.get('dirigente_sindical', militante.dirigente_sindical)
militante.central_sindical = data.get('central_sindical', militante.central_sindical)
# Atualizar datas
if data.get('data_nascimento'):
militante.data_nascimento = data['data_nascimento']
if data.get('data_entrada_oci'):
militante.data_entrada_oci = data['data_entrada_oci']
if data.get('data_efetivacao_oci'):
militante.data_efetivacao_oci = data['data_efetivacao_oci']
# Atualizar endereço
if data.get('endereco') and militante.endereco:
endereco = militante.endereco
endereco.cep = data['endereco'].get('cep', endereco.cep)
endereco.estado = data['endereco'].get('estado', endereco.estado)
endereco.cidade = data['endereco'].get('cidade', endereco.cidade)
endereco.bairro = data['endereco'].get('bairro', endereco.bairro)
endereco.rua = data['endereco'].get('rua', endereco.rua)
endereco.numero = data['endereco'].get('numero', endereco.numero)
endereco.complemento = data['endereco'].get('complemento', endereco.complemento)
# Atualizar email
if data.get('email') and militante.emails:
militante.emails[0].endereco_email = data['email']
db.commit()
# Update cache
cache_key = CacheKeys.militante_detail(militante_id)
militante_data = MilitanteModel.formatar_dados_militante(militante)
cache_service.set(cache_key, militante_data, 1800)
logger.info(f"Militante {militante_id} atualizado e cache atualizado")
return {
'status': 'success',
'message': 'Militante atualizado com sucesso'
}
except Exception as e:
db.rollback()
logger.error(f"Erro ao atualizar militante {militante_id}: {e}")
return {
'status': 'error',
'message': f'Erro ao atualizar militante: {str(e)}'
}
finally:
db.close()
@staticmethod
@invalidate_cache_pattern("militantes:*")
def excluir_militante(militante_id: int) -> Dict:
"""Exclui um militante"""
db = get_db_connection()
try:
militante = db.query(Militante).get(militante_id)
if not militante:
return {
'status': 'error',
'message': 'Militante não encontrado'
}
db.delete(militante)
db.commit()
# Remove from cache
cache_key = CacheKeys.militante_detail(militante_id)
cache_service.delete(cache_key)
logger.info(f"Militante {militante_id} excluído e removido do cache")
return {
'status': 'success',
'message': 'Militante excluído com sucesso'
}
except Exception as e:
db.rollback()
logger.error(f"Erro ao excluir militante {militante_id}: {e}")
return {
'status': 'error',
'message': f'Erro ao excluir militante: {str(e)}'
}
finally:
db.close()
@staticmethod
@cached(expire=1800, key_prefix="militantes")
def buscar_por_cpf(cpf: str) -> Optional[Militante]:
"""Busca um militante por CPF"""
db = get_db_connection()
try:
militante = db.query(Militante).filter_by(cpf=cpf).first()
if militante:
# Cache the militante
cache_key = CacheKeys.militante_detail(militante.id)
militante_data = MilitanteModel.formatar_dados_militante(militante)
cache_service.set(cache_key, militante_data, 1800)
return militante
except Exception as e:
logger.error(f"Erro ao buscar militante por CPF {cpf}: {e}")
return None
finally:
db.close()
@staticmethod
def formatar_dados_militante(militante: Militante) -> Dict:
"""Formata os dados de um militante para retorno JSON"""
def formatar_data_segura(data):
try:
if not data:
return None
return data.strftime('%Y-%m-%d')
except Exception as e:
logger.error(f"Erro ao formatar data: {str(e)}, valor: {data}")
return None
return {
'id': militante.id,
'nome': militante.nome,
'cpf': militante.cpf,
'titulo_eleitoral': militante.titulo_eleitoral,
'data_nascimento': formatar_data_segura(militante.data_nascimento),
'data_entrada_oci': formatar_data_segura(militante.data_entrada_oci),
'data_efetivacao_oci': formatar_data_segura(militante.data_efetivacao_oci),
'telefone1': militante.telefone1,
'telefone2': militante.telefone2,
'profissao': militante.profissao,
'regime_trabalho': militante.regime_trabalho,
'empresa': militante.empresa,
'contratante': militante.contratante,
'instituicao_ensino': militante.instituicao_ensino,
'tipo_instituicao': militante.tipo_instituicao,
'sindicato': militante.sindicato,
'cargo_sindical': militante.cargo_sindical,
'dirigente_sindical': militante.dirigente_sindical,
'central_sindical': militante.central_sindical,
'responsabilidades': militante.responsabilidades,
'estado': militante.estado.value if militante.estado else None,
'celula_id': militante.celula_id,
'email': militante.emails[0].endereco_email if militante.emails else None,
'endereco': {
'cep': militante.endereco.cep if militante.endereco else None,
'estado': militante.endereco.estado if militante.endereco else None,
'cidade': militante.endereco.cidade if militante.endereco else None,
'bairro': militante.endereco.bairro if militante.endereco else None,
'rua': militante.endereco.rua if militante.endereco else None,
'numero': militante.endereco.numero if militante.endereco else None,
'complemento': militante.endereco.complemento if militante.endereco else None
} if militante.endereco else None
}

View File

@@ -1,184 +0,0 @@
from functions.database import get_db_connection, Pagamento, Militante, TipoPagamento
from sqlalchemy.orm import joinedload
from datetime import datetime
from typing import List, Dict, Optional
class PagamentoModel:
"""Model para operações com pagamentos"""
@staticmethod
def criar_pagamento(data: Dict) -> Dict:
"""Cria um novo pagamento"""
db = get_db_connection()
try:
pagamento = Pagamento(
militante_id=data['militante_id'],
tipo_pagamento_id=data.get('tipo_pagamento_id'),
valor=data['valor'],
data_pagamento=data['data_pagamento']
)
db.add(pagamento)
db.commit()
return {
'status': 'success',
'message': 'Pagamento criado com sucesso',
'pagamento_id': pagamento.id
}
except Exception as e:
db.rollback()
return {
'status': 'error',
'message': f'Erro ao criar pagamento: {str(e)}'
}
finally:
db.close()
@staticmethod
def listar_pagamentos() -> List[Pagamento]:
"""Lista todos os pagamentos"""
db = get_db_connection()
try:
return db.query(Pagamento).join(Militante).order_by(Pagamento.data_pagamento.desc()).all()
finally:
db.close()
@staticmethod
def buscar_por_id(pagamento_id: int) -> Optional[Pagamento]:
"""Busca um pagamento por ID"""
db = get_db_connection()
try:
return db.query(Pagamento).get(pagamento_id)
finally:
db.close()
@staticmethod
def atualizar_pagamento(pagamento_id: int, data: Dict) -> Dict:
"""Atualiza um pagamento existente"""
db = get_db_connection()
try:
pagamento = db.query(Pagamento).get(pagamento_id)
if not pagamento:
return {
'status': 'error',
'message': 'Pagamento não encontrado'
}
pagamento.militante_id = data.get('militante_id', pagamento.militante_id)
pagamento.tipo_pagamento_id = data.get('tipo_pagamento_id', pagamento.tipo_pagamento_id)
pagamento.valor = data.get('valor', pagamento.valor)
pagamento.data_pagamento = data.get('data_pagamento', pagamento.data_pagamento)
db.commit()
return {
'status': 'success',
'message': 'Pagamento atualizado com sucesso'
}
except Exception as e:
db.rollback()
return {
'status': 'error',
'message': f'Erro ao atualizar pagamento: {str(e)}'
}
finally:
db.close()
@staticmethod
def excluir_pagamento(pagamento_id: int) -> Dict:
"""Exclui um pagamento"""
db = get_db_connection()
try:
pagamento = db.query(Pagamento).get(pagamento_id)
if not pagamento:
return {
'status': 'error',
'message': 'Pagamento não encontrado'
}
db.delete(pagamento)
db.commit()
return {
'status': 'success',
'message': 'Pagamento excluído com sucesso'
}
except Exception as e:
db.rollback()
return {
'status': 'error',
'message': f'Erro ao excluir pagamento: {str(e)}'
}
finally:
db.close()
@staticmethod
def listar_por_celula(celula_id: int) -> List[Pagamento]:
"""Lista pagamentos de uma célula específica"""
db = get_db_connection()
try:
return db.query(Pagamento).filter_by(celula_id=celula_id).all()
finally:
db.close()
@staticmethod
def listar_por_setor(setor_id: int) -> List[Pagamento]:
"""Lista pagamentos de um setor específico"""
db = get_db_connection()
try:
return db.query(Pagamento).join(Usuario).filter(Usuario.setor_id == setor_id).all()
finally:
db.close()
@staticmethod
def listar_por_cr(cr_id: int) -> List[Pagamento]:
"""Lista pagamentos de um CR específico"""
db = get_db_connection()
try:
return db.query(Pagamento).join(Usuario).filter(Usuario.cr_id == cr_id).all()
finally:
db.close()
@staticmethod
def listar_por_militante(militante_id: int) -> List[Pagamento]:
"""Lista pagamentos de um militante específico"""
db = get_db_connection()
try:
return db.query(Pagamento).filter_by(militante_id=militante_id).order_by(Pagamento.data_pagamento.desc()).all()
finally:
db.close()
@staticmethod
def obter_tipos_pagamento() -> List[TipoPagamento]:
"""Obtém todos os tipos de pagamento"""
db = get_db_connection()
try:
return db.query(TipoPagamento).order_by(TipoPagamento.descricao).all()
finally:
db.close()
@staticmethod
def obter_militantes() -> List[Militante]:
"""Obtém todos os militantes"""
db = get_db_connection()
try:
return db.query(Militante).order_by(Militante.nome).all()
finally:
db.close()
@staticmethod
def formatar_dados_pagamento(pagamento: Pagamento) -> Dict:
"""Formata os dados de um pagamento para retorno JSON"""
return {
'id': pagamento.id,
'militante_id': pagamento.militante_id,
'tipo_pagamento_id': pagamento.tipo_pagamento_id,
'valor': float(pagamento.valor) if pagamento.valor else 0.0,
'data_pagamento': pagamento.data_pagamento.strftime('%Y-%m-%d') if pagamento.data_pagamento else None,
'militante_nome': pagamento.militante.nome if pagamento.militante else None,
'tipo_pagamento_nome': pagamento.tipo_pagamento.descricao if pagamento.tipo_pagamento else None
}

View File

@@ -3,17 +3,17 @@ Flask-SQLAlchemy==3.1.1
Flask-Login==0.6.3 Flask-Login==0.6.3
Flask-WTF==1.2.1 Flask-WTF==1.2.1
Flask-Mail==0.9.1 Flask-Mail==0.9.1
SQLAlchemy>=2.0.36 SQLAlchemy==2.0.27
Werkzeug==3.0.1 Werkzeug==3.0.1
python-dotenv==1.0.1 python-dotenv==1.0.1
pyotp==2.9.0 pyotp==2.9.0
qrcode==7.4.2 qrcode==7.4.2
Pillow>=10.4.0 Pillow==9.5.0
email-validator==2.3.0 email-validator==2.1.0.post1
cryptography==42.0.2 cryptography==42.0.2
bcrypt==4.1.2 bcrypt==4.1.2
Bootstrap-Flask==2.3.3 Bootstrap-Flask==2.3.3
flask-bootstrap5==0.1.dev1
PyJWT==2.8.0 PyJWT==2.8.0
gunicorn==21.2.0 gunicorn==21.2.0
Faker==19.13.0 Faker==19.13.0
redis==5.0.1

View File

@@ -1,6 +1,6 @@
from flask import Blueprint, render_template, flash, redirect, url_for, request, jsonify from flask import Blueprint, render_template, flash, redirect, url_for, request, jsonify
from functions.database import Usuario, get_db_connection from functions.database import Usuario, get_db_connection
from functions.decorators import require_login from functions.decorators import require_permission, require_role, require_minimum_role
from flask_login import login_required, current_user from flask_login import login_required, current_user
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
import pyotp import pyotp
@@ -9,7 +9,10 @@ import secrets
from functools import wraps from functools import wraps
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
import logging import logging
<<<<<<< HEAD
from datetime import datetime from datetime import datetime
=======
>>>>>>> a22b0e4 (refactor(#11): Integra listagem de usuários no dashboard)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -20,7 +23,7 @@ def admin_required(f):
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
if not current_user.is_admin: if not current_user.is_admin:
flash('Acesso não autorizado.', 'danger') flash('Acesso não autorizado.', 'danger')
return redirect(url_for('home.index')) return redirect(url_for('main.index'))
return f(*args, **kwargs) return f(*args, **kwargs)
return decorated_function return decorated_function
@@ -49,8 +52,12 @@ def dashboard():
total_users=total_users, total_users=total_users,
active_users=active_users, active_users=active_users,
inactive_users=inactive_users, inactive_users=inactive_users,
<<<<<<< HEAD
users=users, users=users,
now=now now=now
=======
users=users
>>>>>>> a22b0e4 (refactor(#11): Integra listagem de usuários no dashboard)
) )
except SQLAlchemyError as e: except SQLAlchemyError as e:
logger.error(f"Erro ao buscar dados do dashboard: {str(e)}") logger.error(f"Erro ao buscar dados do dashboard: {str(e)}")
@@ -65,7 +72,7 @@ def dashboard():
@admin_bp.route('/users/<int:user_id>/reset-otp', methods=['POST']) @admin_bp.route('/users/<int:user_id>/reset-otp', methods=['POST'])
@login_required @login_required
@admin_required @require_role('ADMIN')
def reset_user_otp(user_id): def reset_user_otp(user_id):
"""Reseta o OTP de um usuário""" """Reseta o OTP de um usuário"""
db = get_db_connection() db = get_db_connection()
@@ -86,7 +93,7 @@ def reset_user_otp(user_id):
@admin_bp.route('/users/<int:user_id>/reset-password', methods=['POST']) @admin_bp.route('/users/<int:user_id>/reset-password', methods=['POST'])
@login_required @login_required
@admin_required @require_role('ADMIN')
def reset_user_password(user_id): def reset_user_password(user_id):
"""Reseta a senha de um usuário""" """Reseta a senha de um usuário"""
db = get_db_connection() db = get_db_connection()
@@ -108,7 +115,7 @@ def reset_user_password(user_id):
@admin_bp.route('/users/<int:user_id>/toggle-status', methods=['POST']) @admin_bp.route('/users/<int:user_id>/toggle-status', methods=['POST'])
@login_required @login_required
@admin_required @require_role('ADMIN')
def toggle_user_status(user_id): def toggle_user_status(user_id):
"""Ativa/desativa um usuário""" """Ativa/desativa um usuário"""
db = get_db_connection() db = get_db_connection()

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'))

44
scripts/prepare_mvc.sh Executable file
View File

@@ -0,0 +1,44 @@
#!/bin/bash
# Script para preparar a estrutura MVC
echo "Preparando a estrutura MVC para o Sistema de Controles..."
# Criar estrutura de diretórios
echo "Criando estrutura de diretórios..."
mkdir -p models/entities controllers services
# Mover arquivos refatorados
echo "Movendo arquivos refatorados..."
cp app.py.new app.py
# Criar arquivo __init__.py nos diretórios Python
echo "Criando arquivos de inicialização..."
touch models/__init__.py
touch models/entities/__init__.py
touch controllers/__init__.py
touch services/__init__.py
# Criar arquivo __init__.py com importações para models/entities
cat > models/entities/__init__.py << EOF
from models.entities.base import Base
from models.entities.usuario import Usuario, TipoUsuario
from models.entities.militante import Militante, EstadoMilitante
from models.entities.endereco import Endereco
from models.entities.email_militante import EmailMilitante
from models.entities.rede_social import RedeSocial
from models.entities.cota_mensal import CotaMensal
from models.entities.pagamento import Pagamento
from models.entities.tipo_material import TipoMaterial
from models.entities.material_vendido import MaterialVendido
from models.entities.venda_jornal import VendaJornal
from models.entities.venda_jornal_avulso import VendaJornalAvulso
from models.entities.assinatura_jornal import AssinaturaJornal
from models.entities.comprovante import Comprovante
EOF
echo "Todos os arquivos criados com sucesso!"
echo "Para usar a nova estrutura MVC, execute:"
echo "1. chmod +x scripts/prepare_mvc.sh"
echo "2. ./scripts/prepare_mvc.sh"
echo "3. python app.py"

View File

@@ -1,10 +1,11 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from functions.database import ( from functions.database import (
Base, Militante, CotaMensal, TipoPagamento, Pagamento, Base, Militante, CotaMensal, TipoComprovante, Comprovante,
MaterialVendido, TipoMaterial, VendaJornalAvulso, AssinaturaAnual, MaterialVendido, TipoMaterial, VendaJornalAvulso, AssinaturaAnual,
RelatorioCotasMensais, RelatorioVendasMateriais, engine, SessionLocal, RelatorioCotasMensais, RelatorioVendasMateriais, engine, SessionLocal,
Setor, ComiteCentral, Usuario, Role, EmailMilitante, Endereco, Setor, ComiteCentral, Usuario, Role, EmailMilitante, Endereco,
ComiteRegional, Celula, EstadoMilitante ComiteRegional, Celula, EstadoMilitante, get_db_connection,
init_database
) )
import random import random
from faker import Faker from faker import Faker
@@ -54,20 +55,28 @@ def criar_estrutura_organizacional(session):
session.commit() session.commit()
return crs, setores return crs, setores
def criar_tipos_pagamento(session): def criar_tipos_comprovante(session):
"""Cria tipos de pagamento padrão""" """Cria tipos de comprovante padrão"""
print("\nCriando tipos de pagamento...") print("\nCriando tipos de comprovante...")
tipos = [ tipos = [
"Dinheiro", "Comprovante Padrão",
"PIX", "Comprovante Especial",
"Cartão de Crédito", "Comprovante Extraordinário",
"Cartão de Débito", "Jornal Avulso",
"Transferência Bancária" "Assinatura de Jornal",
"Campanha Financeira"
] ]
for tipo in tipos: for tipo in tipos:
if not session.query(TipoPagamento).filter_by(descricao=tipo).first(): if not session.query(TipoComprovante).filter_by(descricao=tipo).first():
session.add(TipoPagamento(descricao=tipo)) session.add(TipoComprovante(descricao=tipo))
session.commit()
try:
session.commit()
print("Tipos de comprovante criados com sucesso!")
except Exception as e:
session.rollback()
print(f"Erro ao criar tipos de comprovante: {e}")
def criar_tipos_material(session): def criar_tipos_material(session):
"""Cria tipos de material padrão""" """Cria tipos de material padrão"""
@@ -211,27 +220,27 @@ def criar_cotas(session, militantes):
print(f"Erro ao criar cotas para militante {militante.nome}: {e}") print(f"Erro ao criar cotas para militante {militante.nome}: {e}")
session.rollback() session.rollback()
def criar_pagamentos(session, militantes): def criar_comprovantes(session, militantes):
"""Cria pagamentos para os militantes""" """Cria comprovantes para os militantes"""
print("\nCriando pagamentos...") print("\nCriando comprovantes...")
tipos_pagamento = session.query(TipoPagamento).all() tipos_comprovante = session.query(TipoComprovante).all()
for militante in militantes: for militante in militantes:
try: try:
# Criar entre 3 e 8 pagamentos por militante # Criar entre 3 e 8 comprovantes por militante
for _ in range(random.randint(3, 8)): for _ in range(random.randint(3, 8)):
tipo = random.choice(tipos_pagamento) tipo = random.choice(tipos_comprovante)
pagamento = Pagamento( comprovante = Comprovante(
militante_id=militante.id, militante_id=militante.id,
tipo_pagamento=tipo.descricao, # Usando a descrição do tipo tipo_comprovante=tipo.descricao, # Usando a descrição do tipo
valor=random.uniform(50, 500), valor=random.uniform(10, 1000),
data_pagamento=fake.date_between(start_date='-1y', end_date='today') data_comprovante=fake.date_between(start_date='-1y', end_date='today')
) )
session.add(pagamento) session.add(comprovante)
session.commit() session.commit()
except Exception as e: except Exception as e:
print(f"Erro ao criar pagamentos para militante {militante.nome}: {e}")
session.rollback() session.rollback()
print(f"Erro ao criar comprovantes para militante {militante.nome}: {e}")
def criar_materiais_vendidos(session, militantes): def criar_materiais_vendidos(session, militantes):
"""Cria registros de materiais vendidos""" """Cria registros de materiais vendidos"""
@@ -302,7 +311,7 @@ def criar_assinaturas(session, militantes):
def seed_database(): def seed_database():
"""Função principal para popular o banco de dados""" """Função principal para popular o banco de dados"""
session = SessionLocal() session = get_db_connection()
try: try:
print("Iniciando população do banco de dados...") print("Iniciando população do banco de dados...")
@@ -310,7 +319,7 @@ def seed_database():
crs, setores = criar_estrutura_organizacional(session) crs, setores = criar_estrutura_organizacional(session)
# Criar tipos básicos # Criar tipos básicos
criar_tipos_pagamento(session) criar_tipos_comprovante(session)
criar_tipos_material(session) criar_tipos_material(session)
# Criar militantes (30 militantes para teste) # Criar militantes (30 militantes para teste)
@@ -318,7 +327,7 @@ def seed_database():
# Criar dados financeiros e materiais # Criar dados financeiros e materiais
criar_cotas(session, militantes) criar_cotas(session, militantes)
criar_pagamentos(session, militantes) criar_comprovantes(session, militantes)
criar_materiais_vendidos(session, militantes) criar_materiais_vendidos(session, militantes)
criar_vendas_jornal(session, militantes) criar_vendas_jornal(session, militantes)
criar_assinaturas(session, militantes) criar_assinaturas(session, militantes)

View File

@@ -1 +0,0 @@
# Services package

View File

@@ -1,157 +0,0 @@
from functions.database import get_db_connection, Usuario
from flask_login import login_user, logout_user
from datetime import datetime
from typing import Dict, Optional
import pyotp
import qrcode
import base64
from io import BytesIO
class AuthService:
"""Service para operações de autenticação"""
@staticmethod
def autenticar_usuario(email_or_username: str, password: str, otp: str = None) -> Dict:
"""Autentica um usuário"""
db = get_db_connection()
try:
# Tenta encontrar o usuário por email ou username
user = db.query(Usuario).filter(
(Usuario.email == email_or_username) |
(Usuario.username == email_or_username)
).first()
if not user or not user.check_password(password):
return {
'status': 'error',
'message': 'Email/usuário ou senha incorretos.'
}
# Verificar OTP se o usuário tiver configurado
if user.otp_secret and not otp:
return {
'status': 'error',
'message': 'Código OTP é obrigatório para sua conta.'
}
if user.otp_secret and not user.verify_otp(otp):
return {
'status': 'error',
'message': 'Código OTP inválido.'
}
# Atualizar último login
user.ultimo_login = datetime.utcnow()
db.commit()
# Fazer login
login_user(user)
return {
'status': 'success',
'user': user
}
except Exception as e:
db.rollback()
return {
'status': 'error',
'message': f'Erro na autenticação: {str(e)}'
}
finally:
db.close()
@staticmethod
def desautenticar_usuario(user) -> Dict:
"""Desautentica um usuário"""
db = get_db_connection()
try:
if user:
user.logout()
db.commit()
logout_user()
return {
'status': 'success',
'message': 'Logout realizado com sucesso!'
}
except Exception as e:
db.rollback()
return {
'status': 'error',
'message': f'Erro no logout: {str(e)}'
}
finally:
db.close()
@staticmethod
def alterar_senha(user_id: int, senha_atual: str, nova_senha: str) -> Dict:
"""Altera a senha de um usuário"""
db = get_db_connection()
try:
user = db.query(Usuario).get(user_id)
if not user:
return {
'status': 'error',
'message': 'Usuário não encontrado.'
}
if not user.check_password(senha_atual):
return {
'status': 'error',
'message': 'Senha atual incorreta.'
}
user.set_password(nova_senha)
db.commit()
return {
'status': 'success',
'message': 'Senha alterada com sucesso!'
}
except Exception as e:
db.rollback()
return {
'status': 'error',
'message': f'Erro ao alterar senha: {str(e)}'
}
finally:
db.close()
@staticmethod
def gerar_qr_code(user) -> str:
"""Gera um QR code para o usuário"""
if not user.otp_secret:
user.otp_secret = pyotp.random_base32()
totp = pyotp.TOTP(user.otp_secret)
qr = qrcode.QRCode(version=1, box_size=10, border=5)
qr.add_data(totp.provisioning_uri(user.email, issuer_name="Sistema de Controles"))
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
buffer = BytesIO()
img.save(buffer, format="PNG")
qr_code = base64.b64encode(buffer.getvalue()).decode('utf-8')
return qr_code
@staticmethod
def verificar_sessao(user) -> Dict:
"""Verifica se a sessão do usuário ainda é válida"""
if not user.is_authenticated:
return {
'valid': False,
'message': 'Usuário não autenticado'
}
if user.is_session_expired():
return {
'valid': False,
'message': 'Sessão expirada'
}
return {
'valid': True
}

View File

@@ -1,268 +0,0 @@
import redis
import json
import pickle
from typing import Any, Optional, Union, Dict, List
from datetime import timedelta
import os
import logging
from functools import wraps
import hashlib
logger = logging.getLogger(__name__)
class CacheService:
"""Service for Redis caching operations"""
def __init__(self, redis_url: str = None):
"""Initialize Redis connection"""
self.redis_url = redis_url or os.getenv('REDIS_URL', 'redis://localhost:6379/0')
self.redis = None
self._connect()
def _connect(self):
"""Establish Redis connection"""
try:
self.redis = redis.from_url(self.redis_url, decode_responses=False)
# Test connection
self.redis.ping()
logger.info("Redis connection established successfully")
except Exception as e:
logger.error(f"Failed to connect to Redis: {e}")
self.redis = None
def _is_connected(self) -> bool:
"""Check if Redis is connected"""
if not self.redis:
return False
try:
self.redis.ping()
return True
except:
return False
def get(self, key: str, default: Any = None) -> Any:
"""Get value from cache"""
if not self._is_connected():
return default
try:
value = self.redis.get(key)
if value is None:
return default
return pickle.loads(value)
except Exception as e:
logger.error(f"Error getting cache key {key}: {e}")
return default
def set(self, key: str, value: Any, expire: int = 3600) -> bool:
"""Set value in cache with expiration"""
if not self._is_connected():
return False
try:
serialized_value = pickle.dumps(value)
return self.redis.setex(key, expire, serialized_value)
except Exception as e:
logger.error(f"Error setting cache key {key}: {e}")
return False
def delete(self, key: str) -> bool:
"""Delete key from cache"""
if not self._is_connected():
return False
try:
return bool(self.redis.delete(key))
except Exception as e:
logger.error(f"Error deleting cache key {key}: {e}")
return False
def exists(self, key: str) -> bool:
"""Check if key exists in cache"""
if not self._is_connected():
return False
try:
return bool(self.redis.exists(key))
except Exception as e:
logger.error(f"Error checking cache key {key}: {e}")
return False
def expire(self, key: str, seconds: int) -> bool:
"""Set expiration for key"""
if not self._is_connected():
return False
try:
return bool(self.redis.expire(key, seconds))
except Exception as e:
logger.error(f"Error setting expiration for cache key {key}: {e}")
return False
def ttl(self, key: str) -> int:
"""Get time to live for key"""
if not self._is_connected():
return -1
try:
return self.redis.ttl(key)
except Exception as e:
logger.error(f"Error getting TTL for cache key {key}: {e}")
return -1
def clear_pattern(self, pattern: str) -> int:
"""Clear all keys matching pattern"""
if not self._is_connected():
return 0
try:
keys = self.redis.keys(pattern)
if keys:
return self.redis.delete(*keys)
return 0
except Exception as e:
logger.error(f"Error clearing cache pattern {pattern}: {e}")
return 0
def clear_all(self) -> bool:
"""Clear all cache"""
if not self._is_connected():
return False
try:
self.redis.flushdb()
return True
except Exception as e:
logger.error(f"Error clearing all cache: {e}")
return False
def get_many(self, keys: List[str]) -> Dict[str, Any]:
"""Get multiple values from cache"""
if not self._is_connected():
return {}
try:
values = self.redis.mget(keys)
result = {}
for key, value in zip(keys, values):
if value is not None:
result[key] = pickle.loads(value)
return result
except Exception as e:
logger.error(f"Error getting multiple cache keys: {e}")
return {}
def set_many(self, data: Dict[str, Any], expire: int = 3600) -> bool:
"""Set multiple values in cache"""
if not self._is_connected():
return False
try:
pipeline = self.redis.pipeline()
for key, value in data.items():
serialized_value = pickle.dumps(value)
pipeline.setex(key, expire, serialized_value)
pipeline.execute()
return True
except Exception as e:
logger.error(f"Error setting multiple cache keys: {e}")
return False
def increment(self, key: str, amount: int = 1) -> Optional[int]:
"""Increment counter in cache"""
if not self._is_connected():
return None
try:
return self.redis.incr(key, amount)
except Exception as e:
logger.error(f"Error incrementing cache key {key}: {e}")
return None
def decrement(self, key: str, amount: int = 1) -> Optional[int]:
"""Decrement counter in cache"""
if not self._is_connected():
return None
try:
return self.redis.decr(key, amount)
except Exception as e:
logger.error(f"Error decrementing cache key {key}: {e}")
return None
# Global cache instance
cache_service = CacheService()
def cache_key_generator(*args, **kwargs) -> str:
"""Generate cache key from function arguments"""
# Create a hash of the arguments
key_data = str(args) + str(sorted(kwargs.items()))
return hashlib.md5(key_data.encode()).hexdigest()
def cached(expire: int = 3600, key_prefix: str = ""):
"""Decorator for caching function results"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# Generate cache key
func_key = f"{key_prefix}:{func.__name__}:{cache_key_generator(*args, **kwargs)}"
# Try to get from cache
cached_result = cache_service.get(func_key)
if cached_result is not None:
logger.debug(f"Cache hit for {func_key}")
return cached_result
# Execute function and cache result
result = func(*args, **kwargs)
cache_service.set(func_key, result, expire)
logger.debug(f"Cache miss for {func_key}, stored result")
return result
return wrapper
return decorator
def invalidate_cache_pattern(pattern: str):
"""Decorator to invalidate cache after function execution"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
cache_service.clear_pattern(pattern)
logger.debug(f"Invalidated cache pattern: {pattern}")
return result
return wrapper
return decorator
# Cache key constants
class CacheKeys:
"""Constants for cache keys"""
MILITANTE_LIST = "militantes:list"
MILITANTE_DETAIL = "militante:detail:{}"
PAGAMENTO_LIST = "pagamentos:list"
PAGAMENTO_DETAIL = "pagamento:detail:{}"
COTA_LIST = "cotas:list"
COTA_DETAIL = "cota:detail:{}"
DASHBOARD_STATS = "dashboard:stats"
USER_SESSION = "user:session:{}"
API_RESPONSE = "api:response:{}"
@staticmethod
def militante_detail(militante_id: int) -> str:
return CacheKeys.MILITANTE_DETAIL.format(militante_id)
@staticmethod
def pagamento_detail(pagamento_id: int) -> str:
return CacheKeys.PAGAMENTO_DETAIL.format(pagamento_id)
@staticmethod
def cota_detail(cota_id: int) -> str:
return CacheKeys.COTA_DETAIL.format(cota_id)
@staticmethod
def user_session(user_id: int) -> str:
return CacheKeys.USER_SESSION.format(user_id)
@staticmethod
def api_response(endpoint: str) -> str:
return CacheKeys.API_RESPONSE.format(endpoint)

View File

@@ -1,78 +0,0 @@
from services.database_service import DatabaseService
from models.entities.celula import Celula
class CelulaService:
"""Service for Celula operations"""
@staticmethod
def get_all_celulas():
"""Get all celulas from the database"""
db = DatabaseService.get_db_connection()
try:
celulas = db.query(Celula).all()
return celulas
finally:
db.close()
@staticmethod
def get_celula_by_id(celula_id):
"""Get a celula by its ID"""
db = DatabaseService.get_db_connection()
try:
celula = db.query(Celula).get(celula_id)
return celula
finally:
db.close()
@staticmethod
def create_celula(data):
"""Create a new celula"""
db = DatabaseService.get_db_connection()
try:
celula = Celula(**data)
db.add(celula)
db.commit()
return celula
except Exception as e:
db.rollback()
raise e
finally:
db.close()
@staticmethod
def update_celula(celula_id, data):
"""Update an existing celula"""
db = DatabaseService.get_db_connection()
try:
celula = db.query(Celula).get(celula_id)
if not celula:
return None
for key, value in data.items():
setattr(celula, key, value)
db.commit()
return celula
except Exception as e:
db.rollback()
raise e
finally:
db.close()
@staticmethod
def delete_celula(celula_id):
"""Delete a celula"""
db = DatabaseService.get_db_connection()
try:
celula = db.query(Celula).get(celula_id)
if not celula:
return False
db.delete(celula)
db.commit()
return True
except Exception as e:
db.rollback()
raise e
finally:
db.close()

View File

@@ -1,254 +0,0 @@
from functions.database import get_db_connection, Militante, Pagamento, CotaMensal, MaterialVendido, AssinaturaAnual, TipoPagamento
from sqlalchemy import func
from sqlalchemy.orm import joinedload
from datetime import datetime, timedelta
from typing import Dict, List, Any
from services.cache_service import cache_service, cached, CacheKeys, invalidate_cache_pattern
import logging
logger = logging.getLogger(__name__)
class DashboardService:
"""Service for dashboard data aggregation with caching"""
@staticmethod
@cached(expire=300, key_prefix="dashboard") # Cache for 5 minutes
def get_dashboard_stats() -> Dict[str, Any]:
"""Get dashboard statistics with caching"""
db = get_db_connection()
try:
# Get cached stats first
cache_key = CacheKeys.DASHBOARD_STATS
cached_stats = cache_service.get(cache_key)
if cached_stats:
logger.debug("Using cached dashboard stats")
return cached_stats
# Calculate fresh stats
stats = DashboardService._calculate_stats(db)
# Cache the results
cache_service.set(cache_key, stats, 300) # 5 minutes
logger.debug("Cached fresh dashboard stats")
return stats
except Exception as e:
logger.error(f"Error getting dashboard stats: {e}")
return DashboardService._get_default_stats()
finally:
db.close()
@staticmethod
def _calculate_stats(db) -> Dict[str, Any]:
"""Calculate dashboard statistics"""
try:
# Total militantes
total_militantes = db.query(func.count(Militante.id)).scalar()
# Total cotas (soma dos valores)
total_cotas_result = db.query(func.sum(CotaMensal.valor_novo)).scalar()
total_cotas = f"{total_cotas_result:.2f}" if total_cotas_result else "0.00"
# Total de materiais vendidos
total_materiais = db.query(func.count(MaterialVendido.id)).scalar()
# Total de assinaturas ativas
total_assinaturas = db.query(func.count(AssinaturaAnual.id)).scalar()
# Últimos militantes cadastrados (limit 5) - eager load emails
militantes_query = db.query(Militante).options(
joinedload(Militante.emails)
).order_by(Militante.id.desc()).limit(5).all()
# Convert militantes to dictionaries to avoid lazy loading issues
ultimos_militantes = []
for militante in militantes_query:
militante_dict = {
'id': militante.id,
'nome': militante.nome,
'emails': [{'endereco_email': email.endereco_email} for email in militante.emails]
}
ultimos_militantes.append(militante_dict)
# Últimos pagamentos (limit 5) - eager load militante
pagamentos_query = db.query(Pagamento).options(
joinedload(Pagamento.militante)
).order_by(Pagamento.data_pagamento.desc()).limit(5).all()
# Convert pagamentos to dictionaries to avoid lazy loading issues
ultimos_pagamentos = []
for pagamento in pagamentos_query:
pagamento_dict = {
'id': pagamento.id,
'valor': pagamento.valor,
'data_pagamento': pagamento.data_pagamento,
'militante': {
'id': pagamento.militante.id,
'nome': pagamento.militante.nome
}
}
ultimos_pagamentos.append(pagamento_dict)
# Estatísticas por período
hoje = datetime.now().date()
inicio_mes = hoje.replace(day=1)
# Militantes cadastrados este mês
militantes_mes = db.query(func.count(Militante.id)).filter(
Militante.id >= 1 # Assuming ID is auto-increment
).scalar()
# Pagamentos este mês
pagamentos_mes = db.query(func.sum(Pagamento.valor)).filter(
Pagamento.data_pagamento >= inicio_mes
).scalar()
total_pagamentos_mes = f"{pagamentos_mes:.2f}" if pagamentos_mes else "0.00"
return {
'total_militantes': total_militantes,
'total_cotas': total_cotas,
'total_materiais': total_materiais,
'total_assinaturas': total_assinaturas,
'ultimos_militantes': ultimos_militantes,
'ultimos_pagamentos': ultimos_pagamentos,
'militantes_mes': militantes_mes,
'pagamentos_mes': total_pagamentos_mes,
'cache_timestamp': datetime.now().isoformat()
}
except Exception as e:
logger.error(f"Error calculating dashboard stats: {e}")
return DashboardService._get_default_stats()
@staticmethod
def _get_default_stats() -> Dict[str, Any]:
"""Get default statistics when calculation fails"""
return {
'total_militantes': 0,
'total_cotas': "0.00",
'total_materiais': 0,
'total_assinaturas': 0,
'ultimos_militantes': [],
'ultimos_pagamentos': [],
'militantes_mes': 0,
'pagamentos_mes': "0.00",
'cache_timestamp': datetime.now().isoformat()
}
@staticmethod
@invalidate_cache_pattern("dashboard:*")
def invalidate_dashboard_cache():
"""Invalidate dashboard cache when data changes"""
logger.info("Dashboard cache invalidated")
@staticmethod
@cached(expire=600, key_prefix="dashboard") # Cache for 10 minutes
def get_militante_stats() -> Dict[str, Any]:
"""Get militante-specific statistics"""
db = get_db_connection()
try:
# Militantes por estado
estados = db.query(Militante.estado, func.count(Militante.id)).group_by(Militante.estado).all()
# Militantes por responsabilidade
responsabilidades = {}
militantes = db.query(Militante).all()
for militante in militantes:
for resp in militante.get_responsabilidades():
responsabilidades[resp] = responsabilidades.get(resp, 0) + 1
return {
'estados': dict(estados),
'responsabilidades': responsabilidades,
'cache_timestamp': datetime.now().isoformat()
}
except Exception as e:
logger.error(f"Error getting militante stats: {e}")
return {'estados': {}, 'responsabilidades': {}, 'cache_timestamp': datetime.now().isoformat()}
finally:
db.close()
@staticmethod
@cached(expire=300, key_prefix="dashboard")
def get_financial_stats() -> Dict[str, Any]:
"""Get financial statistics"""
db = get_db_connection()
try:
# Total de pagamentos
total_pagamentos = db.query(func.sum(Pagamento.valor)).scalar()
# Pagamentos por mês (últimos 6 meses)
hoje = datetime.now().date()
stats_mensais = []
for i in range(6):
inicio_mes = hoje.replace(day=1) - timedelta(days=30*i)
fim_mes = inicio_mes.replace(day=28) + timedelta(days=4)
fim_mes = fim_mes.replace(day=1) - timedelta(days=1)
valor_mes = db.query(func.sum(Pagamento.valor)).filter(
Pagamento.data_pagamento >= inicio_mes,
Pagamento.data_pagamento <= fim_mes
).scalar()
stats_mensais.append({
'mes': inicio_mes.strftime('%Y-%m'),
'valor': float(valor_mes) if valor_mes else 0.0
})
return {
'total_pagamentos': float(total_pagamentos) if total_pagamentos else 0.0,
'stats_mensais': stats_mensais,
'cache_timestamp': datetime.now().isoformat()
}
except Exception as e:
logger.error(f"Error getting financial stats: {e}")
return {
'total_pagamentos': 0.0,
'stats_mensais': [],
'cache_timestamp': datetime.now().isoformat()
}
finally:
db.close()
@staticmethod
def obter_ultimos_militantes(limite: int = 5) -> List[Militante]:
"""Obtém os últimos militantes cadastrados"""
db = get_db_connection()
try:
return db.query(Militante).order_by(Militante.id.desc()).limit(limite).all()
finally:
db.close()
@staticmethod
def obter_ultimos_pagamentos(limite: int = 5) -> List[Pagamento]:
"""Obtém os últimos pagamentos realizados"""
db = get_db_connection()
try:
return db.query(Pagamento).join(Militante).order_by(Pagamento.data_pagamento.desc()).limit(limite).all()
finally:
db.close()
@staticmethod
def obter_tipos_pagamento() -> List[TipoPagamento]:
"""Obtém todos os tipos de pagamento"""
db = get_db_connection()
try:
return db.query(TipoPagamento).all()
finally:
db.close()
@staticmethod
def obter_dados_dashboard() -> Dict:
"""Obtém todos os dados necessários para o dashboard"""
return {
'estatisticas': DashboardService.get_dashboard_stats(),
'ultimos_militantes': DashboardService.obter_ultimos_militantes(),
'ultimos_pagamentos': DashboardService.obter_ultimos_pagamentos(),
'tipos_pagamento': DashboardService.obter_tipos_pagamento(),
'data_atual': datetime.now().strftime("%d/%m/%Y")
}

View File

@@ -0,0 +1,35 @@
from sqlalchemy import text
from models.entities.base import engine, SessionLocal
class DatabaseService:
"""Serviço para gerenciar conexões com o banco de dados"""
@staticmethod
def get_db_connection():
"""Retorna uma nova sessão do banco de dados"""
db = SessionLocal()
try:
# Configurar SQLite para melhor tratamento de concorrência
db.execute(text("PRAGMA journal_mode=WAL"))
db.execute(text("PRAGMA busy_timeout=5000"))
return db
except:
db.close()
raise
@staticmethod
def execute_query(query, params=None):
"""
Executa uma query usando SQLAlchemy
"""
session = DatabaseService.get_db_connection()
try:
result = session.execute(query, params)
session.commit()
return result
except Exception as e:
session.rollback()
raise e
finally:
session.close()

View File

@@ -0,0 +1,161 @@
from sqlalchemy.orm import joinedload
from datetime import datetime
from models.entities.militante import Militante
from models.entities.email_militante import EmailMilitante
from models.entities.endereco import Endereco
from services.database_service import DatabaseService
class MilitanteService:
"""Serviço para operações com militantes"""
@staticmethod
def listar_militantes():
"""Lista todos os militantes"""
db = DatabaseService.get_db_connection()
try:
militantes = db.query(Militante)\
.options(
joinedload(Militante.celula),
joinedload(Militante.emails)
)\
.order_by(Militante.nome)\
.all()
return militantes
finally:
db.close()
@staticmethod
def buscar_militante(militante_id):
"""Busca um militante pelo ID"""
db = DatabaseService.get_db_connection()
try:
militante = db.query(Militante)\
.options(
joinedload(Militante.celula),
joinedload(Militante.emails),
joinedload(Militante.endereco),
joinedload(Militante.redes_sociais)
)\
.get(militante_id)
return militante
finally:
db.close()
@staticmethod
def buscar_por_cpf(cpf):
"""Busca um militante pelo CPF"""
db = DatabaseService.get_db_connection()
try:
militante = db.query(Militante).filter(Militante.cpf == cpf).first()
return militante
finally:
db.close()
@staticmethod
def salvar_militante(militante):
"""Salva um militante no banco de dados"""
db = DatabaseService.get_db_connection()
try:
if militante.id is None: # Novo militante
db.add(militante)
db.flush() # Para obter o ID gerado
militante_id = militante.id
else: # Militante existente
db.merge(militante)
militante_id = militante.id
db.commit()
return militante_id
except Exception as e:
db.rollback()
print(f"Erro ao salvar militante: {e}")
raise
finally:
db.close()
@staticmethod
def salvar_endereco(endereco):
"""Salva um endereço no banco de dados"""
db = DatabaseService.get_db_connection()
try:
db.add(endereco)
db.flush() # Para obter o ID gerado
endereco_id = endereco.id
db.commit()
return endereco_id
except Exception as e:
db.rollback()
print(f"Erro ao salvar endereço: {e}")
raise
finally:
db.close()
@staticmethod
def salvar_email_militante(email_militante):
"""Salva um email de militante no banco de dados"""
db = DatabaseService.get_db_connection()
try:
db.add(email_militante)
db.commit()
return email_militante.id
except Exception as e:
db.rollback()
print(f"Erro ao salvar email: {e}")
raise
finally:
db.close()
@staticmethod
def atualizar_email_militante(militante_id, email):
"""Atualiza ou cria o email principal de um militante"""
db = DatabaseService.get_db_connection()
try:
# Verificar se já existe email
email_existente = db.query(EmailMilitante)\
.filter_by(militante_id=militante_id)\
.first()
if email_existente:
email_existente.endereco_email = email
db.commit()
else:
novo_email = EmailMilitante(
endereco_email=email,
militante_id=militante_id
)
db.add(novo_email)
db.commit()
return True
except Exception as e:
db.rollback()
print(f"Erro ao atualizar email: {e}")
raise
finally:
db.close()
@staticmethod
def excluir_militante(militante_id):
"""Exclui um militante pelo ID"""
db = DatabaseService.get_db_connection()
try:
militante = db.query(Militante).get(militante_id)
if not militante:
return False
# Excluir emails associados
db.query(EmailMilitante)\
.filter_by(militante_id=militante_id)\
.delete()
# Excluir o militante
db.delete(militante)
db.commit()
return True
except Exception as e:
db.rollback()
print(f"Erro ao excluir militante: {e}")
raise
finally:
db.close()

View File

@@ -0,0 +1,95 @@
from sqlalchemy.orm import joinedload
from models.entities.usuario import Usuario
from services.database_service import DatabaseService
class UsuarioService:
"""Serviço para operações com usuários"""
@staticmethod
def listar_usuarios():
"""Lista todos os usuários"""
db = DatabaseService.get_db_connection()
try:
usuarios = db.query(Usuario).options(
joinedload(Usuario.roles),
joinedload(Usuario.militante),
joinedload(Usuario.setor),
joinedload(Usuario.cr),
joinedload(Usuario.celula)
).all()
return usuarios
finally:
db.close()
@staticmethod
def buscar_usuario(user_id):
"""Busca um usuário pelo ID"""
db = DatabaseService.get_db_connection()
try:
usuario = db.query(Usuario).options(
joinedload(Usuario.roles),
joinedload(Usuario.militante),
joinedload(Usuario.setor),
joinedload(Usuario.cr),
joinedload(Usuario.celula)
).get(user_id)
return usuario
finally:
db.close()
@staticmethod
def buscar_por_username(username):
"""Busca um usuário pelo nome de usuário"""
db = DatabaseService.get_db_connection()
try:
usuario = db.query(Usuario).filter(Usuario.username == username).first()
return usuario
finally:
db.close()
@staticmethod
def buscar_por_email(email):
"""Busca um usuário pelo email"""
db = DatabaseService.get_db_connection()
try:
usuario = db.query(Usuario).filter(Usuario.email == email).first()
return usuario
finally:
db.close()
@staticmethod
def salvar_usuario(usuario):
"""Salva um usuário no banco de dados"""
db = DatabaseService.get_db_connection()
try:
if usuario.id is None: # Novo usuário
db.add(usuario)
else: # Usuário existente
db.merge(usuario)
db.commit()
return True
except Exception as e:
db.rollback()
print(f"Erro ao salvar usuário: {e}")
return False
finally:
db.close()
@staticmethod
def excluir_usuario(user_id):
"""Exclui um usuário pelo ID"""
db = DatabaseService.get_db_connection()
try:
usuario = db.query(Usuario).get(user_id)
if usuario:
db.delete(usuario)
db.commit()
return True
return False
except Exception as e:
db.rollback()
print(f"Erro ao excluir usuário: {e}")
return False
finally:
db.close()

51
static/js/comprovantes.js Normal file
View File

@@ -0,0 +1,51 @@
$(document).ready(function() {
// Inicialização da tabela
$('#tabelaComprovantes').DataTable({
language: {
url: '//cdn.datatables.net/plug-ins/1.13.7/i18n/pt-BR.json'
}
});
// Modal de edição
$('#modalEditarComprovante').on('show.bs.modal', function(event) {
var button = $(event.relatedTarget);
var comprovanteId = button.data('comprovante-id');
var militanteId = button.data('militante-id');
var militanteNome = button.data('militante-nome');
var tipoComprovante = button.data('tipo-comprovante');
var valor = button.data('valor');
var dataComprovante = button.data('data-comprovante');
var modal = $(this);
modal.find('#editMilitante').val(militanteId);
modal.find('#editMilitanteNome').val(militanteNome);
modal.find('#editTipoComprovante').val(tipoComprovante);
modal.find('#editValor').val(valor);
modal.find('#editDataComprovante').val(dataComprovante);
modal.find('form').attr('action', '/comprovantes/editar/' + comprovanteId);
});
// Modal de exclusão
$('#modalExcluirComprovante').on('show.bs.modal', function(event) {
var button = $(event.relatedTarget);
var comprovanteId = button.data('comprovante-id');
var comprovanteInfo = button.data('comprovante-info');
var modal = $(this);
modal.find('#comprovanteInfo').text(comprovanteInfo);
modal.find('form').attr('action', '/comprovantes/excluir/' + comprovanteId);
});
// Formatação de valores monetários
$('.money').mask('000.000.000.000.000,00', {reverse: true});
// Validação de formulários
$('form').on('submit', function(e) {
if (!this.checkValidity()) {
e.preventDefault();
e.stopPropagation();
}
$(this).addClass('was-validated');
});
});

View File

@@ -1,10 +1,10 @@
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Configurar clique nos itens da lista de pagamentos // Configurar clique nos itens da lista de comprovantes
document.querySelectorAll('.list-group-item[onclick*="carregarDadosPagamento"]').forEach(item => { document.querySelectorAll('.list-group-item[onclick*="carregarDadosComprovante"]').forEach(item => {
item.addEventListener('click', function(e) { item.addEventListener('click', function(e) {
const pagamentoId = this.getAttribute('data-pagamento-id'); const comprovanteId = this.getAttribute('data-comprovante-id');
if (pagamentoId) { if (comprovanteId) {
carregarDadosPagamento(pagamentoId); carregarDadosComprovante(comprovanteId);
} }
}); });
}); });

View File

@@ -1,316 +0,0 @@
document.addEventListener('DOMContentLoaded', function() {
console.log('Carregando script pagamentos.js...');
// Inicializar DataTable
const table = $('#tabelaPagamentos').DataTable({
language: {
url: '//cdn.datatables.net/plug-ins/1.13.7/i18n/pt-BR.json'
},
columnDefs: [
{
targets: 3, // Coluna de data
type: 'date-br',
render: function(data, type, row) {
if (type === 'sort') {
return data.split('/').reverse().join('');
}
return data;
}
},
{
targets: 2, // Coluna de valor
type: 'numeric',
render: function(data, type, row) {
if (type === 'sort') {
return parseFloat(data.replace('R$ ', '').replace(',', '.'));
}
return data;
}
},
{ targets: -1, orderable: false } // Coluna de ações
],
order: [[3, 'desc']] // Ordenar por data decrescente por padrão
});
// Configuração do modal de edição
const modalEditarPagamento = document.getElementById('modalEditarPagamento');
if (modalEditarPagamento) {
modalEditarPagamento.addEventListener('show.bs.modal', function(event) {
console.log('Modal de edição sendo exibido');
const button = event.relatedTarget;
if (!button) {
console.error('Botão não encontrado!');
return;
}
const pagamentoId = button.getAttribute('data-pagamento-id');
console.log('ID do pagamento:', pagamentoId);
// Dados do pagamento
const dados = {
militanteId: button.getAttribute('data-militante-id'),
militanteNome: button.closest('tr').querySelector('td').textContent.trim(),
tipoPagamento: button.getAttribute('data-tipo-pagamento'),
valor: button.getAttribute('data-valor'),
dataPagamento: button.getAttribute('data-data-pagamento')
};
console.log('Dados do pagamento:', dados);
// Preencher campos
document.getElementById('editMilitante').value = dados.militanteId;
document.getElementById('editMilitanteNome').value = dados.militanteNome;
document.getElementById('editTipoPagamento').value = dados.tipoPagamento;
document.getElementById('editValor').value = dados.valor;
document.getElementById('editDataPagamento').value = dados.dataPagamento;
// Configurar formulário
const form = document.getElementById('formEditarPagamento');
if (form) {
form.action = `/pagamentos/editar/${pagamentoId}`;
console.log('Action do formulário:', form.action);
// Remover listeners antigos para evitar duplicação
const newForm = form.cloneNode(true);
form.parentNode.replaceChild(newForm, form);
// Adicionar listener para o submit do formulário
newForm.addEventListener('submit', function(e) {
e.preventDefault();
console.log('Formulário submetido');
// Criar FormData com os dados do formulário
const formData = new FormData(this);
// Log dos dados sendo enviados
console.log('Dados do formulário:');
for (let [key, value] of formData.entries()) {
console.log(key + ': ' + value);
}
// Enviar requisição
fetch(this.action, {
method: 'POST',
body: formData
})
.then(response => {
console.log('Status da resposta:', response.status);
return response.json();
})
.then(data => {
console.log('Resposta:', data);
if (data.status === 'success') {
// Fechar modal
const modal = bootstrap.Modal.getInstance(modalEditarPagamento);
modal.hide();
// Recarregar página
window.location.reload();
} else {
alert('Erro ao atualizar pagamento: ' + data.message);
}
})
.catch(error => {
console.error('Erro:', error);
alert('Erro ao atualizar pagamento. Por favor, tente novamente.');
});
});
}
});
}
// Configuração do modal de exclusão
const modalExcluirPagamento = document.getElementById('modalExcluirPagamento');
if (modalExcluirPagamento) {
modalExcluirPagamento.addEventListener('show.bs.modal', function(event) {
console.log('Modal de exclusão sendo exibido');
const button = event.relatedTarget;
if (!button) {
console.error('Botão não encontrado!');
return;
}
const pagamentoId = button.getAttribute('data-pagamento-id');
const pagamentoInfo = button.getAttribute('data-pagamento-info');
console.log('ID do pagamento:', pagamentoId);
// Atualizar informações no modal
document.getElementById('pagamentoInfo').textContent = pagamentoInfo;
// Configurar formulário
const form = document.getElementById('formExcluirPagamento');
if (form) {
form.action = `/pagamentos/excluir/${pagamentoId}`;
console.log('Action do formulário:', form.action);
// Remover listeners antigos para evitar duplicação
const newForm = form.cloneNode(true);
form.parentNode.replaceChild(newForm, form);
// Adicionar listener para o submit do formulário
newForm.addEventListener('submit', function(e) {
e.preventDefault();
console.log('Formulário submetido');
// Enviar requisição
fetch(this.action, {
method: 'POST'
})
.then(response => {
console.log('Status da resposta:', response.status);
return response.json();
})
.then(data => {
console.log('Resposta:', data);
if (data.status === 'success') {
// Fechar modal
const modal = bootstrap.Modal.getInstance(modalExcluirPagamento);
modal.hide();
// Recarregar página
window.location.reload();
} else {
alert('Erro ao excluir pagamento: ' + data.message);
}
})
.catch(error => {
console.error('Erro:', error);
alert('Erro ao excluir pagamento. Por favor, tente novamente.');
});
});
}
});
}
// Configuração do formulário de novo pagamento
const formNovoPagamento = document.getElementById('formNovoPagamento');
if (formNovoPagamento) {
formNovoPagamento.addEventListener('submit', function(e) {
e.preventDefault();
console.log('Formulário de novo pagamento submetido');
// Criar FormData com os dados do formulário
const formData = new FormData(this);
// Log dos dados sendo enviados
console.log('Dados do formulário:');
for (let [key, value] of formData.entries()) {
console.log(key + ': ' + value);
}
// Enviar requisição
fetch(this.action, {
method: 'POST',
body: formData
})
.then(response => {
console.log('Status da resposta:', response.status);
return response.json();
})
.then(data => {
console.log('Resposta:', data);
if (data.status === 'success') {
// Fechar modal
const modal = bootstrap.Modal.getInstance(document.getElementById('modalNovoPagamento'));
modal.hide();
// Recarregar página
window.location.reload();
} else {
alert('Erro ao adicionar pagamento: ' + data.message);
}
})
.catch(error => {
console.error('Erro:', error);
alert('Erro ao adicionar pagamento. Por favor, tente novamente.');
});
});
}
// Configuração do botão de exportar
const btnExportar = document.getElementById('btnExportar');
if (btnExportar) {
btnExportar.addEventListener('click', function() {
console.log('Exportando dados...');
// Coletar dados da tabela
const dados = [];
table.rows().every(function() {
const row = this.data();
dados.push({
militante: row[0],
tipo_pagamento: row[1],
valor: row[2].replace('R$ ', ''),
data_pagamento: row[3]
});
});
// Converter para CSV
const csv = [
['Militante', 'Tipo de Pagamento', 'Valor', 'Data do Pagamento'],
...dados.map(row => [
row.militante,
row.tipo_pagamento,
row.valor,
row.data_pagamento
])
]
.map(row => row.join(','))
.join('\n');
// Criar blob e fazer download
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
if (link.download !== undefined) {
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', 'pagamentos.csv');
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
});
}
// Funções de validação e formatação de datas
function validarData(data) {
if (!data) return false;
const dataObj = new Date(data);
if (isNaN(dataObj.getTime())) return false;
const hoje = new Date();
hoje.setHours(0, 0, 0, 0);
return dataObj <= hoje;
}
function formatarData(data) {
if (!data) return '';
const dataObj = new Date(data);
if (isNaN(dataObj.getTime())) return '';
return dataObj.toLocaleDateString('pt-BR');
}
// Configurar campos de data
const camposData = document.querySelectorAll('input[type="date"]');
camposData.forEach(campo => {
// Definir data máxima como hoje
const hoje = new Date().toISOString().split('T')[0];
campo.setAttribute('max', hoje);
campo.addEventListener('change', function() {
if (!validarData(this.value)) {
this.setCustomValidity('Data inválida ou futura');
this.classList.add('is-invalid');
} else {
this.setCustomValidity('');
this.classList.remove('is-invalid');
}
});
});
});

View File

@@ -56,7 +56,7 @@ function configurarOrdenacaoTabela(tabelaId) {
if (column === 'data' || if (column === 'data' ||
column === 'data_vencimento' || column === 'data_vencimento' ||
column === 'data_alteracao' || column === 'data_alteracao' ||
column === 'data_pagamento' || column === 'data_comprovante' ||
column === 'data_venda' || column === 'data_venda' ||
column === 'data_relatorio') { column === 'data_relatorio') {
const aDate = converterDataParaComparacao(aValue); const aDate = converterDataParaComparacao(aValue);
@@ -112,7 +112,7 @@ document.addEventListener('DOMContentLoaded', function() {
'materiaisTable', 'materiaisTable',
'vendasTable', 'vendasTable',
'cotasTable', 'cotasTable',
'pagamentosTable' 'comprovantesTable'
]; ];
tabelas.forEach(tabelaId => { tabelas.forEach(tabelaId => {
@@ -197,4 +197,12 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
}); });
}); });
}); });
function sortTable(table, column, type = 'text') {
// ... existing code ...
if (column === 'data_comprovante') {
// ... existing code ...
}
// ... existing code ...
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

View File

@@ -2,6 +2,7 @@
{% block title %}Dashboard Administrativo{% endblock %} {% block title %}Dashboard Administrativo{% endblock %}
<<<<<<< HEAD
{% block extra_css %} {% block extra_css %}
<style> <style>
.card { .card {
@@ -126,10 +127,21 @@
<h2 class="display-4 mb-0">{{ total_users }}</h2> <h2 class="display-4 mb-0">{{ total_users }}</h2>
<i class="fas fa-users fa-3x opacity-50"></i> <i class="fas fa-users fa-3x opacity-50"></i>
</div> </div>
=======
{% block content %}
<div class="row mb-4">
<div class="col-md-4">
<div class="card">
<div class="card-body">
<h5 class="card-title">Total de Usuários</h5>
<p class="card-text display-4">{{ total_users }}</p>
<i class="fas fa-users fa-2x text-primary"></i>
>>>>>>> a22b0e4 (refactor(#11): Integra listagem de usuários no dashboard)
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<<<<<<< HEAD
<div class="card bg-success text-white"> <div class="card bg-success text-white">
<div class="card-body"> <div class="card-body">
<h5 class="card-title text-uppercase">Usuários Ativos</h5> <h5 class="card-title text-uppercase">Usuários Ativos</h5>
@@ -137,10 +149,18 @@
<h2 class="display-4 mb-0">{{ active_users }}</h2> <h2 class="display-4 mb-0">{{ active_users }}</h2>
<i class="fas fa-user-check fa-3x opacity-50"></i> <i class="fas fa-user-check fa-3x opacity-50"></i>
</div> </div>
=======
<div class="card">
<div class="card-body">
<h5 class="card-title">Usuários Ativos</h5>
<p class="card-text display-4">{{ active_users }}</p>
<i class="fas fa-user-check fa-2x text-success"></i>
>>>>>>> a22b0e4 (refactor(#11): Integra listagem de usuários no dashboard)
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<<<<<<< HEAD
<div class="card bg-danger text-white"> <div class="card bg-danger text-white">
<div class="card-body"> <div class="card-body">
<h5 class="card-title text-uppercase">Usuários Inativos</h5> <h5 class="card-title text-uppercase">Usuários Inativos</h5>
@@ -148,11 +168,19 @@
<h2 class="display-4 mb-0">{{ inactive_users }}</h2> <h2 class="display-4 mb-0">{{ inactive_users }}</h2>
<i class="fas fa-user-times fa-3x opacity-50"></i> <i class="fas fa-user-times fa-3x opacity-50"></i>
</div> </div>
=======
<div class="card">
<div class="card-body">
<h5 class="card-title">Usuários Inativos</h5>
<p class="card-text display-4">{{ inactive_users }}</p>
<i class="fas fa-user-times fa-2x text-danger"></i>
>>>>>>> a22b0e4 (refactor(#11): Integra listagem de usuários no dashboard)
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<<<<<<< HEAD
<div class="card lista-usuarios"> <div class="card lista-usuarios">
<div class="card-header d-flex justify-content-between align-items-center"> <div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0"> <h5 class="mb-0">
@@ -185,16 +213,54 @@
<td> <td>
<div class="btn-group"> <div class="btn-group">
<form action="{{ url_for('admin.reset_user_otp', user_id=user.id) }}" method="post" class="d-inline"> <form action="{{ url_for('admin.reset_user_otp', user_id=user.id) }}" method="post" class="d-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-warning btn-sm" title="Reset OTP" onclick="return confirm('Confirma o reset do OTP deste usuário?')"> <button type="submit" class="btn btn-warning btn-sm" title="Reset OTP" onclick="return confirm('Confirma o reset do OTP deste usuário?')">
<i class="fas fa-key"></i> <i class="fas fa-key"></i>
=======
<div class="card">
<div class="card-header">
<h5 class="mb-0">Gerenciamento de Usuários</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table id="users-table" class="table table-striped">
<thead>
<tr>
<th>Email</th>
<th>Nome</th>
<th>Status</th>
<th>Último Login</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{ user.email }}</td>
<td>{{ user.name }}</td>
<td>
<span class="badge {% if user.is_active %}bg-success{% else %}bg-danger{% endif %}">
{{ "Ativo" if user.is_active else "Inativo" }}
</span>
</td>
<td>{{ user.last_login.strftime('%d/%m/%Y %H:%M') if user.last_login else 'Nunca' }}</td>
<td>
<form action="{{ url_for('admin.reset_user_otp', user_id=user.id) }}" method="post" class="d-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-warning btn-sm" onclick="return confirm('Confirma o reset do OTP deste usuário?')">
<i class="fas fa-key"></i> Reset OTP
>>>>>>> a22b0e4 (refactor(#11): Integra listagem de usuários no dashboard)
</button> </button>
</form> </form>
<form action="{{ url_for('admin.reset_user_password', user_id=user.id) }}" method="post" class="d-inline"> <form action="{{ url_for('admin.reset_user_password', user_id=user.id) }}" method="post" class="d-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<<<<<<< HEAD
<button type="submit" class="btn btn-info btn-sm" title="Reset Senha" onclick="return confirm('Confirma o reset da senha deste usuário?')"> <button type="submit" class="btn btn-info btn-sm" title="Reset Senha" onclick="return confirm('Confirma o reset da senha deste usuário?')">
<i class="fas fa-lock"></i> <i class="fas fa-lock"></i>
</button> </button>
</form> </form>
<form action="{{ url_for('admin.toggle_user_status', user_id=user.id) }}" method="post" class="d-inline"> <form action="{{ url_for('admin.toggle_user_status', user_id=user.id) }}" method="post" class="d-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-{{ 'danger' if user.is_active else 'success' }} btn-sm" title="{{ 'Desativar' if user.is_active else 'Ativar' }} Usuário"> <button type="submit" class="btn btn-{{ 'danger' if user.is_active else 'success' }} btn-sm" title="{{ 'Desativar' if user.is_active else 'Ativar' }} Usuário">
<i class="fas fa-{{ 'user-times' if user.is_active else 'user-check' }}"></i> <i class="fas fa-{{ 'user-times' if user.is_active else 'user-check' }}"></i>
</button> </button>
@@ -205,19 +271,58 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
=======
<button type="submit" class="btn btn-info btn-sm" onclick="return confirm('Confirma o reset da senha deste usuário?')">
<i class="fas fa-lock"></i> Reset Senha
</button>
</form>
<button onclick="toggleUserStatus({{ user.id }})" class="btn btn-{% if user.is_active %}danger{% else %}success{% endif %} btn-sm">
<i class="fas fa-{% if user.is_active %}user-times{% else %}user-check{% endif %}"></i>
{{ "Desativar" if user.is_active else "Ativar" }}
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
>>>>>>> a22b0e4 (refactor(#11): Integra listagem de usuários no dashboard)
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block extra_js %} {% block extra_js %}
<script> <script>
<<<<<<< HEAD
=======
function toggleUserStatus(userId) {
if (confirm('Deseja alterar o status deste usuário?')) {
fetch(`/admin/users/${userId}/toggle-status`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}'
}
}).then(response => {
if (response.ok) {
window.location.reload();
}
});
}
}
>>>>>>> a22b0e4 (refactor(#11): Integra listagem de usuários no dashboard)
$(document).ready(function() { $(document).ready(function() {
$('#users-table').DataTable({ $('#users-table').DataTable({
language: { language: {
url: '//cdn.datatables.net/plug-ins/1.13.7/i18n/pt-BR.json' url: '//cdn.datatables.net/plug-ins/1.13.7/i18n/pt-BR.json'
}, },
<<<<<<< HEAD
order: [[0, 'asc']], order: [[0, 'asc']],
pageLength: 25 pageLength: 25
=======
order: [[1, 'asc']]
>>>>>>> a22b0e4 (refactor(#11): Integra listagem de usuários no dashboard)
}); });
}); });
</script> </script>

View File

@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="{{ csrf_token() if csrf_token is defined else '' }}"> <meta name="csrf-token" content="{{ csrf_token() }}">
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='img/favicon.ico') }}"> <link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='img/favicon.ico') }}">
<title>{% block title %}{% endblock %} - Controles OCI</title> <title>{% block title %}{% endblock %} - Controles OCI</title>
@@ -508,7 +508,7 @@
{% block navbar %} {% block navbar %}
<nav class="navbar navbar-expand-lg navbar-dark"> <nav class="navbar navbar-expand-lg navbar-dark">
<div class="container-fluid"> <div class="container-fluid">
<a class="navbar-brand" href="{{ url_for('home.index') }}"> <a class="navbar-brand" href="{{ url_for('home') }}">
<img src="{{ url_for('static', filename='img/logo002-alpha.png') }}" alt="Logo OCI"> <img src="{{ url_for('static', filename='img/logo002-alpha.png') }}" alt="Logo OCI">
Controles OCI Controles OCI
</a> </a>
@@ -524,7 +524,7 @@
</a> </a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li> <li>
<a class="dropdown-item" href="{{ url_for('militante.listar') }}"> <a class="dropdown-item" href="{{ url_for('listar_militantes') }}">
<i class="fas fa-list"></i>Listar Militantes <i class="fas fa-list"></i>Listar Militantes
</a> </a>
</li> </li>
@@ -536,12 +536,12 @@
</a> </a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li> <li>
<a class="dropdown-item" href="{{ url_for('cota.listar') }}"> <a class="dropdown-item" href="{{ url_for('listar_cotas') }}">
<i class="fas fa-money-bill-wave"></i>Cotas <i class="fas fa-money-bill-wave"></i>Cotas
</a> </a>
</li> </li>
<li> <li>
<a class="dropdown-item" href="{{ url_for('pagamento.listar') }}"> <a class="dropdown-item" href="{{ url_for('listar_pagamentos') }}">
<i class="fas fa-receipt"></i>Pagamentos <i class="fas fa-receipt"></i>Pagamentos
</a> </a>
</li> </li>
@@ -553,23 +553,18 @@
</a> </a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li> <li>
<a class="dropdown-item" href="{{ url_for('material.listar') }}"> <a class="dropdown-item" href="{{ url_for('listar_materiais') }}">
<i class="fas fa-box"></i>Listar Materiais <i class="fas fa-box"></i>Listar Materiais
</a> </a>
</li> </li>
<li> <li>
<a class="dropdown-item" href="{{ url_for('material.listar_tipos') }}"> <a class="dropdown-item" href="{{ url_for('listar_vendas_jornal') }}">
<i class="fas fa-tags"></i>Tipos de Materiais <i class="fas fa-newspaper"></i>Vendas de Jornais
</a> </a>
</li> </li>
<li> <li>
<a class="dropdown-item" href="{{ url_for('material.novo') }}"> <a class="dropdown-item" href="{{ url_for('listar_assinaturas') }}">
<i class="fas fa-plus"></i>Novo Material <i class="fas fa-file-signature"></i>Assinaturas
</a>
</li>
<li>
<a class="dropdown-item" href="{{ url_for('material.novo_tipo') }}">
<i class="fas fa-plus"></i>Novo Tipo
</a> </a>
</li> </li>
</ul> </ul>
@@ -580,12 +575,12 @@
</a> </a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li> <li>
<a class="dropdown-item" href="{{ url_for('home.dashboard') }}"> <a class="dropdown-item" href="{{ url_for('listar_relatorios_cotas') }}">
<i class="fas fa-file-invoice-dollar"></i>Relatórios de Cotas <i class="fas fa-file-invoice-dollar"></i>Relatórios de Cotas
</a> </a>
</li> </li>
<li> <li>
<a class="dropdown-item" href="{{ url_for('home.dashboard') }}"> <a class="dropdown-item" href="{{ url_for('listar_relatorios_vendas') }}">
<i class="fas fa-file-alt"></i>Relatórios de Vendas <i class="fas fa-file-alt"></i>Relatórios de Vendas
</a> </a>
</li> </li>
@@ -598,9 +593,9 @@
<i class="fas fa-user me-1"></i>{{ session.get('username', 'Usuário') }} <i class="fas fa-user me-1"></i>{{ session.get('username', 'Usuário') }}
</a> </a>
<ul class="dropdown-menu dropdown-menu-end"> <ul class="dropdown-menu dropdown-menu-end">
{% if is_admin %} {% if session.get('is_admin') %}
<li> <li>
<a class="dropdown-item" href="{{ url_for('usuario.novo') }}"> <a class="dropdown-item" href="{{ url_for('novo_usuario') }}">
<i class="fas fa-user-plus"></i>Novo Usuário <i class="fas fa-user-plus"></i>Novo Usuário
</a> </a>
</li> </li>
@@ -612,7 +607,7 @@
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
{% endif %} {% endif %}
<li> <li>
<a class="dropdown-item" href="{{ url_for('auth.logout') }}"> <a class="dropdown-item" href="{{ url_for('logout') }}">
<i class="fas fa-sign-out-alt"></i>Sair <i class="fas fa-sign-out-alt"></i>Sair
</a> </a>
</li> </li>
@@ -635,4 +630,4 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
{% block scripts %}{% endblock %} {% block scripts %}{% endblock %}
</body> </body>
</html> </html>

View File

@@ -1,138 +0,0 @@
<!-- Componente para wrapping de elementos baseado em permissões -->
<!-- Uso: {% include 'components/permission_wrapper.html' with context %} -->
<!-- Macro para verificar permissões e renderizar conteúdo condicionalmente -->
{% macro render_if_permission(permission_name, content='', fallback='', show_fallback=false) %}
{% if user_can(permission_name) %}
{{ content | safe }}
{% elif show_fallback %}
{{ fallback | safe }}
{% endif %}
{% endmacro %}
<!-- Macro para botões com permissão -->
{% macro permission_button(permission_name, url, text, icon='', btn_class='btn-primary', title='') %}
{% if user_can(permission_name) %}
<a href="{{ url }}" class="btn {{ btn_class }}" title="{{ title }}">
{% if icon %}<i class="{{ icon }} me-2"></i>{% endif %}{{ text }}
</a>
{% endif %}
{% endmacro %}
<!-- Macro para links de menu com permissão -->
{% macro permission_menu_item(permission_name, url, text, icon='') %}
{% if user_can(permission_name) %}
<li>
<a class="dropdown-item" href="{{ url }}">
{% if icon %}<i class="{{ icon }}"></i>{% endif %}{{ text }}
</a>
</li>
{% endif %}
{% endmacro %}
<!-- Macro para seções de dados com permissão -->
{% macro permission_data_section(permission_name, data, template_content='', empty_message='Nenhum dado disponível') %}
{% if user_can(permission_name) %}
{% if data %}
{{ template_content | safe }}
{% else %}
<div class="alert alert-info">
<i class="fas fa-info-circle me-2"></i>{{ empty_message }}
</div>
{% endif %}
{% else %}
<div class="alert alert-warning">
<i class="fas fa-lock me-2"></i>Você não tem permissão para visualizar estes dados.
</div>
{% endif %}
{% endmacro %}
<!-- Macro para tabelas com dados filtrados por permissão -->
{% macro permission_table(permission_name, data, headers, row_template='', empty_message='Nenhum registro encontrado') %}
{% if user_can(permission_name) %}
{% if data %}
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
{% for header in headers %}
<th>{{ header }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{{ row_template | safe }}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-info text-center">
<i class="fas fa-table me-2"></i>{{ empty_message }}
</div>
{% endif %}
{% else %}
<div class="alert alert-warning text-center">
<i class="fas fa-lock me-2"></i>Você não tem permissão para visualizar estes dados.
</div>
{% endif %}
{% endmacro %}
<!-- Macro para cards de estatísticas com permissão -->
{% macro permission_stats_card(permission_name, title, value, icon, color='primary', url='#') %}
{% if user_can(permission_name) %}
<div class="col-md-3 mb-4">
<div class="card border-0 shadow-sm h-100">
<div class="card-body text-center">
<div class="text-{{ color }} mb-3">
<i class="{{ icon }} fa-3x"></i>
</div>
<h5 class="card-title text-muted">{{ title }}</h5>
<h2 class="card-text text-{{ color }}">{{ value }}</h2>
{% if url != '#' %}
<a href="{{ url }}" class="btn btn-outline-{{ color }} btn-sm">
Ver detalhes <i class="fas fa-arrow-right ms-1"></i>
</a>
{% endif %}
</div>
</div>
</div>
{% endif %}
{% endmacro %}
<!-- Macro para formulários com permissão -->
{% macro permission_form(permission_name, form_content='', action='', method='POST') %}
{% if user_can(permission_name) %}
<form action="{{ action }}" method="{{ method }}" class="needs-validation" novalidate>
{{ form_content | safe }}
</form>
{% else %}
<div class="alert alert-warning">
<i class="fas fa-lock me-2"></i>Você não tem permissão para realizar esta ação.
</div>
{% endif %}
{% endmacro %}
<!-- Macro para modais com permissão -->
{% macro permission_modal(permission_name, modal_id, title, content='', show_button=true, button_text='Abrir', button_class='btn-primary') %}
{% if user_can(permission_name) %}
{% if show_button %}
<button type="button" class="btn {{ button_class }}" data-bs-toggle="modal" data-bs-target="#{{ modal_id }}">
{{ button_text }}
</button>
{% endif %}
<div class="modal fade" id="{{ modal_id }}" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ title }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
{{ content | safe }}
</div>
</div>
</div>
</div>
{% endif %}
{% endmacro %}

View File

@@ -17,7 +17,7 @@
{% endwith %} {% endwith %}
<form method="POST" class="needs-validation" novalidate> <form method="POST" class="needs-validation" novalidate>
<!-- CSRF token removido temporariamente --> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="row"> <div class="row">
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label for="nome" class="form-label">Nome</label> <label for="nome" class="form-label">Nome</label>

View File

@@ -6,7 +6,14 @@
<div class="container mt-4"> <div class="container mt-4">
<h2 class="mb-4"><i class="fas fa-users-cog"></i> Administração de Usuários</h2> <h2 class="mb-4"><i class="fas fa-users-cog"></i> Administração de Usuários</h2>
<div class="card"> <div class="card">
<div class="card-header bg-dark text-white">
<h3 class="mb-0"><i class="fas fa-users-cog"></i> Administração de Usuários</h3>
</div>
<div class="card-body"> <div class="card-body">
<div class="alert alert-info" role="alert">
<i class="fas fa-info-circle"></i> Aqui você pode gerenciar todos os usuários do sistema. Use os controles abaixo para ativar/desativar contas ou alterar níveis de acesso.
</div>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover"> <table class="table table-hover">
<thead class="thead-light"> <thead class="thead-light">
@@ -29,6 +36,7 @@
<td>{{ usuario.last_login }}</td> <td>{{ usuario.last_login }}</td>
<td> <td>
<span class="badge {% if usuario.ativo %}bg-success{% else %}bg-danger{% endif %}"> <span class="badge {% if usuario.ativo %}bg-success{% else %}bg-danger{% endif %}">
<span class="badge {% if usuario.ativo %}badge-success{% else %}badge-danger{% endif %}">
{{ "Ativo" if usuario.ativo else "Inativo" }} {{ "Ativo" if usuario.ativo else "Inativo" }}
</span> </span>
</td> </td>
@@ -195,4 +203,4 @@ $(function () {
$('[data-toggle="tooltip"]').tooltip(); $('[data-toggle="tooltip"]').tooltip();
}); });
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -0,0 +1,43 @@
{% extends 'base.html' %}
{% block title %}Editar Comprovante{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-8 offset-md-2">
<div class="card shadow-sm">
<div class="card-header bg-light">
<h4 class="card-title mb-0">
<i class="fas fa-money-bill-wave me-2"></i>Editar Comprovante
</h4>
</div>
<div class="card-body">
<form method="post">
{% csrf_token %}
<div class="form-group">
<label for="tipo_comprovante_id">Tipo de Comprovante</label>
<select class="form-control" id="tipo_comprovante_id" name="tipo_comprovante_id" required>
<option value="1" {% if comprovante.tipo_comprovante_id == 1 %}selected{% endif %}>1 - Comprovante Padrão</option>
{% if current_user.has_permission('gerenciar_tipos_comprovante') %}
<option value="2" {% if comprovante.tipo_comprovante_id == 2 %}selected{% endif %}>2 - Comprovante Especial</option>
<option value="3" {% if comprovante.tipo_comprovante_id == 3 %}selected{% endif %}>3 - Comprovante Extraordinário</option>
<option value="4" {% if comprovante.tipo_comprovante_id == 4 %}selected{% endif %}>4 - Jornal Avulso</option>
<option value="5" {% if comprovante.tipo_comprovante_id == 5 %}selected{% endif %}>5 - Assinatura de Jornal</option>
<option value="6" {% if comprovante.tipo_comprovante_id == 6 %}selected{% endif %}>6 - Campanha Financeira</option>
{% endif %}
</select>
</div>
<div class="mb-3">
<label for="data_comprovante" class="form-label">Data do Comprovante:</label>
<input type="date" class="form-control" id="data_comprovante" name="data_comprovante"
required max="{{ hoje }}">
</div>
<button type="submit" class="btn btn-primary">Salvar</button>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -23,7 +23,7 @@
<label class="form-check-label" for="pago">Pago</label> <label class="form-check-label" for="pago">Pago</label>
</div> </div>
<button type="submit" class="btn btn-primary">Salvar</button> <button type="submit" class="btn btn-primary">Salvar</button>
<a href="{{ url_for('cota.listar') }}" class="btn btn-secondary">Cancelar</a> <a href="{{ url_for('listar_cotas') }}" class="btn btn-secondary">Cancelar</a>
</form> </form>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -64,7 +64,7 @@
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<button type="submit" class="btn btn-success">Salvar</button> <button type="submit" class="btn btn-success">Salvar</button>
<a href="{{ url_for('material.listar') }}" class="btn btn-outline-secondary">Voltar</a> <a href="{{ url_for('listar_materiais') }}" class="btn btn-outline-secondary">Voltar</a>
</div> </div>
</form> </form>
</div> </div>

View File

@@ -17,7 +17,7 @@
{% endwith %} {% endwith %}
<form id="formEditarMilitante" method="POST" class="needs-validation" novalidate> <form id="formEditarMilitante" method="POST" class="needs-validation" novalidate>
<!-- CSRF token removido temporariamente --> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="militante_id" value="{{ militante.id }}"> <input type="hidden" name="militante_id" value="{{ militante.id }}">
<div class="row"> <div class="row">
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">

View File

@@ -1,12 +1,12 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block title %}Editar Relatório de Pagamentos{% endblock %} {% block title %}Editar Relatório de Comprovantes{% endblock %}
{% block content %} {% block content %}
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<h1 class="mb-4">Editar Relatório de Pagamentos</h1> <h1 class="mb-4">Editar Relatório de Comprovantes</h1>
{% with messages = get_flashed_messages(with_categories=true) %} {% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %} {% if messages %}
@@ -44,10 +44,10 @@
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="total_pagamentos" class="form-label">Total de Pagamentos</label> <label for="total_comprovantes" class="form-label">Total de Comprovantes</label>
<input type="number" class="form-control" id="total_pagamentos" name="total_pagamentos" step="0.01" value="{{ relatorio.total_pagamentos }}" required> <input type="number" class="form-control" id="total_comprovantes" name="total_comprovantes" step="0.01" value="{{ relatorio.total_comprovantes }}" required>
<div class="invalid-feedback"> <div class="invalid-feedback">
Por favor, insira o total de pagamentos. Por favor, insira o total de comprovantes.
</div> </div>
</div> </div>
@@ -61,7 +61,7 @@
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<button type="submit" class="btn btn-success">Salvar</button> <button type="submit" class="btn btn-success">Salvar</button>
<a href="{{ url_for('listar_relatorios_pagamentos') }}" class="btn btn-outline-secondary">Voltar</a> <a href="{{ url_for('listar_relatorios_comprovantes') }}" class="btn btn-outline-secondary">Voltar</a>
</div> </div>
</form> </form>
</div> </div>

View File

@@ -44,7 +44,7 @@
<div class="d-flex justify-content-between mt-4"> <div class="d-flex justify-content-between mt-4">
<button type="submit" class="btn btn-primary">Salvar</button> <button type="submit" class="btn btn-primary">Salvar</button>
<a href="{{ url_for('material.listar_tipos') }}" class="btn btn-secondary">Cancelar</a> <a href="{{ url_for('listar_tipos_materiais') }}" class="btn btn-secondary">Cancelar</a>
</div> </div>
</form> </form>
</div> </div>

View File

@@ -18,7 +18,7 @@
<div class="stats-card blue"> <div class="stats-card blue">
<div class="title">Total de Militantes</div> <div class="title">Total de Militantes</div>
<div class="value">{{ total_militantes }}</div> <div class="value">{{ total_militantes }}</div>
<a href="{{ url_for('militante.listar') }}" class="link"> <a href="{{ url_for('listar_militantes') }}" class="link">
Ver detalhes <i class="fas fa-arrow-right"></i> Ver detalhes <i class="fas fa-arrow-right"></i>
</a> </a>
<div class="icon"> <div class="icon">
@@ -31,7 +31,7 @@
<div class="stats-card green"> <div class="stats-card green">
<div class="title">Total de Cotas</div> <div class="title">Total de Cotas</div>
<div class="value">R$ {{ total_cotas }}</div> <div class="value">R$ {{ total_cotas }}</div>
<a href="{{ url_for('cota.listar') }}" class="link"> <a href="{{ url_for('listar_cotas') }}" class="link">
Ver detalhes <i class="fas fa-arrow-right"></i> Ver detalhes <i class="fas fa-arrow-right"></i>
</a> </a>
<div class="icon"> <div class="icon">
@@ -44,7 +44,7 @@
<div class="stats-card cyan"> <div class="stats-card cyan">
<div class="title">Materiais Vendidos</div> <div class="title">Materiais Vendidos</div>
<div class="value">{{ total_materiais }}</div> <div class="value">{{ total_materiais }}</div>
<a href="{{ url_for('militante.listar') }}" class="link"> <a href="{{ url_for('listar_materiais') }}" class="link">
Ver detalhes <i class="fas fa-arrow-right"></i> Ver detalhes <i class="fas fa-arrow-right"></i>
</a> </a>
<div class="icon"> <div class="icon">
@@ -57,7 +57,7 @@
<div class="stats-card yellow"> <div class="stats-card yellow">
<div class="title">Assinaturas Ativas</div> <div class="title">Assinaturas Ativas</div>
<div class="value">{{ total_assinaturas }}</div> <div class="value">{{ total_assinaturas }}</div>
<a href="{{ url_for('militante.listar') }}" class="link"> <a href="{{ url_for('listar_assinaturas') }}" class="link">
Ver detalhes <i class="fas fa-arrow-right"></i> Ver detalhes <i class="fas fa-arrow-right"></i>
</a> </a>
<div class="icon"> <div class="icon">
@@ -115,7 +115,7 @@
<div class="list-group-item" style="cursor: pointer" onclick="carregarDadosPagamento({{ pagamento.id }})"> <div class="list-group-item" style="cursor: pointer" onclick="carregarDadosPagamento({{ pagamento.id }})">
<div class="militante-info"> <div class="militante-info">
<h6 class="mb-1">{{ pagamento.militante.nome }}</h6> <h6 class="mb-1">{{ pagamento.militante.nome }}</h6>
<small>{{ pagamento.data_pagamento.strftime('%d/%m/%Y') if pagamento.data_pagamento else '' }}</small> <small>{{ pagamento.data_pagamento.strftime('%d/%m/%Y') }}</small>
</div> </div>
<div class="d-flex align-items-center gap-2"> <div class="d-flex align-items-center gap-2">
<span class="badge bg-success">R$ {{ "%.2f"|format(pagamento.valor) }}</span> <span class="badge bg-success">R$ {{ "%.2f"|format(pagamento.valor) }}</span>

View File

@@ -0,0 +1,49 @@
{% extends 'base.html' %}
{% block title %}Lista de Comprovantes{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<div class="card shadow-sm">
<div class="card-header bg-light">
<h4 class="card-title mb-0">
<i class="fas fa-list me-2"></i>Lista de Comprovantes
</h4>
</div>
<div class="card-body">
<table class="table table-striped">
<thead>
<tr>
<th>ID</th>
<th>Data</th>
<th>Valor</th>
<th>Tipo</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{% for comprovante in comprovantes %}
<tr>
<td>{{ comprovante.id }}</td>
<td>{{ comprovante.data.strftime('%d/%m/%Y') }}</td>
<td>R$ {{ "%.2f"|format(comprovante.valor) }}</td>
<td>
{% if comprovante.tipo_comprovante_id == 1 %}
1 - Comprovante Padrão
{% elif current_user.has_permission('gerenciar_tipos_comprovante') %}
{% if comprovante.tipo_comprovante_id == 2 %}
2 - Comprovante Especial
<td>{{ comprovante.tipo }}</td>
<td>{{ comprovante.data }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,14 +1,14 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Pagamentos{% endblock %} {% block title %}Comprovantes{% endblock %}
{% block content %} {% block content %}
<div class="container-fluid mt-3"> <div class="container-fluid mt-3">
<div class="d-flex justify-content-between align-items-center mb-3"> <div class="d-flex justify-content-between align-items-center mb-3">
<h2><i class="fas fa-money-bill-wave"></i> Pagamentos</h2> <h2><i class="fas fa-money-bill-wave"></i> Comprovantes</h2>
<div> <div>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalNovoPagamento"> <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalNovoComprovante">
<i class="fas fa-plus"></i> Novo Pagamento <i class="fas fa-plus"></i> Novo Comprovante
</button> </button>
<button type="button" class="btn btn-outline-primary" id="btnExportar"> <button type="button" class="btn btn-outline-primary" id="btnExportar">
<i class="fas fa-file-export"></i> Exportar <i class="fas fa-file-export"></i> Exportar
@@ -19,56 +19,62 @@
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped table-hover" id="tabelaPagamentos"> <table class="table table-striped table-hover" id="tabelaComprovantes">
<thead> <thead>
<tr> <tr>
<th>Militante</th> <th>Militante</th>
<th>Tipo de Pagamento</th> <th>Tipo de Comprovante</th>
<th>Valor</th> <th>Valor</th>
<th>Data do Pagamento</th> <th>Data do Comprovante</th>
<th>Ações</th> <th>Ações</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for pagamento in pagamentos %} {% for comprovante in comprovantes %}
<tr> <tr>
<td data-militante="{{ pagamento.militante_id }}">{{ pagamento.militante.nome if pagamento.militante else 'N/A' }}</td> <td data-militante="{{ comprovante.militante_id }}">{{ comprovante.militante.nome if comprovante.militante else 'N/A' }}</td>
<td data-tipo="{{ pagamento.tipo_pagamento }}"> <td data-tipo="{{ comprovante.tipo_comprovante }}">
{% if pagamento.tipo_pagamento == 1 %} {% if comprovante.tipo_comprovante == 1 %}
Mensalidade Cota
{% elif pagamento.tipo_pagamento == 2 %} {% elif comprovante.tipo_comprovante == 2 %}
Contribuição Extra Contribuição Extra
{% elif pagamento.tipo_pagamento == 3 %} {% elif comprovante.tipo_comprovante == 3 %}
Doação Doação
{% elif pagamento.tipo_pagamento == 4 %} {% elif comprovante.tipo_comprovante == 4 %}
Taxa de Evento Taxa de Evento
{% elif pagamento.tipo_pagamento == 5 %} {% elif comprovante.tipo_comprovante == 5 %}
Jornal Avulso
{% elif comprovante.tipo_comprovante == 6 %}
Assinatura de Jornal
{% elif comprovante.tipo_comprovante == 7 %}
Campanha Financeira
{% elif comprovante.tipo_comprovante == 8 %}
Outros Outros
{% else %} {% else %}
Não Definido Não Definido
{% endif %} {% endif %}
</td> </td>
<td data-valor="{{ pagamento.valor }}">R$ {{ "%.2f"|format(pagamento.valor) }}</td> <td data-valor="{{ comprovante.valor }}">R$ {{ "%.2f"|format(comprovante.valor) }}</td>
<td data-data="{{ pagamento.data_pagamento }}">{{ pagamento.data_pagamento.strftime('%d/%m/%Y') }}</td> <td data-data="{{ comprovante.data_comprovante }}">{{ comprovante.data_comprovante.strftime('%d/%m/%Y') }}</td>
<td> <td>
<button type="button" <button type="button"
class="btn btn-sm btn-outline-primary" class="btn btn-sm btn-outline-primary"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#modalEditarPagamento" data-bs-target="#modalEditarComprovante"
data-pagamento-id="{{ pagamento.id }}" data-comprovante-id="{{ comprovante.id }}"
data-militante-id="{{ pagamento.militante_id }}" data-militante-id="{{ comprovante.militante_id }}"
data-tipo-pagamento="{{ pagamento.tipo_pagamento }}" data-tipo-comprovante="{{ comprovante.tipo_comprovante }}"
data-valor="{{ pagamento.valor }}" data-valor="{{ comprovante.valor }}"
data-data-pagamento="{{ pagamento.data_pagamento.strftime('%Y-%m-%d') }}" data-data-comprovante="{{ comprovante.data_comprovante.strftime('%Y-%m-%d') }}"
title="Editar"> title="Editar">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</button> </button>
<button type="button" <button type="button"
class="btn btn-sm btn-outline-danger" class="btn btn-sm btn-outline-danger"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#modalExcluirPagamento" data-bs-target="#modalExcluirComprovante"
data-pagamento-id="{{ pagamento.id }}" data-comprovante-id="{{ comprovante.id }}"
data-pagamento-info="Pagamento de {{ pagamento.militante.nome if pagamento.militante else 'N/A' }} - R$ {{ "%.2f"|format(pagamento.valor) }}" data-comprovante-info="Comprovante de {{ comprovante.militante.nome if comprovante.militante else 'N/A' }} - R$ {{ "%.2f"|format(comprovante.valor) }}"
title="Excluir"> title="Excluir">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</button> </button>
@@ -82,16 +88,16 @@
</div> </div>
</div> </div>
<!-- Modal Novo Pagamento --> <!-- Modal Novo Comprovante -->
<div class="modal fade" id="modalNovoPagamento" tabindex="-1"> <div class="modal fade" id="modalNovoComprovante" tabindex="-1">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title"><i class="fas fa-plus"></i> Novo Pagamento</h5> <h5 class="modal-title"><i class="fas fa-plus"></i> Novo Comprovante</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form id="formNovoPagamento" method="post" action="{{ url_for('pagamento.novo') }}"> <form id="formNovoComprovante" method="post" action="{{ url_for('adicionar_comprovante') }}">
<div class="mb-3"> <div class="mb-3">
<label for="militante" class="form-label">Militante:</label> <label for="militante" class="form-label">Militante:</label>
<select class="form-select" id="militante" name="militante_id" required> <select class="form-select" id="militante" name="militante_id" required>
@@ -102,14 +108,19 @@
</select> </select>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="tipoPagamento" class="form-label">Tipo de Pagamento:</label> <label for="tipoComprovante" class="form-label">Tipo de Comprovante:</label>
<select class="form-select" id="tipoPagamento" name="tipo_pagamento" required> <select class="form-select" id="tipoComprovante" name="tipo_comprovante" required>
<option value="">Selecione o tipo</option> <option value="">Selecione o tipo</option>
<option value="1">Mensalidade</option> <option value="1">Cota</option>
{% if current_user.has_permission('gerenciar_tipos_comprovante') %}
<option value="2">Contribuição Extra</option> <option value="2">Contribuição Extra</option>
<option value="3">Doação</option> <option value="3">Doação</option>
<option value="4">Taxa de Evento</option> <option value="4">Taxa de Evento</option>
<option value="5">Outros</option> <option value="5">Jornal Avulso</option>
<option value="6">Assinatura de Jornal</option>
<option value="7">Campanha Financeira</option>
<option value="8">Outros</option>
{% endif %}
</select> </select>
</div> </div>
<div class="mb-3"> <div class="mb-3">
@@ -117,8 +128,8 @@
<input type="number" step="0.01" class="form-control" id="valor" name="valor" required> <input type="number" step="0.01" class="form-control" id="valor" name="valor" required>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="dataPagamento" class="form-label">Data do Pagamento:</label> <label for="dataComprovante" class="form-label">Data do Comprovante:</label>
<input type="date" class="form-control" id="dataPagamento" name="data_pagamento" required> <input type="date" class="form-control" id="dataComprovante" name="data_comprovante" required>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
@@ -130,30 +141,35 @@
</div> </div>
</div> </div>
<!-- Modal Editar Pagamento --> <!-- Modal Editar Comprovante -->
<div class="modal fade" id="modalEditarPagamento" tabindex="-1"> <div class="modal fade" id="modalEditarComprovante" tabindex="-1">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title"><i class="fas fa-edit"></i> Editar Pagamento</h5> <h5 class="modal-title"><i class="fas fa-edit"></i> Editar Comprovante</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form id="formEditarPagamento" method="post"> <form id="formEditarComprovante" method="post">
<div class="mb-3"> <div class="mb-3">
<label for="editMilitante" class="form-label">Militante:</label> <label for="editMilitante" class="form-label">Militante:</label>
<input type="text" class="form-control bg-light" id="editMilitanteNome" readonly> <input type="text" class="form-control bg-light" id="editMilitanteNome" readonly>
<input type="hidden" id="editMilitante" name="militante_id"> <input type="hidden" id="editMilitante" name="militante_id">
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="editTipoPagamento" class="form-label">Tipo de Pagamento:</label> <label for="editTipoComprovante" class="form-label">Tipo de Comprovante:</label>
<select class="form-select" id="editTipoPagamento" name="tipo_pagamento" required> <select class="form-select" id="editTipoComprovante" name="tipo_comprovante" required>
<option value="">Selecione o tipo</option> <option value="">Selecione o tipo</option>
<option value="1">Mensalidade</option> <option value="1">Cota</option>
{% if current_user.has_permission('gerenciar_tipos_comprovante') %}
<option value="2">Contribuição Extra</option> <option value="2">Contribuição Extra</option>
<option value="3">Doação</option> <option value="3">Doação</option>
<option value="4">Taxa de Evento</option> <option value="4">Taxa de Evento</option>
<option value="5">Outros</option> <option value="5">Jornal Avulso</option>
<option value="6">Assinatura de Jornal</option>
<option value="7">Campanha Financeira</option>
<option value="8">Outros</option>
{% endif %}
</select> </select>
</div> </div>
<div class="mb-3"> <div class="mb-3">
@@ -161,8 +177,8 @@
<input type="number" step="0.01" class="form-control" id="editValor" name="valor" required> <input type="number" step="0.01" class="form-control" id="editValor" name="valor" required>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="editDataPagamento" class="form-label">Data do Pagamento:</label> <label for="editDataComprovante" class="form-label">Data do Comprovante:</label>
<input type="date" class="form-control" id="editDataPagamento" name="data_pagamento" required> <input type="date" class="form-control" id="editDataComprovante" name="data_comprovante" required>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
@@ -174,20 +190,20 @@
</div> </div>
</div> </div>
<!-- Modal Excluir Pagamento --> <!-- Modal Excluir Comprovante -->
<div class="modal fade" id="modalExcluirPagamento" tabindex="-1"> <div class="modal fade" id="modalExcluirComprovante" tabindex="-1">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title"><i class="fas fa-trash"></i> Excluir Pagamento</h5> <h5 class="modal-title"><i class="fas fa-trash"></i> Excluir Comprovante</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<p>Tem certeza que deseja excluir este pagamento?</p> <p>Tem certeza que deseja excluir este comprovante?</p>
<p id="pagamentoInfo" class="text-muted"></p> <p id="comprovanteInfo" class="text-muted"></p>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<form id="formExcluirPagamento" method="post"> <form id="formExcluirComprovante" method="post">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" class="btn btn-danger">Excluir</button> <button type="submit" class="btn btn-danger">Excluir</button>
</form> </form>
@@ -198,6 +214,6 @@
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script src="{{ url_for('static', filename='js/pagamentos.js') }}"></script> <script src="{{ url_for('static', filename='js/comprovantes.js') }}"></script>
{% endblock %} {% endblock %}

View File

@@ -110,7 +110,7 @@
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form id="formNovaCota" method="post" action="{{ url_for('cota.novo') }}"> <form id="formNovaCota" method="post" action="{{ url_for('nova_cota') }}">
<div class="mb-3"> <div class="mb-3">
<label for="militante_id" class="form-label">Militante:</label> <label for="militante_id" class="form-label">Militante:</label>
<select class="form-select" id="militante_id" name="militante_id" required> <select class="form-select" id="militante_id" name="militante_id" required>

View File

@@ -99,7 +99,7 @@
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form id="formNovoMaterial" method="post" action="{{ url_for('material.novo') }}"> <form id="formNovoMaterial" method="post" action="{{ url_for('novo_material') }}">
<div class="mb-3"> <div class="mb-3">
<label for="militante_id" class="form-label">Militante:</label> <label for="militante_id" class="form-label">Militante:</label>
<select class="form-select" id="militante_id" name="militante_id" required> <select class="form-select" id="militante_id" name="militante_id" required>

View File

@@ -1,274 +0,0 @@
{% extends "base.html" %}
{% from 'components/permission_wrapper.html' import permission_button, permission_table, permission_data_section %}
{% block title %}Militantes{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>
<i class="fas fa-users me-2"></i>Militantes
</h2>
<!-- Botão de novo militante só aparece se tiver permissão -->
{{ permission_button('create_cell_member', url_for('militante.novo'), 'Novo Militante', 'fas fa-plus', 'btn-success') }}
</div>
</div>
</div>
<!-- Filtros - só aparecem se tiver permissão para ver dados -->
{% if user_can('view_cell_data') %}
<div class="row mb-4">
<div class="col-md-6">
<div class="input-group">
<span class="input-group-text">
<i class="fas fa-search"></i>
</span>
<input type="text" class="form-control" id="filtroNome" placeholder="Filtrar por nome...">
</div>
</div>
<div class="col-md-3">
<select class="form-select" id="filtroCelula">
<option value="">Todas as células</option>
<!-- Opções serão carregadas via JS baseado nas permissões -->
</select>
</div>
<div class="col-md-3">
<select class="form-select" id="filtroEstado">
<option value="">Todos os estados</option>
<option value="ATIVO">Ativo</option>
<option value="LICENCIADO">Licenciado</option>
<option value="SUSPENSO">Suspenso</option>
<option value="DESLIGADO">Desligado</option>
</select>
</div>
</div>
{% endif %}
<!-- Seção de dados com verificação de permissão -->
{% call permission_data_section('view_cell_data', militantes, '', 'Nenhum militante encontrado') %}
<div class="card">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0" id="tabelaMilitantes">
<thead class="table-dark">
<tr>
<th>Nome</th>
<th>Email</th>
<th>Telefone</th>
<th>Célula</th>
<th>Estado</th>
<th>Responsabilidades</th>
<!-- Coluna de ações só aparece se tiver permissão -->
{% if user_can('manage_cell_members') %}
<th width="120">Ações</th>
{% endif %}
</tr>
</thead>
<tbody>
{% for militante in militantes %}
<tr data-militante-id="{{ militante.id }}">
<td>
<div class="d-flex align-items-center">
<div class="avatar-sm bg-primary text-white rounded-circle d-flex align-items-center justify-content-center me-3">
{{ militante.nome[0].upper() }}
</div>
<div>
<div class="fw-bold">{{ militante.nome }}</div>
{% if militante.cpf %}
<small class="text-muted">CPF: {{ militante.cpf }}</small>
{% endif %}
</div>
</div>
</td>
<td>
{% if militante.emails %}
<a href="mailto:{{ militante.emails[0].endereco_email }}" class="text-decoration-none">
{{ militante.emails[0].endereco_email }}
</a>
{% else %}
<span class="text-muted">-</span>
{% endif %}
</td>
<td>
{% if militante.telefone1 %}
<a href="tel:{{ militante.telefone1 }}" class="text-decoration-none">
{{ militante.telefone1 }}
</a>
{% else %}
<span class="text-muted">-</span>
{% endif %}
</td>
<td>
{% if militante.celula %}
<span class="badge bg-info">{{ militante.celula.nome }}</span>
{% else %}
<span class="text-muted">-</span>
{% endif %}
</td>
<td>
{% if militante.estado %}
{% set estado_classes = {
'ATIVO': 'bg-success',
'LICENCIADO': 'bg-warning',
'SUSPENSO': 'bg-danger',
'DESLIGADO': 'bg-secondary'
} %}
<span class="badge {{ estado_classes.get(militante.estado.value, 'bg-secondary') }}">
{{ militante.estado.value.title() }}
</span>
{% else %}
<span class="badge bg-success">Ativo</span>
{% endif %}
</td>
<td>
<div class="d-flex flex-wrap gap-1">
{% for resp in militante.get_responsabilidades() %}
<span class="badge bg-primary" style="font-size: 0.7em;">{{ resp }}</span>
{% endfor %}
</div>
</td>
<!-- Ações só aparecem se tiver permissão -->
{% if user_can('manage_cell_members') %}
<td>
<div class="btn-group btn-group-sm" role="group">
<button type="button"
class="btn btn-outline-primary btn-sm"
data-bs-toggle="modal"
data-bs-target="#modalEditarMilitante"
data-militante-id="{{ militante.id }}"
title="Editar">
<i class="fas fa-edit"></i>
</button>
<!-- Botão de excluir só para níveis superiores -->
{% if user_can('manage_sector_cells') %}
<button type="button"
class="btn btn-outline-danger btn-sm"
onclick="confirmarExclusao({{ militante.id }}, '{{ militante.nome }}')"
title="Excluir">
<i class="fas fa-trash"></i>
</button>
{% endif %}
</div>
</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endcall %}
<!-- Estatísticas - só aparecem se tiver permissão -->
{% if user_can('view_cell_reports') %}
<div class="row mt-4">
<div class="col-md-3">
<div class="card bg-primary text-white">
<div class="card-body text-center">
<h5>Total</h5>
<h2>{{ militantes|length }}</h2>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-success text-white">
<div class="card-body text-center">
<h5>Ativos</h5>
<h2>{{ militantes|selectattr('estado.value', 'equalto', 'ATIVO')|list|length }}</h2>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-warning text-white">
<div class="card-body text-center">
<h5>Licenciados</h5>
<h2>{{ militantes|selectattr('estado.value', 'equalto', 'LICENCIADO')|list|length }}</h2>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-info text-white">
<div class="card-body text-center">
<h5>Aspirantes</h5>
<h2>{{ militantes|selectattr('aspirante', 'equalto', true)|list|length }}</h2>
</div>
</div>
</div>
</div>
{% endif %}
</div>
<!-- Modais só são incluídos se tiver permissão -->
{% if user_can('manage_cell_members') %}
{% include 'modals/militante_editar.html' %}
{% include 'modals/militante_excluir.html' %}
{% endif %}
{% endblock %}
{% block extra_js %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Filtros de tabela
const filtroNome = document.getElementById('filtroNome');
const filtroCelula = document.getElementById('filtroCelula');
const filtroEstado = document.getElementById('filtroEstado');
const tabela = document.getElementById('tabelaMilitantes');
if (filtroNome && tabela) {
function filtrarTabela() {
const nomeFilter = filtroNome.value.toLowerCase();
const celulaFilter = filtroCelula ? filtroCelula.value : '';
const estadoFilter = filtroEstado ? filtroEstado.value : '';
const linhas = tabela.querySelectorAll('tbody tr');
linhas.forEach(linha => {
const nome = linha.cells[0].textContent.toLowerCase();
const celula = linha.cells[3].textContent;
const estado = linha.cells[4].textContent;
const nomeMatch = nome.includes(nomeFilter);
const celulaMatch = !celulaFilter || celula.includes(celulaFilter);
const estadoMatch = !estadoFilter || estado.includes(estadoFilter);
linha.style.display = (nomeMatch && celulaMatch && estadoMatch) ? '' : 'none';
});
}
filtroNome.addEventListener('input', filtrarTabela);
if (filtroCelula) filtroCelula.addEventListener('change', filtrarTabela);
if (filtroEstado) filtroEstado.addEventListener('change', filtrarTabela);
}
// Função para confirmar exclusão
window.confirmarExclusao = function(id, nome) {
if (confirm(`Tem certeza que deseja excluir o militante ${nome}?`)) {
fetch(`/militantes/excluir/${id}`, {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
location.reload();
} else {
alert(data.message || 'Erro ao excluir militante');
}
})
.catch(error => {
console.error('Erro:', error);
alert('Erro ao excluir militante');
});
}
};
});
</script>
{% endblock %}

View File

@@ -1,12 +1,12 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block title %}Listar Relatórios de Pagamentos{% endblock %} {% block title %}Listar Relatórios de Comprovantes{% endblock %}
{% block content %} {% block content %}
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<h1 class="mb-4">Lista de Relatórios de Pagamentos</h1> <h1 class="mb-4">Lista de Relatórios de Comprovantes</h1>
{% with messages = get_flashed_messages(with_categories=true) %} {% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %} {% if messages %}
@@ -17,7 +17,7 @@
{% endwith %} {% endwith %}
<div class="d-flex justify-content-between mb-4"> <div class="d-flex justify-content-between mb-4">
<a href="{{ url_for('novo_relatorio_pagamentos') }}" class="btn btn-success">Novo Relatório</a> <a href="{{ url_for('novo_relatorio_comprovantes') }}" class="btn btn-success">Novo Relatório</a>
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a> <a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
</div> </div>
@@ -28,7 +28,7 @@
<th>ID</th> <th>ID</th>
<th>Setor</th> <th>Setor</th>
<th>Comitê Central</th> <th>Comitê Central</th>
<th>Total de Pagamentos</th> <th>Total de Comprovantes</th>
<th>Data do Relatório</th> <th>Data do Relatório</th>
<th>Ações</th> <th>Ações</th>
</tr> </tr>
@@ -39,11 +39,11 @@
<td>{{ relatorio.id }}</td> <td>{{ relatorio.id }}</td>
<td>{{ relatorio.setor.nome }}</td> <td>{{ relatorio.setor.nome }}</td>
<td>{{ relatorio.comite.nome }}</td> <td>{{ relatorio.comite.nome }}</td>
<td>R$ {{ "%.2f"|format(relatorio.total_pagamentos) }}</td> <td>R$ {{ "%.2f"|format(relatorio.total_comprovantes) }}</td>
<td>{{ relatorio.data_relatorio.strftime('%d/%m/%Y') }}</td> <td>{{ relatorio.data_relatorio.strftime('%d/%m/%Y') }}</td>
<td> <td>
<a href="{{ url_for('editar_relatorio_pagamentos', id=relatorio.id) }}" class="btn btn-primary btn-sm">Editar</a> <a href="{{ url_for('editar_relatorio_comprovantes', id=relatorio.id) }}" class="btn btn-primary btn-sm">Editar</a>
<a href="{{ url_for('deletar_relatorio_pagamentos', id=relatorio.id) }}" class="btn btn-danger btn-sm" onclick="return confirm('Tem certeza que deseja excluir este relatório?')">Excluir</a> <a href="{{ url_for('deletar_relatorio_comprovantes', id=relatorio.id) }}" class="btn btn-danger btn-sm" onclick="return confirm('Tem certeza que deseja excluir este relatório?')">Excluir</a>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@@ -1,57 +1,54 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block title %}Tipos de Materiais{% endblock %} {% block title %}Listar Tipos de Materiais{% endblock %}
{% block content %} {% block content %}
<div class="container-fluid"> <div class="container">
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-md-12">
<div class="card"> <h1 class="mb-4">Lista de Tipos de Materiais</h1>
<div class="card-header">
<h2 class="card-title mb-0"> {% with messages = get_flashed_messages(with_categories=true) %}
<i class="fas fa-tags me-2"></i>Tipos de Materiais {% if messages %}
</h2> {% for category, message in messages %}
</div> <div class="alert alert-{{ category }}">{{ message }}</div>
<div class="card-body"> {% endfor %}
<div class="d-flex justify-content-between mb-4"> {% endif %}
<a href="{{ url_for('material.novo_tipo') }}" class="btn btn-success">Novo Tipo de Material</a> {% endwith %}
<a href="{{ url_for('home.index') }}" class="btn btn-outline-primary">Início</a>
</div> <div class="d-flex justify-content-between mb-4">
<a href="{{ url_for('novo_tipo_material') }}" class="btn btn-success">Novo Tipo de Material</a>
<div class="table-responsive"> <a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
<table class="table table-striped table-hover"> </div>
<thead class="table-dark">
<tr> <div class="table-responsive">
<th>ID</th> <table class="table table-striped table-hover">
<th>Descrição</th> <thead>
<th>Ações</th> <tr>
</tr> <th>ID</th>
</thead> <th>Nome</th>
<tbody> <th>Descrição</th>
{% for tipo in tipos_materiais %} <th>Preço</th>
<tr> <th>Ações</th>
<td>{{ tipo.id }}</td> </tr>
<td>{{ tipo.descricao }}</td> </thead>
<td> <tbody>
<a href="{{ url_for('material.editar_tipo', id=tipo.id) }}" class="btn btn-primary btn-sm">Editar</a> {% for tipo in tipos %}
<a href="{{ url_for('material.deletar_tipo', id=tipo.id) }}" class="btn btn-danger btn-sm" onclick="return confirm('Tem certeza que deseja excluir este tipo de material?')">Excluir</a> <tr>
</td> <td>{{ tipo.id }}</td>
</tr> <td>{{ tipo.nome }}</td>
{% else %} <td>{{ tipo.descricao }}</td>
<tr> <td>R$ {{ "%.2f"|format(tipo.preco) }}</td>
<td colspan="3" class="text-center text-muted"> <td>
<i class="fas fa-inbox fa-2x mb-2"></i> <a href="{{ url_for('editar_tipo_material', id=tipo.id) }}" class="btn btn-primary btn-sm">Editar</a>
<br> <a href="{{ url_for('deletar_tipo_material', id=tipo.id) }}" class="btn btn-danger btn-sm" onclick="return confirm('Tem certeza que deseja excluir este tipo de material?')">Excluir</a>
Nenhum tipo de material encontrado </td>
</td> </tr>
</tr> {% endfor %}
{% endfor %} </tbody>
</tbody> </table>
</table>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div>
{% endblock %} {% endblock %}

View File

@@ -25,7 +25,8 @@
<h4 class="login-title">Controles OCI</h4> <h4 class="login-title">Controles OCI</h4>
</div> </div>
<form method="POST" action="{{ url_for('auth.login') }}" class="needs-validation" novalidate> <form method="POST" action="{{ url_for('login') }}" class="needs-validation" novalidate>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="form-floating mb-3"> <div class="form-floating mb-3">
<input type="text" class="form-control" id="email" name="email" placeholder="Email ou Usuário" required> <input type="text" class="form-control" id="email" name="email" placeholder="Email ou Usuário" required>
<label for="email">Email ou Usuário</label> <label for="email">Email ou Usuário</label>

View File

@@ -10,6 +10,7 @@
</div> </div>
<form id="formEditarMilitante" method="POST" action="/militantes/editar/" novalidate> <form id="formEditarMilitante" method="POST" action="/militantes/editar/" novalidate>
<input type="hidden" id="edit_militante_id" name="militante_id" value=""> <input type="hidden" id="edit_militante_id" name="militante_id" value="">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" id="responsabilidades_values" name="responsabilidades_valor" value="0"> <input type="hidden" id="responsabilidades_values" name="responsabilidades_valor" value="0">
<!-- Tabs de navegação --> <!-- Tabs de navegação -->

View File

@@ -9,7 +9,7 @@
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form id="formNovoMilitante" method="post" action="{{ url_for('militante.criar') }}"> <form id="formNovoMilitante" method="post" action="{{ url_for('criar_militante') }}">
<!-- Nav tabs --> <!-- Nav tabs -->
<ul class="nav nav-tabs nav-fill mb-3" role="tablist"> <ul class="nav nav-tabs nav-fill mb-3" role="tablist">
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">

View File

@@ -23,8 +23,8 @@
</div> </div>
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">Registrar</button> <button type="submit" class="btn btn-primary">Registrar</button>
<a href="{{ url_for('cota.listar') }}" class="btn btn-secondary">Voltar</a> <a href="{{ url_for('listar_cotas') }}" class="btn btn-secondary">Voltar</a>
<a href="{{ url_for('home.index') }}" class="btn btn-outline-primary">Início</a> <a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
</div> </div>
</form> </form>

View File

@@ -1,6 +1,6 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block title %}Novo Pagamento{% endblock %} {% block title %}Novo Comprovante{% endblock %}
{% block content %} {% block content %}
<div class="container"> <div class="container">
@@ -9,7 +9,7 @@
<div class="card shadow-sm"> <div class="card shadow-sm">
<div class="card-header bg-light"> <div class="card-header bg-light">
<h4 class="card-title mb-0"> <h4 class="card-title mb-0">
<i class="fas fa-money-bill-wave me-2"></i>Registrar Novo Pagamento <i class="fas fa-money-bill-wave me-2"></i>Registrar Novo Comprovante
</h4> </h4>
</div> </div>
<div class="card-body"> <div class="card-body">
@@ -27,17 +27,18 @@
</div> </div>
</div> </div>
<div class="mb-3"> <div class="form-group">
<label for="tipo_pagamento_id" class="form-label">Tipo de Pagamento:</label> <label for="tipo_comprovante_id">Tipo de Comprovante</label>
<select class="form-select" id="tipo_pagamento_id" name="tipo_pagamento_id" required> <select class="form-control" id="tipo_comprovante_id" name="tipo_comprovante_id" required>
<option value="">Selecione o tipo de pagamento</option> <option value="1">1 - Comprovante Padrão</option>
{% for tipo in tipos_pagamento %} {% if current_user.has_permission('gerenciar_tipos_comprovante') %}
<option value="{{ tipo.id }}">{{ tipo.descricao }}</option> <option value="2">2 - Comprovante Especial</option>
{% endfor %} <option value="3">3 - Comprovante Extraordinário</option>
<option value="4">4 - Jornal Avulso</option>
<option value="5">5 - Assinatura de Jornal</option>
<option value="6">6 - Campanha Financeira</option>
{% endif %}
</select> </select>
<div class="invalid-feedback">
Por favor, selecione o tipo de pagamento.
</div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
@@ -52,8 +53,8 @@
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="data_pagamento" class="form-label">Data do Pagamento:</label> <label for="data_comprovante" class="form-label">Data do Comprovante:</label>
<input type="date" class="form-control" id="data_pagamento" name="data_pagamento" <input type="date" class="form-control" id="data_comprovante" name="data_comprovante"
required max="{{ hoje }}"> required max="{{ hoje }}">
<div class="invalid-feedback"> <div class="invalid-feedback">
Por favor, informe uma data válida. Por favor, informe uma data válida.
@@ -64,7 +65,7 @@
<button type="submit" class="btn btn-primary"> <button type="submit" class="btn btn-primary">
<i class="fas fa-save me-1"></i>Registrar <i class="fas fa-save me-1"></i>Registrar
</button> </button>
<a href="{{ url_for('pagamento.listar') }}" class="btn btn-secondary"> <a href="{{ url_for('listar_comprovantes') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left me-1"></i>Voltar <i class="fas fa-arrow-left me-1"></i>Voltar
</a> </a>
</div> </div>

View File

@@ -64,7 +64,7 @@
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<button type="submit" class="btn btn-success">Registrar</button> <button type="submit" class="btn btn-success">Registrar</button>
<a href="{{ url_for('material.listar') }}" class="btn btn-outline-secondary">Voltar</a> <a href="{{ url_for('listar_materiais') }}" class="btn btn-outline-secondary">Voltar</a>
</div> </div>
</form> </form>
</div> </div>

View File

@@ -1,12 +1,12 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block title %}Novo Relatório de Pagamentos{% endblock %} {% block title %}Novo Relatório de Comprovantes{% endblock %}
{% block content %} {% block content %}
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<h1 class="mb-4">Novo Relatório de Pagamentos</h1> <h1 class="mb-4">Novo Relatório de Comprovantes</h1>
{% with messages = get_flashed_messages(with_categories=true) %} {% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %} {% if messages %}
@@ -44,10 +44,10 @@
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="total_pagamentos" class="form-label">Total de Pagamentos</label> <label for="total_comprovantes" class="form-label">Total de Comprovantes</label>
<input type="number" class="form-control" id="total_pagamentos" name="total_pagamentos" step="0.01" required> <input type="number" class="form-control" id="total_comprovantes" name="total_comprovantes" step="0.01" required>
<div class="invalid-feedback"> <div class="invalid-feedback">
Por favor, insira o total de pagamentos. Por favor, insira o total de comprovantes.
</div> </div>
</div> </div>
@@ -61,7 +61,7 @@
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<button type="submit" class="btn btn-success">Registrar</button> <button type="submit" class="btn btn-success">Registrar</button>
<a href="{{ url_for('listar_relatorios_pagamentos') }}" class="btn btn-outline-secondary">Voltar</a> <a href="{{ url_for('listar_relatorios_comprovantes') }}" class="btn btn-outline-secondary">Voltar</a>
</div> </div>
</form> </form>
</div> </div>

View File

@@ -44,7 +44,7 @@
<div class="d-flex justify-content-between mt-4"> <div class="d-flex justify-content-between mt-4">
<button type="submit" class="btn btn-primary">Salvar</button> <button type="submit" class="btn btn-primary">Salvar</button>
<a href="{{ url_for('material.listar_tipos') }}" class="btn btn-secondary">Cancelar</a> <a href="{{ url_for('listar_tipos_materiais') }}" class="btn btn-secondary">Cancelar</a>
</div> </div>
</form> </form>
</div> </div>

View File

@@ -1,207 +0,0 @@
import pytest
import requests
import time
import pyotp
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.chrome.options import Options
class TestIntegrationMenu:
"""Testes de integração para navegação pelos menus como um usuário real"""
@pytest.fixture(scope="class")
def driver(self):
"""Configurar driver do Selenium"""
chrome_options = Options()
chrome_options.add_argument("--headless") # Executar sem interface gráfica
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--disable-dev-shm-usage")
driver = webdriver.Chrome(options=chrome_options)
driver.implicitly_wait(10)
yield driver
driver.quit()
def test_complete_menu_navigation(self, driver):
"""Testa navegação completa pelos menus usando Selenium"""
base_url = "http://localhost:5000"
# 1. Acessar página de login
driver.get(f"{base_url}/login")
assert "Login" in driver.title
# 2. Fazer login
driver.find_element(By.NAME, "email").send_keys("admin")
driver.find_element(By.NAME, "password").send_keys("admin123")
# Gerar OTP
totp = pyotp.TOTP('JBSWY3DPEHPK3PXP')
current_otp = totp.now()
driver.find_element(By.NAME, "otp").send_keys(current_otp)
driver.find_element(By.XPATH, "//button[@type='submit']").click()
# 3. Verificar se chegou na home
WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.CLASS_NAME, "navbar"))
)
# 4. Testar cada menu
menus_to_test = [
("Militantes", "/militantes"),
("Financeiro", "/cotas"),
("Materiais", "/materiais"),
]
for menu_name, expected_url in menus_to_test:
# Encontrar e clicar no menu
menu_link = WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((By.PARTIAL_LINK_TEXT, menu_name))
)
menu_link.click()
# Aguardar dropdown aparecer e clicar no primeiro item
time.sleep(1)
dropdown_items = driver.find_elements(By.CLASS_NAME, "dropdown-item")
if dropdown_items:
dropdown_items[0].click()
# Verificar se a página carregou corretamente
WebDriverWait(driver, 10).until(
lambda d: expected_url in d.current_url or d.find_elements(By.CLASS_NAME, "container")
)
# Verificar se não há erros na página
assert "Erro ao carregar dados do usuário" not in driver.page_source
assert "500" not in driver.page_source
assert "404" not in driver.page_source
def test_api_menu_navigation(self):
"""Testa navegação pelos menus usando requests (simulando browser)"""
base_url = "http://localhost:5000"
session = requests.Session()
# 1. Fazer login via API
totp = pyotp.TOTP('JBSWY3DPEHPK3PXP')
current_otp = totp.now()
login_response = session.post(f"{base_url}/api/login",
json={
'email': 'admin',
'password': 'admin123',
'otp': current_otp
},
headers={'Content-Type': 'application/json'})
assert login_response.status_code == 200
# 2. Testar todas as rotas do menu
menu_routes = [
"/",
"/dashboard",
"/militantes",
"/militantes/novo",
"/cotas",
"/cotas/nova",
"/pagamentos",
"/pagamentos/novo",
"/materiais",
"/materiais/novo",
"/tipos-materiais",
"/tipos-materiais/novo",
"/admin/dashboard"
]
failed_routes = []
for route in menu_routes:
try:
response = session.get(f"{base_url}{route}")
if response.status_code != 200:
failed_routes.append((route, response.status_code))
elif "Erro ao carregar dados do usuário" in response.text:
failed_routes.append((route, "Permission Error"))
elif "csrf_token" in response.text and "is undefined" in response.text:
failed_routes.append((route, "CSRF Error"))
except Exception as e:
failed_routes.append((route, str(e)))
# 3. Verificar resultados
if failed_routes:
error_msg = "Rotas com falhas:\n"
for route, error in failed_routes:
error_msg += f" {route}: {error}\n"
pytest.fail(error_msg)
def test_menu_responsiveness(self):
"""Testa se os menus respondem corretamente em diferentes cenários"""
base_url = "http://localhost:5000"
# Testar sem autenticação
response = requests.get(f"{base_url}/militantes")
assert response.status_code in [302, 401], "Rota protegida deveria redirecionar"
# Testar com autenticação
session = requests.Session()
totp = pyotp.TOTP('JBSWY3DPEHPK3PXP')
current_otp = totp.now()
login_response = session.post(f"{base_url}/api/login",
json={
'email': 'admin',
'password': 'admin123',
'otp': current_otp
},
headers={'Content-Type': 'application/json'})
assert login_response.status_code == 200
# Testar acesso às rotas protegidas
protected_routes = ["/militantes", "/cotas", "/pagamentos", "/materiais"]
for route in protected_routes:
response = session.get(f"{base_url}{route}")
assert response.status_code == 200, f"Rota {route} falhou após autenticação"
# Verificar se não há erros específicos
assert "Erro ao carregar dados do usuário" not in response.text
assert "csrf_token() is undefined" not in response.text
def test_menu_performance(self):
"""Testa performance dos menus"""
base_url = "http://localhost:5000"
session = requests.Session()
# Login
totp = pyotp.TOTP('JBSWY3DPEHPK3PXP')
current_otp = totp.now()
session.post(f"{base_url}/api/login",
json={
'email': 'admin',
'password': 'admin123',
'otp': current_otp
},
headers={'Content-Type': 'application/json'})
# Testar tempo de resposta dos menus
routes_to_test = ["/", "/militantes", "/cotas", "/pagamentos", "/materiais"]
slow_routes = []
for route in routes_to_test:
start_time = time.time()
response = session.get(f"{base_url}{route}")
end_time = time.time()
response_time = end_time - start_time
if response_time > 5.0: # Mais de 5 segundos é muito lento
slow_routes.append((route, response_time))
if slow_routes:
error_msg = "Rotas com performance ruim:\n"
for route, time_taken in slow_routes:
error_msg += f" {route}: {time_taken:.2f}s\n"
pytest.fail(error_msg)

Some files were not shown because too many files have changed in this diff Show More