6 Commits

75 changed files with 7643 additions and 1661 deletions

161
README.md
View File

@@ -1,43 +1,134 @@
# Sistema de Controles
# Sistema de Controle de Militantes
## Para instalar
Sistema para gerenciamento de militantes, células, setores e comitês regionais.
```bash
make install
## Estrutura de Permissões (RBAC)
O sistema utiliza um sistema de controle de acesso baseado em papéis (RBAC) com a seguinte hierarquia:
### Níveis de Papéis
1. **Militante Básico** (Nível 1)
- Visualizar próprios dados
- Editar próprios dados
- Visualizar dados da célula
2. **Secretário de Célula** (Nível 2)
- Todas as permissões do Militante Básico
- Gerenciar membros da célula
- Criar membros na célula
- Visualizar relatórios da célula
3. **Membro de Setor** (Nível 3)
- Todas as permissões do Secretário de Célula
- Visualizar relatórios do setor
4. **Secretário de Setor** (Nível 4)
- Todas as permissões do Membro de Setor
- Gerenciar células do setor
- Criar células no setor
5. **Membro de CR** (Nível 5)
- Todas as permissões do Secretário de Setor
- Visualizar relatórios do CR
6. **Secretário de CR** (Nível 6)
- Todas as permissões do Membro de CR
- Gerenciar setores do CR
- Criar setores no CR
7. **Membro do CC** (Nível 7)
- Todas as permissões do Secretário de CR
- Visualizar relatórios nacionais
8. **Secretário Geral** (Nível 8)
- Todas as permissões do Membro do CC
- Gerenciar CRs
- Criar CRs
- Configurar sistema
## Instalação
1. Clone o repositório
2. Crie um ambiente virtual:
```bash
python -m venv venv
source venv/bin/activate # Linux/Mac
# ou
venv\Scripts\activate # Windows
```
3. Instale as dependências:
```bash
pip install -r requirements.txt
```
4. Execute as migrações do banco de dados:
```bash
python sql/migrate_db.py
```
5. Configure as variáveis de ambiente no arquivo `.env`:
```
FLASK_APP=app.py
FLASK_ENV=development
SECRET_KEY=sua_chave_secreta
MAIL_SERVER=seu_servidor_smtp
MAIL_PORT=587
MAIL_USE_TLS=True
MAIL_USERNAME=seu_email
MAIL_PASSWORD=sua_senha
```
6. Execute o aplicativo:
```bash
flask run
```
## Uso
### Decoradores de Permissão
O sistema fornece três decoradores para controle de acesso:
1. `@require_permission(permission_name)`
- Verifica se o usuário tem uma permissão específica
- Exemplo: `@require_permission('create_cell_member')`
2. `@require_role(role_name)`
- Verifica se o usuário tem um papel específico
- Exemplo: `@require_role('Secretário de Célula')`
3. `@require_minimum_role(min_level)`
- Verifica se o usuário tem um papel com nível mínimo
- Exemplo: `@require_minimum_role(Role.SECRETARIO_CR)`
### Verificando Permissões no Código
```python
# Verificar se um usuário tem uma permissão
if user.has_permission('create_cell_member'):
# Faça algo
# Verificar se um usuário tem um papel
if user.has_role('Secretário de Célula'):
# Faça algo
# Obter o papel mais alto do usuário
highest_role = user.get_highest_role()
if highest_role and highest_role.nivel >= Role.SECRETARIO_CR:
# Faça algo
```
## Sobre o QR Code de Autenticação (admin_qr.png)
## Estrutura do Banco de Dados
O arquivo `admin_qr.png` é um QR code gerado automaticamente para configuração da autenticação de dois fatores (2FA) do usuário administrador. Este arquivo é:
O sistema utiliza as seguintes tabelas para o RBAC:
- Gerado na raiz do projeto quando:
- O comando `make reset-admin` é executado
- O servidor é iniciado com `make run` e existe um usuário admin
- Um novo usuário é criado através da interface web
- `roles`: Armazena os papéis disponíveis
- `permissions`: Armazena as permissões disponíveis
- `role_permissions`: Mapeia papéis para permissões
- `user_roles`: Mapeia usuários para papéis
- Usado para:
- Configurar a autenticação 2FA no aplicativo autenticador (Google Authenticator, Microsoft Authenticator, etc.)
- Gerar os códigos OTP necessários para fazer login no sistema
## Segurança
### Importante:
- O QR code é atualizado sempre que um novo usuário é criado ou quando o sistema é reiniciado
- Cada QR code é único e corresponde ao segredo OTP atual do usuário
- Se você recriar o banco de dados ou resetar o admin, será necessário reconfigurar o aplicativo autenticador com o novo QR code
- Mantenha este arquivo seguro, pois ele contém informações sensíveis de autenticação
### Como usar:
1. Instale um aplicativo autenticador no seu celular (Google Authenticator, Microsoft Authenticator, etc.)
2. Escaneie o QR code (`admin_qr.png`) com o aplicativo
3. O aplicativo irá gerar códigos de 6 dígitos a cada 30 segundos
4. Use estes códigos junto com seu usuário e senha para fazer login no sistema
### Comandos relacionados:
```bash
# Limpar banco e criar novo admin com novo QR code
make reset-admin
# Iniciar o servidor (também gera novo QR code se necessário)
make run
```
Acesse por: http://127.0.0.1:5000
- Todas as senhas são armazenadas com hash bcrypt
- Sessões expiram após período de inatividade
- Controle de acesso granular baseado em papéis
- Proteção contra CSRF
- Validação de entrada de dados

1
admin_qr.txt Normal file
View File

@@ -0,0 +1 @@
otpauth://totp/Sistema%20de%20Controles:admin?secret=27NESPSPWKWIXVIDBUJPTK7MPAKGF4WG&issuer=Sistema%20de%20Controles

1232
app.py

File diff suppressed because it is too large Load Diff

View File

@@ -1 +0,0 @@
SECRET_KEY = 'sua_chave_secreta_aqui' # Use uma chave segura em produção

View File

@@ -1,136 +1,91 @@
from functions.database import init_database, Usuario, get_db_connection
import qrcode
import os
from pathlib import Path
from functions.database import get_db_connection, Usuario
from functions.rbac import Role
import pyotp
import qrcode
import base64
from io import BytesIO
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
"""
import qrcode
# 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)
qr.add_data(user.get_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
def create_admin_user():
def create_admin():
"""Cria o usuário admin se não existir"""
db = get_db_connection()
try:
# Inicializar o banco de dados
init_database()
# Obter a sessão
db_session = get_db_connection()
# Verificar se o admin foi criado
admin = db_session.query(Usuario).filter_by(username="admin").first()
# Verificar se o admin já existe
admin = db.query(Usuario).filter_by(username='admin').first()
if admin:
print("\n=== Usuário Admin Encontrado ===")
# Atualizar QR code mesmo se o usuário já existir
qr_path = generate_qr_code(admin)
print("Usuário admin já existe")
# Verificar se o arquivo admin_qr.png existe
if os.path.exists('admin_qr.png'):
print("Usando OTP existente do arquivo admin_qr.png")
# Extrair o OTP secret do QR code existente
with open('admin_qr.png', 'rb') as f:
qr_data = f.read()
# Aqui você precisaria implementar a lógica para extrair o OTP secret do QR code
# Por enquanto, vamos apenas manter o OTP existente
return
else:
print("Gerando novo OTP para o admin...")
# Gerar novo OTP
otp_secret = pyotp.random_base32()
admin.otp_secret = otp_secret
db.commit()
else:
print("\n=== Criando Novo Usuário Admin ===")
# Criar usuário admin com novo segredo OTP
print("Criando usuário admin...")
# Criar usuário admin
admin = Usuario(
username="admin",
password="admin123",
username='admin',
password='admin123',
is_admin=True
)
admin.email = "admin@example.com"
admin.email = 'admin@controles.com'
db.add(admin)
db.commit()
# Adicionar e fazer commit para obter o ID
db_session.add(admin)
db_session.commit()
# Gerar OTP
otp_secret = pyotp.random_base32()
admin.otp_secret = otp_secret
db.commit()
# Recarregar o usuário do banco para garantir dados atualizados
admin = db_session.query(Usuario).filter_by(username="admin").first()
# Atribuir role de Secretário Geral
admin_role = db.query(Role).filter_by(nivel=Role.SECRETARIO_GERAL).first()
if admin_role:
admin.roles.append(admin_role)
db.commit()
# Verificar e mostrar informações do OTP
print("\n=== Informações do Usuário Admin ===")
print(f"Username: admin")
print(f"Senha: admin123")
print(f"Email: {admin.email}")
print(f"Segredo OTP atual: {admin.otp_secret}")
# Gerar QR code
totp = pyotp.TOTP(otp_secret)
provisioning_uri = totp.provisioning_uri(admin.username, issuer_name="Sistema de Controles")
# Gerar URI do OTP usando o segredo armazenado
totp = pyotp.TOTP(admin.otp_secret)
otp_uri = totp.provisioning_uri(
name=admin.username,
issuer_name="Sistema de Controles"
)
qr = qrcode.QRCode(version=1, box_size=10, border=5)
qr.add_data(provisioning_uri)
qr.make(fit=True)
# Usar a função extraída para gerar o QR code
qr_path = generate_qr_code(admin)
img = qr.make_image(fill_color="black", back_color="white")
print("\n=== QR Code Gerado ===")
print(f"QR Code salvo em: {qr_path}")
print(f"URI do OTP: {otp_uri}")
# Salvar QR code como base64
buffered = BytesIO()
img.save(buffered, format="PNG")
qr_base64 = base64.b64encode(buffered.getvalue()).decode()
# Gerar código atual para verificação
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)}")
# Salvar QR code como arquivo
img.save('admin_qr.png')
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.")
print("\nConfiguração do OTP para o admin:")
print(f"OTP Secret: {otp_secret}")
print("\nInstruções:")
print("1. Use um aplicativo autenticador (como Google Authenticator ou Authy)")
print("2. Escaneie o QR code ou insira o OTP Secret manualmente")
print("3. Use o código gerado para fazer login")
print("\nQR code salvo em 'admin_qr.png'")
except Exception as e:
print(f"\nErro durante a execução: {e}")
import traceback
traceback.print_exc()
print(f"Erro ao criar admin: {str(e)}")
db.rollback()
raise
finally:
if 'db_session' in locals():
db_session.close()
db.close()
if __name__ == "__main__":
# Remover banco de dados existente para começar limpo
db_path = Path.home() / '.local' / 'share' / 'controles' / 'database.db'
if db_path.exists():
os.remove(db_path)
print("Banco de dados antigo removido.")
create_admin_user()
if __name__ == '__main__':
create_admin()

104
create_test_users.py Normal file
View File

@@ -0,0 +1,104 @@
from functions.database import get_db_connection, Usuario
from functions.rbac import Role
import pyotp
import qrcode
import os
import base64
from io import BytesIO
def create_test_users():
"""Cria usuários de teste se não existirem"""
db = get_db_connection()
try:
# Usuários de teste
test_users = [
{
'username': 'teste',
'password': 'admin123', # Mesma senha do admin
'email': 'teste@controles.com',
'is_admin': True
},
{
'username': 'aligner',
'password': 'Test123!@#',
'email': 'aligner@controles.com',
'is_admin': False
},
{
'username': 'tester',
'password': 'Test123!@#',
'email': 'tester@controles.com',
'is_admin': False
},
{
'username': 'deployer',
'password': 'Test123!@#',
'email': 'deployer@controles.com',
'is_admin': False
}
]
# Obter o OTP secret do admin se existir
admin = db.query(Usuario).filter_by(username='admin').first()
admin_otp_secret = admin.otp_secret if admin else None
for user_data in test_users:
# Verificar se o usuário já existe
user = db.query(Usuario).filter_by(username=user_data['username']).first()
if not user:
print(f"Criando usuário {user_data['username']}...")
# Criar usuário
user = Usuario(
username=user_data['username'],
password=user_data['password'],
is_admin=user_data['is_admin']
)
user.email = user_data['email']
db.add(user)
db.commit()
# Se for o usuário teste, usar o mesmo OTP do admin
if user_data['username'] == 'teste' and admin_otp_secret:
user.otp_secret = admin_otp_secret
db.commit()
else:
# Gerar novo OTP para outros usuários
otp_secret = pyotp.random_base32()
user.otp_secret = otp_secret
db.commit()
# Atribuir role de Secretário Geral para o usuário teste
if user_data['username'] == 'teste':
admin_role = db.query(Role).filter_by(nivel=Role.SECRETARIO_GERAL).first()
if admin_role:
user.roles.append(admin_role)
db.commit()
print(f"Usuário {user_data['username']} criado com sucesso!")
else:
print(f"Usuário {user_data['username']} já existe")
# Se for o usuário teste e não tiver o OTP do admin, atualizar
if user_data['username'] == 'teste' and admin_otp_secret and user.otp_secret != admin_otp_secret:
user.otp_secret = admin_otp_secret
db.commit()
print(f"OTP do usuário teste atualizado para o mesmo do admin")
# Verificar se o usuário teste tem a role de Secretário Geral
if user_data['username'] == 'teste':
admin_role = db.query(Role).filter_by(nivel=Role.SECRETARIO_GERAL).first()
if admin_role and admin_role not in user.roles:
user.roles.append(admin_role)
db.commit()
print(f"Role de Secretário Geral atribuída ao usuário teste")
except Exception as e:
print(f"Erro ao criar usuários de teste: {str(e)}")
db.rollback()
raise
finally:
db.close()
if __name__ == '__main__':
create_test_users()

8
dao.py
View File

@@ -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

View File

@@ -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)
);

165
docs/README.md Normal file
View 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

239
docs/rbac.md Normal file
View File

@@ -0,0 +1,239 @@
# 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
### 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
### 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
### 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
## 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_PAYMENT`: Registrar pagamentos da célula
- **Tesoureiro(a)**:
- `VIEW_CELL_DATA`: Visualizar dados da célula
- `VIEW_CELL_REPORTS`: Visualizar relatórios da célula
- `REGISTER_CELL_PAYMENT`: Registrar pagamentos 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_PAYMENT`: Registrar pagamentos do setor
- **Tesoureiro(a)**:
- `VIEW_SECTOR_REPORTS`: Visualizar relatórios do setor
- `REGISTER_SECTOR_PAYMENT`: Registrar pagamentos do setor
### CR
- **Secretário(a)**:
- `MANAGE_CR_SECTORS`: Gerenciar setores do CR
- `VIEW_CR_REPORTS`: Visualizar relatórios do CR
- `REGISTER_CR_PAYMENT`: Registrar pagamentos do CR
- **Tesoureiro(a)**:
- `VIEW_CR_REPORTS`: Visualizar relatórios do CR
- `REGISTER_CR_PAYMENT`: Registrar pagamentos do CR
### CC
- **Secretário(a)**:
- `MANAGE_CC_CRS`: Gerenciar CRs
- `VIEW_CC_REPORTS`: Visualizar relatórios do CC
- `REGISTER_CC_PAYMENT`: Registrar pagamentos do CC
- `SYSTEM_CONFIG`: Configurar o sistema
- **Tesoureiro(a)**:
- `VIEW_CC_REPORTS`: Visualizar relatórios do CC
- `REGISTER_CC_PAYMENT`: Registrar pagamentos 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 Pagamentos**:
- Apenas tesoureiros e secretários podem registrar pagamentos
- O registro é restrito à instância do usuário
- O CC pode registrar pagamentos 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

21
functions/base.py Normal file
View File

@@ -0,0 +1,21 @@
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
import os
# Configuração do banco de dados
DATABASE_URL = os.getenv('DATABASE_URL', 'sqlite:///database.db')
engine = create_engine(DATABASE_URL)
Session = sessionmaker(bind=engine)
# Base declarativa do SQLAlchemy
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

View File

@@ -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;
}
}

View File

@@ -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")

View File

@@ -1,35 +1,47 @@
from sqlalchemy import create_engine, Column, Integer, String, Boolean, Numeric, Date, ForeignKey, DateTime
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
import pyotp
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text, Numeric, Date, Enum
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
# 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 engine com NullPool
engine = create_engine(
f'sqlite:///{db_path}',
echo=True,
poolclass=NullPool # Usar NullPool ao invés do pool padrão
)
Base = declarative_base()
SessionLocal = sessionmaker(bind=engine)
def get_db_connection():
"""
Retorna uma nova sessão do banco de dados
Retorna uma nova sessão do banco de dados SQLite e verifica timeout
"""
session = SessionLocal()
try:
return SessionLocal()
finally:
engine.dispose()
# Verificar timeout para usuários logados
usuario_atual = session.query(Usuario).filter(
Usuario.ultimo_login.isnot(None),
Usuario.ultimo_logout.is_(None)
).first()
if usuario_atual and usuario_atual.check_session_timeout():
usuario_atual.logout()
session.commit()
raise Exception("Sessão expirada. Por favor, faça login novamente.")
return session
except Exception as e:
session.close()
raise e
def execute_query(query, params=None):
"""
@@ -46,22 +58,235 @@ def execute_query(query, params=None):
finally:
session.close()
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'))
cr_id = Column(Integer, ForeignKey('comites_regionais.id'))
secretario = Column(Integer, ForeignKey('militantes.id'))
responsavel_financas = Column(Integer, ForeignKey('militantes.id'))
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'))
responsavel_formacao = Column(Integer, ForeignKey('militantes.id'))
secretario_organizacao = Column(Integer, ForeignKey('militantes.id'))
correspondente_jornal = Column(Integer, ForeignKey('militantes.id'))
# 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'))
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'))
# Campos existentes
celula_id = Column(Integer, ForeignKey('celulas.id'))
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)
# 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")
celula = relationship("Celula", back_populates="militantes", foreign_keys=[celula_id])
# 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'
@@ -80,19 +305,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'
@@ -144,12 +372,20 @@ 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'))
responsavel = Column(Integer, ForeignKey('militantes.id'))
responsavel_financas = Column(Integer, ForeignKey('militantes.id'))
# 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")
usuarios = relationship("Usuario", back_populates="setor")
class ComiteCentral(Base):
__tablename__ = 'comites_centrais'
@@ -184,10 +420,16 @@ class RelatorioVendasMateriais(Base):
setor = relationship("Setor", back_populates="relatorios_vendas")
comite = relationship("ComiteCentral", back_populates="relatorios_vendas")
class Usuario(Base):
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, autoincrement=True)
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)
@@ -199,83 +441,193 @@ class Usuario(Base):
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))
role = relationship("Role", back_populates="usuarios")
setor = relationship("Setor", back_populates="usuarios")
# 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, password, is_admin=False):
def get_id(self):
return str(self.id)
@property
def is_authenticated(self):
return True
@property
def is_active(self):
return self.ativo
@property
def is_anonymous(self):
return False
def __init__(self, username, password, is_admin=False, email=None, tipo="USUARIO"):
self.username = username
self.password_hash = generate_password_hash(password)
self.is_admin = is_admin
self.otp_secret = pyotp.random_base32() # Gerar segredo OTP na criação
self.email = email
self.ativo = True
self.session_timeout = 30
self.tipo = tipo
self.ultima_atividade = datetime.utcnow()
def check_password(self, password):
return check_password_hash(self.password_hash, password)
def verify_otp(self, otp_code):
"""Verifica se o código OTP fornecido é válido"""
if not self.otp_secret:
print(f"Erro: Usuário {self.username} não tem segredo OTP configurado")
return False
def update_last_activity(self):
self.ultima_atividade = datetime.utcnow()
totp = pyotp.TOTP(self.otp_secret)
is_valid = totp.verify(otp_code)
print(f"Verificando OTP para {self.username}")
print(f"Segredo: {self.otp_secret}")
print(f"Código fornecido: {otp_code}")
print(f"Resultado: {'válido' if is_valid else 'inválido'}")
return is_valid
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 determinada permissão"""
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 o QR code do OTP"""
"""Gera a URI para autenticação em duas etapas"""
if not self.otp_secret:
self.otp_secret = pyotp.random_base32()
totp = pyotp.TOTP(self.otp_secret)
return totp.provisioning_uri(
name=self.username,
return pyotp.totp.TOTP(self.otp_secret).provisioning_uri(
self.username,
issuer_name="Sistema de Controles"
)
class Role(Base):
__tablename__ = 'roles'
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
class PagamentoCelula(Base):
__tablename__ = 'pagamentos_celula'
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 (1: admin, 2: coordenador, 3: militante)
usuarios = relationship("Usuario", back_populates="role")
permissoes = relationship("RolePermissao", back_populates="role")
class Permissao(Base):
__tablename__ = 'permissoes'
id = Column(Integer, primary_key=True, autoincrement=True)
nome = Column(String(50), unique=True, nullable=False)
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'))
roles = relationship("RolePermissao", back_populates="permissao")
celula = relationship("Celula", back_populates="pagamentos")
registrado_por_rel = relationship("Militante", foreign_keys=[registrado_por])
class RolePermissao(Base):
__tablename__ = 'roles_permissoes'
class Atividade(Base):
__tablename__ = 'atividades'
role_id = Column(Integer, ForeignKey('roles.id'), primary_key=True)
permissao_id = Column(Integer, ForeignKey('permissoes.id'), primary_key=True)
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'))
role = relationship("Role", back_populates="permissoes")
permissao = relationship("Permissao", back_populates="roles")
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'))
pagamento = relationship("Pagamento", back_populates="transacoes_pix")
# Remover o banco de dados existente (se existir)
if os.path.exists(db_path):
os.remove(db_path)
def init_database():
"""Inicializa o banco de dados com dados básicos"""
print("Inicializando banco de dados...")
# Criar todas as tabelas
Base.metadata.create_all(engine)
def init_rbac():
"""Inicializa o sistema RBAC"""
print("Inicializando sistema RBAC...")
session = SessionLocal()
try:
@@ -287,7 +639,7 @@ def init_database():
# Criar role de admin
admin_role = session.query(Role).filter_by(nome="Administrador").first()
if not admin_role:
admin_role = Role(nome="Administrador", nivel=1)
admin_role = Role(nome="Administrador", nivel=Role.SECRETARIO_GERAL)
session.add(admin_role)
session.commit()
@@ -301,6 +653,11 @@ def init_database():
admin.email = "admin@example.com"
admin.role_id = admin_role.id
# Adicionar apenas a permissão de system_config ao admin
permission = session.query(Permission).filter_by(nome='system_config').first()
if permission and permission not in admin_role.permissions:
admin_role.permissions.append(permission)
session.add(admin)
session.commit()
@@ -311,6 +668,99 @@ def init_database():
print(f"OTP Secret: {admin.otp_secret}")
else:
print("Usuário admin já existe")
# Garantir que o admin tenha apenas a permissão de system_config
admin_role = session.query(Role).filter_by(nome="Administrador").first()
if admin_role:
# Remover todas as permissões atuais
admin_role.permissions = []
# Adicionar apenas a permissão de system_config
permission = session.query(Permission).filter_by(nome='system_config').first()
if permission:
admin_role.permissions.append(permission)
session.commit()
except Exception as e:
print(f"Erro na inicialização do sistema RBAC: {e}")
session.rollback()
raise
finally:
session.close()
def init_database():
"""Inicializa o banco de dados com dados básicos"""
print("Inicializando banco de dados...")
# Criar todas as tabelas
Base.metadata.drop_all(engine) # Remover todas as tabelas existentes
Base.metadata.create_all(engine)
session = SessionLocal()
try:
# Criar role de administrador
admin_role = Role(nome="Administrador", nivel=Role.SECRETARIO_GERAL)
session.add(admin_role)
session.commit()
# Verificar se existe um QR code salvo
qr_path = Path('admin_qr.png')
admin_otp_secret = None
if qr_path.exists():
# Extrair o segredo OTP do nome do arquivo temporário dentro do QR
try:
import re
with open('admin_qr.txt', 'r') as f:
qr_content = f.read()
# O segredo OTP está no formato otpauth://totp/admin?secret=XXXXX&issuer=Sistema%20de%20Controles
match = re.search(r'secret=([A-Z0-9]+)&', qr_content)
if match:
admin_otp_secret = match.group(1)
print(f"Usando OTP existente: {admin_otp_secret}")
except Exception as e:
print(f"Erro ao ler OTP existente: {e}")
if not admin_otp_secret:
admin_otp_secret = pyotp.random_base32()
print(f"Novo OTP gerado: {admin_otp_secret}")
# Criar usuário admin
admin = Usuario(
username="admin",
password="admin123",
is_admin=True,
email="admin@example.com",
tipo="ADMIN"
)
admin.role_id = admin_role.id
admin.otp_secret = admin_otp_secret
session.add(admin)
session.commit()
# Gerar novo QR code se não existir
if not qr_path.exists():
totp = pyotp.totp.TOTP(admin_otp_secret)
provisioning_uri = totp.provisioning_uri("admin", issuer_name="Sistema de Controles")
# Salvar a URI em um arquivo texto para referência futura
with open('admin_qr.txt', 'w') as f:
f.write(provisioning_uri)
# Gerar QR code
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: {qr_path}")
except Exception as e:
print(f"Erro na inicialização do banco: {e}")
@@ -319,6 +769,9 @@ def init_database():
finally:
session.close()
# Inicializar o sistema RBAC
init_rbac()
# Inicializar o banco de dados automaticamente quando o módulo for importado
init_database()

168
functions/decorators.py Normal file
View File

@@ -0,0 +1,168 @@
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
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.', 'error')
return redirect(url_for('login'))
db = get_db_connection()
try:
# Carregar o usuário com suas roles
user = db.query(Usuario).options(
joinedload(Usuario.roles)
).get(current_user.id)
if not user:
flash('Usuário não encontrado.', 'error')
return redirect(url_for('login'))
# Atualiza timestamp da última atividade
user.update_last_activity()
db.commit()
return f(*args, **kwargs)
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('Por favor, faça login para acessar esta página.', 'error')
return redirect(url_for('login'))
if not current_user.has_permission(permission_name):
flash('Você não tem permissão para acessar esta página.', 'error')
return redirect(url_for('home'))
return f(*args, **kwargs)
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

222
functions/permissions.py Normal file
View 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

292
functions/rbac.py Normal file
View File

@@ -0,0 +1,292 @@
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"
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.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 get_db_connection
session = get_db_connection()
try:
# Criar roles se não existirem
for nivel, nome in Role.get_roles_list():
role = session.query(Role).filter_by(nivel=nivel).first()
if not role:
role = Role(nome=nome, nivel=nivel)
session.add(role)
# Criar permissões se não existirem
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()
# Mapear permissões para roles
for role in session.query(Role).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.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.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.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.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.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.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.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()
]
# Administrador
elif role.nome == "Administrador":
role.permissions = [
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()

58
init_system.py Normal file
View 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()

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

View File

@@ -1,14 +1,16 @@
Flask==3.0.2
Flask-SQLAlchemy==3.1.1
SQLAlchemy==2.0.39
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==11.0.0
python-dotenv==1.0.1
flask-login==0.6.3
flask-wtf==1.2.1
Pillow==10.2.0
email-validator==2.1.0.post1
Bootstrap-Flask==2.4.1
cryptography==42.0.2
bcrypt==4.1.2
Bootstrap-Flask==2.3.3
flask-bootstrap5==0.1.dev1

18
setup.py Normal file
View File

@@ -0,0 +1,18 @@
from setuptools import setup, find_packages
setup(
name="controles",
version="0.1.0",
packages=find_packages(),
install_requires=[
"fastapi",
"uvicorn",
"sqlalchemy",
"python-jose[cryptography]",
"passlib[bcrypt]",
"python-multipart",
"qrcode",
"pillow",
"python-dotenv"
],
)

66
sql/migrate_db.py Normal file
View 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
View 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
View 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');

View 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 %}

View File

@@ -1,67 +1,156 @@
<!DOCTYPE html>
<html lang="pt-br">
<html lang="pt-BR">
<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() }}
<title>{% block title %}{% endblock %} - Sistema de Controle OCI</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" rel="stylesheet">
<style>
body {
padding-top: 56px;
}
.navbar-brand {
font-weight: bold;
}
.nav-link {
font-weight: 500;
}
.card {
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.card-header {
background-color: #f8f9fa;
border-bottom: 1px solid #dee2e6;
}
.btn-primary {
background-color: #0d6efd;
border-color: #0d6efd;
}
.btn-success {
background-color: #198754;
border-color: #198754;
}
.btn-secondary {
background-color: #6c757d;
border-color: #6c757d;
}
.btn-outline-primary {
color: #0d6efd;
border-color: #0d6efd;
}
.btn-outline-primary:hover {
background-color: #0d6efd;
color: #fff;
}
.alert {
border-radius: 0.25rem;
}
.form-control:focus {
border-color: #0d6efd;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
}
.form-select:focus {
border-color: #0d6efd;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
}
</style>
</head>
<body>
{% if 'user_id' in session %}
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
<div class="container">
<a class="navbar-brand" href="{{ url_for('home') }}">Sistema de Gestão</a>
<a class="navbar-brand" href="{{ url_for('home') }}">Sistema de Controle OCI</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<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>
{% if current_user is defined and current_user.is_authenticated %}
{% if current_user.is_admin %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('dashboard_admin') }}">Dashboard Admin</a>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('home') }}">Início</a>
</li>
{% if current_user.has_permission('view_cell_data') %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('listar_militantes') }}">Militantes</a>
</li>
{% endif %}
{% if current_user.has_permission('view_cell_reports') %}
<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>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('listar_relatorios_vendas') }}">Vendas</a>
</li>
{% endif %}
{% if current_user.has_permission('view_cell_reports') or current_user.has_permission('view_sector_reports') or current_user.has_permission('view_cr_reports') %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown">
Relatórios
</a>
<ul class="dropdown-menu">
{% if current_user.has_permission('view_cell_reports') %}
<li><a class="dropdown-item" href="{{ url_for('listar_relatorios_cotas') }}">Relatórios de Cotas</a></li>
<li><a class="dropdown-item" href="{{ url_for('listar_relatorios_vendas') }}">Relatórios de Vendas</a></li>
{% endif %}
</ul>
</li>
{% endif %}
{% endif %}
{% endif %}
</ul>
<ul class="navbar-nav">
{% if current_user is defined and current_user.is_authenticated %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('logout') }}">
<i class="fas fa-sign-out-alt"></i> Sair
</a>
<a class="nav-link" href="{{ url_for('logout') }}">Sair</a>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('login') }}">Login</a>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
{% endif %}
<div class="container mt-4">
{% block content %}{% endblock %}
</div>
{{ bootstrap.load_js() }}
<!-- Adicionar Font Awesome para o ícone de Sair -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
{% if 'user_id' in session %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Verificar a sessão a cada minuto
setInterval(function() {
// Verificar status da sessão a cada 5 minutos
function checkSession() {
fetch('/check_session')
.then(response => response.json())
.then(data => {
if (data.expired) {
window.location.href = '/login';
}
});
}, 60000);
})
.catch(error => console.error('Erro ao verificar sessão:', error));
}
// Verificar a cada 5 minutos
setInterval(checkSession, 5 * 60 * 1000);
// Verificar também quando a página ganha foco
document.addEventListener('visibilitychange', function() {
if (document.visibilityState === 'visible') {
checkSession();
}
});
</script>
{% endif %}
</body>
</html>

View 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 %}

View File

@@ -0,0 +1,106 @@
{% 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>
<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
View 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 %}

View File

@@ -0,0 +1,83 @@
{% 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 mb-4">
<div class="card-header">
<h5 class="card-title mb-0">Gerenciamento de Usuários</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>Admin</th>
<th>OTP Configurado</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{% for usuario in usuarios %}
<tr>
<td>{{ usuario.id }}</td>
<td>{{ usuario.username }}</td>
<td>{{ usuario.email }}</td>
<td>
{% if usuario.is_admin %}
<span class="badge bg-success">Sim</span>
{% else %}
<span class="badge bg-secondary">Não</span>
{% endif %}
</td>
<td>
{% if usuario.otp_secret %}
<span class="badge bg-success">Sim</span>
{% else %}
<span class="badge bg-danger">Não</span>
{% endif %}
</td>
<td>
<form action="{{ url_for('reset_otp', user_id=usuario.id) }}" method="POST" class="d-inline">
<button type="submit" class="btn btn-warning btn-sm"
onclick="return confirm('Tem certeza que deseja resetar o OTP deste usuário?')">
Resetar OTP
</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Ações Rápidas</h5>
</div>
<div class="card-body">
<div class="d-grid gap-2">
<a href="{{ url_for('novo_usuario') }}" class="btn btn-primary">
Criar Novo Usuário
</a>
</div>
</div>
</div>
</div>
{% endblock %}

View 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 %}

View 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 %}

View 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 %}

View File

@@ -0,0 +1,111 @@
{% extends 'base.html' %}
{% block title %}Editar {{ tipo_instancia }}{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Editar {{ 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" value="{{ instancia.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 }}" {% if superior.id == instancia.instancia_superior_id %}selected{% endif %}>{{ 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 }}" {% if militante.id == instancia.responsavel_geral_id %}selected{% endif %}>{{ 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 }}" {% if militante.id == instancia.responsavel_financas_id %}selected{% endif %}>{{ 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 }}" {% if militante.id == instancia.responsavel_imprensa_id %}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_' + 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 %}

View File

@@ -0,0 +1,94 @@
{% extends 'base.html' %}
{% block title %}Editar Material{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Editar Material</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="mb-3">
<label for="nome" class="form-label">Nome</label>
<input type="text" class="form-control" id="nome" name="nome" value="{{ material.nome }}" required>
<div class="invalid-feedback">
Por favor, insira o nome do material.
</div>
</div>
<div class="mb-3">
<label for="descricao" class="form-label">Descrição</label>
<textarea class="form-control" id="descricao" name="descricao" rows="3" required>{{ material.descricao }}</textarea>
<div class="invalid-feedback">
Por favor, insira a descrição do material.
</div>
</div>
<div class="mb-3">
<label for="preco" class="form-label">Preço</label>
<input type="number" class="form-control" id="preco" name="preco" step="0.01" value="{{ material.preco }}" required>
<div class="invalid-feedback">
Por favor, insira o preço do material.
</div>
</div>
<div class="mb-3">
<label for="quantidade" class="form-label">Quantidade</label>
<input type="number" class="form-control" id="quantidade" name="quantidade" value="{{ material.quantidade }}" required>
<div class="invalid-feedback">
Por favor, insira a quantidade do material.
</div>
</div>
<div class="mb-3">
<label for="tipo_id" class="form-label">Tipo de Material</label>
<select class="form-select" id="tipo_id" name="tipo_id" required>
<option value="">Selecione um tipo</option>
{% for tipo in tipos %}
<option value="{{ tipo.id }}" {% if tipo.id == material.tipo_id %}selected{% endif %}>{{ tipo.nome }}</option>
{% endfor %}
</select>
<div class="invalid-feedback">
Por favor, selecione o tipo do material.
</div>
</div>
<div class="d-flex justify-content-between">
<button type="submit" class="btn btn-success">Salvar</button>
<a href="{{ url_for('listar_materiais') }}" class="btn btn-outline-secondary">Voltar</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 %}

View File

@@ -3,32 +3,227 @@
{% block title %}Editar Militante{% endblock %}
{% block content %}
<h1>Editar Militante</h1>
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Editar 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 %}
{% 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">
Nome: <input type="text" name="nome" required
value="{{ militante.nome }}"><br>
CPF: <input type="text" name="cpf" required
value="{{ militante.cpf }}"
pattern="\d{3}\.?\d{3}\.?\d{3}-?\d{2}"
title="Digite um CPF no formato: xxx.xxx.xxx-xx"><br>
Email: <input type="email" name="email" required
value="{{ militante.email }}"><br>
Telefone: <input type="text" name="telefone"
value="{{ militante.telefone }}"><br>
Endereço: <input type="text" name="endereco"
value="{{ militante.endereco }}"><br>
Filiado: <input type="checkbox" name="filiado"
{% if militante.filiado %}checked{% endif %}><br>
<input type="submit" value="Salvar" class="btn btn-primary">
<a href="{{ url_for('listar_militantes') }}" class="btn btn-secondary">Cancelar</a>
</form>
<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="{{ militante.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" value="{{ militante.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="cpf" class="form-label">CPF</label>
<input type="text" class="form-control" id="cpf" name="cpf" value="{{ militante.cpf }}" required>
<div class="invalid-feedback">
Por favor, insira o CPF do militante.
</div>
</div>
<div class="col-md-6 mb-3">
<label for="titulo_eleitoral" class="form-label">Título Eleitoral</label>
<input type="text" class="form-control" id="titulo_eleitoral" name="titulo_eleitoral" value="{{ militante.titulo_eleitoral }}">
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="data_nascimento" class="form-label">Data de Nascimento</label>
<input type="date" class="form-control" id="data_nascimento" name="data_nascimento" value="{{ militante.data_nascimento.strftime('%Y-%m-%d') }}">
</div>
<div class="col-md-6 mb-3">
<label for="data_entrada_oci" class="form-label">Data de Entrada na OCI</label>
<input type="date" class="form-control" id="data_entrada_oci" name="data_entrada_oci" value="{{ militante.data_entrada_oci.strftime('%Y-%m-%d') }}">
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="data_efetivacao_oci" class="form-label">Data de Efetivação na OCI</label>
<input type="date" class="form-control" id="data_efetivacao_oci" name="data_efetivacao_oci" value="{{ militante.data_efetivacao_oci.strftime('%Y-%m-%d') }}">
</div>
<div class="col-md-6 mb-3">
<label for="telefone1" class="form-label">Telefone 1</label>
<input type="text" class="form-control" id="telefone1" name="telefone1" value="{{ militante.telefone1 }}">
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="telefone2" class="form-label">Telefone 2</label>
<input type="text" class="form-control" id="telefone2" name="telefone2" value="{{ militante.telefone2 }}">
</div>
<div class="col-md-6 mb-3">
<label for="profissao" class="form-label">Profissão</label>
<input type="text" class="form-control" id="profissao" name="profissao" value="{{ militante.profissao }}">
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="regime_trabalho" class="form-label">Regime de Trabalho</label>
<input type="text" class="form-control" id="regime_trabalho" name="regime_trabalho" value="{{ militante.regime_trabalho }}">
</div>
<div class="col-md-6 mb-3">
<label for="empresa" class="form-label">Empresa</label>
<input type="text" class="form-control" id="empresa" name="empresa" value="{{ militante.empresa }}">
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="contratante" class="form-label">Contratante</label>
<input type="text" class="form-control" id="contratante" name="contratante" value="{{ militante.contratante }}">
</div>
<div class="col-md-6 mb-3">
<label for="instituicao_ensino" class="form-label">Instituição de Ensino</label>
<input type="text" class="form-control" id="instituicao_ensino" name="instituicao_ensino" value="{{ militante.instituicao_ensino }}">
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="tipo_instituicao" class="form-label">Tipo de Instituição</label>
<input type="text" class="form-control" id="tipo_instituicao" name="tipo_instituicao" value="{{ militante.tipo_instituicao }}">
</div>
<div class="col-md-6 mb-3">
<label for="sindicato" class="form-label">Sindicato</label>
<input type="text" class="form-control" id="sindicato" name="sindicato" value="{{ militante.sindicato }}">
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="cargo_sindical" class="form-label">Cargo Sindical</label>
<input type="text" class="form-control" id="cargo_sindical" name="cargo_sindical" value="{{ militante.cargo_sindical }}">
</div>
<div class="col-md-6 mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="dirigente_sindical" name="dirigente_sindical" {% if militante.dirigente_sindical %}checked{% endif %}>
<label class="form-check-label" for="dirigente_sindical">
Dirigente Sindical
</label>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="central_sindical" class="form-label">Central Sindical</label>
<input type="text" class="form-control" id="central_sindical" name="central_sindical" value="{{ militante.central_sindical }}">
</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 == militante.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="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 }}" {% if celula.id == militante.celula_id %}selected{% endif %}>{{ 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 }}" {% if militante.responsabilidades & Militante.RESPONSAVEL_FINANCAS %}checked{% endif %}>
<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 }}" {% if militante.responsabilidades & Militante.RESPONSAVEL_IMPRENSA %}checked{% endif %}>
<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 }}" {% if militante.responsabilidades & Militante.QUADRO_ORIENTADOR %}checked{% endif %}>
<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">Salvar</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 %}

View File

@@ -0,0 +1,91 @@
{% extends 'base.html' %}
{% block title %}Editar Relatório de Cotas{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Editar Relatório de Cotas</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="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 == relatorio.setor_id %}selected{% endif %}>{{ setor.nome }}</option>
{% endfor %}
</select>
<div class="invalid-feedback">
Por favor, selecione o setor.
</div>
</div>
<div class="mb-3">
<label for="comite_id" class="form-label">Comitê Central</label>
<select class="form-select" id="comite_id" name="comite_id" required>
<option value="">Selecione um comitê</option>
{% for comite in comites %}
<option value="{{ comite.id }}" {% if comite.id == relatorio.comite_id %}selected{% endif %}>{{ comite.nome }}</option>
{% endfor %}
</select>
<div class="invalid-feedback">
Por favor, selecione o comitê central.
</div>
</div>
<div class="mb-3">
<label for="total_cotas" class="form-label">Total de Cotas</label>
<input type="number" class="form-control" id="total_cotas" name="total_cotas" step="0.01" value="{{ relatorio.total_cotas }}" required>
<div class="invalid-feedback">
Por favor, insira o total de cotas.
</div>
</div>
<div class="mb-3">
<label for="data_relatorio" class="form-label">Data do Relatório</label>
<input type="date" class="form-control" id="data_relatorio" name="data_relatorio" value="{{ relatorio.data_relatorio.strftime('%Y-%m-%d') }}" required>
<div class="invalid-feedback">
Por favor, insira a data do relatório.
</div>
</div>
<div class="d-flex justify-content-between">
<button type="submit" class="btn btn-success">Salvar</button>
<a href="{{ url_for('listar_relatorios_cotas') }}" class="btn btn-outline-secondary">Voltar</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 %}

View File

@@ -0,0 +1,91 @@
{% extends 'base.html' %}
{% block title %}Editar Relatório de Pagamentos{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Editar Relatório de Pagamentos</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="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 == relatorio.setor_id %}selected{% endif %}>{{ setor.nome }}</option>
{% endfor %}
</select>
<div class="invalid-feedback">
Por favor, selecione o setor.
</div>
</div>
<div class="mb-3">
<label for="comite_id" class="form-label">Comitê Central</label>
<select class="form-select" id="comite_id" name="comite_id" required>
<option value="">Selecione um comitê</option>
{% for comite in comites %}
<option value="{{ comite.id }}" {% if comite.id == relatorio.comite_id %}selected{% endif %}>{{ comite.nome }}</option>
{% endfor %}
</select>
<div class="invalid-feedback">
Por favor, selecione o comitê central.
</div>
</div>
<div class="mb-3">
<label for="total_pagamentos" class="form-label">Total de Pagamentos</label>
<input type="number" class="form-control" id="total_pagamentos" name="total_pagamentos" step="0.01" value="{{ relatorio.total_pagamentos }}" required>
<div class="invalid-feedback">
Por favor, insira o total de pagamentos.
</div>
</div>
<div class="mb-3">
<label for="data_relatorio" class="form-label">Data do Relatório</label>
<input type="date" class="form-control" id="data_relatorio" name="data_relatorio" value="{{ relatorio.data_relatorio.strftime('%Y-%m-%d') }}" required>
<div class="invalid-feedback">
Por favor, insira a data do relatório.
</div>
</div>
<div class="d-flex justify-content-between">
<button type="submit" class="btn btn-success">Salvar</button>
<a href="{{ url_for('listar_relatorios_pagamentos') }}" class="btn btn-outline-secondary">Voltar</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 %}

View File

@@ -0,0 +1,91 @@
{% extends 'base.html' %}
{% block title %}Editar Relatório de Vendas{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Editar Relatório de Vendas</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="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 == relatorio.setor_id %}selected{% endif %}>{{ setor.nome }}</option>
{% endfor %}
</select>
<div class="invalid-feedback">
Por favor, selecione o setor.
</div>
</div>
<div class="mb-3">
<label for="comite_id" class="form-label">Comitê Central</label>
<select class="form-select" id="comite_id" name="comite_id" required>
<option value="">Selecione um comitê</option>
{% for comite in comites %}
<option value="{{ comite.id }}" {% if comite.id == relatorio.comite_id %}selected{% endif %}>{{ comite.nome }}</option>
{% endfor %}
</select>
<div class="invalid-feedback">
Por favor, selecione o comitê central.
</div>
</div>
<div class="mb-3">
<label for="total_vendas" class="form-label">Total de Vendas</label>
<input type="number" class="form-control" id="total_vendas" name="total_vendas" step="0.01" value="{{ relatorio.total_vendas }}" required>
<div class="invalid-feedback">
Por favor, insira o total de vendas.
</div>
</div>
<div class="mb-3">
<label for="data_relatorio" class="form-label">Data do Relatório</label>
<input type="date" class="form-control" id="data_relatorio" name="data_relatorio" value="{{ relatorio.data_relatorio.strftime('%Y-%m-%d') }}" required>
<div class="invalid-feedback">
Por favor, insira a data do relatório.
</div>
</div>
<div class="d-flex justify-content-between">
<button type="submit" class="btn btn-success">Salvar</button>
<a href="{{ url_for('listar_relatorios_vendas') }}" class="btn btn-outline-secondary">Voltar</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 %}

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,74 @@
{% extends 'base.html' %}
{% block title %}Editar Tipo de Material{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Editar Tipo de Material</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="{{ tipo.nome }}" required>
<div class="invalid-feedback">
Por favor, insira o nome do tipo de material.
</div>
</div>
<div class="col-md-6 mb-3">
<label for="preco" class="form-label">Preço</label>
<input type="number" class="form-control" id="preco" name="preco" step="0.01" min="0" value="{{ tipo.preco }}" required>
<div class="invalid-feedback">
Por favor, insira o preço do tipo de material.
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 mb-3">
<label for="descricao" class="form-label">Descrição</label>
<textarea class="form-control" id="descricao" name="descricao" rows="3">{{ tipo.descricao }}</textarea>
</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_tipos_materiais') }}" 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 %}

124
templates/editar_venda.html Normal file
View File

@@ -0,0 +1,124 @@
{% extends 'base.html' %}
{% block title %}Editar Venda{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Editar Venda</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="mb-3">
<label for="militante_id" class="form-label">Militante</label>
<select class="form-select" id="militante_id" name="militante_id" required>
<option value="">Selecione um militante</option>
{% for militante in militantes %}
<option value="{{ militante.id }}" {% if militante.id == venda.militante_id %}selected{% endif %}>{{ militante.nome }}</option>
{% endfor %}
</select>
<div class="invalid-feedback">
Por favor, selecione o militante.
</div>
</div>
<div class="mb-3">
<label for="material_id" class="form-label">Material</label>
<select class="form-select" id="material_id" name="material_id" required>
<option value="">Selecione um material</option>
{% for material in materiais %}
<option value="{{ material.id }}" data-preco="{{ material.preco }}" {% if material.id == venda.material_id %}selected{% endif %}>{{ material.nome }} - R$ {{ "%.2f"|format(material.preco) }}</option>
{% endfor %}
</select>
<div class="invalid-feedback">
Por favor, selecione o material.
</div>
</div>
<div class="mb-3">
<label for="quantidade" class="form-label">Quantidade</label>
<input type="number" class="form-control" id="quantidade" name="quantidade" min="1" value="{{ venda.quantidade }}" required>
<div class="invalid-feedback">
Por favor, insira a quantidade.
</div>
</div>
<div class="mb-3">
<label for="valor_total" class="form-label">Valor Total</label>
<input type="number" class="form-control" id="valor_total" name="valor_total" step="0.01" value="{{ venda.valor_total }}" readonly required>
</div>
<div class="mb-3">
<label for="data_venda" class="form-label">Data da Venda</label>
<input type="date" class="form-control" id="data_venda" name="data_venda" value="{{ venda.data_venda.strftime('%Y-%m-%d') }}" required>
<div class="invalid-feedback">
Por favor, insira a data da venda.
</div>
</div>
<div class="d-flex justify-content-between">
<button type="submit" class="btn btn-success">Salvar</button>
<a href="{{ url_for('listar_vendas') }}" class="btn btn-outline-secondary">Voltar</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)
})
})()
// Cálculo do valor total
document.getElementById('material_id').addEventListener('change', function() {
calcularValorTotal();
});
document.getElementById('quantidade').addEventListener('input', function() {
calcularValorTotal();
});
function calcularValorTotal() {
const materialSelect = document.getElementById('material_id');
const quantidadeInput = document.getElementById('quantidade');
const valorTotalInput = document.getElementById('valor_total');
if (materialSelect.value && quantidadeInput.value) {
const preco = parseFloat(materialSelect.options[materialSelect.selectedIndex].dataset.preco);
const quantidade = parseFloat(quantidadeInput.value);
const valorTotal = preco * quantidade;
valorTotalInput.value = valorTotal.toFixed(2);
} else {
valorTotalInput.value = '';
}
}
// Calcular valor total inicial
calcularValorTotal();
</script>
{% endblock %}

View File

@@ -1,12 +1,12 @@
{% extends 'base.html' %}
{% block title %}Início{% endblock %}
{% block title %}Home{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-8 offset-md-2">
<h2 class="mb-4">Menu do Sistema</h2>
<div class="col-md-12">
<h1 class="mb-4">Bem-vindo, {{ current_user.username }}!</h1>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
@@ -16,11 +16,16 @@
{% endif %}
{% endwith %}
<div class="list-group">
{% for url, nome in links %}
<a href="{{ url }}" class="list-group-item list-group-item-action">
{{ nome }}
</a>
<div class="row">
{% for link in links %}
<div class="col-md-4">
<div class="card mb-4">
<div class="card-body">
<h5 class="card-title">{{ link.text }}</h5>
<a href="{{ link.url }}" class="btn btn-primary">Acessar</a>
</div>
</div>
</div>
{% endfor %}
</div>
</div>

View File

@@ -0,0 +1,56 @@
{% extends 'base.html' %}
{% block title %}Listar Células{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Lista de Células</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="d-flex justify-content-between mb-4">
<a href="{{ url_for('nova_celula') }}" class="btn btn-success">Nova Célula</a>
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>ID</th>
<th>Nome</th>
<th>Responsável</th>
<th>Responsável Finanças</th>
<th>Setor</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{% for celula in celulas %}
<tr>
<td>{{ celula.id }}</td>
<td>{{ celula.nome }}</td>
<td>{{ celula.responsavel_rel.nome if celula.responsavel_rel else '-' }}</td>
<td>{{ celula.responsavel_financas_rel.nome if celula.responsavel_financas_rel else '-' }}</td>
<td>{{ celula.setor.nome }}</td>
<td>
<a href="{{ url_for('editar_celula', id=celula.id) }}" class="btn btn-primary btn-sm">Editar</a>
<a href="{{ url_for('deletar_celula', id=celula.id) }}" class="btn btn-danger btn-sm" onclick="return confirm('Tem certeza que deseja excluir esta célula?')">Excluir</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,56 @@
{% extends 'base.html' %}
{% block title %}Listar Comitês Regionais{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Lista de Comitês Regionais</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="d-flex justify-content-between mb-4">
<a href="{{ url_for('novo_comite') }}" class="btn btn-success">Novo Comitê Regional</a>
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>ID</th>
<th>Nome</th>
<th>Responsável</th>
<th>Responsável Finanças</th>
<th>Comitê Central</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{% for comite in comites %}
<tr>
<td>{{ comite.id }}</td>
<td>{{ comite.nome }}</td>
<td>{{ comite.responsavel_rel.nome if comite.responsavel_rel else '-' }}</td>
<td>{{ comite.responsavel_financas_rel.nome if comite.responsavel_financas_rel else '-' }}</td>
<td>{{ comite.comite_central.nome }}</td>
<td>
<a href="{{ url_for('editar_comite', id=comite.id) }}" class="btn btn-primary btn-sm">Editar</a>
<a href="{{ url_for('deletar_comite', id=comite.id) }}" class="btn btn-danger btn-sm" onclick="return confirm('Tem certeza que deseja excluir este comitê regional?')">Excluir</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,54 @@
{% extends 'base.html' %}
{% block title %}Listar Comitês Centrais{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Lista de Comitês Centrais</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="d-flex justify-content-between mb-4">
<a href="{{ url_for('novo_comite_central') }}" class="btn btn-success">Novo Comitê Central</a>
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>ID</th>
<th>Nome</th>
<th>Responsável</th>
<th>Responsável Finanças</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{% for comite in comites %}
<tr>
<td>{{ comite.id }}</td>
<td>{{ comite.nome }}</td>
<td>{{ comite.responsavel_rel.nome if comite.responsavel_rel else '-' }}</td>
<td>{{ comite.responsavel_financas_rel.nome if comite.responsavel_financas_rel else '-' }}</td>
<td>
<a href="{{ url_for('editar_comite_central', id=comite.id) }}" class="btn btn-primary btn-sm">Editar</a>
<a href="{{ url_for('deletar_comite_central', id=comite.id) }}" class="btn btn-danger btn-sm" onclick="return confirm('Tem certeza que deseja excluir este comitê central?')">Excluir</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,79 @@
{% extends 'base.html' %}
{% block title %}Lista de {{ tipo_instancia }}s{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Lista de {{ tipo_instancia }}s</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="d-flex justify-content-between mb-4">
<a href="{{ url_for('criar_' + tipo_instancia.lower()) }}" class="btn btn-primary">Nova {{ tipo_instancia }}</a>
</div>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Nome</th>
{% if tipo_instancia != 'Célula' %}
<th>{{ instancia_superior }}</th>
{% endif %}
<th>Responsável Geral</th>
<th>Responsável de Finanças</th>
<th>Responsável de Imprensa</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{% for instancia in instancias %}
<tr>
<td>{{ instancia.nome }}</td>
{% if tipo_instancia != 'Célula' %}
<td>{{ instancia.instancia_superior.nome }}</td>
{% endif %}
<td>{{ instancia.responsavel_geral.nome }}</td>
<td>
{% if instancia.responsavel_financas %}
{{ instancia.responsavel_financas.nome }}
{% else %}
-
{% endif %}
</td>
<td>
{% if instancia.responsavel_imprensa %}
{{ instancia.responsavel_imprensa.nome }}
{% else %}
-
{% endif %}
</td>
<td>
<a href="{{ url_for('editar_' + tipo_instancia.lower(), id=instancia.id) }}" class="btn btn-sm btn-warning">Editar</a>
<button type="button" class="btn btn-sm btn-danger" onclick="confirmarExclusao({{ instancia.id }})">Excluir</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<script>
function confirmarExclusao(id) {
if (confirm('Tem certeza que deseja excluir esta {{ tipo_instancia }}?')) {
window.location.href = "{{ url_for('excluir_' + tipo_instancia.lower(), id=0) }}".replace('0', id);
}
}
</script>
{% endblock %}

View File

@@ -1,33 +1,58 @@
{% extends 'base.html' %}
{% block title %}Listar Militantes{% endblock %}
{% block title %}Listar Materiais{% endblock %}
{% block content %}
<h1>Materiais Vendidos</h1>
<a href="{{ url_for('novo_material') }}">Adicionar Novo Material</a>
<table border="1">
<thead>
<tr>
<th>ID</th>
<th>Militante ID</th>
<th>Tipo Material</th>
<th>Descrição</th>
<th>Valor</th>
<th>Data da Venda</th>
</tr>
</thead>
<tbody>
{% for material in materiais %}
<tr>
<td>{{ material.id }}</td>
<td>{{ material.militante_id }}</td>
<td>{{ material.tipo_material_id }}</td>
<td>{{ material.descricao }}</td>
<td>R$ {{ material.valor }}</td>
<td>{{ material.data_venda }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<a href="{{ url_for('home') }}">Home</a>
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Lista de Materiais</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="d-flex justify-content-between mb-4">
<a href="{{ url_for('novo_material') }}" class="btn btn-success">Novo Material</a>
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>ID</th>
<th>Nome</th>
<th>Descrição</th>
<th>Preço</th>
<th>Quantidade</th>
<th>Tipo</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{% for material in materiais %}
<tr>
<td>{{ material.id }}</td>
<td>{{ material.nome }}</td>
<td>{{ material.descricao }}</td>
<td>R$ {{ "%.2f"|format(material.preco) }}</td>
<td>{{ material.quantidade }}</td>
<td>{{ material.tipo.nome }}</td>
<td>
<a href="{{ url_for('editar_material', id=material.id) }}" class="btn btn-primary btn-sm">Editar</a>
<a href="{{ url_for('deletar_material', id=material.id) }}" class="btn btn-danger btn-sm" onclick="return confirm('Tem certeza que deseja excluir este material?')">Excluir</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,57 +1,71 @@
{% extends 'base.html' %}
{% block title %}Listar Militantes{% endblock %}
{% block title %}Lista de Militantes{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12">
<h2>Lista de Militantes</h2>
<a href="{{ url_for('novo_militante') }}" class="btn btn-primary mb-3">Novo Militante</a>
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Lista de Militantes</h1>
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Nome</th>
<th>CPF</th>
<th>Email</th>
<th>Telefone</th>
<th>Endereço</th>
<th>Filiado</th>
</tr>
</thead>
<tbody>
{% for militante in militantes %}
<tr class="clickable-row" data-href="{{ url_for('editar_militante', id=militante.id) }}">
<td>{{ militante.nome }}</td>
<td>{{ militante.cpf }}</td>
<td>{{ militante.email }}</td>
<td>{{ militante.telefone }}</td>
<td>{{ militante.endereco }}</td>
<td>{{ 'Sim' if militante.filiado else 'Não' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% 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="d-flex justify-content-between mb-4">
<a href="{{ url_for('criar_militante') }}" class="btn btn-primary">Novo Militante</a>
</div>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Nome</th>
<th>Email</th>
<th>Célula</th>
<th>Responsabilidades</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{% for militante in militantes %}
<tr>
<td>{{ militante.nome }}</td>
<td>{{ militante.email }}</td>
<td>{{ militante.celula.nome }}</td>
<td>
{% if militante.responsabilidades & Militante.RESPONSAVEL_FINANCAS %}
<span class="badge bg-primary">Finanças</span>
{% endif %}
{% if militante.responsabilidades & Militante.RESPONSAVEL_IMPRENSA %}
<span class="badge bg-info">Imprensa</span>
{% endif %}
{% if militante.responsabilidades & Militante.QUADRO_ORIENTADOR %}
<span class="badge bg-success">Quadro-Orientador</span>
{% endif %}
</td>
<td>
<a href="{{ url_for('editar_militante', id=militante.id) }}" class="btn btn-sm btn-warning">Editar</a>
<button type="button" class="btn btn-sm btn-danger" onclick="confirmarExclusao({{ militante.id }})">Excluir</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<style>
.clickable-row {
cursor: pointer;
}
.clickable-row:hover {
background-color: #f5f5f5;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
const rows = document.querySelectorAll('.clickable-row');
rows.forEach(row => {
row.addEventListener('click', function() {
window.location.href = this.dataset.href;
});
});
});
function confirmarExclusao(id) {
if (confirm('Tem certeza que deseja excluir este militante?')) {
window.location.href = "{{ url_for('excluir_militante', id=0) }}".replace('0', id);
}
}
</script>
{% endblock %}

View File

@@ -1,32 +1,57 @@
{% extends 'base.html' %}
{% block title %}Listar Militantes{% endblock %}
{% block title %}Listar Relatórios de Cotas{% endblock %}
{% block content %}
<h1>Relatórios de Cotas Mensais</h1>
<a href="{{ url_for('novo_relatorio_cotas') }}">Adicionar Novo Relatório</a>
<table border="1">
<thead>
<tr>
<th>ID</th>
<th>Setor ID</th>
<th>Comitê ID</th>
<th>Total de Cotas</th>
<th>Data do Relatório</th>
</tr>
</thead>
<tbody>
{% for relatorio in relatorios %}
<tr>
<td>{{ relatorio.id }}</td>
<td>{{ relatorio.setor_id }}</td>
<td>{{ relatorio.comite_id }}</td>
<td>R$ {{ relatorio.total_cotas }}</td>
<td>{{ relatorio.data_relatorio }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<a href="{{ url_for('home') }}">Home</a>
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Lista de Relatórios de Cotas</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="d-flex justify-content-between mb-4">
<a href="{{ url_for('novo_relatorio_cotas') }}" class="btn btn-success">Novo Relatório</a>
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>ID</th>
<th>Setor</th>
<th>Comitê Central</th>
<th>Total de Cotas</th>
<th>Data do Relatório</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{% for relatorio in relatorios %}
<tr>
<td>{{ relatorio.id }}</td>
<td>{{ relatorio.setor.nome }}</td>
<td>{{ relatorio.comite.nome }}</td>
<td>R$ {{ "%.2f"|format(relatorio.total_cotas) }}</td>
<td>{{ relatorio.data_relatorio.strftime('%d/%m/%Y') }}</td>
<td>
<a href="{{ url_for('editar_relatorio_cotas', id=relatorio.id) }}" class="btn btn-primary btn-sm">Editar</a>
<a href="{{ url_for('deletar_relatorio_cotas', id=relatorio.id) }}" class="btn btn-danger btn-sm" onclick="return confirm('Tem certeza que deseja excluir este relatório?')">Excluir</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,56 @@
{% extends 'base.html' %}
{% block title %}Listar Relatórios de Pagamentos{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Lista de Relatórios de Pagamentos</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="d-flex justify-content-between mb-4">
<a href="{{ url_for('novo_relatorio_pagamentos') }}" class="btn btn-success">Novo Relatório</a>
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>ID</th>
<th>Setor</th>
<th>Comitê Central</th>
<th>Total de Pagamentos</th>
<th>Data do Relatório</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{% for relatorio in relatorios %}
<tr>
<td>{{ relatorio.id }}</td>
<td>{{ relatorio.setor.nome }}</td>
<td>{{ relatorio.comite.nome }}</td>
<td>R$ {{ "%.2f"|format(relatorio.total_pagamentos) }}</td>
<td>{{ relatorio.data_relatorio.strftime('%d/%m/%Y') }}</td>
<td>
<a href="{{ url_for('editar_relatorio_pagamentos', id=relatorio.id) }}" class="btn btn-primary btn-sm">Editar</a>
<a href="{{ url_for('deletar_relatorio_pagamentos', id=relatorio.id) }}" class="btn btn-danger btn-sm" onclick="return confirm('Tem certeza que deseja excluir este relatório?')">Excluir</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,32 +1,56 @@
{% extends 'base.html' %}
{% block title %}Listar Militantes{% endblock %}
{% block title %}Listar Relatórios de Vendas{% endblock %}
{% block content %}
<h1>Relatórios de Vendas de Materiais</h1>
<a href="{{ url_for('novo_relatorio_vendas') }}">Adicionar Novo Relatório</a>
<table border="1">
<thead>
<tr>
<th>ID</th>
<th>Setor ID</th>
<th>Comitê ID</th>
<th>Total de Vendas</th>
<th>Data do Relatório</th>
</tr>
</thead>
<tbody>
{% for relatorio in relatorios %}
<tr>
<td>{{ relatorio.id }}</td>
<td>{{ relatorio.setor_id }}</td>
<td>{{ relatorio.comite_id }}</td>
<td>R$ {{ relatorio.total_vendas }}</td>
<td>{{ relatorio.data_relatorio }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<a href="{{ url_for('home') }}">Home</a>
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Lista de Relatórios de Vendas</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="d-flex justify-content-between mb-4">
<a href="{{ url_for('novo_relatorio_vendas') }}" class="btn btn-success">Novo Relatório</a>
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>ID</th>
<th>Setor</th>
<th>Comitê Central</th>
<th>Total de Vendas</th>
<th>Data do Relatório</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{% for relatorio in relatorios %}
<tr>
<td>{{ relatorio.id }}</td>
<td>{{ relatorio.setor.nome }}</td>
<td>{{ relatorio.comite.nome }}</td>
<td>R$ {{ "%.2f"|format(relatorio.total_vendas) }}</td>
<td>{{ relatorio.data_relatorio.strftime('%d/%m/%Y') }}</td>
<td>
<a href="{{ url_for('editar_relatorio_vendas', id=relatorio.id) }}" class="btn btn-primary btn-sm">Editar</a>
<a href="{{ url_for('deletar_relatorio_vendas', id=relatorio.id) }}" class="btn btn-danger btn-sm" onclick="return confirm('Tem certeza que deseja excluir este relatório?')">Excluir</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,56 @@
{% extends 'base.html' %}
{% block title %}Listar Setores{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Lista de Setores</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="d-flex justify-content-between mb-4">
<a href="{{ url_for('novo_setor') }}" class="btn btn-success">Novo Setor</a>
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>ID</th>
<th>Nome</th>
<th>Responsável</th>
<th>Responsável Finanças</th>
<th>Comitê Regional</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{% for setor in setores %}
<tr>
<td>{{ setor.id }}</td>
<td>{{ setor.nome }}</td>
<td>{{ setor.responsavel_rel.nome if setor.responsavel_rel else '-' }}</td>
<td>{{ setor.responsavel_financas_rel.nome if setor.responsavel_financas_rel else '-' }}</td>
<td>{{ setor.comite_regional.nome }}</td>
<td>
<a href="{{ url_for('editar_setor', id=setor.id) }}" class="btn btn-primary btn-sm">Editar</a>
<a href="{{ url_for('deletar_setor', id=setor.id) }}" class="btn btn-danger btn-sm" onclick="return confirm('Tem certeza que deseja excluir este setor?')">Excluir</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,54 @@
{% extends 'base.html' %}
{% block title %}Listar Tipos de Materiais{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Lista de Tipos de Materiais</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="d-flex justify-content-between mb-4">
<a href="{{ url_for('novo_tipo_material') }}" class="btn btn-success">Novo Tipo de Material</a>
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>ID</th>
<th>Nome</th>
<th>Descrição</th>
<th>Preço</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{% for tipo in tipos %}
<tr>
<td>{{ tipo.id }}</td>
<td>{{ tipo.nome }}</td>
<td>{{ tipo.descricao }}</td>
<td>R$ {{ "%.2f"|format(tipo.preco) }}</td>
<td>
<a href="{{ url_for('editar_tipo_material', id=tipo.id) }}" class="btn btn-primary btn-sm">Editar</a>
<a href="{{ url_for('deletar_tipo_material', id=tipo.id) }}" class="btn btn-danger btn-sm" onclick="return confirm('Tem certeza que deseja excluir este tipo de material?')">Excluir</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,58 @@
{% extends 'base.html' %}
{% block title %}Listar Vendas{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Lista de Vendas</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="d-flex justify-content-between mb-4">
<a href="{{ url_for('nova_venda') }}" class="btn btn-success">Nova Venda</a>
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>ID</th>
<th>Militante</th>
<th>Material</th>
<th>Quantidade</th>
<th>Valor Total</th>
<th>Data da Venda</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{% for venda in vendas %}
<tr>
<td>{{ venda.id }}</td>
<td>{{ venda.militante.nome }}</td>
<td>{{ venda.material.nome }}</td>
<td>{{ venda.quantidade }}</td>
<td>R$ {{ "%.2f"|format(venda.valor_total) }}</td>
<td>{{ venda.data_venda.strftime('%d/%m/%Y') }}</td>
<td>
<a href="{{ url_for('editar_venda', id=venda.id) }}" class="btn btn-primary btn-sm">Editar</a>
<a href="{{ url_for('deletar_venda', id=venda.id) }}" class="btn btn-danger btn-sm" onclick="return confirm('Tem certeza que deseja excluir esta venda?')">Excluir</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -4,43 +4,38 @@
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-6 offset-md-3">
<div class="card mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h3 class="text-center">Login</h3>
<h3 class="card-title">Login</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 }}
{% if category == 'danger' %}
<br>
<small>Se o problema persistir, contate o administrador.</small>
{% endif %}
</div>
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST">
<form method="POST" action="{{ url_for('login') }}">
<div class="mb-3">
<label for="username" class="form-label">Usuário</label>
<input type="text" class="form-control" id="username" name="username" required
value="{{ request.form.username }}">
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Senha</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<div class="mb-3">
<label for="otp" class="form-label">Código OTP</label>
<input type="text" class="form-control" id="otp" name="otp" required
pattern="[0-9]{6}" title="Digite o código de 6 dígitos">
<small class="form-text text-muted">Digite o código de 6 dígitos do seu aplicativo autenticador</small>
<label for="otp_code" class="form-label">Código OTP</label>
<input type="text" class="form-control" id="otp_code" name="otp_code" required>
<small class="text-muted">Digite o código gerado pelo seu aplicativo autenticador</small>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">Entrar</button>
</div>

View File

@@ -0,0 +1,94 @@
{% extends 'base.html' %}
{% block title %}Nova Célula{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Nova 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" 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 }}">{{ 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 }}">{{ 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 }}">{{ 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 %}

121
templates/nova_venda.html Normal file
View File

@@ -0,0 +1,121 @@
{% extends 'base.html' %}
{% block title %}Nova Venda{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Nova Venda</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="mb-3">
<label for="militante_id" class="form-label">Militante</label>
<select class="form-select" id="militante_id" name="militante_id" required>
<option value="">Selecione um militante</option>
{% for militante in militantes %}
<option value="{{ militante.id }}">{{ militante.nome }}</option>
{% endfor %}
</select>
<div class="invalid-feedback">
Por favor, selecione o militante.
</div>
</div>
<div class="mb-3">
<label for="material_id" class="form-label">Material</label>
<select class="form-select" id="material_id" name="material_id" required>
<option value="">Selecione um material</option>
{% for material in materiais %}
<option value="{{ material.id }}" data-preco="{{ material.preco }}">{{ material.nome }} - R$ {{ "%.2f"|format(material.preco) }}</option>
{% endfor %}
</select>
<div class="invalid-feedback">
Por favor, selecione o material.
</div>
</div>
<div class="mb-3">
<label for="quantidade" class="form-label">Quantidade</label>
<input type="number" class="form-control" id="quantidade" name="quantidade" min="1" required>
<div class="invalid-feedback">
Por favor, insira a quantidade.
</div>
</div>
<div class="mb-3">
<label for="valor_total" class="form-label">Valor Total</label>
<input type="number" class="form-control" id="valor_total" name="valor_total" step="0.01" readonly required>
</div>
<div class="mb-3">
<label for="data_venda" class="form-label">Data da Venda</label>
<input type="date" class="form-control" id="data_venda" name="data_venda" required>
<div class="invalid-feedback">
Por favor, insira a data da venda.
</div>
</div>
<div class="d-flex justify-content-between">
<button type="submit" class="btn btn-success">Registrar</button>
<a href="{{ url_for('listar_vendas') }}" class="btn btn-outline-secondary">Voltar</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)
})
})()
// Cálculo do valor total
document.getElementById('material_id').addEventListener('change', function() {
calcularValorTotal();
});
document.getElementById('quantidade').addEventListener('input', function() {
calcularValorTotal();
});
function calcularValorTotal() {
const materialSelect = document.getElementById('material_id');
const quantidadeInput = document.getElementById('quantidade');
const valorTotalInput = document.getElementById('valor_total');
if (materialSelect.value && quantidadeInput.value) {
const preco = parseFloat(materialSelect.options[materialSelect.selectedIndex].dataset.preco);
const quantidade = parseFloat(quantidadeInput.value);
const valorTotal = preco * quantidade;
valorTotalInput.value = valorTotal.toFixed(2);
} else {
valorTotalInput.value = '';
}
}
</script>
{% endblock %}

View File

@@ -0,0 +1,74 @@
{% extends 'base.html' %}
{% block title %}Nova Célula{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-8 offset-md-2">
<h1 class="mb-4">Nova 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="mb-4">
<div class="mb-3">
<label for="nome" class="form-label">Nome:</label>
<input type="text" class="form-control" id="nome" name="nome" required>
</div>
<div class="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 o setor</option>
{% for setor in setores %}
<option value="{{ setor.id }}">{{ setor.nome }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="comite_regional_id" class="form-label">Comitê Regional:</label>
<select class="form-select" id="comite_regional_id" name="comite_regional_id" required>
<option value="">Selecione o comitê</option>
{% for comite in comites %}
<option value="{{ comite.id }}">{{ comite.nome }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="responsavel" class="form-label">Responsável:</label>
<select class="form-select" id="responsavel" name="responsavel" required>
<option value="">Selecione o responsável</option>
{% for militante in militantes %}
<option value="{{ militante.id }}">{{ militante.nome }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="responsavel_financas" class="form-label">Responsável por Finanças:</label>
<select class="form-select" id="responsavel_financas" name="responsavel_financas" required>
<option value="">Selecione o responsável</option>
{% for militante in militantes %}
<option value="{{ militante.id }}">{{ militante.nome }}</option>
{% endfor %}
</select>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">Registrar</button>
<a href="{{ url_for('listar_celulas') }}" class="btn btn-secondary">Voltar</a>
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,94 @@
{% extends 'base.html' %}
{% block title %}Novo Comitê Regional{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Novo 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" 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 in comites_centrais %}
<option value="{{ comite.id }}">{{ comite.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 }}">{{ 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 }}">{{ 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 %}

View File

@@ -0,0 +1,81 @@
{% extends 'base.html' %}
{% block title %}Novo Comitê Central{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Novo 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" 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 }}">{{ 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 }}">{{ 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 %}

View File

@@ -0,0 +1,54 @@
{% extends 'base.html' %}
{% block title %}Novo Comitê Regional{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-8 offset-md-2">
<h1 class="mb-4">Novo 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="mb-4">
<div class="mb-3">
<label for="nome" class="form-label">Nome:</label>
<input type="text" class="form-control" id="nome" name="nome" required>
</div>
<div class="mb-3">
<label for="responsavel" class="form-label">Responsável:</label>
<select class="form-select" id="responsavel" name="responsavel" required>
<option value="">Selecione o responsável</option>
{% for militante in militantes %}
<option value="{{ militante.id }}">{{ militante.nome }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="responsavel_financas" class="form-label">Responsável por Finanças:</label>
<select class="form-select" id="responsavel_financas" name="responsavel_financas" required>
<option value="">Selecione o responsável</option>
{% for militante in militantes %}
<option value="{{ militante.id }}">{{ militante.nome }}</option>
{% endfor %}
</select>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">Registrar</button>
<a href="{{ url_for('listar_comites_regionais') }}" class="btn btn-secondary">Voltar</a>
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,74 @@
{% extends 'base.html' %}
{% block title %}Novo Endereço{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-8 offset-md-2">
<h1 class="mb-4">Novo Endereço</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="mb-4">
<div class="mb-3">
<label for="militante_id" class="form-label">Militante:</label>
<select class="form-select" id="militante_id" name="militante_id" required>
<option value="">Selecione o militante</option>
{% for militante in militantes %}
<option value="{{ militante.id }}">{{ militante.nome }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="logradouro" class="form-label">Logradouro:</label>
<input type="text" class="form-control" id="logradouro" name="logradouro" required>
</div>
<div class="mb-3">
<label for="numero" class="form-label">Número:</label>
<input type="text" class="form-control" id="numero" name="numero" required>
</div>
<div class="mb-3">
<label for="complemento" class="form-label">Complemento:</label>
<input type="text" class="form-control" id="complemento" name="complemento">
</div>
<div class="mb-3">
<label for="bairro" class="form-label">Bairro:</label>
<input type="text" class="form-control" id="bairro" name="bairro" required>
</div>
<div class="mb-3">
<label for="cidade" class="form-label">Cidade:</label>
<input type="text" class="form-control" id="cidade" name="cidade" required>
</div>
<div class="mb-3">
<label for="estado" class="form-label">Estado:</label>
<input type="text" class="form-control" id="estado" name="estado" required>
</div>
<div class="mb-3">
<label for="cep" class="form-label">CEP:</label>
<input type="text" class="form-control" id="cep" name="cep" required>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">Registrar</button>
<a href="{{ url_for('listar_enderecos') }}" class="btn btn-secondary">Voltar</a>
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,35 +1,94 @@
{% extends 'base.html' %}
{% block title %}Listar Militantes{% endblock %}
{% block title %}Novo Material{% endblock %}
{% block content %}
<h1>Registrar Novo Material</h1>
<form method="post">
<div>
<label for="militante_id">ID do Militante:</label>
<input type="number" id="militante_id" name="militante_id" required>
</div>
<div>
<label for="tipo_material_id">Tipo de Material:</label>
<input type="number" id="tipo_material_id" name="tipo_material_id" required>
</div>
<div>
<label for="descricao">Descrição:</label>
<input type="text" id="descricao" name="descricao" required>
</div>
<div>
<label for="valor">Valor:</label>
<input type="number" id="valor" name="valor" step="0.01" required>
</div>
<div>
<label for="data_venda">Data da Venda:</label>
<input type="date" id="data_venda" name="data_venda" required>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">Registrar</button>
<a href="{{ url_for('listar_materiais') }}" class="btn btn-secondary">Voltar</a>
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
</div>
</form>
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Novo Material</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="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 material.
</div>
</div>
<div class="mb-3">
<label for="descricao" class="form-label">Descrição</label>
<textarea class="form-control" id="descricao" name="descricao" rows="3" required></textarea>
<div class="invalid-feedback">
Por favor, insira a descrição do material.
</div>
</div>
<div class="mb-3">
<label for="preco" class="form-label">Preço</label>
<input type="number" class="form-control" id="preco" name="preco" step="0.01" required>
<div class="invalid-feedback">
Por favor, insira o preço do material.
</div>
</div>
<div class="mb-3">
<label for="quantidade" class="form-label">Quantidade</label>
<input type="number" class="form-control" id="quantidade" name="quantidade" required>
<div class="invalid-feedback">
Por favor, insira a quantidade do material.
</div>
</div>
<div class="mb-3">
<label for="tipo_id" class="form-label">Tipo de Material</label>
<select class="form-select" id="tipo_id" name="tipo_id" required>
<option value="">Selecione um tipo</option>
{% for tipo in tipos %}
<option value="{{ tipo.id }}">{{ tipo.nome }}</option>
{% endfor %}
</select>
<div class="invalid-feedback">
Por favor, selecione o tipo do material.
</div>
</div>
<div class="d-flex justify-content-between">
<button type="submit" class="btn btn-success">Registrar</button>
<a href="{{ url_for('listar_materiais') }}" class="btn btn-outline-secondary">Voltar</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 %}

View File

@@ -6,7 +6,7 @@
<div class="container">
<div class="row">
<div class="col-md-8 offset-md-2">
<h1 class="mb-4">Criar Novo Militante</h1>
<h1 class="mb-4">Novo Militante</h1>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
@@ -19,44 +19,171 @@
<form method="post" class="mb-4">
<div class="mb-3">
<label for="nome" class="form-label">Nome:</label>
<input type="text" class="form-control" id="nome" name="nome" required
value="{{ dados_anteriores.nome if dados_anteriores else '' }}">
</div>
<div class="mb-3">
<label for="cpf" class="form-label">CPF:</label>
<input type="text" class="form-control" id="cpf" name="cpf" required
value="{{ dados_anteriores.cpf if dados_anteriores else '' }}"
pattern="\d{3}\.?\d{3}\.?\d{3}-?\d{2}"
title="Digite um CPF no formato: xxx.xxx.xxx-xx">
<input type="text" class="form-control" id="nome" name="nome" required>
</div>
<div class="mb-3">
<label for="email" class="form-label">Email:</label>
<input type="email" class="form-control" id="email" name="email" required
value="{{ dados_anteriores.email if dados_anteriores else '' }}">
<input type="email" class="form-control" id="email" name="email" required>
<small class="form-text text-muted">Este email será usado para login e comunicação do sistema</small>
</div>
<div class="mb-3">
<label for="telefone" class="form-label">Telefone:</label>
<input type="text" class="form-control" id="telefone" name="telefone"
value="{{ dados_anteriores.telefone if dados_anteriores else '' }}">
<label for="cpf" class="form-label">CPF:</label>
<input type="text" class="form-control" id="cpf" name="cpf" required>
</div>
<div class="mb-3">
<label for="endereco" class="form-label">Endereço:</label>
<input type="text" class="form-control" id="endereco" name="endereco"
value="{{ dados_anteriores.endereco if dados_anteriores else '' }}">
<label for="titulo_eleitoral" class="form-label">Título Eleitoral:</label>
<input type="text" class="form-control" id="titulo_eleitoral" name="titulo_eleitoral" required>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="filiado" name="filiado"
{% if dados_anteriores and dados_anteriores.filiado %}checked{% endif %}>
<label class="form-check-label" for="filiado">Filiado</label>
<div class="mb-3">
<label for="data_nascimento" class="form-label">Data de Nascimento:</label>
<input type="date" class="form-control" id="data_nascimento" name="data_nascimento" required>
</div>
<div class="mb-3">
<label for="data_entrada_oci" class="form-label">Data de Entrada na OCI:</label>
<input type="date" class="form-control" id="data_entrada_oci" name="data_entrada_oci" required>
</div>
<div class="mb-3">
<label for="data_efetivacao_oci" class="form-label">Data de Efetivação na OCI:</label>
<input type="date" class="form-control" id="data_efetivacao_oci" name="data_efetivacao_oci" required>
</div>
<div class="mb-3">
<label for="telefone1" class="form-label">Telefone 1:</label>
<input type="text" class="form-control" id="telefone1" name="telefone1" required>
</div>
<div class="mb-3">
<label for="telefone2" class="form-label">Telefone 2:</label>
<input type="text" class="form-control" id="telefone2" name="telefone2">
</div>
<div class="mb-3">
<label for="profissao" class="form-label">Profissão:</label>
<input type="text" class="form-control" id="profissao" name="profissao" required>
</div>
<div class="mb-3">
<label for="regime_trabalho" class="form-label">Regime de Trabalho:</label>
<input type="text" class="form-control" id="regime_trabalho" name="regime_trabalho" required>
</div>
<div class="mb-3">
<label for="empresa" class="form-label">Empresa:</label>
<input type="text" class="form-control" id="empresa" name="empresa" required>
</div>
<div class="mb-3">
<label for="contratante" class="form-label">Contratante:</label>
<input type="text" class="form-control" id="contratante" name="contratante" required>
</div>
<div class="mb-3">
<label for="instituicao_ensino" class="form-label">Instituição de Ensino:</label>
<input type="text" class="form-control" id="instituicao_ensino" name="instituicao_ensino">
</div>
<div class="mb-3">
<label for="tipo_instituicao" class="form-label">Tipo de Instituição:</label>
<select class="form-select" id="tipo_instituicao" name="tipo_instituicao">
<option value="">Selecione o tipo</option>
<option value="publica">Pública</option>
<option value="privada">Privada</option>
</select>
</div>
<div class="mb-3">
<label for="sindicato" class="form-label">Sindicato:</label>
<input type="text" class="form-control" id="sindicato" name="sindicato">
</div>
<div class="mb-3">
<label for="cargo_sindical" class="form-label">Cargo Sindical:</label>
<input type="text" class="form-control" id="cargo_sindical" name="cargo_sindical">
</div>
<div class="mb-3">
<label for="dirigente_sindical" class="form-label">Dirigente Sindical:</label>
<select class="form-select" id="dirigente_sindical" name="dirigente_sindical">
<option value="false">Não</option>
<option value="true">Sim</option>
</select>
</div>
<div class="mb-3">
<label for="central_sindical" class="form-label">Central Sindical:</label>
<input type="text" class="form-control" id="central_sindical" name="central_sindical">
</div>
<div class="mb-3">
<label for="cr_id" class="form-label">Comitê Regional:</label>
<select class="form-select" id="cr_id" name="cr_id" required>
<option value="">Selecione o CR</option>
{% for cr in crs %}
<option value="{{ cr.id }}">{{ cr.nome }}</option>
{% endfor %}
</select>
</div>
<div class="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 o setor</option>
{% for setor in setores %}
<option value="{{ setor.id }}" data-cr="{{ setor.cr_id }}">{{ setor.nome }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label class="form-label">Célula:</label>
{% if pode_criar_celula %}
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="radio" name="celula_opcao" id="celula_existente" value="existente" checked>
<label class="form-check-label" for="celula_existente">
Usar célula existente
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="celula_opcao" id="celula_nova" value="nova">
<label class="form-check-label" for="celula_nova">
Criar nova célula
</label>
</div>
</div>
{% endif %}
<div id="celula_existente_container">
<select class="form-select" id="celula_id" name="celula_id" required>
<option value="">Selecione a célula</option>
{% for celula in celulas %}
<option value="{{ celula.id }}" data-setor="{{ celula.setor_id }}">{{ celula.nome }}</option>
{% endfor %}
</select>
</div>
{% if pode_criar_celula %}
<div id="celula_nova_container" style="display: none;">
<div class="mb-3">
<label for="nova_celula_nome" class="form-label">Nome da Nova Célula:</label>
<input type="text" class="form-control" id="nova_celula_nome" name="nova_celula_nome">
</div>
<div class="mb-3">
<label for="nova_celula_quadro_orientador" class="form-label">Quadro Orientador:</label>
<input type="text" class="form-control" id="nova_celula_quadro_orientador" name="nova_celula_quadro_orientador">
</div>
</div>
{% endif %}
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">Criar</button>
<button type="submit" class="btn btn-primary">Registrar</button>
<a href="{{ url_for('listar_militantes') }}" class="btn btn-secondary">Voltar</a>
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
</div>
@@ -64,5 +191,66 @@
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Função para atualizar as células baseado no setor selecionado
function atualizarCelulas() {
const setorId = document.getElementById('setor_id').value;
const celulaSelect = document.getElementById('celula_id');
// Limpar opções existentes
celulaSelect.innerHTML = '<option value="">Selecione a célula</option>';
// Adicionar apenas células do setor selecionado
document.querySelectorAll('#celula_id option').forEach(option => {
if (option.dataset.setor === setorId) {
celulaSelect.appendChild(option.cloneNode(true));
}
});
}
// Função para atualizar os setores baseado no CR selecionado
function atualizarSetores() {
const crId = document.getElementById('cr_id').value;
const setorSelect = document.getElementById('setor_id');
// Limpar opções existentes
setorSelect.innerHTML = '<option value="">Selecione o setor</option>';
// Adicionar apenas setores do CR selecionado
document.querySelectorAll('#setor_id option').forEach(option => {
if (option.dataset.cr === crId) {
setorSelect.appendChild(option.cloneNode(true));
}
});
// Atualizar células após mudar o setor
atualizarCelulas();
}
// Event listeners
document.getElementById('cr_id').addEventListener('change', atualizarSetores);
document.getElementById('setor_id').addEventListener('change', atualizarCelulas);
// Toggle entre célula existente e nova
const celulaExistente = document.getElementById('celula_existente');
const celulaNova = document.getElementById('celula_nova');
const celulaExistenteContainer = document.getElementById('celula_existente_container');
const celulaNovaContainer = document.getElementById('celula_nova_container');
if (celulaExistente && celulaNova) {
celulaExistente.addEventListener('change', function() {
celulaExistenteContainer.style.display = 'block';
celulaNovaContainer.style.display = 'none';
});
celulaNova.addEventListener('change', function() {
celulaExistenteContainer.style.display = 'none';
celulaNovaContainer.style.display = 'block';
});
}
});
</script>
{% endblock %}

View File

@@ -6,17 +6,61 @@
<div class="container">
<div class="row">
<div class="col-md-8 offset-md-2">
<h1 class="mb-4">Registrar Novo Pagamento</h1>
<h1 class="mb-4">Novo Pagamento</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="mb-4">
<div class="mb-3">
<label for="militante_id" class="form-label">ID do Militante:</label>
<input type="number" class="form-control" id="militante_id" name="militante_id" required>
<label for="militante_id" class="form-label">Militante:</label>
<select class="form-select" id="militante_id" name="militante_id" required>
<option value="">Selecione o militante</option>
{% for militante in militantes %}
<option value="{{ militante.id }}">{{ militante.nome }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="tipo_pagamento_id" class="form-label">Tipo de Pagamento:</label>
<input type="number" class="form-control" id="tipo_pagamento_id" name="tipo_pagamento_id" required>
<label for="tipo_pagamento" class="form-label">Tipo de Pagamento:</label>
<select class="form-select" id="tipo_pagamento" name="tipo_pagamento" required>
<option value="">Selecione o tipo</option>
<option value="cota">Cota</option>
<option value="jornal">Jornal</option>
<option value="assinatura">Assinatura</option>
<option value="campanha">Campanha Financeira</option>
</select>
</div>
<div class="mb-3">
<label for="mes_referencia" class="form-label">Mês de Referência:</label>
<input type="month" class="form-control" id="mes_referencia" name="mes_referencia" required>
</div>
<div class="mb-3">
<label for="numero_jornal" class="form-label">Número do Jornal:</label>
<input type="number" class="form-control" id="numero_jornal" name="numero_jornal">
</div>
<div class="mb-3">
<label for="numero_inicial_assinatura" class="form-label">Número Inicial da Assinatura:</label>
<input type="number" class="form-control" id="numero_inicial_assinatura" name="numero_inicial_assinatura">
</div>
<div class="mb-3">
<label for="numero_final_assinatura" class="form-label">Número Final da Assinatura:</label>
<input type="number" class="form-control" id="numero_final_assinatura" name="numero_final_assinatura">
</div>
<div class="mb-3">
<label for="campanha_financeira" class="form-label">Campanha Financeira:</label>
<input type="text" class="form-control" id="campanha_financeira" name="campanha_financeira">
</div>
<div class="mb-3">

View File

@@ -1,30 +1,92 @@
{% extends 'base.html' %}
{% block title %}Listar Militantes{% endblock %}
{% block title %}Novo Relatório de Cotas{% endblock %}
{% block content %}
<h1>Registrar Novo Relatório de Cotas</h1>
<form method="post">
<div>
<label for="setor_id">ID do Setor:</label>
<input type="number" id="setor_id" name="setor_id" required>
</div>
<div>
<label for="comite_id">ID do Comitê:</label>
<input type="number" id="comite_id" name="comite_id" required>
</div>
<div>
<label for="total_cotas">Total de Cotas:</label>
<input type="number" id="total_cotas" name="total_cotas" step="0.01" required>
</div>
<div>
<label for="data_relatorio">Data do Relatório:</label>
<input type="date" id="data_relatorio" name="data_relatorio" required>
</div>
<button type="submit">Registrar Relatório</button>
</form>
<a href="{{ url_for('listar_relatorios_cotas') }}">Voltar para Lista</a>
<a href="{{ url_for('home') }}">Home</a>
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Novo Relatório de Cotas</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="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 }}">{{ setor.nome }}</option>
{% endfor %}
</select>
<div class="invalid-feedback">
Por favor, selecione o setor.
</div>
</div>
<div class="mb-3">
<label for="comite_id" class="form-label">Comitê Central</label>
<select class="form-select" id="comite_id" name="comite_id" required>
<option value="">Selecione um comitê</option>
{% for comite in comites %}
<option value="{{ comite.id }}">{{ comite.nome }}</option>
{% endfor %}
</select>
<div class="invalid-feedback">
Por favor, selecione o comitê central.
</div>
</div>
<div class="mb-3">
<label for="total_cotas" class="form-label">Total de Cotas</label>
<input type="number" class="form-control" id="total_cotas" name="total_cotas" step="0.01" required>
<div class="invalid-feedback">
Por favor, insira o total de cotas.
</div>
</div>
<div class="mb-3">
<label for="data_relatorio" class="form-label">Data do Relatório</label>
<input type="date" class="form-control" id="data_relatorio" name="data_relatorio" required>
<div class="invalid-feedback">
Por favor, insira a data do relatório.
</div>
</div>
<div class="d-flex justify-content-between">
<button type="submit" class="btn btn-success">Registrar</button>
<a href="{{ url_for('listar_relatorios_cotas') }}" class="btn btn-outline-secondary">Voltar</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 %}

View File

@@ -0,0 +1,91 @@
{% extends 'base.html' %}
{% block title %}Novo Relatório de Pagamentos{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Novo Relatório de Pagamentos</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="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 }}">{{ setor.nome }}</option>
{% endfor %}
</select>
<div class="invalid-feedback">
Por favor, selecione o setor.
</div>
</div>
<div class="mb-3">
<label for="comite_id" class="form-label">Comitê Central</label>
<select class="form-select" id="comite_id" name="comite_id" required>
<option value="">Selecione um comitê</option>
{% for comite in comites %}
<option value="{{ comite.id }}">{{ comite.nome }}</option>
{% endfor %}
</select>
<div class="invalid-feedback">
Por favor, selecione o comitê central.
</div>
</div>
<div class="mb-3">
<label for="total_pagamentos" class="form-label">Total de Pagamentos</label>
<input type="number" class="form-control" id="total_pagamentos" name="total_pagamentos" step="0.01" required>
<div class="invalid-feedback">
Por favor, insira o total de pagamentos.
</div>
</div>
<div class="mb-3">
<label for="data_relatorio" class="form-label">Data do Relatório</label>
<input type="date" class="form-control" id="data_relatorio" name="data_relatorio" required>
<div class="invalid-feedback">
Por favor, insira a data do relatório.
</div>
</div>
<div class="d-flex justify-content-between">
<button type="submit" class="btn btn-success">Registrar</button>
<a href="{{ url_for('listar_relatorios_pagamentos') }}" class="btn btn-outline-secondary">Voltar</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 %}

View File

@@ -1,30 +1,91 @@
{% extends 'base.html' %}
{% block title %}Listar Militantes{% endblock %}
{% block title %}Novo Relatório de Vendas{% endblock %}
{% block content %}
<h1>Registrar Novo Relatório de Vendas</h1>
<form method="post">
<div>
<label for="setor_id">ID do Setor:</label>
<input type="number" id="setor_id" name="setor_id" required>
</div>
<div>
<label for="comite_id">ID do Comitê:</label>
<input type="number" id="comite_id" name="comite_id" required>
</div>
<div>
<label for="total_vendas">Total de Vendas:</label>
<input type="number" id="total_vendas" name="total_vendas" step="0.01" required>
</div>
<div>
<label for="data_relatorio">Data do Relatório:</label>
<input type="date" id="data_relatorio" name="data_relatorio" required>
</div>
<button type="submit">Registrar Relatório</button>
</form>
<a href="{{ url_for('listar_relatorios_vendas') }}">Voltar para Lista</a>
<a href="{{ url_for('home') }}">Home</a>
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Novo Relatório de Vendas</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="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 }}">{{ setor.nome }}</option>
{% endfor %}
</select>
<div class="invalid-feedback">
Por favor, selecione o setor.
</div>
</div>
<div class="mb-3">
<label for="comite_id" class="form-label">Comitê Central</label>
<select class="form-select" id="comite_id" name="comite_id" required>
<option value="">Selecione um comitê</option>
{% for comite in comites %}
<option value="{{ comite.id }}">{{ comite.nome }}</option>
{% endfor %}
</select>
<div class="invalid-feedback">
Por favor, selecione o comitê central.
</div>
</div>
<div class="mb-3">
<label for="total_vendas" class="form-label">Total de Vendas</label>
<input type="number" class="form-control" id="total_vendas" name="total_vendas" step="0.01" required>
<div class="invalid-feedback">
Por favor, insira o total de vendas.
</div>
</div>
<div class="mb-3">
<label for="data_relatorio" class="form-label">Data do Relatório</label>
<input type="date" class="form-control" id="data_relatorio" name="data_relatorio" required>
<div class="invalid-feedback">
Por favor, insira a data do relatório.
</div>
</div>
<div class="d-flex justify-content-between">
<button type="submit" class="btn btn-success">Registrar</button>
<a href="{{ url_for('listar_relatorios_vendas') }}" class="btn btn-outline-secondary">Voltar</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/novo_setor.html Normal file
View File

@@ -0,0 +1,94 @@
{% extends 'base.html' %}
{% block title %}Novo Setor{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Novo Setor</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 do setor.
</div>
</div>
<div class="col-md-6 mb-3">
<label for="comite_regional_id" class="form-label">Comitê Regional</label>
<select class="form-select" id="comite_regional_id" name="comite_regional_id" required>
<option value="">Selecione um comitê regional</option>
{% for comite in comites %}
<option value="{{ comite.id }}">{{ comite.nome }}</option>
{% endfor %}
</select>
<div class="invalid-feedback">
Por favor, selecione um comitê regional.
</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 }}">{{ 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 }}">{{ 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_setores') }}" 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 %}

View File

@@ -0,0 +1,74 @@
{% extends 'base.html' %}
{% block title %}Novo Tipo de Material{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Novo Tipo de Material</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 do tipo de material.
</div>
</div>
<div class="col-md-6 mb-3">
<label for="preco" class="form-label">Preço</label>
<input type="number" class="form-control" id="preco" name="preco" step="0.01" min="0" required>
<div class="invalid-feedback">
Por favor, insira o preço do tipo de material.
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 mb-3">
<label for="descricao" class="form-label">Descrição</label>
<textarea class="form-control" id="descricao" name="descricao" rows="3"></textarea>
</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_tipos_materiais') }}" 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 %}

View File

@@ -0,0 +1 @@

View File

@@ -1,58 +0,0 @@
{% extends 'base.html' %}
{% block title %}Novo Usuário{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-8 offset-md-2">
<div class="card">
<div class="card-header">
<h3>Cadastro de Novo Usuário</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">
<div class="mb-3">
<label for="username" class="form-label">Nome de usuário</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="email" class="form-label">E-mail</label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Senha</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<div class="mb-3">
<label for="confirm_password" class="form-label">Confirmar Senha</label>
<input type="password" class="form-control" id="confirm_password" name="confirm_password" required>
</div>
{% if session.get('is_admin') %}
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="is_admin" name="is_admin">
<label class="form-check-label" for="is_admin">Usuário Administrador</label>
</div>
{% endif %}
<button type="submit" class="btn btn-primary">Cadastrar</button>
<a href="{{ url_for('home') }}" class="btn btn-secondary">Voltar</a>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

64
templates/novo_venda.html Normal file
View File

@@ -0,0 +1,64 @@
{% extends 'base.html' %}
{% block title %}Nova Venda{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-8 offset-md-2">
<h1 class="mb-4">Registrar Nova Venda</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="mb-4">
<div class="mb-3">
<label for="militante_id" class="form-label">Militante:</label>
<select class="form-select" id="militante_id" name="militante_id" required>
<option value="">Selecione o militante</option>
{% for militante in militantes %}
<option value="{{ militante.id }}">{{ militante.nome }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="material_id" class="form-label">Material:</label>
<select class="form-select" id="material_id" name="material_id" required>
<option value="">Selecione o material</option>
{% for material in materiais %}
<option value="{{ material.id }}">{{ material.descricao }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="quantidade" class="form-label">Quantidade:</label>
<input type="number" class="form-control" id="quantidade" name="quantidade" min="1" required>
</div>
<div class="mb-3">
<label for="valor_total" class="form-label">Valor Total:</label>
<input type="number" class="form-control" id="valor_total" name="valor_total" step="0.01" required>
</div>
<div class="mb-3">
<label for="data_venda" class="form-label">Data da Venda:</label>
<input type="date" class="form-control" id="data_venda" name="data_venda" required>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">Registrar</button>
<a href="{{ url_for('listar_vendas') }}" class="btn btn-secondary">Voltar</a>
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

205
tests/test_permissions.py Normal file
View File

@@ -0,0 +1,205 @@
import unittest
from flask import Flask, g
from functions.database import db, Militante, Celula, Setor, CR, CC
from functions.permissions import (
can_manage_militante, can_manage_celula, can_manage_setor, can_manage_cr, can_manage_cc,
can_manage_financas, can_manage_imprensa, can_manage_responsabilidades
)
class TestPermissions(unittest.TestCase):
def setUp(self):
self.app = Flask(__name__)
self.app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
self.app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db.init_app(self.app)
with self.app.app_context():
db.create_all()
# Criar instâncias
self.cc = CC(nome='CC 1')
db.session.add(self.cc)
self.cr = CR(nome='CR 1', cc=self.cc)
db.session.add(self.cr)
self.setor = Setor(nome='Setor 1', cr=self.cr)
db.session.add(self.setor)
self.celula = Celula(nome='Célula 1', setor=self.setor)
db.session.add(self.celula)
# Criar militantes com diferentes responsabilidades
self.militante_basico = Militante(
nome='Militante Básico',
email='basico@example.com',
celula=self.celula,
responsabilidades=Militante.MILITANTE_BASICO
)
db.session.add(self.militante_basico)
self.secretario_celula = Militante(
nome='Secretário de Célula',
email='celula@example.com',
celula=self.celula,
responsabilidades=Militante.SECRETARIO_CELULA
)
db.session.add(self.secretario_celula)
self.secretario_setor = Militante(
nome='Secretário de Setor',
email='setor@example.com',
celula=self.celula,
responsabilidades=Militante.SECRETARIO_SETOR
)
db.session.add(self.secretario_setor)
self.secretario_cr = Militante(
nome='Secretário de CR',
email='cr@example.com',
celula=self.celula,
responsabilidades=Militante.SECRETARIO_CR
)
db.session.add(self.secretario_cr)
self.secretario_cc = Militante(
nome='Secretário de CC',
email='cc@example.com',
celula=self.celula,
responsabilidades=Militante.SECRETARIO_CC
)
db.session.add(self.secretario_cc)
self.secretario_geral = Militante(
nome='Secretário Geral',
email='geral@example.com',
celula=self.celula,
responsabilidades=Militante.SECRETARIO_GERAL
)
db.session.add(self.secretario_geral)
self.responsavel_financas = Militante(
nome='Responsável de Finanças',
email='financas@example.com',
celula=self.celula,
responsabilidades=Militante.RESPONSAVEL_FINANCAS
)
db.session.add(self.responsavel_financas)
self.responsavel_imprensa = Militante(
nome='Responsável de Imprensa',
email='imprensa@example.com',
celula=self.celula,
responsabilidades=Militante.RESPONSAVEL_IMPRENSA
)
db.session.add(self.responsavel_imprensa)
# Atribuir responsáveis às instâncias
self.celula.responsavel_geral = self.secretario_celula
self.celula.responsavel_financas = self.responsavel_financas
self.celula.responsavel_imprensa = self.responsavel_imprensa
self.setor.responsavel_geral = self.secretario_setor
self.setor.responsavel_financas = self.responsavel_financas
self.setor.responsavel_imprensa = self.responsavel_imprensa
self.cr.responsavel_geral = self.secretario_cr
self.cr.responsavel_financas = self.responsavel_financas
self.cr.responsavel_imprensa = self.responsavel_imprensa
self.cc.responsavel_geral = self.secretario_cc
self.cc.responsavel_financas = self.responsavel_financas
self.cc.responsavel_imprensa = self.responsavel_imprensa
db.session.commit()
def test_can_manage_militante(self):
with self.app.app_context():
# Militante básico não pode gerenciar outros militantes
g.user = type('User', (), {'militante': self.militante_basico})
self.assertFalse(can_manage_militante(self.militante_basico.id))
# Secretário de célula pode gerenciar militantes da sua célula
g.user = type('User', (), {'militante': self.secretario_celula})
self.assertTrue(can_manage_militante(self.militante_basico.id))
# Secretário de setor pode gerenciar militantes do seu setor
g.user = type('User', (), {'militante': self.secretario_setor})
self.assertTrue(can_manage_militante(self.militante_basico.id))
# Secretário de CR pode gerenciar militantes do seu CR
g.user = type('User', (), {'militante': self.secretario_cr})
self.assertTrue(can_manage_militante(self.militante_basico.id))
# Secretário de CC pode gerenciar militantes do seu CC
g.user = type('User', (), {'militante': self.secretario_cc})
self.assertTrue(can_manage_militante(self.militante_basico.id))
# Secretário Geral pode gerenciar qualquer militante
g.user = type('User', (), {'militante': self.secretario_geral})
self.assertTrue(can_manage_militante(self.militante_basico.id))
def test_can_manage_financas(self):
with self.app.app_context():
# Militante básico não pode gerenciar finanças
g.user = type('User', (), {'militante': self.militante_basico})
self.assertFalse(can_manage_financas(self.celula.id, 'celula'))
# Responsável de finanças pode gerenciar finanças da sua instância
g.user = type('User', (), {'militante': self.responsavel_financas})
self.assertTrue(can_manage_financas(self.celula.id, 'celula'))
self.assertTrue(can_manage_financas(self.setor.id, 'setor'))
self.assertTrue(can_manage_financas(self.cr.id, 'cr'))
self.assertTrue(can_manage_financas(self.cc.id, 'cc'))
# Secretário Geral pode gerenciar finanças de qualquer instância
g.user = type('User', (), {'militante': self.secretario_geral})
self.assertTrue(can_manage_financas(self.celula.id, 'celula'))
self.assertTrue(can_manage_financas(self.setor.id, 'setor'))
self.assertTrue(can_manage_financas(self.cr.id, 'cr'))
self.assertTrue(can_manage_financas(self.cc.id, 'cc'))
def test_can_manage_imprensa(self):
with self.app.app_context():
# Militante básico não pode gerenciar imprensa
g.user = type('User', (), {'militante': self.militante_basico})
self.assertFalse(can_manage_imprensa(self.celula.id, 'celula'))
# Responsável de imprensa pode gerenciar imprensa da sua instância
g.user = type('User', (), {'militante': self.responsavel_imprensa})
self.assertTrue(can_manage_imprensa(self.celula.id, 'celula'))
self.assertTrue(can_manage_imprensa(self.setor.id, 'setor'))
self.assertTrue(can_manage_imprensa(self.cr.id, 'cr'))
self.assertTrue(can_manage_imprensa(self.cc.id, 'cc'))
# Secretário Geral pode gerenciar imprensa de qualquer instância
g.user = type('User', (), {'militante': self.secretario_geral})
self.assertTrue(can_manage_imprensa(self.celula.id, 'celula'))
self.assertTrue(can_manage_imprensa(self.setor.id, 'setor'))
self.assertTrue(can_manage_imprensa(self.cr.id, 'cr'))
self.assertTrue(can_manage_imprensa(self.cc.id, 'cc'))
def test_can_manage_responsabilidades(self):
with self.app.app_context():
# Militante básico não pode gerenciar responsabilidades
g.user = type('User', (), {'militante': self.militante_basico})
self.assertFalse(can_manage_responsabilidades(self.militante_basico.id))
# Secretário de setor pode gerenciar responsabilidades do seu setor
g.user = type('User', (), {'militante': self.secretario_setor})
self.assertTrue(can_manage_responsabilidades(self.militante_basico.id))
# Secretário de CR pode gerenciar responsabilidades do seu CR
g.user = type('User', (), {'militante': self.secretario_cr})
self.assertTrue(can_manage_responsabilidades(self.militante_basico.id))
# Secretário de CC pode gerenciar responsabilidades do seu CC
g.user = type('User', (), {'militante': self.secretario_cc})
self.assertTrue(can_manage_responsabilidades(self.militante_basico.id))
# Secretário Geral pode gerenciar responsabilidades de qualquer militante
g.user = type('User', (), {'militante': self.secretario_geral})
self.assertTrue(can_manage_responsabilidades(self.militante_basico.id))
if __name__ == '__main__':
unittest.main()