Compare commits
105 Commits
cota_calc
...
refactor/M
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
911ead7835 | ||
|
|
91d9bef6c6 | ||
|
|
4742a888b7 | ||
|
|
6a3675b735 | ||
|
|
bb6e5c887b | ||
|
|
63ebf09fb6 | ||
|
|
f87e03640d | ||
|
|
debcbe6663 | ||
|
|
d45fefd72c | ||
|
|
62aaec3fbe | ||
|
|
e6057cd566 | ||
|
|
8255f1d933 | ||
|
|
0f32eae5cf | ||
|
|
47f13e7c18 | ||
|
|
53769cf080 | ||
|
|
92bc21dbd8 | ||
|
|
5057802220 | ||
|
|
e43b089155 | ||
|
|
295a433d59 | ||
|
|
203751deeb | ||
|
|
71f926e6be | ||
|
|
8cef19576e | ||
|
|
abc46704c3 | ||
|
|
c640a756df | ||
|
|
3f2e6e3022 | ||
|
|
179ea3cad0 | ||
|
|
b47c9efc21 | ||
|
|
97711d30c7 | ||
|
|
50ef370c2b | ||
|
|
53594517c0 | ||
|
|
874df1d340 | ||
|
|
b170f94058 | ||
|
|
786040162b | ||
|
|
daaa7fd462 | ||
|
|
ad0ea2f259 | ||
|
|
74e5a1f7e3 | ||
|
|
d07a227e80 | ||
|
|
0635003485 | ||
|
|
d931fb4b5e | ||
|
|
a302a259a6 | ||
|
|
75ba696355 | ||
|
|
7f4fe77711 | ||
|
|
c29eed0c69 | ||
|
|
52a6bf9eb0 | ||
|
|
d468f8ff39 | ||
|
|
5527db8729 | ||
|
|
56b8e7aa54 | ||
|
|
9ffc562357 | ||
|
|
3ed3002410 | ||
|
|
f58c340235 | ||
|
|
9158a86655 | ||
|
|
6b23adcb34 | ||
|
|
c7c3b95f0b | ||
|
|
9bb62c81a7 | ||
|
|
c17a3eaa0f | ||
|
|
07605797d1 | ||
|
|
745803fef3 | ||
|
|
241543ea63 | ||
|
|
50516664e4 | ||
|
|
0447524a91 | ||
|
|
77cf5ad99c | ||
|
|
9cc3f408f8 | ||
|
|
758dbdb26d | ||
|
|
83ae798033 | ||
|
|
742f820bc2 | ||
|
|
a28f543478 | ||
|
|
417b5c3f96 | ||
|
|
10ff9cab3b | ||
|
|
8803c971e4 | ||
|
|
d4869dcfaa | ||
|
|
06e7c79488 | ||
|
|
0a2d5c1d23 | ||
|
|
855f97c72b | ||
|
|
8e6ccb70e9 | ||
|
|
65406276ae | ||
|
|
b1acc2fdfc | ||
|
|
c44ce94bef | ||
|
|
ce3b5a4231 | ||
|
|
f0faf4270b | ||
|
|
178a58bb00 | ||
|
|
e9c1f3aedf | ||
|
|
1ff8e97bbc | ||
|
|
b815f77240 | ||
|
|
ba4f6d6de3 | ||
|
|
ac461ce800 | ||
|
|
4f781b2a0e | ||
|
|
32cd4b70c1 | ||
|
|
54261e455c | ||
|
|
9d17c66c46 | ||
|
|
cbaf227e58 | ||
|
|
8dac8dc234 | ||
|
|
bf93e84cec | ||
|
|
449a203926 | ||
|
|
01f5901eb2 | ||
|
|
6370e8f39b | ||
|
|
bae6b1ae14 | ||
|
|
1367389619 | ||
|
|
0f4056fbff | ||
|
|
cccca2ef29 | ||
|
|
986f90a9cd | ||
|
|
14c88bb1e4 | ||
|
|
aa22102b5a | ||
|
|
0d2238d8e0 | ||
|
|
de132b82c1 | ||
|
|
a847389295 |
50
.dockerignore
Normal file
50
.dockerignore
Normal file
@@ -0,0 +1,50 @@
|
||||
# Arquivos e diretórios do Git
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# Arquivos do Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
env/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Arquivos de ambiente
|
||||
.env
|
||||
.venv
|
||||
venv/
|
||||
ENV/
|
||||
|
||||
# Arquivos de IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Arquivos de log
|
||||
*.log
|
||||
|
||||
# Arquivos de banco de dados
|
||||
*.db
|
||||
*.sqlite3
|
||||
|
||||
# Arquivos temporários
|
||||
*.tmp
|
||||
*.bak
|
||||
*.swp
|
||||
*~
|
||||
10
.gitignore
vendored
10
.gitignore
vendored
@@ -260,5 +260,13 @@ poetry.toml
|
||||
pyrightconfig.json
|
||||
|
||||
database.db
|
||||
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
|
||||
|
||||
# Documentação temporária
|
||||
docs/alteracoes_db_connection.md
|
||||
|
||||
# QR Codes
|
||||
*_qr.png
|
||||
*_qr.txt
|
||||
|
||||
39
Dockerfile
Normal file
39
Dockerfile
Normal file
@@ -0,0 +1,39 @@
|
||||
FROM alpine:latest
|
||||
|
||||
# Instalar dependências do sistema
|
||||
RUN apk update && \
|
||||
apk add --no-cache \
|
||||
python3 \
|
||||
py3-pip \
|
||||
make \
|
||||
git \
|
||||
gcc \
|
||||
python3-dev \
|
||||
musl-dev \
|
||||
linux-headers
|
||||
|
||||
# Criar link simbólico para python3
|
||||
RUN ln -sf python3 /usr/bin/python
|
||||
|
||||
# Definir diretório de trabalho
|
||||
WORKDIR /app
|
||||
|
||||
# Copiar arquivos do projeto
|
||||
COPY . .
|
||||
|
||||
# Criar e ativar ambiente virtual
|
||||
RUN python -m venv /venv && \
|
||||
. /venv/bin/activate && \
|
||||
pip install --upgrade pip && \
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Expor a porta que o Flask usa
|
||||
EXPOSE 5000
|
||||
|
||||
# Definir o ambiente virtual como padrão
|
||||
ENV PATH="/venv/bin:$PATH"
|
||||
ENV FLASK_APP=app.py
|
||||
ENV FLASK_ENV=production
|
||||
|
||||
# Comando para rodar a aplicação
|
||||
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]
|
||||
20
Makefile
20
Makefile
@@ -1,5 +1,23 @@
|
||||
install:
|
||||
pip install -r requirements.txt
|
||||
|
||||
clean:
|
||||
rm -rf ~/.local/share/controles/database.db*
|
||||
rm -f admin_qr.png
|
||||
|
||||
init-db: clean
|
||||
python init_db.py
|
||||
|
||||
seed: init-db
|
||||
python seed.py
|
||||
|
||||
init:
|
||||
python app.py --init
|
||||
|
||||
run:
|
||||
python app.py
|
||||
python app.py
|
||||
|
||||
run-with-seed: seed init run
|
||||
|
||||
reset-admin: clean
|
||||
python create_admin.py
|
||||
|
||||
119
README.md
119
README.md
@@ -1,15 +1,116 @@
|
||||
# controles
|
||||
# Sistema de Controles
|
||||
|
||||
## Para instalar
|
||||
Sistema de gestão para controle de militantes, pagamentos, cotas e relatórios.
|
||||
|
||||
```bash
|
||||
make install
|
||||
## Arquitetura MVC
|
||||
|
||||
O projeto segue a arquitetura Model-View-Controller (MVC) para separação de responsabilidades:
|
||||
|
||||
### Models
|
||||
|
||||
Os modelos representam as entidades do sistema e estão organizados em:
|
||||
|
||||
- **models/entities/**: Classes de entidades do banco de dados (SQLAlchemy)
|
||||
- `base.py`: Configuração do SQLAlchemy e classe Base
|
||||
- `usuario.py`: Modelo de usuário
|
||||
- `militante.py`: Modelo de militante
|
||||
- `cota_mensal.py`: Modelo de cota mensal
|
||||
- etc.
|
||||
|
||||
### Controllers
|
||||
|
||||
Os controladores contêm a lógica de negócio e manipulam os dados dos modelos:
|
||||
|
||||
- **controllers/**: Implementação dos controladores
|
||||
- `auth_controller.py`: Controle de autenticação
|
||||
- `usuario_controller.py`: Operações com usuários
|
||||
- `militante_controller.py`: Operações com militantes
|
||||
- `home_controller.py`: Controlador da página inicial
|
||||
- etc.
|
||||
|
||||
### Views
|
||||
|
||||
As views são os templates que exibem os dados para o usuário:
|
||||
|
||||
- **templates/**: Templates Jinja2
|
||||
- Organizados por funcionalidade (admin, militantes, cotas, etc.)
|
||||
|
||||
### Services
|
||||
|
||||
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
|
||||
|
||||
1. Clone o repositório:
|
||||
```
|
||||
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:
|
||||
```
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
4. Inicialize o banco de dados:
|
||||
```
|
||||
python app.py --init
|
||||
```
|
||||
|
||||
5. Execute a aplicação:
|
||||
```
|
||||
python app.py
|
||||
```
|
||||
|
||||
## Credenciais padrão
|
||||
|
||||
- **Administrador**:
|
||||
- Usuário: admin
|
||||
- Senha: admin123
|
||||
|
||||
## Desenvolvimento
|
||||
|
||||
Para adicionar novos recursos, siga a arquitetura MVC:
|
||||
|
||||
1. Crie modelos necessários em `models/entities/`
|
||||
2. Implemente serviços para acesso a dados em `services/`
|
||||
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/`
|
||||
|
||||
## Testes
|
||||
|
||||
Execute os testes usando pytest:
|
||||
|
||||
```
|
||||
python -m pytest
|
||||
```
|
||||
|
||||
## Para executar
|
||||
Ou use o script de teste:
|
||||
|
||||
```bash
|
||||
make run
|
||||
```
|
||||
|
||||
Acesse por: http://127.0.0.1:5000
|
||||
./run_tests.sh
|
||||
```
|
||||
Binary file not shown.
123
app.py.new
Normal file
123
app.py.new
Normal 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'
|
||||
)
|
||||
124
controllers/auth_controller.py
Normal file
124
controllers/auth_controller.py
Normal file
@@ -0,0 +1,124 @@
|
||||
from flask import session, flash, redirect, url_for, request
|
||||
from flask_login import login_user, logout_user, current_user
|
||||
from datetime import datetime
|
||||
import pyotp
|
||||
import qrcode
|
||||
from io import BytesIO
|
||||
import base64
|
||||
|
||||
from models.entities.usuario import Usuario
|
||||
from services.database_service import DatabaseService
|
||||
|
||||
class AuthController:
|
||||
"""Controlador para funções de autenticação"""
|
||||
|
||||
@staticmethod
|
||||
def login():
|
||||
"""Processa o login de usuário"""
|
||||
if request.method != "POST":
|
||||
return False
|
||||
|
||||
email_or_username = request.form.get("email")
|
||||
password = request.form.get("password")
|
||||
otp = request.form.get("otp")
|
||||
|
||||
if not all([email_or_username, password]):
|
||||
flash("Email/usuário e senha são obrigatórios.", "danger")
|
||||
return False
|
||||
|
||||
db = DatabaseService.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):
|
||||
flash("Email/usuário ou senha incorretos.", "danger")
|
||||
return False
|
||||
|
||||
# Verificar OTP se o usuário tiver configurado
|
||||
if user.otp_secret and not otp:
|
||||
flash("Código OTP é obrigatório para sua conta.", "danger")
|
||||
return False
|
||||
|
||||
if user.otp_secret and not user.verify_otp(otp):
|
||||
flash("Código OTP inválido.", "danger")
|
||||
return False
|
||||
|
||||
# Atualizar último login
|
||||
user.ultimo_login = datetime.utcnow()
|
||||
db.commit()
|
||||
|
||||
# Fazer login e setar sessão
|
||||
login_user(user)
|
||||
session['user_id'] = user.id
|
||||
session['username'] = user.username
|
||||
session['is_admin'] = user.is_admin
|
||||
|
||||
return True
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@staticmethod
|
||||
def logout():
|
||||
"""Processa o logout de usuário"""
|
||||
db = DatabaseService.get_db_connection()
|
||||
try:
|
||||
user = current_user
|
||||
if user.is_authenticated:
|
||||
user.logout()
|
||||
db.commit()
|
||||
logout_user()
|
||||
flash('Logout realizado com sucesso!', 'success')
|
||||
return True
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@staticmethod
|
||||
def alterar_senha(user_id, senha_atual, nova_senha, confirmar_senha):
|
||||
"""Altera a senha do usuário"""
|
||||
if not all([senha_atual, nova_senha, confirmar_senha]):
|
||||
flash("Todos os campos são obrigatórios.", "error")
|
||||
return False
|
||||
|
||||
if nova_senha != confirmar_senha:
|
||||
flash("As senhas não coincidem.", "error")
|
||||
return False
|
||||
|
||||
db = DatabaseService.get_db_connection()
|
||||
try:
|
||||
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):
|
||||
flash("Senha atual incorreta.", "error")
|
||||
return False
|
||||
|
||||
user.set_password(nova_senha)
|
||||
db.commit()
|
||||
flash("Senha alterada com sucesso!", "success")
|
||||
return True
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@staticmethod
|
||||
def generate_qr_code(user):
|
||||
"""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
|
||||
80
controllers/home_controller.py
Normal file
80
controllers/home_controller.py
Normal file
@@ -0,0 +1,80 @@
|
||||
from flask import session, render_template
|
||||
from datetime import datetime
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
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
|
||||
|
||||
class HomeController:
|
||||
"""Controlador para página inicial e dashboard"""
|
||||
|
||||
@staticmethod
|
||||
def dashboard():
|
||||
"""Gera dados para o dashboard principal"""
|
||||
db = DatabaseService.get_db_connection()
|
||||
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()
|
||||
|
||||
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:
|
||||
db.close()
|
||||
270
controllers/militante_controller.py
Normal file
270
controllers/militante_controller.py
Normal file
@@ -0,0 +1,270 @@
|
||||
from flask import request, jsonify, flash
|
||||
from datetime import datetime
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
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
|
||||
|
||||
class MilitanteController:
|
||||
"""Controlador para operações com militantes"""
|
||||
|
||||
@staticmethod
|
||||
def listar_militantes():
|
||||
"""Lista todos os militantes"""
|
||||
return MilitanteService.listar_militantes()
|
||||
|
||||
@staticmethod
|
||||
def buscar_militante(militante_id):
|
||||
"""Busca um militante pelo ID"""
|
||||
militante = MilitanteService.buscar_militante(militante_id)
|
||||
if not militante:
|
||||
raise NotFound(f"Militante com ID {militante_id} não encontrado")
|
||||
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({
|
||||
'status': 'error',
|
||||
'message': 'CPF inválido'
|
||||
}), 400
|
||||
|
||||
# Verificar se já existe militante com este CPF
|
||||
if MilitanteService.buscar_por_cpf(cpf):
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'CPF já cadastrado'
|
||||
}), 400
|
||||
|
||||
try:
|
||||
# Criar endereço
|
||||
endereco = Endereco(
|
||||
cep=form_data.get('cep'),
|
||||
estado=form_data.get('estado'),
|
||||
cidade=form_data.get('cidade'),
|
||||
bairro=form_data.get('bairro'),
|
||||
rua=form_data.get('logradouro'),
|
||||
numero=form_data.get('numero'),
|
||||
complemento=form_data.get('complemento')
|
||||
)
|
||||
|
||||
# Salvar endereço para obter ID
|
||||
endereco_id = MilitanteService.salvar_endereco(endereco)
|
||||
|
||||
# Processar datas
|
||||
data_nascimento = datetime.strptime(form_data.get('data_nascimento'), '%Y-%m-%d') if form_data.get('data_nascimento') else None
|
||||
data_entrada_oci = datetime.strptime(form_data.get('data_entrada_oci'), '%Y-%m-%d') if form_data.get('data_entrada_oci') else None
|
||||
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
|
||||
militante = Militante(
|
||||
# Dados Básicos
|
||||
nome=form_data.get('nome'),
|
||||
cpf=cpf,
|
||||
titulo_eleitoral=form_data.get('titulo_eleitoral'),
|
||||
data_nascimento=data_nascimento,
|
||||
data_entrada_oci=data_entrada_oci,
|
||||
data_efetivacao_oci=data_efetivacao_oci,
|
||||
|
||||
# Contato
|
||||
telefone1=form_data.get('telefone1'),
|
||||
telefone2=form_data.get('telefone2'),
|
||||
endereco_id=endereco_id,
|
||||
|
||||
# Profissional
|
||||
profissao=form_data.get('profissao'),
|
||||
regime_trabalho=form_data.get('regime_trabalho'),
|
||||
empresa=form_data.get('empresa'),
|
||||
contratante=form_data.get('contratante'),
|
||||
|
||||
# Acadêmico
|
||||
instituicao_ensino=form_data.get('instituicao_ensino'),
|
||||
tipo_instituicao=form_data.get('tipo_instituicao'),
|
||||
|
||||
# Sindical
|
||||
sindicato=form_data.get('sindicato'),
|
||||
cargo_sindical=form_data.get('cargo_sindical'),
|
||||
central_sindical=form_data.get('central_sindical'),
|
||||
dirigente_sindical=form_data.get('dirigente_sindical') == 'on',
|
||||
|
||||
# Organização
|
||||
estado=EstadoMilitante(form_data.get('estado', 'ATIVO')),
|
||||
celula_id=form_data.get('celula_id', type=int),
|
||||
responsabilidades=form_data.get('responsabilidades', type=int, default=0),
|
||||
|
||||
# Por padrão, todo novo militante é aspirante
|
||||
aspirante=True,
|
||||
data_inicio_aspirante=datetime.now()
|
||||
)
|
||||
|
||||
# Salvar militante para obter ID
|
||||
militante_id = MilitanteService.salvar_militante(militante)
|
||||
|
||||
# Adicionar email principal se fornecido
|
||||
email = form_data.get('email')
|
||||
if email:
|
||||
email_militante = EmailMilitante(
|
||||
endereco_email=email,
|
||||
militante_id=militante_id
|
||||
)
|
||||
MilitanteService.salvar_email_militante(email_militante)
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'message': 'Militante criado com sucesso!',
|
||||
'id': militante_id
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': f'Erro ao criar militante: {str(e)}'
|
||||
}), 500
|
||||
|
||||
@staticmethod
|
||||
def atualizar_militante(militante_id, form_data):
|
||||
"""Atualiza um militante existente"""
|
||||
try:
|
||||
militante = MilitanteService.buscar_militante(militante_id)
|
||||
if not militante:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Militante não encontrado'
|
||||
}), 404
|
||||
|
||||
# Obter dados do formulário
|
||||
nome = form_data.get('nome')
|
||||
cpf = form_data.get('cpf')
|
||||
titulo_eleitoral = form_data.get('titulo_eleitoral')
|
||||
data_nascimento = form_data.get('data_nascimento')
|
||||
data_entrada_oci = form_data.get('data_entrada_oci')
|
||||
data_efetivacao_oci = form_data.get('data_efetivacao_oci')
|
||||
telefone1 = form_data.get('telefone1')
|
||||
telefone2 = form_data.get('telefone2')
|
||||
email = form_data.get('email')
|
||||
|
||||
# Validar e converter datas
|
||||
try:
|
||||
data_nascimento = converter_data(data_nascimento) if data_nascimento else None
|
||||
data_entrada_oci = converter_data(data_entrada_oci) if data_entrada_oci else None
|
||||
data_efetivacao_oci = converter_data(data_efetivacao_oci) if data_efetivacao_oci else None
|
||||
|
||||
# Validar sequência lógica das datas
|
||||
validar_sequencia_datas(
|
||||
data_nascimento=data_nascimento,
|
||||
data_entrada=data_entrada_oci,
|
||||
data_efetivacao=data_efetivacao_oci
|
||||
)
|
||||
|
||||
except ValueError as e:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': str(e)
|
||||
}), 400
|
||||
|
||||
# Atualizar dados básicos
|
||||
if nome: militante.nome = nome
|
||||
if cpf: militante.cpf = cpf
|
||||
if titulo_eleitoral: militante.titulo_eleitoral = titulo_eleitoral
|
||||
militante.data_nascimento = data_nascimento
|
||||
militante.data_entrada_oci = data_entrada_oci
|
||||
militante.data_efetivacao_oci = data_efetivacao_oci
|
||||
militante.telefone1 = telefone1
|
||||
militante.telefone2 = telefone2
|
||||
|
||||
# Calcular idade
|
||||
if data_nascimento:
|
||||
militante.idade = calcular_idade(data_nascimento)
|
||||
|
||||
# Atualizar ou criar email
|
||||
if email:
|
||||
MilitanteService.atualizar_email_militante(militante_id, email)
|
||||
|
||||
# Salvar alterações
|
||||
MilitanteService.salvar_militante(militante)
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'message': 'Militante atualizado com sucesso',
|
||||
'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:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Militante não encontrado'
|
||||
}), 404
|
||||
|
||||
# Função auxiliar para formatar data com validação
|
||||
def formatar_data_segura(data):
|
||||
try:
|
||||
if not data:
|
||||
return None
|
||||
return data.strftime('%Y-%m-%d')
|
||||
except Exception as e:
|
||||
print(f"Erro ao formatar data: {str(e)}, valor: {data}")
|
||||
return None
|
||||
|
||||
# Formatar datas com validação
|
||||
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,
|
||||
'nome': militante.nome,
|
||||
'cpf': militante.cpf,
|
||||
'titulo_eleitoral': militante.titulo_eleitoral,
|
||||
'data_nascimento': data_nascimento,
|
||||
'data_entrada_oci': data_entrada_oci,
|
||||
'data_efetivacao_oci': data_efetivacao_oci,
|
||||
'emails': [email.endereco_email for email in militante.emails] if militante.emails else [],
|
||||
'telefone1': militante.telefone1,
|
||||
'telefone2': militante.telefone2,
|
||||
'celula_id': militante.celula_id,
|
||||
'responsabilidades_valor': militante.responsabilidades,
|
||||
'sindicato': militante.sindicato,
|
||||
'cargo_sindical': militante.cargo_sindical,
|
||||
'central_sindical': militante.central_sindical,
|
||||
'dirigente_sindical': militante.dirigente_sindical
|
||||
})
|
||||
202
controllers/usuario_controller.py
Normal file
202
controllers/usuario_controller.py
Normal file
@@ -0,0 +1,202 @@
|
||||
from flask import request, jsonify, flash, session
|
||||
from flask_login import current_user
|
||||
from datetime import datetime
|
||||
import secrets
|
||||
import pyotp
|
||||
|
||||
from models.entities.usuario import Usuario
|
||||
from services.usuario_service import UsuarioService
|
||||
from services.database_service import DatabaseService
|
||||
|
||||
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
|
||||
if UsuarioService.buscar_por_username(data['username']):
|
||||
flash('Nome de usuário já existe.', 'danger')
|
||||
return False
|
||||
|
||||
try:
|
||||
# Criar usuário
|
||||
usuario = Usuario(
|
||||
username=data['username'],
|
||||
email=data['email'],
|
||||
nome=data.get('nome'),
|
||||
is_admin=data.get('is_admin', False)
|
||||
)
|
||||
|
||||
# Definir senha
|
||||
usuario.set_password(data['password'])
|
||||
|
||||
# Gerar OTP secret
|
||||
usuario.otp_secret = pyotp.random_base32()
|
||||
|
||||
# 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:
|
||||
flash(f'Erro ao cadastrar usuário: {str(e)}', 'danger')
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def atualizar_usuario(user_id, data):
|
||||
"""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
|
||||
|
||||
usuario = UsuarioService.buscar_usuario(user_id)
|
||||
if not usuario:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Usuário não encontrado.'
|
||||
}), 404
|
||||
|
||||
usuario.ativo = not usuario.ativo
|
||||
if UsuarioService.salvar_usuario(usuario):
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'Usuário {\'ativado\' if usuario.ativo else \'desativado\'}'
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Erro ao salvar alterações.'
|
||||
}), 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
|
||||
|
||||
# Gerar nova senha
|
||||
new_password = secrets.token_urlsafe(8)
|
||||
usuario.set_password(new_password)
|
||||
|
||||
# Salvar alterações
|
||||
if UsuarioService.salvar_usuario(usuario):
|
||||
return True, new_password
|
||||
else:
|
||||
flash('Erro ao resetar senha.', 'danger')
|
||||
return False, None
|
||||
|
||||
@staticmethod
|
||||
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'}
|
||||
151
create_admin.py
Normal file
151
create_admin.py
Normal file
@@ -0,0 +1,151 @@
|
||||
from functions.database import init_database, Usuario, Role, get_db_connection
|
||||
import qrcode
|
||||
import os
|
||||
from pathlib import Path
|
||||
import pyotp
|
||||
|
||||
def generate_qr_code(user):
|
||||
"""
|
||||
Gera o QR code para um usuário específico
|
||||
|
||||
Args:
|
||||
user: Instância do modelo Usuario
|
||||
|
||||
Returns:
|
||||
Path: Caminho do arquivo QR code gerado
|
||||
"""
|
||||
# Gerar QR Code apenas na raiz do projeto
|
||||
qr_path = Path('admin_qr.png')
|
||||
|
||||
# Remover arquivo antigo se existir
|
||||
if qr_path.exists():
|
||||
os.remove(str(qr_path))
|
||||
|
||||
# Gerar e salvar QR Code
|
||||
qr = qrcode.QRCode(version=1, box_size=10, border=5)
|
||||
|
||||
# Gerar URI do OTP
|
||||
totp = pyotp.TOTP(user.otp_secret)
|
||||
otp_uri = totp.provisioning_uri(
|
||||
name=user.username,
|
||||
issuer_name="Sistema de Controles"
|
||||
)
|
||||
|
||||
qr.add_data(otp_uri)
|
||||
qr.make(fit=True)
|
||||
img = qr.make_image(fill_color="black", back_color="white")
|
||||
img.save(str(qr_path))
|
||||
|
||||
print(f"\nQR Code gerado em: {os.path.abspath(qr_path)}")
|
||||
|
||||
return qr_path, otp_uri
|
||||
|
||||
def create_admin_user():
|
||||
"""Cria ou atualiza o usuário admin"""
|
||||
try:
|
||||
# Inicializar banco de dados
|
||||
init_database()
|
||||
|
||||
# Criar sessão
|
||||
db = get_db_connection()
|
||||
|
||||
try:
|
||||
# Verificar se já existe um usuário admin
|
||||
admin = db.query(Usuario).filter_by(username="admin").first()
|
||||
|
||||
if admin:
|
||||
print("\n=== Usuário Admin Encontrado ===")
|
||||
if not admin.otp_secret:
|
||||
print("Gerando novo segredo OTP...")
|
||||
admin.generate_otp_secret()
|
||||
db.commit()
|
||||
else:
|
||||
print("\n=== Criando Novo Usuário Admin ===")
|
||||
# Criar novo usuário admin
|
||||
admin = Usuario(
|
||||
username="admin",
|
||||
email="admin@example.com",
|
||||
is_admin=True
|
||||
)
|
||||
admin.set_password("admin123")
|
||||
admin.generate_otp_secret()
|
||||
|
||||
# Buscar ou criar role de admin
|
||||
admin_role = db.query(Role).filter_by(nome="admin").first()
|
||||
if not admin_role:
|
||||
admin_role = Role(nome="admin", nivel=0) # Nível 0 é o mais alto
|
||||
db.add(admin_role)
|
||||
|
||||
# Adicionar role ao usuário
|
||||
admin.roles.append(admin_role)
|
||||
|
||||
# Adicionar e fazer commit
|
||||
db.add(admin)
|
||||
db.commit()
|
||||
|
||||
# Gerar QR code apenas se solicitado ou se for novo usuário
|
||||
if not os.path.exists('admin_qr.png'):
|
||||
qr_path, otp_uri = generate_qr_code(admin)
|
||||
print("\n=== QR Code Gerado ===")
|
||||
print(f"QR Code salvo em: {qr_path}")
|
||||
print(f"URI do OTP: {otp_uri}")
|
||||
else:
|
||||
print("\n=== QR Code Existente ===")
|
||||
print("Usando QR Code existente em: admin_qr.png")
|
||||
qr_path = 'admin_qr.png'
|
||||
|
||||
# Mostrar informações
|
||||
print("\n=== Informações do Admin ===")
|
||||
print(f"Username: {admin.username}")
|
||||
print(f"Email: {admin.email}")
|
||||
print(f"Senha: admin123")
|
||||
print(f"Segredo OTP: {admin.otp_secret}")
|
||||
|
||||
# Gerar código atual para verificação
|
||||
totp = pyotp.TOTP(admin.otp_secret)
|
||||
current_code = totp.now()
|
||||
print("\n=== Verificação do OTP ===")
|
||||
print(f"Código OTP atual: {current_code}")
|
||||
print(f"Verificação do código: {totp.verify(current_code)}")
|
||||
|
||||
print("\n=== Instruções para Configuração ===")
|
||||
print("1. Instale um aplicativo autenticador no seu celular")
|
||||
print(" (Google Authenticator, Microsoft Authenticator, etc)")
|
||||
print("2. Abra o aplicativo")
|
||||
print("3. Selecione a opção para adicionar uma nova conta")
|
||||
print("4. Escaneie o QR Code salvo em:", qr_path)
|
||||
print("\nOU configure manualmente:")
|
||||
print(f"- Nome da conta: {admin.username}")
|
||||
print(f"- Segredo: {admin.otp_secret}")
|
||||
print("- Tipo: Baseado em tempo (TOTP)")
|
||||
print("- Algoritmo: SHA1")
|
||||
print("- Dígitos: 6")
|
||||
print("- Intervalo: 30 segundos")
|
||||
|
||||
# Verificação final
|
||||
print("\n=== Teste de Verificação ===")
|
||||
test_code = totp.now()
|
||||
print(f"Código de teste: {test_code}")
|
||||
is_valid = admin.verify_otp(test_code)
|
||||
print(f"Verificação do código: {'Sucesso' if is_valid else 'Falha'}")
|
||||
|
||||
if not is_valid:
|
||||
print("\nALERTA: Verificação do OTP falhou!")
|
||||
print("Por favor, verifique se o segredo OTP está correto.")
|
||||
|
||||
# Fazer commit final para garantir que tudo foi salvo
|
||||
db.commit()
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise e
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
except Exception as e:
|
||||
print(f"\nErro durante a execução: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
if __name__ == "__main__":
|
||||
create_admin_user()
|
||||
56
create_test_users.py
Normal file
56
create_test_users.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from functions.database import get_db_connection, Usuario, Role
|
||||
from werkzeug.security import generate_password_hash
|
||||
|
||||
def create_test_users():
|
||||
"""Cria usuários de teste"""
|
||||
db = get_db_connection()
|
||||
try:
|
||||
# Lista de usuários de teste
|
||||
test_users = [
|
||||
{
|
||||
'username': 'aligner',
|
||||
'email': 'aligner@test.com',
|
||||
'password': 'Test123!@#',
|
||||
'is_admin': False
|
||||
},
|
||||
{
|
||||
'username': 'tester',
|
||||
'email': 'tester@test.com',
|
||||
'password': 'Test123!@#',
|
||||
'is_admin': False
|
||||
},
|
||||
{
|
||||
'username': 'deployer',
|
||||
'email': 'deployer@test.com',
|
||||
'password': 'Test123!@#',
|
||||
'is_admin': False
|
||||
}
|
||||
]
|
||||
|
||||
# Criar cada usuário
|
||||
for user_data in test_users:
|
||||
user = db.query(Usuario).filter_by(username=user_data['username']).first()
|
||||
|
||||
if not user:
|
||||
user = Usuario(
|
||||
username=user_data['username'],
|
||||
email=user_data['email'],
|
||||
is_admin=user_data['is_admin']
|
||||
)
|
||||
user.set_password(user_data['password'])
|
||||
db.add(user)
|
||||
print(f"Usuário {user_data['username']} criado")
|
||||
else:
|
||||
print(f"Usuário {user_data['username']} já existe")
|
||||
|
||||
db.commit()
|
||||
print("Usuários de teste criados com sucesso")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Erro ao criar usuários de teste: {str(e)}")
|
||||
db.rollback()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
create_test_users()
|
||||
8
dao.py
8
dao.py
@@ -1,8 +0,0 @@
|
||||
from functions.database import execute_query
|
||||
|
||||
def get_user_by_email(email):
|
||||
query = "SELECT * FROM users WHERE email = %s"
|
||||
cursor = execute_query(query, (email,))
|
||||
if cursor:
|
||||
return cursor.fetchone()
|
||||
return None
|
||||
99
database.sql
99
database.sql
@@ -1,99 +0,0 @@
|
||||
-- Tabela de Militantes
|
||||
CREATE TABLE militantes (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
nome VARCHAR(100) NOT NULL,
|
||||
cpf VARCHAR(14) UNIQUE,
|
||||
email VARCHAR(100) UNIQUE,
|
||||
telefone VARCHAR(15),
|
||||
endereco VARCHAR(255),
|
||||
filiado BOOLEAN DEFAULT false
|
||||
);
|
||||
|
||||
-- Tabela de Cotas Mensais
|
||||
CREATE TABLE cotas_mensais (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
militante_id INT,
|
||||
valor_antigo DECIMAL(10, 2) NOT NULL,
|
||||
valor_novo DECIMAL(10, 2) NOT NULL,
|
||||
data_alteracao DATE NOT NULL,
|
||||
FOREIGN KEY (militante_id) REFERENCES militantes(id)
|
||||
);
|
||||
|
||||
-- Tabela de Pagamentos
|
||||
CREATE TABLE tipos_pagamento (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
descricao VARCHAR(100) NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE pagamentos (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
militante_id INT,
|
||||
tipo_pagamento_id INT,
|
||||
valor DECIMAL(10, 2) NOT NULL,
|
||||
data_pagamento DATE NOT NULL,
|
||||
FOREIGN KEY (militante_id) REFERENCES militantes(id),
|
||||
FOREIGN KEY (tipo_pagamento_id) REFERENCES tipos_pagamento(id)
|
||||
);
|
||||
|
||||
-- Tabela de Tipos de Materiais
|
||||
CREATE TABLE tipos_materiais (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
descricao VARCHAR(100) NOT NULL
|
||||
);
|
||||
|
||||
-- Tabela de Materiais Vendidos
|
||||
CREATE TABLE materiais_vendidos (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
militante_id INT,
|
||||
tipo_material_id INT,
|
||||
descricao VARCHAR(255) NOT NULL,
|
||||
valor DECIMAL(10, 2) NOT NULL,
|
||||
data_venda DATE NOT NULL,
|
||||
FOREIGN KEY (militante_id) REFERENCES militantes(id),
|
||||
FOREIGN KEY (tipo_material_id) REFERENCES tipos_materiais(id)
|
||||
);
|
||||
|
||||
-- Tabela de Vendas de Jornais Avulsos
|
||||
CREATE TABLE vendas_jornais_avulsos (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
militante_id INT,
|
||||
quantidade INT NOT NULL,
|
||||
valor_total DECIMAL(10, 2) NOT NULL,
|
||||
data_venda DATE NOT NULL,
|
||||
FOREIGN KEY (militante_id) REFERENCES militantes(id)
|
||||
);
|
||||
|
||||
-- Tabela de Assinaturas Anuais
|
||||
CREATE TABLE assinaturas_anuais (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
militante_id INT,
|
||||
tipo_material_id INT,
|
||||
quantidade INT NOT NULL,
|
||||
valor_total DECIMAL(10, 2) NOT NULL,
|
||||
data_inicio DATE NOT NULL,
|
||||
data_fim DATE NOT NULL,
|
||||
FOREIGN KEY (militante_id) REFERENCES militantes(id),
|
||||
FOREIGN KEY (tipo_material_id) REFERENCES tipos_materiais(id)
|
||||
);
|
||||
|
||||
-- Tabela de Relatório de Cotas Mensais
|
||||
CREATE TABLE relatorio_cotas_mensais (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
setor_id INT,
|
||||
comite_id INT,
|
||||
total_cotas DECIMAL(10, 2) NOT NULL,
|
||||
data_relatorio DATE NOT NULL,
|
||||
FOREIGN KEY (setor_id) REFERENCES setores(id),
|
||||
FOREIGN KEY (comite_id) REFERENCES comites_centrais(id)
|
||||
);
|
||||
|
||||
-- Tabela de Relatório de Vendas de Materiais
|
||||
CREATE TABLE relatorio_vendas_materiais (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
setor_id INT,
|
||||
comite_id INT,
|
||||
total_vendas DECIMAL(10, 2) NOT NULL,
|
||||
data_relatorio DATE NOT NULL,
|
||||
FOREIGN KEY (setor_id) REFERENCES setores(id),
|
||||
FOREIGN KEY (comite_id) REFERENCES comites_centrais(id)
|
||||
);
|
||||
14
docker-compose.yml
Normal file
14
docker-compose.yml
Normal file
@@ -0,0 +1,14 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
ports:
|
||||
- "5000:5000"
|
||||
volumes:
|
||||
- .:/app
|
||||
- ~/.local/share/controles:/root/.local/share/controles
|
||||
environment:
|
||||
- FLASK_ENV=development
|
||||
- FLASK_APP=app.py
|
||||
restart: unless-stopped
|
||||
165
docs/README.md
Normal file
165
docs/README.md
Normal file
@@ -0,0 +1,165 @@
|
||||
# Sistema de Controle OCI
|
||||
|
||||
## Hierarquia e Permissões
|
||||
|
||||
### Níveis de Acesso
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
3. **Membro de Setor**
|
||||
- Pode ver apenas os dados do setor ao qual pertence
|
||||
- Não pode alterar níveis de outros usuários
|
||||
|
||||
4. **Secretário de Setor**
|
||||
- Pode ver e gerenciar todos os dados do seu setor
|
||||
- Pode alterar níveis de militantes do setor, transformando-os em secretários
|
||||
- Não pode alterar níveis de membros de outros setores
|
||||
|
||||
5. **Membro de CR**
|
||||
- Pode ver apenas os dados do CR ao qual pertence
|
||||
- Não pode alterar níveis de outros usuários
|
||||
|
||||
6. **Secretário de CR**
|
||||
- Pode ver e gerenciar todos os dados do seu CR
|
||||
- Pode alterar níveis de membros do CR
|
||||
- Não pode alterar níveis de membros de outros CRs
|
||||
|
||||
7. **Membro do CC**
|
||||
- Pode ver todos os dados do sistema
|
||||
- Não pode alterar níveis de outros usuários
|
||||
|
||||
8. **Secretário Geral e Secretário de Organização**
|
||||
- Pode ver todos os dados do sistema
|
||||
- Pode alterar níveis de qualquer usuário em qualquer instância
|
||||
|
||||
### Regras de Visualização
|
||||
|
||||
- Cada militante só pode ver os membros da sua própria célula
|
||||
- Membros de setor só veem dados do setor ao qual pertencem
|
||||
- Membros de CR só veem informações do CR ao qual pertencem
|
||||
- Membros do CC podem ver todas as informações do sistema
|
||||
|
||||
### Regras de Edição
|
||||
|
||||
- Apenas o Secretário Geral e o Secretário de Organização podem alterar níveis em todas as instâncias
|
||||
- Secretários de CR podem alterar níveis apenas dentro do seu CR
|
||||
- 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
|
||||
54
docs/alteracoes_db_connection.md
Normal file
54
docs/alteracoes_db_connection.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Alterações no Gerenciamento de Conexões com o Banco de Dados
|
||||
|
||||
## Commit
|
||||
- ID: [ID do commit será adicionado após o commit]
|
||||
- Data: [Data do commit]
|
||||
- Autor: [Nome do autor]
|
||||
|
||||
## Contexto
|
||||
O sistema estava utilizando uma única sessão global do SQLAlchemy (`db_session`) que era criada no início da aplicação. Isso poderia causar problemas de concorrência e vazamento de recursos.
|
||||
|
||||
## Alterações Realizadas
|
||||
|
||||
### 1. Remoção da Sessão Global
|
||||
- Removida a linha `db_session = get_db_connection()` do início do arquivo
|
||||
- Todas as rotas agora criam sua própria sessão
|
||||
|
||||
### 2. Novo Padrão de Gerenciamento de Sessão
|
||||
Em cada rota, implementamos o seguinte padrão:
|
||||
```python
|
||||
db = get_db_connection()
|
||||
try:
|
||||
# Operações com o banco
|
||||
db.commit()
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
# Tratamento de erro
|
||||
finally:
|
||||
db.close()
|
||||
```
|
||||
|
||||
### 3. Melhorias no Tratamento de Erros
|
||||
- Adicionado `db.rollback()` em caso de exceção
|
||||
- Melhoradas as mensagens de erro
|
||||
- Garantido que a sessão seja fechada mesmo em caso de erro
|
||||
|
||||
### 4. Padronização de Código
|
||||
- Uso de `request.form.get()` ao invés de acessar diretamente o dicionário
|
||||
- Conversão explícita de tipos (float, int, date)
|
||||
- Validação de dados antes de criar objetos
|
||||
- Mensagens de feedback mais claras para o usuário
|
||||
|
||||
## Impacto no Frontend
|
||||
Não houve alterações necessárias nos templates, pois as mudanças foram apenas na forma como o backend gerencia as conexões com o banco de dados.
|
||||
|
||||
## Benefícios
|
||||
1. Maior segurança (evita vazamentos de recursos)
|
||||
2. Maior robustez (melhor tratamento de erros)
|
||||
3. Código mais fácil de manter (padronização)
|
||||
4. Maior eficiência (sessões são fechadas adequadamente)
|
||||
|
||||
## Observações
|
||||
- Esta alteração foi feita para melhorar a arquitetura do sistema
|
||||
- Não afeta a funcionalidade existente
|
||||
- Recomenda-se seguir este padrão em novas implementações
|
||||
243
docs/rbac.md
Normal file
243
docs/rbac.md
Normal file
@@ -0,0 +1,243 @@
|
||||
# Sistema de Permissões RBAC (Role-Based Access Control)
|
||||
|
||||
## Níveis de Permissão
|
||||
|
||||
O sistema de permissões é hierárquico, onde cada nível herda as permissões do nível anterior. A hierarquia é a seguinte (do menor para o maior nível):
|
||||
|
||||
### 1. Militante Básico
|
||||
- Acesso apenas aos seus próprios dados
|
||||
- Visualização de sua célula
|
||||
- Sem permissões administrativas
|
||||
|
||||
### 2. Secretário de Célula
|
||||
- Todas as permissões do Militante Básico
|
||||
- Gerenciamento de militantes da sua célula
|
||||
- Visualização de dados da célula
|
||||
- Cadastro de novos militantes na célula
|
||||
|
||||
### 3. Membro de Setor
|
||||
- Todas as permissões do Secretário de Célula
|
||||
- Visualização de dados de todas as células do setor
|
||||
- Acesso a relatórios do setor
|
||||
|
||||
### 4. Secretário de Setor
|
||||
- Todas as permissões do Membro de Setor
|
||||
- Gerenciamento de todas as células do setor
|
||||
- Criação de novas células no setor
|
||||
- Geração de relatórios do setor
|
||||
- Gerenciamento de militantes do setor
|
||||
|
||||
### 5. Membro de CR (Comitê Regional)
|
||||
- Todas as permissões do Secretário de Setor
|
||||
- Visualização de dados de todos os setores do CR
|
||||
- Acesso a relatórios do CR
|
||||
|
||||
### 6. Secretário de CR
|
||||
- Todas as permissões do Membro de CR
|
||||
- Gerenciamento de todos os setores do CR
|
||||
- Criação de novos setores no CR
|
||||
- Geração de relatórios do CR
|
||||
- Gerenciamento de militantes do CR
|
||||
|
||||
### 7. Membro do CC (Comitê Central)
|
||||
- Todas as permissões do Secretário de CR
|
||||
- Visualização de dados de todos os CRs
|
||||
- Acesso a relatórios nacionais
|
||||
|
||||
### 8. Secretário Geral / Secretário de Organização do CC
|
||||
- Todas as permissões do Membro do CC
|
||||
- Gerenciamento de todos os CRs
|
||||
- Criação de novos CRs
|
||||
- Geração de relatórios nacionais
|
||||
- Gerenciamento de todos os militantes
|
||||
- Configurações do sistema
|
||||
|
||||
## Implementação Técnica
|
||||
|
||||
O sistema RBAC é implementado através de:
|
||||
|
||||
1. **Roles**: Definem os níveis de acesso
|
||||
2. **Permissions**: Definem as ações permitidas
|
||||
3. **Role-Permission Mapping**: Mapeia quais permissões cada role possui
|
||||
4. **User-Role Assignment**: Atribui roles aos usuários
|
||||
|
||||
### Estrutura do Banco de Dados
|
||||
|
||||
```sql
|
||||
-- Roles
|
||||
CREATE TABLE roles (
|
||||
id INTEGER PRIMARY KEY,
|
||||
nome VARCHAR(50) UNIQUE NOT NULL,
|
||||
nivel INTEGER NOT NULL,
|
||||
descricao TEXT
|
||||
);
|
||||
|
||||
-- Permissions
|
||||
CREATE TABLE permissions (
|
||||
id INTEGER PRIMARY KEY,
|
||||
nome VARCHAR(50) UNIQUE NOT NULL,
|
||||
descricao TEXT
|
||||
);
|
||||
|
||||
-- Role-Permission Mapping
|
||||
CREATE TABLE role_permissions (
|
||||
role_id INTEGER,
|
||||
permission_id INTEGER,
|
||||
PRIMARY KEY (role_id, permission_id),
|
||||
FOREIGN KEY (role_id) REFERENCES roles(id),
|
||||
FOREIGN KEY (permission_id) REFERENCES permissions(id)
|
||||
);
|
||||
|
||||
-- User-Role Assignment
|
||||
CREATE TABLE user_roles (
|
||||
user_id INTEGER,
|
||||
role_id INTEGER,
|
||||
PRIMARY KEY (user_id, role_id),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
FOREIGN KEY (role_id) REFERENCES roles(id)
|
||||
);
|
||||
```
|
||||
|
||||
## Exemplos de Permissões
|
||||
|
||||
### Permissões Básicas
|
||||
- `view_own_data`: Visualizar seus próprios dados
|
||||
- `edit_own_data`: Editar seus próprios dados
|
||||
- `view_cell_data`: Visualizar dados da célula
|
||||
|
||||
### Permissões de Célula
|
||||
- `manage_cell_members`: Gerenciar membros da célula
|
||||
- `create_cell_member`: Criar novos membros na célula
|
||||
- `view_cell_reports`: Visualizar relatórios da célula
|
||||
- `REGISTER_CELL_RECEIPT`: Registrar comprovantes da célula
|
||||
|
||||
### Permissões de Setor
|
||||
- `manage_sector_cells`: Gerenciar células do setor
|
||||
- `create_sector_cell`: Criar novas células no setor
|
||||
- `view_sector_reports`: Visualizar relatórios do setor
|
||||
- `REGISTER_SECTOR_RECEIPT`: Registrar comprovantes do setor
|
||||
|
||||
### Permissões de CR
|
||||
- `manage_cr_sectors`: Gerenciar setores do CR
|
||||
- `create_cr_sector`: Criar novos setores no CR
|
||||
- `view_cr_reports`: Visualizar relatórios do CR
|
||||
- `REGISTER_CR_RECEIPT`: Registrar comprovantes do CR
|
||||
|
||||
### Permissões de CC
|
||||
- `manage_cc_crs`: Gerenciar CRs
|
||||
- `create_cc_cr`: Criar novos CRs
|
||||
- `view_cc_reports`: Visualizar relatórios nacionais
|
||||
- `system_config`: Configurar o sistema
|
||||
- `REGISTER_CC_RECEIPT`: Registrar comprovantes do CC
|
||||
|
||||
## Uso no Código
|
||||
|
||||
```python
|
||||
# Verificar permissão
|
||||
if user.has_permission('manage_cell_members'):
|
||||
# Permitir ação
|
||||
|
||||
# Verificar nível
|
||||
if user.has_role_level(3): # Membro de Setor
|
||||
# Permitir ação
|
||||
|
||||
# Verificar hierarquia
|
||||
if user.is_higher_or_equal_than(other_user):
|
||||
# Permitir ação
|
||||
```
|
||||
|
||||
# Controle de Acesso Baseado em Funções (RBAC)
|
||||
|
||||
## Estrutura Hierárquica
|
||||
|
||||
O sistema possui uma estrutura hierárquica com os seguintes níveis:
|
||||
- Célula (base)
|
||||
- Setor (agrupa células)
|
||||
- Comitê Regional - CR (agrupa setores)
|
||||
- Comitê Central - CC (único, agrupa CRs)
|
||||
|
||||
## Regras de Associação
|
||||
|
||||
- Cada militante pertence a apenas uma célula
|
||||
- Cada célula pertence a apenas um setor
|
||||
- Cada setor pertence a apenas um CR
|
||||
- Existe apenas um Comitê Central (CC)
|
||||
|
||||
## Permissões por Instância
|
||||
|
||||
### Célula
|
||||
- **Secretário(a)**:
|
||||
- `MANAGE_CELL_MEMBERS`: Gerenciar membros da célula
|
||||
- `VIEW_CELL_DATA`: Visualizar dados da célula
|
||||
- `VIEW_CELL_REPORTS`: Visualizar relatórios da célula
|
||||
- `REGISTER_CELL_RECEIPT`: Registrar comprovantes da célula
|
||||
|
||||
- **Tesoureiro(a)**:
|
||||
- `VIEW_CELL_DATA`: Visualizar dados da célula
|
||||
- `VIEW_CELL_REPORTS`: Visualizar relatórios da célula
|
||||
- `REGISTER_CELL_RECEIPT`: Registrar comprovantes da célula
|
||||
|
||||
- **Militante**:
|
||||
- `VIEW_OWN_DATA`: Visualizar apenas seus próprios dados
|
||||
|
||||
### Setor
|
||||
- **Secretário(a)**:
|
||||
- `MANAGE_SECTOR_CELLS`: Gerenciar células do setor
|
||||
- `VIEW_SECTOR_REPORTS`: Visualizar relatórios do setor
|
||||
- `REGISTER_SECTOR_RECEIPT`: Registrar comprovantes do setor
|
||||
|
||||
- **Tesoureiro(a)**:
|
||||
- `VIEW_SECTOR_REPORTS`: Visualizar relatórios do setor
|
||||
- `REGISTER_SECTOR_RECEIPT`: Registrar comprovantes do setor
|
||||
|
||||
### CR
|
||||
- **Secretário(a)**:
|
||||
- `MANAGE_CR_SECTORS`: Gerenciar setores do CR
|
||||
- `VIEW_CR_REPORTS`: Visualizar relatórios do CR
|
||||
- `REGISTER_CR_RECEIPT`: Registrar comprovantes do CR
|
||||
|
||||
- **Tesoureiro(a)**:
|
||||
- `VIEW_CR_REPORTS`: Visualizar relatórios do CR
|
||||
- `REGISTER_CR_RECEIPT`: Registrar comprovantes do CR
|
||||
|
||||
### CC
|
||||
- **Secretário(a)**:
|
||||
- `MANAGE_CC_CRS`: Gerenciar CRs
|
||||
- `VIEW_CC_REPORTS`: Visualizar relatórios do CC
|
||||
- `REGISTER_CC_RECEIPT`: Registrar comprovantes do CC
|
||||
- `SYSTEM_CONFIG`: Configurar o sistema
|
||||
|
||||
- **Tesoureiro(a)**:
|
||||
- `VIEW_CC_REPORTS`: Visualizar relatórios do CC
|
||||
- `REGISTER_CC_RECEIPT`: Registrar comprovantes do CC
|
||||
|
||||
## Regras de Acesso a Dados
|
||||
|
||||
1. **Visualização de Dados**:
|
||||
- Militantes podem ver apenas seus próprios dados
|
||||
- Secretários e tesoureiros podem ver dados de sua instância
|
||||
- O CC tem acesso a todos os dados
|
||||
|
||||
2. **Registro de Comprovantes**:
|
||||
- Apenas tesoureiros e secretários podem registrar comprovantes
|
||||
- O registro é restrito à instância do usuário
|
||||
- O CC pode registrar comprovantes em qualquer nível
|
||||
|
||||
## Implementação Técnica
|
||||
|
||||
O controle de acesso é implementado através de:
|
||||
|
||||
1. **Decorators**:
|
||||
- `@require_login`: Verifica se o usuário está logado
|
||||
- `@require_permission`: Verifica se o usuário tem uma permissão específica
|
||||
- `@require_instance_permission`: Verifica permissão em uma instância específica
|
||||
- `@require_instance_access`: Verifica acesso a uma instância específica
|
||||
|
||||
2. **Verificações de Acesso**:
|
||||
- Cada rota verifica as permissões necessárias
|
||||
- O acesso é negado se o usuário não tiver as permissões requeridas
|
||||
- Mensagens de erro são exibidas para o usuário
|
||||
|
||||
3. **Filtragem de Dados**:
|
||||
- As consultas ao banco de dados são filtradas baseadas nas permissões
|
||||
- Cada nível hierárquico tem suas próprias regras de acesso
|
||||
Binary file not shown.
33
functions/base.py
Normal file
33
functions/base.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
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'
|
||||
|
||||
# Configurar SQLite com opções para melhor concorrência
|
||||
engine = create_engine(
|
||||
f'sqlite:///{db_path}',
|
||||
connect_args={
|
||||
'timeout': 30, # Tempo de espera em segundos
|
||||
'check_same_thread': False # Permite acesso de múltiplas threads
|
||||
},
|
||||
pool_pre_ping=True, # Verifica conexão antes de usar
|
||||
pool_recycle=3600 # Recicla conexões após 1 hora
|
||||
)
|
||||
|
||||
Session = sessionmaker(bind=engine)
|
||||
Base = declarative_base()
|
||||
|
||||
def get_db_connection():
|
||||
"""Retorna uma nova sessão do banco de dados"""
|
||||
session = Session()
|
||||
try:
|
||||
return session
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
raise e
|
||||
@@ -1,692 +0,0 @@
|
||||
// TODO: extract all CONTANTS TO EASILY CHANGE CELLS
|
||||
|
||||
const planilhaID= "13sLipAAD5LkzZK19iuzgscbCmODiS11hJDRgaNsnYvw";
|
||||
|
||||
// LOCAIS DE LIMPEZA \/\/\/\/
|
||||
const cotas = 'B5:E40' ;
|
||||
const contribuintes = 'B43:E57' ;
|
||||
const brochuras = 'B60:D65';
|
||||
const campanha = 'B68:D84' ;
|
||||
const outras = 'B87:D94';
|
||||
const assinantes = 'B97:D109';
|
||||
const jornal = 'B112:D126';
|
||||
const despesaCE = 'D129';
|
||||
const depositos = 'B134:F251' ;
|
||||
const carimbo = 'Q287' ;
|
||||
// ACABOU :LOCAIS DE LIMPEZA /\/\/\/\
|
||||
|
||||
const contagemRF='E2';
|
||||
const celulaPrincipal = 'A1' ;
|
||||
|
||||
const enddepositos = 'D252';
|
||||
const endvendas = 'D130' ;
|
||||
|
||||
const celulaValorTotalCotas = 'E41';
|
||||
|
||||
const timeZone = Session.getScriptTimeZone();
|
||||
|
||||
const CRSP = "crsptesouraria@gmail.com";
|
||||
const areaAProteger = 'A1:Y999' ;
|
||||
|
||||
function getUser(){ return Session.getEffectiveUser();}
|
||||
|
||||
function voltaAoTopo(){
|
||||
SpreadsheetApp.getActiveSheet().setCurrentCell(SpreadsheetApp.getActiveSheet().getRange(celulaPrincipal)) ;
|
||||
|
||||
}
|
||||
|
||||
function onOpen() {
|
||||
var ui = SpreadsheetApp.getUi();
|
||||
ui.createMenu('CR')
|
||||
.addItem('Enviar RF', 'menuItem1')
|
||||
.addItem('Totalizar Cotas', 'menuItem2')
|
||||
.addItem('Teste - Não usar', 'menuItem3')
|
||||
.addToUi();
|
||||
}
|
||||
|
||||
/// MENU ITEMS
|
||||
function menuItem1() {
|
||||
SpreadsheetApp.getUi()
|
||||
{
|
||||
Logger.log(getUser());
|
||||
resultado = enviaCR();
|
||||
Logger.log("Resultado: " + resultado + ".");
|
||||
}
|
||||
}
|
||||
|
||||
function menuItem2() {
|
||||
SpreadsheetApp.getUi()
|
||||
{
|
||||
Logger.log(getUser());
|
||||
resultado = totalizar(curName)
|
||||
Logger.log("Resultado: " + resultado + ".");
|
||||
}
|
||||
}
|
||||
|
||||
function menuItem3() {
|
||||
SpreadsheetApp.getUi()
|
||||
{
|
||||
Logger.log(getUser());
|
||||
carimboValue = pegarCarimbo(SpreadsheetApp.getActiveSpreadsheet().getActiveSheet()) ;
|
||||
if(!isNaN(parseFloat(carimboValue)) ) {
|
||||
var mesAtual = Utilities.formatDate(carimboValue,timeZone, "MM");
|
||||
Logger.log("Carimbo lido: " + mesAtual + ".");
|
||||
}
|
||||
else {
|
||||
mesAtual = Utilities.formatDate(new Date(),timeZone, "MM") ;
|
||||
Logger.log("Carimbo vazio, mês atual: " + mesAtual + ".");
|
||||
}
|
||||
|
||||
voltaAoTopo();
|
||||
}
|
||||
}
|
||||
/// FUNCTIONS BELOW
|
||||
|
||||
|
||||
|
||||
/// SEND RF
|
||||
function enviaCR() {
|
||||
var ss = SpreadsheetApp.getActiveSpreadsheet(); // cria o objeto do arquivo da planilha
|
||||
var sheet = ss.getActiveSheet(); // cria objeto da Sheet ativa agora
|
||||
var curName = ss.getActiveSheet().getName() ; // pega nome da Sheet
|
||||
|
||||
// validar contas
|
||||
if (validar(sheet))
|
||||
{
|
||||
// subir dados na planilha de controle
|
||||
var resultadoEnvio = enviando(curName,sheet,ss);
|
||||
if (resultadoEnvio == "Enviado" )
|
||||
{SpreadsheetApp.getUi().alert('Relatório Enviado!');}
|
||||
else
|
||||
{SpreadsheetApp.getUi().alert('ERRO: ' + resultadoEnvio );}
|
||||
} return resultadoEnvio;
|
||||
}
|
||||
|
||||
|
||||
// VALIDAR VALORES TODO: ADIOCIONAR NOVAS
|
||||
function validar(sheet){
|
||||
// trocar vendas por centralizado
|
||||
var celulaDepositos = sheet.getRange(enddepositos);
|
||||
var depositos = sheet.setCurrentCell(celulaDepositos).getValue();
|
||||
var celulaVendas = sheet.getRange(endvendas);
|
||||
var vendas = sheet.setCurrentCell(celulaVendas).getValue();
|
||||
if ( vendas === depositos )
|
||||
{ return true;}
|
||||
else
|
||||
{ SpreadsheetApp.getUi().alert('Centralizado ' + vendas + ' não bate com Depósitos ' + depositos ); return false ;}
|
||||
}
|
||||
|
||||
|
||||
function enviando(curName,sheet,ss) {
|
||||
valorCotas = pegarTotalCota(curName, ss); // TOTAL das cotas
|
||||
marcaCarimbos(curName, valorCotas, sheet); // SALVA TOTAL DAS COTAS ETC
|
||||
novaAba = renomearAba(curName, ss); // Renomeia Aba e coloca nomero da nova aba no numero do relatorio
|
||||
limpaEntradas(novaAba) ; // limpa carimbo e entradas
|
||||
|
||||
if (travar(curName, ss) === "Travada"){
|
||||
ss.setActiveSheet(novaAba); // coloca novo em evidencia
|
||||
return "Enviado";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function pegarTotalCota(curName, ss){
|
||||
var sheet = ss.getSheetByName(curName);
|
||||
var valorNovaAvulso = Number(sheet.setCurrentCell(sheet.getRange(celulaValorTotalCotas)).getValue());
|
||||
Logger.log(" valorNovaAvulso: " + valorNovaAvulso + ".");
|
||||
return valorNovaAvulso;
|
||||
}
|
||||
|
||||
function marcaCarimbos(curName,totalCota,sheet){
|
||||
var gravarTempo = Utilities.formatDate(new Date(),timeZone, "yyyyMMddHHmmssSSS");
|
||||
var celulaTempo = 'D900';
|
||||
var celulaTotalCotas = 'D901';
|
||||
var celulaResponsavel = 'D902';
|
||||
var celulaNomeContagem = 'D902';
|
||||
var celulaResponsavelCel = 'H2';
|
||||
var username = getUser();
|
||||
sheet.setCurrentCell(sheet.getRange(celulaTempo)).setValue(gravarTempo);
|
||||
sheet.setCurrentCell(sheet.getRange(celulaResponsavel)).setValue(username);
|
||||
sheet.setCurrentCell(sheet.getRange(celulaNomeContagem)).setValue(curName);
|
||||
sheet.setCurrentCell(sheet.getRange(celulaResponsavelCel)).setValue(username);
|
||||
sheet.setCurrentCell(sheet.getRange(celulaTotalCotas)).setValue(totalCota);
|
||||
|
||||
}
|
||||
|
||||
function pegarCarimbo(sheet)
|
||||
{
|
||||
new Date(sheet.setCurrentCell(sheet.getRange(carimbo)).getValue())
|
||||
}
|
||||
|
||||
|
||||
function renomearAba(curName,ss){
|
||||
|
||||
var newName = Number(curName) + 1 ; // cria nome da nova
|
||||
ss.moveActiveSheet(ss.getNumSheets() - 1); // move a atual para a ultima posicao antes da Validacao que é escondida
|
||||
ss.duplicateActiveSheet(); // duplica ativa
|
||||
ss.renameActiveSheet(newName); // renomeia nova
|
||||
ss.moveActiveSheet(1); // move para a primeira posicao
|
||||
var sheet = ss.getSheetByName(newName); // torna a nova ativa usando nome
|
||||
sheet.getRange(contagemRF).setValue(newName); //altera contagem do relatorio usando numero da aba
|
||||
return sheet;
|
||||
}
|
||||
|
||||
function limpaEntradas(sheet)
|
||||
{
|
||||
function limpaTudo(value){
|
||||
sheet.getRange(value).clearContent();
|
||||
}
|
||||
var limpeza = [ cotas, contribuintes, brochuras, campanha , outras, assinantes, jornal , despesaCE, depositos, carimbo ];
|
||||
limpeza.forEach(limpaTudo) ;
|
||||
|
||||
let range = sheet.getRange("I:Y");
|
||||
sheet.hideColumn(range);
|
||||
range = sheet.getRange("A258:A999");
|
||||
sheet.hideRow(range);
|
||||
|
||||
SpreadsheetApp.getActiveSheet().setCurrentCell(SpreadsheetApp.getActiveSheet().getRange(celulaPrincipal)) ;
|
||||
|
||||
}
|
||||
|
||||
function travar(curName, ss){
|
||||
var sheet = ss.getSheetByName(curName);
|
||||
var areaProtegida = false ;
|
||||
var abaProtegida = false ;
|
||||
var userName = getUser();
|
||||
var protections = sheet.getProtections(SpreadsheetApp.ProtectionType.SHEET);
|
||||
|
||||
for (var i = 0; i < protections.length; i++) {
|
||||
var desc = protections[i].getDescription();
|
||||
Logger.log("protection desc: " + desc);
|
||||
if ( desc === 'Area protegida' ){ areaProtegida = true ; }
|
||||
if ( desc === 'Aba protegida') { abaProtegida = true ; }
|
||||
}
|
||||
|
||||
|
||||
// Protege area, e remove todos da lista de editores.
|
||||
var range = sheet.getRange(areaAProteger);
|
||||
|
||||
if (areaProtegida === false && userName != CRSP ) {
|
||||
proteRange = range.protect().setDescription('Area protegida') ;
|
||||
areaProtegida = true ;
|
||||
proteRange.removeEditor(userName);
|
||||
if (proteRange.canDomainEdit()) {
|
||||
proteRange.setDomainEdit(false);
|
||||
}
|
||||
}
|
||||
|
||||
Logger.log(userName);
|
||||
|
||||
if (abaProtegida === false && userName != CRSP ) {
|
||||
var proteSheet = sheet.protect().setDescription('Aba protegida');
|
||||
abaProtegida = true ;
|
||||
Logger.log("Removendo: " + userName);
|
||||
proteSheet.removeEditor(userName);
|
||||
if (proteSheet.canDomainEdit()) {
|
||||
proteSheet.setDomainEdit(false);
|
||||
}
|
||||
|
||||
}
|
||||
if (abaProtegida === true && areaProtegida === true ) { return "Travada" ;}
|
||||
}
|
||||
|
||||
function efetuarRotinaMadrugada(){
|
||||
travaNoturna();
|
||||
totalizar();
|
||||
}
|
||||
|
||||
function travaNoturna(){
|
||||
var ss = SpreadsheetApp.openById(planilhaID);
|
||||
var trava = 0 ;
|
||||
console.log( getUser());
|
||||
|
||||
// Protects the sheet.
|
||||
const sampleProtectedSheet = sheet.protect();
|
||||
// Logs whether domain users have permission to edit the protected sheet to the console.
|
||||
console.log(sampleProtectedSheet.canDomainEdit());
|
||||
|
||||
var sheets = SpreadsheetApp.getActiveSpreadsheet().getSheets();
|
||||
for (var cadaSheet = 0 ; cadaSheet < sheets.length ; cadaSheet++){
|
||||
var nomeSheet = sheets[cadaSheet].getName();
|
||||
Logger.log(" TravaNoturna nomeSheet: " + nomeSheet);
|
||||
if (!isNaN(parseFloat(nomeSheet)) && isFinite(nomeSheet) && nomeSheet === anterior) {
|
||||
SpreadsheetApp.setActiveSheet(sheets[cadaSheet]);
|
||||
var protections = sheets[cadaSheet].getProtections(SpreadsheetApp.ProtectionType.SHEET);
|
||||
for (var i = 0; i < protections.length; i++) {
|
||||
var desc = protections[i].getDescription();
|
||||
Logger.log("trava desc: " + desc);
|
||||
if ( desc === 'Area protegida' || desc === 'Aba protegida' ) {
|
||||
trava = trava + 1 ;
|
||||
}
|
||||
}
|
||||
if ( trava == 2 ){
|
||||
const protection = sheets[cadaSheet].protect();
|
||||
// Logs whether domain users have permission to edit the protected sheet to the console.
|
||||
console.log(protection.canDomainEdit());
|
||||
protection.removeEditors(protection.getEditors());
|
||||
if (protection.canDomainEdit()) {
|
||||
protection.setDomainEdit(false);
|
||||
}
|
||||
console.log(protection.canDomainEdit());
|
||||
protection.setDescription('Trava Noturna');
|
||||
}
|
||||
}
|
||||
var sheet = ss.getSheetByName(anterior);
|
||||
sheet.setCurrentCell(sheet.getRange(celulaPrincipal)) ;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
function totalizar(curName){
|
||||
var anterior = curName ;
|
||||
Logger.log("anterior: " + anterior + ".");
|
||||
|
||||
var ss = SpreadsheetApp.getActiveSpreadsheet() ;
|
||||
var sheets = SpreadsheetApp.getActiveSpreadsheet().getSheets();
|
||||
var gravou = 0;
|
||||
|
||||
function enviarTotal() {
|
||||
var sheet = ss.getSheetByName(anterior);
|
||||
|
||||
var brochuras = sheet.setCurrentCell(sheet.getRange('D66')).getValue() ;
|
||||
var campanha = sheet.setCurrentCell(sheet.getRange('D71')).getValue();
|
||||
var campanhaCCCE = sheet.setCurrentCell(sheet.getRange('D85')).getValue();
|
||||
|
||||
var outras = sheet.setCurrentCell(sheet.getRange('D95')).getValue();
|
||||
var assinantes = sheet.setCurrentCell(sheet.getRange('D115')).getValue();
|
||||
var jornal = sheet.setCurrentCell(sheet.getRange('D127')).getValue();
|
||||
var carimboValue = pegarCarimbo() ;
|
||||
|
||||
var sheet = ss.getSheetByName("TOTAL");
|
||||
|
||||
// ABA TOTAL COLUNAS DE VALORES TOTALIZADOS
|
||||
var cotascol = 'C'; // 0
|
||||
var contribuintescol = 'E'; // 1
|
||||
var brochurascol = 'H' ; // 2
|
||||
var cfcol = 'J'; // 3
|
||||
var outrascol = 'L'; // 4
|
||||
var asscol = 'P'; // 5
|
||||
var jornalcol ='R'; // 6
|
||||
var varJaneiro = 3 ;
|
||||
var varFevereiro = 4 ;
|
||||
var varMarço = 5 ;
|
||||
var varAbril = 6 ;
|
||||
var varMaio = 7 ;
|
||||
var varJunho = 8 ;
|
||||
var varJulho = 9 ;
|
||||
var varAgosto = 10 ;
|
||||
var varSetembro = 11 ;
|
||||
var varOutubro = 12 ;
|
||||
var varNovembro = 13 ;
|
||||
var varDezembro = 14 ;
|
||||
var decimoTerceiro = 15 ;
|
||||
var decimoQuarto = 16 ;
|
||||
var decimoQuinto = 17 ;
|
||||
var colunas = [ cotascol, contribuintescol , brochurascol , cfcol , outrascol , asscol , jornalcol ] ;
|
||||
var linhas = [varJaneiro , varFevereiro ,varMarço ,varAbril ,varMaio ,varJunho ,varJulho ,varAgosto ,varSetembro ,varOutubro ,varNovembro ,varDezembro, decimoTerceiro, decimoQuarto, decimoQuinto] ;
|
||||
// TERMINOU TABELA TOTAL /\
|
||||
|
||||
|
||||
// PEGAR MES ATUAL
|
||||
|
||||
if(!isNaN(parseFloat(carimboValue)) ) {var mesAtual = Utilities.formatDate(carimboValue,timeZone, "MM");}
|
||||
else { mesAtual = Utilities.formatDate(new Date(),timeZone, "MM") }
|
||||
|
||||
// Para cada Coluna de TOTAL executar totalização:
|
||||
colunas.forEach(function(letra,coluna,tudo) {
|
||||
Logger.log("letra: " + letra );
|
||||
// começa com cota, checa se é o mes e coloca no switch.
|
||||
switch (letra){
|
||||
case cotascol:
|
||||
// mes igual mes da primeira linha
|
||||
for (var cadaMesdeCota = 5 ; cadaMesdeCota <=40 ; cadaMesdeCota++ ){
|
||||
var celulaMilitante = 'B' + cadaMesdeCota ;
|
||||
var celulaAno = 'C' + cadaMesdeCota ;
|
||||
var celulaMes = 'D' + cadaMesdeCota ;
|
||||
var celulaValor = 'E' + cadaMesdeCota ;
|
||||
|
||||
|
||||
// Vai pra Anterior pra pegar cota de cadaMesdeCota ++++++++++++++++++++++++++++++++++++++++++++++
|
||||
var sheet = ss.getSheetByName(anterior);
|
||||
|
||||
valorCelula = sheet.setCurrentCell(sheet.getRange(celulaValor)).getValue() ;
|
||||
|
||||
// if (!isNaN(parseFloat(mesCelula)) && !isNaN(parseFloat(valorCelula)))
|
||||
if (!isNaN(parseFloat(valorCelula)))
|
||||
{
|
||||
var mesCelula = sheet.setCurrentCell(sheet.getRange(celulaMes)).getValue();
|
||||
var retornoMes = checkMonth(mesCelula);
|
||||
militanteCota = sheet.setCurrentCell(sheet.getRange(celulaMilitante)).getValue();
|
||||
anoCota = sheet.setCurrentCell(sheet.getRange(celulaAno)).getValue();
|
||||
Logger.log( " COTA valorCelula: " + valorCelula + " militanteCota " + militanteCota + "retornoMes" + retornoMes);
|
||||
|
||||
if ( !isNaN(parseFloat(retornoMes)) ) {
|
||||
var mesNovaCota = new Date(retornoMes) ;
|
||||
var mesNCemN = Number(Utilities.formatDate(mesNovaCota,timeZone, "MM")) - 1;
|
||||
var valorNovaCota = valorCelula ;
|
||||
|
||||
if (!isNaN(parseFloat(valorNovaCota))){
|
||||
// ENVIA PARA TOTAL:
|
||||
Logger.log( " COTA valorNovaCota: " + valorNovaCota + ".");
|
||||
|
||||
var sheet = ss.getSheetByName("TOTAL");
|
||||
var celulaObjetivo = letra + linhas[mesNCemN] ;
|
||||
Logger.log( " COTA celulaObjetivo: " + celulaObjetivo + ".");
|
||||
|
||||
|
||||
var valorAntigoCota = sheet.setCurrentCell(sheet.getRange(celulaObjetivo)).getValue() ;
|
||||
if (!isNaN(parseFloat(valorAntigoCota))){
|
||||
Logger.log( " COTA valorAntigoCota: " + valorAntigoCota + ".");
|
||||
var gravar = valorAntigoCota + valorNovaCota ;
|
||||
}
|
||||
else { var gravar = valorNovaCota ; }
|
||||
sheet.setCurrentCell(sheet.getRange(celulaObjetivo)).setValue(gravar);
|
||||
Logger.log( " COTA Gravou: " + gravar + ".");
|
||||
// ENVIOU PARA TOTAL /\
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
// segunda iteração contribuintes, checa se é o mes e coloca no switch.
|
||||
case contribuintescol:
|
||||
// mes igual mes da primeira linha
|
||||
for (var cadaMesContrib = 43 ; cadaMesContrib <=57 ; cadaMesContrib++ ){
|
||||
var celulaContribuinte = 'B' + cadaMesContrib ;
|
||||
var celulaAno = 'C' + cadaMesContrib ;
|
||||
var celulaMes = 'D' + cadaMesContrib ;
|
||||
var celulaValor = 'E' + cadaMesContrib ;
|
||||
var celulaResponsavel = 'F' + cadaMesContrib ;
|
||||
|
||||
// Vai pra Anterior pra pegar Contribuição de cadaMesdeCota
|
||||
var sheet = ss.getSheetByName(anterior);
|
||||
var retornoMes = sheet.setCurrentCell(sheet.getRange(celulaMes)).getValue();
|
||||
|
||||
if ( !isNaN(parseFloat(retornoMes)) ) {
|
||||
var mesNovaContrib = new Date(checkMonth(retornoMes)) ;
|
||||
var mesNCemN = Number(Utilities.formatDate(mesNovaContrib,timeZone, "MM")) - 1;
|
||||
var valorNovaContr = Number(sheet.setCurrentCell(sheet.getRange(celulaValor)).getValue());
|
||||
if (!isNaN(parseFloat(valorNovaContr))){
|
||||
Logger.log( " CONTRIB valorNovaContr: " + valorNovaContr + ".");
|
||||
var celulaObjetivo = letra + linhas[mesNCemN] ;
|
||||
Logger.log( " CONTRIB celulaObjetivo: " + celulaObjetivo + ".");
|
||||
var sheet = ss.getSheetByName("TOTAL");
|
||||
var valorAntigoContr = sheet.setCurrentCell(sheet.getRange(celulaObjetivo)).getValue() ;
|
||||
Logger.log( " CONTRIB valorAntigoContr: " + valorAntigoContr + ".");
|
||||
if (!isNaN(parseFloat(valorAntigoContr)) ){
|
||||
var gravar = valorNovaContr + valorAntigoContr ; }
|
||||
else {
|
||||
gravar = valorNovaContr ;
|
||||
}
|
||||
Logger.log( " CONTRIB celulaMes: " + celulaMes + " celulaValor: " + celulaValor + ".");
|
||||
var sheet = ss.getSheetByName("TOTAL");
|
||||
sheet.setCurrentCell(sheet.getRange(celulaObjetivo)).setValue(gravar);
|
||||
Logger.log( " CONTRIB Gravou: " + gravar + ".");
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
// FALTA TERMINAR BROCHURAS
|
||||
case brochurascol:
|
||||
for (var linhaBro = 60 ; linhaBro <=65 ; linhaBro++ ){
|
||||
// var celulaNome = 'B' +linhaBro ;
|
||||
var celulaQuantidade = 'C' + linhaBro ;
|
||||
var celulaValor = 'D' + linhaBro ;
|
||||
var celulaCodigo = 'E' + linhaBro ;
|
||||
// Vai pra Anterior pra pegar dados acima
|
||||
var sheet = ss.getSheetByName(anterior);
|
||||
var difLinBro = 2 ;
|
||||
var quantidadeBro = sheet.setCurrentCell(sheet.getRange(celulaQuantidade)).getValue();
|
||||
if ( !isNaN(parseFloat(quantidadeBro)) ) {
|
||||
var valorNovaBro = Number(sheet.setCurrentCell(sheet.getRange(celulaValor)).getValue());
|
||||
var codigoNovaBro = Number(sheet.setCurrentCell(sheet.getRange(celulaCodigo)).getValue()) + difLinBro ;
|
||||
Logger.log(" BROCHURAS valorNovaBro: " + valorNovaBro + " codigoNovaBro: " + codigoNovaBro);
|
||||
if (!isNaN(parseFloat(valorNovaBro))){
|
||||
var celulaObjetivo = letra + codigoNovaBro ;
|
||||
var qtdObjetivo = 'G' + codigoNovaBro ;
|
||||
var qtdAntigoBro = sheet.setCurrentCell(sheet.getRange(qtdObjetivo)).getValue() ;
|
||||
var sheet = ss.getSheetByName("TOTAL");
|
||||
var valorAntigoBro = sheet.setCurrentCell(sheet.getRange(celulaObjetivo)).getValue() ;
|
||||
if (!isNaN(parseFloat(valorAntigoBro)) ){
|
||||
var gravar = valorNovaBro + valorAntigoBro ; }
|
||||
else {
|
||||
gravar = valorNovaBro ;
|
||||
}
|
||||
Logger.log(" BROCHURAS celulaQuantidade: " + celulaQuantidade + " celulaValor: " + celulaValor + " gravar: " + gravar );
|
||||
|
||||
if (!isNaN(parseFloat(qtdAntigoBro)) ){
|
||||
var gravarQtd = quantidadeBro + qtdAntigoBro ; }
|
||||
else {
|
||||
var gravarQtd = quantidadeBro ;
|
||||
}
|
||||
var sheet = ss.getSheetByName("TOTAL");
|
||||
// Grava Valor
|
||||
Logger.log(" BROCHURAS celulaValorObjetivo: " + celulaObjetivo + " gravar: " + gravar );
|
||||
sheet.setCurrentCell(sheet.getRange(celulaObjetivo)).setValue(gravar);
|
||||
Logger.log( " BROCHURAS Gravou Valor: " + gravar + ".");
|
||||
// Grava quantidade
|
||||
sheet.setCurrentCell(sheet.getRange(qtdObjetivo)).setValue(gravarQtd);
|
||||
Logger.log( " BROCHURAS Gravou Qtd: " + gravarQtd + ".")
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case cfcol:
|
||||
for (var linhaCF = 68 ; linhaCF <=84 ; linhaCF++ ){
|
||||
var celulaNome = 'B' + linhaCF ;
|
||||
var celulaQuantidade = 'C' + linhaCF ;
|
||||
var celulaValor = 'D' + linhaCF ;
|
||||
var celulaCodigo = 'F' + linhaCF ;
|
||||
var sheet = ss.getSheetByName(anterior);
|
||||
var valorCF = sheet.setCurrentCell(sheet.getRange(celulaValor)).getValue();
|
||||
if ( !isNaN(parseFloat(valorCF)) ) {
|
||||
var militanteNovaCF = sheet.setCurrentCell(sheet.getRange(celulaNome)).getValue();
|
||||
// VAI PRA TOTAL
|
||||
var sheet = ss.getSheetByName("TOTAL");
|
||||
var linhaSalvar = sheet.getRange('I3:J22').createTextFinder(militanteNovaCF).findNext();
|
||||
if (linhaSalvar){
|
||||
var valorAntigoCF = linhaSalvar.offset(0,1).getValue();
|
||||
Logger.log(" CF linhaSalvar.getA1Notation(): " + linhaSalvar.getA1Notation() + " militanteNovaCF: " + militanteNovaCF + " valorCF: " + valorCF + " valorAntigoCF: " + valorAntigoCF );
|
||||
if (!isNaN(parseFloat(valorAntigoCF)) ){ var gravar = valorCF + valorAntigoCF ; }
|
||||
else { gravar = valorCF ; }
|
||||
// Grava Valor
|
||||
linhaSalvar.offset(0,1).setValue(gravar);
|
||||
Logger.log( " CF Gravou Valor: " + gravar + ".");
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case outrascol:
|
||||
for (var linhaOutros = 87 ; linhaOutros <=94 ; linhaOutros++ ){
|
||||
var celulaQuantidade = 'C' + linhaOutros ;
|
||||
var celulaValor = 'D' + linhaOutros ;
|
||||
var celulaCodigo = 'E' + linhaOutros ;
|
||||
|
||||
// Vai pra Anterior pra pegar dados acima
|
||||
var sheet = ss.getSheetByName(anterior);
|
||||
|
||||
var quantidadeOutro = sheet.setCurrentCell(sheet.getRange(celulaQuantidade)).getValue();
|
||||
if ( !isNaN(parseFloat(quantidadeOutro)) ) {
|
||||
var valorNovaOutro = Number(sheet.setCurrentCell(sheet.getRange(celulaValor)).getValue());
|
||||
var difLinOut = 2 ;
|
||||
var codigoNovaOutro = Number(sheet.setCurrentCell(sheet.getRange(celulaCodigo)).getValue()) + difLinOut ;
|
||||
if (!isNaN(parseFloat(valorNovaOutro))){
|
||||
var celulaObjetivo = letra + codigoNovaOutro ;
|
||||
var qtdObjetivo = 'K' + codigoNovaOutro ;
|
||||
var qtdAntigoOutro = sheet.setCurrentCell(sheet.getRange(qtdObjetivo)).getValue() ;
|
||||
var sheet = ss.getSheetByName("TOTAL");
|
||||
var valorAntigoOutro = sheet.setCurrentCell(sheet.getRange(celulaObjetivo)).getValue() ;
|
||||
if (!isNaN(parseFloat(valorAntigoOutro)) ){
|
||||
var gravar = valorNovaOutro + valorAntigoOutro ; }
|
||||
else {
|
||||
gravar = valorNovaOutro ;
|
||||
}
|
||||
Logger.log( " OUTRAS celulaQuantidade: " + celulaQuantidade + " celulaValor: " + celulaValor + " gravar: " + gravar );
|
||||
|
||||
if (!isNaN(parseFloat(qtdAntigoOutro)) ){
|
||||
var gravarQtd = quantidadeOutro + qtdAntigoOutro ; }
|
||||
else {
|
||||
var gravarQtd = quantidadeOutro ;
|
||||
}
|
||||
var sheet = ss.getSheetByName("TOTAL");
|
||||
// Grava Valor
|
||||
Logger.log( " OUTRAS celulaValorObjetivo: " + celulaObjetivo + " gravar: " + gravar );
|
||||
sheet.setCurrentCell(sheet.getRange(celulaObjetivo)).setValue(gravar);
|
||||
Logger.log( " OUTRAS Gravou Valor: " + gravar + ".");
|
||||
// Grava quantidade
|
||||
sheet.setCurrentCell(sheet.getRange(qtdObjetivo)).setValue(gravarQtd);
|
||||
Logger.log( " OUTRAS Gravou Qtd: " + gravarQtd + ".")
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case asscol:
|
||||
// ARRUMAR ASSINATURAS
|
||||
var linha = mesAtual - 1 ;
|
||||
Logger.log(" ASSINATURA linhas[linha]: " + linhas[linha] + " linha: " + linha + " mesAtual: " + mesAtual + ".");
|
||||
var celula = letra + linhas[linha] ;
|
||||
|
||||
// PEGAR TOTAL ATUAL
|
||||
var sheet = ss.getSheetByName(anterior);
|
||||
var totalatual = sheet.setCurrentCell(sheet.getRange(celula)).getValue() ;
|
||||
if ( !isNaN(parseFloat(outras)) && assinantes > 0 ){
|
||||
var gravar = totalatual + assinantes ;
|
||||
// GRAVAR
|
||||
var sheet = ss.getSheetByName("TOTAL");
|
||||
sheet.setCurrentCell(sheet.getRange(celula)).setValue(gravar);
|
||||
}
|
||||
break ;
|
||||
case jornalcol:
|
||||
for (var linhaAvulso = 112 ; linhaAvulso <=126 ; linhaAvulso++ ){
|
||||
var celulaQuantidade = 'C' + linhaAvulso ;
|
||||
var celulaValor = 'D' + linhaAvulso ;
|
||||
var celulaEdicao = 'B' + linhaAvulso ;
|
||||
|
||||
// Vai pra Anterior pra pegar dados acima
|
||||
var sheet = ss.getSheetByName(anterior);
|
||||
var difLinEdicao = 11
|
||||
var quantidadeAvulso = sheet.setCurrentCell(sheet.getRange(celulaQuantidade)).getValue();
|
||||
if ( !isNaN(parseFloat(quantidadeAvulso)) ) {
|
||||
var valorNovaAvulso = Number(sheet.setCurrentCell(sheet.getRange(celulaValor)).getValue());
|
||||
var edicaoNovaAvulso = Number(sheet.setCurrentCell(sheet.getRange(celulaEdicao)).getValue()) - difLinEdicao ;
|
||||
Logger.log( " JORNALA. edicaoNovaAvulso: " + edicaoNovaAvulso + "valorNovaAvulso: " + valorNovaAvulso + "Quantidade: " + quantidadeAvulso );
|
||||
if (!isNaN(parseFloat(valorNovaAvulso))){
|
||||
var celulaValorObjetivo = letra + edicaoNovaAvulso ;
|
||||
var qtdVendido = 'T' + edicaoNovaAvulso ;
|
||||
var sheet = ss.getSheetByName("TOTAL");
|
||||
var valorAntigoAvulso = sheet.setCurrentCell(sheet.getRange(celulaValorObjetivo)).getValue() ;
|
||||
var qtdAntigoAvulso = sheet.setCurrentCell(sheet.getRange(qtdVendido)).getValue() ;
|
||||
if (!isNaN(parseFloat(valorAntigoAvulso)) ){
|
||||
var gravar = valorNovaAvulso + valorAntigoAvulso ; }
|
||||
else {
|
||||
var gravar = valorNovaAvulso ;
|
||||
}
|
||||
if (!isNaN(parseFloat(qtdAntigoAvulso)) ){
|
||||
var gravarQtd = quantidadeAvulso + qtdAntigoAvulso ; }
|
||||
else {
|
||||
var gravarQtd = quantidadeAvulso ;
|
||||
}
|
||||
var sheet = ss.getSheetByName("TOTAL");
|
||||
// Grava Valor
|
||||
sheet.setCurrentCell(sheet.getRange(celulaValorObjetivo)).setValue(gravar);
|
||||
Logger.log( " JORNALA. Gravou Valor: " + gravar );
|
||||
// Grava quantidade
|
||||
sheet.setCurrentCell(sheet.getRange(qtdVendido)).setValue(gravarQtd);
|
||||
Logger.log( " JORNALA. Gravou Qtd: " + gravarQtd );
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
} )
|
||||
}
|
||||
|
||||
if (sheets.length > 1) {
|
||||
for (var cadaSheet = 0 ; cadaSheet < sheets.length ; cadaSheet++)
|
||||
{
|
||||
var nomeSheet = sheets[cadaSheet].getName();
|
||||
Logger.log("nomeSheet: " + nomeSheet);
|
||||
if (!isNaN(parseFloat(nomeSheet)) && isFinite(nomeSheet) && nomeSheet === anterior) {
|
||||
SpreadsheetApp.setActiveSheet(sheets[cadaSheet]);
|
||||
var protections = sheets[cadaSheet].getProtections(SpreadsheetApp.ProtectionType.SHEET);
|
||||
for (var i = 0; i < protections.length; i++) {
|
||||
var desc = protections[i].getDescription();
|
||||
Logger.log("protection desc: " + desc);
|
||||
if ( desc === 'Aba protegida') {
|
||||
enviarTotal();
|
||||
gravou = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
var sheet = ss.getSheetByName(anterior);
|
||||
sheet.setCurrentCell(sheet.getRange(celulaPrincipal)) ;
|
||||
}
|
||||
}
|
||||
if (gravou === 1 ) { return "Totalizado";}
|
||||
Logger.log("gravou: " + gravou + ".");
|
||||
|
||||
}
|
||||
|
||||
|
||||
function checkMonth(nomedoMes)
|
||||
{
|
||||
Logger.log(" checkMonth nomedoMes: " + nomedoMes + ".");
|
||||
switch (nomedoMes){
|
||||
case 1:
|
||||
case "Janeiro" :
|
||||
return "1";
|
||||
break;
|
||||
case 2:
|
||||
case "Fevereiro":
|
||||
return "2";
|
||||
break;
|
||||
case 3:
|
||||
case "Março":
|
||||
return "3";
|
||||
break;
|
||||
case 4:
|
||||
case "Abril":
|
||||
return "4";
|
||||
break;
|
||||
case 5:
|
||||
case "Maio":
|
||||
return "5";
|
||||
break;
|
||||
case 6:
|
||||
case "Junho":
|
||||
return "6";
|
||||
break;
|
||||
case 7:
|
||||
case "Julho":
|
||||
return "7";
|
||||
break;
|
||||
case 8:
|
||||
case "Agosto":
|
||||
return "8";
|
||||
break;
|
||||
case 9:
|
||||
case "Setembro":
|
||||
return "9";
|
||||
break;
|
||||
case 10:
|
||||
case "Outubro":
|
||||
return "10";
|
||||
break;
|
||||
case 11:
|
||||
case "Novembro":
|
||||
return "11";
|
||||
break;
|
||||
case 12:
|
||||
case "Dezembro":
|
||||
return "12";
|
||||
break;
|
||||
default:
|
||||
return nomedoMes;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
import qrcode
|
||||
|
||||
def gerar_carteirinha(militante_id, nome):
|
||||
# Criar imagem base
|
||||
img = Image.new('RGB', (300, 200), color=(255, 255, 255))
|
||||
d = ImageDraw.Draw(img)
|
||||
|
||||
# Adicionar texto
|
||||
font = ImageFont.load_default()
|
||||
d.text((10, 10), f"Nome: {nome}", font=font, fill=(0, 0, 0))
|
||||
d.text((10, 30), f"ID: {militante_id}", font=font, fill=(0, 0, 0))
|
||||
|
||||
# Gerar QR code
|
||||
qr = qrcode.make(f"ID: {militante_id}")
|
||||
img.paste(qr, (200, 50))
|
||||
|
||||
# Salvar imagem
|
||||
img.save(f"carteirinha_{militante_id}.png")
|
||||
84
functions/controle.py
Normal file
84
functions/controle.py
Normal file
@@ -0,0 +1,84 @@
|
||||
from datetime import datetime, UTC
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from functions.database import get_db_connection, Controle as ControleModel
|
||||
|
||||
class Controle:
|
||||
def __init__(self):
|
||||
self.db = get_db_connection()
|
||||
|
||||
def registrar_controle(self, militante_id: int, tipo: str, valor: float, observacao: str = None) -> bool:
|
||||
"""
|
||||
Registra um novo controle no sistema
|
||||
|
||||
Args:
|
||||
militante_id: ID do militante
|
||||
tipo: Tipo do controle (ex: 'pagamento', 'cota')
|
||||
valor: Valor do controle
|
||||
observacao: Observação opcional sobre o controle
|
||||
|
||||
Returns:
|
||||
bool: True se o controle foi registrado com sucesso, False caso contrário
|
||||
"""
|
||||
try:
|
||||
data_registro = datetime.now(UTC)
|
||||
|
||||
novo_controle = ControleModel(
|
||||
militante_id=militante_id,
|
||||
tipo=tipo,
|
||||
valor=valor,
|
||||
data_registro=data_registro,
|
||||
observacao=observacao
|
||||
)
|
||||
|
||||
self.db.add(novo_controle)
|
||||
self.db.commit()
|
||||
return True
|
||||
|
||||
except SQLAlchemyError as e:
|
||||
self.db.rollback()
|
||||
print(f"Erro ao registrar controle: {str(e)}")
|
||||
return False
|
||||
finally:
|
||||
self.db.close()
|
||||
|
||||
def listar_controles(self, militante_id: int = None) -> list:
|
||||
"""
|
||||
Lista os controles registrados no sistema
|
||||
|
||||
Args:
|
||||
militante_id: ID do militante para filtrar (opcional)
|
||||
|
||||
Returns:
|
||||
list: Lista de controles encontrados
|
||||
"""
|
||||
try:
|
||||
query = self.db.query(ControleModel)
|
||||
|
||||
if militante_id:
|
||||
query = query.filter(ControleModel.militante_id == militante_id)
|
||||
|
||||
return query.all()
|
||||
|
||||
except SQLAlchemyError as e:
|
||||
print(f"Erro ao listar controles: {str(e)}")
|
||||
return []
|
||||
finally:
|
||||
self.db.close()
|
||||
|
||||
def buscar_controle(self, controle_id: int) -> ControleModel:
|
||||
"""
|
||||
Busca um controle específico pelo ID
|
||||
|
||||
Args:
|
||||
controle_id: ID do controle
|
||||
|
||||
Returns:
|
||||
ControleModel: Objeto do controle encontrado ou None
|
||||
"""
|
||||
try:
|
||||
return self.db.query(ControleModel).filter(ControleModel.id == controle_id).first()
|
||||
except SQLAlchemyError as e:
|
||||
print(f"Erro ao buscar controle: {str(e)}")
|
||||
return None
|
||||
finally:
|
||||
self.db.close()
|
||||
@@ -1,16 +1,42 @@
|
||||
from sqlalchemy import create_engine, Column, Integer, String, Boolean, Numeric, Date, ForeignKey
|
||||
from sqlalchemy.orm import relationship, sessionmaker
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from datetime import datetime, timedelta
|
||||
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, Float
|
||||
from sqlalchemy.orm import sessionmaker, relationship, backref
|
||||
import os
|
||||
import pyotp
|
||||
from pathlib import Path
|
||||
from sqlalchemy.pool import NullPool
|
||||
import secrets
|
||||
from flask_mail import Message
|
||||
from flask import url_for
|
||||
import enum
|
||||
from flask_login import UserMixin
|
||||
from .rbac import Role, Permission, role_permissions, user_roles
|
||||
from .base import Base, engine, Session
|
||||
import logging
|
||||
|
||||
Base = declarative_base()
|
||||
engine = create_engine('sqlite:///database.db', echo=True)
|
||||
SessionLocal = sessionmaker(bind=engine)
|
||||
# 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)
|
||||
|
||||
def get_db_connection():
|
||||
"""
|
||||
Retorna uma nova sessão do banco de dados
|
||||
"""
|
||||
return SessionLocal()
|
||||
"""Retorna uma nova sessão do banco de dados"""
|
||||
Session = sessionmaker(bind=engine)
|
||||
db = Session()
|
||||
|
||||
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
|
||||
|
||||
def execute_query(query, params=None):
|
||||
"""
|
||||
@@ -27,22 +53,248 @@ def execute_query(query, params=None):
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
class EstadoMilitante(enum.Enum):
|
||||
ATIVO = 'ativo'
|
||||
DESLIGADO = 'desligado'
|
||||
SUSPENSO = 'suspenso'
|
||||
AFASTADO = 'afastado'
|
||||
|
||||
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")
|
||||
|
||||
class ComiteRegional(Base):
|
||||
__tablename__ = 'comites_regionais'
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
nome = Column(String(100), nullable=False)
|
||||
responsavel_financas = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_cr_responsavel_financas'))
|
||||
responsavel_formacao = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_cr_responsavel_formacao'))
|
||||
secretario_organizacao = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_cr_secretario_organizacao'))
|
||||
correspondente_jornal = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_cr_correspondente_jornal'))
|
||||
|
||||
# Relacionamentos
|
||||
responsavel_financas_rel = relationship("Militante", foreign_keys=[responsavel_financas])
|
||||
responsavel_formacao_rel = relationship("Militante", foreign_keys=[responsavel_formacao])
|
||||
secretario_organizacao_rel = relationship("Militante", foreign_keys=[secretario_organizacao])
|
||||
correspondente_jornal_rel = relationship("Militante", foreign_keys=[correspondente_jornal])
|
||||
setores = relationship("Setor", back_populates="cr")
|
||||
celulas = relationship("Celula", back_populates="cr")
|
||||
usuarios = relationship("Usuario", back_populates="cr")
|
||||
|
||||
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))
|
||||
militante = relationship("Militante", back_populates="emails")
|
||||
|
||||
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))
|
||||
militantes = relationship("Militante", back_populates="endereco")
|
||||
|
||||
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))
|
||||
militante = relationship("Militante", back_populates="redes_sociais")
|
||||
|
||||
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)
|
||||
email = Column(String(100), unique=True)
|
||||
telefone = Column(String(15))
|
||||
endereco = Column(String(255))
|
||||
filiado = Column(Boolean, default=False)
|
||||
# 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 = relationship("VendaJornalAvulso", back_populates="militante")
|
||||
assinaturas = relationship("AssinaturaAnual", back_populates="militante")
|
||||
vendas_jornais = relationship("VendaJornal", back_populates="militante")
|
||||
assinaturas = relationship("AssinaturaJornal", back_populates="militante")
|
||||
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
|
||||
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 send_otp_email(self, mail):
|
||||
"""
|
||||
Envia email com link para QR code
|
||||
"""
|
||||
token = self.generate_temp_token()
|
||||
qr_url = url_for('get_qr_code', token=token, _external=True)
|
||||
|
||||
msg = Message(
|
||||
'Configuração de Autenticação em Duas Etapas',
|
||||
recipients=[self.email]
|
||||
)
|
||||
msg.body = f"""
|
||||
Olá {self.nome},
|
||||
|
||||
Para configurar sua autenticação em duas etapas, acesse o link abaixo:
|
||||
{qr_url}
|
||||
|
||||
Este link expirará em 48 horas.
|
||||
|
||||
Instruções:
|
||||
1. Instale um aplicativo autenticador (Google Authenticator, Microsoft Authenticator)
|
||||
2. Acesse o link acima
|
||||
3. Escaneie o QR code com o aplicativo
|
||||
4. Use o código gerado para fazer login no sistema
|
||||
|
||||
Atenciosamente,
|
||||
Sistema de Controles
|
||||
"""
|
||||
|
||||
mail.send(msg)
|
||||
|
||||
def generate_username(self):
|
||||
"""Gera um nome de usuário único baseado no primeiro nome e um código"""
|
||||
from sqlalchemy import func
|
||||
db = get_db_connection()
|
||||
try:
|
||||
# Pega o primeiro nome
|
||||
primeiro_nome = self.nome.split()[0].lower()
|
||||
|
||||
# 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()
|
||||
|
||||
class CotaMensal(Base):
|
||||
__tablename__ = 'cotas_mensais'
|
||||
@@ -52,6 +304,8 @@ class CotaMensal(Base):
|
||||
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")
|
||||
|
||||
@@ -61,19 +315,22 @@ class TipoPagamento(Base):
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
descricao = Column(String(100), nullable=False)
|
||||
|
||||
pagamentos = relationship("Pagamento", back_populates="tipo_pagamento")
|
||||
|
||||
class Pagamento(Base):
|
||||
__tablename__ = 'pagamentos'
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
militante_id = Column(Integer, ForeignKey('militantes.id'))
|
||||
tipo_pagamento_id = Column(Integer, ForeignKey('tipos_pagamento.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")
|
||||
tipo_pagamento = relationship("TipoPagamento", back_populates="pagamentos")
|
||||
transacoes_pix = relationship("TransacaoPIX", back_populates="pagamento")
|
||||
|
||||
class TipoMaterial(Base):
|
||||
__tablename__ = 'tipos_materiais'
|
||||
@@ -106,7 +363,7 @@ class VendaJornalAvulso(Base):
|
||||
valor_total = Column(Numeric(10, 2), 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):
|
||||
__tablename__ = 'assinaturas_anuais'
|
||||
@@ -125,9 +382,18 @@ class AssinaturaAnual(Base):
|
||||
class Setor(Base):
|
||||
__tablename__ = 'setores'
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
id = Column(Integer, primary_key=True)
|
||||
nome = Column(String(100), nullable=False)
|
||||
|
||||
cr_id = Column(Integer, ForeignKey('comites_regionais.id', use_alter=True, name='fk_setor_cr'))
|
||||
responsavel = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_setor_responsavel'))
|
||||
responsavel_financas = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_setor_responsavel_financas'))
|
||||
|
||||
# Relacionamentos
|
||||
cr = relationship("ComiteRegional", back_populates="setores")
|
||||
responsavel_rel = relationship("Militante", foreign_keys=[responsavel])
|
||||
responsavel_financas_rel = relationship("Militante", foreign_keys=[responsavel_financas])
|
||||
usuarios = relationship("Usuario", back_populates="setor")
|
||||
celulas = relationship("Celula", back_populates="setor")
|
||||
relatorios_cotas = relationship("RelatorioCotasMensais", back_populates="setor")
|
||||
relatorios_vendas = relationship("RelatorioVendasMateriais", back_populates="setor")
|
||||
|
||||
@@ -164,4 +430,337 @@ class RelatorioVendasMateriais(Base):
|
||||
setor = relationship("Setor", back_populates="relatorios_vendas")
|
||||
comite = relationship("ComiteCentral", back_populates="relatorios_vendas")
|
||||
|
||||
Base.metadata.create_all(engine)
|
||||
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'))
|
||||
militante = relationship("Militante", backref=backref("usuario", uselist=False))
|
||||
|
||||
# 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')
|
||||
|
||||
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
|
||||
|
||||
print(f"Verificando OTP para usuário {self.username}")
|
||||
print(f"OTP Secret: {self.otp_secret}")
|
||||
print(f"Código fornecido: {code}")
|
||||
|
||||
totp = pyotp.totp.TOTP(self.otp_secret)
|
||||
is_valid = totp.verify(code)
|
||||
|
||||
print(f"Resultado da verificação: {'Válido' if is_valid else 'Inválido'}")
|
||||
print(f"Tempo atual: {datetime.utcnow()}")
|
||||
print(f"Período atual: {totp.timecode(datetime.utcnow())}")
|
||||
|
||||
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)
|
||||
|
||||
class PagamentoCelula(Base):
|
||||
__tablename__ = 'pagamentos_celula'
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
celula_id = Column(Integer, ForeignKey('celulas.id'))
|
||||
data = Column(Date)
|
||||
valor = Column(Numeric(10, 2))
|
||||
metodo_pagamento = Column(String(20)) # PIX, Dinheiro, etc.
|
||||
codigo_pix = Column(String(100))
|
||||
descricao = Column(String(255))
|
||||
registrado_por = Column(Integer, ForeignKey('militantes.id'))
|
||||
|
||||
celula = relationship("Celula", back_populates="pagamentos")
|
||||
registrado_por_rel = relationship("Militante", foreign_keys=[registrado_por])
|
||||
|
||||
class Atividade(Base):
|
||||
__tablename__ = 'atividades'
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
descricao = Column(String(255))
|
||||
data = Column(Date)
|
||||
responsavel1 = Column(Integer, ForeignKey('militantes.id'))
|
||||
responsavel2 = Column(Integer, ForeignKey('militantes.id'))
|
||||
|
||||
responsavel1_rel = relationship("Militante", foreign_keys=[responsavel1])
|
||||
responsavel2_rel = relationship("Militante", foreign_keys=[responsavel2])
|
||||
materiais = relationship("MaterialAtividade", back_populates="atividade")
|
||||
|
||||
class MaterialAtividade(Base):
|
||||
__tablename__ = 'materiais_atividades'
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
atividade_id = Column(Integer, ForeignKey('atividades.id'))
|
||||
tipo = Column(String(20)) # Jornal, Revista, etc.
|
||||
quantidade = Column(Integer)
|
||||
detalhes = Column(String(255))
|
||||
|
||||
atividade = relationship("Atividade", back_populates="materiais")
|
||||
|
||||
class Relatorio(Base):
|
||||
__tablename__ = 'relatorios'
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
tipo = Column(String(50)) # Semanal, Quinzenal, Mensal
|
||||
periodo_inicio = Column(Date)
|
||||
periodo_fim = Column(Date)
|
||||
gerado_por = Column(Integer, ForeignKey('militantes.id'))
|
||||
conteudo = Column(Text)
|
||||
# Relacionamento hierárquico
|
||||
celula_id = Column(Integer, ForeignKey('celulas.id'))
|
||||
setor_id = Column(Integer, ForeignKey('setores.id'))
|
||||
cr_id = Column(Integer, ForeignKey('comites_regionais.id'))
|
||||
|
||||
gerado_por_rel = relationship("Militante", foreign_keys=[gerado_por])
|
||||
celula = relationship("Celula", foreign_keys=[celula_id])
|
||||
setor = relationship("Setor", foreign_keys=[setor_id])
|
||||
cr = relationship("ComiteRegional", foreign_keys=[cr_id])
|
||||
|
||||
class TransacaoPIX(Base):
|
||||
__tablename__ = 'transacoes_pix'
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
chave_pix = Column(String(100))
|
||||
valor = Column(Numeric(10, 2))
|
||||
data_geracao = Column(DateTime)
|
||||
data_pagamento = Column(DateTime)
|
||||
status = Column(String(20)) # Pendente, Pago, Expirado
|
||||
qr_code = Column(Text)
|
||||
pagamento_id = Column(Integer, ForeignKey('pagamentos.id'))
|
||||
comprovante_id = Column(Integer, ForeignKey('comprovantes.id'))
|
||||
|
||||
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():
|
||||
"""Inicializa o banco de dados com dados básicos"""
|
||||
print("Inicializando banco de dados...")
|
||||
|
||||
session = get_db_connection()
|
||||
try:
|
||||
# Criar todas as tabelas
|
||||
Base.metadata.drop_all(engine) # Remover todas as tabelas existentes
|
||||
Base.metadata.create_all(engine)
|
||||
|
||||
# Criar roles padrão
|
||||
roles = [
|
||||
("Administrador", Role.SECRETARIO_GERAL),
|
||||
("Secretário", Role.SECRETARIO_CELULA),
|
||||
("Militante", Role.MILITANTE_BASICO)
|
||||
]
|
||||
|
||||
for nome, nivel in roles:
|
||||
if not session.query(Role).filter_by(nome=nome).first():
|
||||
role = Role(nome=nome, nivel=nivel)
|
||||
session.add(role)
|
||||
session.commit()
|
||||
|
||||
# Criar setores padrão
|
||||
setores = ["Setor 1", "Setor 2", "Setor 3"]
|
||||
for nome in setores:
|
||||
if not session.query(Setor).filter_by(nome=nome).first():
|
||||
setor = Setor(nome=nome)
|
||||
session.add(setor)
|
||||
session.commit()
|
||||
|
||||
# Criar comitês padrão
|
||||
comites = ["Comitê 1", "Comitê 2", "Comitê 3"]
|
||||
for nome in comites:
|
||||
if not session.query(ComiteCentral).filter_by(nome=nome).first():
|
||||
comite = ComiteCentral(nome=nome)
|
||||
session.add(comite)
|
||||
session.commit()
|
||||
|
||||
# Gerar OTP para admin
|
||||
admin_otp_secret = pyotp.random_base32()
|
||||
print(f"Novo OTP gerado: {admin_otp_secret}")
|
||||
|
||||
# Criar usuário admin
|
||||
admin_role = session.query(Role).filter_by(nome="Administrador").first()
|
||||
setor = session.query(Setor).first()
|
||||
|
||||
admin = Usuario(
|
||||
username="admin",
|
||||
email="admin@example.com",
|
||||
is_admin=True
|
||||
)
|
||||
admin.set_password("admin123")
|
||||
admin.tipo = "ADMIN"
|
||||
admin.otp_secret = admin_otp_secret
|
||||
admin.roles.append(admin_role)
|
||||
admin.setor = setor
|
||||
session.add(admin)
|
||||
session.commit()
|
||||
|
||||
# Gerar QR code
|
||||
totp = pyotp.totp.TOTP(admin_otp_secret)
|
||||
provisioning_uri = totp.provisioning_uri("admin", issuer_name="Sistema de Controles")
|
||||
|
||||
import qrcode
|
||||
qr = qrcode.QRCode(version=1, box_size=10, border=5)
|
||||
qr.add_data(provisioning_uri)
|
||||
qr.make(fit=True)
|
||||
img = qr.make_image(fill_color="black", back_color="white")
|
||||
img.save('admin_qr.png')
|
||||
|
||||
print("=== Usuário Admin Criado ===")
|
||||
print(f"Username: admin")
|
||||
print(f"Senha: admin123")
|
||||
print(f"Email: {admin.email}")
|
||||
print(f"OTP Secret: {admin_otp_secret}")
|
||||
print(f"QR Code: admin_qr.png")
|
||||
|
||||
# Importar e executar o seed após criar todas as dependências
|
||||
from seed_data import seed_database
|
||||
print("\nPopulando banco de dados com dados de teste...")
|
||||
seed_database()
|
||||
print("Dados de teste criados com sucesso!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Erro na inicialização do banco: {e}")
|
||||
session.rollback()
|
||||
raise
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
init_database()
|
||||
204
functions/decorators.py
Normal file
204
functions/decorators.py
Normal file
@@ -0,0 +1,204 @@
|
||||
from functools import wraps
|
||||
from flask import session, redirect, url_for, flash
|
||||
from flask_login import current_user, login_required
|
||||
from sqlalchemy.orm import joinedload
|
||||
from .database import get_db_connection, Usuario, Role
|
||||
from .rbac import Permission
|
||||
|
||||
def require_login(f):
|
||||
"""Decorador para verificar se o usuário está logado"""
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not current_user.is_authenticated:
|
||||
flash('Por favor, faça login para acessar esta página.', 'danger')
|
||||
return redirect(url_for('login'))
|
||||
|
||||
db = get_db_connection()
|
||||
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
|
||||
|
||||
def require_permission(permission_name):
|
||||
"""Decorador para verificar se o usuário tem uma permissão específica"""
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not current_user.is_authenticated:
|
||||
flash('Você precisa estar logado para acessar esta página.', 'error')
|
||||
return redirect(url_for('login'))
|
||||
|
||||
db = get_db_connection()
|
||||
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.', 'error')
|
||||
return redirect(url_for('login'))
|
||||
|
||||
if not user.has_permission(permission_name):
|
||||
flash('Você não tem permissão para acessar esta página.', 'error')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
# 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)
|
||||
|
||||
return f(*args, **kwargs)
|
||||
finally:
|
||||
db.close()
|
||||
return decorated_function
|
||||
return decorator
|
||||
|
||||
def require_role(role_name):
|
||||
"""Decorador para verificar se o usuário tem um papel específico"""
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not current_user.is_authenticated:
|
||||
flash('Você precisa estar logado para acessar esta página.', 'error')
|
||||
return redirect(url_for('login'))
|
||||
|
||||
db = get_db_connection()
|
||||
try:
|
||||
user = db.query(Usuario).get(current_user.id)
|
||||
if not user or not user.has_role(role_name):
|
||||
flash('Você não tem permissão para acessar esta página.', 'error')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
# Atualiza timestamp da última atividade
|
||||
user.update_last_activity()
|
||||
db.commit()
|
||||
|
||||
return f(*args, **kwargs)
|
||||
finally:
|
||||
db.close()
|
||||
return decorated_function
|
||||
return decorator
|
||||
|
||||
def require_minimum_role(min_level):
|
||||
"""Decorador para verificar se o usuário tem um papel com nível mínimo"""
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not current_user.is_authenticated:
|
||||
flash('Você precisa estar logado para acessar esta página.', 'error')
|
||||
return redirect(url_for('login'))
|
||||
|
||||
db = get_db_connection()
|
||||
try:
|
||||
user = db.query(Usuario).get(current_user.id)
|
||||
if not user:
|
||||
flash('Usuário não encontrado.', 'error')
|
||||
return redirect(url_for('login'))
|
||||
|
||||
highest_role = user.get_highest_role()
|
||||
if not highest_role or highest_role.nivel < min_level:
|
||||
flash('Você não tem permissão para acessar esta página.', 'error')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
# Atualiza timestamp da última atividade
|
||||
user.update_last_activity()
|
||||
db.commit()
|
||||
|
||||
return f(*args, **kwargs)
|
||||
finally:
|
||||
db.close()
|
||||
return decorated_function
|
||||
return decorator
|
||||
|
||||
def require_instance_permission(permission_name, instance_param):
|
||||
"""Decorator para verificar se o usuário tem permissão em uma instância específica"""
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not current_user.is_authenticated:
|
||||
flash('Por favor, faça login para acessar esta página.', 'error')
|
||||
return redirect(url_for('login'))
|
||||
|
||||
# Obtém o ID da instância dos argumentos da função
|
||||
instance_id = kwargs.get(instance_param)
|
||||
if instance_id is None:
|
||||
flash('ID da instância não encontrado.', 'error')
|
||||
return redirect(url_for('home'))
|
||||
|
||||
if not current_user.has_instance_permission(permission_name, instance_id):
|
||||
flash('Você não tem permissão para acessar esta instância.', 'error')
|
||||
return redirect(url_for('home'))
|
||||
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
return decorator
|
||||
|
||||
def require_instance_access(instance_type, instance_id):
|
||||
"""Decorator para verificar se o usuário tem acesso a uma instância específica"""
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not current_user.is_authenticated:
|
||||
flash('Por favor, faça login para acessar esta página.', 'error')
|
||||
return redirect(url_for('login'))
|
||||
|
||||
# Verificar acesso baseado na instância do usuário
|
||||
if instance_type == 'celula':
|
||||
if not (current_user.celula_id == instance_id or
|
||||
current_user.has_permission(Permission.VIEW_SECTOR_REPORTS) or
|
||||
current_user.has_permission(Permission.VIEW_CR_REPORTS) or
|
||||
current_user.has_permission(Permission.VIEW_CC_REPORTS)):
|
||||
flash('Você não tem acesso a esta célula.', 'error')
|
||||
return redirect(url_for('index'))
|
||||
elif instance_type == 'setor':
|
||||
if not (current_user.setor_id == instance_id or
|
||||
current_user.has_permission(Permission.VIEW_CR_REPORTS) or
|
||||
current_user.has_permission(Permission.VIEW_CC_REPORTS)):
|
||||
flash('Você não tem acesso a este setor.', 'error')
|
||||
return redirect(url_for('index'))
|
||||
elif instance_type == 'cr':
|
||||
if not (current_user.cr_id == instance_id or
|
||||
current_user.has_permission(Permission.VIEW_CC_REPORTS)):
|
||||
flash('Você não tem acesso a este CR.', 'error')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
# Atualiza timestamp da última atividade
|
||||
current_user.update_last_activity()
|
||||
db_session.commit()
|
||||
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
return decorator
|
||||
1
functions/notificacao.py
Normal file
1
functions/notificacao.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
222
functions/permissions.py
Normal file
222
functions/permissions.py
Normal file
@@ -0,0 +1,222 @@
|
||||
from functools import wraps
|
||||
from flask import abort, g
|
||||
from .database import Militante, Celula, Setor, CR, CC
|
||||
|
||||
def check_permission(permission_func):
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not permission_func(*args, **kwargs):
|
||||
abort(403)
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
return decorator
|
||||
|
||||
def can_manage_militante(militante_id):
|
||||
"""Verifica se o usuário atual pode gerenciar um militante específico."""
|
||||
if not g.user or not g.user.militante:
|
||||
return False
|
||||
|
||||
militante = Militante.query.get(militante_id)
|
||||
if not militante:
|
||||
return False
|
||||
|
||||
# Secretário Geral e Secretário de Organização podem gerenciar qualquer militante
|
||||
if g.user.militante.responsabilidades & (Militante.SECRETARIO_GERAL | Militante.SECRETARIO_ORGANIZACAO):
|
||||
return True
|
||||
|
||||
# Secretário de CC pode gerenciar militantes do seu CC
|
||||
if g.user.militante.responsabilidades & Militante.SECRETARIO_CC:
|
||||
if militante.celula.setor.cr.cc_id == g.user.militante.celula.setor.cr.cc_id:
|
||||
return True
|
||||
|
||||
# Secretário de CR pode gerenciar militantes do seu CR
|
||||
if g.user.militante.responsabilidades & Militante.SECRETARIO_CR:
|
||||
if militante.celula.setor.cr_id == g.user.militante.celula.setor.cr_id:
|
||||
return True
|
||||
|
||||
# Secretário de Setor pode gerenciar militantes do seu setor
|
||||
if g.user.militante.responsabilidades & Militante.SECRETARIO_SETOR:
|
||||
if militante.celula.setor_id == g.user.militante.celula.setor_id:
|
||||
return True
|
||||
|
||||
# Secretário de Célula pode gerenciar militantes da sua célula
|
||||
if g.user.militante.responsabilidades & Militante.SECRETARIO_CELULA:
|
||||
if militante.celula_id == g.user.militante.celula_id:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def can_manage_celula(celula_id):
|
||||
"""Verifica se o usuário atual pode gerenciar uma célula específica."""
|
||||
if not g.user or not g.user.militante:
|
||||
return False
|
||||
|
||||
celula = Celula.query.get(celula_id)
|
||||
if not celula:
|
||||
return False
|
||||
|
||||
# Secretário Geral e Secretário de Organização podem gerenciar qualquer célula
|
||||
if g.user.militante.responsabilidades & (Militante.SECRETARIO_GERAL | Militante.SECRETARIO_ORGANIZACAO):
|
||||
return True
|
||||
|
||||
# Secretário de CC pode gerenciar células do seu CC
|
||||
if g.user.militante.responsabilidades & Militante.SECRETARIO_CC:
|
||||
if celula.setor.cr.cc_id == g.user.militante.celula.setor.cr.cc_id:
|
||||
return True
|
||||
|
||||
# Secretário de CR pode gerenciar células do seu CR
|
||||
if g.user.militante.responsabilidades & Militante.SECRETARIO_CR:
|
||||
if celula.setor.cr_id == g.user.militante.celula.setor.cr_id:
|
||||
return True
|
||||
|
||||
# Secretário de Setor pode gerenciar células do seu setor
|
||||
if g.user.militante.responsabilidades & Militante.SECRETARIO_SETOR:
|
||||
if celula.setor_id == g.user.militante.celula.setor_id:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def can_manage_setor(setor_id):
|
||||
"""Verifica se o usuário atual pode gerenciar um setor específico."""
|
||||
if not g.user or not g.user.militante:
|
||||
return False
|
||||
|
||||
setor = Setor.query.get(setor_id)
|
||||
if not setor:
|
||||
return False
|
||||
|
||||
# Secretário Geral e Secretário de Organização podem gerenciar qualquer setor
|
||||
if g.user.militante.responsabilidades & (Militante.SECRETARIO_GERAL | Militante.SECRETARIO_ORGANIZACAO):
|
||||
return True
|
||||
|
||||
# Secretário de CC pode gerenciar setores do seu CC
|
||||
if g.user.militante.responsabilidades & Militante.SECRETARIO_CC:
|
||||
if setor.cr.cc_id == g.user.militante.celula.setor.cr.cc_id:
|
||||
return True
|
||||
|
||||
# Secretário de CR pode gerenciar setores do seu CR
|
||||
if g.user.militante.responsabilidades & Militante.SECRETARIO_CR:
|
||||
if setor.cr_id == g.user.militante.celula.setor.cr_id:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def can_manage_cr(cr_id):
|
||||
"""Verifica se o usuário atual pode gerenciar um CR específico."""
|
||||
if not g.user or not g.user.militante:
|
||||
return False
|
||||
|
||||
cr = CR.query.get(cr_id)
|
||||
if not cr:
|
||||
return False
|
||||
|
||||
# Secretário Geral e Secretário de Organização podem gerenciar qualquer CR
|
||||
if g.user.militante.responsabilidades & (Militante.SECRETARIO_GERAL | Militante.SECRETARIO_ORGANIZACAO):
|
||||
return True
|
||||
|
||||
# Secretário de CC pode gerenciar CRs do seu CC
|
||||
if g.user.militante.responsabilidades & Militante.SECRETARIO_CC:
|
||||
if cr.cc_id == g.user.militante.celula.setor.cr.cc_id:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def can_manage_cc(cc_id):
|
||||
"""Verifica se o usuário atual pode gerenciar um CC específico."""
|
||||
if not g.user or not g.user.militante:
|
||||
return False
|
||||
|
||||
# Apenas Secretário Geral e Secretário de Organização podem gerenciar CCs
|
||||
if g.user.militante.responsabilidades & (Militante.SECRETARIO_GERAL | Militante.SECRETARIO_ORGANIZACAO):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def can_manage_financas(instancia_id, tipo_instancia):
|
||||
"""Verifica se o usuário atual pode gerenciar finanças de uma instância específica."""
|
||||
if not g.user or not g.user.militante:
|
||||
return False
|
||||
|
||||
# Secretário Geral e Secretário de Organização podem gerenciar finanças de qualquer instância
|
||||
if g.user.militante.responsabilidades & (Militante.SECRETARIO_GERAL | Militante.SECRETARIO_ORGANIZACAO):
|
||||
return True
|
||||
|
||||
# Responsável de Finanças da instância pode gerenciar suas finanças
|
||||
if tipo_instancia == 'celula':
|
||||
celula = Celula.query.get(instancia_id)
|
||||
if celula and celula.responsavel_financas_id == g.user.militante.id:
|
||||
return True
|
||||
elif tipo_instancia == 'setor':
|
||||
setor = Setor.query.get(instancia_id)
|
||||
if setor and setor.responsavel_financas_id == g.user.militante.id:
|
||||
return True
|
||||
elif tipo_instancia == 'cr':
|
||||
cr = CR.query.get(instancia_id)
|
||||
if cr and cr.responsavel_financas_id == g.user.militante.id:
|
||||
return True
|
||||
elif tipo_instancia == 'cc':
|
||||
cc = CC.query.get(instancia_id)
|
||||
if cc and cc.responsavel_financas_id == g.user.militante.id:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def can_manage_imprensa(instancia_id, tipo_instancia):
|
||||
"""Verifica se o usuário atual pode gerenciar imprensa de uma instância específica."""
|
||||
if not g.user or not g.user.militante:
|
||||
return False
|
||||
|
||||
# Secretário Geral e Secretário de Organização podem gerenciar imprensa de qualquer instância
|
||||
if g.user.militante.responsabilidades & (Militante.SECRETARIO_GERAL | Militante.SECRETARIO_ORGANIZACAO):
|
||||
return True
|
||||
|
||||
# Responsável de Imprensa da instância pode gerenciar sua imprensa
|
||||
if tipo_instancia == 'celula':
|
||||
celula = Celula.query.get(instancia_id)
|
||||
if celula and celula.responsavel_imprensa_id == g.user.militante.id:
|
||||
return True
|
||||
elif tipo_instancia == 'setor':
|
||||
setor = Setor.query.get(instancia_id)
|
||||
if setor and setor.responsavel_imprensa_id == g.user.militante.id:
|
||||
return True
|
||||
elif tipo_instancia == 'cr':
|
||||
cr = CR.query.get(instancia_id)
|
||||
if cr and cr.responsavel_imprensa_id == g.user.militante.id:
|
||||
return True
|
||||
elif tipo_instancia == 'cc':
|
||||
cc = CC.query.get(instancia_id)
|
||||
if cc and cc.responsavel_imprensa_id == g.user.militante.id:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def can_manage_responsabilidades(militante_id):
|
||||
"""Verifica se o usuário atual pode gerenciar responsabilidades de um militante específico."""
|
||||
if not g.user or not g.user.militante:
|
||||
return False
|
||||
|
||||
militante = Militante.query.get(militante_id)
|
||||
if not militante:
|
||||
return False
|
||||
|
||||
# Secretário Geral e Secretário de Organização podem gerenciar responsabilidades de qualquer militante
|
||||
if g.user.militante.responsabilidades & (Militante.SECRETARIO_GERAL | Militante.SECRETARIO_ORGANIZACAO):
|
||||
return True
|
||||
|
||||
# Secretário de CC pode gerenciar responsabilidades de militantes do seu CC
|
||||
if g.user.militante.responsabilidades & Militante.SECRETARIO_CC:
|
||||
if militante.celula.setor.cr.cc_id == g.user.militante.celula.setor.cr.cc_id:
|
||||
return True
|
||||
|
||||
# Secretário de CR pode gerenciar responsabilidades de militantes do seu CR
|
||||
if g.user.militante.responsabilidades & Militante.SECRETARIO_CR:
|
||||
if militante.celula.setor.cr_id == g.user.militante.celula.setor.cr_id:
|
||||
return True
|
||||
|
||||
# Secretário de Setor pode gerenciar responsabilidades de militantes do seu setor
|
||||
if g.user.militante.responsabilidades & Militante.SECRETARIO_SETOR:
|
||||
if militante.celula.setor_id == g.user.militante.celula.setor_id:
|
||||
return True
|
||||
|
||||
return False
|
||||
315
functions/rbac.py
Normal file
315
functions/rbac.py
Normal file
@@ -0,0 +1,315 @@
|
||||
from sqlalchemy import Column, Integer, String, Text, ForeignKey, Table
|
||||
from sqlalchemy.orm import relationship
|
||||
from .base import Base
|
||||
|
||||
# Tabela de mapeamento Role-Permission
|
||||
role_permissions = Table(
|
||||
'role_permissions',
|
||||
Base.metadata,
|
||||
Column('role_id', Integer, ForeignKey('roles.id'), primary_key=True),
|
||||
Column('permission_id', Integer, ForeignKey('permissions.id'), primary_key=True)
|
||||
)
|
||||
|
||||
# Tabela de mapeamento User-Role
|
||||
user_roles = Table(
|
||||
'user_roles',
|
||||
Base.metadata,
|
||||
Column('user_id', Integer, ForeignKey('usuarios.id'), primary_key=True),
|
||||
Column('role_id', Integer, ForeignKey('roles.id'), primary_key=True)
|
||||
)
|
||||
|
||||
class Role(Base):
|
||||
__tablename__ = 'roles'
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
nome = Column(String(50), unique=True, nullable=False)
|
||||
nivel = Column(Integer, nullable=False) # Nível hierárquico
|
||||
descricao = Column(Text)
|
||||
|
||||
# Relacionamentos
|
||||
permissions = relationship("Permission", secondary=role_permissions, back_populates="roles")
|
||||
users = relationship("Usuario", secondary=user_roles, back_populates="roles")
|
||||
|
||||
# Níveis de role
|
||||
MILITANTE_BASICO = 1
|
||||
SECRETARIO_CELULA = 2
|
||||
MEMBRO_SETOR = 3
|
||||
SECRETARIO_SETOR = 4
|
||||
MEMBRO_CR = 5
|
||||
SECRETARIO_CR = 6
|
||||
MEMBRO_CC = 7
|
||||
SECRETARIO_GERAL = 8
|
||||
|
||||
@staticmethod
|
||||
def get_roles_list():
|
||||
return [
|
||||
(Role.MILITANTE_BASICO, "Militante Básico"),
|
||||
(Role.SECRETARIO_CELULA, "Secretário de Célula"),
|
||||
(Role.MEMBRO_SETOR, "Membro de Setor"),
|
||||
(Role.SECRETARIO_SETOR, "Secretário de Setor"),
|
||||
(Role.MEMBRO_CR, "Membro de CR"),
|
||||
(Role.SECRETARIO_CR, "Secretário de CR"),
|
||||
(Role.MEMBRO_CC, "Membro do CC"),
|
||||
(Role.SECRETARIO_GERAL, "Secretário Geral")
|
||||
]
|
||||
|
||||
class Permission(Base):
|
||||
__tablename__ = 'permissions'
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
nome = Column(String(50), unique=True, nullable=False)
|
||||
descricao = Column(Text)
|
||||
|
||||
# Relacionamentos
|
||||
roles = relationship("Role", secondary=role_permissions, back_populates="permissions")
|
||||
|
||||
# Permissões básicas
|
||||
VIEW_OWN_DATA = "view_own_data"
|
||||
EDIT_OWN_DATA = "edit_own_data"
|
||||
VIEW_CELL_DATA = "view_cell_data"
|
||||
CREATE_MILITANT = "create_militant" # Nova permissão para criar militantes
|
||||
|
||||
# Permissões de célula
|
||||
MANAGE_CELL_MEMBERS = "manage_cell_members"
|
||||
CREATE_CELL_MEMBER = "create_cell_member"
|
||||
VIEW_CELL_REPORTS = "view_cell_reports"
|
||||
MANAGE_CELL_REPORTS = "manage_cell_reports" # Nova permissão
|
||||
REGISTER_CELL_PAYMENT = "register_cell_payment"
|
||||
|
||||
# Permissões de setor
|
||||
MANAGE_SECTOR_CELLS = "manage_sector_cells"
|
||||
CREATE_SECTOR_CELL = "create_sector_cell"
|
||||
VIEW_SECTOR_REPORTS = "view_sector_reports"
|
||||
REGISTER_SECTOR_PAYMENT = "register_sector_payment"
|
||||
|
||||
# Permissões de CR
|
||||
MANAGE_CR_SECTORS = "manage_cr_sectors"
|
||||
CREATE_CR_SECTOR = "create_cr_sector"
|
||||
VIEW_CR_REPORTS = "view_cr_reports"
|
||||
REGISTER_CR_PAYMENT = "register_cr_payment"
|
||||
|
||||
# Permissões de CC
|
||||
MANAGE_CC_CRS = "manage_cc_crs"
|
||||
CREATE_CC_CR = "create_cc_cr"
|
||||
VIEW_CC_REPORTS = "view_cc_reports"
|
||||
REGISTER_CC_PAYMENT = "register_cc_payment"
|
||||
SYSTEM_CONFIG = "system_config"
|
||||
|
||||
@staticmethod
|
||||
def get_permissions_list():
|
||||
return [
|
||||
# Permissões básicas
|
||||
(Permission.VIEW_OWN_DATA, "Visualizar próprios dados"),
|
||||
(Permission.EDIT_OWN_DATA, "Editar próprios dados"),
|
||||
(Permission.VIEW_CELL_DATA, "Visualizar dados da célula"),
|
||||
(Permission.CREATE_MILITANT, "Criar novos militantes"), # Nova permissão
|
||||
|
||||
# Permissões de célula
|
||||
(Permission.MANAGE_CELL_MEMBERS, "Gerenciar membros da célula"),
|
||||
(Permission.CREATE_CELL_MEMBER, "Criar membros na célula"),
|
||||
(Permission.VIEW_CELL_REPORTS, "Visualizar relatórios da célula"),
|
||||
(Permission.MANAGE_CELL_REPORTS, "Gerenciar relatórios da célula"), # Nova permissão
|
||||
(Permission.REGISTER_CELL_PAYMENT, "Registrar pagamentos da célula"),
|
||||
|
||||
# Permissões de setor
|
||||
(Permission.MANAGE_SECTOR_CELLS, "Gerenciar células do setor"),
|
||||
(Permission.CREATE_SECTOR_CELL, "Criar células no setor"),
|
||||
(Permission.VIEW_SECTOR_REPORTS, "Visualizar relatórios do setor"),
|
||||
(Permission.REGISTER_SECTOR_PAYMENT, "Registrar pagamentos do setor"),
|
||||
|
||||
# Permissões de CR
|
||||
(Permission.MANAGE_CR_SECTORS, "Gerenciar setores do CR"),
|
||||
(Permission.CREATE_CR_SECTOR, "Criar setores no CR"),
|
||||
(Permission.VIEW_CR_REPORTS, "Visualizar relatórios do CR"),
|
||||
(Permission.REGISTER_CR_PAYMENT, "Registrar pagamentos do CR"),
|
||||
|
||||
# Permissões de CC
|
||||
(Permission.MANAGE_CC_CRS, "Gerenciar CRs"),
|
||||
(Permission.CREATE_CC_CR, "Criar CRs"),
|
||||
(Permission.VIEW_CC_REPORTS, "Visualizar relatórios nacionais"),
|
||||
(Permission.REGISTER_CC_PAYMENT, "Registrar pagamentos nacionais"),
|
||||
(Permission.SYSTEM_CONFIG, "Configurar sistema")
|
||||
]
|
||||
|
||||
def init_rbac():
|
||||
"""Inicializa o sistema RBAC com roles e permissões básicas"""
|
||||
from .database import Usuario, get_db_connection
|
||||
session = get_db_connection()
|
||||
|
||||
try:
|
||||
# Criar role de administrador primeiro
|
||||
admin_role = session.query(Role).filter_by(nome="Administrador").first()
|
||||
if not admin_role:
|
||||
admin_role = Role(nome="Administrador", nivel=Role.SECRETARIO_GERAL)
|
||||
session.add(admin_role)
|
||||
session.commit()
|
||||
|
||||
# Criar outras roles
|
||||
for nivel, nome in Role.get_roles_list():
|
||||
if nome != "Administrador": # Pular Administrador pois já foi criado
|
||||
role = session.query(Role).filter_by(nivel=nivel).first()
|
||||
if not role:
|
||||
role = Role(nome=nome, nivel=nivel)
|
||||
session.add(role)
|
||||
|
||||
# Criar permissões
|
||||
for nome, descricao in Permission.get_permissions_list():
|
||||
permission = session.query(Permission).filter_by(nome=nome).first()
|
||||
if not permission:
|
||||
permission = Permission(nome=nome, descricao=descricao)
|
||||
session.add(permission)
|
||||
|
||||
session.commit()
|
||||
|
||||
# Dar todas as permissões para o admin
|
||||
all_permissions = session.query(Permission).all()
|
||||
admin_role.permissions = all_permissions
|
||||
session.commit()
|
||||
|
||||
# Buscar usuário admin e atribuir role de administrador
|
||||
admin_user = session.query(Usuario).filter_by(username="admin").first()
|
||||
if admin_user:
|
||||
if admin_role not in admin_user.roles:
|
||||
admin_user.roles = [admin_role] # Substituir roles existentes
|
||||
session.commit()
|
||||
|
||||
# Mapear permissões para outros roles
|
||||
for role in session.query(Role).filter(Role.nome != "Administrador").all():
|
||||
# Militante Básico
|
||||
if role.nivel == Role.MILITANTE_BASICO:
|
||||
role.permissions = [
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first()
|
||||
]
|
||||
|
||||
# Secretário de Célula
|
||||
elif role.nivel == Role.SECRETARIO_CELULA:
|
||||
role.permissions = [
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.REGISTER_CELL_PAYMENT).first()
|
||||
]
|
||||
|
||||
# Membro de Setor
|
||||
elif role.nivel == Role.MEMBRO_SETOR:
|
||||
role.permissions = [
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.REGISTER_SECTOR_PAYMENT).first()
|
||||
]
|
||||
|
||||
# Secretário de Setor
|
||||
elif role.nivel == Role.SECRETARIO_SETOR:
|
||||
role.permissions = [
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.REGISTER_SECTOR_PAYMENT).first()
|
||||
]
|
||||
|
||||
# Membro de CR
|
||||
elif role.nivel == Role.MEMBRO_CR:
|
||||
role.permissions = [
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_CR_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.REGISTER_CR_PAYMENT).first()
|
||||
]
|
||||
|
||||
# Secretário de CR
|
||||
elif role.nivel == Role.SECRETARIO_CR:
|
||||
role.permissions = [
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_CR_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.MANAGE_CR_SECTORS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.CREATE_CR_SECTOR).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.REGISTER_CR_PAYMENT).first()
|
||||
]
|
||||
|
||||
# Membro do CC
|
||||
elif role.nivel == Role.MEMBRO_CC:
|
||||
role.permissions = [
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_CR_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.MANAGE_CR_SECTORS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.CREATE_CR_SECTOR).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_CC_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.REGISTER_CC_PAYMENT).first()
|
||||
]
|
||||
|
||||
# Secretário Geral
|
||||
elif role.nivel == Role.SECRETARIO_GERAL:
|
||||
role.permissions = [
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_CR_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.MANAGE_CR_SECTORS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.CREATE_CR_SECTOR).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_CC_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.MANAGE_CC_CRS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.CREATE_CC_CR).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.REGISTER_CC_PAYMENT).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.SYSTEM_CONFIG).first()
|
||||
]
|
||||
|
||||
session.commit()
|
||||
|
||||
except Exception as e:
|
||||
print(f"Erro ao inicializar RBAC: {e}")
|
||||
session.rollback()
|
||||
raise
|
||||
finally:
|
||||
session.close()
|
||||
1
functions/relatorio.py
Normal file
1
functions/relatorio.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
23
functions/usuario.py
Normal file
23
functions/usuario.py
Normal 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, [])
|
||||
19
init_db.py
Normal file
19
init_db.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from functions.database import init_database
|
||||
from functions.rbac import init_rbac
|
||||
from create_admin import create_admin_user
|
||||
from create_test_users import create_test_users
|
||||
|
||||
def init_system():
|
||||
print("Inicializando banco de dados...")
|
||||
init_database()
|
||||
|
||||
print("Inicializando sistema RBAC...")
|
||||
init_rbac()
|
||||
|
||||
print("Criando usuários iniciais...")
|
||||
create_admin_user()
|
||||
create_test_users()
|
||||
|
||||
if __name__ == "__main__":
|
||||
init_system()
|
||||
print("Sistema inicializado com sucesso!")
|
||||
58
init_system.py
Normal file
58
init_system.py
Normal file
@@ -0,0 +1,58 @@
|
||||
from create_admin import create_admin
|
||||
from create_test_users import create_test_users
|
||||
from functions.database import get_db_connection, Usuario
|
||||
from functions.rbac import Role
|
||||
|
||||
def init_system():
|
||||
print("=== Inicializando Sistema ===")
|
||||
|
||||
# Criar admin
|
||||
print("\nCriando usuário admin...")
|
||||
create_admin()
|
||||
|
||||
# Criar usuários de teste
|
||||
print("\nCriando usuários de teste...")
|
||||
create_test_users()
|
||||
|
||||
# Verificar configuração
|
||||
print("\n=== Verificando Configuração ===")
|
||||
session = get_db_connection()
|
||||
try:
|
||||
# Verificar admin
|
||||
admin = session.query(Usuario).filter_by(username='admin').first()
|
||||
if admin:
|
||||
print("Admin: OK")
|
||||
print(f"OTP configurado: {'Sim' if admin.otp_secret else 'Não'}")
|
||||
else:
|
||||
print("Admin: FALHOU")
|
||||
|
||||
# Verificar usuários de teste
|
||||
test_users = ['aligner', 'tester', 'deployer']
|
||||
for username in test_users:
|
||||
user = session.query(Usuario).filter_by(username=username).first()
|
||||
if user:
|
||||
print(f"{username}: OK")
|
||||
print(f"OTP configurado: {'Sim' if user.otp_secret else 'Não'}")
|
||||
else:
|
||||
print(f"{username}: FALHOU")
|
||||
|
||||
print("\n=== Instruções ===")
|
||||
print("1. Use o aplicativo autenticador para configurar o OTP de cada usuário")
|
||||
print("2. Faça login com cada usuário para testar")
|
||||
print("3. Altere a senha no primeiro login")
|
||||
print("\nCredenciais:")
|
||||
print("Admin:")
|
||||
print(" Usuário: admin")
|
||||
print(" Senha: admin123")
|
||||
print("\nUsuários de teste:")
|
||||
print(" Usuário: aligner, tester, deployer")
|
||||
print(" Senha: Test123!@#")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Erro ao verificar configuração: {str(e)}")
|
||||
session.rollback()
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
init_system()
|
||||
64
migrations/versions/add_responsaveis_financas_imprensa.py
Normal file
64
migrations/versions/add_responsaveis_financas_imprensa.py
Normal file
@@ -0,0 +1,64 @@
|
||||
"""add_responsaveis_financas_imprensa
|
||||
|
||||
Revision ID: add_responsaveis_financas_imprensa
|
||||
Revises: add_aspirante_fields
|
||||
Create Date: 2024-03-19 10:00:00.000000
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'add_responsaveis_financas_imprensa'
|
||||
down_revision = 'add_aspirante_fields'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# Adicionar colunas na tabela celulas
|
||||
op.add_column('celulas', sa.Column('responsavel_financas_id', sa.Integer(), nullable=True))
|
||||
op.add_column('celulas', sa.Column('responsavel_imprensa_id', sa.Integer(), nullable=True))
|
||||
op.create_foreign_key('fk_celulas_responsavel_financas', 'celulas', 'militantes', ['responsavel_financas_id'], ['id'])
|
||||
op.create_foreign_key('fk_celulas_responsavel_imprensa', 'celulas', 'militantes', ['responsavel_imprensa_id'], ['id'])
|
||||
|
||||
# Adicionar colunas na tabela setores
|
||||
op.add_column('setores', sa.Column('responsavel_financas_id', sa.Integer(), nullable=True))
|
||||
op.add_column('setores', sa.Column('responsavel_imprensa_id', sa.Integer(), nullable=True))
|
||||
op.create_foreign_key('fk_setores_responsavel_financas', 'setores', 'militantes', ['responsavel_financas_id'], ['id'])
|
||||
op.create_foreign_key('fk_setores_responsavel_imprensa', 'setores', 'militantes', ['responsavel_imprensa_id'], ['id'])
|
||||
|
||||
# Adicionar colunas na tabela crs
|
||||
op.add_column('crs', sa.Column('responsavel_financas_id', sa.Integer(), nullable=True))
|
||||
op.add_column('crs', sa.Column('responsavel_imprensa_id', sa.Integer(), nullable=True))
|
||||
op.create_foreign_key('fk_crs_responsavel_financas', 'crs', 'militantes', ['responsavel_financas_id'], ['id'])
|
||||
op.create_foreign_key('fk_crs_responsavel_imprensa', 'crs', 'militantes', ['responsavel_imprensa_id'], ['id'])
|
||||
|
||||
# Adicionar colunas na tabela ccs
|
||||
op.add_column('ccs', sa.Column('responsavel_financas_id', sa.Integer(), nullable=True))
|
||||
op.add_column('ccs', sa.Column('responsavel_imprensa_id', sa.Integer(), nullable=True))
|
||||
op.create_foreign_key('fk_ccs_responsavel_financas', 'ccs', 'militantes', ['responsavel_financas_id'], ['id'])
|
||||
op.create_foreign_key('fk_ccs_responsavel_imprensa', 'ccs', 'militantes', ['responsavel_imprensa_id'], ['id'])
|
||||
|
||||
|
||||
def downgrade():
|
||||
# Remover foreign keys
|
||||
op.drop_constraint('fk_celulas_responsavel_financas', 'celulas', type_='foreignkey')
|
||||
op.drop_constraint('fk_celulas_responsavel_imprensa', 'celulas', type_='foreignkey')
|
||||
op.drop_constraint('fk_setores_responsavel_financas', 'setores', type_='foreignkey')
|
||||
op.drop_constraint('fk_setores_responsavel_imprensa', 'setores', type_='foreignkey')
|
||||
op.drop_constraint('fk_crs_responsavel_financas', 'crs', type_='foreignkey')
|
||||
op.drop_constraint('fk_crs_responsavel_imprensa', 'crs', type_='foreignkey')
|
||||
op.drop_constraint('fk_ccs_responsavel_financas', 'ccs', type_='foreignkey')
|
||||
op.drop_constraint('fk_ccs_responsavel_imprensa', 'ccs', type_='foreignkey')
|
||||
|
||||
# Remover colunas
|
||||
op.drop_column('celulas', 'responsavel_financas_id')
|
||||
op.drop_column('celulas', 'responsavel_imprensa_id')
|
||||
op.drop_column('setores', 'responsavel_financas_id')
|
||||
op.drop_column('setores', 'responsavel_imprensa_id')
|
||||
op.drop_column('crs', 'responsavel_financas_id')
|
||||
op.drop_column('crs', 'responsavel_imprensa_id')
|
||||
op.drop_column('ccs', 'responsavel_financas_id')
|
||||
op.drop_column('ccs', 'responsavel_imprensa_id')
|
||||
18
models/entities/assinatura_jornal.py
Normal file
18
models/entities/assinatura_jornal.py
Normal 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
17
models/entities/base.py
Normal 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()
|
||||
15
models/entities/comprovante.py
Normal file
15
models/entities/comprovante.py
Normal 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")
|
||||
17
models/entities/cota_mensal.py
Normal file
17
models/entities/cota_mensal.py
Normal 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")
|
||||
14
models/entities/email_militante.py
Normal file
14
models/entities/email_militante.py
Normal 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")
|
||||
19
models/entities/endereco.py
Normal file
19
models/entities/endereco.py
Normal 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")
|
||||
17
models/entities/material_vendido.py
Normal file
17
models/entities/material_vendido.py
Normal 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")
|
||||
155
models/entities/militante.py
Normal file
155
models/entities/militante.py
Normal 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()
|
||||
21
models/entities/pagamento.py
Normal file
21
models/entities/pagamento.py
Normal 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")
|
||||
15
models/entities/rede_social.py
Normal file
15
models/entities/rede_social.py
Normal 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")
|
||||
13
models/entities/tipo_material.py
Normal file
13
models/entities/tipo_material.py
Normal 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
126
models/entities/usuario.py
Normal 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)
|
||||
15
models/entities/venda_jornal.py
Normal file
15
models/entities/venda_jornal.py
Normal 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])
|
||||
15
models/entities/venda_jornal_avulso.py
Normal file
15
models/entities/venda_jornal_avulso.py
Normal 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")
|
||||
5
pytest.ini
Normal file
5
pytest.ini
Normal file
@@ -0,0 +1,5 @@
|
||||
[pytest]
|
||||
pythonpath = .
|
||||
testpaths = tests
|
||||
python_files = test_*.py
|
||||
addopts = -v --cov=. --cov-report=term-missing
|
||||
@@ -1,19 +1,19 @@
|
||||
black==24.10.0
|
||||
blinker==1.9.0
|
||||
click==8.1.7
|
||||
Flask==3.1.0
|
||||
greenlet==3.1.1
|
||||
importlib_metadata==8.5.0
|
||||
itsdangerous==2.2.0
|
||||
Jinja2==3.1.4
|
||||
MarkupSafe==3.0.2
|
||||
mypy-extensions==1.0.0
|
||||
mysql-connector-python==9.1.0
|
||||
packaging==24.2
|
||||
pathspec==0.12.1
|
||||
platformdirs==4.3.6
|
||||
SQLAlchemy==2.0.36
|
||||
tomli==2.2.1
|
||||
typing_extensions==4.12.2
|
||||
Werkzeug==3.1.3
|
||||
zipp==3.21.0
|
||||
Flask==3.0.2
|
||||
Flask-SQLAlchemy==3.1.1
|
||||
Flask-Login==0.6.3
|
||||
Flask-WTF==1.2.1
|
||||
Flask-Mail==0.9.1
|
||||
SQLAlchemy==2.0.27
|
||||
Werkzeug==3.0.1
|
||||
python-dotenv==1.0.1
|
||||
pyotp==2.9.0
|
||||
qrcode==7.4.2
|
||||
Pillow==9.5.0
|
||||
email-validator==2.1.0.post1
|
||||
cryptography==42.0.2
|
||||
bcrypt==4.1.2
|
||||
Bootstrap-Flask==2.3.3
|
||||
flask-bootstrap5==0.1.dev1
|
||||
PyJWT==2.8.0
|
||||
gunicorn==21.2.0
|
||||
Faker==19.13.0
|
||||
|
||||
2
routes/__init__.py
Normal file
2
routes/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
# Este arquivo está intencionalmente vazio
|
||||
# Ele é usado para marcar o diretório como um pacote Python
|
||||
135
routes/admin.py
Normal file
135
routes/admin.py
Normal file
@@ -0,0 +1,135 @@
|
||||
from flask import Blueprint, render_template, flash, redirect, url_for, request, jsonify
|
||||
from functions.database import Usuario, get_db_connection
|
||||
from functions.decorators import require_permission, require_role, require_minimum_role
|
||||
from flask_login import login_required, current_user
|
||||
from sqlalchemy.orm import joinedload
|
||||
import pyotp
|
||||
from werkzeug.security import generate_password_hash
|
||||
import secrets
|
||||
from functools import wraps
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
import logging
|
||||
<<<<<<< HEAD
|
||||
from datetime import datetime
|
||||
=======
|
||||
>>>>>>> a22b0e4 (refactor(#11): Integra listagem de usuários no dashboard)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
|
||||
|
||||
def admin_required(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not current_user.is_admin:
|
||||
flash('Acesso não autorizado.', 'danger')
|
||||
return redirect(url_for('main.index'))
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
@admin_bp.route('/')
|
||||
@login_required
|
||||
@admin_required
|
||||
def dashboard():
|
||||
"""Dashboard principal da área administrativa com lista de usuários"""
|
||||
db = get_db_connection()
|
||||
try:
|
||||
now = datetime.now()
|
||||
|
||||
# Carregar estatísticas relevantes
|
||||
total_users = db.query(Usuario).count()
|
||||
active_users = db.query(Usuario).filter(Usuario.is_active == True).count()
|
||||
inactive_users = total_users - active_users
|
||||
|
||||
# Carregar lista de usuários
|
||||
users = db.query(Usuario).options(
|
||||
joinedload(Usuario.roles),
|
||||
joinedload(Usuario.militante)
|
||||
).all()
|
||||
|
||||
return render_template(
|
||||
'admin/dashboard.html',
|
||||
total_users=total_users,
|
||||
active_users=active_users,
|
||||
inactive_users=inactive_users,
|
||||
<<<<<<< HEAD
|
||||
users=users,
|
||||
now=now
|
||||
=======
|
||||
users=users
|
||||
>>>>>>> a22b0e4 (refactor(#11): Integra listagem de usuários no dashboard)
|
||||
)
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"Erro ao buscar dados do dashboard: {str(e)}")
|
||||
flash('Erro ao carregar dados. Por favor, tente novamente.', 'danger')
|
||||
return render_template('admin/dashboard.html',
|
||||
total_users=0,
|
||||
active_users=0,
|
||||
inactive_users=0,
|
||||
users=[])
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@admin_bp.route('/users/<int:user_id>/reset-otp', methods=['POST'])
|
||||
@login_required
|
||||
@require_role('ADMIN')
|
||||
def reset_user_otp(user_id):
|
||||
"""Reseta o OTP de um usuário"""
|
||||
db = get_db_connection()
|
||||
try:
|
||||
user = db.query(Usuario).get(user_id)
|
||||
if not user:
|
||||
flash('Usuário não encontrado.', 'danger')
|
||||
return redirect(url_for('admin.dashboard'))
|
||||
|
||||
# Gerar novo segredo OTP
|
||||
user.otp_secret = pyotp.random_base32()
|
||||
db.commit()
|
||||
|
||||
flash(f'OTP resetado com sucesso para {user.email}.', 'success')
|
||||
return redirect(url_for('admin.dashboard'))
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@admin_bp.route('/users/<int:user_id>/reset-password', methods=['POST'])
|
||||
@login_required
|
||||
@require_role('ADMIN')
|
||||
def reset_user_password(user_id):
|
||||
"""Reseta a senha de um usuário"""
|
||||
db = get_db_connection()
|
||||
try:
|
||||
user = db.query(Usuario).get(user_id)
|
||||
if not user:
|
||||
flash('Usuário não encontrado.', 'danger')
|
||||
return redirect(url_for('admin.dashboard'))
|
||||
|
||||
# Gerar nova senha aleatória
|
||||
new_password = secrets.token_urlsafe(8)
|
||||
user.password = generate_password_hash(new_password)
|
||||
db.commit()
|
||||
|
||||
flash(f'Senha resetada com sucesso. Nova senha: {new_password}', 'success')
|
||||
return redirect(url_for('admin.dashboard'))
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@admin_bp.route('/users/<int:user_id>/toggle-status', methods=['POST'])
|
||||
@login_required
|
||||
@require_role('ADMIN')
|
||||
def toggle_user_status(user_id):
|
||||
"""Ativa/desativa um usuário"""
|
||||
db = get_db_connection()
|
||||
try:
|
||||
user = db.query(Usuario).get(user_id)
|
||||
if not user:
|
||||
flash('Usuário não encontrado.', 'danger')
|
||||
return redirect(url_for('admin.dashboard'))
|
||||
|
||||
user.is_active = not user.is_active
|
||||
db.commit()
|
||||
|
||||
status = 'ativado' if user.is_active else 'desativado'
|
||||
flash(f'Usuário {status} com sucesso.', 'success')
|
||||
return redirect(url_for('admin.dashboard'))
|
||||
finally:
|
||||
db.close()
|
||||
61
routes/auth.py
Normal file
61
routes/auth.py
Normal 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())
|
||||
147
routes/cota.py
147
routes/cota.py
@@ -1,30 +1,123 @@
|
||||
from flask import Blueprint, request, jsonify
|
||||
from models.integracao import calcular_cota
|
||||
from flask import Blueprint, render_template, redirect, url_for, request, jsonify, flash
|
||||
from flask_login import login_required
|
||||
|
||||
cota_bp = Blueprint('cota', __name__)
|
||||
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.route('/calculate_cota', methods=['POST'])
|
||||
def calculate_cota():
|
||||
try:
|
||||
data = request.get_json()
|
||||
|
||||
# Extrair dados do request
|
||||
salary = float(data.get('salary', 0))
|
||||
num_children = int(data.get('num_children', 0))
|
||||
pays_school = bool(data.get('pays_school', False))
|
||||
pays_rent = bool(data.get('pays_rent', False))
|
||||
num_parents = int(data.get('num_parents', 0))
|
||||
|
||||
# Calcular a cota (implemente sua lógica de cálculo aqui)
|
||||
cota = calcular_cota(
|
||||
salary=salary,
|
||||
num_children=num_children,
|
||||
pays_school=pays_school,
|
||||
pays_rent=pays_rent,
|
||||
num_parents=num_parents
|
||||
)
|
||||
|
||||
return jsonify({'cota': cota})
|
||||
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"
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 400
|
||||
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
41
routes/main.py
Normal 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
50
routes/militante.py
Normal 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
116
routes/pagamento.py
Normal 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
149
routes/relatorio.py
Normal 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'))
|
||||
17
run_tests.sh
Executable file
17
run_tests.sh
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Criar e ativar ambiente virtual
|
||||
python -m venv venv
|
||||
source venv/bin/activate
|
||||
|
||||
# Instalar dependências de teste
|
||||
pip install -r tests/requirements-test.txt
|
||||
|
||||
# Instalar o projeto em modo de desenvolvimento
|
||||
pip install -e .
|
||||
|
||||
# Executar testes
|
||||
python -m pytest
|
||||
|
||||
# Desativar ambiente virtual
|
||||
deactivate
|
||||
27
scripts/init_db.py
Normal file
27
scripts/init_db.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from functions.database import Role, Permissao, RolePermissao, Base, engine
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
def init_db():
|
||||
Base.metadata.create_all(engine)
|
||||
|
||||
with Session(engine) as session:
|
||||
# Criar roles
|
||||
admin = Role(nome='Administrador', nivel=1)
|
||||
coord = Role(nome='Coordenador', nivel=2)
|
||||
milit = Role(nome='Militante', nivel=3)
|
||||
|
||||
# Criar permissões
|
||||
perm_admin = Permissao(nome='admin', descricao='Acesso total')
|
||||
perm_militantes = Permissao(nome='ver_militantes', descricao='Ver militantes')
|
||||
# ... outras permissões ...
|
||||
|
||||
session.add_all([admin, coord, milit, perm_admin, perm_militantes])
|
||||
session.commit()
|
||||
|
||||
# Associar permissões aos roles
|
||||
session.add(RolePermissao(role=admin, permissao=perm_admin))
|
||||
session.add(RolePermissao(role=coord, permissao=perm_militantes))
|
||||
session.commit()
|
||||
|
||||
if __name__ == '__main__':
|
||||
init_db()
|
||||
44
scripts/prepare_mvc.sh
Executable file
44
scripts/prepare_mvc.sh
Executable 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"
|
||||
344
seed_data.py
Normal file
344
seed_data.py
Normal file
@@ -0,0 +1,344 @@
|
||||
from datetime import datetime, timedelta
|
||||
from functions.database import (
|
||||
Base, Militante, CotaMensal, TipoComprovante, Comprovante,
|
||||
MaterialVendido, TipoMaterial, VendaJornalAvulso, AssinaturaAnual,
|
||||
RelatorioCotasMensais, RelatorioVendasMateriais, engine, SessionLocal,
|
||||
Setor, ComiteCentral, Usuario, Role, EmailMilitante, Endereco,
|
||||
ComiteRegional, Celula, EstadoMilitante, get_db_connection,
|
||||
init_database
|
||||
)
|
||||
import random
|
||||
from faker import Faker
|
||||
import time
|
||||
from werkzeug.security import generate_password_hash
|
||||
|
||||
fake = Faker('pt_BR')
|
||||
|
||||
def criar_estrutura_organizacional(session):
|
||||
"""Cria a estrutura organizacional básica"""
|
||||
print("\nCriando estrutura organizacional...")
|
||||
|
||||
# Criar Comitê Central
|
||||
cc = ComiteCentral(nome="Comitê Central SP")
|
||||
session.add(cc)
|
||||
session.flush()
|
||||
|
||||
# Criar Comitês Regionais
|
||||
crs = []
|
||||
for nome in ["CR São Paulo", "CR ABC", "CR Campinas"]:
|
||||
cr = ComiteRegional(nome=nome)
|
||||
session.add(cr)
|
||||
session.flush()
|
||||
crs.append(cr)
|
||||
|
||||
# Criar Setores para cada CR
|
||||
setores = []
|
||||
for cr in crs:
|
||||
for i in range(2): # 2 setores por CR
|
||||
setor = Setor(
|
||||
nome=f"Setor {i+1} - {cr.nome}",
|
||||
cr_id=cr.id
|
||||
)
|
||||
session.add(setor)
|
||||
session.flush()
|
||||
setores.append(setor)
|
||||
|
||||
# Criar Células para cada Setor
|
||||
for setor in setores:
|
||||
for i in range(2): # 2 células por setor
|
||||
celula = Celula(
|
||||
nome=f"Célula {i+1} - {setor.nome}",
|
||||
setor_id=setor.id
|
||||
)
|
||||
session.add(celula)
|
||||
|
||||
session.commit()
|
||||
return crs, setores
|
||||
|
||||
def criar_tipos_comprovante(session):
|
||||
"""Cria tipos de comprovante padrão"""
|
||||
print("\nCriando tipos de comprovante...")
|
||||
tipos = [
|
||||
"Comprovante Padrão",
|
||||
"Comprovante Especial",
|
||||
"Comprovante Extraordinário",
|
||||
"Jornal Avulso",
|
||||
"Assinatura de Jornal",
|
||||
"Campanha Financeira"
|
||||
]
|
||||
|
||||
for tipo in tipos:
|
||||
if not session.query(TipoComprovante).filter_by(descricao=tipo).first():
|
||||
session.add(TipoComprovante(descricao=tipo))
|
||||
|
||||
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):
|
||||
"""Cria tipos de material padrão"""
|
||||
print("\nCriando tipos de material...")
|
||||
tipos = [
|
||||
"Jornal",
|
||||
"Revista",
|
||||
"Livro",
|
||||
"Panfleto",
|
||||
"Cartilha"
|
||||
]
|
||||
for tipo in tipos:
|
||||
if not session.query(TipoMaterial).filter_by(descricao=tipo).first():
|
||||
session.add(TipoMaterial(descricao=tipo))
|
||||
session.commit()
|
||||
|
||||
def criar_militantes(session, num_militantes, setores):
|
||||
"""Cria militantes com todos os dados necessários"""
|
||||
print(f"\nCriando {num_militantes} militantes...")
|
||||
militantes = []
|
||||
emails_usados = set()
|
||||
|
||||
for i in range(num_militantes):
|
||||
try:
|
||||
# Dados básicos
|
||||
nome = fake.name()
|
||||
cpf = fake.cpf()
|
||||
|
||||
# Email único
|
||||
while True:
|
||||
email = fake.email()
|
||||
if email not in emails_usados:
|
||||
emails_usados.add(email)
|
||||
break
|
||||
|
||||
# Criar endereço
|
||||
endereco = Endereco(
|
||||
cep=fake.postcode(),
|
||||
estado=fake.estado_sigla(),
|
||||
cidade=fake.city(),
|
||||
bairro=fake.bairro(),
|
||||
rua=fake.street_name(),
|
||||
numero=str(random.randint(1, 999)),
|
||||
complemento=f"Bloco {random.randint(1, 10)}, Apto {random.randint(1, 999)}" if random.random() < 0.3 else None
|
||||
)
|
||||
session.add(endereco)
|
||||
session.flush()
|
||||
|
||||
# Selecionar setor e célula aleatórios
|
||||
setor = random.choice(setores)
|
||||
celula = random.choice(session.query(Celula).filter_by(setor_id=setor.id).all())
|
||||
|
||||
# Definir responsabilidades
|
||||
responsabilidades = 0
|
||||
if random.random() < 0.2: # 20% chance de ser Responsável de Finanças
|
||||
responsabilidades |= Militante.RESPONSAVEL_FINANCAS
|
||||
if random.random() < 0.2: # 20% chance de ser Responsável de Imprensa
|
||||
responsabilidades |= Militante.RESPONSAVEL_IMPRENSA
|
||||
if random.random() < 0.2: # 20% chance de ser Quadro-Orientador
|
||||
responsabilidades |= Militante.QUADRO_ORIENTADOR
|
||||
if random.random() < 0.2: # 20% chance de ser Secretário
|
||||
responsabilidades |= Militante.SECRETARIO
|
||||
if random.random() < 0.2: # 20% chance de ser MPS
|
||||
responsabilidades |= Militante.MPS
|
||||
if random.random() < 0.2: # 20% chance de ser Tesoureiro
|
||||
responsabilidades |= Militante.TESOUREIRO
|
||||
if random.random() < 0.2: # 20% chance de ser MNS
|
||||
responsabilidades |= Militante.MNS
|
||||
if random.random() < 0.2: # 20% chance de ser da Juventude
|
||||
responsabilidades |= Militante.JUVENTUDE
|
||||
if random.random() < 0.3: # 30% chance de ser Aspirante
|
||||
responsabilidades |= Militante.ASPIRANTE
|
||||
|
||||
print(f"Criando militante {i+1}: {nome}")
|
||||
|
||||
# Criar militante com todos os dados
|
||||
militante = Militante(
|
||||
nome=nome,
|
||||
cpf=cpf,
|
||||
titulo_eleitoral=str(random.randint(100000000000, 999999999999)),
|
||||
data_nascimento=fake.date_of_birth(minimum_age=18, maximum_age=65),
|
||||
data_entrada_oci=fake.date_between(start_date='-5y', end_date='today'),
|
||||
data_efetivacao_oci=fake.date_between(start_date='-4y', end_date='today'),
|
||||
telefone1=fake.phone_number(),
|
||||
telefone2=fake.phone_number() if random.random() < 0.3 else None,
|
||||
profissao=fake.job(),
|
||||
regime_trabalho=random.choice(['CLT', 'PJ', 'Estatutário', 'Autônomo']),
|
||||
empresa=fake.company(),
|
||||
contratante=fake.company() if random.random() < 0.2 else None,
|
||||
instituicao_ensino=fake.company() if random.random() < 0.4 else None,
|
||||
tipo_instituicao=random.choice(['Federal', 'Estadual', 'Municipal', 'Privada']) if random.random() < 0.4 else None,
|
||||
sindicato=fake.company() if random.random() < 0.6 else None,
|
||||
cargo_sindical=random.choice(['Diretor', 'Delegado', 'Conselheiro']) if random.random() < 0.3 else None,
|
||||
dirigente_sindical=random.random() < 0.2,
|
||||
central_sindical=random.choice(['CUT', 'CSP-Conlutas', 'CTB', 'Força Sindical']) if random.random() < 0.4 else None,
|
||||
endereco_id=endereco.id,
|
||||
celula_id=celula.id,
|
||||
responsabilidades=responsabilidades,
|
||||
estado=random.choice(list(EstadoMilitante))
|
||||
)
|
||||
session.add(militante)
|
||||
session.flush()
|
||||
|
||||
# Criar email do militante
|
||||
email_militante = EmailMilitante(
|
||||
militante_id=militante.id,
|
||||
endereco_email=email
|
||||
)
|
||||
session.add(email_militante)
|
||||
|
||||
militantes.append(militante)
|
||||
session.commit()
|
||||
|
||||
except Exception as e:
|
||||
print(f"Erro ao criar militante {i+1}: {e}")
|
||||
session.rollback()
|
||||
continue
|
||||
|
||||
return militantes
|
||||
|
||||
def criar_cotas(session, militantes):
|
||||
"""Cria cotas mensais para os militantes"""
|
||||
print("\nCriando cotas mensais...")
|
||||
for militante in militantes:
|
||||
try:
|
||||
# Criar 12 cotas (1 ano) para cada militante
|
||||
for i in range(12):
|
||||
data_base = datetime.now() - timedelta(days=30 * i)
|
||||
valor = random.uniform(50, 200)
|
||||
cota = CotaMensal(
|
||||
militante_id=militante.id,
|
||||
valor_antigo=valor,
|
||||
valor_novo=valor * 1.1,
|
||||
data_alteracao=data_base,
|
||||
data_vencimento=data_base + timedelta(days=30),
|
||||
pago=random.choice([True, False])
|
||||
)
|
||||
session.add(cota)
|
||||
session.commit()
|
||||
except Exception as e:
|
||||
print(f"Erro ao criar cotas para militante {militante.nome}: {e}")
|
||||
session.rollback()
|
||||
|
||||
def criar_comprovantes(session, militantes):
|
||||
"""Cria comprovantes para os militantes"""
|
||||
print("\nCriando comprovantes...")
|
||||
tipos_comprovante = session.query(TipoComprovante).all()
|
||||
|
||||
for militante in militantes:
|
||||
try:
|
||||
# Criar entre 3 e 8 comprovantes por militante
|
||||
for _ in range(random.randint(3, 8)):
|
||||
tipo = random.choice(tipos_comprovante)
|
||||
comprovante = Comprovante(
|
||||
militante_id=militante.id,
|
||||
tipo_comprovante=tipo.descricao, # Usando a descrição do tipo
|
||||
valor=random.uniform(10, 1000),
|
||||
data_comprovante=fake.date_between(start_date='-1y', end_date='today')
|
||||
)
|
||||
session.add(comprovante)
|
||||
session.commit()
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
print(f"Erro ao criar comprovantes para militante {militante.nome}: {e}")
|
||||
|
||||
def criar_materiais_vendidos(session, militantes):
|
||||
"""Cria registros de materiais vendidos"""
|
||||
print("\nCriando materiais vendidos...")
|
||||
tipos_material = session.query(TipoMaterial).all()
|
||||
|
||||
for militante in militantes:
|
||||
try:
|
||||
# Criar entre 2 e 5 materiais vendidos por militante
|
||||
for _ in range(random.randint(2, 5)):
|
||||
material = MaterialVendido(
|
||||
militante_id=militante.id,
|
||||
tipo_material_id=random.choice(tipos_material).id,
|
||||
descricao=fake.sentence(),
|
||||
valor=random.uniform(20, 100),
|
||||
data_venda=fake.date_time_between(start_date='-1y', end_date='now')
|
||||
)
|
||||
session.add(material)
|
||||
session.commit()
|
||||
except Exception as e:
|
||||
print(f"Erro ao criar materiais vendidos para militante {militante.nome}: {e}")
|
||||
session.rollback()
|
||||
|
||||
def criar_vendas_jornal(session, militantes):
|
||||
"""Cria vendas de jornal avulso"""
|
||||
print("\nCriando vendas de jornal...")
|
||||
for militante in militantes:
|
||||
try:
|
||||
# Criar entre 2 e 6 vendas de jornal por militante
|
||||
for _ in range(random.randint(2, 6)):
|
||||
quantidade = random.randint(1, 10)
|
||||
valor_unitario = random.uniform(5, 15)
|
||||
venda = VendaJornalAvulso(
|
||||
militante_id=militante.id,
|
||||
quantidade=quantidade,
|
||||
valor_total=quantidade * valor_unitario,
|
||||
data_venda=fake.date_time_between(start_date='-1y', end_date='now')
|
||||
)
|
||||
session.add(venda)
|
||||
session.commit()
|
||||
except Exception as e:
|
||||
print(f"Erro ao criar vendas de jornal para militante {militante.nome}: {e}")
|
||||
session.rollback()
|
||||
|
||||
def criar_assinaturas(session, militantes):
|
||||
"""Cria assinaturas anuais"""
|
||||
print("\nCriando assinaturas anuais...")
|
||||
tipos_material = session.query(TipoMaterial).all()
|
||||
|
||||
for militante in militantes:
|
||||
try:
|
||||
# 30% de chance de ter assinatura
|
||||
if random.random() < 0.3:
|
||||
data_inicio = fake.date_time_between(start_date='-1y', end_date='now')
|
||||
assinatura = AssinaturaAnual(
|
||||
militante_id=militante.id,
|
||||
tipo_material_id=random.choice(tipos_material).id,
|
||||
quantidade=random.randint(1, 3),
|
||||
valor_total=random.uniform(100, 500),
|
||||
data_inicio=data_inicio,
|
||||
data_fim=data_inicio + timedelta(days=365)
|
||||
)
|
||||
session.add(assinatura)
|
||||
session.commit()
|
||||
except Exception as e:
|
||||
print(f"Erro ao criar assinatura para militante {militante.nome}: {e}")
|
||||
session.rollback()
|
||||
|
||||
def seed_database():
|
||||
"""Função principal para popular o banco de dados"""
|
||||
session = get_db_connection()
|
||||
try:
|
||||
print("Iniciando população do banco de dados...")
|
||||
|
||||
# Criar estrutura organizacional
|
||||
crs, setores = criar_estrutura_organizacional(session)
|
||||
|
||||
# Criar tipos básicos
|
||||
criar_tipos_comprovante(session)
|
||||
criar_tipos_material(session)
|
||||
|
||||
# Criar militantes (30 militantes para teste)
|
||||
militantes = criar_militantes(session, 30, setores)
|
||||
|
||||
# Criar dados financeiros e materiais
|
||||
criar_cotas(session, militantes)
|
||||
criar_comprovantes(session, militantes)
|
||||
criar_materiais_vendidos(session, militantes)
|
||||
criar_vendas_jornal(session, militantes)
|
||||
criar_assinaturas(session, militantes)
|
||||
|
||||
print("\nBanco de dados populado com sucesso!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Erro durante a população do banco: {e}")
|
||||
session.rollback()
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
seed_database()
|
||||
35
services/database_service.py
Normal file
35
services/database_service.py
Normal 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()
|
||||
161
services/militante_service.py
Normal file
161
services/militante_service.py
Normal 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()
|
||||
95
services/usuario_service.py
Normal file
95
services/usuario_service.py
Normal 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()
|
||||
18
setup.py
Normal file
18
setup.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="controles",
|
||||
version="0.1",
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
install_requires=[
|
||||
'flask',
|
||||
'flask-login',
|
||||
'flask-sqlalchemy',
|
||||
'flask-wtf',
|
||||
'flask-mail',
|
||||
'python-dotenv',
|
||||
'pyotp',
|
||||
'qrcode',
|
||||
],
|
||||
)
|
||||
66
sql/migrate_db.py
Normal file
66
sql/migrate_db.py
Normal file
@@ -0,0 +1,66 @@
|
||||
import os
|
||||
import sqlite3
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Adiciona o diretório raiz ao PYTHONPATH
|
||||
root_dir = str(Path(__file__).parent.parent)
|
||||
sys.path.append(root_dir)
|
||||
|
||||
from functions.base import Base, engine
|
||||
from functions.database import init_database
|
||||
from functions.rbac import init_rbac
|
||||
|
||||
def execute_sql_file(file_path):
|
||||
"""Executa um arquivo SQL"""
|
||||
print(f"Executando arquivo {file_path}...")
|
||||
|
||||
try:
|
||||
with open(file_path, 'r') as sql_file:
|
||||
sql_commands = sql_file.read().split(';')
|
||||
|
||||
conn = sqlite3.connect('database.db')
|
||||
cursor = conn.cursor()
|
||||
|
||||
for command in sql_commands:
|
||||
command = command.strip()
|
||||
if command:
|
||||
try:
|
||||
cursor.execute(command)
|
||||
except sqlite3.OperationalError as e:
|
||||
if "already exists" in str(e):
|
||||
print(f"Aviso: {str(e)}")
|
||||
else:
|
||||
raise e
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
print(f"Arquivo {file_path} executado com sucesso!")
|
||||
except Exception as e:
|
||||
print(f"Erro ao executar {file_path}: {str(e)}")
|
||||
raise e
|
||||
|
||||
def migrate_database():
|
||||
"""Executa a migração do banco de dados"""
|
||||
print("Inicializando banco de dados...")
|
||||
|
||||
# Criar todas as tabelas
|
||||
Base.metadata.create_all(engine)
|
||||
|
||||
# Executar scripts SQL
|
||||
sql_dir = Path(__file__).parent
|
||||
rbac_tables_sql = sql_dir / 'rbac_tables.sql'
|
||||
|
||||
if rbac_tables_sql.exists():
|
||||
execute_sql_file(rbac_tables_sql)
|
||||
|
||||
# Inicializar RBAC
|
||||
init_rbac()
|
||||
|
||||
# Inicializar banco de dados
|
||||
init_database()
|
||||
|
||||
print("Migração concluída com sucesso!")
|
||||
|
||||
if __name__ == '__main__':
|
||||
migrate_database()
|
||||
47
sql/migrate_rbac.py
Normal file
47
sql/migrate_rbac.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from functions.database import get_db_connection, Usuario
|
||||
from functions.rbac import Role, Permission
|
||||
|
||||
def migrate_existing_users():
|
||||
"""Migra os usuários existentes para o novo sistema RBAC"""
|
||||
session = get_db_connection()
|
||||
|
||||
try:
|
||||
# Buscar todos os usuários
|
||||
usuarios = session.query(Usuario).all()
|
||||
|
||||
# Buscar ou criar role de administrador
|
||||
admin_role = session.query(Role).filter_by(nome="Administrador").first()
|
||||
if not admin_role:
|
||||
admin_role = Role(nome="Administrador", nivel=Role.SECRETARIO_GERAL)
|
||||
session.add(admin_role)
|
||||
|
||||
# Buscar ou criar role de militante básico
|
||||
militante_role = session.query(Role).filter_by(nome="Militante Básico").first()
|
||||
if not militante_role:
|
||||
militante_role = Role(nome="Militante Básico", nivel=Role.MILITANTE_BASICO)
|
||||
session.add(militante_role)
|
||||
|
||||
# Atualizar usuários
|
||||
for usuario in usuarios:
|
||||
# Se o usuário já tem roles, pular
|
||||
if usuario.roles:
|
||||
continue
|
||||
|
||||
# Atribuir role com base no is_admin
|
||||
if usuario.is_admin:
|
||||
usuario.roles.append(admin_role)
|
||||
else:
|
||||
usuario.roles.append(militante_role)
|
||||
|
||||
session.commit()
|
||||
print("Migração de usuários concluída com sucesso!")
|
||||
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
print(f"Erro durante a migração de usuários: {str(e)}")
|
||||
raise e
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
migrate_existing_users()
|
||||
152
sql/rbac_tables.sql
Normal file
152
sql/rbac_tables.sql
Normal file
@@ -0,0 +1,152 @@
|
||||
-- Tabela de roles
|
||||
CREATE TABLE IF NOT EXISTS roles (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
nome VARCHAR(50) NOT NULL UNIQUE,
|
||||
nivel INTEGER NOT NULL,
|
||||
descricao TEXT
|
||||
);
|
||||
|
||||
-- Tabela de permissões
|
||||
CREATE TABLE IF NOT EXISTS permissions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
nome VARCHAR(50) NOT NULL UNIQUE,
|
||||
descricao TEXT
|
||||
);
|
||||
|
||||
-- Tabela de mapeamento Role-Permission
|
||||
CREATE TABLE IF NOT EXISTS role_permissions (
|
||||
role_id INTEGER NOT NULL,
|
||||
permission_id INTEGER NOT NULL,
|
||||
PRIMARY KEY (role_id, permission_id),
|
||||
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Tabela de mapeamento User-Role
|
||||
CREATE TABLE IF NOT EXISTS user_roles (
|
||||
user_id INTEGER NOT NULL,
|
||||
role_id INTEGER NOT NULL,
|
||||
PRIMARY KEY (user_id, role_id),
|
||||
FOREIGN KEY (user_id) REFERENCES usuarios(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Inserir roles básicas
|
||||
INSERT OR IGNORE INTO roles (nome, nivel, descricao) VALUES
|
||||
('Militante Básico', 1, 'Militante com permissões básicas'),
|
||||
('Secretário de Célula', 2, 'Responsável por uma célula'),
|
||||
('Membro de Setor', 3, 'Membro de um setor'),
|
||||
('Secretário de Setor', 4, 'Responsável por um setor'),
|
||||
('Membro de CR', 5, 'Membro de um Comitê Regional'),
|
||||
('Secretário de CR', 6, 'Responsável por um Comitê Regional'),
|
||||
('Membro do CC', 7, 'Membro do Comitê Central'),
|
||||
('Secretário Geral', 8, 'Secretário Geral ou de Organização do CC');
|
||||
|
||||
-- Inserir permissões básicas
|
||||
INSERT OR IGNORE INTO permissions (nome, descricao) VALUES
|
||||
-- Permissões básicas
|
||||
('view_own_data', 'Visualizar próprios dados'),
|
||||
('edit_own_data', 'Editar próprios dados'),
|
||||
('view_cell_data', 'Visualizar dados da célula'),
|
||||
('create_militant', 'Criar novos militantes'),
|
||||
|
||||
-- Permissões de célula
|
||||
('manage_cell_members', 'Gerenciar membros da célula'),
|
||||
('create_cell_member', 'Criar membros na célula'),
|
||||
('view_cell_reports', 'Visualizar relatórios da célula'),
|
||||
|
||||
-- Permissões de setor
|
||||
('manage_sector_cells', 'Gerenciar células do setor'),
|
||||
('create_sector_cell', 'Criar células no setor'),
|
||||
('view_sector_reports', 'Visualizar relatórios do setor'),
|
||||
|
||||
-- Permissões de CR
|
||||
('manage_cr_sectors', 'Gerenciar setores do CR'),
|
||||
('create_cr_sector', 'Criar setores no CR'),
|
||||
('view_cr_reports', 'Visualizar relatórios do CR'),
|
||||
|
||||
-- Permissões de CC
|
||||
('manage_cc_crs', 'Gerenciar CRs'),
|
||||
('create_cc_cr', 'Criar CRs'),
|
||||
('view_cc_reports', 'Visualizar relatórios nacionais'),
|
||||
('system_config', 'Configurar sistema');
|
||||
|
||||
-- Mapear permissões para roles
|
||||
-- Militante Básico
|
||||
INSERT OR IGNORE INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r, permissions p
|
||||
WHERE r.nome = 'Militante Básico'
|
||||
AND p.nome IN ('view_own_data', 'edit_own_data', 'view_cell_data');
|
||||
|
||||
-- Secretário de Célula
|
||||
INSERT OR IGNORE INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r, permissions p
|
||||
WHERE r.nome = 'Secretário de Célula'
|
||||
AND p.nome IN ('view_own_data', 'edit_own_data', 'view_cell_data',
|
||||
'manage_cell_members', 'create_cell_member', 'view_cell_reports',
|
||||
'create_militant');
|
||||
|
||||
-- Membro de Setor
|
||||
INSERT OR IGNORE INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r, permissions p
|
||||
WHERE r.nome = 'Membro de Setor'
|
||||
AND p.nome IN ('view_own_data', 'edit_own_data', 'view_cell_data',
|
||||
'manage_cell_members', 'create_cell_member', 'view_cell_reports',
|
||||
'view_sector_reports', 'create_militant');
|
||||
|
||||
-- Secretário de Setor
|
||||
INSERT OR IGNORE INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r, permissions p
|
||||
WHERE r.nome = 'Secretário de Setor'
|
||||
AND p.nome IN ('view_own_data', 'edit_own_data', 'view_cell_data',
|
||||
'manage_cell_members', 'create_cell_member', 'view_cell_reports',
|
||||
'view_sector_reports', 'manage_sector_cells', 'create_sector_cell',
|
||||
'create_militant');
|
||||
|
||||
-- Membro de CR
|
||||
INSERT OR IGNORE INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r, permissions p
|
||||
WHERE r.nome = 'Membro de CR'
|
||||
AND p.nome IN ('view_own_data', 'edit_own_data', 'view_cell_data',
|
||||
'manage_cell_members', 'create_cell_member', 'view_cell_reports',
|
||||
'view_sector_reports', 'manage_sector_cells', 'create_sector_cell',
|
||||
'view_cr_reports', 'create_militant');
|
||||
|
||||
-- Secretário de CR
|
||||
INSERT OR IGNORE INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r, permissions p
|
||||
WHERE r.nome = 'Secretário de CR'
|
||||
AND p.nome IN ('view_own_data', 'edit_own_data', 'view_cell_data',
|
||||
'manage_cell_members', 'create_cell_member', 'view_cell_reports',
|
||||
'view_sector_reports', 'manage_sector_cells', 'create_sector_cell',
|
||||
'view_cr_reports', 'manage_cr_sectors', 'create_cr_sector',
|
||||
'create_militant');
|
||||
|
||||
-- Membro do CC
|
||||
INSERT OR IGNORE INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r, permissions p
|
||||
WHERE r.nome = 'Membro do CC'
|
||||
AND p.nome IN ('view_own_data', 'edit_own_data', 'view_cell_data',
|
||||
'manage_cell_members', 'create_cell_member', 'view_cell_reports',
|
||||
'view_sector_reports', 'manage_sector_cells', 'create_sector_cell',
|
||||
'view_cr_reports', 'manage_cr_sectors', 'create_cr_sector',
|
||||
'view_cc_reports', 'create_militant');
|
||||
|
||||
-- Secretário Geral
|
||||
INSERT OR IGNORE INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r, permissions p
|
||||
WHERE r.nome = 'Secretário Geral'
|
||||
AND p.nome IN ('view_own_data', 'edit_own_data', 'view_cell_data',
|
||||
'manage_cell_members', 'create_cell_member', 'view_cell_reports',
|
||||
'view_sector_reports', 'manage_sector_cells', 'create_sector_cell',
|
||||
'view_cr_reports', 'manage_cr_sectors', 'create_cr_sector',
|
||||
'view_cc_reports', 'manage_cc_crs', 'create_cc_cr',
|
||||
'system_config', 'create_militant');
|
||||
626
static/css/components.css
Normal file
626
static/css/components.css
Normal file
@@ -0,0 +1,626 @@
|
||||
/* Variáveis globais */
|
||||
:root {
|
||||
--table-header-bg: #d8dde2;
|
||||
--table-hover-bg: rgba(0, 0, 0, 0.02);
|
||||
--border-color: #dee2e6;
|
||||
--blue: #0d6efd;
|
||||
--green: #198754;
|
||||
--cyan: #0dcaf0;
|
||||
--yellow: #ffc107;
|
||||
--primary-color: #dc3545;
|
||||
--primary-hover: #bb2d3b;
|
||||
--text-color: #333;
|
||||
--text-muted: #6c757d;
|
||||
--bg-hover: #f8f9fa;
|
||||
--tab-active-color: var(--primary-color);
|
||||
--tab-hover-color: rgba(220, 53, 69, 0.1);
|
||||
|
||||
/* Variáveis para os botões */
|
||||
--bs-success: #198754;
|
||||
--bs-success-dark: #157347;
|
||||
--bs-secondary: #6c757d;
|
||||
--bs-secondary-dark: #565e64;
|
||||
|
||||
/* Variáveis para status */
|
||||
--status-active: #28a745;
|
||||
--status-inactive: #dc3545;
|
||||
}
|
||||
|
||||
/* Tabelas */
|
||||
.table-container {
|
||||
background: #fff;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.table {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.table thead {
|
||||
background-color: var(--table-header-bg) !important;
|
||||
}
|
||||
|
||||
.table thead th {
|
||||
border-bottom: none;
|
||||
font-weight: 600;
|
||||
padding: 1rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.table tbody td {
|
||||
padding: 1rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.table-hover tbody tr:hover {
|
||||
background-color: var(--table-hover-bg) !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.table-hover tbody tr {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* Botões de ação */
|
||||
.btn-group-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.btn-group-actions .btn {
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Botões padrão */
|
||||
.btn-outline-primary {
|
||||
color: #0d6efd;
|
||||
border-color: #0d6efd;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.btn-outline-primary:hover {
|
||||
color: #fff;
|
||||
background-color: #0d6efd;
|
||||
border-color: #0d6efd;
|
||||
}
|
||||
|
||||
/* Cabeçalho de listagem */
|
||||
.list-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.list-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.list-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Barra de pesquisa e filtros */
|
||||
.search-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.search-input-group {
|
||||
flex: 1;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.search-input-group .input-group-text {
|
||||
background-color: #f8f9fa;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.search-input-group .form-control {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.search-input-group .form-control:focus {
|
||||
box-shadow: none;
|
||||
border-color: #dee2e6;
|
||||
}
|
||||
|
||||
/* Badges */
|
||||
.badge {
|
||||
font-weight: 500;
|
||||
padding: 0.5em 0.8em;
|
||||
}
|
||||
|
||||
.badge.bg-success {
|
||||
background-color: #198754 !important;
|
||||
}
|
||||
|
||||
.badge.bg-secondary {
|
||||
background-color: #6c757d !important;
|
||||
}
|
||||
|
||||
/* Paginação */
|
||||
.pagination-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.page-link {
|
||||
padding: 0.375rem 0.75rem;
|
||||
}
|
||||
|
||||
/* Responsividade */
|
||||
@media (max-width: 768px) {
|
||||
.search-bar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.search-input-group {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.list-actions {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn-group-actions {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* Cards do Dashboard */
|
||||
.stats-card {
|
||||
position: relative;
|
||||
padding: 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
background: #fff;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.stats-card .title {
|
||||
font-size: 0.875rem;
|
||||
color: #6c757d;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.stats-card .value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.stats-card .link {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.stats-card .icon {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
font-size: 1.5rem;
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
.stats-card.blue {
|
||||
background: linear-gradient(135deg, var(--blue) 0%, #0a58ca 100%);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.stats-card.green {
|
||||
background: linear-gradient(135deg, var(--green) 0%, #146c43 100%);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.stats-card.cyan {
|
||||
background: linear-gradient(135deg, var(--cyan) 0%, #0aa2c0 100%);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.stats-card.yellow {
|
||||
background: linear-gradient(135deg, var(--yellow) 0%, #cc9a06 100%);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Welcome Header */
|
||||
.welcome-header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.welcome-header h2 {
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.welcome-header h4 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* Tabs */
|
||||
.nav-tabs {
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link,
|
||||
.nav-tabs .nav-link:focus,
|
||||
.nav-tabs .nav-link:hover,
|
||||
.nav-tabs .nav-link.active {
|
||||
color: var(--primary-color) !important;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
padding: 0.75rem 1.5rem;
|
||||
margin-bottom: -2px;
|
||||
transition: all 0.2s ease-in-out;
|
||||
font-weight: 500;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link:hover {
|
||||
background-color: var(--tab-hover-color);
|
||||
border-bottom: 2px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link.active {
|
||||
font-weight: 600;
|
||||
background-color: var(--tab-hover-color);
|
||||
border-bottom: 2px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link i {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.tab-pane {
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(5px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsividade das abas */
|
||||
@media (max-width: 768px) {
|
||||
.nav-tabs {
|
||||
flex-wrap: nowrap;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link {
|
||||
white-space: nowrap;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Estilo para botões com largura fixa */
|
||||
.btn-fixed-width {
|
||||
min-width: 120px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.375rem 0.75rem;
|
||||
text-align: center;
|
||||
height: 38px;
|
||||
line-height: 1.5;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.btn-fixed-width i {
|
||||
margin-right: 8px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Estilo para o backdrop com blur em todos os modais */
|
||||
.modal-backdrop.show {
|
||||
backdrop-filter: blur(8px);
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
/* Estilo para o botão de fechar dos modais */
|
||||
.btn-close {
|
||||
background-color: transparent;
|
||||
padding: 0.5rem;
|
||||
opacity: 0.8;
|
||||
transition: opacity 0.2s;
|
||||
filter: invert(1) grayscale(100%) brightness(200%);
|
||||
}
|
||||
|
||||
.btn-close:hover {
|
||||
opacity: 1;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* Estilos do Modal */
|
||||
.modal-header {
|
||||
background-color: #343a40;
|
||||
color: #fff;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-header i {
|
||||
color: #fff;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.modal-header .btn-close {
|
||||
filter: invert(1) grayscale(100%) brightness(200%);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.modal-header .btn-close:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Estilos globais de formulário */
|
||||
.form-control:focus,
|
||||
.form-select:focus,
|
||||
.form-check-input:focus,
|
||||
.btn:focus,
|
||||
.btn-check:focus + .btn {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.25);
|
||||
}
|
||||
|
||||
.form-control:hover,
|
||||
.form-select:hover {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* Input group com foco */
|
||||
.input-group .form-control:focus,
|
||||
.input-group .form-select:focus {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* Checkbox e radio */
|
||||
.form-check-input:checked {
|
||||
background-color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* Date picker */
|
||||
input[type="date"]:focus {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.25);
|
||||
}
|
||||
|
||||
/* Estilo para colunas ordenáveis */
|
||||
th[data-sort] {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
th[data-sort] i {
|
||||
margin-left: 5px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
th[data-sort].sort-asc i,
|
||||
th[data-sort].sort-desc i {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* Animação para linhas da tabela */
|
||||
#militantesTable tbody tr {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* Estilos globais para botões */
|
||||
.btn-success,
|
||||
.modal-footer .btn-success,
|
||||
button.btn-success,
|
||||
input.btn-success,
|
||||
.btn-success.active,
|
||||
.btn-success:active,
|
||||
.show > .btn-success.dropdown-toggle {
|
||||
background-color: #198754 !important;
|
||||
border-color: #198754 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.btn-success:hover,
|
||||
.modal-footer .btn-success:hover,
|
||||
button.btn-success:hover,
|
||||
input.btn-success:hover,
|
||||
.btn-success:focus,
|
||||
.btn-success:active,
|
||||
.modal-footer .btn-success:focus,
|
||||
.modal-footer .btn-success:active,
|
||||
.btn-success:not(:disabled):not(.disabled):active,
|
||||
.btn-success:not(:disabled):not(.disabled).active,
|
||||
.show > .btn-success.dropdown-toggle:hover {
|
||||
background-color: #146c43 !important;
|
||||
border-color: #146c43 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.btn-secondary,
|
||||
.modal-footer .btn-secondary,
|
||||
button.btn-secondary,
|
||||
input.btn-secondary,
|
||||
.btn-secondary.active,
|
||||
.btn-secondary:active,
|
||||
.show > .btn-secondary.dropdown-toggle {
|
||||
background-color: #6c757d !important;
|
||||
border-color: #6c757d !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.btn-secondary:hover,
|
||||
.modal-footer .btn-secondary:hover,
|
||||
button.btn-secondary:hover,
|
||||
input.btn-secondary:hover,
|
||||
.btn-secondary:focus,
|
||||
.btn-secondary:active,
|
||||
.modal-footer .btn-secondary:focus,
|
||||
.modal-footer .btn-secondary:active,
|
||||
.btn-secondary:not(:disabled):not(.disabled):active,
|
||||
.btn-secondary:not(:disabled):not(.disabled).active,
|
||||
.show > .btn-secondary.dropdown-toggle:hover {
|
||||
background-color: #5c636a !important;
|
||||
border-color: #5c636a !important;
|
||||
}
|
||||
|
||||
.btn-secondary:not(:disabled):not(.disabled).active {
|
||||
background-color: #4b545c !important;
|
||||
border-color: #4b545c !important;
|
||||
color: white !important;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
|
||||
/* Estilos para botões nos modais */
|
||||
.modal .btn,
|
||||
.modal-footer .btn {
|
||||
font-weight: 500;
|
||||
padding: 0.5rem 1.5rem;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.modal .btn:hover,
|
||||
.modal-footer .btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Garantir que o botão primário mantenha suas cores */
|
||||
.modal .btn-primary,
|
||||
.modal-footer .btn-primary,
|
||||
.modal .btn-primary.active,
|
||||
.modal .btn-primary:active,
|
||||
.modal-footer .btn-primary.active,
|
||||
.modal-footer .btn-primary:active,
|
||||
.modal .btn-primary:not(:disabled):not(.disabled):active,
|
||||
.modal .btn-primary:not(:disabled):not(.disabled).active,
|
||||
.modal-footer .btn-primary:not(:disabled):not(.disabled):active,
|
||||
.modal-footer .btn-primary:not(:disabled):not(.disabled).active,
|
||||
.show > .modal .btn-primary.dropdown-toggle,
|
||||
.show > .modal-footer .btn-primary.dropdown-toggle {
|
||||
background-color: #0d6efd !important;
|
||||
border-color: #0d6efd !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.modal .btn-primary:hover,
|
||||
.modal-footer .btn-primary:hover,
|
||||
.modal .btn-primary:focus,
|
||||
.modal-footer .btn-primary:focus,
|
||||
.modal .btn-primary:active,
|
||||
.modal-footer .btn-primary:active,
|
||||
.modal .btn-primary:not(:disabled):not(.disabled):active:focus,
|
||||
.modal .btn-primary:not(:disabled):not(.disabled).active:focus,
|
||||
.modal-footer .btn-primary:not(:disabled):not(.disabled):active:focus,
|
||||
.modal-footer .btn-primary:not(:disabled):not(.disabled).active:focus {
|
||||
background-color: #0b5ed7 !important;
|
||||
border-color: #0b5ed7 !important;
|
||||
color: white !important;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
|
||||
/* Estilos para alertas */
|
||||
.alert {
|
||||
position: fixed;
|
||||
top: 1rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 9999;
|
||||
min-width: 300px;
|
||||
max-width: 600px;
|
||||
text-align: center;
|
||||
padding: 1rem 2.5rem 1rem 1rem;
|
||||
margin: 0;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.alert .btn-close {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 1rem;
|
||||
transform: translateY(-50%);
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
color: #0f5132;
|
||||
background-color: #d1e7dd;
|
||||
border-color: #badbcc;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
color: #842029;
|
||||
background-color: #f8d7da;
|
||||
border-color: #f5c2c7;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
color: #664d03;
|
||||
background-color: #fff3cd;
|
||||
border-color: #ffecb5;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
color: #055160;
|
||||
background-color: #cff4fc;
|
||||
border-color: #b6effb;
|
||||
}
|
||||
|
||||
/* Status styles */
|
||||
.status-active {
|
||||
color: var(--status-active);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-inactive {
|
||||
color: var(--status-inactive);
|
||||
font-weight: 500;
|
||||
}
|
||||
450
static/css/style.css
Normal file
450
static/css/style.css
Normal file
@@ -0,0 +1,450 @@
|
||||
:root {
|
||||
--primary-color: #E8000C;
|
||||
--primary-dark: #B5000A;
|
||||
--primary-light: #FF1A1A;
|
||||
--secondary-color: #2D2D2D;
|
||||
--secondary-light: #404040;
|
||||
--secondary-dark: #1A1A1A;
|
||||
--background-color: #FFFFFF;
|
||||
--text-color: #2D2D2D;
|
||||
--text-light: #FFFFFF;
|
||||
--hover-color: #FF1A1A;
|
||||
--disabled-color: #999999;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--background-color);
|
||||
color: var(--text-color);
|
||||
font-family: 'Roboto', sans-serif;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
background: linear-gradient(to right, var(--secondary-dark), var(--secondary-color)) !important;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||
border-bottom: 3px solid var(--primary-color);
|
||||
padding: 0.8rem 1rem;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 1.3rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-light) !important;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.navbar-brand img {
|
||||
height: 40px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.navbar-logo {
|
||||
height: 32px;
|
||||
width: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.login-logo {
|
||||
height: 80px;
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
font-weight: 400;
|
||||
font-size: 0.95rem;
|
||||
letter-spacing: 0.3px;
|
||||
transition: all 0.3s ease;
|
||||
color: rgba(255, 255, 255, 0.85) !important;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
color: var(--text-light) !important;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.nav-link i {
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
|
||||
transition: all 0.3s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.card .card-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
/* Cards de estatísticas */
|
||||
.card.bg-primary {
|
||||
background: linear-gradient(135deg, #0d6efd, #0a58ca) !important;
|
||||
}
|
||||
|
||||
.card.bg-success {
|
||||
background: linear-gradient(135deg, #198754, #146c43) !important;
|
||||
}
|
||||
|
||||
.card.bg-info {
|
||||
background: linear-gradient(135deg, #0dcaf0, #0aa2c0) !important;
|
||||
}
|
||||
|
||||
.card.bg-warning {
|
||||
background: linear-gradient(135deg, #ffc107, #cc9a06) !important;
|
||||
}
|
||||
|
||||
.card .fs-1 {
|
||||
opacity: 0.8;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.card:hover .fs-1 {
|
||||
opacity: 1;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 600;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.card h6 {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 400;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.card a {
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.9;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.card a:hover {
|
||||
opacity: 1;
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
/* Cards de listagem */
|
||||
.card .card-header {
|
||||
background: linear-gradient(to right, var(--secondary-dark), var(--secondary-color));
|
||||
color: var(--text-light);
|
||||
padding: 1rem 1.5rem;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.card .card-header h5 {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.list-group-item {
|
||||
padding: 1rem 1.5rem;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.list-group-item:hover {
|
||||
background-color: rgba(0,0,0,0.02);
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
.list-group-item h6 {
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.list-group-item small {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.badge {
|
||||
padding: 0.5em 0.8em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary-color);
|
||||
border: none;
|
||||
padding: 0.5rem 1.5rem;
|
||||
border-radius: 5px;
|
||||
font-weight: 500;
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--hover-color);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 2px 5px rgba(232, 0, 12, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
background-color: var(--disabled-color);
|
||||
}
|
||||
|
||||
.table {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.table thead th {
|
||||
background-color: var(--secondary-color);
|
||||
color: var(--text-light);
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.table tbody tr:hover {
|
||||
background-color: rgba(232, 0, 12, 0.05);
|
||||
}
|
||||
|
||||
.form-control {
|
||||
border-radius: 5px;
|
||||
border: 1px solid #e0e0e0;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 0.2rem rgba(232, 0, 12, 0.25);
|
||||
}
|
||||
|
||||
/* Alert styles */
|
||||
.alert {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 1rem 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
opacity: 1 !important;
|
||||
background-color: rgba(255, 255, 255, 0.98) !important;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.alert i {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
color: #155724 !important;
|
||||
background-color: #d4edda !important;
|
||||
border-left: 4px solid #28a745;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
color: #721c24 !important;
|
||||
background-color: #f8d7da !important;
|
||||
border-left: 4px solid #dc3545;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
color: #856404 !important;
|
||||
background-color: #fff3cd !important;
|
||||
border-left: 4px solid #ffc107;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
color: #0c5460 !important;
|
||||
background-color: #d1ecf1 !important;
|
||||
border-left: 4px solid #17a2b8;
|
||||
}
|
||||
|
||||
/* Animações para feedback */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translate(-50%, -20px); }
|
||||
to { opacity: 1; transform: translate(-50%, 0); }
|
||||
}
|
||||
|
||||
.alert {
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
/* Responsividade */
|
||||
@media (max-width: 768px) {
|
||||
.navbar-brand {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.navbar-logo {
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.alert {
|
||||
margin: 1rem;
|
||||
width: calc(100% - 2rem);
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
background: linear-gradient(to bottom right, var(--secondary-dark), var(--secondary-color));
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
||||
padding: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
color: rgba(255, 255, 255, 0.85) !important;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 400;
|
||||
padding: 0.6rem 1rem;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.dropdown-item:hover {
|
||||
background-color: var(--primary-color);
|
||||
color: var(--text-light) !important;
|
||||
transform: translateX(3px);
|
||||
}
|
||||
|
||||
.dropdown-divider {
|
||||
border-top: 1px solid var(--secondary-light);
|
||||
}
|
||||
|
||||
/* Estilo para o menu mobile */
|
||||
@media (max-width: 768px) {
|
||||
.navbar-collapse {
|
||||
background-color: var(--secondary-color);
|
||||
padding: 1rem;
|
||||
border-radius: 0 0 10px 10px;
|
||||
}
|
||||
|
||||
.navbar-brand img {
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Data styles */
|
||||
.date-header {
|
||||
padding: 1.5rem 0;
|
||||
margin-bottom: 1.5rem;
|
||||
border-bottom: 1px solid rgba(0,0,0,0.1);
|
||||
color: var(--secondary-color);
|
||||
font-weight: 400;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
/* Navbar styles */
|
||||
.navbar-nav .nav-link {
|
||||
color: rgba(255, 255, 255, 0.85) !important;
|
||||
transition: all 0.3s ease;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.navbar-nav .nav-link:hover {
|
||||
color: var(--primary-color) !important;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.navbar-nav .dropdown-menu {
|
||||
background: linear-gradient(to bottom right, var(--secondary-dark), var(--secondary-color));
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
||||
padding: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
color: rgba(255, 255, 255, 0.85) !important;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 400;
|
||||
padding: 0.6rem 1rem;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.dropdown-item:hover {
|
||||
background-color: var(--primary-color);
|
||||
color: var(--text-light) !important;
|
||||
transform: translateX(3px);
|
||||
}
|
||||
|
||||
/* Data styles */
|
||||
.date-header {
|
||||
padding: 1.5rem 0;
|
||||
margin-bottom: 1.5rem;
|
||||
border-bottom: 1px solid rgba(0,0,0,0.1);
|
||||
color: var(--secondary-color);
|
||||
font-weight: 400;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.date-header {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.navbar-collapse {
|
||||
background-color: var(--secondary-color);
|
||||
padding: 1rem;
|
||||
border-radius: 0 0 10px 10px;
|
||||
}
|
||||
|
||||
.navbar-brand img {
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.welcome-header {
|
||||
background: linear-gradient(to right, var(--background-color), rgba(232, 0, 12, 0.05));
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.welcome-header h2 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.welcome-header h4 {
|
||||
font-size: 1.2rem;
|
||||
color: var(--secondary-color);
|
||||
opacity: 0.8;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background: linear-gradient(to right, var(--secondary-dark), var(--secondary-color));
|
||||
color: var(--text-light);
|
||||
padding: 1rem 1.5rem;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.list-group-item-action {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.list-group-item-action:hover {
|
||||
transform: translateX(5px);
|
||||
background-color: rgba(232, 0, 12, 0.05);
|
||||
}
|
||||
53
static/css/styles.css
Normal file
53
static/css/styles.css
Normal file
@@ -0,0 +1,53 @@
|
||||
/* Estilos globais para alertas do sistema */
|
||||
.alert {
|
||||
position: relative;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* Estilo base para o botão de fechar */
|
||||
.alert .btn-close {
|
||||
filter: none;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Alert Success */
|
||||
.alert-success .btn-close {
|
||||
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23198754'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;
|
||||
}
|
||||
|
||||
/* Alert Danger */
|
||||
.alert-danger .btn-close {
|
||||
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23842029'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;
|
||||
}
|
||||
|
||||
/* Alert Warning */
|
||||
.alert-warning .btn-close {
|
||||
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23997404'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;
|
||||
}
|
||||
|
||||
/* Alert Info */
|
||||
.alert-info .btn-close {
|
||||
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23055160'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;
|
||||
}
|
||||
|
||||
/* Efeito hover para todos os botões de fechar */
|
||||
.alert .btn-close:hover {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
/* Estilo das abas do modal */
|
||||
.nav-tabs .nav-link {
|
||||
/* remover estilos */
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link.active {
|
||||
/* remover estilos */
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link:hover:not(.active) {
|
||||
/* remover estilos */
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link i {
|
||||
/* remover estilos */
|
||||
}
|
||||
1
static/img/favicon.ico
Normal file
1
static/img/favicon.ico
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
BIN
static/img/logo001-alpha.png
Normal file
BIN
static/img/logo001-alpha.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
BIN
static/img/logo001.png
Normal file
BIN
static/img/logo001.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
BIN
static/img/logo002-alpha.png
Normal file
BIN
static/img/logo002-alpha.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
BIN
static/img/logoComunaTec.jpg
Normal file
BIN
static/img/logoComunaTec.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
51
static/js/comprovantes.js
Normal file
51
static/js/comprovantes.js
Normal 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');
|
||||
});
|
||||
});
|
||||
127
static/js/cotas.js
Normal file
127
static/js/cotas.js
Normal file
@@ -0,0 +1,127 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('Carregando script cotas.js...');
|
||||
|
||||
// Configuração do modal de edição
|
||||
const modalEditarCota = document.getElementById('modalEditarCota');
|
||||
if (modalEditarCota) {
|
||||
modalEditarCota.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 cotaId = button.getAttribute('data-cota-id');
|
||||
console.log('ID da cota:', cotaId);
|
||||
|
||||
// Dados da cota
|
||||
const dados = {
|
||||
militanteId: button.getAttribute('data-cota-militante'),
|
||||
militanteNome: button.closest('tr').querySelector('td').textContent.trim(),
|
||||
valorAntigo: button.closest('tr').querySelector('td[data-valor_antigo]').getAttribute('data-valor_antigo'),
|
||||
valorNovo: button.closest('tr').querySelector('td[data-valor_novo]').getAttribute('data-valor_novo'),
|
||||
dataAlteracao: button.getAttribute('data-cota-data-alteracao'),
|
||||
dataVencimento: button.getAttribute('data-cota-data-vencimento'),
|
||||
pago: button.getAttribute('data-cota-pago') === 'true'
|
||||
};
|
||||
console.log('Dados da cota:', dados);
|
||||
|
||||
// Preencher campos
|
||||
document.getElementById('editMilitante').value = dados.militanteId;
|
||||
document.getElementById('editMilitanteNome').value = dados.militanteNome;
|
||||
document.getElementById('editValorAntigo').value = dados.valorAntigo;
|
||||
document.getElementById('editValorNovo').value = dados.valorNovo;
|
||||
document.getElementById('editDataAlteracao').value = dados.dataAlteracao;
|
||||
document.getElementById('editDataVencimento').value = dados.dataVencimento;
|
||||
document.getElementById('editPago').checked = dados.pago;
|
||||
|
||||
// Configurar formulário
|
||||
const form = document.getElementById('formEditarCota');
|
||||
if (form) {
|
||||
form.action = `/cotas/editar/${cotaId}`;
|
||||
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);
|
||||
|
||||
// Adicionar campo pago com o valor correto
|
||||
const isPago = document.getElementById('editPago').checked;
|
||||
formData.set('pago', isPago ? 'true' : 'false');
|
||||
|
||||
// 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(modalEditarCota);
|
||||
modal.hide();
|
||||
|
||||
// Recarregar página
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert('Erro ao atualizar cota: ' + data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Erro:', error);
|
||||
alert('Erro ao atualizar cota. Por favor, tente novamente.');
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Configuração do modal de exclusão
|
||||
const deleteModal = document.getElementById('deleteModal');
|
||||
if (deleteModal) {
|
||||
deleteModal.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 cotaId = button.getAttribute('data-cota-id');
|
||||
const cotaInfo = button.getAttribute('data-cota-info');
|
||||
console.log('ID da cota:', cotaId);
|
||||
console.log('Info da cota:', cotaInfo);
|
||||
|
||||
// Atualizar texto do modal
|
||||
document.getElementById('cotaInfo').textContent = cotaInfo;
|
||||
|
||||
// Configurar formulário de exclusão
|
||||
const form = document.getElementById('deleteForm');
|
||||
if (form) {
|
||||
form.action = `/cotas/excluir/${cotaId}`;
|
||||
console.log('Action do formulário:', form.action);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
203
static/js/forms.js
Normal file
203
static/js/forms.js
Normal file
@@ -0,0 +1,203 @@
|
||||
// Validação de CPF
|
||||
function validarCPF(cpf) {
|
||||
cpf = cpf.replace(/[^\d]/g, '');
|
||||
|
||||
if (cpf.length !== 11) return false;
|
||||
|
||||
// Verifica se todos os dígitos são iguais
|
||||
if (/^(\d)\1{10}$/.test(cpf)) return false;
|
||||
|
||||
// Validação do primeiro dígito verificador
|
||||
let soma = 0;
|
||||
for (let i = 0; i < 9; i++) {
|
||||
soma += parseInt(cpf.charAt(i)) * (10 - i);
|
||||
}
|
||||
let resto = 11 - (soma % 11);
|
||||
let dv1 = resto > 9 ? 0 : resto;
|
||||
|
||||
if (dv1 !== parseInt(cpf.charAt(9))) return false;
|
||||
|
||||
// Validação do segundo dígito verificador
|
||||
soma = 0;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
soma += parseInt(cpf.charAt(i)) * (11 - i);
|
||||
}
|
||||
resto = 11 - (soma % 11);
|
||||
let dv2 = resto > 9 ? 0 : resto;
|
||||
|
||||
if (dv2 !== parseInt(cpf.charAt(10))) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Validação de email
|
||||
function validarEmail(email) {
|
||||
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return re.test(email);
|
||||
}
|
||||
|
||||
// Validação de telefone
|
||||
function validarTelefone(telefone) {
|
||||
telefone = telefone.replace(/[^\d]/g, '');
|
||||
return telefone.length >= 10 && telefone.length <= 11;
|
||||
}
|
||||
|
||||
// Inicialização dos formulários
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Validação personalizada para CPF
|
||||
const cpfInputs = document.querySelectorAll('input[name="cpf"]');
|
||||
cpfInputs.forEach(input => {
|
||||
input.addEventListener('blur', function() {
|
||||
const cpf = this.value;
|
||||
if (!validarCPF(cpf)) {
|
||||
this.setCustomValidity('CPF inválido');
|
||||
this.classList.add('is-invalid');
|
||||
} else {
|
||||
this.setCustomValidity('');
|
||||
this.classList.remove('is-invalid');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Validação personalizada para email
|
||||
const emailInputs = document.querySelectorAll('input[type="email"]');
|
||||
emailInputs.forEach(input => {
|
||||
input.addEventListener('blur', function() {
|
||||
const email = this.value;
|
||||
if (!validarEmail(email)) {
|
||||
this.setCustomValidity('Email inválido');
|
||||
this.classList.add('is-invalid');
|
||||
} else {
|
||||
this.setCustomValidity('');
|
||||
this.classList.remove('is-invalid');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Validação personalizada para telefone
|
||||
const phoneInputs = document.querySelectorAll('input[name="telefone"]');
|
||||
phoneInputs.forEach(input => {
|
||||
input.addEventListener('blur', function() {
|
||||
const telefone = this.value;
|
||||
if (!validarTelefone(telefone)) {
|
||||
this.setCustomValidity('Telefone inválido');
|
||||
this.classList.add('is-invalid');
|
||||
} else {
|
||||
this.setCustomValidity('');
|
||||
this.classList.remove('is-invalid');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Validação de campos monetários
|
||||
const moneyInputs = document.querySelectorAll('input[type="number"][step="0.01"]');
|
||||
moneyInputs.forEach(input => {
|
||||
input.addEventListener('blur', function() {
|
||||
const value = parseFloat(this.value);
|
||||
if (isNaN(value) || value < 0) {
|
||||
this.setCustomValidity('Valor inválido');
|
||||
this.classList.add('is-invalid');
|
||||
} else {
|
||||
this.setCustomValidity('');
|
||||
this.classList.remove('is-invalid');
|
||||
this.value = value.toFixed(2);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Validação de datas
|
||||
const dateInputs = document.querySelectorAll('input[type="date"], input.date-mask');
|
||||
dateInputs.forEach(input => {
|
||||
input.addEventListener('change', function() {
|
||||
console.log('Validando data:', this.value);
|
||||
|
||||
let dataValida = true;
|
||||
let mensagemErro = '';
|
||||
|
||||
// Se for um campo com máscara, validar o formato
|
||||
if (this.classList.contains('date-mask')) {
|
||||
if (!validarData(this.value)) {
|
||||
dataValida = false;
|
||||
mensagemErro = 'Por favor, insira uma data válida no formato DD/MM/AAAA';
|
||||
}
|
||||
} else {
|
||||
// Para campos type="date", converter para Date
|
||||
const date = new Date(this.value);
|
||||
if (isNaN(date.getTime())) {
|
||||
dataValida = false;
|
||||
mensagemErro = 'Data inválida';
|
||||
}
|
||||
}
|
||||
|
||||
// Validar limites de data
|
||||
if (dataValida) {
|
||||
const hoje = new Date();
|
||||
hoje.setHours(0, 0, 0, 0);
|
||||
|
||||
let dataComparacao;
|
||||
if (this.classList.contains('date-mask')) {
|
||||
const [dia, mes, ano] = this.value.split('/').map(Number);
|
||||
dataComparacao = new Date(ano, mes - 1, dia);
|
||||
} else {
|
||||
dataComparacao = new Date(this.value);
|
||||
}
|
||||
|
||||
// Verificar data mínima
|
||||
if (this.hasAttribute('min')) {
|
||||
const minDate = new Date(this.getAttribute('min'));
|
||||
if (dataComparacao < minDate) {
|
||||
dataValida = false;
|
||||
mensagemErro = `A data não pode ser anterior a ${minDate.toLocaleDateString()}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Verificar data máxima
|
||||
if (this.hasAttribute('max')) {
|
||||
const maxDate = new Date(this.getAttribute('max'));
|
||||
if (dataComparacao > maxDate) {
|
||||
dataValida = false;
|
||||
mensagemErro = `A data não pode ser posterior a ${maxDate.toLocaleDateString()}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Verificar se é data futura (quando não permitido)
|
||||
if (this.hasAttribute('data-no-future') && dataComparacao > hoje) {
|
||||
dataValida = false;
|
||||
mensagemErro = 'A data não pode ser futura';
|
||||
}
|
||||
}
|
||||
|
||||
// Atualizar validação do campo
|
||||
if (!dataValida) {
|
||||
console.warn('Data inválida:', this.value, mensagemErro);
|
||||
this.setCustomValidity(mensagemErro);
|
||||
this.classList.add('is-invalid');
|
||||
|
||||
// Atualizar mensagem de feedback
|
||||
const feedback = this.nextElementSibling;
|
||||
if (feedback && feedback.classList.contains('invalid-feedback')) {
|
||||
feedback.textContent = mensagemErro;
|
||||
}
|
||||
} else {
|
||||
console.log('Data válida:', this.value);
|
||||
this.setCustomValidity('');
|
||||
this.classList.remove('is-invalid');
|
||||
}
|
||||
});
|
||||
|
||||
// Limpar validação ao começar a digitar
|
||||
input.addEventListener('input', function() {
|
||||
this.setCustomValidity('');
|
||||
this.classList.remove('is-invalid');
|
||||
});
|
||||
});
|
||||
|
||||
// Feedback visual para campos obrigatórios
|
||||
const requiredInputs = document.querySelectorAll('input[required], select[required], textarea[required]');
|
||||
requiredInputs.forEach(input => {
|
||||
const label = input.previousElementSibling;
|
||||
if (label && label.tagName === 'LABEL') {
|
||||
label.innerHTML += ' <span class="text-danger">*</span>';
|
||||
}
|
||||
});
|
||||
});
|
||||
11
static/js/home.js
Normal file
11
static/js/home.js
Normal file
@@ -0,0 +1,11 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Configurar clique nos itens da lista de comprovantes
|
||||
document.querySelectorAll('.list-group-item[onclick*="carregarDadosComprovante"]').forEach(item => {
|
||||
item.addEventListener('click', function(e) {
|
||||
const comprovanteId = this.getAttribute('data-comprovante-id');
|
||||
if (comprovanteId) {
|
||||
carregarDadosComprovante(comprovanteId);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
145
static/js/main.js
Normal file
145
static/js/main.js
Normal file
@@ -0,0 +1,145 @@
|
||||
// Máscaras para campos de formulário
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Máscara para CPF
|
||||
const cpfInputs = document.querySelectorAll('input[name="cpf"]');
|
||||
cpfInputs.forEach(input => {
|
||||
input.addEventListener('input', function(e) {
|
||||
let value = e.target.value.replace(/\D/g, '');
|
||||
if (value.length <= 11) {
|
||||
value = value.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, "$1.$2.$3-$4");
|
||||
e.target.value = value;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Máscara para telefone
|
||||
const phoneInputs = document.querySelectorAll('input[name="telefone"]');
|
||||
phoneInputs.forEach(input => {
|
||||
input.addEventListener('input', function(e) {
|
||||
let value = e.target.value.replace(/\D/g, '');
|
||||
if (value.length <= 11) {
|
||||
if (value.length === 11) {
|
||||
value = value.replace(/(\d{2})(\d{5})(\d{4})/, "($1) $2-$3");
|
||||
} else {
|
||||
value = value.replace(/(\d{2})(\d{4})(\d{4})/, "($1) $2-$3");
|
||||
}
|
||||
e.target.value = value;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Formatação de valores monetários
|
||||
const moneyInputs = document.querySelectorAll('input[type="number"][step="0.01"]');
|
||||
moneyInputs.forEach(input => {
|
||||
input.addEventListener('blur', function(e) {
|
||||
const value = parseFloat(e.target.value);
|
||||
if (!isNaN(value)) {
|
||||
e.target.value = value.toFixed(2);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Funções para tabelas
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const tables = document.querySelectorAll('.table');
|
||||
tables.forEach(table => {
|
||||
// Ordenação
|
||||
const headers = table.querySelectorAll('th[data-sort]');
|
||||
headers.forEach(header => {
|
||||
header.addEventListener('click', function() {
|
||||
const column = this.dataset.sort;
|
||||
const asc = this.classList.toggle('sort-asc');
|
||||
const tbody = table.querySelector('tbody');
|
||||
const rows = Array.from(tbody.querySelectorAll('tr'));
|
||||
|
||||
rows.sort((a, b) => {
|
||||
const aVal = a.querySelector(`td[data-${column}]`).dataset[column];
|
||||
const bVal = b.querySelector(`td[data-${column}]`).dataset[column];
|
||||
return asc ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal);
|
||||
});
|
||||
|
||||
rows.forEach(row => tbody.appendChild(row));
|
||||
});
|
||||
});
|
||||
|
||||
// Filtro
|
||||
const filterInput = document.querySelector(`#filter-${table.id}`);
|
||||
if (filterInput) {
|
||||
filterInput.addEventListener('input', function() {
|
||||
const searchTerm = this.value.toLowerCase();
|
||||
const rows = table.querySelectorAll('tbody tr');
|
||||
|
||||
rows.forEach(row => {
|
||||
const text = row.textContent.toLowerCase();
|
||||
row.style.display = text.includes(searchTerm) ? '' : 'none';
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Validação de formulários
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const forms = document.querySelectorAll('form');
|
||||
forms.forEach(form => {
|
||||
form.addEventListener('submit', function(e) {
|
||||
if (!form.checkValidity()) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Destacar campos inválidos
|
||||
const invalidInputs = form.querySelectorAll(':invalid');
|
||||
invalidInputs.forEach(input => {
|
||||
input.classList.add('is-invalid');
|
||||
|
||||
// Adicionar mensagem de erro
|
||||
const feedback = document.createElement('div');
|
||||
feedback.className = 'invalid-feedback';
|
||||
feedback.textContent = input.validationMessage;
|
||||
input.parentNode.appendChild(feedback);
|
||||
});
|
||||
}
|
||||
|
||||
form.classList.add('was-validated');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Animações e feedback visual
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Animar cards ao carregar
|
||||
const cards = document.querySelectorAll('.card');
|
||||
cards.forEach((card, index) => {
|
||||
card.style.opacity = '0';
|
||||
card.style.transform = 'translateY(20px)';
|
||||
setTimeout(() => {
|
||||
card.style.transition = 'all 0.3s ease';
|
||||
card.style.opacity = '1';
|
||||
card.style.transform = 'translateY(0)';
|
||||
}, index * 100);
|
||||
});
|
||||
|
||||
// Feedback visual para ações
|
||||
const actionButtons = document.querySelectorAll('[data-action]');
|
||||
actionButtons.forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
button.classList.add('animate__animated', 'animate__pulse');
|
||||
setTimeout(() => {
|
||||
button.classList.remove('animate__animated', 'animate__pulse');
|
||||
}, 1000);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Confirmações de ações
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const deleteButtons = document.querySelectorAll('[data-confirm]');
|
||||
deleteButtons.forEach(button => {
|
||||
button.addEventListener('click', function(e) {
|
||||
if (!confirm(this.dataset.confirm)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
1461
static/js/militantes.js
Normal file
1461
static/js/militantes.js
Normal file
File diff suppressed because it is too large
Load Diff
208
static/js/table_sort.js
Normal file
208
static/js/table_sort.js
Normal file
@@ -0,0 +1,208 @@
|
||||
// Função para converter data DD/MM/YYYY para objeto Date
|
||||
function converterDataParaComparacao(dataStr) {
|
||||
console.log('Convertendo data para comparação:', dataStr);
|
||||
|
||||
if (!dataStr) return null;
|
||||
|
||||
try {
|
||||
// Se já estiver no formato ISO
|
||||
if (/^\d{4}-\d{2}-\d{2}/.test(dataStr)) {
|
||||
const data = new Date(dataStr);
|
||||
console.log('Data ISO convertida:', data);
|
||||
return data;
|
||||
}
|
||||
|
||||
// Se estiver no formato DD/MM/YYYY
|
||||
if (/^\d{2}\/\d{2}\/\d{4}/.test(dataStr)) {
|
||||
const [dia, mes, ano] = dataStr.split('/').map(Number);
|
||||
const data = new Date(ano, mes - 1, dia);
|
||||
console.log('Data DD/MM/YYYY convertida:', data);
|
||||
return data;
|
||||
}
|
||||
|
||||
console.warn('Formato de data não reconhecido:', dataStr);
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Erro ao converter data:', error, 'Data:', dataStr);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Função para ordenar tabelas
|
||||
function configurarOrdenacaoTabela(tabelaId) {
|
||||
console.log('Configurando ordenação para tabela:', tabelaId);
|
||||
|
||||
const table = document.getElementById(tabelaId);
|
||||
if (!table) {
|
||||
console.warn('Tabela não encontrada:', tabelaId);
|
||||
return;
|
||||
}
|
||||
|
||||
const headers = table.querySelectorAll('th[data-sort]');
|
||||
headers.forEach(header => {
|
||||
if (header.dataset.sort) {
|
||||
header.addEventListener('click', () => {
|
||||
const column = header.dataset.sort;
|
||||
const tbody = table.getElementsByTagName('tbody')[0];
|
||||
const rows = Array.from(tbody.getElementsByTagName('tr'));
|
||||
|
||||
console.log('Ordenando coluna:', column);
|
||||
|
||||
rows.sort((a, b) => {
|
||||
const aValue = a.querySelector(`td[data-${column}]`).dataset[column];
|
||||
const bValue = b.querySelector(`td[data-${column}]`).dataset[column];
|
||||
|
||||
// Ordenação por data
|
||||
if (column === 'data' ||
|
||||
column === 'data_vencimento' ||
|
||||
column === 'data_alteracao' ||
|
||||
column === 'data_comprovante' ||
|
||||
column === 'data_venda' ||
|
||||
column === 'data_relatorio') {
|
||||
const aDate = converterDataParaComparacao(aValue);
|
||||
const bDate = converterDataParaComparacao(bValue);
|
||||
|
||||
// Se alguma data for inválida
|
||||
if (!aDate && !bDate) return 0;
|
||||
if (!aDate) return 1;
|
||||
if (!bDate) return -1;
|
||||
|
||||
return aDate - bDate;
|
||||
}
|
||||
|
||||
// Ordenação por valor monetário
|
||||
if (column === 'valor' ||
|
||||
column === 'valor_total' ||
|
||||
column === 'valor_antigo' ||
|
||||
column === 'valor_novo') {
|
||||
const aNum = parseFloat(aValue.replace(/[^\d,-]/g, '').replace(',', '.'));
|
||||
const bNum = parseFloat(bValue.replace(/[^\d,-]/g, '').replace(',', '.'));
|
||||
return aNum - bNum;
|
||||
}
|
||||
|
||||
// Ordenação padrão para texto
|
||||
return aValue.localeCompare(bValue);
|
||||
});
|
||||
|
||||
// Alternar direção da ordenação
|
||||
if (header.classList.contains('asc')) {
|
||||
rows.reverse();
|
||||
header.classList.remove('asc');
|
||||
header.classList.add('desc');
|
||||
console.log('Ordenação descendente');
|
||||
} else {
|
||||
header.classList.remove('desc');
|
||||
header.classList.add('asc');
|
||||
console.log('Ordenação ascendente');
|
||||
}
|
||||
|
||||
// Atualizar tabela
|
||||
tbody.innerHTML = '';
|
||||
rows.forEach(row => tbody.appendChild(row));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Configurar ordenação para todas as tabelas que precisam
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('Configurando ordenação para todas as tabelas...');
|
||||
|
||||
const tabelas = [
|
||||
'materiaisTable',
|
||||
'vendasTable',
|
||||
'cotasTable',
|
||||
'comprovantesTable'
|
||||
];
|
||||
|
||||
tabelas.forEach(tabelaId => {
|
||||
configurarOrdenacaoTabela(tabelaId);
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('Carregando script table_sort.js...');
|
||||
|
||||
// Função para comparar datas no formato DD/MM/YYYY
|
||||
function compararDatas(a, b) {
|
||||
if (!a || !b) return 0;
|
||||
|
||||
const [diaA, mesA, anoA] = a.split('/').map(Number);
|
||||
const [diaB, mesB, anoB] = b.split('/').map(Number);
|
||||
|
||||
const dataA = new Date(anoA, mesA - 1, diaA);
|
||||
const dataB = new Date(anoB, mesB - 1, diaB);
|
||||
|
||||
return dataA - dataB;
|
||||
}
|
||||
|
||||
// Função para comparar valores monetários
|
||||
function compararValores(a, b) {
|
||||
const valorA = parseFloat(a.replace('R$ ', '').replace('.', '').replace(',', '.'));
|
||||
const valorB = parseFloat(b.replace('R$ ', '').replace('.', '').replace(',', '.'));
|
||||
|
||||
if (isNaN(valorA)) return -1;
|
||||
if (isNaN(valorB)) return 1;
|
||||
|
||||
return valorA - valorB;
|
||||
}
|
||||
|
||||
// Configurar ordenação para todas as tabelas com classe 'table-sort'
|
||||
document.querySelectorAll('table.table-sort').forEach(tabela => {
|
||||
const tbody = tabela.querySelector('tbody');
|
||||
const headers = tabela.querySelectorAll('th[data-sort]');
|
||||
|
||||
headers.forEach(header => {
|
||||
const tipoOrdenacao = header.dataset.sort;
|
||||
|
||||
header.addEventListener('click', () => {
|
||||
const rows = Array.from(tbody.querySelectorAll('tr'));
|
||||
const colIndex = Array.from(header.parentNode.children).indexOf(header);
|
||||
|
||||
rows.sort((rowA, rowB) => {
|
||||
const cellA = rowA.children[colIndex].dataset[tipoOrdenacao] || rowA.children[colIndex].textContent.trim();
|
||||
const cellB = rowB.children[colIndex].dataset[tipoOrdenacao] || rowB.children[colIndex].textContent.trim();
|
||||
|
||||
switch (tipoOrdenacao) {
|
||||
case 'data':
|
||||
return compararDatas(cellA, cellB);
|
||||
case 'valor':
|
||||
return compararValores(cellA, cellB);
|
||||
case 'numero':
|
||||
return parseFloat(cellA) - parseFloat(cellB);
|
||||
default:
|
||||
return cellA.localeCompare(cellB);
|
||||
}
|
||||
});
|
||||
|
||||
if (header.classList.contains('asc')) {
|
||||
rows.reverse();
|
||||
header.classList.remove('asc');
|
||||
header.classList.add('desc');
|
||||
} else {
|
||||
header.classList.remove('desc');
|
||||
header.classList.add('asc');
|
||||
}
|
||||
|
||||
// Remover classes de ordenação de outros headers
|
||||
headers.forEach(h => {
|
||||
if (h !== header) {
|
||||
h.classList.remove('asc', 'desc');
|
||||
}
|
||||
});
|
||||
|
||||
// Atualizar tabela
|
||||
tbody.innerHTML = '';
|
||||
rows.forEach(row => tbody.appendChild(row));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function sortTable(table, column, type = 'text') {
|
||||
// ... existing code ...
|
||||
if (column === 'data_comprovante') {
|
||||
// ... existing code ...
|
||||
}
|
||||
// ... existing code ...
|
||||
}
|
||||
284
static/js/testes.js
Normal file
284
static/js/testes.js
Normal file
@@ -0,0 +1,284 @@
|
||||
// Testes para o formulário de edição de militantes
|
||||
console.log('Iniciando testes do formulário de edição...');
|
||||
|
||||
// Lista de campos que devem existir no formulário
|
||||
const camposEsperados = {
|
||||
'edit_militante_id': { tipo: 'hidden', obrigatorio: true },
|
||||
'edit_nome': { tipo: 'text', obrigatorio: true },
|
||||
'edit_cpf': { tipo: 'text', obrigatorio: true },
|
||||
'edit_titulo_eleitoral': { tipo: 'text', obrigatorio: false },
|
||||
'edit_data_nascimento': { tipo: 'text', obrigatorio: false },
|
||||
'edit_data_entrada_oci': { tipo: 'text', obrigatorio: false },
|
||||
'edit_data_efetivacao_oci': { tipo: 'text', obrigatorio: false },
|
||||
'edit_email': { tipo: 'email', obrigatorio: true },
|
||||
'edit_telefone1': { tipo: 'text', obrigatorio: false },
|
||||
'edit_telefone2': { tipo: 'text', obrigatorio: false },
|
||||
'edit_cep': { tipo: 'text', obrigatorio: false },
|
||||
'edit_estado': { tipo: 'select', obrigatorio: false },
|
||||
'edit_cidade': { tipo: 'text', obrigatorio: false },
|
||||
'edit_bairro': { tipo: 'text', obrigatorio: false },
|
||||
'edit_rua': { tipo: 'text', obrigatorio: false },
|
||||
'edit_numero': { tipo: 'text', obrigatorio: false },
|
||||
'edit_complemento': { tipo: 'text', obrigatorio: false },
|
||||
'edit_empresa': { tipo: 'text', obrigatorio: false },
|
||||
'edit_contratante': { tipo: 'text', obrigatorio: false },
|
||||
'edit_instituicao_ensino': { tipo: 'text', obrigatorio: false },
|
||||
'edit_tipo_instituicao': { tipo: 'select', obrigatorio: false },
|
||||
'edit_sindicato': { tipo: 'text', obrigatorio: false },
|
||||
'edit_cargo_sindical': { tipo: 'text', obrigatorio: false },
|
||||
'edit_central_sindical': { tipo: 'text', obrigatorio: false },
|
||||
'edit_celula': { tipo: 'select', obrigatorio: false },
|
||||
'responsabilidades_values': { tipo: 'hidden', obrigatorio: false }
|
||||
};
|
||||
|
||||
// Função para testar a existência e configuração dos campos
|
||||
function testarCamposFormulario() {
|
||||
console.log('Testando campos do formulário...');
|
||||
const form = document.getElementById('formEditarMilitante');
|
||||
const erros = [];
|
||||
|
||||
if (!form) {
|
||||
console.error('Formulário não encontrado!');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Testar cada campo esperado
|
||||
for (const [id, config] of Object.entries(camposEsperados)) {
|
||||
const campo = document.getElementById(id);
|
||||
if (!campo) {
|
||||
erros.push(`Campo ${id} não encontrado`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verificar tipo
|
||||
if (campo.type !== config.tipo && config.tipo !== 'select') {
|
||||
erros.push(`Campo ${id} tem tipo ${campo.type}, esperado ${config.tipo}`);
|
||||
}
|
||||
|
||||
// Verificar obrigatoriedade
|
||||
if (config.obrigatorio && !campo.hasAttribute('required')) {
|
||||
erros.push(`Campo ${id} deveria ser obrigatório`);
|
||||
}
|
||||
|
||||
// Verificar se o campo tem name attribute
|
||||
if (!campo.hasAttribute('name')) {
|
||||
erros.push(`Campo ${id} não tem atributo name`);
|
||||
}
|
||||
}
|
||||
|
||||
// Reportar erros encontrados
|
||||
if (erros.length > 0) {
|
||||
console.error('Erros encontrados nos campos:', erros);
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log('Todos os campos estão configurados corretamente');
|
||||
return true;
|
||||
}
|
||||
|
||||
// Função para testar o carregamento de dados
|
||||
async function testarCarregamentoDados(militanteId) {
|
||||
console.log('Testando carregamento de dados...');
|
||||
try {
|
||||
const response = await fetch(`/militantes/dados/${militanteId}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Erro HTTP: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('Dados recebidos:', data);
|
||||
|
||||
// Verificar se os dados foram carregados corretamente
|
||||
const erros = [];
|
||||
|
||||
// Verificar campos básicos
|
||||
if (!data.nome) erros.push('Nome não carregado');
|
||||
if (!data.cpf) erros.push('CPF não carregado');
|
||||
|
||||
// Verificar se os campos foram preenchidos
|
||||
for (const [id, config] of Object.entries(camposEsperados)) {
|
||||
const campo = document.getElementById(id);
|
||||
if (!campo) continue;
|
||||
|
||||
// Mapear campos do servidor para campos do formulário
|
||||
let valorEsperado = '';
|
||||
switch(id) {
|
||||
case 'edit_nome': valorEsperado = data.nome; break;
|
||||
case 'edit_cpf': valorEsperado = data.cpf; break;
|
||||
case 'edit_email': valorEsperado = data.emails?.[0]; break;
|
||||
case 'edit_telefone1': valorEsperado = data.telefone1; break;
|
||||
case 'edit_celula': valorEsperado = data.celula_id?.toString(); break;
|
||||
case 'edit_cargo_sindical': valorEsperado = data.cargo_sindical; break;
|
||||
case 'edit_central_sindical': valorEsperado = data.central_sindical; break;
|
||||
case 'edit_sindicato': valorEsperado = data.sindicato; break;
|
||||
// Adicione mais campos conforme necessário
|
||||
}
|
||||
|
||||
if (config.obrigatorio && !valorEsperado) {
|
||||
erros.push(`Campo obrigatório ${id} não tem valor no servidor`);
|
||||
}
|
||||
|
||||
if (valorEsperado && campo.value !== valorEsperado) {
|
||||
erros.push(`Campo ${id} tem valor diferente do servidor. Esperado: ${valorEsperado}, Atual: ${campo.value}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (erros.length > 0) {
|
||||
console.error('Erros no carregamento:', erros);
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log('Dados carregados corretamente');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Erro ao carregar dados:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Função para testar o salvamento de dados
|
||||
async function testarSalvamentoDados(militanteId) {
|
||||
console.log('Testando salvamento de dados...');
|
||||
try {
|
||||
const form = document.getElementById('formEditarMilitante');
|
||||
const formData = new FormData(form);
|
||||
|
||||
// Guardar valores originais para comparação
|
||||
const valoresOriginais = {
|
||||
nome: formData.get('nome'),
|
||||
cpf: formData.get('cpf'),
|
||||
email: formData.get('email'),
|
||||
celula: formData.get('celula'),
|
||||
cargo_sindical: formData.get('cargo_sindical'),
|
||||
central_sindical: formData.get('central_sindical'),
|
||||
sindicato: formData.get('sindicato'),
|
||||
responsabilidades: formData.get('responsabilidades_values')
|
||||
};
|
||||
|
||||
const response = await fetch(`/militantes/editar/${militanteId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Erro HTTP: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('Resposta do servidor:', data);
|
||||
|
||||
// Verificar se os dados foram salvos corretamente
|
||||
const row = document.querySelector(`tr[data-militante="${militanteId}"]`);
|
||||
if (!row) {
|
||||
console.error('Linha da tabela não encontrada após salvamento');
|
||||
return false;
|
||||
}
|
||||
|
||||
const erros = [];
|
||||
|
||||
// Verificar dados básicos na tabela
|
||||
const nome = row.querySelector('td[data-nome]')?.textContent;
|
||||
const cpf = row.querySelector('td[data-cpf]')?.textContent;
|
||||
const email = row.querySelector('td[data-email]')?.textContent;
|
||||
|
||||
if (nome !== valoresOriginais.nome) erros.push(`Nome não atualizado na tabela. Esperado: ${valoresOriginais.nome}, Atual: ${nome}`);
|
||||
if (cpf !== valoresOriginais.cpf) erros.push(`CPF não atualizado na tabela. Esperado: ${valoresOriginais.cpf}, Atual: ${cpf}`);
|
||||
if (email !== valoresOriginais.email) erros.push(`Email não atualizado na tabela. Esperado: ${valoresOriginais.email}, Atual: ${email}`);
|
||||
|
||||
// Verificar atributos para filtros
|
||||
const celulaId = row.getAttribute('data-celula-id');
|
||||
const responsabilidades = row.getAttribute('data-responsabilidades');
|
||||
|
||||
if (celulaId !== valoresOriginais.celula) erros.push(`Célula não atualizada na tabela. Esperado: ${valoresOriginais.celula}, Atual: ${celulaId}`);
|
||||
if (responsabilidades !== valoresOriginais.responsabilidades) erros.push(`Responsabilidades não atualizadas na tabela. Esperado: ${valoresOriginais.responsabilidades}, Atual: ${responsabilidades}`);
|
||||
|
||||
// Verificar botão de edição
|
||||
const btnEditar = row.querySelector('button[data-bs-target="#modalEditarMilitante"]');
|
||||
if (btnEditar) {
|
||||
if (btnEditar.getAttribute('data-militante-nome') !== valoresOriginais.nome) {
|
||||
erros.push('Nome não atualizado no botão de edição');
|
||||
}
|
||||
if (btnEditar.getAttribute('data-celula-id') !== valoresOriginais.celula) {
|
||||
erros.push('Célula não atualizada no botão de edição');
|
||||
}
|
||||
}
|
||||
|
||||
if (erros.length > 0) {
|
||||
console.error('Erros no salvamento:', erros);
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log('Dados salvos e atualizados corretamente');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Erro ao salvar dados:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Função principal de teste
|
||||
async function testarFormularioEdicao(militanteId) {
|
||||
console.log('Iniciando teste completo do formulário...');
|
||||
|
||||
// Testar campos do formulário
|
||||
if (!testarCamposFormulario()) {
|
||||
console.error('Teste dos campos falhou');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Testar carregamento de dados
|
||||
if (!await testarCarregamentoDados(militanteId)) {
|
||||
console.error('Teste de carregamento falhou');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Testar salvamento de dados
|
||||
if (!await testarSalvamentoDados(militanteId)) {
|
||||
console.error('Teste de salvamento falhou');
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log('Todos os testes passaram com sucesso!');
|
||||
return true;
|
||||
}
|
||||
|
||||
// Executar testes quando o documento estiver carregado
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Adicionar botão de teste na interface
|
||||
const btnTeste = document.createElement('button');
|
||||
btnTeste.className = 'btn btn-info me-2';
|
||||
btnTeste.innerHTML = '<i class="fas fa-vial me-2"></i>Testar Formulário';
|
||||
btnTeste.onclick = function() {
|
||||
// Pegar ID do primeiro militante da lista
|
||||
const primeiraLinha = document.querySelector('#militantesTable tbody tr');
|
||||
if (!primeiraLinha) {
|
||||
mostrarAlerta('danger', 'Nenhum militante encontrado para teste');
|
||||
return;
|
||||
}
|
||||
|
||||
const militanteId = primeiraLinha.getAttribute('data-militante');
|
||||
if (!militanteId) {
|
||||
mostrarAlerta('danger', 'ID do militante não encontrado');
|
||||
return;
|
||||
}
|
||||
|
||||
// Executar testes
|
||||
testarFormularioEdicao(militanteId).then(sucesso => {
|
||||
if (sucesso) {
|
||||
mostrarAlerta('success', 'Testes concluídos com sucesso!');
|
||||
} else {
|
||||
mostrarAlerta('danger', 'Alguns testes falharam. Verifique o console para mais detalhes.');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Adicionar botão ao lado do botão de exportar
|
||||
const btnExportar = document.querySelector('.btn-exportar');
|
||||
if (btnExportar && btnExportar.parentNode) {
|
||||
btnExportar.parentNode.insertBefore(btnTeste, btnExportar);
|
||||
}
|
||||
});
|
||||
119
static/js/vendas.js
Normal file
119
static/js/vendas.js
Normal file
@@ -0,0 +1,119 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('Carregando script vendas.js...');
|
||||
|
||||
// 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');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Configurar tabela de vendas
|
||||
const tabelaVendas = $('#vendasTable').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
|
||||
});
|
||||
|
||||
// Atualizar valor total ao mudar quantidade ou material
|
||||
const campoQuantidade = document.getElementById('quantidade');
|
||||
const campoMaterial = document.getElementById('material_id');
|
||||
const campoValorTotal = document.getElementById('valor_total');
|
||||
|
||||
function atualizarValorTotal() {
|
||||
if (!campoQuantidade || !campoMaterial || !campoValorTotal) return;
|
||||
|
||||
const quantidade = parseInt(campoQuantidade.value) || 0;
|
||||
const materialSelecionado = campoMaterial.options[campoMaterial.selectedIndex];
|
||||
const preco = materialSelecionado ? parseFloat(materialSelecionado.dataset.preco) || 0 : 0;
|
||||
|
||||
campoValorTotal.value = (quantidade * preco).toFixed(2);
|
||||
}
|
||||
|
||||
if (campoQuantidade) {
|
||||
campoQuantidade.addEventListener('change', atualizarValorTotal);
|
||||
}
|
||||
if (campoMaterial) {
|
||||
campoMaterial.addEventListener('change', atualizarValorTotal);
|
||||
}
|
||||
|
||||
// Configurar modal de edição
|
||||
const modalEditarVenda = document.getElementById('modalEditarVenda');
|
||||
if (modalEditarVenda) {
|
||||
modalEditarVenda.addEventListener('show.bs.modal', function(event) {
|
||||
const button = event.relatedTarget;
|
||||
if (!button) return;
|
||||
|
||||
const vendaId = button.getAttribute('data-venda-id');
|
||||
const militanteId = button.getAttribute('data-militante-id');
|
||||
const materialId = button.getAttribute('data-material-id');
|
||||
const quantidade = button.getAttribute('data-quantidade');
|
||||
const valorTotal = button.getAttribute('data-valor-total');
|
||||
const dataVenda = button.getAttribute('data-data-venda');
|
||||
|
||||
document.getElementById('editVendaId').value = vendaId;
|
||||
document.getElementById('editMilitanteId').value = militanteId;
|
||||
document.getElementById('editMaterialId').value = materialId;
|
||||
document.getElementById('editQuantidade').value = quantidade;
|
||||
document.getElementById('editValorTotal').value = valorTotal;
|
||||
document.getElementById('editDataVenda').value = dataVenda;
|
||||
});
|
||||
}
|
||||
});
|
||||
102
templates/admin/base.html
Normal file
102
templates/admin/base.html
Normal file
@@ -0,0 +1,102 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Área Administrativa{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<!-- Sidebar -->
|
||||
<nav id="sidebar" class="col-md-3 col-lg-2 d-md-block bg-light sidebar">
|
||||
<div class="position-sticky pt-3">
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.endpoint == 'admin.dashboard' %}active{% endif %}"
|
||||
href="{{ url_for('admin.dashboard') }}">
|
||||
<i class="fas fa-tachometer-alt me-2"></i>
|
||||
Dashboard
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.endpoint == 'admin.list_users' %}active{% endif %}"
|
||||
href="{{ url_for('admin.list_users') }}">
|
||||
<i class="fas fa-users me-2"></i>
|
||||
Usuários
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('home') }}">
|
||||
<i class="fas fa-arrow-left me-2"></i>
|
||||
Voltar ao Sistema
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Main content -->
|
||||
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
|
||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<h1 class="h2">{% block admin_title %}{% endblock %}</h1>
|
||||
</div>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% block admin_content %}{% endblock %}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 100;
|
||||
padding: 48px 0 0;
|
||||
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
|
||||
}
|
||||
|
||||
.sidebar .nav-link {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.sidebar .nav-link.active {
|
||||
color: #2470dc;
|
||||
}
|
||||
|
||||
.sidebar-heading {
|
||||
font-size: .75rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
main {
|
||||
padding-top: 48px;
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.sidebar {
|
||||
position: static;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
main {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}{% endblock %}
|
||||
329
templates/admin/dashboard.html
Normal file
329
templates/admin/dashboard.html
Normal file
@@ -0,0 +1,329 @@
|
||||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block title %}Dashboard Administrativo{% endblock %}
|
||||
|
||||
<<<<<<< HEAD
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.card {
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
|
||||
transition: all 0.3s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.bg-primary {
|
||||
background: linear-gradient(135deg, #0d6efd, #0a58ca) !important;
|
||||
}
|
||||
|
||||
.bg-success {
|
||||
background: linear-gradient(135deg, #198754, #146c43) !important;
|
||||
}
|
||||
|
||||
.bg-danger {
|
||||
background: linear-gradient(135deg, #dc3545, #b02a37) !important;
|
||||
}
|
||||
|
||||
.card .opacity-50 {
|
||||
opacity: 0.2 !important;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.card:hover .opacity-50 {
|
||||
opacity: 0.3 !important;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
text-transform: uppercase;
|
||||
color: rgba(255,255,255,0.8);
|
||||
}
|
||||
|
||||
.display-4 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 600;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
/* Estilo da lista de usuários */
|
||||
.card.lista-usuarios {
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
transition: none;
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.card.lista-usuarios:hover {
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.card.lista-usuarios .card-header {
|
||||
background: linear-gradient(to right, var(--secondary-dark), var(--secondary-color));
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
|
||||
.card.lista-usuarios .card-header h5 {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.card.lista-usuarios .table {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.card.lista-usuarios .table th {
|
||||
border-top: none;
|
||||
font-weight: 600;
|
||||
padding: 1rem;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.card.lista-usuarios .table td {
|
||||
padding: 1rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.card.lista-usuarios .badge {
|
||||
padding: 0.5em 0.8em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn-group .btn {
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2 class="mb-4">
|
||||
<i class="fas fa-users-cog"></i>
|
||||
Administração de Usuários
|
||||
</h2>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card bg-primary text-white">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-uppercase">Total de Usuários</h5>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h2 class="display-4 mb-0">{{ total_users }}</h2>
|
||||
<i class="fas fa-users fa-3x opacity-50"></i>
|
||||
</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 class="col-md-4">
|
||||
<<<<<<< HEAD
|
||||
<div class="card bg-success text-white">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-uppercase">Usuários Ativos</h5>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h2 class="display-4 mb-0">{{ active_users }}</h2>
|
||||
<i class="fas fa-user-check fa-3x opacity-50"></i>
|
||||
</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 class="col-md-4">
|
||||
<<<<<<< HEAD
|
||||
<div class="card bg-danger text-white">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-uppercase">Usuários Inativos</h5>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h2 class="display-4 mb-0">{{ inactive_users }}</h2>
|
||||
<i class="fas fa-user-times fa-3x opacity-50"></i>
|
||||
</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>
|
||||
|
||||
<<<<<<< HEAD
|
||||
<div class="card lista-usuarios">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-users me-2"></i>
|
||||
Lista de Usuários
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table id="users-table" class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nome</th>
|
||||
<th>Email</th>
|
||||
<th>Status</th>
|
||||
<th>Último Login</th>
|
||||
<th>Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in users %}
|
||||
<tr>
|
||||
<td>{{ user.name }}</td>
|
||||
<td>{{ user.email }}</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>
|
||||
<div class="btn-group">
|
||||
<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?')">
|
||||
<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>
|
||||
</form>
|
||||
<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?')">
|
||||
<i class="fas fa-lock"></i>
|
||||
</button>
|
||||
</form>
|
||||
<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">
|
||||
<i class="fas fa-{{ 'user-times' if user.is_active else 'user-check' }}"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</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>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<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() {
|
||||
$('#users-table').DataTable({
|
||||
language: {
|
||||
url: '//cdn.datatables.net/plug-ins/1.13.7/i18n/pt-BR.json'
|
||||
},
|
||||
<<<<<<< HEAD
|
||||
order: [[0, 'asc']],
|
||||
pageLength: 25
|
||||
=======
|
||||
order: [[1, 'asc']]
|
||||
>>>>>>> a22b0e4 (refactor(#11): Integra listagem de usuários no dashboard)
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
51
templates/alterar_senha.html
Normal file
51
templates/alterar_senha.html
Normal file
@@ -0,0 +1,51 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Alterar Senha{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Alterar Senha</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="POST" action="{{ url_for('alterar_senha') }}">
|
||||
<div class="mb-3">
|
||||
<label for="senha_atual" class="form-label">Senha Atual</label>
|
||||
<input type="password" class="form-control" id="senha_atual" name="senha_atual" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="nova_senha" class="form-label">Nova Senha</label>
|
||||
<input type="password" class="form-control" id="nova_senha" name="nova_senha" required>
|
||||
<small class="text-muted">
|
||||
A senha deve ter no mínimo 8 caracteres e conter letras e números.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="confirmar_senha" class="form-label">Confirmar Nova Senha</label>
|
||||
<input type="password" class="form-control" id="confirmar_senha" name="confirmar_senha" required>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-primary">Alterar Senha</button>
|
||||
<a href="{{ url_for('home') }}" class="btn btn-secondary">Cancelar</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -3,39 +3,631 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}{% endblock %} - Sistema de Gestão</title>
|
||||
{{ bootstrap.load_css() }}
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='img/favicon.ico') }}">
|
||||
<title>{% block title %}{% endblock %} - Controles OCI</title>
|
||||
|
||||
<!-- Bootstrap 5 CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css?v=1" rel="stylesheet">
|
||||
<!-- Font Awesome 6 -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
<!-- Componentes CSS -->
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/components.css') }}?v={{ range(1, 10000) | random }}">
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #dc3545;
|
||||
--primary-light: #e35d6a;
|
||||
--secondary-color: #6c757d;
|
||||
--secondary-light: #868e96;
|
||||
--success-color: #198754;
|
||||
--danger-color: #dc3545;
|
||||
--warning-color: #ffc107;
|
||||
--info-color: #0dcaf0;
|
||||
--background-gradient: linear-gradient(135deg, var(--primary-color) 40%, white 100%);
|
||||
--navbar-stripe: 4px solid var(--primary-color);
|
||||
|
||||
/* Adicionando variáveis para os botões */
|
||||
--bs-success: #198754;
|
||||
--bs-success-dark: #157347;
|
||||
--bs-secondary: #6c757d;
|
||||
--bs-secondary-dark: #565e64;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: #f8f9fa;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
background: #343a40 !important;
|
||||
padding: 0.5rem 1rem;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
border-bottom: var(--navbar-stripe);
|
||||
}
|
||||
|
||||
.navbar > .container-fluid {
|
||||
width: 100%;
|
||||
max-width: 1320px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
flex: 0 0 auto;
|
||||
margin-right: 2rem;
|
||||
font-weight: 500;
|
||||
color: #fff !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.navbar-brand img {
|
||||
height: 35px;
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
|
||||
#navbarNav {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.navbar-nav.mx-auto {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.navbar-nav:last-child {
|
||||
flex: 0 0 auto;
|
||||
margin-left: 2rem;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
color: rgba(255,255,255,0.85) !important;
|
||||
transition: all 0.2s ease;
|
||||
padding: 0.75rem 1rem;
|
||||
white-space: nowrap;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
color: #fff !important;
|
||||
background-color: var(--primary-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.nav-link i {
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.9;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
background-color: #343a40;
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
||||
padding: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
border-radius: 8px;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
color: rgba(255,255,255,0.85) !important;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 400;
|
||||
padding: 0.6rem 1rem;
|
||||
transition: all 0.2s ease;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.dropdown-item:hover {
|
||||
background-color: var(--primary-color);
|
||||
color: #fff !important;
|
||||
transform: translateX(3px);
|
||||
}
|
||||
|
||||
.dropdown-item i {
|
||||
margin-right: 0.75rem;
|
||||
width: 1.25rem;
|
||||
text-align: center;
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.dropdown-divider {
|
||||
border-top: 1px solid rgba(255,255,255,0.1);
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
/* Estilo para o menu mobile */
|
||||
@media (max-width: 768px) {
|
||||
.navbar-collapse {
|
||||
background-color: #343a40;
|
||||
padding: 1rem;
|
||||
border-radius: 0 0 10px 10px;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.navbar-brand img {
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
background-color: rgba(0,0,0,0.2);
|
||||
margin-left: 1rem;
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.navbar-nav {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1320px !important;
|
||||
margin: 0 auto !important;
|
||||
}
|
||||
|
||||
@media (max-width: 1400px) {
|
||||
.container {
|
||||
max-width: 1140px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.container {
|
||||
max-width: 960px !important;
|
||||
}
|
||||
.page-wrapper {
|
||||
padding: 1.5rem 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.container {
|
||||
max-width: 720px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
max-width: 540px !important;
|
||||
}
|
||||
.page-wrapper {
|
||||
padding: 1rem 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.page-wrapper {
|
||||
padding: 0.75rem 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Cards da Dashboard */
|
||||
.card {
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background-color: #f8f9fa;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.card-header .card-title {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.card-header h5 {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.card-header h5 i {
|
||||
margin-right: 0.75rem;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
background: none;
|
||||
border-top: 1px solid rgba(0,0,0,0.05);
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
|
||||
/* Estatísticas da Dashboard */
|
||||
.stats-card {
|
||||
position: relative;
|
||||
padding: 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
color: white;
|
||||
box-shadow: 0 0.25rem 0.5rem rgba(0,0,0,0.1);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
overflow: hidden;
|
||||
min-height: 140px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.stats-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 0.5rem 1rem rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.stats-card.blue {
|
||||
background: linear-gradient(45deg, var(--primary-color), var(--primary-light));
|
||||
}
|
||||
|
||||
.stats-card.green {
|
||||
background: linear-gradient(45deg, #1cc88a, #13855c);
|
||||
}
|
||||
|
||||
.stats-card.cyan {
|
||||
background: linear-gradient(45deg, #36b9cc, #258391);
|
||||
}
|
||||
|
||||
.stats-card.yellow {
|
||||
background: linear-gradient(45deg, #f6c23e, #dda20a);
|
||||
}
|
||||
|
||||
.stats-card .title {
|
||||
font-size: 0.9rem;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 0.5rem;
|
||||
color: rgba(255,255,255,0.8);
|
||||
}
|
||||
|
||||
.stats-card .value {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
margin: 0.5rem 0;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.stats-card .link {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.8;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.stats-card .link:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.stats-card .icon {
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
bottom: 1rem;
|
||||
font-size: 4rem;
|
||||
opacity: 0.2;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Tabelas e Listas */
|
||||
.table {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.table th {
|
||||
border-top: none;
|
||||
font-weight: 600;
|
||||
padding: 1rem;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.table td {
|
||||
padding: 1rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.table-hover tbody tr:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.list-group-item {
|
||||
border: none;
|
||||
border-bottom: 1px solid rgba(0,0,0,0.05);
|
||||
padding: 1rem 1.5rem;
|
||||
transition: background-color 0.2s ease;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.list-group-item:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.list-group-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.militante-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.militante-info h6 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.militante-info small {
|
||||
color: var(--secondary-color);
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
/* Botões e Alertas */
|
||||
.alert {
|
||||
border-radius: 0.5rem;
|
||||
border: none;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,0.075);
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.25rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background-color: var(--success-color);
|
||||
border-color: var(--success-color);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: var(--danger-color);
|
||||
border-color: var(--danger-color);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: var(--secondary-color);
|
||||
border-color: var(--secondary-color);
|
||||
}
|
||||
|
||||
/* Badges e Labels */
|
||||
.badge {
|
||||
padding: 0.5em 0.75em;
|
||||
font-weight: 500;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: var(--secondary-color) !important;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
border-top: 1px solid #e9ecef;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* Login page specific */
|
||||
.login-page {
|
||||
background: var(--background-gradient);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 0.5rem 1rem rgba(0,0,0,0.15);
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.login-logo {
|
||||
height: 60px;
|
||||
width: auto;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
color: var(--primary-color);
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.login-subtitle {
|
||||
color: var(--secondary-color);
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.welcome-header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.stats-card {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.page-wrapper {
|
||||
padding: 2rem 1rem;
|
||||
min-height: calc(100vh - 70px);
|
||||
}
|
||||
</style>
|
||||
{% block extra_css %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="{{ url_for('home') }}">Sistema de Gestão</a>
|
||||
{% block navbar %}
|
||||
<nav class="navbar navbar-expand-lg navbar-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="{{ url_for('home') }}">
|
||||
<img src="{{ url_for('static', filename='img/logo002-alpha.png') }}" alt="Logo OCI">
|
||||
Controles OCI
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
{% if session.get('user_id') %}
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav mx-auto">
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link" href="#" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-users me-1"></i>Militantes
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ url_for('listar_militantes') }}">
|
||||
<i class="fas fa-list"></i>Listar Militantes
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link" href="#" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-dollar-sign me-1"></i>Financeiro
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ url_for('listar_cotas') }}">
|
||||
<i class="fas fa-money-bill-wave"></i>Cotas
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ url_for('listar_pagamentos') }}">
|
||||
<i class="fas fa-receipt"></i>Pagamentos
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link" href="#" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-box me-1"></i>Materiais
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ url_for('listar_materiais') }}">
|
||||
<i class="fas fa-box"></i>Listar Materiais
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ url_for('listar_vendas_jornal') }}">
|
||||
<i class="fas fa-newspaper"></i>Vendas de Jornais
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ url_for('listar_assinaturas') }}">
|
||||
<i class="fas fa-file-signature"></i>Assinaturas
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link" href="#" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-chart-bar me-1"></i>Relatórios
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ url_for('listar_relatorios_cotas') }}">
|
||||
<i class="fas fa-file-invoice-dollar"></i>Relatórios de Cotas
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ url_for('listar_relatorios_vendas') }}">
|
||||
<i class="fas fa-file-alt"></i>Relatórios de Vendas
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('listar_militantes') }}">Militantes</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('listar_cotas') }}">Cotas</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('listar_pagamentos') }}">Pagamentos</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('listar_materiais') }}">Materiais</a>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link" href="#" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-user me-1"></i>{{ session.get('username', 'Usuário') }}
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
{% if session.get('is_admin') %}
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ url_for('novo_usuario') }}">
|
||||
<i class="fas fa-user-plus"></i>Novo Usuário
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ url_for('admin.dashboard') }}">
|
||||
<i class="fas fa-cog fa fa-cog fa-solid fa-cog" style="display: inline-block !important; visibility: visible !important;"></i>Administração
|
||||
</a>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ url_for('logout') }}">
|
||||
<i class="fas fa-sign-out-alt"></i>Sair
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</nav>
|
||||
{% endblock %}
|
||||
|
||||
<div class="container mt-4">
|
||||
{% block content %}{% endblock %}
|
||||
<div class="page-wrapper">
|
||||
<div class="container py-4">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ bootstrap.load_js() }}
|
||||
<!-- Bootstrap 5 JS Bundle with Popper -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
111
templates/criar_instancia.html
Normal file
111
templates/criar_instancia.html
Normal file
@@ -0,0 +1,111 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Criar {{ tipo_instancia }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h1 class="mb-4">Criar {{ tipo_instancia }}</h1>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="POST" class="needs-validation" novalidate>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="nome" class="form-label">Nome</label>
|
||||
<input type="text" class="form-control" id="nome" name="nome" required>
|
||||
<div class="invalid-feedback">
|
||||
Por favor, insira o nome da {{ tipo_instancia }}.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if tipo_instancia != 'Célula' %}
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="instancia_superior_id" class="form-label">{{ instancia_superior }}</label>
|
||||
<select class="form-select" id="instancia_superior_id" name="instancia_superior_id" required>
|
||||
<option value="">Selecione uma {{ instancia_superior }}</option>
|
||||
{% for superior in instancias_superiores %}
|
||||
<option value="{{ superior.id }}">{{ superior.nome }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div class="invalid-feedback">
|
||||
Por favor, selecione uma {{ instancia_superior }}.
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="responsavel_geral_id" class="form-label">Responsável Geral</label>
|
||||
<select class="form-select" id="responsavel_geral_id" name="responsavel_geral_id" required>
|
||||
<option value="">Selecione o responsável geral</option>
|
||||
{% for militante in militantes %}
|
||||
<option value="{{ militante.id }}">{{ militante.nome }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div class="invalid-feedback">
|
||||
Por favor, selecione o responsável geral.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="responsavel_financas_id" class="form-label">Responsável de Finanças</label>
|
||||
<select class="form-select" id="responsavel_financas_id" name="responsavel_financas_id">
|
||||
<option value="">Selecione o responsável de finanças</option>
|
||||
{% for militante in militantes %}
|
||||
<option value="{{ militante.id }}">{{ militante.nome }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="responsavel_imprensa_id" class="form-label">Responsável de Imprensa</label>
|
||||
<select class="form-select" id="responsavel_imprensa_id" name="responsavel_imprensa_id">
|
||||
<option value="">Selecione o responsável de imprensa</option>
|
||||
{% for militante in militantes %}
|
||||
<option value="{{ militante.id }}">{{ militante.nome }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between mt-4">
|
||||
<button type="submit" class="btn btn-primary">Criar</button>
|
||||
<a href="{{ url_for('listar_' + tipo_instancia.lower() + 's') }}" class="btn btn-secondary">Cancelar</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Validação do formulário
|
||||
(function () {
|
||||
'use strict'
|
||||
|
||||
var forms = document.querySelectorAll('.needs-validation')
|
||||
|
||||
Array.prototype.slice.call(forms)
|
||||
.forEach(function (form) {
|
||||
form.addEventListener('submit', function (event) {
|
||||
if (!form.checkValidity()) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
form.classList.add('was-validated')
|
||||
}, false)
|
||||
})
|
||||
})()
|
||||
</script>
|
||||
{% endblock %}
|
||||
107
templates/criar_militante.html
Normal file
107
templates/criar_militante.html
Normal file
@@ -0,0 +1,107 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Criar Militante{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h1 class="mb-4">Criar Militante</h1>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="POST" class="needs-validation" novalidate>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="nome" class="form-label">Nome</label>
|
||||
<input type="text" class="form-control" id="nome" name="nome" required>
|
||||
<div class="invalid-feedback">
|
||||
Por favor, insira o nome do militante.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="email" class="form-label">Email</label>
|
||||
<input type="email" class="form-control" id="email" name="email" required>
|
||||
<div class="invalid-feedback">
|
||||
Por favor, insira um email válido.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="celula_id" class="form-label">Célula</label>
|
||||
<select class="form-select" id="celula_id" name="celula_id" required>
|
||||
<option value="">Selecione uma célula</option>
|
||||
{% for celula in celulas %}
|
||||
<option value="{{ celula.id }}">{{ celula.nome }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div class="invalid-feedback">
|
||||
Por favor, selecione uma célula.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 mb-3">
|
||||
<label class="form-label">Responsabilidades</label>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="responsavel_financas" name="responsabilidades" value="{{ Militante.RESPONSAVEL_FINANCAS }}">
|
||||
<label class="form-check-label" for="responsavel_financas">
|
||||
Responsável de Finanças
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="responsavel_imprensa" name="responsabilidades" value="{{ Militante.RESPONSAVEL_IMPRENSA }}">
|
||||
<label class="form-check-label" for="responsavel_imprensa">
|
||||
Responsável de Imprensa
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="quadro_orientador" name="responsabilidades" value="{{ Militante.QUADRO_ORIENTADOR }}">
|
||||
<label class="form-check-label" for="quadro_orientador">
|
||||
Quadro-Orientador
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between mt-4">
|
||||
<button type="submit" class="btn btn-primary">Criar</button>
|
||||
<a href="{{ url_for('listar_militantes') }}" class="btn btn-secondary">Cancelar</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Validação do formulário
|
||||
(function () {
|
||||
'use strict'
|
||||
|
||||
var forms = document.querySelectorAll('.needs-validation')
|
||||
|
||||
Array.prototype.slice.call(forms)
|
||||
.forEach(function (form) {
|
||||
form.addEventListener('submit', function (event) {
|
||||
if (!form.checkValidity()) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
form.classList.add('was-validated')
|
||||
}, false)
|
||||
})
|
||||
})()
|
||||
</script>
|
||||
{% endblock %}
|
||||
284
templates/dashboard.html
Normal file
284
templates/dashboard.html
Normal file
@@ -0,0 +1,284 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Dashboard Administrativo{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<h1 class="mb-4">Dashboard Administrativo</h1>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Gerenciamento de Acessos</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Usuário</th>
|
||||
<th>Email</th>
|
||||
<th>Status</th>
|
||||
<th>Último Login</th>
|
||||
<th>Nível</th>
|
||||
<th>Responsabilidades</th>
|
||||
<th>Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in users %}
|
||||
{% if current_user.has_permission('system_config') or
|
||||
(current_user.has_permission('manage_cr_sectors') and user.cr_id == current_user.cr_id) or
|
||||
(current_user.has_permission('manage_sector_cells') and user.setor_id == current_user.setor_id) or
|
||||
(current_user.has_permission('manage_cell_members') and user.celula_id == current_user.celula_id) %}
|
||||
<tr>
|
||||
<td>{{ user.id }}</td>
|
||||
<td>{{ user.username }}</td>
|
||||
<td>{{ user.email }}</td>
|
||||
<td>
|
||||
{% if user.ativo %}
|
||||
<span class="badge bg-success">Ativo</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger">Inativo</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ user.ultimo_login.strftime('%d/%m/%Y %H:%M') if user.ultimo_login else 'Nunca' }}</td>
|
||||
<td>
|
||||
<span class="badge bg-info">{{ user.role }}</span>
|
||||
{% if current_user.has_permission('system_config') or
|
||||
(current_user.has_permission('manage_cr_sectors') and user.cr_id == current_user.cr_id) or
|
||||
(current_user.has_permission('manage_sector_cells') and user.setor_id == current_user.setor_id) %}
|
||||
<select class="form-select form-select-sm d-inline-block w-auto" onchange="alterarNivel({{ user.id }}, this.value)">
|
||||
<option value="">Alterar Nível</option>
|
||||
{% if current_user.has_permission('system_config') %}
|
||||
<option value="militante_basico">Militante Básico</option>
|
||||
<option value="secretario_celula">Secretário de Célula</option>
|
||||
<option value="membro_setor">Membro de Setor</option>
|
||||
<option value="secretario_setor">Secretário de Setor</option>
|
||||
<option value="membro_cr">Membro de CR</option>
|
||||
<option value="secretario_cr">Secretário de CR</option>
|
||||
<option value="membro_cc">Membro do CC</option>
|
||||
<option value="secretario_geral">Secretário Geral</option>
|
||||
{% elif current_user.has_permission('manage_cr_sectors') %}
|
||||
<option value="membro_cr">Membro de CR</option>
|
||||
<option value="secretario_cr">Secretário de CR</option>
|
||||
{% elif current_user.has_permission('manage_sector_cells') %}
|
||||
<option value="membro_setor">Membro de Setor</option>
|
||||
<option value="secretario_setor">Secretário de Setor</option>
|
||||
{% endif %}
|
||||
</select>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if user.militante %}
|
||||
{% if user.militante.quadro_orientador %}
|
||||
<span class="badge bg-primary">Quadro-Orientador</span>
|
||||
{% endif %}
|
||||
{% if user.militante.aspirante %}
|
||||
<span class="badge bg-warning">Aspirante</span>
|
||||
<small class="text-muted">
|
||||
(desde {{ user.militante.data_inicio_aspirante.strftime('%d/%m/%Y') }})
|
||||
</small>
|
||||
{% if user.militante.avaliacao_aspirante %}
|
||||
<button type="button" class="btn btn-sm btn-info"
|
||||
onclick="verAvaliacaoAspirante({{ user.id }})">
|
||||
Ver Avaliação
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if current_user.has_permission('system_config') or
|
||||
(current_user.has_permission('manage_cr_sectors') and user.cr_id == current_user.cr_id) or
|
||||
(current_user.has_permission('manage_sector_cells') and user.setor_id == current_user.setor_id) %}
|
||||
{% if user.militante.quadro_orientador %}
|
||||
<button type="button" class="btn btn-sm btn-danger"
|
||||
onclick="toggleQuadroOrientador({{ user.id }}, {{ user.militante.quadro_orientador|lower }})">
|
||||
Remover QO
|
||||
</button>
|
||||
{% else %}
|
||||
<button type="button" class="btn btn-sm btn-success"
|
||||
onclick="toggleQuadroOrientador({{ user.id }}, {{ user.militante.quadro_orientador|lower }})">
|
||||
Tornar QO
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if user.militante.aspirante %}
|
||||
{% if datetime.utcnow() - user.militante.data_inicio_aspirante >= timedelta(days=90) %}
|
||||
{% if not user.militante.avaliacao_aspirante %}
|
||||
<button type="button" class="btn btn-sm btn-primary"
|
||||
onclick="avaliarAspirante({{ user.id }})">
|
||||
Avaliar Aspirante
|
||||
</button>
|
||||
{% endif %}
|
||||
<button type="button" class="btn btn-sm btn-danger"
|
||||
onclick="toggleAspirante({{ user.id }}, {{ user.militante.aspirante|lower }})">
|
||||
Remover Aspirante
|
||||
</button>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<button type="button" class="btn btn-sm btn-warning"
|
||||
onclick="toggleAspirante({{ user.id }}, {{ user.militante.aspirante|lower }})">
|
||||
Tornar Aspirante
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group" role="group">
|
||||
{% if current_user.has_permission('system_config') or
|
||||
(current_user.has_permission('manage_cr_sectors') and user.cr_id == current_user.cr_id) or
|
||||
(current_user.has_permission('manage_sector_cells') and user.setor_id == current_user.setor_id) or
|
||||
(current_user.has_permission('manage_cell_members') and user.celula_id == current_user.celula_id) %}
|
||||
<button type="button" class="btn btn-sm btn-primary"
|
||||
onclick="resetOTP({{ user.id }})">
|
||||
Gerar Novo OTP
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-warning"
|
||||
onclick="resetPassword({{ user.id }})">
|
||||
Resetar Senha
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm {% if user.ativo %}btn-danger{% else %}btn-success{% endif %}"
|
||||
onclick="toggleUserStatus({{ user.id }}, {{ user.ativo|lower }})">
|
||||
{% if user.ativo %}Desativar{% else %}Ativar{% endif %} Login
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function resetOTP(userId) {
|
||||
if (confirm('Tem certeza que deseja gerar um novo OTP para este usuário? O OTP atual será invalidado.')) {
|
||||
fetch(`/usuarios/${userId}/otp/reset`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert('Novo OTP gerado com sucesso!');
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Erro ao gerar novo OTP: ' + data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert('Erro ao gerar novo OTP: ' + error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function resetPassword(userId) {
|
||||
if (confirm('Tem certeza que deseja resetar a senha deste usuário? Uma nova senha será gerada e enviada por email.')) {
|
||||
fetch(`/usuarios/${userId}/password/reset`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert('Senha resetada com sucesso! A nova senha foi enviada por email.');
|
||||
} else {
|
||||
alert('Erro ao resetar senha: ' + data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert('Erro ao resetar senha: ' + error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function toggleUserStatus(userId, currentStatus) {
|
||||
const action = currentStatus ? 'desativar' : 'ativar';
|
||||
if (confirm(`Tem certeza que deseja ${action} o login deste usuário?`)) {
|
||||
fetch(`/usuarios/${userId}/toggle_status`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert(`Login ${action}do com sucesso!`);
|
||||
location.reload();
|
||||
} else {
|
||||
alert(`Erro ao ${action} login: ` + data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert(`Erro ao ${action} login: ` + error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function alterarNivel(userId, novoNivel) {
|
||||
if (!novoNivel) return;
|
||||
|
||||
if (confirm('Tem certeza que deseja alterar o nível deste usuário?')) {
|
||||
fetch(`/usuarios/${userId}/alterar_nivel`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ nivel: novoNivel })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert('Nível do usuário alterado com sucesso!');
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Erro ao alterar nível: ' + data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert('Erro ao alterar nível: ' + error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function toggleQuadroOrientador(userId, isQuadroOrientador) {
|
||||
const action = isQuadroOrientador ? 'remover' : 'adicionar';
|
||||
if (confirm(`Tem certeza que deseja ${action} a responsabilidade de Quadro-Orientador deste militante?`)) {
|
||||
fetch(`/usuarios/${userId}/toggle_quadro_orientador`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert(`Responsabilidade de Quadro-Orientador ${action}da com sucesso!`);
|
||||
location.reload();
|
||||
} else {
|
||||
alert(`Erro ao ${action} responsabilidade: ` + data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert(`Erro ao ${action} responsabilidade: ` + error);
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
206
templates/dashboard_admin.html
Normal file
206
templates/dashboard_admin.html
Normal file
@@ -0,0 +1,206 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Dashboard Administrativo{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<h2 class="mb-4"><i class="fas fa-users-cog"></i> Administração de Usuários</h2>
|
||||
<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="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">
|
||||
<table class="table table-hover">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th>Usuário</th>
|
||||
<th>Email</th>
|
||||
<th>Nome</th>
|
||||
<th>Último Acesso</th>
|
||||
<th>Status</th>
|
||||
<th>Nível</th>
|
||||
<th>Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for usuario in usuarios %}
|
||||
<tr>
|
||||
<td>{{ usuario.username }}</td>
|
||||
<td>{{ usuario.email }}</td>
|
||||
<td>{{ usuario.nome }}</td>
|
||||
<td>{{ usuario.last_login }}</td>
|
||||
<td>
|
||||
<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" }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if usuario.is_admin %}
|
||||
Administrador
|
||||
{% else %}
|
||||
{{ usuario.nivel }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group" role="group">
|
||||
<button class="btn btn-sm btn-outline-primary"
|
||||
onclick="toggleStatus('{{ usuario.id }}')"
|
||||
data-toggle="tooltip"
|
||||
title="{{ 'Desativar' if usuario.ativo else 'Ativar' }} usuário">
|
||||
<i class="fas {% if usuario.ativo %}fa-user-times{% else %}fa-user-check{% endif %}"></i>
|
||||
</button>
|
||||
|
||||
<button class="btn btn-sm btn-outline-warning"
|
||||
onclick="resetarSenha('{{ usuario.id }}')"
|
||||
data-toggle="tooltip"
|
||||
title="Resetar senha">
|
||||
<i class="fas fa-key"></i>
|
||||
</button>
|
||||
|
||||
{% if not usuario.is_admin %}
|
||||
<button class="btn btn-sm btn-outline-info"
|
||||
onclick="alterarNivel('{{ usuario.id }}')"
|
||||
data-toggle="tooltip"
|
||||
title="Alterar nível">
|
||||
<i class="fas fa-level-up-alt"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal de Feedback -->
|
||||
<div class="modal fade" id="feedbackModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Aviso</h5>
|
||||
<button type="button" class="close" data-dismiss="modal">
|
||||
<span>×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p id="feedbackMessage"></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Fechar</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
function showFeedback(message, type = 'info') {
|
||||
const modal = document.getElementById('feedbackModal');
|
||||
const messageElement = document.getElementById('feedbackMessage');
|
||||
messageElement.textContent = message;
|
||||
messageElement.className = `alert alert-${type}`;
|
||||
$(modal).modal('show');
|
||||
}
|
||||
|
||||
function handleResponse(response) {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
function toggleStatus(userId) {
|
||||
if (!confirm('Tem certeza que deseja alterar o status deste usuário?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`/usuarios/${userId}/toggle_status`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').content
|
||||
}
|
||||
})
|
||||
.then(handleResponse)
|
||||
.then(data => {
|
||||
showFeedback(data.message || 'Status alterado com sucesso!', data.success ? 'success' : 'danger');
|
||||
if (data.success) {
|
||||
setTimeout(() => location.reload(), 1500);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
showFeedback('Erro ao alterar status do usuário. Por favor, tente novamente.', 'danger');
|
||||
});
|
||||
}
|
||||
|
||||
function resetarSenha(userId) {
|
||||
if (!confirm('Tem certeza que deseja resetar a senha deste usuário?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`/reset_password/${userId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').content
|
||||
}
|
||||
})
|
||||
.then(handleResponse)
|
||||
.then(data => {
|
||||
showFeedback(data.message || 'Senha resetada com sucesso!', data.success ? 'success' : 'danger');
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
showFeedback('Erro ao resetar senha. Por favor, tente novamente.', 'danger');
|
||||
});
|
||||
}
|
||||
|
||||
function alterarNivel(userId) {
|
||||
const novoNivel = prompt('Digite o novo nível do usuário (1-5):');
|
||||
if (!novoNivel) return;
|
||||
|
||||
if (!/^[1-5]$/.test(novoNivel)) {
|
||||
showFeedback('Por favor, insira um nível válido entre 1 e 5.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`/usuarios/${userId}/alterar_nivel`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').content
|
||||
},
|
||||
body: JSON.stringify({ nivel: parseInt(novoNivel) })
|
||||
})
|
||||
.then(handleResponse)
|
||||
.then(data => {
|
||||
showFeedback(data.message || 'Nível alterado com sucesso!', data.success ? 'success' : 'danger');
|
||||
if (data.success) {
|
||||
setTimeout(() => location.reload(), 1500);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
showFeedback('Erro ao alterar nível. Por favor, tente novamente.', 'danger');
|
||||
});
|
||||
}
|
||||
|
||||
// Inicializa os tooltips do Bootstrap
|
||||
$(function () {
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
94
templates/editar_celula.html
Normal file
94
templates/editar_celula.html
Normal file
@@ -0,0 +1,94 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Editar Célula{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h1 class="mb-4">Editar Célula</h1>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="POST" class="needs-validation" novalidate>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="nome" class="form-label">Nome</label>
|
||||
<input type="text" class="form-control" id="nome" name="nome" value="{{ celula.nome }}" required>
|
||||
<div class="invalid-feedback">
|
||||
Por favor, insira o nome da célula.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="setor_id" class="form-label">Setor</label>
|
||||
<select class="form-select" id="setor_id" name="setor_id" required>
|
||||
<option value="">Selecione um setor</option>
|
||||
{% for setor in setores %}
|
||||
<option value="{{ setor.id }}" {% if setor.id == celula.setor_id %}selected{% endif %}>{{ setor.nome }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div class="invalid-feedback">
|
||||
Por favor, selecione um setor.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="responsavel" class="form-label">Responsável</label>
|
||||
<select class="form-select" id="responsavel" name="responsavel">
|
||||
<option value="">Selecione um responsável</option>
|
||||
{% for militante in militantes %}
|
||||
<option value="{{ militante.id }}" {% if militante.id == celula.responsavel %}selected{% endif %}>{{ militante.nome }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="responsavel_financas" class="form-label">Responsável Finanças</label>
|
||||
<select class="form-select" id="responsavel_financas" name="responsavel_financas">
|
||||
<option value="">Selecione um responsável financeiro</option>
|
||||
{% for militante in militantes %}
|
||||
<option value="{{ militante.id }}" {% if militante.id == celula.responsavel_financas %}selected{% endif %}>{{ militante.nome }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between mt-4">
|
||||
<button type="submit" class="btn btn-primary">Salvar</button>
|
||||
<a href="{{ url_for('listar_celulas') }}" class="btn btn-secondary">Cancelar</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Validação do formulário
|
||||
(function () {
|
||||
'use strict'
|
||||
|
||||
var forms = document.querySelectorAll('.needs-validation')
|
||||
|
||||
Array.prototype.slice.call(forms)
|
||||
.forEach(function (form) {
|
||||
form.addEventListener('submit', function (event) {
|
||||
if (!form.checkValidity()) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
form.classList.add('was-validated')
|
||||
}, false)
|
||||
})
|
||||
})()
|
||||
</script>
|
||||
{% endblock %}
|
||||
94
templates/editar_comite.html
Normal file
94
templates/editar_comite.html
Normal file
@@ -0,0 +1,94 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Editar Comitê Regional{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h1 class="mb-4">Editar Comitê Regional</h1>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="POST" class="needs-validation" novalidate>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="nome" class="form-label">Nome</label>
|
||||
<input type="text" class="form-control" id="nome" name="nome" value="{{ comite.nome }}" required>
|
||||
<div class="invalid-feedback">
|
||||
Por favor, insira o nome do comitê regional.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="comite_central_id" class="form-label">Comitê Central</label>
|
||||
<select class="form-select" id="comite_central_id" name="comite_central_id" required>
|
||||
<option value="">Selecione um comitê central</option>
|
||||
{% for comite_central in comites_centrais %}
|
||||
<option value="{{ comite_central.id }}" {% if comite_central.id == comite.comite_central_id %}selected{% endif %}>{{ comite_central.nome }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div class="invalid-feedback">
|
||||
Por favor, selecione um comitê central.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="responsavel" class="form-label">Responsável</label>
|
||||
<select class="form-select" id="responsavel" name="responsavel">
|
||||
<option value="">Selecione um responsável</option>
|
||||
{% for militante in militantes %}
|
||||
<option value="{{ militante.id }}" {% if militante.id == comite.responsavel %}selected{% endif %}>{{ militante.nome }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="responsavel_financas" class="form-label">Responsável Finanças</label>
|
||||
<select class="form-select" id="responsavel_financas" name="responsavel_financas">
|
||||
<option value="">Selecione um responsável financeiro</option>
|
||||
{% for militante in militantes %}
|
||||
<option value="{{ militante.id }}" {% if militante.id == comite.responsavel_financas %}selected{% endif %}>{{ militante.nome }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between mt-4">
|
||||
<button type="submit" class="btn btn-primary">Salvar</button>
|
||||
<a href="{{ url_for('listar_comites') }}" class="btn btn-secondary">Cancelar</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Validação do formulário
|
||||
(function () {
|
||||
'use strict'
|
||||
|
||||
var forms = document.querySelectorAll('.needs-validation')
|
||||
|
||||
Array.prototype.slice.call(forms)
|
||||
.forEach(function (form) {
|
||||
form.addEventListener('submit', function (event) {
|
||||
if (!form.checkValidity()) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
form.classList.add('was-validated')
|
||||
}, false)
|
||||
})
|
||||
})()
|
||||
</script>
|
||||
{% endblock %}
|
||||
81
templates/editar_comite_central.html
Normal file
81
templates/editar_comite_central.html
Normal file
@@ -0,0 +1,81 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Editar Comitê Central{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h1 class="mb-4">Editar Comitê Central</h1>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="POST" class="needs-validation" novalidate>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="nome" class="form-label">Nome</label>
|
||||
<input type="text" class="form-control" id="nome" name="nome" value="{{ comite.nome }}" required>
|
||||
<div class="invalid-feedback">
|
||||
Por favor, insira o nome do comitê central.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="responsavel" class="form-label">Responsável</label>
|
||||
<select class="form-select" id="responsavel" name="responsavel">
|
||||
<option value="">Selecione um responsável</option>
|
||||
{% for militante in militantes %}
|
||||
<option value="{{ militante.id }}" {% if militante.id == comite.responsavel %}selected{% endif %}>{{ militante.nome }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="responsavel_financas" class="form-label">Responsável Finanças</label>
|
||||
<select class="form-select" id="responsavel_financas" name="responsavel_financas">
|
||||
<option value="">Selecione um responsável financeiro</option>
|
||||
{% for militante in militantes %}
|
||||
<option value="{{ militante.id }}" {% if militante.id == comite.responsavel_financas %}selected{% endif %}>{{ militante.nome }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between mt-4">
|
||||
<button type="submit" class="btn btn-primary">Salvar</button>
|
||||
<a href="{{ url_for('listar_comites_centrais') }}" class="btn btn-secondary">Cancelar</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Validação do formulário
|
||||
(function () {
|
||||
'use strict'
|
||||
|
||||
var forms = document.querySelectorAll('.needs-validation')
|
||||
|
||||
Array.prototype.slice.call(forms)
|
||||
.forEach(function (form) {
|
||||
form.addEventListener('submit', function (event) {
|
||||
if (!form.checkValidity()) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
form.classList.add('was-validated')
|
||||
}, false)
|
||||
})
|
||||
})()
|
||||
</script>
|
||||
{% endblock %}
|
||||
43
templates/editar_comprovante.html
Normal file
43
templates/editar_comprovante.html
Normal 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 %}
|
||||
29
templates/editar_cota.html
Normal file
29
templates/editar_cota.html
Normal file
@@ -0,0 +1,29 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<h2>Editar Cota</h2>
|
||||
<form method="POST" class="needs-validation" novalidate>
|
||||
<div class="mb-3">
|
||||
<label for="valor_novo" class="form-label">Valor</label>
|
||||
<input type="number" step="0.01" class="form-control" id="valor_novo" name="valor_novo" value="{{ cota.valor_novo }}" required>
|
||||
<div class="invalid-feedback">
|
||||
Por favor, insira um valor válido.
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="data_vencimento" class="form-label">Data de Vencimento</label>
|
||||
<input type="date" class="form-control" id="data_vencimento" name="data_vencimento" value="{{ cota.data_vencimento }}" required>
|
||||
<div class="invalid-feedback">
|
||||
Por favor, selecione uma data de vencimento.
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" id="pago" name="pago" value="true" {% if cota.pago %}checked{% endif %}>
|
||||
<label class="form-check-label" for="pago">Pago</label>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Salvar</button>
|
||||
<a href="{{ url_for('listar_cotas') }}" class="btn btn-secondary">Cancelar</a>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user