66 Commits

Author SHA1 Message Date
andersonid
a302a259a6 feat: implementa paginação e contagem correta na lista de militantes 2025-04-04 13:31:52 -03:00
andersonid
75ba696355 feat: adiciona modal global para edição de militantes na home 2025-04-04 13:02:23 -03:00
andersonid
7f4fe77711 fix: corrige estilos dos botões nos modais para manter cores e visibilidade no hover 2025-04-04 13:01:22 -03:00
andersonid
c29eed0c69 refactor: Remove filtro de Filiados/Não Filiados da listagem de militantes 2025-04-04 12:22:05 -03:00
andersonid
52a6bf9eb0 fix: Atualiza valores dos checkboxes de responsabilidades no template do modal 2025-04-04 12:16:41 -03:00
andersonid
d468f8ff39 fix: Corrige processamento das responsabilidades no modal de edição dos militantes 2025-04-04 12:15:19 -03:00
andersonid
5527db8729 chore: Ignora arquivos de QR code gerados dinamicamente 2025-04-04 11:39:15 -03:00
andersonid
56b8e7aa54 feat: Melhorias de segurança e interface - Segurança: Implementação de CSRF token em formulários, validação no backend, proteção AJAX - QR Code: Preservação do otp_secret, evita geração desnecessária - Interface: Correções visuais, padronização de cores, melhorias em formulários 2025-04-04 11:37:48 -03:00
andersonid
9ffc562357 feat: implementa filtros e pesquisa AJAX na interface de militantes 2025-04-04 09:49:06 -03:00
andersonid
3ed3002410 style: padroniza altura dos botões na interface de militantes 2025-04-04 09:48:14 -03:00
andersonid
f58c340235 chore: remove arquivo .pyc do controle de versão 2025-04-04 09:26:22 -03:00
andersonid
9158a86655 chore: atualiza QR code do admin 2025-04-04 09:25:08 -03:00
andersonid
6b23adcb34 chore: atualiza Makefile para melhor limpeza e inicialização do ambiente 2025-04-04 09:24:56 -03:00
andersonid
c7c3b95f0b refactor: melhora processo de seed de dados com melhor tratamento de erros e concorrência 2025-04-04 09:24:45 -03:00
andersonid
9bb62c81a7 fix: ajusta configurações do SQLite para melhor concorrência e tratamento de locks 2025-04-04 09:24:34 -03:00
andersonid
c17a3eaa0f fix: corrige exibição das responsabilidades dos militantes na interface /militantes 2025-04-04 09:23:45 -03:00
andersonid
07605797d1 . 2025-04-04 02:35:54 -03:00
andersonid
745803fef3 fix: corrige problemas de permissões e rotas 2025-04-04 02:34:51 -03:00
andersonid
241543ea63 feat: melhorias na interface da home e navbar - Ajustes no layout da navbar e menu - Correção do logo e nome do sistema - Melhorias no estilo dos cards da dashboard - Ajustes nas permissões e autenticação - Correção de bugs na exibição de mensagens 2025-04-03 20:58:02 -03:00
andersonid
50516664e4 feat: melhoria no layout da tela de login - ajustes no container, responsividade e estilização dos campos 2025-04-03 20:29:07 -03:00
andersonid
0447524a91 feat: padroniza tela de pagamentos - Adiciona DataTables, modais e funcionalidades CRUD 2025-04-03 18:28:03 -03:00
andersonid
77cf5ad99c fix: corrige edição de cotas - Ajusta exibição do militante e valores no modal de edição 2025-04-03 18:28:03 -03:00
andersonid
9cc3f408f8 feat: padroniza componentes e estilos globais - Cria components.css, ajusta cores e organiza modais 2025-04-03 18:27:37 -03:00
andersonid
758dbdb26d fix: corrige funcionamento do modal de edição de militantes - Corrige bloco de scripts e melhora tratamento de dados 2025-04-03 18:26:41 -03:00
andersonid
83ae798033 fix: corrige fluxo de modais na home - Ajusta modal de militantes para abrir detalhes primeiro - Corrige preenchimento de dados nos modais - Mantém fluxo: detalhes -> editar/excluir 2025-04-03 18:24:57 -03:00
andersonid
742f820bc2 style: melhora layout e espaçamento das interfaces - Ajusta margens e padding do container principal - Adiciona page-wrapper com espaçamento consistente - Corrige cores do menu para melhor legibilidade - Implementa responsividade para diferentes tamanhos de tela - Mantém estrutura consistente em todas as páginas 2025-04-03 18:24:07 -03:00
andersonid
a28f543478 refactor: melhorias na interface e funcionalidades - Atualização do layout do dashboard com Bootstrap 5 - Remoção do template editar_pagamento.html (integrado ao modal) - Melhorias no template home.html com cards estatísticos - Ajustes nos estilos e responsividade - Correções nas rotas e conexões do banco de dados - Implementação do modal de edição de pagamentos - Adição de efeitos hover e melhorias visuais 2025-04-03 18:23:08 -03:00
andersonid
417b5c3f96 fix: restaura layout da dashboard e corrige exibição das listas 2025-04-03 18:17:46 -03:00
andersonid
10ff9cab3b feat: Melhorias na interface do modal de detalhes do militante - Adicionado efeito de blur consistente em todos os modais - Corrigido estilo do botão de editar para manter a cor azul padrão - Adicionado backdrop-filter no modal de confirmação de exclusão - Melhorias na responsividade dos modais para dispositivos móveis - Ajustado comportamento de fechamento dos modais 2025-04-03 17:59:52 -03:00
andersonid
8803c971e4 feat: Melhorias no Dashboard
- Interface:
  - Adicionada saudação personalizada com nome do usuário
  - Melhorado formato da data em português
  - Ajustado layout do header com gradiente e sombra
  - Corrigida categoria de mensagens flash de 'error' para 'danger'

- Card de Cotas:
  - Reorganizado layout para melhor exibição de valores grandes
  - Ajustado tamanho da fonte usando calc(1.2rem + 0.8vw)
  - Adicionado container específico para valor com min-width: 0
  - Redimensionado e reposicionado ícone
  - Melhorado espaçamento e alinhamento

- Lista de Militantes:
  - Ajustada query para ordenar por ID
  - Removida dependência da coluna created_at
  - Adicionado ID do militante na listagem

- Estilos:
  - Adicionadas classes valor-container e icon-container
  - Melhorado responsividade dos valores monetários
  - Ajustado gradiente no header de boas-vindas
  - Refinado espaçamento e margens dos componentes
2025-04-03 17:59:41 -03:00
andersonid
d4869dcfaa feat: adiciona script de dados fictícios e padroniza tratamento de erros 2025-04-03 17:56:44 -03:00
andersonid
06e7c79488 refactor: padroniza tratamento de erro na listagem de militantes 2025-04-03 17:54:29 -03:00
andersonid
0a2d5c1d23 fix: corrige tratamento de erro na listagem de cotas 2025-04-03 17:54:29 -03:00
andersonid
855f97c72b feat: moderniza a página de listagem de cotas 2025-04-03 17:54:25 -03:00
andersonid
8e6ccb70e9 fix: corrige blocos não fechados no template de listagem de militantes 2025-04-03 17:53:24 -03:00
andersonid
65406276ae feat: adiciona hover vermelho nos menus e centraliza data no mobile 2025-04-03 17:53:24 -03:00
andersonid
b1acc2fdfc fix: corrige referências dos logos de acordo com o fundo 2025-04-03 17:53:24 -03:00
andersonid
c44ce94bef refactor: simplifica e moderniza a tela de login 2025-04-03 17:53:17 -03:00
andersonid
ce3b5a4231 refactor: melhorias na UI - formulário de pagamento e cards 2025-04-03 17:52:30 -03:00
andersonid
f0faf4270b refactor: melhorias na UI - navbar, logo e layout da data 2025-04-03 17:51:27 -03:00
andersonid
178a58bb00 fix: correções no logo, remoção de mensagem de login e auto-dismiss de alertas 2025-04-03 17:49:42 -03:00
andersonid
e9c1f3aedf fix: correções no logo, degradê e alertas 2025-04-03 17:48:47 -03:00
andersonid
1ff8e97bbc fix: ajustes visuais - degradê, card branco, logo e barra vermelha 2025-04-03 17:48:41 -03:00
andersonid
b815f77240 refactor: melhorias na interface de login e alertas, foco em mobile-first 2025-04-03 17:47:05 -03:00
andersonid
ba4f6d6de3 fix: ajustes no logo e nome do sistema em todas as interfaces 2025-04-03 17:44:44 -03:00
andersonid
ac461ce800 fix: removido Flask-Moment e adicionada data formatada em português 2025-04-03 17:41:29 -03:00
andersonid
4f781b2a0e refactor: ajustes na interface - logo, nome do sistema e layout da home 2025-04-03 17:41:29 -03:00
andersonid
32cd4b70c1 feat: atualização da identidade visual com cores e logo da OCI 2025-04-03 17:41:29 -03:00
andersonid
54261e455c feat: melhorias na interface e estrutura do frontend 2025-04-03 17:41:29 -03:00
LS
9d17c66c46 fix: corrige uso do decorator require_instance_permission - Modifica o decorator para aceitar o nome do parâmetro da instância - Atualiza as rotas de pagamentos para usar o decorator corretamente - Adiciona verificação do ID da instância nos argumentos da função - Melhora mensagens de erro para casos de permissão negada 2025-04-03 17:10:43 -03:00
LS
cbaf227e58 feat: implementa sistema de responsabilidades e instâncias - Adiciona responsabilidades de Finanças e Imprensa para todas as instâncias - Cria templates genéricos para gerenciamento de instâncias - Implementa sistema de permissões baseado em RBAC - Adiciona status de Aspirante com avaliação obrigatória - Atualiza documentação com novas regras e responsabilidades - Cria testes para validação das permissões - Adiciona migração para novos campos no banco de dados 2025-04-03 15:58:07 -03:00
LS
8dac8dc234 Limapdo o repo e corrigidos bugs 2025-04-03 11:24:47 -03:00
LS
bf93e84cec Adicionados os campos mínimos do Banco de Dados, precisa melhorar interface e controle de acesso que será posterior nessa branch mesmo 2025-04-03 10:30:48 -03:00
LS
449a203926 feat rbac com adicionados os novos campos para hierarquia 2025-04-01 15:27:16 -03:00
Levy Sant'Anna
01f5901eb2 Merge pull request #22 from ComunaTec/login
Login
2025-03-31 12:03:24 -03:00
LS
6370e8f39b Atualizado o README.md 2025-03-27 14:49:29 -03:00
LS
bae6b1ae14 Login finalizado, admin funcionando corretamente e sendo gerado oQRcode na raiz do projeto 2025-03-27 14:34:16 -03:00
LS
1367389619 adicionado timeout no login e botão de Sair 2025-03-24 16:34:38 -03:00
LS
0f4056fbff Login funcionando 2025-03-24 14:50:42 -03:00
Levy Sant'Anna
cccca2ef29 Merge pull request #20 from ComunaTec/main
remove pycache
2025-03-24 09:03:26 -03:00
LS
986f90a9cd continuando 2025-03-18 17:36:42 -03:00
LS
14c88bb1e4 Login ainda nao funciona mas esta quase 2025-03-18 17:31:59 -03:00
LS
aa22102b5a adicionando login - ainda precisa corrigir 2025-02-28 13:47:22 -03:00
Gabu Bellon
0d2238d8e0 remove pycache 2025-02-24 20:39:46 -03:00
LS
de132b82c1 adicionei bootstrap flask no requirements 2025-02-22 11:54:17 -03:00
Levy Sant'Anna
a847389295 Merge pull request #6 from ComunaTec/cota_calc
Cota calc
2025-02-20 10:41:29 -03:00
104 changed files with 15074 additions and 1519 deletions

8
.gitignore vendored
View File

@@ -260,5 +260,13 @@ poetry.toml
pyrightconfig.json
database.db
admin_qr.png
# End of https://www.toptal.com/developers/gitignore/api/python,flask
# Documentação temporária
docs/alteracoes_db_connection.md
# QR Codes
*_qr.png
*_qr.txt

View File

@@ -1,5 +1,18 @@
install:
pip install -r requirements.txt
clean:
rm -rf ~/.local/share/controles/database.db
rm -f admin_qr.png
run:
python app.py
seed:
python seed.py
run-with-seed: clean
python app.py & sleep 5 && python seed.py
reset-admin: clean
python create_admin.py

137
README.md
View File

@@ -1,15 +1,134 @@
# 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
```
## Para executar
## Estrutura do Banco de Dados
```bash
make run
```
O sistema utiliza as seguintes tabelas para o RBAC:
Acesse por: http://127.0.0.1:5000
- `roles`: Armazena os papéis disponíveis
- `permissions`: Armazena as permissões disponíveis
- `role_permissions`: Mapeia papéis para permissões
- `user_roles`: Mapeia usuários para papéis
## Segurança
- Todas as senhas são armazenadas com hash bcrypt
- Sessões expiram após período de inatividade
- Controle de acesso granular baseado em papéis
- Proteção contra CSRF
- Validação de entrada de dados

Binary file not shown.

1759
app.py

File diff suppressed because it is too large Load Diff

142
create_admin.py Normal file
View File

@@ -0,0 +1,142 @@
from functions.database import init_database, Usuario, Role, get_db_connection
import qrcode
import os
from pathlib import Path
import pyotp
def generate_qr_code(user):
"""
Gera o QR code para um usuário específico
Args:
user: Instância do modelo Usuario
Returns:
Path: Caminho do arquivo QR code gerado
"""
# Gerar QR Code apenas na raiz do projeto
qr_path = Path('admin_qr.png')
# Remover arquivo antigo se existir
if qr_path.exists():
os.remove(str(qr_path))
# Gerar e salvar QR Code
qr = qrcode.QRCode(version=1, box_size=10, border=5)
# Gerar URI do OTP
totp = pyotp.TOTP(user.otp_secret)
otp_uri = totp.provisioning_uri(
name=user.username,
issuer_name="Sistema de Controles"
)
qr.add_data(otp_uri)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
img.save(str(qr_path))
print(f"\nQR Code gerado em: {os.path.abspath(qr_path)}")
return qr_path, otp_uri
def create_admin_user():
"""Cria ou atualiza o usuário admin"""
try:
# Inicializar banco de dados
init_database()
# Criar sessão
db = get_db_connection()
try:
# Verificar se já existe um usuário admin
admin = db.query(Usuario).filter_by(username="admin").first()
if admin:
print("\n=== Usuário Admin Encontrado ===")
if not admin.otp_secret:
print("Gerando novo segredo OTP...")
admin.generate_otp_secret()
db.commit()
else:
print("\n=== Criando Novo Usuário Admin ===")
# Criar novo usuário admin
admin = Usuario(
username="admin",
email="admin@example.com",
is_admin=True
)
admin.set_password("admin123")
admin.generate_otp_secret()
# Adicionar e fazer commit
db.add(admin)
db.commit()
# Gerar QR code apenas se solicitado ou se for novo usuário
if not os.path.exists('admin_qr.png'):
qr_path, otp_uri = generate_qr_code(admin)
print("\n=== QR Code Gerado ===")
print(f"QR Code salvo em: {qr_path}")
print(f"URI do OTP: {otp_uri}")
else:
print("\n=== QR Code Existente ===")
print("Usando QR Code existente em: admin_qr.png")
qr_path = 'admin_qr.png'
# Mostrar informações
print("\n=== Informações do Admin ===")
print(f"Username: {admin.username}")
print(f"Email: {admin.email}")
print(f"Senha: admin123")
print(f"Segredo OTP: {admin.otp_secret}")
# Gerar código atual para verificação
totp = pyotp.TOTP(admin.otp_secret)
current_code = totp.now()
print("\n=== Verificação do OTP ===")
print(f"Código OTP atual: {current_code}")
print(f"Verificação do código: {totp.verify(current_code)}")
print("\n=== Instruções para Configuração ===")
print("1. Instale um aplicativo autenticador no seu celular")
print(" (Google Authenticator, Microsoft Authenticator, etc)")
print("2. Abra o aplicativo")
print("3. Selecione a opção para adicionar uma nova conta")
print("4. Escaneie o QR Code salvo em:", qr_path)
print("\nOU configure manualmente:")
print(f"- Nome da conta: {admin.username}")
print(f"- Segredo: {admin.otp_secret}")
print("- Tipo: Baseado em tempo (TOTP)")
print("- Algoritmo: SHA1")
print("- Dígitos: 6")
print("- Intervalo: 30 segundos")
# Verificação final
print("\n=== Teste de Verificação ===")
test_code = totp.now()
print(f"Código de teste: {test_code}")
is_valid = admin.verify_otp(test_code)
print(f"Verificação do código: {'Sucesso' if is_valid else 'Falha'}")
if not is_valid:
print("\nALERTA: Verificação do OTP falhou!")
print("Por favor, verifique se o segredo OTP está correto.")
# Fazer commit final para garantir que tudo foi salvo
db.commit()
except Exception as e:
db.rollback()
raise e
finally:
db.close()
except Exception as e:
print(f"\nErro durante a execução: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
create_admin_user()

130
create_test_users.py Normal file
View File

@@ -0,0 +1,130 @@
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'],
email=user_data['email'],
is_admin=user_data['is_admin']
)
user.set_password(user_data['password'])
user.tipo = "ADMIN" if user_data['is_admin'] else "USUARIO"
# 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
else:
# Gerar novo OTP para outros usuários
user.otp_secret = pyotp.random_base32()
db.add(user)
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!")
# Gerar QR code para o novo usuário
qr_path = f"{user_data['username']}_qr.png"
if not os.path.exists(qr_path):
totp = pyotp.TOTP(user.otp_secret)
qr = qrcode.QRCode(version=1, box_size=10, border=5)
qr.add_data(totp.provisioning_uri(user.email, issuer_name="Sistema de Controles"))
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
img.save(qr_path)
print(f"QR Code gerado para {user_data['username']} em: {qr_path}")
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")
elif not user.otp_secret:
# Se não tiver OTP, gerar um novo
user.otp_secret = pyotp.random_base32()
db.commit()
print(f"Novo OTP gerado para {user_data['username']}")
# Gerar QR code
qr_path = f"{user_data['username']}_qr.png"
if not os.path.exists(qr_path):
totp = pyotp.TOTP(user.otp_secret)
qr = qrcode.QRCode(version=1, box_size=10, border=5)
qr.add_data(totp.provisioning_uri(user.email, issuer_name="Sistema de Controles"))
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
img.save(qr_path)
print(f"QR Code gerado para {user_data['username']} em: {qr_path}")
# 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

View File

@@ -0,0 +1,54 @@
# Alterações no Gerenciamento de Conexões com o Banco de Dados
## Commit
- ID: [ID do commit será adicionado após o commit]
- Data: [Data do commit]
- Autor: [Nome do autor]
## Contexto
O sistema estava utilizando uma única sessão global do SQLAlchemy (`db_session`) que era criada no início da aplicação. Isso poderia causar problemas de concorrência e vazamento de recursos.
## Alterações Realizadas
### 1. Remoção da Sessão Global
- Removida a linha `db_session = get_db_connection()` do início do arquivo
- Todas as rotas agora criam sua própria sessão
### 2. Novo Padrão de Gerenciamento de Sessão
Em cada rota, implementamos o seguinte padrão:
```python
db = get_db_connection()
try:
# Operações com o banco
db.commit()
except Exception as e:
db.rollback()
# Tratamento de erro
finally:
db.close()
```
### 3. Melhorias no Tratamento de Erros
- Adicionado `db.rollback()` em caso de exceção
- Melhoradas as mensagens de erro
- Garantido que a sessão seja fechada mesmo em caso de erro
### 4. Padronização de Código
- Uso de `request.form.get()` ao invés de acessar diretamente o dicionário
- Conversão explícita de tipos (float, int, date)
- Validação de dados antes de criar objetos
- Mensagens de feedback mais claras para o usuário
## Impacto no Frontend
Não houve alterações necessárias nos templates, pois as mudanças foram apenas na forma como o backend gerencia as conexões com o banco de dados.
## Benefícios
1. Maior segurança (evita vazamentos de recursos)
2. Maior robustez (melhor tratamento de erros)
3. Código mais fácil de manter (padronização)
4. Maior eficiência (sessões são fechadas adequadamente)
## Observações
- Esta alteração foi feita para melhorar a arquitetura do sistema
- Não afeta a funcionalidade existente
- Recomenda-se seguir este padrão em novas implementações

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

33
functions/base.py Normal file
View File

@@ -0,0 +1,33 @@
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from pathlib import Path
import os
# Configurar caminho do banco de dados
db_dir = Path.home() / '.local' / 'share' / 'controles'
db_dir.mkdir(parents=True, exist_ok=True)
db_path = db_dir / 'database.db'
# Configurar SQLite com opções para melhor concorrência
engine = create_engine(
f'sqlite:///{db_path}',
connect_args={
'timeout': 30, # Tempo de espera em segundos
'check_same_thread': False # Permite acesso de múltiplas threads
},
pool_pre_ping=True, # Verifica conexão antes de usar
pool_recycle=3600 # Recicla conexões após 1 hora
)
Session = sessionmaker(bind=engine)
Base = declarative_base()
def get_db_connection():
"""Retorna uma nova sessão do banco de dados"""
session = Session()
try:
return session
except Exception as e:
session.rollback()
raise e

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,16 +1,38 @@
from sqlalchemy import create_engine, Column, Integer, String, Boolean, Numeric, Date, ForeignKey
from sqlalchemy.orm import relationship, sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from datetime import datetime, timedelta
from werkzeug.security import generate_password_hash, check_password_hash
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text, Numeric, Date, Enum, create_engine, text
from sqlalchemy.orm import sessionmaker, relationship, backref
import os
import pyotp
from pathlib import Path
from sqlalchemy.pool import NullPool
import secrets
from flask_mail import Message
from flask import url_for
import enum
from flask_login import UserMixin
from .rbac import Role, Permission, role_permissions, user_roles
from .base import Base, engine, Session
import logging
# 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'
Base = declarative_base()
engine = create_engine('sqlite:///database.db', echo=True)
SessionLocal = sessionmaker(bind=engine)
def get_db_connection():
"""
Retorna uma nova sessão do banco de dados
"""
return SessionLocal()
"""Retorna uma nova conexão com o banco de dados"""
db = SessionLocal()
try:
# Configurar SQLite para melhor tratamento de concorrência
db.execute(text("PRAGMA journal_mode=WAL"))
db.execute(text("PRAGMA busy_timeout=5000"))
return db
except:
db.close()
raise
def execute_query(query, params=None):
"""
@@ -27,22 +49,246 @@ def execute_query(query, params=None):
finally:
session.close()
class EstadoMilitante(enum.Enum):
ATIVO = 'ativo'
DESLIGADO = 'desligado'
SUSPENSO = 'suspenso'
AFASTADO = 'afastado'
class Celula(Base):
__tablename__ = 'celulas'
id = Column(Integer, primary_key=True, autoincrement=True)
nome = Column(String(100), nullable=False)
setor_id = Column(Integer, ForeignKey('setores.id'))
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)
# Campos para estado do militante
estado = Column(Enum(EstadoMilitante), default=EstadoMilitante.ATIVO)
data_desligamento = Column(DateTime)
motivo_desligamento = Column(Text)
# Relacionamentos existentes
cotas_mensais = relationship("CotaMensal", back_populates="militante")
pagamentos = relationship("Pagamento", back_populates="militante")
materiais_vendidos = relationship("MaterialVendido", back_populates="militante")
vendas_jornais = relationship("VendaJornalAvulso", back_populates="militante")
assinaturas = relationship("AssinaturaAnual", back_populates="militante")
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'
@@ -52,6 +298,8 @@ class CotaMensal(Base):
valor_antigo = Column(Numeric(10, 2), nullable=False)
valor_novo = Column(Numeric(10, 2), nullable=False)
data_alteracao = Column(Date, nullable=False)
data_vencimento = Column(Date, nullable=False)
pago = Column(Boolean, default=False)
militante = relationship("Militante", back_populates="cotas_mensais")
@@ -61,19 +309,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'
@@ -125,9 +376,18 @@ class AssinaturaAnual(Base):
class Setor(Base):
__tablename__ = 'setores'
id = Column(Integer, primary_key=True, autoincrement=True)
id = Column(Integer, primary_key=True)
nome = Column(String(100), nullable=False)
cr_id = Column(Integer, ForeignKey('comites_regionais.id'))
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")
@@ -164,4 +424,313 @@ class RelatorioVendasMateriais(Base):
setor = relationship("Setor", back_populates="relatorios_vendas")
comite = relationship("ComiteCentral", back_populates="relatorios_vendas")
Base.metadata.create_all(engine)
class TipoUsuario(enum.Enum):
ADMIN = "admin"
CR_RESPONSAVEL = "cr_responsavel"
SETOR_RESPONSAVEL = "setor_responsavel"
USUARIO = "usuario"
class Usuario(Base, UserMixin):
__tablename__ = 'usuarios'
id = Column(Integer, primary_key=True)
username = Column(String(50), unique=True, nullable=False)
password_hash = Column(String(255), nullable=False)
email = Column(String(100), unique=True, nullable=False)
otp_secret = Column(String(32))
role_id = Column(Integer, ForeignKey('roles.id'))
setor_id = Column(Integer, ForeignKey('setores.id'))
ativo = Column(Boolean, default=True)
is_admin = Column(Boolean, default=False)
ultimo_login = Column(DateTime)
ultimo_logout = Column(DateTime)
motivo_logout = Column(String(100))
cr_id = Column(Integer, ForeignKey('comites_regionais.id'))
celula_id = Column(Integer, ForeignKey('celulas.id'))
session_timeout = Column(Integer, default=30)
tipo = Column(String(17), nullable=False)
ultima_atividade = Column(DateTime, default=datetime.utcnow)
# Relacionamento com militante
militante_id = Column(Integer, ForeignKey('militantes.id'))
militante = relationship("Militante", backref=backref("usuario", uselist=False))
# Relacionamentos
roles = relationship("Role", secondary="user_roles", back_populates="users")
setor = relationship('Setor', back_populates='usuarios')
cr = relationship('ComiteRegional', back_populates='usuarios')
celula = relationship('Celula', back_populates='usuarios')
def __init__(self, username, email=None, is_admin=False):
self.username = username
self.email = email
self.is_admin = is_admin
self.email = email
self.ativo = True
self.session_timeout = 30
self.tipo = "USUARIO"
self.ultima_atividade = datetime.utcnow()
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
def update_last_activity(self):
self.ultima_atividade = datetime.utcnow()
def is_session_expired(self):
if not self.ultima_atividade:
return True
time_diff = datetime.utcnow() - self.ultima_atividade
return time_diff.total_seconds() > (self.session_timeout * 60)
def check_session_timeout(self):
"""Verifica se a sessão do usuário expirou"""
if not self.ultima_atividade:
return True
time_diff = datetime.utcnow() - self.ultima_atividade
return time_diff.total_seconds() > (self.session_timeout * 60)
def has_permission(self, permission_name):
"""Verifica se o usuário tem uma permissão específica"""
if self.is_admin: # Se for admin, tem todas as permissões
return True
# Verifica se o usuário tem a permissão através de suas roles
for role in self.roles:
for permission in role.permissions:
if permission.nome == permission_name:
return True
return False
def has_role(self, role_nivel):
"""Verifica se o usuário tem um determinado nível de role"""
for role in self.roles:
if role.nivel == role_nivel:
return True
return False
def get_otp_uri(self):
"""Gera a URI para autenticação em duas etapas"""
if not self.otp_secret:
self.otp_secret = pyotp.random_base32()
return pyotp.totp.TOTP(self.otp_secret).provisioning_uri(
self.username,
issuer_name="Sistema de Controles"
)
def verify_otp(self, code):
"""Verifica se um código OTP é válido"""
if not self.otp_secret:
print(f"Erro: OTP secret não configurado para o usuário {self.username}")
return False
print(f"Verificando OTP para usuário {self.username}")
print(f"OTP Secret: {self.otp_secret}")
print(f"Código fornecido: {code}")
totp = pyotp.totp.TOTP(self.otp_secret)
is_valid = totp.verify(code)
print(f"Resultado da verificação: {'Válido' if is_valid else 'Inválido'}")
print(f"Tempo atual: {datetime.utcnow()}")
print(f"Período atual: {totp.timecode(datetime.utcnow())}")
return is_valid
def logout(self):
"""Registra o logout do usuário"""
self.ultimo_logout = datetime.utcnow()
self.motivo_logout = "Logout manual"
self.ultima_atividade = None
class PagamentoCelula(Base):
__tablename__ = 'pagamentos_celula'
id = Column(Integer, primary_key=True, autoincrement=True)
celula_id = Column(Integer, ForeignKey('celulas.id'))
data = Column(Date)
valor = Column(Numeric(10, 2))
metodo_pagamento = Column(String(20)) # PIX, Dinheiro, etc.
codigo_pix = Column(String(100))
descricao = Column(String(255))
registrado_por = Column(Integer, ForeignKey('militantes.id'))
celula = relationship("Celula", back_populates="pagamentos")
registrado_por_rel = relationship("Militante", foreign_keys=[registrado_por])
class Atividade(Base):
__tablename__ = 'atividades'
id = Column(Integer, primary_key=True, autoincrement=True)
descricao = Column(String(255))
data = Column(Date)
responsavel1 = Column(Integer, ForeignKey('militantes.id'))
responsavel2 = Column(Integer, ForeignKey('militantes.id'))
responsavel1_rel = relationship("Militante", foreign_keys=[responsavel1])
responsavel2_rel = relationship("Militante", foreign_keys=[responsavel2])
materiais = relationship("MaterialAtividade", back_populates="atividade")
class MaterialAtividade(Base):
__tablename__ = 'materiais_atividades'
id = Column(Integer, primary_key=True, autoincrement=True)
atividade_id = Column(Integer, ForeignKey('atividades.id'))
tipo = Column(String(20)) # Jornal, Revista, etc.
quantidade = Column(Integer)
detalhes = Column(String(255))
atividade = relationship("Atividade", back_populates="materiais")
class Relatorio(Base):
__tablename__ = 'relatorios'
id = Column(Integer, primary_key=True, autoincrement=True)
tipo = Column(String(50)) # Semanal, Quinzenal, Mensal
periodo_inicio = Column(Date)
periodo_fim = Column(Date)
gerado_por = Column(Integer, ForeignKey('militantes.id'))
conteudo = Column(Text)
# Relacionamento hierárquico
celula_id = Column(Integer, ForeignKey('celulas.id'))
setor_id = Column(Integer, ForeignKey('setores.id'))
cr_id = Column(Integer, ForeignKey('comites_regionais.id'))
gerado_por_rel = relationship("Militante", foreign_keys=[gerado_por])
celula = relationship("Celula", foreign_keys=[celula_id])
setor = relationship("Setor", foreign_keys=[setor_id])
cr = relationship("ComiteRegional", foreign_keys=[cr_id])
class TransacaoPIX(Base):
__tablename__ = 'transacoes_pix'
id = Column(Integer, primary_key=True, autoincrement=True)
chave_pix = Column(String(100))
valor = Column(Numeric(10, 2))
data_geracao = Column(DateTime)
data_pagamento = Column(DateTime)
status = Column(String(20)) # Pendente, Pago, Expirado
qr_code = Column(Text)
pagamento_id = Column(Integer, ForeignKey('pagamentos.id'))
pagamento = relationship("Pagamento", back_populates="transacoes_pix")
def init_database():
"""Inicializa o banco de dados com dados básicos"""
print("Inicializando banco de dados...")
session = get_db_connection()
try:
# Configurar SQLite para melhor tratamento de concorrência
session.execute(text("PRAGMA journal_mode=WAL"))
session.execute(text("PRAGMA busy_timeout=5000"))
# Criar todas as tabelas
Base.metadata.drop_all(engine) # Remover todas as tabelas existentes
Base.metadata.create_all(engine)
# Criar roles padrão
roles = [
("Administrador", Role.SECRETARIO_GERAL),
("Secretário", Role.SECRETARIO_CELULA),
("Militante", Role.MILITANTE_BASICO)
]
for nome, nivel in roles:
if not session.query(Role).filter_by(nome=nome).first():
role = Role(nome=nome, nivel=nivel)
session.add(role)
session.commit()
# Criar setores padrão
setores = ["Setor 1", "Setor 2", "Setor 3"]
for nome in setores:
if not session.query(Setor).filter_by(nome=nome).first():
setor = Setor(nome=nome)
session.add(setor)
session.commit()
# Criar comitês padrão
comites = ["Comitê 1", "Comitê 2", "Comitê 3"]
for nome in comites:
if not session.query(ComiteCentral).filter_by(nome=nome).first():
comite = ComiteCentral(nome=nome)
session.add(comite)
session.commit()
# Verificar se existe um QR code salvo
qr_path = Path('admin_qr.png')
admin_otp_secret = None
if qr_path.exists():
try:
import re
with open('admin_qr.txt', 'r') as f:
qr_content = f.read()
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_role = session.query(Role).filter_by(nome="Administrador").first()
setor = session.query(Setor).first()
admin = Usuario(
username="admin",
email="admin@example.com",
is_admin=True
)
admin.set_password("admin123")
admin.tipo = "ADMIN"
admin.otp_secret = admin_otp_secret
admin.roles.append(admin_role)
admin.setor = setor
session.add(admin)
session.commit()
# Gerar 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")
with open('admin_qr.txt', 'w') as f:
f.write(provisioning_uri)
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}")
# Importar e executar o seed após criar todas as dependências
from seed_data import seed_database
print("\nPopulando banco de dados com dados de teste...")
seed_database()
print("Dados de teste criados com sucesso!")
except Exception as e:
print(f"Erro na inicialização do banco: {e}")
session.rollback()
raise
finally:
session.close()
if __name__ == "__main__":
init_database()

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.', 'danger')
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.', 'danger')
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.', 'danger')
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.', 'danger')
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

315
functions/rbac.py Normal file
View File

@@ -0,0 +1,315 @@
from sqlalchemy import Column, Integer, String, Text, ForeignKey, Table
from sqlalchemy.orm import relationship
from .base import Base
# Tabela de mapeamento Role-Permission
role_permissions = Table(
'role_permissions',
Base.metadata,
Column('role_id', Integer, ForeignKey('roles.id'), primary_key=True),
Column('permission_id', Integer, ForeignKey('permissions.id'), primary_key=True)
)
# Tabela de mapeamento User-Role
user_roles = Table(
'user_roles',
Base.metadata,
Column('user_id', Integer, ForeignKey('usuarios.id'), primary_key=True),
Column('role_id', Integer, ForeignKey('roles.id'), primary_key=True)
)
class Role(Base):
__tablename__ = 'roles'
id = Column(Integer, primary_key=True, autoincrement=True)
nome = Column(String(50), unique=True, nullable=False)
nivel = Column(Integer, nullable=False) # Nível hierárquico
descricao = Column(Text)
# Relacionamentos
permissions = relationship("Permission", secondary=role_permissions, back_populates="roles")
users = relationship("Usuario", secondary=user_roles, back_populates="roles")
# Níveis de role
MILITANTE_BASICO = 1
SECRETARIO_CELULA = 2
MEMBRO_SETOR = 3
SECRETARIO_SETOR = 4
MEMBRO_CR = 5
SECRETARIO_CR = 6
MEMBRO_CC = 7
SECRETARIO_GERAL = 8
@staticmethod
def get_roles_list():
return [
(Role.MILITANTE_BASICO, "Militante Básico"),
(Role.SECRETARIO_CELULA, "Secretário de Célula"),
(Role.MEMBRO_SETOR, "Membro de Setor"),
(Role.SECRETARIO_SETOR, "Secretário de Setor"),
(Role.MEMBRO_CR, "Membro de CR"),
(Role.SECRETARIO_CR, "Secretário de CR"),
(Role.MEMBRO_CC, "Membro do CC"),
(Role.SECRETARIO_GERAL, "Secretário Geral")
]
class Permission(Base):
__tablename__ = 'permissions'
id = Column(Integer, primary_key=True, autoincrement=True)
nome = Column(String(50), unique=True, nullable=False)
descricao = Column(Text)
# Relacionamentos
roles = relationship("Role", secondary=role_permissions, back_populates="permissions")
# Permissões básicas
VIEW_OWN_DATA = "view_own_data"
EDIT_OWN_DATA = "edit_own_data"
VIEW_CELL_DATA = "view_cell_data"
CREATE_MILITANT = "create_militant" # Nova permissão para criar militantes
# Permissões de célula
MANAGE_CELL_MEMBERS = "manage_cell_members"
CREATE_CELL_MEMBER = "create_cell_member"
VIEW_CELL_REPORTS = "view_cell_reports"
MANAGE_CELL_REPORTS = "manage_cell_reports" # Nova permissão
REGISTER_CELL_PAYMENT = "register_cell_payment"
# Permissões de setor
MANAGE_SECTOR_CELLS = "manage_sector_cells"
CREATE_SECTOR_CELL = "create_sector_cell"
VIEW_SECTOR_REPORTS = "view_sector_reports"
REGISTER_SECTOR_PAYMENT = "register_sector_payment"
# Permissões de CR
MANAGE_CR_SECTORS = "manage_cr_sectors"
CREATE_CR_SECTOR = "create_cr_sector"
VIEW_CR_REPORTS = "view_cr_reports"
REGISTER_CR_PAYMENT = "register_cr_payment"
# Permissões de CC
MANAGE_CC_CRS = "manage_cc_crs"
CREATE_CC_CR = "create_cc_cr"
VIEW_CC_REPORTS = "view_cc_reports"
REGISTER_CC_PAYMENT = "register_cc_payment"
SYSTEM_CONFIG = "system_config"
@staticmethod
def get_permissions_list():
return [
# Permissões básicas
(Permission.VIEW_OWN_DATA, "Visualizar próprios dados"),
(Permission.EDIT_OWN_DATA, "Editar próprios dados"),
(Permission.VIEW_CELL_DATA, "Visualizar dados da célula"),
(Permission.CREATE_MILITANT, "Criar novos militantes"), # Nova permissão
# Permissões de célula
(Permission.MANAGE_CELL_MEMBERS, "Gerenciar membros da célula"),
(Permission.CREATE_CELL_MEMBER, "Criar membros na célula"),
(Permission.VIEW_CELL_REPORTS, "Visualizar relatórios da célula"),
(Permission.MANAGE_CELL_REPORTS, "Gerenciar relatórios da célula"), # Nova permissão
(Permission.REGISTER_CELL_PAYMENT, "Registrar pagamentos da célula"),
# Permissões de setor
(Permission.MANAGE_SECTOR_CELLS, "Gerenciar células do setor"),
(Permission.CREATE_SECTOR_CELL, "Criar células no setor"),
(Permission.VIEW_SECTOR_REPORTS, "Visualizar relatórios do setor"),
(Permission.REGISTER_SECTOR_PAYMENT, "Registrar pagamentos do setor"),
# Permissões de CR
(Permission.MANAGE_CR_SECTORS, "Gerenciar setores do CR"),
(Permission.CREATE_CR_SECTOR, "Criar setores no CR"),
(Permission.VIEW_CR_REPORTS, "Visualizar relatórios do CR"),
(Permission.REGISTER_CR_PAYMENT, "Registrar pagamentos do CR"),
# Permissões de CC
(Permission.MANAGE_CC_CRS, "Gerenciar CRs"),
(Permission.CREATE_CC_CR, "Criar CRs"),
(Permission.VIEW_CC_REPORTS, "Visualizar relatórios nacionais"),
(Permission.REGISTER_CC_PAYMENT, "Registrar pagamentos nacionais"),
(Permission.SYSTEM_CONFIG, "Configurar sistema")
]
def init_rbac():
"""Inicializa o sistema RBAC com roles e permissões básicas"""
from .database import Usuario, get_db_connection
session = get_db_connection()
try:
# Criar role de administrador primeiro
admin_role = session.query(Role).filter_by(nome="Administrador").first()
if not admin_role:
admin_role = Role(nome="Administrador", nivel=Role.SECRETARIO_GERAL)
session.add(admin_role)
session.commit()
# Criar outras roles
for nivel, nome in Role.get_roles_list():
if nome != "Administrador": # Pular Administrador pois já foi criado
role = session.query(Role).filter_by(nivel=nivel).first()
if not role:
role = Role(nome=nome, nivel=nivel)
session.add(role)
# Criar permissões
for nome, descricao in Permission.get_permissions_list():
permission = session.query(Permission).filter_by(nome=nome).first()
if not permission:
permission = Permission(nome=nome, descricao=descricao)
session.add(permission)
session.commit()
# Dar todas as permissões para o admin
all_permissions = session.query(Permission).all()
admin_role.permissions = all_permissions
session.commit()
# Buscar usuário admin e atribuir role de administrador
admin_user = session.query(Usuario).filter_by(username="admin").first()
if admin_user:
if admin_role not in admin_user.roles:
admin_user.roles = [admin_role] # Substituir roles existentes
session.commit()
# Mapear permissões para outros roles
for role in session.query(Role).filter(Role.nome != "Administrador").all():
# Militante Básico
if role.nivel == Role.MILITANTE_BASICO:
role.permissions = [
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first()
]
# Secretário de Célula
elif role.nivel == Role.SECRETARIO_CELULA:
role.permissions = [
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.REGISTER_CELL_PAYMENT).first()
]
# Membro de Setor
elif role.nivel == Role.MEMBRO_SETOR:
role.permissions = [
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.REGISTER_SECTOR_PAYMENT).first()
]
# Secretário de Setor
elif role.nivel == Role.SECRETARIO_SETOR:
role.permissions = [
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(),
session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(),
session.query(Permission).filter_by(nome=Permission.REGISTER_SECTOR_PAYMENT).first()
]
# Membro de CR
elif role.nivel == Role.MEMBRO_CR:
role.permissions = [
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(),
session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CR_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.REGISTER_CR_PAYMENT).first()
]
# Secretário de CR
elif role.nivel == Role.SECRETARIO_CR:
role.permissions = [
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(),
session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CR_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_CR_SECTORS).first(),
session.query(Permission).filter_by(nome=Permission.CREATE_CR_SECTOR).first(),
session.query(Permission).filter_by(nome=Permission.REGISTER_CR_PAYMENT).first()
]
# Membro do CC
elif role.nivel == Role.MEMBRO_CC:
role.permissions = [
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(),
session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CR_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_CR_SECTORS).first(),
session.query(Permission).filter_by(nome=Permission.CREATE_CR_SECTOR).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CC_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.REGISTER_CC_PAYMENT).first()
]
# Secretário Geral
elif role.nivel == Role.SECRETARIO_GERAL:
role.permissions = [
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(),
session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CR_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_CR_SECTORS).first(),
session.query(Permission).filter_by(nome=Permission.CREATE_CR_SECTOR).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CC_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_CC_CRS).first(),
session.query(Permission).filter_by(nome=Permission.CREATE_CC_CR).first(),
session.query(Permission).filter_by(nome=Permission.REGISTER_CC_PAYMENT).first(),
session.query(Permission).filter_by(nome=Permission.SYSTEM_CONFIG).first()
]
session.commit()
except Exception as e:
print(f"Erro ao inicializar RBAC: {e}")
session.rollback()
raise
finally:
session.close()

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

23
models.py Normal file
View File

@@ -0,0 +1,23 @@
from sqlalchemy import Column, Integer, String, Date, Float, Boolean, ForeignKey, Table, Enum, DateTime
from sqlalchemy.orm import relationship, declarative_base
from sqlalchemy.sql import func
from datetime import datetime, date
import enum
Base = declarative_base()
class AssinaturaAnual(Base):
__tablename__ = 'assinaturas_anuais'
id = Column(Integer, primary_key=True)
militante_id = Column(Integer, ForeignKey('militantes.id'), nullable=False)
data_inicio = Column(Date, nullable=False)
data_fim = Column(Date, nullable=False)
valor = Column(Float, nullable=False)
militante = relationship('Militante', backref='assinaturas')
@property
def ativa(self):
hoje = date.today()
return self.data_inicio <= hoje <= self.data_fim

View File

@@ -1,19 +1,17 @@
black==24.10.0
blinker==1.9.0
click==8.1.7
Flask==3.1.0
greenlet==3.1.1
importlib_metadata==8.5.0
itsdangerous==2.2.0
Jinja2==3.1.4
MarkupSafe==3.0.2
mypy-extensions==1.0.0
mysql-connector-python==9.1.0
packaging==24.2
pathspec==0.12.1
platformdirs==4.3.6
SQLAlchemy==2.0.36
tomli==2.2.1
typing_extensions==4.12.2
Werkzeug==3.1.3
zipp==3.21.0
Flask==3.0.2
Flask-SQLAlchemy==3.1.1
Flask-Login==0.6.3
Flask-WTF==1.2.1
Flask-Mail==0.9.1
SQLAlchemy==2.0.27
Werkzeug==3.0.1
python-dotenv==1.0.1
pyotp==2.9.0
qrcode==7.4.2
Pillow==10.2.0
email-validator==2.1.0.post1
cryptography==42.0.2
bcrypt==4.1.2
Bootstrap-Flask==2.3.3
flask-bootstrap5==0.1.dev1
PyJWT==2.8.0

View File

@@ -1,30 +0,0 @@
from flask import Blueprint, request, jsonify
from models.integracao import calcular_cota
cota_bp = Blueprint('cota', __name__)
@cota_bp.route('/calculate_cota', methods=['POST'])
def calculate_cota():
try:
data = request.get_json()
# Extrair dados do request
salary = float(data.get('salary', 0))
num_children = int(data.get('num_children', 0))
pays_school = bool(data.get('pays_school', False))
pays_rent = bool(data.get('pays_rent', False))
num_parents = int(data.get('num_parents', 0))
# Calcular a cota (implemente sua lógica de cálculo aqui)
cota = calcular_cota(
salary=salary,
num_children=num_children,
pays_school=pays_school,
pays_rent=pays_rent,
num_parents=num_parents
)
return jsonify({'cota': cota})
except Exception as e:
return jsonify({'error': str(e)}), 400

27
scripts/init_db.py Normal file
View File

@@ -0,0 +1,27 @@
from functions.database import Role, Permissao, RolePermissao, Base, engine
from sqlalchemy.orm import Session
def init_db():
Base.metadata.create_all(engine)
with Session(engine) as session:
# Criar roles
admin = Role(nome='Administrador', nivel=1)
coord = Role(nome='Coordenador', nivel=2)
milit = Role(nome='Militante', nivel=3)
# Criar permissões
perm_admin = Permissao(nome='admin', descricao='Acesso total')
perm_militantes = Permissao(nome='ver_militantes', descricao='Ver militantes')
# ... outras permissões ...
session.add_all([admin, coord, milit, perm_admin, perm_militantes])
session.commit()
# Associar permissões aos roles
session.add(RolePermissao(role=admin, permissao=perm_admin))
session.add(RolePermissao(role=coord, permissao=perm_militantes))
session.commit()
if __name__ == '__main__':
init_db()

32
seed.py Normal file
View File

@@ -0,0 +1,32 @@
from seed_data import seed_database
from functions.database import Base, engine, get_db_connection
import time
import os
def wait_for_db():
db_path = os.path.expanduser("~/.local/share/controles/database.db")
max_attempts = 30
attempt = 0
while attempt < max_attempts:
if os.path.exists(db_path):
try:
db = get_db_connection()
db.execute("SELECT 1")
return True
except:
pass
print(f"Aguardando banco de dados... tentativa {attempt + 1}/{max_attempts}")
time.sleep(1)
attempt += 1
return False
if __name__ == "__main__":
print("Aguardando banco de dados estar pronto...")
if wait_for_db():
print("Iniciando população do banco de dados...")
seed_database()
print("Banco de dados populado com sucesso!")
else:
print("Erro: Banco de dados não ficou pronto a tempo.")

305
seed_data.py Normal file
View File

@@ -0,0 +1,305 @@
from datetime import datetime, timedelta
from functions.database import (
Base, Militante, CotaMensal, TipoPagamento, Pagamento,
MaterialVendido, TipoMaterial, VendaJornalAvulso, AssinaturaAnual,
RelatorioCotasMensais, RelatorioVendasMateriais, engine, SessionLocal,
Setor, ComiteCentral, Usuario, Role, EmailMilitante, Endereco
)
import random
from faker import Faker
import time
fake = Faker('pt_BR')
def criar_tipos_pagamento(session):
"""Cria tipos de pagamento padrão"""
tipos = [
"Dinheiro",
"PIX",
"Cartão de Crédito",
"Cartão de Débito",
"Transferência Bancária"
]
for tipo in tipos:
if not session.query(TipoPagamento).filter_by(descricao=tipo).first():
session.add(TipoPagamento(descricao=tipo))
session.commit()
def criar_tipos_material(session):
"""Cria tipos de material padrão"""
tipos = [
"Jornal",
"Revista",
"Livro",
"Panfleto",
"Cartilha"
]
for tipo in tipos:
if not session.query(TipoMaterial).filter_by(descricao=tipo).first():
session.add(TipoMaterial(descricao=tipo))
session.commit()
def criar_militantes(session, num_militantes):
print(f"\nCriando {num_militantes} militantes...")
militantes = []
emails_usados = set()
# Obter um setor existente
setor = session.query(Setor).first()
if not setor:
print("Erro: Nenhum setor encontrado!")
return []
for i in range(num_militantes):
try:
nome = fake.name()
cpf = fake.cpf()
while True:
email = fake.email()
if email not in emails_usados:
emails_usados.add(email)
break
endereco = Endereco(
estado=fake.estado_sigla(),
cidade=fake.city(),
bairro=fake.bairro(),
rua=fake.street_name(),
numero=str(random.randint(1, 999)),
complemento=f"Bloco {random.randint(1, 10)}, Apto {random.randint(1, 999)}" if random.random() < 0.3 else None,
cep=fake.postcode()
)
session.add(endereco)
session.flush()
print(f"Criando militante {i+1}: {nome} (CPF: {cpf})")
militante = Militante(
nome=nome,
cpf=cpf,
titulo_eleitoral=str(random.randint(100000000000, 999999999999)),
data_nascimento=fake.date_of_birth(minimum_age=18, maximum_age=65),
data_entrada_oci=fake.date_between(start_date='-5y', end_date='today'),
data_efetivacao_oci=fake.date_between(start_date='-4y', end_date='today'),
telefone1=fake.phone_number(),
telefone2=fake.phone_number() if random.random() < 0.3 else None,
profissao=fake.job(),
regime_trabalho=random.choice(['CLT', 'PJ', 'Estatutário', 'Autônomo']),
empresa=fake.company(),
contratante=fake.company() if random.random() < 0.2 else None,
instituicao_ensino=fake.company() if random.random() < 0.4 else None,
tipo_instituicao=random.choice(['Federal', 'Estadual', 'Municipal', 'Privada']) if random.random() < 0.4 else None,
sindicato=fake.company() if random.random() < 0.6 else None,
cargo_sindical=random.choice(['Diretor', 'Delegado', 'Conselheiro']) if random.random() < 0.3 else None,
dirigente_sindical=random.random() < 0.2,
central_sindical=random.choice(['CUT', 'CSP-Conlutas', 'CTB', 'Força Sindical']) if random.random() < 0.4 else None,
endereco_id=endereco.id,
responsabilidades=random.randint(0, 1023)
)
session.add(militante)
session.flush()
email_militante = EmailMilitante(
militante_id=militante.id,
endereco_email=email
)
session.add(email_militante)
militantes.append(militante)
# Commit a cada militante para evitar transações muito longas
session.commit()
except Exception as e:
print(f"Erro ao criar militante {i+1}: {e}")
session.rollback()
continue
return militantes
def criar_cotas(session, militantes, quantidade_por_militante=3):
print(f"Criando {quantidade_por_militante} cotas para cada um dos {len(militantes)} militantes...")
for militante in militantes:
try:
print(f"Criando cotas para militante {militante.nome}")
for i in range(quantidade_por_militante):
data_base = datetime.now() - timedelta(days=30 * i)
valor = random.uniform(50, 200)
cota = CotaMensal(
militante_id=militante.id,
valor_antigo=valor,
valor_novo=valor * 1.1,
data_alteracao=data_base,
data_vencimento=data_base + timedelta(days=30),
pago=random.choice([True, False])
)
session.add(cota)
session.commit()
except Exception as e:
print(f"Erro ao criar cotas para militante {militante.nome}: {e}")
session.rollback()
continue
print("Cotas criadas com sucesso!")
def criar_pagamentos(militantes):
"""Cria pagamentos fictícios"""
tipos_pagamento = ["Cota", "Jornal", "Assinatura", "Campanha Financeira"]
for militante in militantes:
for _ in range(random.randint(1, 5)):
pagamento = Pagamento(
militante_id=militante.id,
tipo_pagamento=random.choice(tipos_pagamento),
valor=random.uniform(50, 500),
data_pagamento=fake.date_between(start_date='-1y', end_date='today')
)
db_session.add(pagamento)
db_session.commit()
def criar_materiais_vendidos(militantes):
"""Cria materiais vendidos fictícios"""
tipos_material = db_session.query(TipoMaterial).all()
for militante in militantes:
for _ in range(random.randint(1, 3)):
material = MaterialVendido(
militante_id=militante.id,
tipo_material_id=random.choice(tipos_material).id,
descricao=fake.sentence(),
valor=random.uniform(20, 100),
data_venda=fake.date_time_between(start_date='-1y', end_date='now')
)
db_session.add(material)
db_session.commit()
def criar_vendas_jornal(militantes):
"""Cria vendas de jornal avulso fictícias"""
for militante in militantes:
for _ in range(random.randint(1, 4)):
venda = VendaJornalAvulso(
militante_id=militante.id,
quantidade=random.randint(1, 10),
valor_total=random.uniform(10, 100),
data_venda=fake.date_time_between(start_date='-1y', end_date='now')
)
db_session.add(venda)
db_session.commit()
def criar_assinaturas(militantes):
"""Cria assinaturas anuais fictícias"""
tipos_material = db_session.query(TipoMaterial).all()
for militante in militantes:
if random.random() < 0.3: # 30% de chance de ter assinatura
data_inicio = fake.date_time_between(start_date='-1y', end_date='now')
assinatura = AssinaturaAnual(
militante_id=militante.id,
tipo_material_id=random.choice(tipos_material).id,
quantidade=random.randint(1, 3),
valor_total=random.uniform(100, 500),
data_inicio=data_inicio,
data_fim=data_inicio + timedelta(days=365)
)
db_session.add(assinatura)
db_session.commit()
def criar_relatorios():
"""Cria relatórios fictícios"""
for _ in range(12): # Um relatório por mês do último ano
data = fake.date_time_between(start_date='-1y', end_date='now')
relatorio_cotas = RelatorioCotasMensais(
setor_id=random.randint(1, 5),
comite_id=random.randint(1, 3),
total_cotas=random.uniform(1000, 5000),
data_relatorio=data
)
relatorio_vendas = RelatorioVendasMateriais(
setor_id=random.randint(1, 5),
comite_id=random.randint(1, 3),
total_vendas=random.uniform(500, 3000),
data_relatorio=data
)
db_session.add(relatorio_cotas)
db_session.add(relatorio_vendas)
db_session.commit()
def criar_setores():
"""Cria setores padrão"""
setores = [
"Setor 1",
"Setor 2",
"Setor 3",
"Setor 4",
"Setor 5"
]
for setor in setores:
if not db_session.query(Setor).filter_by(nome=setor).first():
db_session.add(Setor(nome=setor))
db_session.commit()
def criar_comites():
"""Cria comitês padrão"""
comites = [
"Comitê 1",
"Comitê 2",
"Comitê 3"
]
for comite in comites:
if not db_session.query(ComiteCentral).filter_by(nome=comite).first():
db_session.add(ComiteCentral(nome=comite))
db_session.commit()
def criar_roles():
"""Cria roles padrão"""
roles = [
("admin", 1), # Nível 1: Administrador
("gestor", 2), # Nível 2: Gestor
("usuario", 3) # Nível 3: Usuário comum
]
for nome, nivel in roles:
if not db_session.query(Role).filter_by(nome=nome).first():
db_session.add(Role(nome=nome, nivel=nivel))
db_session.commit()
def criar_usuario_admin():
"""Cria usuário admin inicial"""
if not db_session.query(Usuario).filter_by(username='admin').first():
role_admin = db_session.query(Role).filter_by(nome='admin').first()
setor = db_session.query(Setor).first()
admin = Usuario(
username='admin',
email='admin@example.com',
is_admin=True,
ativo=True,
role_id=role_admin.id if role_admin else None,
setor_id=setor.id if setor else None
)
admin.set_password('admin123') # Método que deve existir na classe Usuario
db_session.add(admin)
db_session.commit()
print("Usuário admin criado com sucesso!")
def seed_database():
"""Função principal para popular o banco de dados com dados fictícios"""
print("Populando banco de dados com dados fictícios...")
session = SessionLocal()
try:
criar_tipos_pagamento(session)
criar_tipos_material(session)
militantes = criar_militantes(session, 50)
if militantes:
criar_cotas(session, militantes)
print("Dados fictícios criados com sucesso!")
except Exception as e:
print(f"Erro ao popular banco de dados: {e}")
session.rollback()
finally:
session.close()
if __name__ == "__main__":
seed_database()

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

563
static/css/components.css Normal file
View File

@@ -0,0 +1,563 @@
/* Variáveis globais */
:root {
--table-header-bg: #d8dde2;
--table-hover-bg: rgba(0, 0, 0, 0.02);
--border-color: #dee2e6;
--blue: #0d6efd;
--green: #198754;
--cyan: #0dcaf0;
--yellow: #ffc107;
--primary-color: #dc3545;
--primary-hover: #bb2d3b;
--text-color: #333;
--text-muted: #6c757d;
--bg-hover: #f8f9fa;
--tab-active-color: var(--primary-color);
--tab-hover-color: rgba(220, 53, 69, 0.1);
/* Variáveis para os botões */
--bs-success: #198754;
--bs-success-dark: #157347;
--bs-secondary: #6c757d;
--bs-secondary-dark: #565e64;
}
/* Tabelas */
.table-container {
background: #fff;
border-radius: 0.5rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
margin-bottom: 1.5rem;
}
.table {
margin-bottom: 0;
}
.table thead {
background-color: var(--table-header-bg) !important;
}
.table thead th {
border-bottom: none;
font-weight: 600;
padding: 1rem;
white-space: nowrap;
}
.table tbody td {
padding: 1rem;
vertical-align: middle;
}
.table-hover tbody tr:hover {
background-color: var(--table-hover-bg) !important;
cursor: pointer;
}
.table-hover tbody tr {
transition: all 0.3s ease;
}
/* Botões de ação */
.btn-group-actions {
display: flex;
gap: 0.5rem;
justify-content: flex-end;
}
.btn-group-actions .btn {
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
}
/* Botões padrão */
.btn-outline-primary {
color: #0d6efd;
border-color: #0d6efd;
background-color: transparent;
}
.btn-outline-primary:hover {
color: #fff;
background-color: #0d6efd;
border-color: #0d6efd;
}
/* Cabeçalho de listagem */
.list-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.list-title {
font-size: 1.5rem;
font-weight: 600;
margin: 0;
display: flex;
align-items: center;
gap: 0.5rem;
}
.list-actions {
display: flex;
gap: 0.5rem;
}
/* Barra de pesquisa e filtros */
.search-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
gap: 1rem;
}
.search-input-group {
flex: 1;
max-width: 500px;
}
.search-input-group .input-group-text {
background-color: #f8f9fa;
border-right: none;
}
.search-input-group .form-control {
border-left: none;
}
.search-input-group .form-control:focus {
box-shadow: none;
border-color: #dee2e6;
}
/* Badges */
.badge {
font-weight: 500;
padding: 0.5em 0.8em;
}
.badge.bg-success {
background-color: #198754 !important;
}
.badge.bg-secondary {
background-color: #6c757d !important;
}
/* Paginação */
.pagination-container {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
border-top: 1px solid var(--border-color);
}
.pagination {
margin: 0;
}
.page-link {
padding: 0.375rem 0.75rem;
}
/* Responsividade */
@media (max-width: 768px) {
.search-bar {
flex-direction: column;
align-items: stretch;
}
.search-input-group {
max-width: 100%;
}
.list-actions {
flex-wrap: wrap;
}
.btn-group-actions {
justify-content: center;
}
}
/* Cards do Dashboard */
.stats-card {
position: relative;
padding: 1.5rem;
border-radius: 0.5rem;
background: #fff;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
overflow: hidden;
height: 100%;
}
.stats-card .title {
font-size: 0.875rem;
color: #6c757d;
margin-bottom: 0.5rem;
}
.stats-card .value {
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 1rem;
}
.stats-card .link {
color: inherit;
text-decoration: none;
font-size: 0.875rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.stats-card .icon {
position: absolute;
top: 1rem;
right: 1rem;
font-size: 1.5rem;
opacity: 0.2;
}
.stats-card.blue {
background: linear-gradient(135deg, var(--blue) 0%, #0a58ca 100%);
color: #fff;
}
.stats-card.green {
background: linear-gradient(135deg, var(--green) 0%, #146c43 100%);
color: #fff;
}
.stats-card.cyan {
background: linear-gradient(135deg, var(--cyan) 0%, #0aa2c0 100%);
color: #fff;
}
.stats-card.yellow {
background: linear-gradient(135deg, var(--yellow) 0%, #cc9a06 100%);
color: #fff;
}
/* Welcome Header */
.welcome-header {
margin-bottom: 2rem;
}
.welcome-header h2 {
font-size: 2rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.welcome-header h4 {
font-size: 1.25rem;
font-weight: 400;
}
/* Tabs */
.nav-tabs {
border-bottom: 2px solid var(--border-color);
margin-bottom: 1rem;
}
.nav-tabs .nav-link,
.nav-tabs .nav-link:focus,
.nav-tabs .nav-link:hover,
.nav-tabs .nav-link.active {
color: var(--primary-color) !important;
border: none;
border-bottom: 2px solid transparent;
padding: 0.75rem 1.5rem;
margin-bottom: -2px;
transition: all 0.2s ease-in-out;
font-weight: 500;
background-color: transparent;
}
.nav-tabs .nav-link:hover {
background-color: var(--tab-hover-color);
border-bottom: 2px solid var(--primary-color);
}
.nav-tabs .nav-link.active {
font-weight: 600;
background-color: var(--tab-hover-color);
border-bottom: 2px solid var(--primary-color);
}
.nav-tabs .nav-link i {
margin-right: 0.5rem;
}
.tab-content {
padding: 1rem 0;
}
.tab-pane {
animation: fadeIn 0.3s ease-in-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(5px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Responsividade das abas */
@media (max-width: 768px) {
.nav-tabs {
flex-wrap: nowrap;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.nav-tabs .nav-link {
white-space: nowrap;
padding: 0.5rem 1rem;
}
}
/* Estilo para botões com largura fixa */
.btn-fixed-width {
min-width: 120px;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.375rem 0.75rem;
text-align: center;
height: 38px;
line-height: 1.5;
vertical-align: middle;
}
.btn-fixed-width i {
margin-right: 8px;
font-size: 0.875rem;
}
/* Estilo para o backdrop com blur em todos os modais */
.modal-backdrop.show {
backdrop-filter: blur(8px);
background-color: rgba(0, 0, 0, 0.7);
}
/* Estilo para o botão de fechar dos modais */
.btn-close {
background-color: transparent;
padding: 0.5rem;
opacity: 0.8;
transition: opacity 0.2s;
filter: invert(1) grayscale(100%) brightness(200%);
}
.btn-close:hover {
opacity: 1;
background-color: transparent;
}
/* Estilos do Modal */
.modal-header {
background-color: #343a40;
color: #fff;
padding: 1rem;
}
.modal-title {
color: #fff;
font-weight: 600;
display: flex;
align-items: center;
}
.modal-header i {
color: #fff;
margin-right: 0.5rem;
}
.modal-header .btn-close {
filter: invert(1) grayscale(100%) brightness(200%);
opacity: 0.8;
}
.modal-header .btn-close:hover {
opacity: 1;
}
/* Estilos globais de formulário */
.form-control:focus,
.form-select:focus,
.form-check-input:focus,
.btn:focus,
.btn-check:focus + .btn {
border-color: var(--primary-color);
box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.25);
}
.form-control:hover,
.form-select:hover {
border-color: var(--primary-color);
}
/* Input group com foco */
.input-group .form-control:focus,
.input-group .form-select:focus {
border-color: var(--primary-color);
}
/* Checkbox e radio */
.form-check-input:checked {
background-color: var(--primary-color);
border-color: var(--primary-color);
}
/* Date picker */
input[type="date"]:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.25);
}
/* Estilo para colunas ordenáveis */
th[data-sort] {
cursor: pointer;
user-select: none;
}
th[data-sort] i {
margin-left: 5px;
color: #ccc;
}
th[data-sort].sort-asc i,
th[data-sort].sort-desc i {
color: var(--primary-color);
}
/* Animação para linhas da tabela */
#militantesTable tbody tr {
transition: all 0.3s ease;
}
/* Estilos globais para botões */
.btn-success,
.modal-footer .btn-success,
button.btn-success,
input.btn-success,
.btn-success.active,
.btn-success:active,
.show > .btn-success.dropdown-toggle {
background-color: #198754 !important;
border-color: #198754 !important;
color: #fff !important;
}
.btn-success:hover,
.modal-footer .btn-success:hover,
button.btn-success:hover,
input.btn-success:hover,
.btn-success:focus,
.btn-success:active,
.modal-footer .btn-success:focus,
.modal-footer .btn-success:active,
.btn-success:not(:disabled):not(.disabled):active,
.btn-success:not(:disabled):not(.disabled).active,
.show > .btn-success.dropdown-toggle:hover {
background-color: #146c43 !important;
border-color: #146c43 !important;
color: #fff !important;
}
.btn-secondary,
.modal-footer .btn-secondary,
button.btn-secondary,
input.btn-secondary,
.btn-secondary.active,
.btn-secondary:active,
.show > .btn-secondary.dropdown-toggle {
background-color: #6c757d !important;
border-color: #6c757d !important;
color: #fff !important;
}
.btn-secondary:hover,
.modal-footer .btn-secondary:hover,
button.btn-secondary:hover,
input.btn-secondary:hover,
.btn-secondary:focus,
.btn-secondary:active,
.modal-footer .btn-secondary:focus,
.modal-footer .btn-secondary:active,
.btn-secondary:not(:disabled):not(.disabled):active,
.btn-secondary:not(:disabled):not(.disabled).active,
.show > .btn-secondary.dropdown-toggle:hover {
background-color: #5c636a !important;
border-color: #5c636a !important;
}
.btn-secondary:not(:disabled):not(.disabled).active {
background-color: #4b545c !important;
border-color: #4b545c !important;
color: white !important;
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important;
}
/* Estilos para botões nos modais */
.modal .btn,
.modal-footer .btn {
font-weight: 500;
padding: 0.5rem 1.5rem;
border-radius: 4px;
transition: all 0.2s ease-in-out;
}
.modal .btn:hover,
.modal-footer .btn:hover {
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* Garantir que o botão primário mantenha suas cores */
.modal .btn-primary,
.modal-footer .btn-primary,
.modal .btn-primary.active,
.modal .btn-primary:active,
.modal-footer .btn-primary.active,
.modal-footer .btn-primary:active,
.modal .btn-primary:not(:disabled):not(.disabled):active,
.modal .btn-primary:not(:disabled):not(.disabled).active,
.modal-footer .btn-primary:not(:disabled):not(.disabled):active,
.modal-footer .btn-primary:not(:disabled):not(.disabled).active,
.show > .modal .btn-primary.dropdown-toggle,
.show > .modal-footer .btn-primary.dropdown-toggle {
background-color: #0d6efd !important;
border-color: #0d6efd !important;
color: white !important;
}
.modal .btn-primary:hover,
.modal-footer .btn-primary:hover,
.modal .btn-primary:focus,
.modal-footer .btn-primary:focus,
.modal .btn-primary:active,
.modal-footer .btn-primary:active,
.modal .btn-primary:not(:disabled):not(.disabled):active:focus,
.modal .btn-primary:not(:disabled):not(.disabled).active:focus,
.modal-footer .btn-primary:not(:disabled):not(.disabled):active:focus,
.modal-footer .btn-primary:not(:disabled):not(.disabled).active:focus {
background-color: #0b5ed7 !important;
border-color: #0b5ed7 !important;
color: white !important;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important;
}

450
static/css/style.css Normal file
View File

@@ -0,0 +1,450 @@
:root {
--primary-color: #E8000C;
--primary-dark: #B5000A;
--primary-light: #FF1A1A;
--secondary-color: #2D2D2D;
--secondary-light: #404040;
--secondary-dark: #1A1A1A;
--background-color: #FFFFFF;
--text-color: #2D2D2D;
--text-light: #FFFFFF;
--hover-color: #FF1A1A;
--disabled-color: #999999;
}
body {
background-color: var(--background-color);
color: var(--text-color);
font-family: 'Roboto', sans-serif;
}
.navbar {
background: linear-gradient(to right, var(--secondary-dark), var(--secondary-color)) !important;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
border-bottom: 3px solid var(--primary-color);
padding: 0.8rem 1rem;
}
.navbar-brand {
display: flex;
align-items: center;
gap: 12px;
font-size: 1.3rem;
font-weight: 500;
color: var(--text-light) !important;
letter-spacing: 0.5px;
}
.navbar-brand img {
height: 40px;
margin-right: 10px;
}
.navbar-logo {
height: 32px;
width: auto;
display: block;
}
.login-logo {
height: 80px;
width: 80px;
}
.nav-link {
font-weight: 400;
font-size: 0.95rem;
letter-spacing: 0.3px;
transition: all 0.3s ease;
color: rgba(255, 255, 255, 0.85) !important;
padding: 0.5rem 1rem;
}
.nav-link:hover {
color: var(--text-light) !important;
transform: translateY(-1px);
}
.nav-link i {
font-size: 0.9rem;
opacity: 0.9;
}
.card {
border: none;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
transition: all 0.3s ease;
overflow: hidden;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 24px rgba(0,0,0,0.1);
}
.card .card-body {
padding: 1.5rem;
}
/* Cards de estatísticas */
.card.bg-primary {
background: linear-gradient(135deg, #0d6efd, #0a58ca) !important;
}
.card.bg-success {
background: linear-gradient(135deg, #198754, #146c43) !important;
}
.card.bg-info {
background: linear-gradient(135deg, #0dcaf0, #0aa2c0) !important;
}
.card.bg-warning {
background: linear-gradient(135deg, #ffc107, #cc9a06) !important;
}
.card .fs-1 {
opacity: 0.8;
transition: all 0.3s ease;
}
.card:hover .fs-1 {
opacity: 1;
transform: scale(1.1);
}
.card h2 {
font-size: 2.5rem;
font-weight: 600;
margin: 0.5rem 0;
}
.card h6 {
font-size: 0.9rem;
font-weight: 400;
opacity: 0.9;
}
.card a {
font-size: 0.9rem;
opacity: 0.9;
transition: all 0.3s ease;
}
.card a:hover {
opacity: 1;
transform: translateX(5px);
}
/* Cards de listagem */
.card .card-header {
background: linear-gradient(to right, var(--secondary-dark), var(--secondary-color));
color: var(--text-light);
padding: 1rem 1.5rem;
border: none;
}
.card .card-header h5 {
font-size: 1.1rem;
font-weight: 500;
}
.list-group-item {
padding: 1rem 1.5rem;
border-left: none;
border-right: none;
transition: all 0.3s ease;
}
.list-group-item:hover {
background-color: rgba(0,0,0,0.02);
transform: translateX(5px);
}
.list-group-item h6 {
font-size: 1rem;
font-weight: 500;
margin: 0;
}
.list-group-item small {
font-size: 0.85rem;
}
.badge {
padding: 0.5em 0.8em;
font-weight: 500;
}
.btn-primary {
background-color: var(--primary-color);
border: none;
padding: 0.5rem 1.5rem;
border-radius: 5px;
font-weight: 500;
color: var(--text-light);
}
.btn-primary:hover {
background-color: var(--hover-color);
transform: translateY(-2px);
box-shadow: 0 2px 5px rgba(232, 0, 12, 0.3);
}
.btn-primary:disabled {
background-color: var(--disabled-color);
}
.table {
background: white;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
}
.table thead th {
background-color: var(--secondary-color);
color: var(--text-light);
font-weight: 500;
border: none;
}
.table tbody tr:hover {
background-color: rgba(232, 0, 12, 0.05);
}
.form-control {
border-radius: 5px;
border: 1px solid #e0e0e0;
padding: 0.75rem;
}
.form-control:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 0.2rem rgba(232, 0, 12, 0.25);
}
/* Alert styles */
.alert {
border: none;
border-radius: 8px;
padding: 1rem 1.5rem;
margin-bottom: 1rem;
opacity: 1 !important;
background-color: rgba(255, 255, 255, 0.98) !important;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.alert i {
margin-right: 8px;
}
.alert-success {
color: #155724 !important;
background-color: #d4edda !important;
border-left: 4px solid #28a745;
}
.alert-danger {
color: #721c24 !important;
background-color: #f8d7da !important;
border-left: 4px solid #dc3545;
}
.alert-warning {
color: #856404 !important;
background-color: #fff3cd !important;
border-left: 4px solid #ffc107;
}
.alert-info {
color: #0c5460 !important;
background-color: #d1ecf1 !important;
border-left: 4px solid #17a2b8;
}
/* Animações para feedback */
@keyframes fadeIn {
from { opacity: 0; transform: translate(-50%, -20px); }
to { opacity: 1; transform: translate(-50%, 0); }
}
.alert {
animation: fadeIn 0.3s ease;
}
/* Responsividade */
@media (max-width: 768px) {
.navbar-brand {
font-size: 1.2rem;
}
.navbar-logo {
height: 30px;
}
.container {
padding: 1rem;
}
.card {
margin-bottom: 1rem;
}
.alert {
margin: 1rem;
width: calc(100% - 2rem);
max-width: none;
}
}
.dropdown-menu {
background: linear-gradient(to bottom right, var(--secondary-dark), var(--secondary-color));
border: 1px solid rgba(255,255,255,0.1);
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
padding: 0.5rem;
margin-top: 0.5rem;
border-radius: 8px;
}
.dropdown-item {
color: rgba(255, 255, 255, 0.85) !important;
font-size: 0.9rem;
font-weight: 400;
padding: 0.6rem 1rem;
border-radius: 6px;
transition: all 0.2s ease;
}
.dropdown-item:hover {
background-color: var(--primary-color);
color: var(--text-light) !important;
transform: translateX(3px);
}
.dropdown-divider {
border-top: 1px solid var(--secondary-light);
}
/* Estilo para o menu mobile */
@media (max-width: 768px) {
.navbar-collapse {
background-color: var(--secondary-color);
padding: 1rem;
border-radius: 0 0 10px 10px;
}
.navbar-brand img {
height: 30px;
}
}
/* Data styles */
.date-header {
padding: 1.5rem 0;
margin-bottom: 1.5rem;
border-bottom: 1px solid rgba(0,0,0,0.1);
color: var(--secondary-color);
font-weight: 400;
font-size: 1.4rem;
}
/* Navbar styles */
.navbar-nav .nav-link {
color: rgba(255, 255, 255, 0.85) !important;
transition: all 0.3s ease;
font-weight: 400;
}
.navbar-nav .nav-link:hover {
color: var(--primary-color) !important;
transform: translateY(-1px);
}
.navbar-nav .dropdown-menu {
background: linear-gradient(to bottom right, var(--secondary-dark), var(--secondary-color));
border: 1px solid rgba(255,255,255,0.1);
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
padding: 0.5rem;
margin-top: 0.5rem;
border-radius: 8px;
}
.dropdown-item {
color: rgba(255, 255, 255, 0.85) !important;
font-size: 0.9rem;
font-weight: 400;
padding: 0.6rem 1rem;
border-radius: 6px;
transition: all 0.2s ease;
}
.dropdown-item:hover {
background-color: var(--primary-color);
color: var(--text-light) !important;
transform: translateX(3px);
}
/* Data styles */
.date-header {
padding: 1.5rem 0;
margin-bottom: 1.5rem;
border-bottom: 1px solid rgba(0,0,0,0.1);
color: var(--secondary-color);
font-weight: 400;
font-size: 1.4rem;
}
@media (max-width: 768px) {
.date-header {
text-align: center;
}
.navbar-collapse {
background-color: var(--secondary-color);
padding: 1rem;
border-radius: 0 0 10px 10px;
}
.navbar-brand img {
height: 30px;
}
}
.welcome-header {
background: linear-gradient(to right, var(--background-color), rgba(232, 0, 12, 0.05));
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 2rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
.welcome-header h2 {
font-size: 2rem;
margin-bottom: 0.5rem;
color: var(--primary-color);
}
.welcome-header h4 {
font-size: 1.2rem;
color: var(--secondary-color);
opacity: 0.8;
margin: 0;
}
.card-header {
background: linear-gradient(to right, var(--secondary-dark), var(--secondary-color));
color: var(--text-light);
padding: 1rem 1.5rem;
border: none;
}
.list-group-item-action {
transition: all 0.3s ease;
}
.list-group-item-action:hover {
transform: translateX(5px);
background-color: rgba(232, 0, 12, 0.05);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
static/img/logo001.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

127
static/js/cotas.js Normal file
View File

@@ -0,0 +1,127 @@
document.addEventListener('DOMContentLoaded', function() {
console.log('Carregando script cotas.js...');
// Configuração do modal de edição
const modalEditarCota = document.getElementById('modalEditarCota');
if (modalEditarCota) {
modalEditarCota.addEventListener('show.bs.modal', function(event) {
console.log('Modal de edição sendo exibido');
const button = event.relatedTarget;
if (!button) {
console.error('Botão não encontrado!');
return;
}
const cotaId = button.getAttribute('data-cota-id');
console.log('ID da cota:', cotaId);
// Dados da cota
const dados = {
militanteId: button.getAttribute('data-cota-militante'),
militanteNome: button.closest('tr').querySelector('td').textContent.trim(),
valorAntigo: button.closest('tr').querySelector('td[data-valor_antigo]').getAttribute('data-valor_antigo'),
valorNovo: button.closest('tr').querySelector('td[data-valor_novo]').getAttribute('data-valor_novo'),
dataAlteracao: button.getAttribute('data-cota-data-alteracao'),
dataVencimento: button.getAttribute('data-cota-data-vencimento'),
pago: button.getAttribute('data-cota-pago') === 'true'
};
console.log('Dados da cota:', dados);
// Preencher campos
document.getElementById('editMilitante').value = dados.militanteId;
document.getElementById('editMilitanteNome').value = dados.militanteNome;
document.getElementById('editValorAntigo').value = dados.valorAntigo;
document.getElementById('editValorNovo').value = dados.valorNovo;
document.getElementById('editDataAlteracao').value = dados.dataAlteracao;
document.getElementById('editDataVencimento').value = dados.dataVencimento;
document.getElementById('editPago').checked = dados.pago;
// Configurar formulário
const form = document.getElementById('formEditarCota');
if (form) {
form.action = `/cotas/editar/${cotaId}`;
console.log('Action do formulário:', form.action);
// Remover listeners antigos para evitar duplicação
const newForm = form.cloneNode(true);
form.parentNode.replaceChild(newForm, form);
// Adicionar listener para o submit do formulário
newForm.addEventListener('submit', function(e) {
e.preventDefault();
console.log('Formulário submetido');
// Criar FormData com os dados do formulário
const formData = new FormData(this);
// Adicionar campo pago com o valor correto
const isPago = document.getElementById('editPago').checked;
formData.set('pago', isPago ? 'true' : 'false');
// Log dos dados sendo enviados
console.log('Dados do formulário:');
for (let [key, value] of formData.entries()) {
console.log(key + ': ' + value);
}
// Enviar requisição
fetch(this.action, {
method: 'POST',
body: formData
})
.then(response => {
console.log('Status da resposta:', response.status);
return response.json();
})
.then(data => {
console.log('Resposta:', data);
if (data.status === 'success') {
// Fechar modal
const modal = bootstrap.Modal.getInstance(modalEditarCota);
modal.hide();
// Recarregar página
window.location.reload();
} else {
alert('Erro ao atualizar cota: ' + data.message);
}
})
.catch(error => {
console.error('Erro:', error);
alert('Erro ao atualizar cota. Por favor, tente novamente.');
});
});
}
});
}
// Configuração do modal de exclusão
const deleteModal = document.getElementById('deleteModal');
if (deleteModal) {
deleteModal.addEventListener('show.bs.modal', function(event) {
console.log('Modal de exclusão sendo exibido');
const button = event.relatedTarget;
if (!button) {
console.error('Botão não encontrado!');
return;
}
const cotaId = button.getAttribute('data-cota-id');
const cotaInfo = button.getAttribute('data-cota-info');
console.log('ID da cota:', cotaId);
console.log('Info da cota:', cotaInfo);
// Atualizar texto do modal
document.getElementById('cotaInfo').textContent = cotaInfo;
// Configurar formulário de exclusão
const form = document.getElementById('deleteForm');
if (form) {
form.action = `/cotas/excluir/${cotaId}`;
console.log('Action do formulário:', form.action);
}
});
}
});

146
static/js/forms.js Normal file
View File

@@ -0,0 +1,146 @@
// Validação de CPF
function validarCPF(cpf) {
cpf = cpf.replace(/[^\d]/g, '');
if (cpf.length !== 11) return false;
// Verifica se todos os dígitos são iguais
if (/^(\d)\1{10}$/.test(cpf)) return false;
// Validação do primeiro dígito verificador
let soma = 0;
for (let i = 0; i < 9; i++) {
soma += parseInt(cpf.charAt(i)) * (10 - i);
}
let resto = 11 - (soma % 11);
let dv1 = resto > 9 ? 0 : resto;
if (dv1 !== parseInt(cpf.charAt(9))) return false;
// Validação do segundo dígito verificador
soma = 0;
for (let i = 0; i < 10; i++) {
soma += parseInt(cpf.charAt(i)) * (11 - i);
}
resto = 11 - (soma % 11);
let dv2 = resto > 9 ? 0 : resto;
if (dv2 !== parseInt(cpf.charAt(10))) return false;
return true;
}
// Validação de email
function validarEmail(email) {
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return re.test(email);
}
// Validação de telefone
function validarTelefone(telefone) {
telefone = telefone.replace(/[^\d]/g, '');
return telefone.length >= 10 && telefone.length <= 11;
}
// Inicialização dos formulários
document.addEventListener('DOMContentLoaded', function() {
// Validação personalizada para CPF
const cpfInputs = document.querySelectorAll('input[name="cpf"]');
cpfInputs.forEach(input => {
input.addEventListener('blur', function() {
const cpf = this.value;
if (!validarCPF(cpf)) {
this.setCustomValidity('CPF inválido');
this.classList.add('is-invalid');
} else {
this.setCustomValidity('');
this.classList.remove('is-invalid');
}
});
});
// Validação personalizada para email
const emailInputs = document.querySelectorAll('input[type="email"]');
emailInputs.forEach(input => {
input.addEventListener('blur', function() {
const email = this.value;
if (!validarEmail(email)) {
this.setCustomValidity('Email inválido');
this.classList.add('is-invalid');
} else {
this.setCustomValidity('');
this.classList.remove('is-invalid');
}
});
});
// Validação personalizada para telefone
const phoneInputs = document.querySelectorAll('input[name="telefone"]');
phoneInputs.forEach(input => {
input.addEventListener('blur', function() {
const telefone = this.value;
if (!validarTelefone(telefone)) {
this.setCustomValidity('Telefone inválido');
this.classList.add('is-invalid');
} else {
this.setCustomValidity('');
this.classList.remove('is-invalid');
}
});
});
// Validação de campos monetários
const moneyInputs = document.querySelectorAll('input[type="number"][step="0.01"]');
moneyInputs.forEach(input => {
input.addEventListener('blur', function() {
const value = parseFloat(this.value);
if (isNaN(value) || value < 0) {
this.setCustomValidity('Valor inválido');
this.classList.add('is-invalid');
} else {
this.setCustomValidity('');
this.classList.remove('is-invalid');
this.value = value.toFixed(2);
}
});
});
// Validação de datas
const dateInputs = document.querySelectorAll('input[type="date"]');
dateInputs.forEach(input => {
input.addEventListener('change', function() {
const date = new Date(this.value);
const today = new Date();
if (this.hasAttribute('min')) {
const minDate = new Date(this.getAttribute('min'));
if (date < minDate) {
this.setCustomValidity(`A data não pode ser anterior a ${minDate.toLocaleDateString()}`);
this.classList.add('is-invalid');
return;
}
}
if (this.hasAttribute('max')) {
const maxDate = new Date(this.getAttribute('max'));
if (date > maxDate) {
this.setCustomValidity(`A data não pode ser posterior a ${maxDate.toLocaleDateString()}`);
this.classList.add('is-invalid');
return;
}
}
this.setCustomValidity('');
this.classList.remove('is-invalid');
});
});
// Feedback visual para campos obrigatórios
const requiredInputs = document.querySelectorAll('input[required], select[required], textarea[required]');
requiredInputs.forEach(input => {
const label = input.previousElementSibling;
if (label && label.tagName === 'LABEL') {
label.innerHTML += ' <span class="text-danger">*</span>';
}
});
});

11
static/js/home.js Normal file
View File

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

145
static/js/main.js Normal file
View File

@@ -0,0 +1,145 @@
// Máscaras para campos de formulário
document.addEventListener('DOMContentLoaded', function() {
// Máscara para CPF
const cpfInputs = document.querySelectorAll('input[name="cpf"]');
cpfInputs.forEach(input => {
input.addEventListener('input', function(e) {
let value = e.target.value.replace(/\D/g, '');
if (value.length <= 11) {
value = value.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, "$1.$2.$3-$4");
e.target.value = value;
}
});
});
// Máscara para telefone
const phoneInputs = document.querySelectorAll('input[name="telefone"]');
phoneInputs.forEach(input => {
input.addEventListener('input', function(e) {
let value = e.target.value.replace(/\D/g, '');
if (value.length <= 11) {
if (value.length === 11) {
value = value.replace(/(\d{2})(\d{5})(\d{4})/, "($1) $2-$3");
} else {
value = value.replace(/(\d{2})(\d{4})(\d{4})/, "($1) $2-$3");
}
e.target.value = value;
}
});
});
// Formatação de valores monetários
const moneyInputs = document.querySelectorAll('input[type="number"][step="0.01"]');
moneyInputs.forEach(input => {
input.addEventListener('blur', function(e) {
const value = parseFloat(e.target.value);
if (!isNaN(value)) {
e.target.value = value.toFixed(2);
}
});
});
});
// Funções para tabelas
document.addEventListener('DOMContentLoaded', function() {
const tables = document.querySelectorAll('.table');
tables.forEach(table => {
// Ordenação
const headers = table.querySelectorAll('th[data-sort]');
headers.forEach(header => {
header.addEventListener('click', function() {
const column = this.dataset.sort;
const asc = this.classList.toggle('sort-asc');
const tbody = table.querySelector('tbody');
const rows = Array.from(tbody.querySelectorAll('tr'));
rows.sort((a, b) => {
const aVal = a.querySelector(`td[data-${column}]`).dataset[column];
const bVal = b.querySelector(`td[data-${column}]`).dataset[column];
return asc ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal);
});
rows.forEach(row => tbody.appendChild(row));
});
});
// Filtro
const filterInput = document.querySelector(`#filter-${table.id}`);
if (filterInput) {
filterInput.addEventListener('input', function() {
const searchTerm = this.value.toLowerCase();
const rows = table.querySelectorAll('tbody tr');
rows.forEach(row => {
const text = row.textContent.toLowerCase();
row.style.display = text.includes(searchTerm) ? '' : 'none';
});
});
}
});
});
// Validação de formulários
document.addEventListener('DOMContentLoaded', function() {
const forms = document.querySelectorAll('form');
forms.forEach(form => {
form.addEventListener('submit', function(e) {
if (!form.checkValidity()) {
e.preventDefault();
e.stopPropagation();
// Destacar campos inválidos
const invalidInputs = form.querySelectorAll(':invalid');
invalidInputs.forEach(input => {
input.classList.add('is-invalid');
// Adicionar mensagem de erro
const feedback = document.createElement('div');
feedback.className = 'invalid-feedback';
feedback.textContent = input.validationMessage;
input.parentNode.appendChild(feedback);
});
}
form.classList.add('was-validated');
});
});
});
// Animações e feedback visual
document.addEventListener('DOMContentLoaded', function() {
// Animar cards ao carregar
const cards = document.querySelectorAll('.card');
cards.forEach((card, index) => {
card.style.opacity = '0';
card.style.transform = 'translateY(20px)';
setTimeout(() => {
card.style.transition = 'all 0.3s ease';
card.style.opacity = '1';
card.style.transform = 'translateY(0)';
}, index * 100);
});
// Feedback visual para ações
const actionButtons = document.querySelectorAll('[data-action]');
actionButtons.forEach(button => {
button.addEventListener('click', function() {
button.classList.add('animate__animated', 'animate__pulse');
setTimeout(() => {
button.classList.remove('animate__animated', 'animate__pulse');
}, 1000);
});
});
});
// Confirmações de ações
document.addEventListener('DOMContentLoaded', function() {
const deleteButtons = document.querySelectorAll('[data-confirm]');
deleteButtons.forEach(button => {
button.addEventListener('click', function(e) {
if (!confirm(this.dataset.confirm)) {
e.preventDefault();
}
});
});
});

742
static/js/militantes.js Normal file
View File

@@ -0,0 +1,742 @@
console.log('Carregando script militantes.js...');
// Variáveis globais para controle dos filtros e paginação
let filtroAtual = 'todos';
let filtroResponsabilidade = null;
let filtroCelula = null;
let currentPage = 1;
let rowsPerPage = 20;
let totalRows = 0;
// Função para calcular o total de páginas
function calculateTotalPages() {
const allRows = document.querySelectorAll('#militantesTable tbody tr');
const visibleRows = Array.from(allRows).filter(row =>
!row.hasAttribute('data-filtered-out')
);
totalRows = visibleRows.length;
return Math.ceil(totalRows / rowsPerPage);
}
// Função para atualizar o texto de contagem
function updateCountText() {
const allRows = document.querySelectorAll('#militantesTable tbody tr');
const visibleRows = Array.from(allRows).filter(row =>
!row.hasAttribute('data-filtered-out')
);
totalRows = visibleRows.length;
const startIndex = (currentPage - 1) * rowsPerPage + 1;
const endIndex = Math.min(currentPage * rowsPerPage, totalRows);
// Atualizar texto de contagem
document.getElementById('countMilitantes').textContent =
`${startIndex}-${endIndex} de ${totalRows}`;
}
// Função para atualizar a paginação
function updatePagination() {
const totalPages = calculateTotalPages();
const paginationUl = document.querySelector('.pagination');
const prevPage = document.getElementById('prevPage');
const nextPage = document.getElementById('nextPage');
// Limpar páginas existentes (exceto prev e next)
const pageItems = paginationUl.querySelectorAll('li:not(#prevPage):not(#nextPage)');
pageItems.forEach(item => item.remove());
// Adicionar novas páginas
for (let i = 1; i <= totalPages; i++) {
const li = document.createElement('li');
li.className = `page-item${i === currentPage ? ' active' : ''}`;
li.innerHTML = `<a class="page-link" href="#">${i}</a>`;
li.addEventListener('click', (e) => {
e.preventDefault();
currentPage = i;
updateVisibleRows();
updatePagination();
});
paginationUl.insertBefore(li, nextPage);
}
// Atualizar estado dos botões prev/next
prevPage.classList.toggle('disabled', currentPage === 1);
nextPage.classList.toggle('disabled', currentPage === totalPages || totalPages === 0);
// Adicionar eventos aos botões prev/next
prevPage.onclick = (e) => {
e.preventDefault();
if (currentPage > 1) {
currentPage--;
updateVisibleRows();
updatePagination();
}
};
nextPage.onclick = (e) => {
e.preventDefault();
if (currentPage < totalPages) {
currentPage++;
updateVisibleRows();
updatePagination();
}
};
// Atualizar texto de contagem
updateCountText();
}
// Função para atualizar as linhas visíveis
function updateVisibleRows() {
const allRows = document.querySelectorAll('#militantesTable tbody tr');
const visibleRows = Array.from(allRows).filter(row =>
!row.hasAttribute('data-filtered-out')
);
const startIndex = (currentPage - 1) * rowsPerPage;
const endIndex = startIndex + rowsPerPage;
visibleRows.forEach((row, index) => {
if (index >= startIndex && index < endIndex) {
row.style.display = '';
} else {
row.style.display = 'none';
}
});
updateCountText();
}
// Função para filtrar militantes
function filtrarMilitantes() {
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
const rows = document.querySelectorAll('#militantesTable tbody tr');
rows.forEach(row => {
let shouldShow = true;
// Filtro de texto
const textContent = row.textContent.toLowerCase();
if (!textContent.includes(searchTerm)) {
shouldShow = false;
}
// Filtro de responsabilidades
if (filtroResponsabilidade) {
const badges = row.querySelectorAll('.badge');
const hasResponsabilidade = Array.from(badges).some(badge =>
badge.textContent.toLowerCase() === filtroResponsabilidade.toLowerCase()
);
if (!hasResponsabilidade) {
shouldShow = false;
}
}
// Filtro de célula
if (filtroCelula) {
const celula = row.querySelector('[data-celula]').getAttribute('data-celula');
if (celula !== filtroCelula) {
shouldShow = false;
}
}
// Marcar linha como filtrada ou não
if (shouldShow) {
row.removeAttribute('data-filtered-out');
} else {
row.setAttribute('data-filtered-out', '');
row.style.display = 'none';
}
});
// Resetar para a primeira página e atualizar paginação
currentPage = 1;
updateVisibleRows();
updatePagination();
}
// Configurar eventos quando o DOM estiver carregado
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM carregado, configurando eventos...');
// Configurar seletor de linhas por página
const rowsPerPageSelect = document.getElementById('rowsPerPage');
if (rowsPerPageSelect) {
rowsPerPageSelect.addEventListener('change', function() {
rowsPerPage = parseInt(this.value);
currentPage = 1;
updateVisibleRows();
updatePagination();
});
}
// Configurar pesquisa
const searchInput = document.getElementById('searchInput');
if (searchInput) {
let timeoutId;
searchInput.addEventListener('input', function() {
clearTimeout(timeoutId);
timeoutId = setTimeout(filtrarMilitantes, 300);
});
}
// Configurar filtros
document.querySelectorAll('.dropdown-item[data-filter]').forEach(item => {
item.addEventListener('click', function(e) {
e.preventDefault();
const filter = this.getAttribute('data-filter');
const celula = this.getAttribute('data-celula');
// Resetar filtros anteriores
if (filter === 'todos') {
filtroAtual = 'todos';
filtroResponsabilidade = null;
filtroCelula = null;
} else if (['financas', 'imprensa', 'quadro-orientador'].includes(filter)) {
filtroAtual = 'todos';
filtroResponsabilidade = filter === 'financas' ? 'Finanças' :
filter === 'imprensa' ? 'Imprensa' :
'Quadro-Orientador';
filtroCelula = null;
} else if (filter === 'celula') {
filtroAtual = 'todos';
filtroResponsabilidade = null;
filtroCelula = celula;
}
filtrarMilitantes();
// Atualizar texto do botão de filtro
const filterText = this.textContent;
const dropdownButton = document.querySelector('.dropdown-toggle');
dropdownButton.innerHTML = `<i class="fas fa-filter me-2"></i>${filterText}`;
});
});
console.log('Configurando modal de edição...');
// Configuração do modal de edição
const modalEditarMilitante = document.getElementById('modalEditarMilitante');
if (modalEditarMilitante) {
console.log('Modal encontrado, configurando eventos...');
// Criar instância do modal Bootstrap
const modalInstance = new bootstrap.Modal(modalEditarMilitante);
console.log('Instância do modal criada:', modalInstance);
// Configurar eventos quando o modal é mostrado
modalEditarMilitante.addEventListener('show.bs.modal', function(event) {
console.log('Modal sendo aberto...');
// Obter o botão que disparou o modal
const button = event.relatedTarget;
const militanteId = button.getAttribute('data-militante-id');
const militanteNome = button.getAttribute('data-militante-nome');
console.log('ID do militante:', militanteId);
console.log('Nome do militante:', militanteNome);
// Atualizar o título do modal
const modalTitle = modalEditarMilitante.querySelector('.modal-title');
if (modalTitle) {
modalTitle.innerHTML = `<i class="fas fa-user-edit me-2"></i>Editar ${militanteNome}`;
}
// Definir o ID do militante no campo hidden
const idField = document.getElementById('edit_militante_id');
if (idField) {
idField.value = militanteId;
}
// Buscar dados do militante
fetch(`/militantes/dados/${militanteId}`, {
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => {
if (!response.ok) {
return response.json().then(data => {
throw new Error(data.message || `HTTP error! status: ${response.status}`);
});
}
return response.json();
})
.then(dados => {
console.log('Dados do militante recebidos:', dados);
if (!dados) {
throw new Error('Dados do militante não encontrados');
}
try {
// Preencher os campos do formulário
const campos = {
'edit_nome': dados.nome,
'edit_cpf': dados.cpf,
'edit_titulo_eleitoral': dados.titulo_eleitoral,
'edit_data_nascimento': dados.data_nascimento,
'edit_data_entrada': dados.data_entrada_oci,
'edit_data_efetivacao': dados.data_efetivacao_oci,
'edit_telefone1': dados.telefone1,
'edit_telefone2': dados.telefone2,
'edit_email': dados.email,
'edit_cep': dados.endereco?.cep,
'edit_estado': dados.endereco?.estado,
'edit_cidade': dados.endereco?.cidade,
'edit_bairro': dados.endereco?.bairro,
'edit_rua': dados.endereco?.rua,
'edit_numero': dados.endereco?.numero,
'edit_complemento': dados.endereco?.complemento,
'edit_profissao': dados.profissao,
'edit_regime_trabalho': dados.regime_trabalho,
'edit_empresa': dados.empresa,
'edit_contratante': dados.contratante,
'edit_instituicao_ensino': dados.instituicao_ensino,
'edit_tipo_instituicao': dados.tipo_instituicao,
'edit_sindicato': dados.sindicato,
'edit_cargo_sindical': dados.cargo_sindical,
'edit_central_sindical': dados.central_sindical,
'edit_estado_militante': dados.estado,
'edit_celula': dados.celula_id
};
// Preencher cada campo se ele existir
Object.entries(campos).forEach(([id, valor]) => {
const campo = document.getElementById(id);
if (campo && valor !== undefined && valor !== null) {
campo.value = valor;
}
});
// Checkbox de dirigente sindical
const checkDirigente = document.getElementById('edit_dirigente_sindical');
if (checkDirigente) {
checkDirigente.checked = dados.dirigente_sindical;
}
// Checkboxes de responsabilidades
const responsabilidadesMap = {
'Finanças': 'edit_resp_1',
'Imprensa': 'edit_resp_2',
'Quadro-Orientador': 'edit_resp_4'
};
console.log('Responsabilidades recebidas:', dados.responsabilidades);
// Resetar todos os checkboxes primeiro
Object.values(responsabilidadesMap).forEach(id => {
const checkbox = document.getElementById(id);
if (checkbox) {
checkbox.checked = false;
}
});
// Marcar os checkboxes baseado nas responsabilidades recebidas
if (Array.isArray(dados.responsabilidades)) {
dados.responsabilidades.forEach(resp => {
const checkboxId = responsabilidadesMap[resp];
if (checkboxId) {
const checkbox = document.getElementById(checkboxId);
if (checkbox) {
checkbox.checked = true;
console.log(`Marcando checkbox ${checkboxId} para responsabilidade ${resp}`);
}
}
});
}
console.log('Formulário preenchido com sucesso');
} catch (error) {
console.error('Erro ao preencher formulário:', error);
mostrarAlerta('Erro ao carregar dados do militante', 'danger');
}
})
.catch(error => {
console.error('Erro ao buscar dados:', error);
mostrarAlerta('Erro ao carregar dados do militante', 'danger');
});
});
// Envio do formulário de edição via AJAX
const formEditarMilitante = document.getElementById('formEditarMilitante');
if (formEditarMilitante) {
formEditarMilitante.addEventListener('submit', function(e) {
e.preventDefault();
console.log('Enviando formulário de edição...');
const formData = new FormData(this);
// Coletar responsabilidades selecionadas
const responsabilidades = [];
if (document.getElementById('edit_resp_1').checked) responsabilidades.push('Finanças');
if (document.getElementById('edit_resp_2').checked) responsabilidades.push('Imprensa');
if (document.getElementById('edit_resp_4').checked) responsabilidades.push('Quadro-Orientador');
formData.set('responsabilidades', JSON.stringify(responsabilidades));
console.log('Responsabilidades enviadas:', responsabilidades);
// Obter o CSRF token
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
// Obter o ID do militante do campo hidden
const militanteId = document.getElementById('edit_militante_id').value;
if (!militanteId) {
console.error('ID do militante não encontrado!');
mostrarAlerta('Erro: ID do militante não encontrado', 'danger');
return;
}
// Garantir que o campo de endereço está correto
const logradouro = formData.get('logradouro');
if (logradouro) {
formData.set('rua', logradouro);
formData.delete('logradouro');
}
fetch(`/militantes/editar/${militanteId}`, {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRFToken': csrfToken
},
credentials: 'same-origin'
})
.then(response => {
console.log('Resposta recebida:', response);
if (!response.ok) {
return response.json().then(data => {
throw new Error(data.message || `HTTP error! status: ${response.status}`);
});
}
return response.json();
})
.then(data => {
console.log('Resposta processada:', data);
if (data.status === 'success') {
// Fechar o modal
bootstrap.Modal.getInstance(modalEditarMilitante).hide();
// Mostrar mensagem de sucesso
mostrarAlerta(data.message, 'success');
// Recarregar a página após um breve delay
setTimeout(() => {
location.reload();
}, 1500);
} else {
throw new Error(data.message || 'Erro ao salvar dados');
}
})
.catch(error => {
console.error('Erro ao enviar formulário:', error);
mostrarAlerta(`Erro ao salvar dados: ${error.message}`, 'danger');
});
});
}
// Limpar alertas quando o modal for fechado
modalEditarMilitante.addEventListener('hidden.bs.modal', function () {
const alerts = this.querySelectorAll('.alert');
alerts.forEach(alert => alert.remove());
});
} else {
console.error('Modal de edição não encontrado!');
}
// Envio do formulário via AJAX
const formNovoMilitante = document.getElementById('formNovoMilitante');
if (formNovoMilitante) {
formNovoMilitante.addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
fetch(this.action, {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
// Fechar o modal
const modal = bootstrap.Modal.getInstance(document.getElementById('modalNovoMilitante'));
modal.hide();
// Limpar o formulário
formNovoMilitante.reset();
// Adicionar o novo militante à tabela
const tbody = document.querySelector('#militantesTable tbody');
const tr = document.createElement('tr');
tr.setAttribute('data-militante', data.militante.id);
tr.setAttribute('data-filiado', data.militante.filiado ? 'sim' : 'nao');
tr.innerHTML = `
<td data-nome="${data.militante.nome}">${data.militante.nome}</td>
<td data-cpf="${data.militante.cpf}">${data.militante.cpf}</td>
<td data-email="${data.militante.email}">${data.militante.email}</td>
<td data-telefone="${data.militante.telefone}">${data.militante.telefone}</td>
<td data-filiado="${data.militante.filiado}">
<span class="badge ${data.militante.filiado ? 'bg-success' : 'bg-secondary'}">
${data.militante.filiado ? 'Sim' : 'Não'}
</span>
</td>
<td class="text-end">
<div class="btn-group">
<button type="button"
class="btn btn-sm btn-outline-primary"
data-bs-toggle="modal"
data-bs-target="#modalEditarMilitante"
data-militante-id="${data.militante.id}"
data-militante-nome="${data.militante.nome}"
data-militante-cpf="${data.militante.cpf}"
data-militante-email="${data.militante.email}"
data-militante-telefone="${data.militante.telefone}"
data-militante-endereco="${data.militante.endereco}"
data-militante-filiado="${data.militante.filiado}"
title="Editar">
<i class="fas fa-edit"></i>
</button>
<button type="button"
class="btn btn-sm btn-outline-danger"
data-bs-toggle="modal"
data-bs-target="#deleteModal"
data-militante-id="${data.militante.id}"
data-militante-nome="${data.militante.nome}"
title="Excluir">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
`;
// Inserir no início da tabela
tbody.insertBefore(tr, tbody.firstChild);
// Atualizar contador
const countElement = document.getElementById('countMilitantes');
countElement.textContent = parseInt(countElement.textContent) + 1;
// Mostrar mensagem de sucesso
const alertDiv = document.createElement('div');
alertDiv.className = 'alert alert-success alert-dismissible fade show';
alertDiv.innerHTML = `
${data.message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.querySelector('.container').insertBefore(alertDiv, document.querySelector('.container').firstChild);
} else {
// Mostrar erro
const alertDiv = document.createElement('div');
alertDiv.className = 'alert alert-danger alert-dismissible fade show';
alertDiv.innerHTML = `
${data.message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.querySelector('.modal-body').insertBefore(alertDiv, formNovoMilitante);
}
})
.catch(error => {
console.error('Erro:', error);
// Mostrar erro genérico
const alertDiv = document.createElement('div');
alertDiv.className = 'alert alert-danger alert-dismissible fade show';
alertDiv.innerHTML = `
Erro ao cadastrar militante. Tente novamente.
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.querySelector('.modal-body').insertBefore(alertDiv, formNovoMilitante);
});
});
}
// Máscara para CPF
const cpfInputs = document.querySelectorAll('input[name="cpf"]');
cpfInputs.forEach(input => {
input.addEventListener('input', function(e) {
let value = e.target.value.replace(/\D/g, '');
if (value.length <= 11) {
value = value.replace(/(\d{3})(\d)/, '$1.$2');
value = value.replace(/(\d{3})(\d)/, '$1.$2');
value = value.replace(/(\d{3})(\d{1,2})$/, '$1-$2');
e.target.value = value;
}
});
});
// Máscara para telefone
const telefoneInputs = document.querySelectorAll('input[name="telefone1"], input[name="telefone2"]');
telefoneInputs.forEach(input => {
input.addEventListener('input', function(e) {
let value = e.target.value.replace(/\D/g, '');
if (value.length <= 11) {
value = value.replace(/(\d{2})(\d)/, '($1) $2');
value = value.replace(/(\d{4,5})(\d{4})$/, '$1-$2');
e.target.value = value;
}
});
});
// Limpar formulário e alertas quando o modal for fechado
const modalNovoMilitante = document.getElementById('modalNovoMilitante');
if (modalNovoMilitante) {
modalNovoMilitante.addEventListener('hidden.bs.modal', function () {
formNovoMilitante.reset();
const alerts = this.querySelectorAll('.alert');
alerts.forEach(alert => alert.remove());
});
}
// Configuração do modal de exclusão
const deleteModal = document.getElementById('deleteModal');
if (deleteModal) {
deleteModal.addEventListener('show.bs.modal', function(event) {
const button = event.relatedTarget;
const militanteId = button.getAttribute('data-militante-id');
const militanteNome = button.getAttribute('data-militante-nome');
document.getElementById('militanteNome').textContent = militanteNome;
document.getElementById('deleteForm').action = `/militantes/excluir/${militanteId}`;
});
}
// Ordenação
const headers = document.querySelectorAll('#militantesTable th[data-sort]');
headers.forEach(header => {
header.addEventListener('click', function() {
const column = this.getAttribute('data-sort');
const tbody = document.querySelector('#militantesTable tbody');
const rows = Array.from(tbody.querySelectorAll('tr'));
const isAsc = !this.classList.contains('sort-asc');
// Remover classes de ordenação de todos os headers
headers.forEach(h => {
h.classList.remove('sort-asc', 'sort-desc');
h.querySelector('i').className = 'fas fa-sort';
});
// Adicionar classe de ordenação ao header clicado
this.classList.add(isAsc ? 'sort-asc' : 'sort-desc');
this.querySelector('i').className = `fas fa-sort-${isAsc ? 'up' : 'down'}`;
// Ordenar linhas
rows.sort((a, b) => {
const aVal = a.querySelector(`td[data-${column}]`).getAttribute(`data-${column}`);
const bVal = b.querySelector(`td[data-${column}]`).getAttribute(`data-${column}`);
return isAsc ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal);
});
// Reposicionar linhas
rows.forEach(row => tbody.appendChild(row));
});
});
// Exportar para CSV
const btnExportar = document.getElementById('btnExportar');
if (btnExportar) {
btnExportar.addEventListener('click', function() {
const rows = document.querySelectorAll('#militantesTable tbody tr:not([style*="display: none"])');
const headers = ['Nome', 'CPF', 'Email', 'Telefone', 'Filiado'];
let csv = headers.join(',') + '\n';
rows.forEach(row => {
const cols = row.querySelectorAll('td');
const values = [
cols[0].textContent,
cols[1].textContent,
cols[2].textContent,
cols[3].textContent,
cols[4].textContent.trim()
].map(val => `"${val}"`);
csv += values.join(',') + '\n';
});
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.setAttribute('download', 'militantes.csv');
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});
}
// Configurar máscaras de input
// CEP
const cepInputs = document.querySelectorAll('input[name="cep"]');
cepInputs.forEach(input => {
input.addEventListener('input', function(e) {
let value = e.target.value.replace(/\D/g, '');
if (value.length <= 8) {
value = value.replace(/(\d{5})(\d)/, '$1-$2');
e.target.value = value;
}
});
// Buscar endereço pelo CEP
input.addEventListener('blur', function(e) {
const cep = e.target.value.replace(/\D/g, '');
if (cep.length === 8) {
fetch(`https://viacep.com.br/ws/${cep}/json/`)
.then(response => response.json())
.then(data => {
if (!data.erro) {
const form = input.closest('form');
form.querySelector('input[name="logradouro"]').value = data.logradouro;
form.querySelector('input[name="bairro"]').value = data.bairro;
form.querySelector('input[name="cidade"]').value = data.localidade;
form.querySelector('select[name="estado"]').value = data.uf;
}
});
}
});
});
// Título Eleitoral
const tituloInputs = document.querySelectorAll('input[name="titulo_eleitoral"]');
tituloInputs.forEach(input => {
input.addEventListener('input', function(e) {
let value = e.target.value.replace(/\D/g, '');
if (value.length <= 12) {
value = value.replace(/(\d{4})(\d)/, '$1 $2');
value = value.replace(/(\d{4})(\d)/, '$1 $2');
e.target.value = value;
}
});
});
// Inicializar paginação
updateVisibleRows();
updatePagination();
});
// Função para mostrar alertas
function mostrarAlerta(mensagem, tipo) {
// Criar o elemento de alerta
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${tipo} alert-dismissible fade show position-fixed top-0 start-50 translate-middle-x mt-3`;
alertDiv.style.zIndex = '9999';
alertDiv.innerHTML = `
${mensagem}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
// Adicionar o alerta ao corpo do documento
document.body.appendChild(alertDiv);
// Configurar o Bootstrap alert
const bsAlert = new bootstrap.Alert(alertDiv);
// Remover o alerta após 3 segundos
setTimeout(() => {
bsAlert.close();
}, 3000);
}

256
static/js/pagamentos.js Normal file
View File

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

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

@@ -3,39 +3,625 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{% endblock %} - Sistema de Gestão</title>
{{ bootstrap.load_css() }}
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{% block title %}{% endblock %} - Controles OCI</title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css?v=1" rel="stylesheet">
<!-- Font Awesome 6 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css?v=1">
<!-- Componentes CSS -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/components.css') }}">
<style>
:root {
--primary-color: #dc3545;
--primary-light: #e35d6a;
--secondary-color: #6c757d;
--secondary-light: #868e96;
--success-color: #198754;
--danger-color: #dc3545;
--warning-color: #ffc107;
--info-color: #0dcaf0;
--background-gradient: linear-gradient(135deg, var(--primary-color) 40%, white 100%);
--navbar-stripe: 4px solid var(--primary-color);
/* Adicionando variáveis para os botões */
--bs-success: #198754;
--bs-success-dark: #157347;
--bs-secondary: #6c757d;
--bs-secondary-dark: #565e64;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f8f9fa;
margin: 0;
padding: 0;
min-height: 100vh;
}
.navbar {
background: #343a40 !important;
padding: 0.5rem 1rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
border-bottom: var(--navbar-stripe);
}
.navbar > .container-fluid {
width: 100%;
max-width: 1320px;
margin: 0 auto;
padding: 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.navbar-brand {
flex: 0 0 auto;
margin-right: 2rem;
font-weight: 500;
color: #fff !important;
display: flex;
align-items: center;
white-space: nowrap;
font-size: 1.2rem;
}
.navbar-brand img {
height: 35px;
margin-right: 0.75rem;
}
#navbarNav {
display: flex;
justify-content: center;
flex: 1;
}
.navbar-nav.mx-auto {
margin: 0 auto;
}
.navbar-nav:last-child {
flex: 0 0 auto;
margin-left: 2rem;
}
.nav-link {
color: rgba(255,255,255,0.85) !important;
transition: all 0.2s ease;
padding: 0.75rem 1rem;
white-space: nowrap;
font-size: 0.95rem;
font-weight: 400;
letter-spacing: 0.3px;
}
.nav-link:hover {
color: #fff !important;
background-color: var(--primary-color);
border-radius: 4px;
}
.nav-link i {
font-size: 0.9rem;
opacity: 0.9;
margin-right: 0.5rem;
}
.dropdown-menu {
background-color: #343a40;
border: 1px solid rgba(255,255,255,0.1);
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
padding: 0.5rem;
margin-top: 0.5rem;
border-radius: 8px;
min-width: 200px;
}
.dropdown-item {
color: rgba(255,255,255,0.85) !important;
font-size: 0.9rem;
font-weight: 400;
padding: 0.6rem 1rem;
transition: all 0.2s ease;
border-radius: 6px;
}
.dropdown-item:hover {
background-color: var(--primary-color);
color: #fff !important;
transform: translateX(3px);
}
.dropdown-item i {
margin-right: 0.75rem;
width: 1.25rem;
text-align: center;
font-size: 0.9rem;
opacity: 0.9;
}
.dropdown-divider {
border-top: 1px solid rgba(255,255,255,0.1);
margin: 0.5rem 0;
}
/* Estilo para o menu mobile */
@media (max-width: 768px) {
.navbar-collapse {
background-color: #343a40;
padding: 1rem;
border-radius: 0 0 10px 10px;
margin-top: 0.5rem;
}
.navbar-brand img {
height: 30px;
}
.dropdown-menu {
background-color: rgba(0,0,0,0.2);
margin-left: 1rem;
min-width: auto;
}
.nav-link {
padding: 0.5rem 1rem;
}
.navbar-nav {
flex-direction: column;
align-items: stretch;
}
}
.container {
max-width: 1320px !important;
margin: 0 auto !important;
}
@media (max-width: 1400px) {
.container {
max-width: 1140px !important;
}
}
@media (max-width: 1200px) {
.container {
max-width: 960px !important;
}
.page-wrapper {
padding: 1.5rem 0.75rem;
}
}
@media (max-width: 992px) {
.container {
max-width: 720px !important;
}
}
@media (max-width: 768px) {
.container {
max-width: 540px !important;
}
.page-wrapper {
padding: 1rem 0.5rem;
}
}
@media (max-width: 576px) {
.page-wrapper {
padding: 0.75rem 0.25rem;
}
}
/* Cards da Dashboard */
.card {
border: none;
border-radius: 0.5rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
height: 100%;
}
.card-header {
background-color: #f8f9fa;
border-bottom: 1px solid #e9ecef;
padding: 1rem;
}
.card-header .card-title {
margin: 0;
display: flex;
align-items: center;
gap: 0.5rem;
}
.card-header h5 {
margin: 0;
display: flex;
align-items: center;
font-size: 1.1rem;
}
.card-header h5 i {
margin-right: 0.75rem;
color: var(--primary-color);
}
.card-body {
padding: 1.5rem;
}
.card-footer {
background: none;
border-top: 1px solid rgba(0,0,0,0.05);
padding: 1rem 1.5rem;
}
/* Estatísticas da Dashboard */
.stats-card {
position: relative;
padding: 1.5rem;
border-radius: 0.5rem;
color: white;
box-shadow: 0 0.25rem 0.5rem rgba(0,0,0,0.1);
transition: transform 0.2s ease, box-shadow 0.2s ease;
overflow: hidden;
min-height: 140px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.stats-card:hover {
transform: translateY(-3px);
box-shadow: 0 0.5rem 1rem rgba(0,0,0,0.15);
}
.stats-card.blue {
background: linear-gradient(45deg, var(--primary-color), var(--primary-light));
}
.stats-card.green {
background: linear-gradient(45deg, #1cc88a, #13855c);
}
.stats-card.cyan {
background: linear-gradient(45deg, #36b9cc, #258391);
}
.stats-card.yellow {
background: linear-gradient(45deg, #f6c23e, #dda20a);
}
.stats-card .title {
font-size: 0.9rem;
font-weight: bold;
text-transform: uppercase;
margin-bottom: 0.5rem;
color: rgba(255,255,255,0.8);
}
.stats-card .value {
font-size: 2rem;
font-weight: bold;
margin: 0.5rem 0;
color: white;
}
.stats-card .link {
color: white;
text-decoration: none;
font-size: 0.9rem;
opacity: 0.8;
transition: opacity 0.2s ease;
}
.stats-card .link:hover {
opacity: 1;
}
.stats-card .icon {
position: absolute;
right: 1rem;
bottom: 1rem;
font-size: 4rem;
opacity: 0.2;
color: white;
}
/* Tabelas e Listas */
.table {
margin-bottom: 0;
}
.table th {
border-top: none;
font-weight: 600;
padding: 1rem;
background-color: #f8f9fa;
}
.table td {
padding: 1rem;
vertical-align: middle;
}
.table-hover tbody tr:hover {
background-color: #f8f9fa;
}
.list-group-item {
border: none;
border-bottom: 1px solid rgba(0,0,0,0.05);
padding: 1rem 1.5rem;
transition: background-color 0.2s ease;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
}
.list-group-item:hover {
background-color: #f8f9fa;
}
.list-group-item:last-child {
border-bottom: none;
}
.militante-info {
display: flex;
flex-direction: column;
flex: 1;
}
.militante-info h6 {
margin: 0;
color: #333;
font-weight: 500;
}
.militante-info small {
color: var(--secondary-color);
margin-top: 0.25rem;
}
/* Botões e Alertas */
.alert {
border-radius: 0.5rem;
border: none;
box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,0.075);
}
.btn {
padding: 0.5rem 1rem;
border-radius: 0.25rem;
font-weight: 500;
}
.btn-primary {
background-color: var(--primary-color);
border-color: var(--primary-color);
}
.btn-success {
background-color: var(--success-color);
border-color: var(--success-color);
}
.btn-danger {
background-color: var(--danger-color);
border-color: var(--danger-color);
}
.btn-secondary {
background-color: var(--secondary-color);
border-color: var(--secondary-color);
}
/* Badges e Labels */
.badge {
padding: 0.5em 0.75em;
font-weight: 500;
border-radius: 0.375rem;
}
.text-muted {
color: var(--secondary-color) !important;
}
.modal-content {
border: none;
border-radius: 0.5rem;
}
.modal-header {
border-bottom: 1px solid #e9ecef;
padding: 1rem;
}
.modal-footer {
border-top: 1px solid #e9ecef;
padding: 1rem;
}
/* Login page specific */
.login-page {
background: var(--background-gradient);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.login-container {
background: rgba(255, 255, 255, 0.95);
border-radius: 1rem;
padding: 2rem;
box-shadow: 0 0.5rem 1rem rgba(0,0,0,0.15);
width: 100%;
max-width: 400px;
}
.login-logo {
height: 60px;
width: auto;
margin-bottom: 1rem;
}
.login-title {
color: var(--primary-color);
font-weight: 500;
margin-bottom: 0.5rem;
}
.login-subtitle {
color: var(--secondary-color);
font-size: 0.9rem;
margin-bottom: 2rem;
}
.welcome-header {
margin-bottom: 2rem;
}
@media (max-width: 768px) {
.stats-card {
margin-bottom: 1rem;
}
}
.page-wrapper {
padding: 2rem 1rem;
min-height: calc(100vh - 70px);
}
</style>
{% block extra_css %}{% endblock %}
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="{{ url_for('home') }}">Sistema de Gestão</a>
{% block navbar %}
<nav class="navbar navbar-expand-lg navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="{{ url_for('home') }}">
<img src="{{ url_for('static', filename='img/logo002-alpha.png') }}" alt="Logo OCI">
Controles OCI
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
{% if session.get('user_id') %}
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav mx-auto">
<li class="nav-item dropdown">
<a class="nav-link" href="#" data-bs-toggle="dropdown">
<i class="fas fa-users me-1"></i>Militantes
</a>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" href="{{ url_for('listar_militantes') }}">
<i class="fas fa-list"></i>Listar Militantes
</a>
</li>
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link" href="#" data-bs-toggle="dropdown">
<i class="fas fa-dollar-sign me-1"></i>Financeiro
</a>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" href="{{ url_for('listar_cotas') }}">
<i class="fas fa-money-bill-wave"></i>Cotas
</a>
</li>
<li>
<a class="dropdown-item" href="{{ url_for('listar_pagamentos') }}">
<i class="fas fa-receipt"></i>Pagamentos
</a>
</li>
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link" href="#" data-bs-toggle="dropdown">
<i class="fas fa-box me-1"></i>Materiais
</a>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" href="{{ url_for('listar_materiais') }}">
<i class="fas fa-box"></i>Listar Materiais
</a>
</li>
<li>
<a class="dropdown-item" href="{{ url_for('listar_vendas_jornal') }}">
<i class="fas fa-newspaper"></i>Vendas de Jornais
</a>
</li>
<li>
<a class="dropdown-item" href="{{ url_for('listar_assinaturas') }}">
<i class="fas fa-file-signature"></i>Assinaturas
</a>
</li>
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link" href="#" data-bs-toggle="dropdown">
<i class="fas fa-chart-bar me-1"></i>Relatórios
</a>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" href="{{ url_for('listar_relatorios_cotas') }}">
<i class="fas fa-file-invoice-dollar"></i>Relatórios de Cotas
</a>
</li>
<li>
<a class="dropdown-item" href="{{ url_for('listar_relatorios_vendas') }}">
<i class="fas fa-file-alt"></i>Relatórios de Vendas
</a>
</li>
</ul>
</li>
</ul>
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="{{ url_for('listar_militantes') }}">Militantes</a>
<li class="nav-item dropdown">
<a class="nav-link" href="#" data-bs-toggle="dropdown">
<i class="fas fa-user me-1"></i>{{ session.get('username', 'Usuário') }}
</a>
<ul class="dropdown-menu dropdown-menu-end">
{% if session.get('is_admin') %}
<li>
<a class="dropdown-item" href="{{ url_for('novo_usuario') }}">
<i class="fas fa-user-plus"></i>Novo Usuário
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('listar_cotas') }}">Cotas</a>
<li><hr class="dropdown-divider"></li>
{% endif %}
<li>
<a class="dropdown-item" href="{{ url_for('logout') }}">
<i class="fas fa-sign-out-alt"></i>Sair
</a>
</li>
<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>
</ul>
</li>
</ul>
</div>
{% endif %}
</div>
</nav>
{% endblock %}
<div class="container mt-4">
<div class="page-wrapper">
<div class="container py-4">
{% block content %}{% endblock %}
</div>
</div>
{{ bootstrap.load_js() }}
<!-- Bootstrap 5 JS Bundle with Popper -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
{% block scripts %}{% endblock %}
</body>
</html>

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,29 @@
{% extends "layout.html" %}
{% block content %}
<div class="container mt-4">
<h2>Editar Cota</h2>
<form method="POST" class="needs-validation" novalidate>
<div class="mb-3">
<label for="valor_novo" class="form-label">Valor</label>
<input type="number" step="0.01" class="form-control" id="valor_novo" name="valor_novo" value="{{ cota.valor_novo }}" required>
<div class="invalid-feedback">
Por favor, insira um valor válido.
</div>
</div>
<div class="mb-3">
<label for="data_vencimento" class="form-label">Data de Vencimento</label>
<input type="date" class="form-control" id="data_vencimento" name="data_vencimento" value="{{ cota.data_vencimento }}" required>
<div class="invalid-feedback">
Por favor, selecione uma data de vencimento.
</div>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="pago" name="pago" value="true" {% if cota.pago %}checked{% endif %}>
<label class="form-check-label" for="pago">Pago</label>
</div>
<button type="submit" class="btn btn-primary">Salvar</button>
<a href="{{ url_for('listar_cotas') }}" class="btn btn-secondary">Cancelar</a>
</form>
</div>
{% endblock %}

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,7 +3,10 @@
{% 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 %}
@@ -13,22 +16,214 @@
{% 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">
<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,18 +1,612 @@
{% extends 'base.html' %}
{% extends "base.html" %}
{% block title %}Início{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12">
<h2>Menu do Sistema</h2>
<div class="list-group">
{% for url, endpoint in links %}
<a href="{{ url }}" class="list-group-item list-group-item-action">
{{ endpoint|replace('_', ' ')|title }}
<div class="row g-4">
<div class="col-12">
<div class="welcome-header">
<h2 class="mb-2">Olá, {{ nome_usuario }}!</h2>
<h4 class="text-muted">
{{ data_atual }}
</h4>
</div>
</div>
<!-- Cards de Estatísticas -->
<div class="col-md-6 col-lg-3">
<div class="stats-card blue">
<div class="title">Total de Militantes</div>
<div class="value">{{ total_militantes }}</div>
<a href="{{ url_for('listar_militantes') }}" class="link">
Ver detalhes <i class="fas fa-arrow-right"></i>
</a>
{% endfor %}
<div class="icon">
<i class="fas fa-users"></i>
</div>
</div>
</div>
<div class="col-md-6 col-lg-3">
<div class="stats-card green">
<div class="title">Total de Cotas</div>
<div class="value">R$ {{ total_cotas }}</div>
<a href="{{ url_for('listar_cotas') }}" class="link">
Ver detalhes <i class="fas fa-arrow-right"></i>
</a>
<div class="icon">
<i class="fas fa-dollar-sign"></i>
</div>
</div>
</div>
<div class="col-md-6 col-lg-3">
<div class="stats-card cyan">
<div class="title">Materiais Vendidos</div>
<div class="value">{{ total_materiais }}</div>
<a href="{{ url_for('listar_materiais') }}" class="link">
Ver detalhes <i class="fas fa-arrow-right"></i>
</a>
<div class="icon">
<i class="fas fa-book"></i>
</div>
</div>
</div>
<div class="col-md-6 col-lg-3">
<div class="stats-card yellow">
<div class="title">Assinaturas Ativas</div>
<div class="value">{{ total_assinaturas }}</div>
<a href="{{ url_for('listar_assinaturas') }}" class="link">
Ver detalhes <i class="fas fa-arrow-right"></i>
</a>
<div class="icon">
<i class="fas fa-newspaper"></i>
</div>
</div>
</div>
</div>
<div class="row mt-4">
<!-- Últimos Militantes -->
<div class="col-md-6 mb-4">
<div class="card h-100">
<div class="card-header">
<h5 class="card-title">
<i class="fas fa-user-plus"></i>Últimos Militantes Cadastrados
</h5>
</div>
<div class="card-body p-0">
{% if ultimos_militantes %}
<div class="list-group list-group-flush">
{% for militante in ultimos_militantes %}
<div class="list-group-item" style="cursor: pointer"
data-bs-toggle="modal"
data-bs-target="#modalEditarMilitante"
data-militante-id="{{ militante.id }}"
data-militante-nome="{{ militante.nome }}">
<div class="militante-info">
<h6 class="mb-1">{{ militante.nome }}</h6>
<small>{{ militante.email }}</small>
</div>
<i class="fas fa-chevron-right text-muted"></i>
</div>
{% endfor %}
</div>
{% else %}
<p class="text-muted m-3">Nenhum militante cadastrado recentemente.</p>
{% endif %}
</div>
</div>
</div>
<!-- Últimos Pagamentos -->
<div class="col-md-6 mb-4">
<div class="card h-100">
<div class="card-header">
<h5 class="card-title">
<i class="fas fa-money-bill-wave"></i>Últimos Pagamentos
</h5>
</div>
<div class="card-body p-0">
{% if ultimos_pagamentos %}
<div class="list-group list-group-flush">
{% for pagamento in ultimos_pagamentos %}
<div class="list-group-item" style="cursor: pointer" onclick="carregarDadosPagamento({{ pagamento.id }})">
<div class="militante-info">
<h6 class="mb-1">{{ pagamento.militante.nome }}</h6>
<small>{{ pagamento.data_pagamento.strftime('%d/%m/%Y') }}</small>
</div>
<div class="d-flex align-items-center gap-2">
<span class="badge bg-success">R$ {{ "%.2f"|format(pagamento.valor) }}</span>
<div class="dropdown">
<button class="btn btn-link text-secondary p-0" type="button" data-bs-toggle="dropdown">
<i class="fas fa-ellipsis-v"></i>
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<a class="dropdown-item" href="#" onclick="event.stopPropagation(); carregarDadosPagamento({{ pagamento.id }})">
<i class="fas fa-edit me-2"></i>Editar
</a>
</li>
<li>
<a class="dropdown-item text-danger" href="#" onclick="event.stopPropagation(); confirmarExclusao({{ pagamento.id }}, 'pagamentos')">
<i class="fas fa-trash me-2"></i>Excluir
</a>
</li>
</ul>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<p class="text-muted m-3">Nenhum pagamento registrado recentemente.</p>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Modal de Edição de Pagamento -->
<div class="modal fade" id="modalEditarPagamento" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-money-bill-wave me-2"></i>Editar Pagamento
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="formEditarPagamento" method="post">
<input type="hidden" id="editPagamentoId" name="id">
<div class="mb-3">
<label for="editValor" class="form-label">Valor</label>
<div class="input-group">
<span class="input-group-text">R$</span>
<input type="number" step="0.01" class="form-control" id="editValor" name="valor" required>
</div>
</div>
<div class="mb-3">
<label for="editDataPagamento" class="form-label">Data do Pagamento</label>
<input type="date" class="form-control" id="editDataPagamento" name="data_pagamento" required>
</div>
<div class="mb-3">
<label for="editTipoPagamento" class="form-label">Tipo de Pagamento</label>
<select class="form-select" id="editTipoPagamento" name="tipo_pagamento" required>
{% for tipo in tipos_pagamento %}
<option value="{{ tipo.id }}">{{ tipo.nome }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="editObservacao" class="form-label">Observação</label>
<textarea class="form-control" id="editObservacao" name="observacao" rows="3"></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<i class="fas fa-times me-2"></i>Cancelar
</button>
<button type="submit" form="formEditarPagamento" class="btn btn-primary">
<i class="fas fa-save me-2"></i>Salvar Alterações
</button>
</div>
</div>
</div>
</div>
<!-- Modal de Confirmação de Exclusão -->
<div class="modal fade" id="deleteModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-exclamation-triangle me-2"></i>Confirmar Exclusão
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>Tem certeza que deseja excluir este item?</p>
<p class="text-danger"><small>Esta ação não pode ser desfeita.</small></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<i class="fas fa-times me-2"></i>Cancelar
</button>
<button type="button" class="btn btn-danger" id="btnConfirmarExclusao">
<i class="fas fa-trash me-2"></i>Excluir
</button>
</div>
</div>
</div>
</div>
<!-- Incluir os modais globais de militantes -->
{% include 'modals/militante_editar.html' %}
{% include 'modals/militante_excluir.html' %}
<style>
.welcome-header {
background: linear-gradient(to right, var(--background-color), rgba(232, 0, 12, 0.05));
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 2rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
.welcome-header h2 {
font-size: 2rem;
margin-bottom: 0.5rem;
color: var(--primary-color);
}
.welcome-header h4 {
font-size: 1.2rem;
color: var(--secondary-color);
opacity: 0.8;
margin: 0;
}
.valor-container {
flex: 1;
min-width: 0; /* Permite que o texto quebre corretamente */
}
.valor-cota {
font-size: calc(1.2rem + 0.8vw);
line-height: 1.2;
word-break: break-word;
margin-right: 0.5rem;
}
.icon-container {
font-size: 1.5rem;
opacity: 0.8;
margin-left: 8px;
margin-top: 4px;
}
/* Estilo para o backdrop com blur em todos os modais */
.modal-backdrop.show {
backdrop-filter: blur(8px);
background-color: rgba(0, 0, 0, 0.7);
}
/* Estilo para o botão de fechar dos modais */
.btn-close {
background-color: transparent;
padding: 0.5rem;
opacity: 0.8;
transition: opacity 0.2s;
filter: invert(1) grayscale(100%) brightness(200%);
}
.btn-close:hover {
opacity: 1;
background-color: transparent;
}
/* Estilo para modais */
.modal-content {
border: none;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
}
.modal-header {
background: linear-gradient(to right, var(--bs-gray-dark), var(--bs-gray));
color: white;
border-radius: 12px 12px 0 0;
border-bottom: none;
padding: 1rem;
}
.modal-body {
padding: 1.5rem;
}
.modal-footer {
border-top: 1px solid #eee;
padding: 1rem;
}
/* Garantir que o botão de editar fique azul */
.btn-primary {
background-color: var(--bs-primary) !important;
border-color: var(--bs-primary) !important;
}
.btn-primary:hover,
.btn-primary:focus,
.btn-primary:active {
background-color: var(--bs-primary-dark) !important;
border-color: var(--bs-primary-dark) !important;
}
/* Estilo para os itens da lista de militantes */
.list-group-item {
transition: all 0.3s ease;
border-left: 3px solid transparent;
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
}
.list-group-item:hover {
background-color: rgba(232, 0, 12, 0.05);
border-left-color: var(--primary-color);
transform: translateX(5px);
}
.list-group-item .militante-info {
flex: 1;
}
.list-group-item .fa-chevron-right {
color: var(--primary-color);
opacity: 0.5;
transition: opacity 0.3s ease;
}
.list-group-item:hover .fa-chevron-right {
opacity: 1;
}
/* Garantir que o botão de salvar mantenha a cor correta */
.btn-success,
.modal-footer .btn-success {
background-color: var(--bs-success) !important;
border-color: var(--bs-success) !important;
color: white !important;
}
.btn-success:hover,
.btn-success:focus,
.btn-success:active,
.modal-footer .btn-success:hover,
.modal-footer .btn-success:focus,
.modal-footer .btn-success:active {
background-color: var(--bs-success-dark) !important;
border-color: var(--bs-success-dark) !important;
color: white !important;
opacity: 0.9;
}
/* Garantir que o botão de cancelar mantenha a cor correta */
.btn-secondary,
.modal-footer .btn-secondary {
background-color: var(--bs-secondary) !important;
border-color: var(--bs-secondary) !important;
color: white !important;
}
.btn-secondary:hover,
.btn-secondary:focus,
.btn-secondary:active,
.modal-footer .btn-secondary:hover,
.modal-footer .btn-secondary:focus,
.modal-footer .btn-secondary:active {
background-color: var(--bs-secondary-dark) !important;
border-color: var(--bs-secondary-dark) !important;
color: white !important;
opacity: 0.9;
}
</style>
{% block extra_js %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Modal de Detalhes
const militanteModal = document.getElementById('militanteModal');
militanteModal.addEventListener('show.bs.modal', function(event) {
const button = event.relatedTarget;
const militanteId = button.getAttribute('data-militante-id');
// Preencher os dados do militante
document.getElementById('militanteNome').textContent = button.getAttribute('data-militante-nome');
document.getElementById('militanteCPF').textContent = button.getAttribute('data-militante-cpf');
document.getElementById('militanteEmail').textContent = button.getAttribute('data-militante-email');
document.getElementById('militanteTelefone').textContent = button.getAttribute('data-militante-telefone');
document.getElementById('militanteEndereco').textContent = button.getAttribute('data-militante-endereco');
document.getElementById('militanteFiliado').textContent = button.getAttribute('data-militante-filiado') === 'True' ? 'Filiado' : 'Não Filiado';
// Configurar dados para o modal de edição
const btnEditar = this.querySelector('.btn-primary');
btnEditar.addEventListener('click', function() {
const modalEditar = document.getElementById('modalEditarMilitante');
// Preencher o formulário de edição
document.getElementById('editNome').value = button.getAttribute('data-militante-nome');
document.getElementById('editCpf').value = button.getAttribute('data-militante-cpf');
document.getElementById('editEmail').value = button.getAttribute('data-militante-email');
document.getElementById('editTelefone').value = button.getAttribute('data-militante-telefone');
document.getElementById('editEndereco').value = button.getAttribute('data-militante-endereco');
document.getElementById('editFiliado').checked = button.getAttribute('data-militante-filiado') === 'True';
// Configurar action do formulário
document.getElementById('formEditarMilitante').action = `/militantes/editar/${militanteId}`;
});
// Configurar dados para o modal de exclusão
const btnExcluir = this.querySelector('.btn-danger');
btnExcluir.addEventListener('click', function() {
const deleteModal = document.getElementById('deleteModal');
const btnConfirmarExclusao = deleteModal.querySelector('#btnConfirmarExclusao');
// Atualizar texto do modal
deleteModal.querySelector('.modal-body p').textContent = `Tem certeza que deseja excluir o militante ${button.getAttribute('data-militante-nome')}?`;
// Configurar ação de exclusão
btnConfirmarExclusao.onclick = function() {
fetch(`/militantes/excluir/${militanteId}`, {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
// Fechar os modais
bootstrap.Modal.getInstance(deleteModal).hide();
bootstrap.Modal.getInstance(militanteModal).hide();
// Atualizar a página
location.reload();
} else {
alert(data.message || 'Erro ao excluir militante');
}
})
.catch(error => {
console.error('Erro:', error);
alert('Erro ao excluir militante');
});
};
});
});
// Limpar event listeners quando o modal for fechado
militanteModal.addEventListener('hidden.bs.modal', function () {
const btnEditar = this.querySelector('.btn-primary');
const btnExcluir = this.querySelector('.btn-danger');
btnEditar.replaceWith(btnEditar.cloneNode(true));
btnExcluir.replaceWith(btnExcluir.cloneNode(true));
});
// Envio do formulário de edição via AJAX
const formEditarMilitante = document.getElementById('formEditarMilitante');
formEditarMilitante.addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
fetch(this.action, {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
// Fechar os modais
bootstrap.Modal.getInstance(document.getElementById('modalEditarMilitante')).hide();
bootstrap.Modal.getInstance(document.getElementById('militanteModal')).hide();
// Atualizar a página
location.reload();
} else {
// Mostrar erro
const alertDiv = document.createElement('div');
alertDiv.className = 'alert alert-danger alert-dismissible fade show';
alertDiv.innerHTML = `
${data.message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.querySelector('.modal-body').insertBefore(alertDiv, formEditarMilitante);
}
})
.catch(error => {
console.error('Erro:', error);
const alertDiv = document.createElement('div');
alertDiv.className = 'alert alert-danger alert-dismissible fade show';
alertDiv.innerHTML = `
Erro ao atualizar militante. Tente novamente.
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.querySelector('.modal-body').insertBefore(alertDiv, formEditarMilitante);
});
});
// Função para carregar dados do pagamento no modal
function carregarDadosPagamento(id) {
fetch(`/api/pagamentos/${id}`)
.then(response => response.json())
.then(data => {
document.getElementById('editPagamentoId').value = data.id;
document.getElementById('editValor').value = data.valor;
document.getElementById('editDataPagamento').value = data.data_pagamento;
document.getElementById('editTipoPagamento').value = data.tipo_pagamento_id;
document.getElementById('editObservacao').value = data.observacao || '';
// Abre o modal
new bootstrap.Modal(document.getElementById('modalEditarPagamento')).show();
})
.catch(error => {
console.error('Erro ao carregar dados:', error);
alert('Erro ao carregar dados do pagamento');
});
}
// Função para salvar alterações do pagamento
document.getElementById('formEditarPagamento').addEventListener('submit', function(e) {
e.preventDefault();
const id = document.getElementById('editPagamentoId').value;
const formData = new FormData(this);
fetch(`/api/pagamentos/${id}`, {
method: 'PUT',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Fecha o modal
bootstrap.Modal.getInstance(document.getElementById('modalEditarPagamento')).hide();
// Recarrega a página
location.reload();
} else {
alert('Erro ao salvar alterações: ' + data.message);
}
})
.catch(error => {
console.error('Erro ao salvar:', error);
alert('Erro ao salvar alterações');
});
});
// Configuração do modal de exclusão
let itemParaExcluir = null;
let tipoItem = null;
function confirmarExclusao(id, tipo) {
itemParaExcluir = id;
tipoItem = tipo;
new bootstrap.Modal(document.getElementById('deleteModal')).show();
}
document.getElementById('btnConfirmarExclusao').addEventListener('click', function() {
if (!itemParaExcluir || !tipoItem) return;
fetch(`/api/${tipoItem}/${itemParaExcluir}`, {
method: 'DELETE'
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
} else {
alert('Erro ao excluir: ' + data.message);
}
})
.catch(error => {
console.error('Erro ao excluir:', error);
alert('Erro ao excluir item');
});
});
});
</script>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/militantes.js') }}"></script>
<script src="{{ url_for('static', filename='js/home.js') }}"></script>
{% endblock %}
{% endblock %}

View File

@@ -1,35 +1,284 @@
{% extends 'base.html' %}
{% block title %}Início{% endblock %}
{% block title %}Assinaturas{% endblock %}
{% block content %}
<h1>Assinaturas Anuais</h1>
<a href="{{ url_for('nova_assinatura') }}">Adicionar Nova Assinatura</a>
<table border="1">
<div class="container">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="fas fa-newspaper me-2"></i>Assinaturas</h2>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalNovaAssinatura">
<i class="fas fa-plus me-2"></i>Nova Assinatura
</button>
</div>
<div class="card">
<div class="card-body p-0">
{% if assinaturas %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>ID</th>
<th>Militante ID</th>
<th>Tipo Material</th>
<th>Quantidade</th>
<th>Valor Total</th>
<th>Militante</th>
<th>Data Início</th>
<th>Data Fim</th>
<th>Status</th>
<th>Valor</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{% for assinatura in assinaturas %}
<tr>
<td>{{ assinatura.id }}</td>
<td>{{ assinatura.militante_id }}</td>
<td>{{ assinatura.tipo_material_id }}</td>
<td>{{ assinatura.quantidade }}</td>
<td>R$ {{ assinatura.valor_total }}</td>
<td>{{ assinatura.data_inicio }}</td>
<td>{{ assinatura.data_fim }}</td>
<td>{{ assinatura.militante.nome }}</td>
<td>{{ assinatura.data_inicio.strftime('%d/%m/%Y') }}</td>
<td>{{ assinatura.data_fim.strftime('%d/%m/%Y') }}</td>
<td>
{% if assinatura.ativa %}
<span class="badge bg-success">Ativa</span>
{% else %}
<span class="badge bg-danger">Inativa</span>
{% endif %}
</td>
<td>R$ {{ "%.2f"|format(assinatura.valor) }}</td>
<td>
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-primary"
onclick="editarAssinatura({{ assinatura.id }})">
<i class="fas fa-edit"></i>
</button>
<button type="button" class="btn btn-sm btn-outline-danger"
onclick="confirmarExclusao({{ assinatura.id }})">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<a href="{{ url_for('home') }}">Home</a>
</div>
{% else %}
<div class="text-center py-4">
<p class="text-muted mb-0">Nenhuma assinatura encontrada.</p>
</div>
{% endif %}
</div>
</div>
</div>
<!-- Modal Nova Assinatura -->
<div class="modal fade" id="modalNovaAssinatura" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Nova Assinatura</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="formNovaAssinatura">
<div class="mb-3">
<label for="militante" class="form-label">Militante</label>
<select class="form-select" id="militante" name="militante_id" required>
<option value="">Selecione um militante</option>
{% for militante in militantes %}
<option value="{{ militante.id }}">{{ militante.nome }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="dataInicio" class="form-label">Data de Início</label>
<input type="date" class="form-control" id="dataInicio" name="data_inicio" required>
</div>
<div class="mb-3">
<label for="dataFim" class="form-label">Data de Fim</label>
<input type="date" class="form-control" id="dataFim" name="data_fim" required>
</div>
<div class="mb-3">
<label for="valor" class="form-label">Valor</label>
<div class="input-group">
<span class="input-group-text">R$</span>
<input type="number" step="0.01" class="form-control" id="valor" name="valor" required>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" form="formNovaAssinatura" class="btn btn-primary">Salvar</button>
</div>
</div>
</div>
</div>
<!-- Modal de Confirmação de Exclusão -->
<div class="modal fade" id="modalConfirmarExclusao" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Confirmar Exclusão</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>Tem certeza que deseja excluir esta assinatura?</p>
<p class="text-danger mb-0"><small>Esta ação não pode ser desfeita.</small></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="button" class="btn btn-danger" onclick="excluirAssinatura()">Excluir</button>
</div>
</div>
</div>
</div>
<script>
let assinaturaIdParaExcluir = null;
function editarAssinatura(id) {
// Implementar edição
}
function confirmarExclusao(id) {
assinaturaIdParaExcluir = id;
new bootstrap.Modal(document.getElementById('modalConfirmarExclusao')).show();
}
function excluirAssinatura() {
if (!assinaturaIdParaExcluir) return;
fetch(`/assinaturas/excluir/${assinaturaIdParaExcluir}`, {
method: 'POST'
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
} else {
alert('Erro ao excluir assinatura: ' + data.message);
}
})
.catch(error => {
console.error('Erro:', error);
alert('Erro ao excluir assinatura');
});
}
document.getElementById('formNovaAssinatura').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
fetch('/assinaturas/novo', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
} else {
alert('Erro ao criar assinatura: ' + data.message);
}
})
.catch(error => {
console.error('Erro:', error);
alert('Erro ao criar assinatura');
});
});
</script>
{% endblock %}
{% block extra_css %}
<style>
/* Estilo para colunas ordenáveis */
th[data-sort] {
cursor: pointer;
user-select: none;
}
th[data-sort] i {
margin-left: 5px;
color: #ccc;
}
th[data-sort].sort-asc i,
th[data-sort].sort-desc i {
color: var(--primary-color);
}
/* Animação para linhas da tabela */
#assinaturasTable tbody tr {
transition: all 0.3s ease;
}
#assinaturasTable tbody tr:hover {
background-color: rgba(0,0,0,0.02);
transform: translateX(5px);
}
/* Estilo para botões de ação */
.btn-group .btn {
padding: 0.25rem 0.5rem;
}
.btn-group .btn i {
width: 16px;
text-align: center;
}
/* Responsividade */
@media (max-width: 768px) {
.btn-group {
display: flex;
margin-top: 1rem;
}
.btn-group .btn {
flex: 1;
}
}
/* Estilo para o backdrop com blur em todos os modais */
.modal-backdrop.show {
backdrop-filter: blur(8px);
background-color: rgba(0, 0, 0, 0.7);
}
/* Estilo para o botão de fechar dos modais */
.btn-close {
background-color: transparent;
padding: 0.5rem;
opacity: 0.8;
transition: opacity 0.2s;
filter: invert(1) grayscale(100%) brightness(200%);
}
.btn-close:hover {
opacity: 1;
background-color: transparent;
}
/* Estilo para modais */
.modal-content {
border: none;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
}
.modal-header {
background: linear-gradient(to right, var(--bs-gray-dark), var(--bs-gray));
color: white;
border-radius: 12px 12px 0 0;
border-bottom: none;
padding: 1rem;
}
.modal-body {
padding: 1.5rem;
}
.modal-footer {
border-top: 1px solid #eee;
padding: 1rem;
}
</style>
{% endblock %}

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

@@ -1,31 +1,323 @@
{% extends 'base.html' %}
{% extends "base.html" %}
{% block title %}Listar Militantes{% endblock %}
{% block title %}Cotas{% endblock %}
{% block content %}
<h1>Cotas Mensais</h1>
<a href="{{ url_for('nova_cota') }}">Adicionar Nova Cota</a>
<table border="1">
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<h1 class="mb-0">
<i class="fas fa-money-bill me-2"></i>Cotas
</h1>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalNovaCota">
<i class="fas fa-plus me-2"></i>Nova Cota
</button>
</div>
</div>
</div>
<div class="card shadow-sm">
<div class="card-body">
<div class="row mb-4">
<div class="col-md-6">
<div class="input-group">
<span class="input-group-text">
<i class="fas fa-search"></i>
</span>
<input type="text" class="form-control" id="searchInput" placeholder="Pesquisar cotas...">
</div>
</div>
<div class="col-md-6 text-end">
<button id="btnExportar" class="btn btn-outline-primary">
<i class="fas fa-download me-2"></i>Exportar
</button>
</div>
</div>
<div class="table-responsive">
<table class="table table-hover" id="cotasTable">
<thead>
<tr>
<th>ID</th>
<th>Militante ID</th>
<th>Valor Antigo</th>
<th>Valor Novo</th>
<th>Data de Alteração</th>
<th data-sort="militante">Militante <i class="fas fa-sort"></i></th>
<th data-sort="valor_antigo">Valor Antigo <i class="fas fa-sort"></i></th>
<th data-sort="valor_novo">Valor Novo <i class="fas fa-sort"></i></th>
<th data-sort="data_alteracao">Data de Alteração <i class="fas fa-sort"></i></th>
<th data-sort="data_vencimento">Data de Vencimento <i class="fas fa-sort"></i></th>
<th data-sort="status">Status <i class="fas fa-sort"></i></th>
<th class="text-end">Ações</th>
</tr>
</thead>
<tbody>
{% for cota in cotas %}
<tr>
<td>{{ cota.id }}</td>
<td>{{ cota.militante_id }}</td>
<td>R$ {{ cota.valor_antigo }}</td>
<td>R$ {{ cota.valor_novo }}</td>
<td>{{ cota.data_alteracao }}</td>
<td data-militante="{{ cota.militante.nome }}">{{ cota.militante.nome }}</td>
<td data-valor_antigo="{{ cota.valor_antigo }}">R$ {{ "%.2f"|format(cota.valor_antigo) }}</td>
<td data-valor_novo="{{ cota.valor_novo }}">R$ {{ "%.2f"|format(cota.valor_novo) }}</td>
<td data-data_alteracao="{{ cota.data_alteracao }}">{{ cota.data_alteracao.strftime('%d/%m/%Y') }}</td>
<td data-data_vencimento="{{ cota.data_vencimento }}">{{ cota.data_vencimento.strftime('%d/%m/%Y') }}</td>
<td data-status="{{ cota.status }}">
{% if cota.status == 'paga' %}
<span class="badge bg-success">Paga</span>
{% elif cota.status == 'atrasada' %}
<span class="badge bg-danger">Atrasada</span>
{% else %}
<span class="badge bg-warning text-dark">Pendente</span>
{% endif %}
</td>
<td class="text-end">
<div class="btn-group">
<button type="button"
class="btn btn-sm btn-outline-primary"
data-bs-toggle="modal"
data-bs-target="#modalEditarCota"
data-cota-id="{{ cota.id }}"
data-cota-militante="{{ cota.militante_id }}"
data-cota-valor-antigo="{{ cota.valor_antigo }}"
data-cota-valor-novo="{{ cota.valor_novo }}"
data-cota-data-alteracao="{{ cota.data_alteracao.strftime('%Y-%m-%d') }}"
data-cota-data-vencimento="{{ cota.data_vencimento.strftime('%Y-%m-%d') }}"
data-cota-pago="{{ 'true' if cota.pago else 'false' }}"
title="Editar">
<i class="fas fa-edit"></i>
</button>
<button type="button"
class="btn btn-sm btn-outline-danger"
data-bs-toggle="modal"
data-bs-target="#deleteModal"
data-cota-id="{{ cota.id }}"
data-cota-info="{{ cota.militante.nome }} - R$ {{ "%.2f"|format(cota.valor_novo) }}"
title="Excluir">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<a href="{{ url_for('home') }}">Home</a>
</div>
</div>
</div>
<!-- Modal Nova Cota -->
<div class="modal fade" id="modalNovaCota" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-plus me-2"></i>Nova Cota
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="formNovaCota" method="post" action="{{ url_for('nova_cota') }}">
<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>
<div class="mb-3">
<label for="valor_antigo" class="form-label">Valor Antigo:</label>
<input type="number" step="0.01" class="form-control" id="valor_antigo" name="valor_antigo" required>
</div>
<div class="mb-3">
<label for="valor_novo" class="form-label">Valor Novo:</label>
<input type="number" step="0.01" class="form-control" id="valor_novo" name="valor_novo" required>
</div>
<div class="mb-3">
<label for="data_alteracao" class="form-label">Data de Alteração:</label>
<input type="date" class="form-control" id="data_alteracao" name="data_alteracao" required>
</div>
<div class="mb-3">
<label for="data_vencimento" class="form-label">Data de Vencimento:</label>
<input type="date" class="form-control" id="data_vencimento" name="data_vencimento" required>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" form="formNovaCota" class="btn btn-success">
<i class="fas fa-save me-2"></i>Salvar
</button>
</div>
</div>
</div>
</div>
<!-- Modal de Edição -->
<div class="modal fade" id="modalEditarCota" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-edit me-2"></i>Editar Cota
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="formEditarCota" method="post">
<div class="mb-3">
<label for="editMilitanteNome" class="form-label">Militante:</label>
<input type="text" class="form-control bg-light" id="editMilitanteNome" readonly>
<input type="hidden" id="editMilitante" name="militante_id">
</div>
<div class="mb-3">
<label for="editValorAntigo" class="form-label">Valor Antigo:</label>
<input type="number" step="0.01" class="form-control" id="editValorAntigo" name="valor_antigo" required>
</div>
<div class="mb-3">
<label for="editValorNovo" class="form-label">Valor Novo:</label>
<input type="number" step="0.01" class="form-control" id="editValorNovo" name="valor_novo" required>
</div>
<div class="mb-3">
<label for="editDataAlteracao" class="form-label">Data de Alteração:</label>
<input type="date" class="form-control" id="editDataAlteracao" name="data_alteracao" required>
</div>
<div class="mb-3">
<label for="editDataVencimento" class="form-label">Data de Vencimento:</label>
<input type="date" class="form-control" id="editDataVencimento" name="data_vencimento" required>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="editPago" name="pago">
<label class="form-check-label" for="editPago">Pago</label>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" form="formEditarCota" class="btn btn-success">
<i class="fas fa-save me-2"></i>Salvar
</button>
</div>
</div>
</div>
</div>
<!-- Modal de Exclusão -->
<div class="modal fade" id="deleteModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Confirmar Exclusão</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>Tem certeza que deseja excluir a cota de <strong id="cotaInfo"></strong>?</p>
<p class="text-danger mb-0">Esta ação não pode ser desfeita.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<form action="" method="POST" id="deleteForm" class="d-inline">
<button type="submit" class="btn btn-danger">
<i class="fas fa-trash me-2"></i>Excluir
</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/cotas.js') }}"></script>
{% endblock %}
{% block extra_css %}
<style>
/* Estilo para colunas ordenáveis */
th[data-sort] {
cursor: pointer;
user-select: none;
}
th[data-sort] i {
margin-left: 5px;
color: #ccc;
}
th[data-sort].sort-asc i,
th[data-sort].sort-desc i {
color: var(--primary-color);
}
/* Animação para linhas da tabela */
#cotasTable tbody tr {
transition: all 0.3s ease;
}
#cotasTable tbody tr:hover {
background-color: rgba(0,0,0,0.02);
transform: translateX(5px);
}
/* Estilo para botões de ação */
.btn-group .btn {
padding: 0.25rem 0.5rem;
}
.btn-group .btn i {
width: 16px;
text-align: center;
}
/* Responsividade */
@media (max-width: 768px) {
.btn-group {
display: flex;
margin-top: 1rem;
}
.btn-group .btn {
flex: 1;
}
}
/* Estilo para o backdrop com blur em todos os modais */
.modal-backdrop.show {
backdrop-filter: blur(8px);
background-color: rgba(0, 0, 0, 0.7);
}
/* Estilo para o botão de fechar dos modais */
.btn-close {
background-color: transparent;
padding: 0.5rem;
opacity: 0.8;
transition: opacity 0.2s;
filter: invert(1) grayscale(100%) brightness(200%);
}
.btn-close:hover {
opacity: 1;
background-color: transparent;
}
/* Estilo para modais */
.modal-content {
border: none;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
}
.modal-header {
background: linear-gradient(to right, var(--bs-gray-dark), var(--bs-gray));
color: white;
border-radius: 12px 12px 0 0;
border-bottom: none;
padding: 1rem;
}
.modal-body {
padding: 1.5rem;
}
.modal-footer {
border-top: 1px solid #eee;
padding: 1rem;
}
</style>
{% 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,330 @@
{% extends 'base.html' %}
{% extends "base.html" %}
{% block title %}Listar Militantes{% endblock %}
{% block title %}Materiais{% endblock %}
{% block content %}
<h1>Materiais Vendidos</h1>
<a href="{{ url_for('novo_material') }}">Adicionar Novo Material</a>
<table border="1">
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<h1 class="mb-0">
<i class="fas fa-box me-2"></i>Materiais
</h1>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalNovoMaterial">
<i class="fas fa-plus me-2"></i>Novo Material
</button>
</div>
</div>
</div>
<div class="card shadow-sm">
<div class="card-body">
<div class="row mb-4">
<div class="col-md-6">
<div class="input-group">
<span class="input-group-text">
<i class="fas fa-search"></i>
</span>
<input type="text" class="form-control" id="searchInput" placeholder="Pesquisar materiais...">
</div>
</div>
<div class="col-md-6 text-end">
<button id="btnExportar" class="btn btn-outline-primary">
<i class="fas fa-download me-2"></i>Exportar
</button>
</div>
</div>
<div class="table-responsive">
<table class="table table-hover" id="materiaisTable">
<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>
<th data-sort="militante">Militante <i class="fas fa-sort"></i></th>
<th data-sort="tipo">Tipo <i class="fas fa-sort"></i></th>
<th data-sort="descricao">Descrição <i class="fas fa-sort"></i></th>
<th data-sort="valor">Valor <i class="fas fa-sort"></i></th>
<th data-sort="data">Data <i class="fas fa-sort"></i></th>
<th class="text-end">Ações</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>
<td data-militante="{{ material.militante.nome }}">{{ material.militante.nome }}</td>
<td data-tipo="{{ material.tipo_material.nome }}">{{ material.tipo_material.nome }}</td>
<td data-descricao="{{ material.descricao }}">{{ material.descricao }}</td>
<td data-valor="{{ material.valor }}">R$ {{ "%.2f"|format(material.valor) }}</td>
<td data-data="{{ material.data_venda }}">{{ material.data_venda.strftime('%d/%m/%Y') }}</td>
<td class="text-end">
<div class="btn-group">
<button type="button"
class="btn btn-sm btn-outline-primary"
data-bs-toggle="modal"
data-bs-target="#modalEditarMaterial"
data-material-id="{{ material.id }}"
data-material-militante="{{ material.militante_id }}"
data-material-tipo="{{ material.tipo_material_id }}"
data-material-descricao="{{ material.descricao }}"
data-material-valor="{{ material.valor }}"
data-material-data="{{ material.data_venda.strftime('%Y-%m-%d') }}"
title="Editar">
<i class="fas fa-edit"></i>
</button>
<button type="button"
class="btn btn-sm btn-outline-danger"
data-bs-toggle="modal"
data-bs-target="#deleteModal"
data-material-id="{{ material.id }}"
data-material-info="{{ material.militante.nome }} - {{ material.tipo_material.nome }}"
title="Excluir">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<a href="{{ url_for('home') }}">Home</a>
</div>
</div>
</div>
<!-- Modal Novo Material -->
<div class="modal fade" id="modalNovoMaterial" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-plus me-2"></i>Novo Material
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="formNovoMaterial" method="post" action="{{ url_for('novo_material') }}">
<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>
<div class="mb-3">
<label for="tipo_material_id" class="form-label">Tipo de Material:</label>
<select class="form-select" id="tipo_material_id" name="tipo_material_id" required>
<option value="">Selecione um tipo</option>
{% for tipo in tipos_material %}
<option value="{{ tipo.id }}">{{ tipo.nome }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="descricao" class="form-label">Descrição:</label>
<input type="text" class="form-control" id="descricao" name="descricao" required>
</div>
<div class="mb-3">
<label for="valor" class="form-label">Valor:</label>
<input type="number" step="0.01" class="form-control" id="valor" name="valor" 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>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" form="formNovoMaterial" class="btn btn-success">
<i class="fas fa-save me-2"></i>Salvar
</button>
</div>
</div>
</div>
</div>
<!-- Modal de Edição -->
<div class="modal fade" id="modalEditarMaterial" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-edit me-2"></i>Editar Material
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="formEditarMaterial" method="post">
<div class="mb-3">
<label for="editMilitante" class="form-label">Militante:</label>
<select class="form-select" id="editMilitante" name="militante_id" required>
<option value="">Selecione um militante</option>
{% for militante in militantes %}
<option value="{{ militante.id }}">{{ militante.nome }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="editTipo" class="form-label">Tipo de Material:</label>
<select class="form-select" id="editTipo" name="tipo_material_id" required>
<option value="">Selecione um tipo</option>
{% for tipo in tipos_material %}
<option value="{{ tipo.id }}">{{ tipo.nome }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="editDescricao" class="form-label">Descrição:</label>
<input type="text" class="form-control" id="editDescricao" name="descricao" required>
</div>
<div class="mb-3">
<label for="editValor" class="form-label">Valor:</label>
<input type="number" step="0.01" class="form-control" id="editValor" name="valor" required>
</div>
<div class="mb-3">
<label for="editData" class="form-label">Data da Venda:</label>
<input type="date" class="form-control" id="editData" name="data_venda" required>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" form="formEditarMaterial" class="btn btn-success">
<i class="fas fa-save me-2"></i>Salvar
</button>
</div>
</div>
</div>
</div>
<!-- Modal de Confirmação de Exclusão -->
<div class="modal fade" id="deleteModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-exclamation-triangle me-2 text-danger"></i>Confirmar Exclusão
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>Tem certeza que deseja excluir o material <strong id="materialInfo"></strong>?</p>
<p class="text-danger mb-0">Esta ação não pode ser desfeita.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<form id="formDeleteMaterial" method="post" style="display: inline;">
<button type="submit" class="btn btn-danger">
<i class="fas fa-trash me-2"></i>Excluir
</button>
</form>
</div>
</div>
</div>
</div>
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Configuração da tabela
const table = document.getElementById('materiaisTable');
const searchInput = document.getElementById('searchInput');
const exportBtn = document.getElementById('btnExportar');
// Função de pesquisa
searchInput.addEventListener('input', function() {
const searchTerm = this.value.toLowerCase();
const rows = table.getElementsByTagName('tbody')[0].getElementsByTagName('tr');
Array.from(rows).forEach(row => {
const text = row.textContent.toLowerCase();
row.style.display = text.includes(searchTerm) ? '' : 'none';
});
});
// Função de ordenação
const headers = table.getElementsByTagName('th');
Array.from(headers).forEach(header => {
if (header.dataset.sort) {
header.addEventListener('click', () => {
const column = header.dataset.sort;
const tbody = table.getElementsByTagName('tbody')[0];
const rows = Array.from(tbody.getElementsByTagName('tr'));
rows.sort((a, b) => {
const aValue = a.querySelector(`td[data-${column}]`).dataset[column];
const bValue = b.querySelector(`td[data-${column}]`).dataset[column];
if (column === 'valor') {
return parseFloat(aValue) - parseFloat(bValue);
} else if (column === 'data') {
return new Date(aValue) - new Date(bValue);
}
return aValue.localeCompare(bValue);
});
if (header.classList.contains('asc')) {
rows.reverse();
header.classList.remove('asc');
header.classList.add('desc');
} else {
header.classList.remove('desc');
header.classList.add('asc');
}
tbody.innerHTML = '';
rows.forEach(row => tbody.appendChild(row));
});
}
});
// Configuração do modal de edição
const editModal = document.getElementById('modalEditarMaterial');
editModal.addEventListener('show.bs.modal', function(event) {
const button = event.relatedTarget;
const materialId = button.dataset.materialId;
const form = this.querySelector('form');
form.action = `/editar_material/${materialId}`;
document.getElementById('editMilitante').value = button.dataset.materialMilitante;
document.getElementById('editTipo').value = button.dataset.materialTipo;
document.getElementById('editDescricao').value = button.dataset.materialDescricao;
document.getElementById('editValor').value = button.dataset.materialValor;
document.getElementById('editData').value = button.dataset.materialData;
});
// Configuração do modal de exclusão
const deleteModal = document.getElementById('deleteModal');
deleteModal.addEventListener('show.bs.modal', function(event) {
const button = event.relatedTarget;
const materialId = button.dataset.materialId;
const materialInfo = button.dataset.materialInfo;
document.getElementById('materialInfo').textContent = materialInfo;
document.getElementById('formDeleteMaterial').action = `/deletar_material/${materialId}`;
});
// Configuração do botão de exportação
exportBtn.addEventListener('click', function() {
const rows = Array.from(table.getElementsByTagName('tbody')[0].getElementsByTagName('tr'));
const csv = [
['Militante', 'Tipo', 'Descrição', 'Valor', 'Data'],
...rows.map(row => [
row.cells[0].textContent,
row.cells[1].textContent,
row.cells[2].textContent,
row.cells[3].textContent,
row.cells[4].textContent
])
].map(row => row.join(',')).join('\n');
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = 'materiais.csv';
link.click();
});
});
</script>
{% endblock %}
{% endblock %}

View File

@@ -1,57 +1,298 @@
{% extends 'base.html' %}
{% extends "base.html" %}
{% block title %}Listar Militantes{% endblock %}
{% block title %}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="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<h1 class="h3 mb-0">
<i class="fas fa-users me-2"></i>Militantes
</h1>
{% if current_user.has_permission('gerenciar_militantes') %}
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalNovoMilitante">
<i class="fas fa-user-plus me-2"></i>Novo Militante
</button>
{% endif %}
</div>
</div>
</div>
<table class="table table-striped table-hover">
<div class="card">
<div class="card-body">
<div class="row mb-4">
<div class="col-md-6">
<div class="input-group">
<span class="input-group-text">
<i class="fas fa-search"></i>
</span>
<input type="text" class="form-control" id="searchInput" placeholder="Pesquisar militantes...">
</div>
</div>
<div class="col-md-6 text-end">
<div class="btn-group me-2">
<button type="button" class="btn btn-outline-secondary btn-fixed-width dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fas fa-filter me-2"></i>Filtrar
</button>
<ul class="dropdown-menu">
<li><h6 class="dropdown-header">Status</h6></li>
<li><a class="dropdown-item" href="#" data-filter="todos">Todos</a></li>
<li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">Responsabilidades</h6></li>
<li><a class="dropdown-item" href="#" data-filter="financas">Finanças</a></li>
<li><a class="dropdown-item" href="#" data-filter="imprensa">Imprensa</a></li>
<li><a class="dropdown-item" href="#" data-filter="quadro-orientador">Quadro-Orientador</a></li>
<li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">Célula</h6></li>
{% for celula in celulas %}
<li><a class="dropdown-item" href="#" data-filter="celula" data-celula="{{ celula.nome }}">{{ celula.nome }}</a></li>
{% endfor %}
</ul>
</div>
<button class="btn btn-outline-primary btn-fixed-width" type="button" id="btnExportar">
<i class="fas fa-file-export me-2"></i>Exportar
</button>
</div>
</div>
<div class="table-responsive">
<table class="table table-hover" id="militantesTable">
<thead>
<tr>
<th>Nome</th>
<th>CPF</th>
<th>Email</th>
<th>Telefone</th>
<th>Endereço</th>
<th>Filiado</th>
<th data-sort="nome">Nome <i class="fas fa-sort"></i></th>
<th data-sort="cpf">CPF <i class="fas fa-sort"></i></th>
<th data-sort="email">Email <i class="fas fa-sort"></i></th>
<th data-sort="telefone">Telefone <i class="fas fa-sort"></i></th>
<th data-sort="celula">Célula <i class="fas fa-sort"></i></th>
<th>Responsabilidades</th>
<th class="text-end">Ações</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 data-militante="{{ militante.id }}" data-filiado="{{ 'sim' if militante.filiado else 'nao' }}">
<td data-nome="{{ militante.nome }}">{{ militante.nome }}</td>
<td data-cpf="{{ militante.cpf }}">{{ militante.cpf }}</td>
<td data-email="{{ militante.email }}">{{ militante.email }}</td>
<td data-telefone="{{ militante.telefone }}">{{ militante.telefone }}</td>
<td data-celula="{{ militante.celula.nome }}">{{ militante.celula.nome }}</td>
<td>
{% if militante.responsabilidades|bitwise_and(Militante.RESPONSAVEL_FINANCAS) %}
<span class="badge bg-primary">Finanças</span>
{% endif %}
{% if militante.responsabilidades|bitwise_and(Militante.RESPONSAVEL_IMPRENSA) %}
<span class="badge bg-info">Imprensa</span>
{% endif %}
{% if militante.responsabilidades|bitwise_and(Militante.QUADRO_ORIENTADOR) %}
<span class="badge bg-success">Quadro-Orientador</span>
{% endif %}
</td>
<td class="text-end">
<div class="btn-group">
{% if current_user.has_permission('gerenciar_militantes') %}
<button type="button"
class="btn btn-sm btn-outline-primary"
data-bs-toggle="modal"
data-bs-target="#modalEditarMilitante"
data-militante-id="{{ militante.id }}"
data-militante-nome="{{ militante.nome }}"
data-militante-cpf="{{ militante.cpf }}"
data-militante-email="{{ militante.email }}"
data-militante-telefone="{{ militante.telefone }}"
data-militante-endereco="{{ militante.endereco }}"
data-militante-filiado="{{ militante.filiado }}"
title="Editar">
<i class="fas fa-edit"></i>
</button>
<button type="button"
class="btn btn-sm btn-outline-danger"
data-bs-toggle="modal"
data-bs-target="#deleteModal"
data-militante-id="{{ militante.id }}"
data-militante-nome="{{ militante.nome }}"
title="Excluir">
<i class="fas fa-trash"></i>
</button>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="pagination-container d-flex justify-content-between align-items-center">
<div class="text-muted">
Mostrando <span id="countMilitantes">{{ militantes|length }}</span> militantes
</div>
<div class="d-flex align-items-center gap-3">
<div class="d-flex align-items-center">
<span class="me-2">Mostrar</span>
<select class="form-select form-select-sm me-2" id="rowsPerPage" style="width: auto;">
<option value="10">10</option>
<option value="20" selected>20</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
<span>linhas</span>
</div>
<nav aria-label="Navegação de páginas">
<ul class="pagination mb-0">
<li class="page-item disabled" id="prevPage">
<a class="page-link" href="#"><i class="fas fa-chevron-left"></i></a>
</li>
<li class="page-item active"><a class="page-link" href="#">1</a></li>
<li class="page-item"><a class="page-link" href="#">2</a></li>
<li class="page-item"><a class="page-link" href="#">3</a></li>
<li class="page-item" id="nextPage">
<a class="page-link" href="#"><i class="fas fa-chevron-right"></i></a>
</li>
</ul>
</nav>
</div>
</div>
</div>
</div>
<!-- Modais -->
{% include 'modals/militante_novo.html' %}
{% include 'modals/militante_editar.html' %}
{% include 'modals/militante_excluir.html' %}
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/militantes.js') }}"></script>
{% endblock %}
{% block extra_css %}
<style>
.clickable-row {
cursor: pointer;
/* Estilo para o backdrop com blur em todos os modais */
.modal-backdrop.show {
backdrop-filter: blur(8px);
background-color: rgba(0, 0, 0, 0.7);
}
.clickable-row:hover {
background-color: #f5f5f5;
/* Estilo para botões com largura fixa */
.btn-fixed-width {
min-width: 120px;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.375rem 0.75rem;
text-align: center;
height: 38px; /* Altura padrão do Bootstrap para btn */
line-height: 1.5;
vertical-align: middle;
}
.btn-fixed-width i {
margin-right: 8px;
font-size: 0.875rem; /* 14px - tamanho padrão de ícone */
}
.dropdown-toggle::after {
margin-left: 8px;
}
/* Estilo para o botão de fechar dos modais */
.btn-close {
background-color: transparent;
padding: 0.5rem;
opacity: 0.8;
transition: opacity 0.2s;
filter: invert(1) grayscale(100%) brightness(200%);
}
.btn-close:hover {
opacity: 1;
background-color: transparent;
}
/* Estilo para colunas ordenáveis */
th[data-sort] {
cursor: pointer;
user-select: none;
}
th[data-sort] i {
margin-left: 5px;
color: #ccc;
}
th[data-sort].sort-asc i,
th[data-sort].sort-desc i {
color: var(--primary-color);
}
/* Animação para linhas da tabela */
#militantesTable tbody tr {
transition: all 0.3s ease;
}
#militantesTable tbody tr:hover {
background-color: rgba(0,0,0,0.02);
transform: translateX(5px);
}
/* Estilo para badges */
.badge {
font-weight: 500;
padding: 0.5em 0.8em;
}
/* Estilo para botões de ação */
.btn-group .btn {
padding: 0.25rem 0.5rem;
}
.btn-group .btn i {
width: 16px;
text-align: center;
}
/* Estilo para modais */
.modal-content {
border: none;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
}
.modal-header {
background: linear-gradient(to right, var(--bs-gray-dark), var(--bs-gray));
color: white;
border-radius: 12px 12px 0 0;
border-bottom: none;
padding: 1rem;
}
.modal-body {
padding: 1.5rem;
}
.modal-footer {
border-top: 1px solid #eee;
padding: 1rem;
}
/* Responsividade */
@media (max-width: 768px) {
.btn-group {
display: flex;
margin-top: 1rem;
}
.btn-group .btn {
flex: 1;
}
.modal-footer {
justify-content: center;
}
.modal-footer .btn {
min-width: 120px;
}
}
</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;
});
});
});
</script>
{% endblock %}

View File

@@ -1,32 +1,203 @@
{% extends 'base.html' %}
{% extends "base.html" %}
{% block title %}Listar Militantes{% endblock %}
{% block title %}Pagamentos{% endblock %}
{% block content %}
<h1>Pagamentos</h1>
<a href="{{ url_for('novo_pagamento') }}">Adicionar Novo Pagamento</a>
<table border="1">
<div class="container-fluid mt-3">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2><i class="fas fa-money-bill-wave"></i> Pagamentos</h2>
<div>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalNovoPagamento">
<i class="fas fa-plus"></i> Novo Pagamento
</button>
<button type="button" class="btn btn-outline-primary" id="btnExportar">
<i class="fas fa-file-export"></i> Exportar
</button>
</div>
</div>
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover" id="tabelaPagamentos">
<thead>
<tr>
<th>ID</th>
<th>Militante ID</th>
<th>Militante</th>
<th>Tipo de Pagamento</th>
<th>Valor</th>
<th>Data do Pagamento</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{% for pagamento in pagamentos %}
<tr>
<td>{{ pagamento.id }}</td>
<td>{{ pagamento.militante_id }}</td>
<td>{{ pagamento.tipo_pagamento_id }}</td>
<td>R$ {{ pagamento.valor }}</td>
<td>{{ pagamento.data_pagamento }}</td>
<td data-militante="{{ pagamento.militante_id }}">{{ pagamento.militante.nome if pagamento.militante else 'N/A' }}</td>
<td data-tipo="{{ pagamento.tipo_pagamento }}">
{% if pagamento.tipo_pagamento == 1 %}
Mensalidade
{% elif pagamento.tipo_pagamento == 2 %}
Contribuição Extra
{% elif pagamento.tipo_pagamento == 3 %}
Doação
{% elif pagamento.tipo_pagamento == 4 %}
Taxa de Evento
{% elif pagamento.tipo_pagamento == 5 %}
Outros
{% else %}
Não Definido
{% endif %}
</td>
<td data-valor="{{ pagamento.valor }}">R$ {{ "%.2f"|format(pagamento.valor) }}</td>
<td data-data="{{ pagamento.data_pagamento }}">{{ pagamento.data_pagamento.strftime('%d/%m/%Y') }}</td>
<td>
<button type="button"
class="btn btn-sm btn-outline-primary"
data-bs-toggle="modal"
data-bs-target="#modalEditarPagamento"
data-pagamento-id="{{ pagamento.id }}"
data-militante-id="{{ pagamento.militante_id }}"
data-tipo-pagamento="{{ pagamento.tipo_pagamento }}"
data-valor="{{ pagamento.valor }}"
data-data-pagamento="{{ pagamento.data_pagamento.strftime('%Y-%m-%d') }}"
title="Editar">
<i class="fas fa-edit"></i>
</button>
<button type="button"
class="btn btn-sm btn-outline-danger"
data-bs-toggle="modal"
data-bs-target="#modalExcluirPagamento"
data-pagamento-id="{{ pagamento.id }}"
data-pagamento-info="Pagamento de {{ pagamento.militante.nome if pagamento.militante else 'N/A' }} - R$ {{ "%.2f"|format(pagamento.valor) }}"
title="Excluir">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<a href="{{ url_for('home') }}">Home</a>
</div>
</div>
</div>
</div>
<!-- Modal Novo Pagamento -->
<div class="modal fade" id="modalNovoPagamento" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="fas fa-plus"></i> Novo Pagamento</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="formNovoPagamento" method="post" action="{{ url_for('adicionar_pagamento') }}">
<div class="mb-3">
<label for="militante" class="form-label">Militante:</label>
<select class="form-select" id="militante" name="militante_id" required>
<option value="">Selecione um militante</option>
{% for militante in militantes %}
<option value="{{ militante.id }}">{{ militante.nome }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="tipoPagamento" class="form-label">Tipo de Pagamento:</label>
<select class="form-select" id="tipoPagamento" name="tipo_pagamento" required>
<option value="">Selecione o tipo</option>
<option value="1">Mensalidade</option>
<option value="2">Contribuição Extra</option>
<option value="3">Doação</option>
<option value="4">Taxa de Evento</option>
<option value="5">Outros</option>
</select>
</div>
<div class="mb-3">
<label for="valor" class="form-label">Valor:</label>
<input type="number" step="0.01" class="form-control" id="valor" name="valor" required>
</div>
<div class="mb-3">
<label for="dataPagamento" class="form-label">Data do Pagamento:</label>
<input type="date" class="form-control" id="dataPagamento" name="data_pagamento" required>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" class="btn btn-primary">Salvar</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Modal Editar Pagamento -->
<div class="modal fade" id="modalEditarPagamento" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="fas fa-edit"></i> Editar Pagamento</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="formEditarPagamento" method="post">
<div class="mb-3">
<label for="editMilitante" class="form-label">Militante:</label>
<input type="text" class="form-control bg-light" id="editMilitanteNome" readonly>
<input type="hidden" id="editMilitante" name="militante_id">
</div>
<div class="mb-3">
<label for="editTipoPagamento" class="form-label">Tipo de Pagamento:</label>
<select class="form-select" id="editTipoPagamento" name="tipo_pagamento" required>
<option value="">Selecione o tipo</option>
<option value="1">Mensalidade</option>
<option value="2">Contribuição Extra</option>
<option value="3">Doação</option>
<option value="4">Taxa de Evento</option>
<option value="5">Outros</option>
</select>
</div>
<div class="mb-3">
<label for="editValor" class="form-label">Valor:</label>
<input type="number" step="0.01" class="form-control" id="editValor" name="valor" required>
</div>
<div class="mb-3">
<label for="editDataPagamento" class="form-label">Data do Pagamento:</label>
<input type="date" class="form-control" id="editDataPagamento" name="data_pagamento" required>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" class="btn btn-primary">Salvar</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Modal Excluir Pagamento -->
<div class="modal fade" id="modalExcluirPagamento" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="fas fa-trash"></i> Excluir Pagamento</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>Tem certeza que deseja excluir este pagamento?</p>
<p id="pagamentoInfo" class="text-muted"></p>
</div>
<div class="modal-footer">
<form id="formExcluirPagamento" method="post">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" class="btn btn-danger">Excluir</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/pagamentos.js') }}"></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">
<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 ID</th>
<th>Comitê 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_id }}</td>
<td>{{ relatorio.comite_id }}</td>
<td>R$ {{ relatorio.total_cotas }}</td>
<td>{{ relatorio.data_relatorio }}</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>
<a href="{{ url_for('home') }}">Home</a>
</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">
<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 ID</th>
<th>Comitê 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_id }}</td>
<td>{{ relatorio.comite_id }}</td>
<td>R$ {{ relatorio.total_vendas }}</td>
<td>{{ relatorio.data_relatorio }}</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>
<a href="{{ url_for('home') }}">Home</a>
</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

@@ -1,32 +1,515 @@
{% extends 'base.html' %}
{% extends "base.html" %}
{% block title %}Listar Militantes{% endblock %}
{% block title %}Vendas de Jornais{% endblock %}
{% block content %}
<h1>Vendas de Jornais Avulsos</h1>
<a href="{{ url_for('nova_venda_jornal') }}">Adicionar Nova Venda</a>
<table border="1">
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<h1 class="mb-0">
<i class="fas fa-newspaper me-2"></i>Vendas de Jornais
</h1>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalNovaVenda">
<i class="fas fa-plus me-2"></i>Nova Venda
</button>
</div>
</div>
</div>
<div class="card shadow-sm">
<div class="card-body">
<div class="row mb-4">
<div class="col-md-6">
<div class="input-group">
<span class="input-group-text">
<i class="fas fa-search"></i>
</span>
<input type="text" class="form-control" id="searchInput" placeholder="Pesquisar vendas...">
</div>
</div>
<div class="col-md-6 text-end">
<button id="btnExportar" class="btn btn-outline-primary">
<i class="fas fa-download me-2"></i>Exportar
</button>
</div>
</div>
<div class="table-responsive">
<table class="table table-hover" id="vendasTable">
<thead>
<tr>
<th>ID</th>
<th>Militante ID</th>
<th>Quantidade</th>
<th>Valor Total</th>
<th>Data da Venda</th>
<th data-sort="militante">Militante <i class="fas fa-sort"></i></th>
<th data-sort="quantidade">Quantidade <i class="fas fa-sort"></i></th>
<th data-sort="valor_total">Valor Total <i class="fas fa-sort"></i></th>
<th data-sort="data">Data <i class="fas fa-sort"></i></th>
<th class="text-end">Ações</th>
</tr>
</thead>
<tbody>
{% for venda in vendas %}
<tr>
<td>{{ venda.id }}</td>
<td>{{ venda.militante_id }}</td>
<td>{{ venda.quantidade }}</td>
<td>R$ {{ venda.valor_total }}</td>
<td>{{ venda.data_venda }}</td>
<td data-militante="{{ venda.militante.nome }}">{{ venda.militante.nome }}</td>
<td data-quantidade="{{ venda.quantidade }}">{{ venda.quantidade }}</td>
<td data-valor_total="{{ venda.valor_total }}">R$ {{ "%.2f"|format(venda.valor_total) }}</td>
<td data-data="{{ venda.data_venda }}">{{ venda.data_venda.strftime('%d/%m/%Y') }}</td>
<td class="text-end">
<div class="btn-group">
<button type="button"
class="btn btn-sm btn-outline-primary"
data-bs-toggle="modal"
data-bs-target="#modalEditarVenda"
data-venda-id="{{ venda.id }}"
data-venda-militante="{{ venda.militante_id }}"
data-venda-quantidade="{{ venda.quantidade }}"
data-venda-valor-total="{{ venda.valor_total }}"
data-venda-data="{{ venda.data_venda.strftime('%Y-%m-%d') }}"
title="Editar">
<i class="fas fa-edit"></i>
</button>
<button type="button"
class="btn btn-sm btn-outline-danger"
data-bs-toggle="modal"
data-bs-target="#deleteModal"
data-venda-id="{{ venda.id }}"
data-venda-info="{{ venda.militante.nome }} - {{ venda.quantidade }} jornais"
title="Excluir">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<a href="{{ url_for('home') }}">Home</a>
</div>
</div>
</div>
<!-- Modal Nova Venda -->
<div class="modal fade" id="modalNovaVenda" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-plus me-2"></i>Nova Venda de Jornal
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="formNovaVenda" method="post" action="{{ url_for('nova_venda_jornal') }}">
<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>
<div class="mb-3">
<label for="quantidade" class="form-label">Quantidade:</label>
<input type="number" class="form-control" id="quantidade" name="quantidade" required>
</div>
<div class="mb-3">
<label for="valor_total" class="form-label">Valor Total:</label>
<input type="number" step="0.01" class="form-control" id="valor_total" name="valor_total" 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>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" form="formNovaVenda" class="btn btn-success">
<i class="fas fa-save me-2"></i>Salvar
</button>
</div>
</div>
</div>
</div>
<!-- Modal de Edição -->
<div class="modal fade" id="modalEditarVenda" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-edit me-2"></i>Editar Venda de Jornal
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="formEditarVenda" method="post">
<div class="mb-3">
<label for="editMilitante" class="form-label">Militante:</label>
<select class="form-select" id="editMilitante" name="militante_id" required>
<option value="">Selecione um militante</option>
{% for militante in militantes %}
<option value="{{ militante.id }}">{{ militante.nome }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="editQuantidade" class="form-label">Quantidade:</label>
<input type="number" class="form-control" id="editQuantidade" name="quantidade" required>
</div>
<div class="mb-3">
<label for="editValorTotal" class="form-label">Valor Total:</label>
<input type="number" step="0.01" class="form-control" id="editValorTotal" name="valor_total" required>
</div>
<div class="mb-3">
<label for="editData" class="form-label">Data da Venda:</label>
<input type="date" class="form-control" id="editData" name="data_venda" required>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" form="formEditarVenda" class="btn btn-success">
<i class="fas fa-save me-2"></i>Salvar
</button>
</div>
</div>
</div>
</div>
<!-- Modal de Exclusão -->
<div class="modal fade" id="deleteModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Confirmar Exclusão</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>Tem certeza que deseja excluir a venda de <strong id="vendaInfo"></strong>?</p>
<p class="text-danger mb-0">Esta ação não pode ser desfeita.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<form action="" method="POST" id="deleteForm" class="d-inline">
<button type="submit" class="btn btn-danger">
<i class="fas fa-trash me-2"></i>Excluir
</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Configuração do modal de exclusão
const deleteModal = document.getElementById('deleteModal');
deleteModal.addEventListener('show.bs.modal', function(event) {
const button = event.relatedTarget;
const vendaId = button.getAttribute('data-venda-id');
const vendaInfo = button.getAttribute('data-venda-info');
document.getElementById('vendaInfo').textContent = vendaInfo;
document.getElementById('deleteForm').action = `/jornais/excluir/${vendaId}`;
});
// Envio do formulário de nova venda via AJAX
const formNovaVenda = document.getElementById('formNovaVenda');
formNovaVenda.addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
fetch(this.action, {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
// Fechar o modal
bootstrap.Modal.getInstance(document.getElementById('modalNovaVenda')).hide();
// Atualizar a lista
location.reload();
// Mostrar mensagem de sucesso
const alertDiv = document.createElement('div');
alertDiv.className = 'alert alert-success alert-dismissible fade show';
alertDiv.innerHTML = `
${data.message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.querySelector('.container').insertBefore(alertDiv, document.querySelector('.container').firstChild);
} else {
// Mostrar erro
const alertDiv = document.createElement('div');
alertDiv.className = 'alert alert-danger alert-dismissible fade show';
alertDiv.innerHTML = `
${data.message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.querySelector('.modal-body').insertBefore(alertDiv, formNovaVenda);
}
})
.catch(error => {
console.error('Erro:', error);
const alertDiv = document.createElement('div');
alertDiv.className = 'alert alert-danger alert-dismissible fade show';
alertDiv.innerHTML = `
Erro ao cadastrar venda. Tente novamente.
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.querySelector('.modal-body').insertBefore(alertDiv, formNovaVenda);
});
});
// Configuração do modal de edição
const modalEditarVenda = document.getElementById('modalEditarVenda');
modalEditarVenda.addEventListener('show.bs.modal', function(event) {
const button = event.relatedTarget;
const vendaId = button.getAttribute('data-venda-id');
// Preencher o formulário com os dados da venda
document.getElementById('editMilitante').value = button.getAttribute('data-venda-militante');
document.getElementById('editQuantidade').value = button.getAttribute('data-venda-quantidade');
document.getElementById('editValorTotal').value = button.getAttribute('data-venda-valor-total');
document.getElementById('editData').value = button.getAttribute('data-venda-data');
// Configurar a action do formulário
document.getElementById('formEditarVenda').action = `/jornais/editar/${vendaId}`;
});
// Envio do formulário de edição via AJAX
const formEditarVenda = document.getElementById('formEditarVenda');
formEditarVenda.addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
fetch(this.action, {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
// Fechar o modal
bootstrap.Modal.getInstance(modalEditarVenda).hide();
// Atualizar a lista
location.reload();
// Mostrar mensagem de sucesso
const alertDiv = document.createElement('div');
alertDiv.className = 'alert alert-success alert-dismissible fade show';
alertDiv.innerHTML = `
${data.message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.querySelector('.container').insertBefore(alertDiv, document.querySelector('.container').firstChild);
} else {
// Mostrar erro
const alertDiv = document.createElement('div');
alertDiv.className = 'alert alert-danger alert-dismissible fade show';
alertDiv.innerHTML = `
${data.message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.querySelector('.modal-body').insertBefore(alertDiv, formEditarVenda);
}
})
.catch(error => {
console.error('Erro:', error);
const alertDiv = document.createElement('div');
alertDiv.className = 'alert alert-danger alert-dismissible fade show';
alertDiv.innerHTML = `
Erro ao atualizar venda. Tente novamente.
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.querySelector('.modal-body').insertBefore(alertDiv, formEditarVenda);
});
});
// Limpar alertas quando os modais forem fechados
[modalEditarVenda, document.getElementById('modalNovaVenda')].forEach(modal => {
modal.addEventListener('hidden.bs.modal', function () {
const alerts = this.querySelectorAll('.alert');
alerts.forEach(alert => alert.remove());
});
});
// Pesquisa em tempo real
const searchInput = document.getElementById('searchInput');
searchInput.addEventListener('input', function() {
const searchTerm = this.value.toLowerCase();
const rows = document.querySelectorAll('#vendasTable tbody tr');
rows.forEach(row => {
const text = row.textContent.toLowerCase();
row.style.display = text.includes(searchTerm) ? '' : 'none';
});
});
// Ordenação
const headers = document.querySelectorAll('#vendasTable th[data-sort]');
headers.forEach(header => {
header.addEventListener('click', function() {
const column = this.getAttribute('data-sort');
const tbody = document.querySelector('#vendasTable tbody');
const rows = Array.from(tbody.querySelectorAll('tr'));
const isAsc = !this.classList.contains('sort-asc');
// Remover classes de ordenação de todos os headers
headers.forEach(h => {
h.classList.remove('sort-asc', 'sort-desc');
h.querySelector('i').className = 'fas fa-sort';
});
// Adicionar classe de ordenação ao header clicado
this.classList.add(isAsc ? 'sort-asc' : 'sort-desc');
this.querySelector('i').className = `fas fa-sort-${isAsc ? 'up' : 'down'}`;
// Ordenar linhas
rows.sort((a, b) => {
const aVal = a.querySelector(`td[data-${column}]`).getAttribute(`data-${column}`);
const bVal = b.querySelector(`td[data-${column}]`).getAttribute(`data-${column}`);
return isAsc ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal);
});
// Reposicionar linhas
rows.forEach(row => tbody.appendChild(row));
});
});
// Exportar para CSV
document.getElementById('btnExportar').addEventListener('click', function() {
const rows = document.querySelectorAll('#vendasTable tbody tr:not([style*="display: none"])');
const headers = ['Militante', 'Quantidade', 'Valor Total', 'Data'];
let csv = headers.join(',') + '\n';
rows.forEach(row => {
const cols = row.querySelectorAll('td');
const values = [
cols[0].textContent,
cols[1].textContent,
cols[2].textContent,
cols[3].textContent
].map(val => `"${val}"`);
csv += values.join(',') + '\n';
});
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.setAttribute('download', 'vendas_jornal.csv');
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});
});
</script>
{% endblock %}
{% block extra_css %}
<style>
/* Estilo para colunas ordenáveis */
th[data-sort] {
cursor: pointer;
user-select: none;
}
th[data-sort] i {
margin-left: 5px;
color: #ccc;
}
th[data-sort].sort-asc i,
th[data-sort].sort-desc i {
color: var(--primary-color);
}
/* Animação para linhas da tabela */
#vendasTable tbody tr {
transition: all 0.3s ease;
}
#vendasTable tbody tr:hover {
background-color: rgba(0,0,0,0.02);
transform: translateX(5px);
}
/* Estilo para botões de ação */
.btn-group .btn {
padding: 0.25rem 0.5rem;
}
.btn-group .btn i {
width: 16px;
text-align: center;
}
/* Responsividade */
@media (max-width: 768px) {
.btn-group {
display: flex;
margin-top: 1rem;
}
.btn-group .btn {
flex: 1;
}
}
/* Estilo para o backdrop com blur em todos os modais */
.modal-backdrop.show {
backdrop-filter: blur(8px);
background-color: rgba(0, 0, 0, 0.7);
}
/* Estilo para o botão de fechar dos modais */
.btn-close {
background-color: transparent;
padding: 0.5rem;
opacity: 0.8;
transition: opacity 0.2s;
filter: invert(1) grayscale(100%) brightness(200%);
}
.btn-close:hover {
opacity: 1;
background-color: transparent;
}
/* Estilo para modais */
.modal-content {
border: none;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
}
.modal-header {
background: linear-gradient(to right, var(--bs-gray-dark), var(--bs-gray));
color: white;
border-radius: 12px 12px 0 0;
border-bottom: none;
padding: 1rem;
}
.modal-body {
padding: 1.5rem;
}
.modal-footer {
border-top: 1px solid #eee;
padding: 1rem;
}
</style>
{% endblock %}

211
templates/login.html Normal file
View File

@@ -0,0 +1,211 @@
{% extends "base.html" %}
{% block title %}Login{% endblock %}
{% block navbar %}{% endblock %}
{% block content %}
<div class="login-container">
<div class="login-content">
<div class="login-header">
<img src="{{ url_for('static', filename='img/logo001-alpha.png') }}" alt="Logo OCI" class="login-logo">
<h4 class="login-title">Controles OCI</h4>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST" action="{{ url_for('login') }}" class="needs-validation" novalidate>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="form-floating mb-3">
<input type="text" class="form-control" id="email" name="email" placeholder="Email ou Usuário" required>
<label for="email">Email ou Usuário</label>
<div class="invalid-feedback">
Por favor, informe seu email ou nome de usuário.
</div>
</div>
<div class="form-floating mb-3 position-relative">
<input type="password" class="form-control" id="password" name="password" placeholder="Senha" required>
<label for="password">Senha</label>
<div class="invalid-feedback">
Por favor, informe sua senha.
</div>
<button class="btn btn-link text-secondary position-absolute end-0 top-50 translate-middle-y me-2" type="button" id="togglePassword">
<i class="fas fa-eye"></i>
</button>
</div>
<div class="form-floating mb-4">
<input type="text" class="form-control" id="otp" name="otp" placeholder="Código OTP" required>
<label for="otp">Código OTP</label>
<div class="invalid-feedback">
Por favor, informe o código OTP.
</div>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-lg login-button">
<i class="fas fa-sign-in-alt me-2"></i>Entrar
</button>
</div>
</form>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Form validation
const form = document.querySelector('form');
form.addEventListener('submit', function(event) {
if (!form.checkValidity()) {
event.preventDefault();
event.stopPropagation();
}
form.classList.add('was-validated');
});
// Toggle password visibility
const togglePassword = document.getElementById('togglePassword');
const password = document.getElementById('password');
togglePassword.addEventListener('click', function() {
const type = password.getAttribute('type') === 'password' ? 'text' : 'password';
password.setAttribute('type', type);
this.querySelector('i').classList.toggle('fa-eye');
this.querySelector('i').classList.toggle('fa-eye-slash');
});
// Auto-hide alerts after 5 seconds
const alerts = document.querySelectorAll('.alert');
alerts.forEach(alert => {
setTimeout(() => {
const bsAlert = new bootstrap.Alert(alert);
bsAlert.close();
}, 5000);
});
});
</script>
<style>
body {
background: var(--primary-color);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
margin: 0;
}
.login-container {
width: 100%;
max-width: 800px;
margin: auto;
background: #ffffff;
border-radius: 1rem;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
overflow: hidden;
}
.login-content {
padding: 2rem;
display: flex;
flex-direction: column;
align-items: center;
}
.login-header {
text-align: center;
margin-bottom: 2rem;
}
.login-logo {
height: 50px;
width: auto;
margin-bottom: 1rem;
}
.login-title {
color: #343a40;
font-weight: 600;
margin: 0;
}
form {
width: 100%;
max-width: 400px;
}
.form-floating > .form-control {
border: 1px solid #dee2e6;
border-radius: 0.5rem;
}
.form-floating > .form-control:hover {
border-color: rgba(220, 53, 69, 0.3);
}
.form-floating > .form-control:focus {
border-color: rgba(220, 53, 69, 0.5);
box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.15);
}
.form-floating > label {
padding: 1rem 0.75rem;
}
.login-button {
background-color: #0d6efd;
border-color: #0d6efd;
color: white;
border-radius: 0.5rem;
padding: 0.75rem 1.5rem;
font-weight: 500;
transition: all 0.2s ease-in-out;
}
.login-button:hover,
.login-button:focus,
.login-button:active {
background-color: #0b5ed7;
border-color: #0b5ed7;
color: white;
transform: translateY(-1px);
}
@media (min-width: 768px) {
.login-container {
display: flex;
align-items: stretch;
}
.login-content {
padding: 3rem;
}
.login-logo {
height: 60px;
}
}
@media (max-width: 767px) {
.login-container {
margin: 1rem;
}
.login-content {
padding: 1.5rem;
}
}
</style>
{% endblock %}

23
templates/militantes.html Normal file
View File

@@ -0,0 +1,23 @@
<!-- Botões de ação -->
<td class="text-end">
<div class="btn-group">
<button type="button"
class="btn btn-sm btn-outline-primary"
data-bs-toggle="modal"
data-bs-target="#modalEditarMilitante"
data-militante-id="{{ militante.id }}"
data-militante-nome="{{ militante.nome }}"
title="Editar">
<i class="fas fa-edit"></i>
</button>
<button type="button"
class="btn btn-sm btn-outline-danger"
data-bs-toggle="modal"
data-bs-target="#deleteModal"
data-militante-id="{{ militante.id }}"
data-militante-nome="{{ militante.nome }}"
title="Excluir">
<i class="fas fa-trash"></i>
</button>
</div>
</td>

View File

@@ -0,0 +1,267 @@
<!-- Modal de Editar Militante -->
<div class="modal fade" id="modalEditarMilitante" tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-user-edit me-2"></i>Editar Militante
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form id="formEditarMilitante" method="POST">
<input type="hidden" id="edit_militante_id" name="militante_id">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="modal-body">
<!-- Nav tabs -->
<ul class="nav nav-tabs nav-fill mb-3" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#edit-dados-basicos" type="button">
<i class="fas fa-user me-2"></i>Dados Básicos
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#edit-contato" type="button">
<i class="fas fa-address-book me-2"></i>Contato
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#edit-profissional" type="button">
<i class="fas fa-briefcase me-2"></i>Profissional
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#edit-organizacao" type="button">
<i class="fas fa-users me-2"></i>Organização
</button>
</li>
</ul>
<!-- Tab content -->
<div class="tab-content">
<!-- Dados Básicos -->
<div class="tab-pane fade show active" id="edit-dados-basicos">
<div class="row">
<div class="col-md-6 mb-3">
<label for="edit_nome" class="form-label">Nome</label>
<input type="text" class="form-control" id="edit_nome" name="nome" required>
</div>
<div class="col-md-6 mb-3">
<label for="edit_cpf" class="form-label">CPF</label>
<input type="text" class="form-control" id="edit_cpf" name="cpf" required>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="edit_titulo_eleitoral" class="form-label">Título Eleitoral</label>
<input type="text" class="form-control" id="edit_titulo_eleitoral" name="titulo_eleitoral">
</div>
<div class="col-md-6 mb-3">
<label for="edit_data_nascimento" class="form-label">Data de Nascimento</label>
<input type="date" class="form-control" id="edit_data_nascimento" name="data_nascimento">
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="edit_data_entrada" class="form-label">Data de Entrada OCI</label>
<input type="date" class="form-control" id="edit_data_entrada" name="data_entrada_oci">
</div>
<div class="col-md-6 mb-3">
<label for="edit_data_efetivacao" class="form-label">Data de Efetivação</label>
<input type="date" class="form-control" id="edit_data_efetivacao" name="data_efetivacao_oci">
</div>
</div>
</div>
<!-- Contato -->
<div class="tab-pane fade" id="edit-contato">
<div class="row">
<div class="col-md-6 mb-3">
<label for="edit_telefone1" class="form-label">Telefone Principal</label>
<input type="text" class="form-control" id="edit_telefone1" name="telefone1">
</div>
<div class="col-md-6 mb-3">
<label for="edit_telefone2" class="form-label">Telefone Alternativo</label>
<input type="text" class="form-control" id="edit_telefone2" name="telefone2">
</div>
</div>
<!-- Email Principal -->
<div class="mb-3">
<label for="edit_email" class="form-label">Email Principal</label>
<input type="email" class="form-control" id="edit_email" name="email" required>
</div>
<!-- Endereço -->
<div class="endereco-container">
<div class="row">
<div class="col-md-4 mb-3">
<label for="edit_cep" class="form-label">CEP</label>
<input type="text" class="form-control" id="edit_cep" name="cep">
</div>
<div class="col-md-4 mb-3">
<label for="edit_estado" class="form-label">Estado</label>
<select class="form-select" id="edit_estado" name="estado">
<option value="">Selecione...</option>
<!-- Estados serão carregados via JavaScript -->
</select>
</div>
<div class="col-md-4 mb-3">
<label for="edit_cidade" class="form-label">Cidade</label>
<input type="text" class="form-control" id="edit_cidade" name="cidade">
</div>
</div>
<div class="row">
<div class="col-md-4 mb-3">
<label for="edit_bairro" class="form-label">Bairro</label>
<input type="text" class="form-control" id="edit_bairro" name="bairro">
</div>
<div class="col-md-6 mb-3">
<label for="edit_rua" class="form-label">Rua</label>
<input type="text" class="form-control" id="edit_rua" name="rua">
</div>
<div class="col-md-2 mb-3">
<label for="edit_numero" class="form-label">Número</label>
<input type="text" class="form-control" id="edit_numero" name="numero">
</div>
</div>
<div class="mb-3">
<label for="edit_complemento" class="form-label">Complemento</label>
<input type="text" class="form-control" id="edit_complemento" name="complemento">
</div>
</div>
</div>
<!-- Profissional -->
<div class="tab-pane fade" id="edit-profissional">
<div class="row">
<div class="col-md-6 mb-3">
<label for="edit_profissao" class="form-label">Profissão</label>
<input type="text" class="form-control" id="edit_profissao" name="profissao">
</div>
<div class="col-md-6 mb-3">
<label for="edit_regime_trabalho" class="form-label">Regime de Trabalho</label>
<select class="form-select" id="edit_regime_trabalho" name="regime_trabalho">
<option value="">Selecione...</option>
<option value="CLT">CLT</option>
<option value="Estatutário">Estatutário</option>
<option value="Terceirizado">Terceirizado</option>
<option value="Autônomo">Autônomo</option>
</select>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="edit_empresa" class="form-label">Empresa</label>
<input type="text" class="form-control" id="edit_empresa" name="empresa">
</div>
<div class="col-md-6 mb-3">
<label for="edit_contratante" class="form-label">Contratante</label>
<input type="text" class="form-control" id="edit_contratante" name="contratante">
<small class="text-muted">Para terceirizados</small>
</div>
</div>
<hr>
<!-- Dados Acadêmicos -->
<div class="row">
<div class="col-md-8 mb-3">
<label for="edit_instituicao_ensino" class="form-label">Instituição de Ensino</label>
<input type="text" class="form-control" id="edit_instituicao_ensino" name="instituicao_ensino">
</div>
<div class="col-md-4 mb-3">
<label for="edit_tipo_instituicao" class="form-label">Tipo</label>
<select class="form-select" id="edit_tipo_instituicao" name="tipo_instituicao">
<option value="">Selecione...</option>
<option value="Federal">Federal</option>
<option value="Estadual">Estadual</option>
<option value="Municipal">Municipal</option>
<option value="Privada">Privada</option>
</select>
</div>
</div>
</div>
<!-- Organização -->
<div class="tab-pane fade" id="edit-organizacao">
<!-- Dados Sindicais -->
<div class="row">
<div class="col-md-6 mb-3">
<label for="edit_sindicato" class="form-label">Sindicato</label>
<input type="text" class="form-control" id="edit_sindicato" name="sindicato">
</div>
<div class="col-md-6 mb-3">
<label for="edit_cargo_sindical" class="form-label">Cargo Sindical</label>
<input type="text" class="form-control" id="edit_cargo_sindical" name="cargo_sindical">
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="edit_central_sindical" class="form-label">Central Sindical</label>
<input type="text" class="form-control" id="edit_central_sindical" name="central_sindical">
</div>
<div class="col-md-6 mb-3 d-flex align-items-center">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="edit_dirigente_sindical" name="dirigente_sindical">
<label class="form-check-label" for="edit_dirigente_sindical">Dirigente Sindical</label>
</div>
</div>
</div>
<hr>
<!-- Estado na Organização -->
<div class="row">
<div class="col-md-6 mb-3">
<label for="edit_estado_militante" class="form-label">Estado</label>
<select class="form-select" id="edit_estado_militante" name="estado">
<option value="ATIVO">Ativo</option>
<option value="LICENCIADO">Licenciado</option>
<option value="SUSPENSO">Suspenso</option>
<option value="DESLIGADO">Desligado</option>
</select>
</div>
<div class="col-md-6 mb-3">
<label for="edit_celula" class="form-label">Célula</label>
<select class="form-select" id="edit_celula" name="celula_id">
<option value="">Selecione...</option>
{% for celula in celulas %}
<option value="{{ celula.id }}">{{ celula.nome }}</option>
{% endfor %}
</select>
</div>
</div>
<!-- Responsabilidades -->
<div class="mb-3">
<label class="form-label d-block">Responsabilidades</label>
<div class="row g-3">
<div class="col-md-4">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="edit_resp_1" name="responsabilidades" value="256">
<label class="form-check-label" for="edit_resp_1">Finanças</label>
</div>
</div>
<div class="col-md-4">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="edit_resp_2" name="responsabilidades" value="512">
<label class="form-check-label" for="edit_resp_2">Imprensa</label>
</div>
</div>
<div class="col-md-4">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="edit_resp_4" name="responsabilidades" value="64">
<label class="form-check-label" for="edit_resp_4">Quadro-Orientador</label>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-2"></i>Salvar
</button>
</div>
</form>
</div>
</div>
</div>

View File

@@ -0,0 +1,23 @@
<!-- Modal de Confirmação de Exclusão -->
<div class="modal fade" id="deleteModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Confirmar Exclusão</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>Tem certeza que deseja excluir o militante <strong id="militanteNome"></strong>?</p>
<p class="text-danger mb-0">Esta ação não pode ser desfeita.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<form action="" method="POST" id="deleteForm" class="d-inline">
<button type="submit" class="btn btn-danger">
<i class="fas fa-trash me-2"></i>Excluir
</button>
</form>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,256 @@
<!-- Modal de Novo Militante -->
<div class="modal fade" id="modalNovoMilitante" tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-user-plus me-2"></i>Novo Militante
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="formNovoMilitante" method="post" action="{{ url_for('criar_militante') }}">
<!-- Nav tabs -->
<ul class="nav nav-tabs nav-fill mb-3" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#tab-dados-basicos" type="button">
<i class="fas fa-user me-2"></i>Dados Básicos
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tab-contato" type="button">
<i class="fas fa-address-book me-2"></i>Contato
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tab-profissional" type="button">
<i class="fas fa-briefcase me-2"></i>Profissional
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tab-organizacao" type="button">
<i class="fas fa-users me-2"></i>Organização
</button>
</li>
</ul>
<!-- Tab content -->
<div class="tab-content">
<!-- Dados Básicos -->
<div class="tab-pane fade show active" id="tab-dados-basicos">
<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>
<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" required
pattern="\d{3}\.?\d{3}\.?\d{3}-?\d{2}"
title="Digite um CPF no formato: xxx.xxx.xxx-xx">
</div>
</div>
<div class="row">
<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">
</div>
<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">
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="data_entrada" class="form-label">Data de Entrada OCI</label>
<input type="date" class="form-control" id="data_entrada" name="data_entrada_oci">
</div>
<div class="col-md-6 mb-3">
<label for="data_efetivacao" class="form-label">Data de Efetivação</label>
<input type="date" class="form-control" id="data_efetivacao" name="data_efetivacao_oci">
</div>
</div>
</div>
<!-- Contato -->
<div class="tab-pane fade" id="tab-contato">
<div class="row">
<div class="col-md-6 mb-3">
<label for="telefone1" class="form-label">Telefone Principal</label>
<input type="text" class="form-control" id="telefone1" name="telefone1">
</div>
<div class="col-md-6 mb-3">
<label for="telefone2" class="form-label">Telefone Alternativo</label>
<input type="text" class="form-control" id="telefone2" name="telefone2">
</div>
</div>
<!-- Email Principal -->
<div class="mb-3">
<label for="email" class="form-label">Email Principal</label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
<!-- Endereço -->
<div class="endereco-container">
<div class="row">
<div class="col-md-4 mb-3">
<label for="cep" class="form-label">CEP</label>
<input type="text" class="form-control" id="cep" name="cep">
</div>
<div class="col-md-4 mb-3">
<label for="estado" class="form-label">Estado</label>
<select class="form-select" id="estado" name="estado">
<option value="">Selecione...</option>
<!-- Estados serão carregados via JavaScript -->
</select>
</div>
<div class="col-md-4 mb-3">
<label for="cidade" class="form-label">Cidade</label>
<input type="text" class="form-control" id="cidade" name="cidade">
</div>
</div>
<div class="row">
<div class="col-md-4 mb-3">
<label for="bairro" class="form-label">Bairro</label>
<input type="text" class="form-control" id="bairro" name="bairro">
</div>
<div class="col-md-6 mb-3">
<label for="logradouro" class="form-label">Logradouro</label>
<input type="text" class="form-control" id="logradouro" name="logradouro">
</div>
<div class="col-md-2 mb-3">
<label for="numero" class="form-label">Número</label>
<input type="text" class="form-control" id="numero" name="numero">
</div>
</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>
</div>
<!-- Profissional -->
<div class="tab-pane fade" id="tab-profissional">
<div class="row">
<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">
</div>
<div class="col-md-6 mb-3">
<label for="regime_trabalho" class="form-label">Regime de Trabalho</label>
<select class="form-select" id="regime_trabalho" name="regime_trabalho">
<option value="">Selecione...</option>
<option value="CLT">CLT</option>
<option value="Estatutário">Estatutário</option>
<option value="Terceirizado">Terceirizado</option>
<option value="Autônomo">Autônomo</option>
</select>
</div>
</div>
<div class="row">
<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">
</div>
<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">
<small class="text-muted">Para terceirizados</small>
</div>
</div>
<hr>
<!-- Dados Acadêmicos -->
<div class="row">
<div class="col-md-8 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="col-md-4 mb-3">
<label for="tipo_instituicao" class="form-label">Tipo</label>
<select class="form-select" id="tipo_instituicao" name="tipo_instituicao">
<option value="">Selecione...</option>
<option value="Federal">Federal</option>
<option value="Estadual">Estadual</option>
<option value="Municipal">Municipal</option>
<option value="Privada">Privada</option>
</select>
</div>
</div>
</div>
<!-- Organização -->
<div class="tab-pane fade" id="tab-organizacao">
<!-- Dados Sindicais -->
<div class="row">
<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">
</div>
<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">
</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">
</div>
<div class="col-md-6 mb-3 d-flex align-items-center">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="dirigente_sindical" name="dirigente_sindical">
<label class="form-check-label" for="dirigente_sindical">Dirigente Sindical</label>
</div>
</div>
</div>
<hr>
<!-- Estado na Organização -->
<div class="row">
<div class="col-md-6 mb-3">
<label for="estado_militante" class="form-label">Estado</label>
<select class="form-select" id="estado_militante" name="estado">
<option value="ATIVO">Ativo</option>
<option value="DESLIGADO">Desligado</option>
<option value="SUSPENSO">Suspenso</option>
<option value="AFASTADO">Afastado</option>
</select>
</div>
<div class="col-md-6 mb-3">
<label for="celula" class="form-label">Célula</label>
<select class="form-select" id="celula" name="celula_id" required>
<option value="">Selecione...</option>
{% for celula in celulas %}
<option value="{{ celula.id }}">{{ celula.nome }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="mb-3">
<label class="form-label d-block">Responsabilidades</label>
<div class="row g-3">
{% for valor, nome in Militante.get_responsabilidades_list() %}
<div class="col-md-6">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="resp_{{ valor }}"
name="responsabilidades" value="{{ valor }}">
<label class="form-check-label" for="resp_{{ valor }}">{{ nome }}</label>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" form="formNovoMilitante" class="btn btn-primary">
<i class="fas fa-save me-2"></i>Salvar
</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,41 @@
{% extends 'base.html' %}
{% block title %}Configurar Autenticação em Dois Fatores{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-8 offset-md-2 text-center">
<div class="card">
<div class="card-header">
<h3>Configure a Autenticação em Dois Fatores</h3>
</div>
<div class="card-body">
<p class="lead">Siga os passos abaixo para configurar a autenticação em dois fatores:</p>
<ol class="text-start mb-4">
<li>Instale um aplicativo autenticador no seu celular (Google Authenticator, Microsoft Authenticator, etc)</li>
<li>Abra o aplicativo e escaneie o QR Code abaixo</li>
<li>O aplicativo irá gerar um código de 6 dígitos a cada 30 segundos</li>
<li>Use este código ao fazer login no sistema</li>
</ol>
<div class="mb-4">
<img src="https://chart.googleapis.com/chart?cht=qr&chs=300x300&chl={{ qr_uri|urlencode }}"
class="img-fluid" alt="QR Code para OTP">
</div>
<div class="alert alert-warning">
<strong>Importante:</strong> Guarde este QR Code em um lugar seguro.
Você precisará dele caso troque de celular ou reinstale o aplicativo autenticador.
</div>
<div class="mt-4">
<a href="{{ url_for('login') }}" class="btn btn-primary">Ir para Login</a>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

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 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>
<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 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>
<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 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 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>
<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,36 +6,89 @@
<div class="container">
<div class="row">
<div class="col-md-8 offset-md-2">
<h1 class="mb-4">Registrar Novo Pagamento</h1>
<form method="post" class="mb-4">
<div class="card shadow-sm">
<div class="card-header bg-light">
<h4 class="card-title mb-0">
<i class="fas fa-money-bill-wave me-2"></i>Registrar Novo Pagamento
</h4>
</div>
<div class="card-body">
<form method="post" class="needs-validation" novalidate>
<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 um militante</option>
{% for militante in militantes %}
<option value="{{ militante.id }}">{{ militante.nome }}</option>
{% endfor %}
</select>
<div class="invalid-feedback">
Por favor, selecione um militante.
</div>
</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>
<select class="form-select" id="tipo_pagamento_id" name="tipo_pagamento_id" required>
<option value="">Selecione o tipo de pagamento</option>
{% for tipo in tipos_pagamento %}
<option value="{{ tipo.id }}">{{ tipo.descricao }}</option>
{% endfor %}
</select>
<div class="invalid-feedback">
Por favor, selecione o tipo de pagamento.
</div>
</div>
<div class="mb-3">
<label for="valor" class="form-label">Valor:</label>
<input type="number" class="form-control" id="valor" name="valor" step="0.01" required>
<div class="input-group">
<span class="input-group-text">R$</span>
<input type="text" class="form-control money" id="valor" name="valor" required>
</div>
<div class="invalid-feedback">
Por favor, informe um valor válido.
</div>
</div>
<div class="mb-3">
<label for="data_pagamento" class="form-label">Data do Pagamento:</label>
<input type="date" class="form-control" id="data_pagamento" name="data_pagamento" required>
<input type="date" class="form-control" id="data_pagamento" name="data_pagamento"
required max="{{ hoje }}">
<div class="invalid-feedback">
Por favor, informe uma data válida.
</div>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">Registrar</button>
<a href="{{ url_for('listar_pagamentos') }}" class="btn btn-secondary">Voltar</a>
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-1"></i>Registrar
</button>
<a href="{{ url_for('listar_pagamentos') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left me-1"></i>Voltar
</a>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.mask/1.14.16/jquery.mask.min.js"></script>
<script>
$(document).ready(function(){
$('.money').mask('000.000.000.000.000,00', {reverse: true});
// Converter valor para formato aceito pelo backend
$('form').on('submit', function(e) {
e.preventDefault();
const valor = $('#valor').val().replace(/\./g, '').replace(',', '.');
$('#valor').val(valor);
this.submit();
});
});
</script>
{% endblock %}

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 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>
<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 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>
<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>
<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>
<a href="{{ url_for('listar_relatorios_vendas') }}">Voltar para Lista</a>
<a href="{{ url_for('home') }}">Home</a>
</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 %}

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