Compare commits
66 Commits
cota_calc
...
front/ui-i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a302a259a6 | ||
|
|
75ba696355 | ||
|
|
7f4fe77711 | ||
|
|
c29eed0c69 | ||
|
|
52a6bf9eb0 | ||
|
|
d468f8ff39 | ||
|
|
5527db8729 | ||
|
|
56b8e7aa54 | ||
|
|
9ffc562357 | ||
|
|
3ed3002410 | ||
|
|
f58c340235 | ||
|
|
9158a86655 | ||
|
|
6b23adcb34 | ||
|
|
c7c3b95f0b | ||
|
|
9bb62c81a7 | ||
|
|
c17a3eaa0f | ||
|
|
07605797d1 | ||
|
|
745803fef3 | ||
|
|
241543ea63 | ||
|
|
50516664e4 | ||
|
|
0447524a91 | ||
|
|
77cf5ad99c | ||
|
|
9cc3f408f8 | ||
|
|
758dbdb26d | ||
|
|
83ae798033 | ||
|
|
742f820bc2 | ||
|
|
a28f543478 | ||
|
|
417b5c3f96 | ||
|
|
10ff9cab3b | ||
|
|
8803c971e4 | ||
|
|
d4869dcfaa | ||
|
|
06e7c79488 | ||
|
|
0a2d5c1d23 | ||
|
|
855f97c72b | ||
|
|
8e6ccb70e9 | ||
|
|
65406276ae | ||
|
|
b1acc2fdfc | ||
|
|
c44ce94bef | ||
|
|
ce3b5a4231 | ||
|
|
f0faf4270b | ||
|
|
178a58bb00 | ||
|
|
e9c1f3aedf | ||
|
|
1ff8e97bbc | ||
|
|
b815f77240 | ||
|
|
ba4f6d6de3 | ||
|
|
ac461ce800 | ||
|
|
4f781b2a0e | ||
|
|
32cd4b70c1 | ||
|
|
54261e455c | ||
|
|
9d17c66c46 | ||
|
|
cbaf227e58 | ||
|
|
8dac8dc234 | ||
|
|
bf93e84cec | ||
|
|
449a203926 | ||
|
|
01f5901eb2 | ||
|
|
6370e8f39b | ||
|
|
bae6b1ae14 | ||
|
|
1367389619 | ||
|
|
0f4056fbff | ||
|
|
cccca2ef29 | ||
|
|
986f90a9cd | ||
|
|
14c88bb1e4 | ||
|
|
aa22102b5a | ||
|
|
0d2238d8e0 | ||
|
|
de132b82c1 | ||
|
|
a847389295 |
8
.gitignore
vendored
8
.gitignore
vendored
@@ -260,5 +260,13 @@ poetry.toml
|
|||||||
pyrightconfig.json
|
pyrightconfig.json
|
||||||
|
|
||||||
database.db
|
database.db
|
||||||
|
admin_qr.png
|
||||||
|
|
||||||
# End of https://www.toptal.com/developers/gitignore/api/python,flask
|
# End of https://www.toptal.com/developers/gitignore/api/python,flask
|
||||||
|
|
||||||
|
# Documentação temporária
|
||||||
|
docs/alteracoes_db_connection.md
|
||||||
|
|
||||||
|
# QR Codes
|
||||||
|
*_qr.png
|
||||||
|
*_qr.txt
|
||||||
|
|||||||
13
Makefile
13
Makefile
@@ -1,5 +1,18 @@
|
|||||||
install:
|
install:
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf ~/.local/share/controles/database.db
|
||||||
|
rm -f admin_qr.png
|
||||||
|
|
||||||
run:
|
run:
|
||||||
python app.py
|
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
|
||||||
|
|||||||
133
README.md
133
README.md
@@ -1,15 +1,134 @@
|
|||||||
# controles
|
# Sistema de Controle de Militantes
|
||||||
|
|
||||||
## Para instalar
|
Sistema para gerenciamento de militantes, células, setores e comitês regionais.
|
||||||
|
|
||||||
|
## 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
|
```bash
|
||||||
make install
|
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
|
||||||
```
|
```
|
||||||
|
|
||||||
## Para executar
|
## Uso
|
||||||
|
|
||||||
```bash
|
### Decoradores de Permissão
|
||||||
make run
|
|
||||||
|
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
|
||||||
```
|
```
|
||||||
|
|
||||||
Acesse por: http://127.0.0.1:5000
|
## Estrutura do Banco de Dados
|
||||||
|
|
||||||
|
O sistema utiliza as seguintes tabelas para o RBAC:
|
||||||
|
|
||||||
|
- `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.
142
create_admin.py
Normal file
142
create_admin.py
Normal 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
130
create_test_users.py
Normal 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
8
dao.py
@@ -1,8 +0,0 @@
|
|||||||
from functions.database import execute_query
|
|
||||||
|
|
||||||
def get_user_by_email(email):
|
|
||||||
query = "SELECT * FROM users WHERE email = %s"
|
|
||||||
cursor = execute_query(query, (email,))
|
|
||||||
if cursor:
|
|
||||||
return cursor.fetchone()
|
|
||||||
return None
|
|
||||||
99
database.sql
99
database.sql
@@ -1,99 +0,0 @@
|
|||||||
-- Tabela de Militantes
|
|
||||||
CREATE TABLE militantes (
|
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
|
||||||
nome VARCHAR(100) NOT NULL,
|
|
||||||
cpf VARCHAR(14) UNIQUE,
|
|
||||||
email VARCHAR(100) UNIQUE,
|
|
||||||
telefone VARCHAR(15),
|
|
||||||
endereco VARCHAR(255),
|
|
||||||
filiado BOOLEAN DEFAULT false
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Tabela de Cotas Mensais
|
|
||||||
CREATE TABLE cotas_mensais (
|
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
|
||||||
militante_id INT,
|
|
||||||
valor_antigo DECIMAL(10, 2) NOT NULL,
|
|
||||||
valor_novo DECIMAL(10, 2) NOT NULL,
|
|
||||||
data_alteracao DATE NOT NULL,
|
|
||||||
FOREIGN KEY (militante_id) REFERENCES militantes(id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Tabela de Pagamentos
|
|
||||||
CREATE TABLE tipos_pagamento (
|
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
|
||||||
descricao VARCHAR(100) NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE pagamentos (
|
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
|
||||||
militante_id INT,
|
|
||||||
tipo_pagamento_id INT,
|
|
||||||
valor DECIMAL(10, 2) NOT NULL,
|
|
||||||
data_pagamento DATE NOT NULL,
|
|
||||||
FOREIGN KEY (militante_id) REFERENCES militantes(id),
|
|
||||||
FOREIGN KEY (tipo_pagamento_id) REFERENCES tipos_pagamento(id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Tabela de Tipos de Materiais
|
|
||||||
CREATE TABLE tipos_materiais (
|
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
|
||||||
descricao VARCHAR(100) NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Tabela de Materiais Vendidos
|
|
||||||
CREATE TABLE materiais_vendidos (
|
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
|
||||||
militante_id INT,
|
|
||||||
tipo_material_id INT,
|
|
||||||
descricao VARCHAR(255) NOT NULL,
|
|
||||||
valor DECIMAL(10, 2) NOT NULL,
|
|
||||||
data_venda DATE NOT NULL,
|
|
||||||
FOREIGN KEY (militante_id) REFERENCES militantes(id),
|
|
||||||
FOREIGN KEY (tipo_material_id) REFERENCES tipos_materiais(id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Tabela de Vendas de Jornais Avulsos
|
|
||||||
CREATE TABLE vendas_jornais_avulsos (
|
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
|
||||||
militante_id INT,
|
|
||||||
quantidade INT NOT NULL,
|
|
||||||
valor_total DECIMAL(10, 2) NOT NULL,
|
|
||||||
data_venda DATE NOT NULL,
|
|
||||||
FOREIGN KEY (militante_id) REFERENCES militantes(id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Tabela de Assinaturas Anuais
|
|
||||||
CREATE TABLE assinaturas_anuais (
|
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
|
||||||
militante_id INT,
|
|
||||||
tipo_material_id INT,
|
|
||||||
quantidade INT NOT NULL,
|
|
||||||
valor_total DECIMAL(10, 2) NOT NULL,
|
|
||||||
data_inicio DATE NOT NULL,
|
|
||||||
data_fim DATE NOT NULL,
|
|
||||||
FOREIGN KEY (militante_id) REFERENCES militantes(id),
|
|
||||||
FOREIGN KEY (tipo_material_id) REFERENCES tipos_materiais(id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Tabela de Relatório de Cotas Mensais
|
|
||||||
CREATE TABLE relatorio_cotas_mensais (
|
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
|
||||||
setor_id INT,
|
|
||||||
comite_id INT,
|
|
||||||
total_cotas DECIMAL(10, 2) NOT NULL,
|
|
||||||
data_relatorio DATE NOT NULL,
|
|
||||||
FOREIGN KEY (setor_id) REFERENCES setores(id),
|
|
||||||
FOREIGN KEY (comite_id) REFERENCES comites_centrais(id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Tabela de Relatório de Vendas de Materiais
|
|
||||||
CREATE TABLE relatorio_vendas_materiais (
|
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
|
||||||
setor_id INT,
|
|
||||||
comite_id INT,
|
|
||||||
total_vendas DECIMAL(10, 2) NOT NULL,
|
|
||||||
data_relatorio DATE NOT NULL,
|
|
||||||
FOREIGN KEY (setor_id) REFERENCES setores(id),
|
|
||||||
FOREIGN KEY (comite_id) REFERENCES comites_centrais(id)
|
|
||||||
);
|
|
||||||
165
docs/README.md
Normal file
165
docs/README.md
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
# Sistema de Controle OCI
|
||||||
|
|
||||||
|
## Hierarquia e Permissões
|
||||||
|
|
||||||
|
### Níveis de Acesso
|
||||||
|
|
||||||
|
1. **Militante Básico**
|
||||||
|
- Pode ver apenas os membros da sua própria célula
|
||||||
|
- Não pode alterar níveis de outros usuários
|
||||||
|
|
||||||
|
2. **Secretário de Célula**
|
||||||
|
- Pode ver e gerenciar apenas os membros da sua célula
|
||||||
|
- Não pode alterar níveis de outros usuários
|
||||||
|
|
||||||
|
3. **Membro de Setor**
|
||||||
|
- Pode ver apenas os dados do setor ao qual pertence
|
||||||
|
- Não pode alterar níveis de outros usuários
|
||||||
|
|
||||||
|
4. **Secretário de Setor**
|
||||||
|
- Pode ver e gerenciar todos os dados do seu setor
|
||||||
|
- Pode alterar níveis de militantes do setor, transformando-os em secretários
|
||||||
|
- Não pode alterar níveis de membros de outros setores
|
||||||
|
|
||||||
|
5. **Membro de CR**
|
||||||
|
- Pode ver apenas os dados do CR ao qual pertence
|
||||||
|
- Não pode alterar níveis de outros usuários
|
||||||
|
|
||||||
|
6. **Secretário de CR**
|
||||||
|
- Pode ver e gerenciar todos os dados do seu CR
|
||||||
|
- Pode alterar níveis de membros do CR
|
||||||
|
- Não pode alterar níveis de membros de outros CRs
|
||||||
|
|
||||||
|
7. **Membro do CC**
|
||||||
|
- Pode ver todos os dados do sistema
|
||||||
|
- Não pode alterar níveis de outros usuários
|
||||||
|
|
||||||
|
8. **Secretário Geral e Secretário de Organização**
|
||||||
|
- Pode ver todos os dados do sistema
|
||||||
|
- Pode alterar níveis de qualquer usuário em qualquer instância
|
||||||
|
|
||||||
|
### Regras de Visualização
|
||||||
|
|
||||||
|
- Cada militante só pode ver os membros da sua própria célula
|
||||||
|
- Membros de setor só veem dados do setor ao qual pertencem
|
||||||
|
- Membros de CR só veem informações do CR ao qual pertencem
|
||||||
|
- Membros do CC podem ver todas as informações do sistema
|
||||||
|
|
||||||
|
### Regras de Edição
|
||||||
|
|
||||||
|
- Apenas o Secretário Geral e o Secretário de Organização podem alterar níveis em todas as instâncias
|
||||||
|
- Secretários de CR podem alterar níveis apenas dentro do seu CR
|
||||||
|
- Secretários de Setor podem alterar níveis apenas dentro do seu setor, transformando militantes em secretários
|
||||||
|
- Outros níveis não podem alterar níveis de outros usuários
|
||||||
|
|
||||||
|
## Responsabilidades
|
||||||
|
|
||||||
|
O sistema suporta as seguintes responsabilidades para militantes:
|
||||||
|
|
||||||
|
- Militante Básico (1)
|
||||||
|
- Secretário de Célula (2)
|
||||||
|
- Secretário de Setor (4)
|
||||||
|
- Secretário de CR (8)
|
||||||
|
- Secretário de CC (16)
|
||||||
|
- Secretário Geral (32)
|
||||||
|
- Quadro-Orientador (64)
|
||||||
|
- Responsável de Finanças (256)
|
||||||
|
- Responsável de Imprensa (512)
|
||||||
|
|
||||||
|
### Status de Aspirante
|
||||||
|
|
||||||
|
Todo novo militante começa como Aspirante. Este status tem as seguintes características:
|
||||||
|
|
||||||
|
1. **Duração Mínima**: O status de Aspirante deve ser mantido por pelo menos 3 meses após a integração do militante.
|
||||||
|
|
||||||
|
2. **Avaliação Obrigatória**: Para remover o status de Aspirante, é necessário:
|
||||||
|
- Ter passado o período mínimo de 3 meses
|
||||||
|
- Registrar uma avaliação detalhada da atuação do militante durante este período
|
||||||
|
|
||||||
|
3. **Quem pode Avaliar**: A avaliação e remoção do status de Aspirante pode ser feita por:
|
||||||
|
- Secretário Geral
|
||||||
|
- Secretário de Organização
|
||||||
|
- Secretários de CR (para militantes de seu CR)
|
||||||
|
- Secretários de Setor (para militantes de seu setor)
|
||||||
|
|
||||||
|
4. **Registro da Avaliação**: A avaliação deve incluir:
|
||||||
|
- Análise da participação do militante nas atividades
|
||||||
|
- Desenvolvimento político e organizativo
|
||||||
|
- Pontos fortes e aspectos a melhorar
|
||||||
|
- Recomendações para o desenvolvimento futuro
|
||||||
|
|
||||||
|
5. **Histórico**: O sistema mantém registro de:
|
||||||
|
- Data de início do período como Aspirante
|
||||||
|
- Data da avaliação
|
||||||
|
- Texto completo da avaliação
|
||||||
|
|
||||||
|
O Quadro-Orientador é uma responsabilidade especial que pode ser atribuída a militantes em qualquer nível hierárquico, incluindo membros de CR e CC. Esta responsabilidade indica que o militante tem a função de orientar e apoiar outros militantes em sua formação política e organizativa.
|
||||||
|
|
||||||
|
A atribuição da responsabilidade de Quadro-Orientador pode ser feita por:
|
||||||
|
- Secretário Geral
|
||||||
|
- Secretário de Organização
|
||||||
|
- Secretários de CR (para militantes de seu CR)
|
||||||
|
- Secretários de Setor (para militantes de seu setor)
|
||||||
|
|
||||||
|
### Responsáveis de Finanças e Imprensa
|
||||||
|
|
||||||
|
Cada instância (Célula, Setor, CR e CC) possui três responsáveis:
|
||||||
|
|
||||||
|
1. **Responsável Geral**: Obrigatório para todas as instâncias. É o principal responsável pela instância.
|
||||||
|
|
||||||
|
2. **Responsável de Finanças**: Opcional. Responsável por:
|
||||||
|
- Controle financeiro da instância
|
||||||
|
- Arrecadação de contribuições
|
||||||
|
- Prestação de contas
|
||||||
|
- Planejamento financeiro
|
||||||
|
|
||||||
|
3. **Responsável de Imprensa**: Opcional. Responsável por:
|
||||||
|
- Comunicação externa da instância
|
||||||
|
- Produção de materiais de divulgação
|
||||||
|
- Gestão de redes sociais
|
||||||
|
- Relacionamento com a mídia
|
||||||
|
|
||||||
|
Os responsáveis de finanças e imprensa são designados pelo responsável geral da instância, com aprovação da instância superior.
|
||||||
|
|
||||||
|
## Hierarquia de Instâncias
|
||||||
|
|
||||||
|
1. **Comitê Central (CC)**
|
||||||
|
- Instância máxima da organização
|
||||||
|
- Possui responsável geral, de finanças e de imprensa
|
||||||
|
- Coordena todos os CRs
|
||||||
|
|
||||||
|
2. **Comitê Regional (CR)**
|
||||||
|
- Subordinado ao CC
|
||||||
|
- Possui responsável geral, de finanças e de imprensa
|
||||||
|
- Coordena os setores da sua região
|
||||||
|
|
||||||
|
3. **Setor**
|
||||||
|
- Subordinado ao CR
|
||||||
|
- Possui responsável geral, de finanças e de imprensa
|
||||||
|
- Coordena as células do seu setor
|
||||||
|
|
||||||
|
4. **Célula**
|
||||||
|
- Subordinada ao Setor
|
||||||
|
- Possui responsável geral, de finanças e de imprensa
|
||||||
|
- Unidade básica de organização
|
||||||
|
|
||||||
|
## Permissões
|
||||||
|
|
||||||
|
As permissões no sistema são baseadas nas responsabilidades do militante e na hierarquia das instâncias:
|
||||||
|
|
||||||
|
1. **Visualização**
|
||||||
|
- Militantes básicos veem apenas sua célula
|
||||||
|
- Secretários de célula veem sua célula
|
||||||
|
- Secretários de setor veem seu setor e células
|
||||||
|
- Secretários de CR veem seu CR, setores e células
|
||||||
|
- Secretários de CC veem todos os dados
|
||||||
|
|
||||||
|
2. **Edição**
|
||||||
|
- Cada nível pode gerenciar apenas os níveis abaixo
|
||||||
|
- Responsáveis de finanças e imprensa podem editar apenas suas áreas
|
||||||
|
- Quadros-Orientadores podem avaliar militantes
|
||||||
|
|
||||||
|
3. **Responsabilidades**
|
||||||
|
- Apenas o nível superior pode atribuir responsabilidades
|
||||||
|
- Responsáveis de finanças e imprensa são designados pelo responsável geral
|
||||||
|
- O status de Quadro-Orientador segue regras específicas
|
||||||
54
docs/alteracoes_db_connection.md
Normal file
54
docs/alteracoes_db_connection.md
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# Alterações no Gerenciamento de Conexões com o Banco de Dados
|
||||||
|
|
||||||
|
## Commit
|
||||||
|
- ID: [ID do commit será adicionado após o commit]
|
||||||
|
- Data: [Data do commit]
|
||||||
|
- Autor: [Nome do autor]
|
||||||
|
|
||||||
|
## Contexto
|
||||||
|
O sistema estava utilizando uma única sessão global do SQLAlchemy (`db_session`) que era criada no início da aplicação. Isso poderia causar problemas de concorrência e vazamento de recursos.
|
||||||
|
|
||||||
|
## Alterações Realizadas
|
||||||
|
|
||||||
|
### 1. Remoção da Sessão Global
|
||||||
|
- Removida a linha `db_session = get_db_connection()` do início do arquivo
|
||||||
|
- Todas as rotas agora criam sua própria sessão
|
||||||
|
|
||||||
|
### 2. Novo Padrão de Gerenciamento de Sessão
|
||||||
|
Em cada rota, implementamos o seguinte padrão:
|
||||||
|
```python
|
||||||
|
db = get_db_connection()
|
||||||
|
try:
|
||||||
|
# Operações com o banco
|
||||||
|
db.commit()
|
||||||
|
except Exception as e:
|
||||||
|
db.rollback()
|
||||||
|
# Tratamento de erro
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Melhorias no Tratamento de Erros
|
||||||
|
- Adicionado `db.rollback()` em caso de exceção
|
||||||
|
- Melhoradas as mensagens de erro
|
||||||
|
- Garantido que a sessão seja fechada mesmo em caso de erro
|
||||||
|
|
||||||
|
### 4. Padronização de Código
|
||||||
|
- Uso de `request.form.get()` ao invés de acessar diretamente o dicionário
|
||||||
|
- Conversão explícita de tipos (float, int, date)
|
||||||
|
- Validação de dados antes de criar objetos
|
||||||
|
- Mensagens de feedback mais claras para o usuário
|
||||||
|
|
||||||
|
## Impacto no Frontend
|
||||||
|
Não houve alterações necessárias nos templates, pois as mudanças foram apenas na forma como o backend gerencia as conexões com o banco de dados.
|
||||||
|
|
||||||
|
## Benefícios
|
||||||
|
1. Maior segurança (evita vazamentos de recursos)
|
||||||
|
2. Maior robustez (melhor tratamento de erros)
|
||||||
|
3. Código mais fácil de manter (padronização)
|
||||||
|
4. Maior eficiência (sessões são fechadas adequadamente)
|
||||||
|
|
||||||
|
## Observações
|
||||||
|
- Esta alteração foi feita para melhorar a arquitetura do sistema
|
||||||
|
- Não afeta a funcionalidade existente
|
||||||
|
- Recomenda-se seguir este padrão em novas implementações
|
||||||
239
docs/rbac.md
Normal file
239
docs/rbac.md
Normal 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
|
||||||
Binary file not shown.
33
functions/base.py
Normal file
33
functions/base.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
from pathlib import Path
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Configurar caminho do banco de dados
|
||||||
|
db_dir = Path.home() / '.local' / 'share' / 'controles'
|
||||||
|
db_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
db_path = db_dir / 'database.db'
|
||||||
|
|
||||||
|
# Configurar SQLite com opções para melhor concorrência
|
||||||
|
engine = create_engine(
|
||||||
|
f'sqlite:///{db_path}',
|
||||||
|
connect_args={
|
||||||
|
'timeout': 30, # Tempo de espera em segundos
|
||||||
|
'check_same_thread': False # Permite acesso de múltiplas threads
|
||||||
|
},
|
||||||
|
pool_pre_ping=True, # Verifica conexão antes de usar
|
||||||
|
pool_recycle=3600 # Recicla conexões após 1 hora
|
||||||
|
)
|
||||||
|
|
||||||
|
Session = sessionmaker(bind=engine)
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
def get_db_connection():
|
||||||
|
"""Retorna uma nova sessão do banco de dados"""
|
||||||
|
session = Session()
|
||||||
|
try:
|
||||||
|
return session
|
||||||
|
except Exception as e:
|
||||||
|
session.rollback()
|
||||||
|
raise e
|
||||||
@@ -1,692 +0,0 @@
|
|||||||
// TODO: extract all CONTANTS TO EASILY CHANGE CELLS
|
|
||||||
|
|
||||||
const planilhaID= "13sLipAAD5LkzZK19iuzgscbCmODiS11hJDRgaNsnYvw";
|
|
||||||
|
|
||||||
// LOCAIS DE LIMPEZA \/\/\/\/
|
|
||||||
const cotas = 'B5:E40' ;
|
|
||||||
const contribuintes = 'B43:E57' ;
|
|
||||||
const brochuras = 'B60:D65';
|
|
||||||
const campanha = 'B68:D84' ;
|
|
||||||
const outras = 'B87:D94';
|
|
||||||
const assinantes = 'B97:D109';
|
|
||||||
const jornal = 'B112:D126';
|
|
||||||
const despesaCE = 'D129';
|
|
||||||
const depositos = 'B134:F251' ;
|
|
||||||
const carimbo = 'Q287' ;
|
|
||||||
// ACABOU :LOCAIS DE LIMPEZA /\/\/\/\
|
|
||||||
|
|
||||||
const contagemRF='E2';
|
|
||||||
const celulaPrincipal = 'A1' ;
|
|
||||||
|
|
||||||
const enddepositos = 'D252';
|
|
||||||
const endvendas = 'D130' ;
|
|
||||||
|
|
||||||
const celulaValorTotalCotas = 'E41';
|
|
||||||
|
|
||||||
const timeZone = Session.getScriptTimeZone();
|
|
||||||
|
|
||||||
const CRSP = "crsptesouraria@gmail.com";
|
|
||||||
const areaAProteger = 'A1:Y999' ;
|
|
||||||
|
|
||||||
function getUser(){ return Session.getEffectiveUser();}
|
|
||||||
|
|
||||||
function voltaAoTopo(){
|
|
||||||
SpreadsheetApp.getActiveSheet().setCurrentCell(SpreadsheetApp.getActiveSheet().getRange(celulaPrincipal)) ;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function onOpen() {
|
|
||||||
var ui = SpreadsheetApp.getUi();
|
|
||||||
ui.createMenu('CR')
|
|
||||||
.addItem('Enviar RF', 'menuItem1')
|
|
||||||
.addItem('Totalizar Cotas', 'menuItem2')
|
|
||||||
.addItem('Teste - Não usar', 'menuItem3')
|
|
||||||
.addToUi();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// MENU ITEMS
|
|
||||||
function menuItem1() {
|
|
||||||
SpreadsheetApp.getUi()
|
|
||||||
{
|
|
||||||
Logger.log(getUser());
|
|
||||||
resultado = enviaCR();
|
|
||||||
Logger.log("Resultado: " + resultado + ".");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function menuItem2() {
|
|
||||||
SpreadsheetApp.getUi()
|
|
||||||
{
|
|
||||||
Logger.log(getUser());
|
|
||||||
resultado = totalizar(curName)
|
|
||||||
Logger.log("Resultado: " + resultado + ".");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function menuItem3() {
|
|
||||||
SpreadsheetApp.getUi()
|
|
||||||
{
|
|
||||||
Logger.log(getUser());
|
|
||||||
carimboValue = pegarCarimbo(SpreadsheetApp.getActiveSpreadsheet().getActiveSheet()) ;
|
|
||||||
if(!isNaN(parseFloat(carimboValue)) ) {
|
|
||||||
var mesAtual = Utilities.formatDate(carimboValue,timeZone, "MM");
|
|
||||||
Logger.log("Carimbo lido: " + mesAtual + ".");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
mesAtual = Utilities.formatDate(new Date(),timeZone, "MM") ;
|
|
||||||
Logger.log("Carimbo vazio, mês atual: " + mesAtual + ".");
|
|
||||||
}
|
|
||||||
|
|
||||||
voltaAoTopo();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// FUNCTIONS BELOW
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// SEND RF
|
|
||||||
function enviaCR() {
|
|
||||||
var ss = SpreadsheetApp.getActiveSpreadsheet(); // cria o objeto do arquivo da planilha
|
|
||||||
var sheet = ss.getActiveSheet(); // cria objeto da Sheet ativa agora
|
|
||||||
var curName = ss.getActiveSheet().getName() ; // pega nome da Sheet
|
|
||||||
|
|
||||||
// validar contas
|
|
||||||
if (validar(sheet))
|
|
||||||
{
|
|
||||||
// subir dados na planilha de controle
|
|
||||||
var resultadoEnvio = enviando(curName,sheet,ss);
|
|
||||||
if (resultadoEnvio == "Enviado" )
|
|
||||||
{SpreadsheetApp.getUi().alert('Relatório Enviado!');}
|
|
||||||
else
|
|
||||||
{SpreadsheetApp.getUi().alert('ERRO: ' + resultadoEnvio );}
|
|
||||||
} return resultadoEnvio;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// VALIDAR VALORES TODO: ADIOCIONAR NOVAS
|
|
||||||
function validar(sheet){
|
|
||||||
// trocar vendas por centralizado
|
|
||||||
var celulaDepositos = sheet.getRange(enddepositos);
|
|
||||||
var depositos = sheet.setCurrentCell(celulaDepositos).getValue();
|
|
||||||
var celulaVendas = sheet.getRange(endvendas);
|
|
||||||
var vendas = sheet.setCurrentCell(celulaVendas).getValue();
|
|
||||||
if ( vendas === depositos )
|
|
||||||
{ return true;}
|
|
||||||
else
|
|
||||||
{ SpreadsheetApp.getUi().alert('Centralizado ' + vendas + ' não bate com Depósitos ' + depositos ); return false ;}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function enviando(curName,sheet,ss) {
|
|
||||||
valorCotas = pegarTotalCota(curName, ss); // TOTAL das cotas
|
|
||||||
marcaCarimbos(curName, valorCotas, sheet); // SALVA TOTAL DAS COTAS ETC
|
|
||||||
novaAba = renomearAba(curName, ss); // Renomeia Aba e coloca nomero da nova aba no numero do relatorio
|
|
||||||
limpaEntradas(novaAba) ; // limpa carimbo e entradas
|
|
||||||
|
|
||||||
if (travar(curName, ss) === "Travada"){
|
|
||||||
ss.setActiveSheet(novaAba); // coloca novo em evidencia
|
|
||||||
return "Enviado";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function pegarTotalCota(curName, ss){
|
|
||||||
var sheet = ss.getSheetByName(curName);
|
|
||||||
var valorNovaAvulso = Number(sheet.setCurrentCell(sheet.getRange(celulaValorTotalCotas)).getValue());
|
|
||||||
Logger.log(" valorNovaAvulso: " + valorNovaAvulso + ".");
|
|
||||||
return valorNovaAvulso;
|
|
||||||
}
|
|
||||||
|
|
||||||
function marcaCarimbos(curName,totalCota,sheet){
|
|
||||||
var gravarTempo = Utilities.formatDate(new Date(),timeZone, "yyyyMMddHHmmssSSS");
|
|
||||||
var celulaTempo = 'D900';
|
|
||||||
var celulaTotalCotas = 'D901';
|
|
||||||
var celulaResponsavel = 'D902';
|
|
||||||
var celulaNomeContagem = 'D902';
|
|
||||||
var celulaResponsavelCel = 'H2';
|
|
||||||
var username = getUser();
|
|
||||||
sheet.setCurrentCell(sheet.getRange(celulaTempo)).setValue(gravarTempo);
|
|
||||||
sheet.setCurrentCell(sheet.getRange(celulaResponsavel)).setValue(username);
|
|
||||||
sheet.setCurrentCell(sheet.getRange(celulaNomeContagem)).setValue(curName);
|
|
||||||
sheet.setCurrentCell(sheet.getRange(celulaResponsavelCel)).setValue(username);
|
|
||||||
sheet.setCurrentCell(sheet.getRange(celulaTotalCotas)).setValue(totalCota);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function pegarCarimbo(sheet)
|
|
||||||
{
|
|
||||||
new Date(sheet.setCurrentCell(sheet.getRange(carimbo)).getValue())
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function renomearAba(curName,ss){
|
|
||||||
|
|
||||||
var newName = Number(curName) + 1 ; // cria nome da nova
|
|
||||||
ss.moveActiveSheet(ss.getNumSheets() - 1); // move a atual para a ultima posicao antes da Validacao que é escondida
|
|
||||||
ss.duplicateActiveSheet(); // duplica ativa
|
|
||||||
ss.renameActiveSheet(newName); // renomeia nova
|
|
||||||
ss.moveActiveSheet(1); // move para a primeira posicao
|
|
||||||
var sheet = ss.getSheetByName(newName); // torna a nova ativa usando nome
|
|
||||||
sheet.getRange(contagemRF).setValue(newName); //altera contagem do relatorio usando numero da aba
|
|
||||||
return sheet;
|
|
||||||
}
|
|
||||||
|
|
||||||
function limpaEntradas(sheet)
|
|
||||||
{
|
|
||||||
function limpaTudo(value){
|
|
||||||
sheet.getRange(value).clearContent();
|
|
||||||
}
|
|
||||||
var limpeza = [ cotas, contribuintes, brochuras, campanha , outras, assinantes, jornal , despesaCE, depositos, carimbo ];
|
|
||||||
limpeza.forEach(limpaTudo) ;
|
|
||||||
|
|
||||||
let range = sheet.getRange("I:Y");
|
|
||||||
sheet.hideColumn(range);
|
|
||||||
range = sheet.getRange("A258:A999");
|
|
||||||
sheet.hideRow(range);
|
|
||||||
|
|
||||||
SpreadsheetApp.getActiveSheet().setCurrentCell(SpreadsheetApp.getActiveSheet().getRange(celulaPrincipal)) ;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function travar(curName, ss){
|
|
||||||
var sheet = ss.getSheetByName(curName);
|
|
||||||
var areaProtegida = false ;
|
|
||||||
var abaProtegida = false ;
|
|
||||||
var userName = getUser();
|
|
||||||
var protections = sheet.getProtections(SpreadsheetApp.ProtectionType.SHEET);
|
|
||||||
|
|
||||||
for (var i = 0; i < protections.length; i++) {
|
|
||||||
var desc = protections[i].getDescription();
|
|
||||||
Logger.log("protection desc: " + desc);
|
|
||||||
if ( desc === 'Area protegida' ){ areaProtegida = true ; }
|
|
||||||
if ( desc === 'Aba protegida') { abaProtegida = true ; }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Protege area, e remove todos da lista de editores.
|
|
||||||
var range = sheet.getRange(areaAProteger);
|
|
||||||
|
|
||||||
if (areaProtegida === false && userName != CRSP ) {
|
|
||||||
proteRange = range.protect().setDescription('Area protegida') ;
|
|
||||||
areaProtegida = true ;
|
|
||||||
proteRange.removeEditor(userName);
|
|
||||||
if (proteRange.canDomainEdit()) {
|
|
||||||
proteRange.setDomainEdit(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.log(userName);
|
|
||||||
|
|
||||||
if (abaProtegida === false && userName != CRSP ) {
|
|
||||||
var proteSheet = sheet.protect().setDescription('Aba protegida');
|
|
||||||
abaProtegida = true ;
|
|
||||||
Logger.log("Removendo: " + userName);
|
|
||||||
proteSheet.removeEditor(userName);
|
|
||||||
if (proteSheet.canDomainEdit()) {
|
|
||||||
proteSheet.setDomainEdit(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
if (abaProtegida === true && areaProtegida === true ) { return "Travada" ;}
|
|
||||||
}
|
|
||||||
|
|
||||||
function efetuarRotinaMadrugada(){
|
|
||||||
travaNoturna();
|
|
||||||
totalizar();
|
|
||||||
}
|
|
||||||
|
|
||||||
function travaNoturna(){
|
|
||||||
var ss = SpreadsheetApp.openById(planilhaID);
|
|
||||||
var trava = 0 ;
|
|
||||||
console.log( getUser());
|
|
||||||
|
|
||||||
// Protects the sheet.
|
|
||||||
const sampleProtectedSheet = sheet.protect();
|
|
||||||
// Logs whether domain users have permission to edit the protected sheet to the console.
|
|
||||||
console.log(sampleProtectedSheet.canDomainEdit());
|
|
||||||
|
|
||||||
var sheets = SpreadsheetApp.getActiveSpreadsheet().getSheets();
|
|
||||||
for (var cadaSheet = 0 ; cadaSheet < sheets.length ; cadaSheet++){
|
|
||||||
var nomeSheet = sheets[cadaSheet].getName();
|
|
||||||
Logger.log(" TravaNoturna nomeSheet: " + nomeSheet);
|
|
||||||
if (!isNaN(parseFloat(nomeSheet)) && isFinite(nomeSheet) && nomeSheet === anterior) {
|
|
||||||
SpreadsheetApp.setActiveSheet(sheets[cadaSheet]);
|
|
||||||
var protections = sheets[cadaSheet].getProtections(SpreadsheetApp.ProtectionType.SHEET);
|
|
||||||
for (var i = 0; i < protections.length; i++) {
|
|
||||||
var desc = protections[i].getDescription();
|
|
||||||
Logger.log("trava desc: " + desc);
|
|
||||||
if ( desc === 'Area protegida' || desc === 'Aba protegida' ) {
|
|
||||||
trava = trava + 1 ;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ( trava == 2 ){
|
|
||||||
const protection = sheets[cadaSheet].protect();
|
|
||||||
// Logs whether domain users have permission to edit the protected sheet to the console.
|
|
||||||
console.log(protection.canDomainEdit());
|
|
||||||
protection.removeEditors(protection.getEditors());
|
|
||||||
if (protection.canDomainEdit()) {
|
|
||||||
protection.setDomainEdit(false);
|
|
||||||
}
|
|
||||||
console.log(protection.canDomainEdit());
|
|
||||||
protection.setDescription('Trava Noturna');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var sheet = ss.getSheetByName(anterior);
|
|
||||||
sheet.setCurrentCell(sheet.getRange(celulaPrincipal)) ;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function totalizar(curName){
|
|
||||||
var anterior = curName ;
|
|
||||||
Logger.log("anterior: " + anterior + ".");
|
|
||||||
|
|
||||||
var ss = SpreadsheetApp.getActiveSpreadsheet() ;
|
|
||||||
var sheets = SpreadsheetApp.getActiveSpreadsheet().getSheets();
|
|
||||||
var gravou = 0;
|
|
||||||
|
|
||||||
function enviarTotal() {
|
|
||||||
var sheet = ss.getSheetByName(anterior);
|
|
||||||
|
|
||||||
var brochuras = sheet.setCurrentCell(sheet.getRange('D66')).getValue() ;
|
|
||||||
var campanha = sheet.setCurrentCell(sheet.getRange('D71')).getValue();
|
|
||||||
var campanhaCCCE = sheet.setCurrentCell(sheet.getRange('D85')).getValue();
|
|
||||||
|
|
||||||
var outras = sheet.setCurrentCell(sheet.getRange('D95')).getValue();
|
|
||||||
var assinantes = sheet.setCurrentCell(sheet.getRange('D115')).getValue();
|
|
||||||
var jornal = sheet.setCurrentCell(sheet.getRange('D127')).getValue();
|
|
||||||
var carimboValue = pegarCarimbo() ;
|
|
||||||
|
|
||||||
var sheet = ss.getSheetByName("TOTAL");
|
|
||||||
|
|
||||||
// ABA TOTAL COLUNAS DE VALORES TOTALIZADOS
|
|
||||||
var cotascol = 'C'; // 0
|
|
||||||
var contribuintescol = 'E'; // 1
|
|
||||||
var brochurascol = 'H' ; // 2
|
|
||||||
var cfcol = 'J'; // 3
|
|
||||||
var outrascol = 'L'; // 4
|
|
||||||
var asscol = 'P'; // 5
|
|
||||||
var jornalcol ='R'; // 6
|
|
||||||
var varJaneiro = 3 ;
|
|
||||||
var varFevereiro = 4 ;
|
|
||||||
var varMarço = 5 ;
|
|
||||||
var varAbril = 6 ;
|
|
||||||
var varMaio = 7 ;
|
|
||||||
var varJunho = 8 ;
|
|
||||||
var varJulho = 9 ;
|
|
||||||
var varAgosto = 10 ;
|
|
||||||
var varSetembro = 11 ;
|
|
||||||
var varOutubro = 12 ;
|
|
||||||
var varNovembro = 13 ;
|
|
||||||
var varDezembro = 14 ;
|
|
||||||
var decimoTerceiro = 15 ;
|
|
||||||
var decimoQuarto = 16 ;
|
|
||||||
var decimoQuinto = 17 ;
|
|
||||||
var colunas = [ cotascol, contribuintescol , brochurascol , cfcol , outrascol , asscol , jornalcol ] ;
|
|
||||||
var linhas = [varJaneiro , varFevereiro ,varMarço ,varAbril ,varMaio ,varJunho ,varJulho ,varAgosto ,varSetembro ,varOutubro ,varNovembro ,varDezembro, decimoTerceiro, decimoQuarto, decimoQuinto] ;
|
|
||||||
// TERMINOU TABELA TOTAL /\
|
|
||||||
|
|
||||||
|
|
||||||
// PEGAR MES ATUAL
|
|
||||||
|
|
||||||
if(!isNaN(parseFloat(carimboValue)) ) {var mesAtual = Utilities.formatDate(carimboValue,timeZone, "MM");}
|
|
||||||
else { mesAtual = Utilities.formatDate(new Date(),timeZone, "MM") }
|
|
||||||
|
|
||||||
// Para cada Coluna de TOTAL executar totalização:
|
|
||||||
colunas.forEach(function(letra,coluna,tudo) {
|
|
||||||
Logger.log("letra: " + letra );
|
|
||||||
// começa com cota, checa se é o mes e coloca no switch.
|
|
||||||
switch (letra){
|
|
||||||
case cotascol:
|
|
||||||
// mes igual mes da primeira linha
|
|
||||||
for (var cadaMesdeCota = 5 ; cadaMesdeCota <=40 ; cadaMesdeCota++ ){
|
|
||||||
var celulaMilitante = 'B' + cadaMesdeCota ;
|
|
||||||
var celulaAno = 'C' + cadaMesdeCota ;
|
|
||||||
var celulaMes = 'D' + cadaMesdeCota ;
|
|
||||||
var celulaValor = 'E' + cadaMesdeCota ;
|
|
||||||
|
|
||||||
|
|
||||||
// Vai pra Anterior pra pegar cota de cadaMesdeCota ++++++++++++++++++++++++++++++++++++++++++++++
|
|
||||||
var sheet = ss.getSheetByName(anterior);
|
|
||||||
|
|
||||||
valorCelula = sheet.setCurrentCell(sheet.getRange(celulaValor)).getValue() ;
|
|
||||||
|
|
||||||
// if (!isNaN(parseFloat(mesCelula)) && !isNaN(parseFloat(valorCelula)))
|
|
||||||
if (!isNaN(parseFloat(valorCelula)))
|
|
||||||
{
|
|
||||||
var mesCelula = sheet.setCurrentCell(sheet.getRange(celulaMes)).getValue();
|
|
||||||
var retornoMes = checkMonth(mesCelula);
|
|
||||||
militanteCota = sheet.setCurrentCell(sheet.getRange(celulaMilitante)).getValue();
|
|
||||||
anoCota = sheet.setCurrentCell(sheet.getRange(celulaAno)).getValue();
|
|
||||||
Logger.log( " COTA valorCelula: " + valorCelula + " militanteCota " + militanteCota + "retornoMes" + retornoMes);
|
|
||||||
|
|
||||||
if ( !isNaN(parseFloat(retornoMes)) ) {
|
|
||||||
var mesNovaCota = new Date(retornoMes) ;
|
|
||||||
var mesNCemN = Number(Utilities.formatDate(mesNovaCota,timeZone, "MM")) - 1;
|
|
||||||
var valorNovaCota = valorCelula ;
|
|
||||||
|
|
||||||
if (!isNaN(parseFloat(valorNovaCota))){
|
|
||||||
// ENVIA PARA TOTAL:
|
|
||||||
Logger.log( " COTA valorNovaCota: " + valorNovaCota + ".");
|
|
||||||
|
|
||||||
var sheet = ss.getSheetByName("TOTAL");
|
|
||||||
var celulaObjetivo = letra + linhas[mesNCemN] ;
|
|
||||||
Logger.log( " COTA celulaObjetivo: " + celulaObjetivo + ".");
|
|
||||||
|
|
||||||
|
|
||||||
var valorAntigoCota = sheet.setCurrentCell(sheet.getRange(celulaObjetivo)).getValue() ;
|
|
||||||
if (!isNaN(parseFloat(valorAntigoCota))){
|
|
||||||
Logger.log( " COTA valorAntigoCota: " + valorAntigoCota + ".");
|
|
||||||
var gravar = valorAntigoCota + valorNovaCota ;
|
|
||||||
}
|
|
||||||
else { var gravar = valorNovaCota ; }
|
|
||||||
sheet.setCurrentCell(sheet.getRange(celulaObjetivo)).setValue(gravar);
|
|
||||||
Logger.log( " COTA Gravou: " + gravar + ".");
|
|
||||||
// ENVIOU PARA TOTAL /\
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
// segunda iteração contribuintes, checa se é o mes e coloca no switch.
|
|
||||||
case contribuintescol:
|
|
||||||
// mes igual mes da primeira linha
|
|
||||||
for (var cadaMesContrib = 43 ; cadaMesContrib <=57 ; cadaMesContrib++ ){
|
|
||||||
var celulaContribuinte = 'B' + cadaMesContrib ;
|
|
||||||
var celulaAno = 'C' + cadaMesContrib ;
|
|
||||||
var celulaMes = 'D' + cadaMesContrib ;
|
|
||||||
var celulaValor = 'E' + cadaMesContrib ;
|
|
||||||
var celulaResponsavel = 'F' + cadaMesContrib ;
|
|
||||||
|
|
||||||
// Vai pra Anterior pra pegar Contribuição de cadaMesdeCota
|
|
||||||
var sheet = ss.getSheetByName(anterior);
|
|
||||||
var retornoMes = sheet.setCurrentCell(sheet.getRange(celulaMes)).getValue();
|
|
||||||
|
|
||||||
if ( !isNaN(parseFloat(retornoMes)) ) {
|
|
||||||
var mesNovaContrib = new Date(checkMonth(retornoMes)) ;
|
|
||||||
var mesNCemN = Number(Utilities.formatDate(mesNovaContrib,timeZone, "MM")) - 1;
|
|
||||||
var valorNovaContr = Number(sheet.setCurrentCell(sheet.getRange(celulaValor)).getValue());
|
|
||||||
if (!isNaN(parseFloat(valorNovaContr))){
|
|
||||||
Logger.log( " CONTRIB valorNovaContr: " + valorNovaContr + ".");
|
|
||||||
var celulaObjetivo = letra + linhas[mesNCemN] ;
|
|
||||||
Logger.log( " CONTRIB celulaObjetivo: " + celulaObjetivo + ".");
|
|
||||||
var sheet = ss.getSheetByName("TOTAL");
|
|
||||||
var valorAntigoContr = sheet.setCurrentCell(sheet.getRange(celulaObjetivo)).getValue() ;
|
|
||||||
Logger.log( " CONTRIB valorAntigoContr: " + valorAntigoContr + ".");
|
|
||||||
if (!isNaN(parseFloat(valorAntigoContr)) ){
|
|
||||||
var gravar = valorNovaContr + valorAntigoContr ; }
|
|
||||||
else {
|
|
||||||
gravar = valorNovaContr ;
|
|
||||||
}
|
|
||||||
Logger.log( " CONTRIB celulaMes: " + celulaMes + " celulaValor: " + celulaValor + ".");
|
|
||||||
var sheet = ss.getSheetByName("TOTAL");
|
|
||||||
sheet.setCurrentCell(sheet.getRange(celulaObjetivo)).setValue(gravar);
|
|
||||||
Logger.log( " CONTRIB Gravou: " + gravar + ".");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
// FALTA TERMINAR BROCHURAS
|
|
||||||
case brochurascol:
|
|
||||||
for (var linhaBro = 60 ; linhaBro <=65 ; linhaBro++ ){
|
|
||||||
// var celulaNome = 'B' +linhaBro ;
|
|
||||||
var celulaQuantidade = 'C' + linhaBro ;
|
|
||||||
var celulaValor = 'D' + linhaBro ;
|
|
||||||
var celulaCodigo = 'E' + linhaBro ;
|
|
||||||
// Vai pra Anterior pra pegar dados acima
|
|
||||||
var sheet = ss.getSheetByName(anterior);
|
|
||||||
var difLinBro = 2 ;
|
|
||||||
var quantidadeBro = sheet.setCurrentCell(sheet.getRange(celulaQuantidade)).getValue();
|
|
||||||
if ( !isNaN(parseFloat(quantidadeBro)) ) {
|
|
||||||
var valorNovaBro = Number(sheet.setCurrentCell(sheet.getRange(celulaValor)).getValue());
|
|
||||||
var codigoNovaBro = Number(sheet.setCurrentCell(sheet.getRange(celulaCodigo)).getValue()) + difLinBro ;
|
|
||||||
Logger.log(" BROCHURAS valorNovaBro: " + valorNovaBro + " codigoNovaBro: " + codigoNovaBro);
|
|
||||||
if (!isNaN(parseFloat(valorNovaBro))){
|
|
||||||
var celulaObjetivo = letra + codigoNovaBro ;
|
|
||||||
var qtdObjetivo = 'G' + codigoNovaBro ;
|
|
||||||
var qtdAntigoBro = sheet.setCurrentCell(sheet.getRange(qtdObjetivo)).getValue() ;
|
|
||||||
var sheet = ss.getSheetByName("TOTAL");
|
|
||||||
var valorAntigoBro = sheet.setCurrentCell(sheet.getRange(celulaObjetivo)).getValue() ;
|
|
||||||
if (!isNaN(parseFloat(valorAntigoBro)) ){
|
|
||||||
var gravar = valorNovaBro + valorAntigoBro ; }
|
|
||||||
else {
|
|
||||||
gravar = valorNovaBro ;
|
|
||||||
}
|
|
||||||
Logger.log(" BROCHURAS celulaQuantidade: " + celulaQuantidade + " celulaValor: " + celulaValor + " gravar: " + gravar );
|
|
||||||
|
|
||||||
if (!isNaN(parseFloat(qtdAntigoBro)) ){
|
|
||||||
var gravarQtd = quantidadeBro + qtdAntigoBro ; }
|
|
||||||
else {
|
|
||||||
var gravarQtd = quantidadeBro ;
|
|
||||||
}
|
|
||||||
var sheet = ss.getSheetByName("TOTAL");
|
|
||||||
// Grava Valor
|
|
||||||
Logger.log(" BROCHURAS celulaValorObjetivo: " + celulaObjetivo + " gravar: " + gravar );
|
|
||||||
sheet.setCurrentCell(sheet.getRange(celulaObjetivo)).setValue(gravar);
|
|
||||||
Logger.log( " BROCHURAS Gravou Valor: " + gravar + ".");
|
|
||||||
// Grava quantidade
|
|
||||||
sheet.setCurrentCell(sheet.getRange(qtdObjetivo)).setValue(gravarQtd);
|
|
||||||
Logger.log( " BROCHURAS Gravou Qtd: " + gravarQtd + ".")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case cfcol:
|
|
||||||
for (var linhaCF = 68 ; linhaCF <=84 ; linhaCF++ ){
|
|
||||||
var celulaNome = 'B' + linhaCF ;
|
|
||||||
var celulaQuantidade = 'C' + linhaCF ;
|
|
||||||
var celulaValor = 'D' + linhaCF ;
|
|
||||||
var celulaCodigo = 'F' + linhaCF ;
|
|
||||||
var sheet = ss.getSheetByName(anterior);
|
|
||||||
var valorCF = sheet.setCurrentCell(sheet.getRange(celulaValor)).getValue();
|
|
||||||
if ( !isNaN(parseFloat(valorCF)) ) {
|
|
||||||
var militanteNovaCF = sheet.setCurrentCell(sheet.getRange(celulaNome)).getValue();
|
|
||||||
// VAI PRA TOTAL
|
|
||||||
var sheet = ss.getSheetByName("TOTAL");
|
|
||||||
var linhaSalvar = sheet.getRange('I3:J22').createTextFinder(militanteNovaCF).findNext();
|
|
||||||
if (linhaSalvar){
|
|
||||||
var valorAntigoCF = linhaSalvar.offset(0,1).getValue();
|
|
||||||
Logger.log(" CF linhaSalvar.getA1Notation(): " + linhaSalvar.getA1Notation() + " militanteNovaCF: " + militanteNovaCF + " valorCF: " + valorCF + " valorAntigoCF: " + valorAntigoCF );
|
|
||||||
if (!isNaN(parseFloat(valorAntigoCF)) ){ var gravar = valorCF + valorAntigoCF ; }
|
|
||||||
else { gravar = valorCF ; }
|
|
||||||
// Grava Valor
|
|
||||||
linhaSalvar.offset(0,1).setValue(gravar);
|
|
||||||
Logger.log( " CF Gravou Valor: " + gravar + ".");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case outrascol:
|
|
||||||
for (var linhaOutros = 87 ; linhaOutros <=94 ; linhaOutros++ ){
|
|
||||||
var celulaQuantidade = 'C' + linhaOutros ;
|
|
||||||
var celulaValor = 'D' + linhaOutros ;
|
|
||||||
var celulaCodigo = 'E' + linhaOutros ;
|
|
||||||
|
|
||||||
// Vai pra Anterior pra pegar dados acima
|
|
||||||
var sheet = ss.getSheetByName(anterior);
|
|
||||||
|
|
||||||
var quantidadeOutro = sheet.setCurrentCell(sheet.getRange(celulaQuantidade)).getValue();
|
|
||||||
if ( !isNaN(parseFloat(quantidadeOutro)) ) {
|
|
||||||
var valorNovaOutro = Number(sheet.setCurrentCell(sheet.getRange(celulaValor)).getValue());
|
|
||||||
var difLinOut = 2 ;
|
|
||||||
var codigoNovaOutro = Number(sheet.setCurrentCell(sheet.getRange(celulaCodigo)).getValue()) + difLinOut ;
|
|
||||||
if (!isNaN(parseFloat(valorNovaOutro))){
|
|
||||||
var celulaObjetivo = letra + codigoNovaOutro ;
|
|
||||||
var qtdObjetivo = 'K' + codigoNovaOutro ;
|
|
||||||
var qtdAntigoOutro = sheet.setCurrentCell(sheet.getRange(qtdObjetivo)).getValue() ;
|
|
||||||
var sheet = ss.getSheetByName("TOTAL");
|
|
||||||
var valorAntigoOutro = sheet.setCurrentCell(sheet.getRange(celulaObjetivo)).getValue() ;
|
|
||||||
if (!isNaN(parseFloat(valorAntigoOutro)) ){
|
|
||||||
var gravar = valorNovaOutro + valorAntigoOutro ; }
|
|
||||||
else {
|
|
||||||
gravar = valorNovaOutro ;
|
|
||||||
}
|
|
||||||
Logger.log( " OUTRAS celulaQuantidade: " + celulaQuantidade + " celulaValor: " + celulaValor + " gravar: " + gravar );
|
|
||||||
|
|
||||||
if (!isNaN(parseFloat(qtdAntigoOutro)) ){
|
|
||||||
var gravarQtd = quantidadeOutro + qtdAntigoOutro ; }
|
|
||||||
else {
|
|
||||||
var gravarQtd = quantidadeOutro ;
|
|
||||||
}
|
|
||||||
var sheet = ss.getSheetByName("TOTAL");
|
|
||||||
// Grava Valor
|
|
||||||
Logger.log( " OUTRAS celulaValorObjetivo: " + celulaObjetivo + " gravar: " + gravar );
|
|
||||||
sheet.setCurrentCell(sheet.getRange(celulaObjetivo)).setValue(gravar);
|
|
||||||
Logger.log( " OUTRAS Gravou Valor: " + gravar + ".");
|
|
||||||
// Grava quantidade
|
|
||||||
sheet.setCurrentCell(sheet.getRange(qtdObjetivo)).setValue(gravarQtd);
|
|
||||||
Logger.log( " OUTRAS Gravou Qtd: " + gravarQtd + ".")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case asscol:
|
|
||||||
// ARRUMAR ASSINATURAS
|
|
||||||
var linha = mesAtual - 1 ;
|
|
||||||
Logger.log(" ASSINATURA linhas[linha]: " + linhas[linha] + " linha: " + linha + " mesAtual: " + mesAtual + ".");
|
|
||||||
var celula = letra + linhas[linha] ;
|
|
||||||
|
|
||||||
// PEGAR TOTAL ATUAL
|
|
||||||
var sheet = ss.getSheetByName(anterior);
|
|
||||||
var totalatual = sheet.setCurrentCell(sheet.getRange(celula)).getValue() ;
|
|
||||||
if ( !isNaN(parseFloat(outras)) && assinantes > 0 ){
|
|
||||||
var gravar = totalatual + assinantes ;
|
|
||||||
// GRAVAR
|
|
||||||
var sheet = ss.getSheetByName("TOTAL");
|
|
||||||
sheet.setCurrentCell(sheet.getRange(celula)).setValue(gravar);
|
|
||||||
}
|
|
||||||
break ;
|
|
||||||
case jornalcol:
|
|
||||||
for (var linhaAvulso = 112 ; linhaAvulso <=126 ; linhaAvulso++ ){
|
|
||||||
var celulaQuantidade = 'C' + linhaAvulso ;
|
|
||||||
var celulaValor = 'D' + linhaAvulso ;
|
|
||||||
var celulaEdicao = 'B' + linhaAvulso ;
|
|
||||||
|
|
||||||
// Vai pra Anterior pra pegar dados acima
|
|
||||||
var sheet = ss.getSheetByName(anterior);
|
|
||||||
var difLinEdicao = 11
|
|
||||||
var quantidadeAvulso = sheet.setCurrentCell(sheet.getRange(celulaQuantidade)).getValue();
|
|
||||||
if ( !isNaN(parseFloat(quantidadeAvulso)) ) {
|
|
||||||
var valorNovaAvulso = Number(sheet.setCurrentCell(sheet.getRange(celulaValor)).getValue());
|
|
||||||
var edicaoNovaAvulso = Number(sheet.setCurrentCell(sheet.getRange(celulaEdicao)).getValue()) - difLinEdicao ;
|
|
||||||
Logger.log( " JORNALA. edicaoNovaAvulso: " + edicaoNovaAvulso + "valorNovaAvulso: " + valorNovaAvulso + "Quantidade: " + quantidadeAvulso );
|
|
||||||
if (!isNaN(parseFloat(valorNovaAvulso))){
|
|
||||||
var celulaValorObjetivo = letra + edicaoNovaAvulso ;
|
|
||||||
var qtdVendido = 'T' + edicaoNovaAvulso ;
|
|
||||||
var sheet = ss.getSheetByName("TOTAL");
|
|
||||||
var valorAntigoAvulso = sheet.setCurrentCell(sheet.getRange(celulaValorObjetivo)).getValue() ;
|
|
||||||
var qtdAntigoAvulso = sheet.setCurrentCell(sheet.getRange(qtdVendido)).getValue() ;
|
|
||||||
if (!isNaN(parseFloat(valorAntigoAvulso)) ){
|
|
||||||
var gravar = valorNovaAvulso + valorAntigoAvulso ; }
|
|
||||||
else {
|
|
||||||
var gravar = valorNovaAvulso ;
|
|
||||||
}
|
|
||||||
if (!isNaN(parseFloat(qtdAntigoAvulso)) ){
|
|
||||||
var gravarQtd = quantidadeAvulso + qtdAntigoAvulso ; }
|
|
||||||
else {
|
|
||||||
var gravarQtd = quantidadeAvulso ;
|
|
||||||
}
|
|
||||||
var sheet = ss.getSheetByName("TOTAL");
|
|
||||||
// Grava Valor
|
|
||||||
sheet.setCurrentCell(sheet.getRange(celulaValorObjetivo)).setValue(gravar);
|
|
||||||
Logger.log( " JORNALA. Gravou Valor: " + gravar );
|
|
||||||
// Grava quantidade
|
|
||||||
sheet.setCurrentCell(sheet.getRange(qtdVendido)).setValue(gravarQtd);
|
|
||||||
Logger.log( " JORNALA. Gravou Qtd: " + gravarQtd );
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} )
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sheets.length > 1) {
|
|
||||||
for (var cadaSheet = 0 ; cadaSheet < sheets.length ; cadaSheet++)
|
|
||||||
{
|
|
||||||
var nomeSheet = sheets[cadaSheet].getName();
|
|
||||||
Logger.log("nomeSheet: " + nomeSheet);
|
|
||||||
if (!isNaN(parseFloat(nomeSheet)) && isFinite(nomeSheet) && nomeSheet === anterior) {
|
|
||||||
SpreadsheetApp.setActiveSheet(sheets[cadaSheet]);
|
|
||||||
var protections = sheets[cadaSheet].getProtections(SpreadsheetApp.ProtectionType.SHEET);
|
|
||||||
for (var i = 0; i < protections.length; i++) {
|
|
||||||
var desc = protections[i].getDescription();
|
|
||||||
Logger.log("protection desc: " + desc);
|
|
||||||
if ( desc === 'Aba protegida') {
|
|
||||||
enviarTotal();
|
|
||||||
gravou = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var sheet = ss.getSheetByName(anterior);
|
|
||||||
sheet.setCurrentCell(sheet.getRange(celulaPrincipal)) ;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (gravou === 1 ) { return "Totalizado";}
|
|
||||||
Logger.log("gravou: " + gravou + ".");
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function checkMonth(nomedoMes)
|
|
||||||
{
|
|
||||||
Logger.log(" checkMonth nomedoMes: " + nomedoMes + ".");
|
|
||||||
switch (nomedoMes){
|
|
||||||
case 1:
|
|
||||||
case "Janeiro" :
|
|
||||||
return "1";
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
case "Fevereiro":
|
|
||||||
return "2";
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
case "Março":
|
|
||||||
return "3";
|
|
||||||
break;
|
|
||||||
case 4:
|
|
||||||
case "Abril":
|
|
||||||
return "4";
|
|
||||||
break;
|
|
||||||
case 5:
|
|
||||||
case "Maio":
|
|
||||||
return "5";
|
|
||||||
break;
|
|
||||||
case 6:
|
|
||||||
case "Junho":
|
|
||||||
return "6";
|
|
||||||
break;
|
|
||||||
case 7:
|
|
||||||
case "Julho":
|
|
||||||
return "7";
|
|
||||||
break;
|
|
||||||
case 8:
|
|
||||||
case "Agosto":
|
|
||||||
return "8";
|
|
||||||
break;
|
|
||||||
case 9:
|
|
||||||
case "Setembro":
|
|
||||||
return "9";
|
|
||||||
break;
|
|
||||||
case 10:
|
|
||||||
case "Outubro":
|
|
||||||
return "10";
|
|
||||||
break;
|
|
||||||
case 11:
|
|
||||||
case "Novembro":
|
|
||||||
return "11";
|
|
||||||
break;
|
|
||||||
case 12:
|
|
||||||
case "Dezembro":
|
|
||||||
return "12";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return nomedoMes;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
from PIL import Image, ImageDraw, ImageFont
|
|
||||||
import qrcode
|
|
||||||
|
|
||||||
def gerar_carteirinha(militante_id, nome):
|
|
||||||
# Criar imagem base
|
|
||||||
img = Image.new('RGB', (300, 200), color=(255, 255, 255))
|
|
||||||
d = ImageDraw.Draw(img)
|
|
||||||
|
|
||||||
# Adicionar texto
|
|
||||||
font = ImageFont.load_default()
|
|
||||||
d.text((10, 10), f"Nome: {nome}", font=font, fill=(0, 0, 0))
|
|
||||||
d.text((10, 30), f"ID: {militante_id}", font=font, fill=(0, 0, 0))
|
|
||||||
|
|
||||||
# Gerar QR code
|
|
||||||
qr = qrcode.make(f"ID: {militante_id}")
|
|
||||||
img.paste(qr, (200, 50))
|
|
||||||
|
|
||||||
# Salvar imagem
|
|
||||||
img.save(f"carteirinha_{militante_id}.png")
|
|
||||||
@@ -1,16 +1,38 @@
|
|||||||
from sqlalchemy import create_engine, Column, Integer, String, Boolean, Numeric, Date, ForeignKey
|
from datetime import datetime, timedelta
|
||||||
from sqlalchemy.orm import relationship, sessionmaker
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
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)
|
SessionLocal = sessionmaker(bind=engine)
|
||||||
|
|
||||||
def get_db_connection():
|
def get_db_connection():
|
||||||
"""
|
"""Retorna uma nova conexão com o banco de dados"""
|
||||||
Retorna uma nova sessão do banco de dados
|
db = SessionLocal()
|
||||||
"""
|
try:
|
||||||
return SessionLocal()
|
# 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):
|
def execute_query(query, params=None):
|
||||||
"""
|
"""
|
||||||
@@ -27,22 +49,246 @@ def execute_query(query, params=None):
|
|||||||
finally:
|
finally:
|
||||||
session.close()
|
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):
|
class Militante(Base):
|
||||||
__tablename__ = 'militantes'
|
__tablename__ = 'militantes'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
nome = Column(String(100), nullable=False)
|
nome = Column(String(100), nullable=False)
|
||||||
cpf = Column(String(14), unique=True)
|
cpf = Column(String(14), unique=True)
|
||||||
email = Column(String(100), unique=True)
|
# Novos campos básicos
|
||||||
telefone = Column(String(15))
|
titulo_eleitoral = Column(String(20))
|
||||||
endereco = Column(String(255))
|
data_nascimento = Column(Date)
|
||||||
filiado = Column(Boolean, default=False)
|
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")
|
cotas_mensais = relationship("CotaMensal", back_populates="militante")
|
||||||
pagamentos = relationship("Pagamento", back_populates="militante")
|
pagamentos = relationship("Pagamento", back_populates="militante")
|
||||||
materiais_vendidos = relationship("MaterialVendido", back_populates="militante")
|
materiais_vendidos = relationship("MaterialVendido", back_populates="militante")
|
||||||
vendas_jornais = relationship("VendaJornalAvulso", back_populates="militante")
|
vendas_jornais = relationship("VendaJornalAvulso", back_populates="militante")
|
||||||
assinaturas = relationship("AssinaturaAnual", 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):
|
class CotaMensal(Base):
|
||||||
__tablename__ = 'cotas_mensais'
|
__tablename__ = 'cotas_mensais'
|
||||||
@@ -52,6 +298,8 @@ class CotaMensal(Base):
|
|||||||
valor_antigo = Column(Numeric(10, 2), nullable=False)
|
valor_antigo = Column(Numeric(10, 2), nullable=False)
|
||||||
valor_novo = Column(Numeric(10, 2), nullable=False)
|
valor_novo = Column(Numeric(10, 2), nullable=False)
|
||||||
data_alteracao = Column(Date, 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")
|
militante = relationship("Militante", back_populates="cotas_mensais")
|
||||||
|
|
||||||
@@ -61,19 +309,22 @@ class TipoPagamento(Base):
|
|||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
descricao = Column(String(100), nullable=False)
|
descricao = Column(String(100), nullable=False)
|
||||||
|
|
||||||
pagamentos = relationship("Pagamento", back_populates="tipo_pagamento")
|
|
||||||
|
|
||||||
class Pagamento(Base):
|
class Pagamento(Base):
|
||||||
__tablename__ = 'pagamentos'
|
__tablename__ = 'pagamentos'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
militante_id = Column(Integer, ForeignKey('militantes.id'))
|
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)
|
valor = Column(Numeric(10, 2), nullable=False)
|
||||||
data_pagamento = Column(Date, nullable=False)
|
data_pagamento = Column(Date, nullable=False)
|
||||||
|
|
||||||
militante = relationship("Militante", back_populates="pagamentos")
|
militante = relationship("Militante", back_populates="pagamentos")
|
||||||
tipo_pagamento = relationship("TipoPagamento", back_populates="pagamentos")
|
transacoes_pix = relationship("TransacaoPIX", back_populates="pagamento")
|
||||||
|
|
||||||
class TipoMaterial(Base):
|
class TipoMaterial(Base):
|
||||||
__tablename__ = 'tipos_materiais'
|
__tablename__ = 'tipos_materiais'
|
||||||
@@ -125,9 +376,18 @@ class AssinaturaAnual(Base):
|
|||||||
class Setor(Base):
|
class Setor(Base):
|
||||||
__tablename__ = 'setores'
|
__tablename__ = 'setores'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
id = Column(Integer, primary_key=True)
|
||||||
nome = Column(String(100), nullable=False)
|
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_cotas = relationship("RelatorioCotasMensais", back_populates="setor")
|
||||||
relatorios_vendas = relationship("RelatorioVendasMateriais", back_populates="setor")
|
relatorios_vendas = relationship("RelatorioVendasMateriais", back_populates="setor")
|
||||||
|
|
||||||
@@ -164,4 +424,313 @@ class RelatorioVendasMateriais(Base):
|
|||||||
setor = relationship("Setor", back_populates="relatorios_vendas")
|
setor = relationship("Setor", back_populates="relatorios_vendas")
|
||||||
comite = relationship("ComiteCentral", back_populates="relatorios_vendas")
|
comite = relationship("ComiteCentral", back_populates="relatorios_vendas")
|
||||||
|
|
||||||
|
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)
|
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
168
functions/decorators.py
Normal 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
222
functions/permissions.py
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
from functools import wraps
|
||||||
|
from flask import abort, g
|
||||||
|
from .database import Militante, Celula, Setor, CR, CC
|
||||||
|
|
||||||
|
def check_permission(permission_func):
|
||||||
|
def decorator(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
if not permission_func(*args, **kwargs):
|
||||||
|
abort(403)
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
return decorated_function
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
def can_manage_militante(militante_id):
|
||||||
|
"""Verifica se o usuário atual pode gerenciar um militante específico."""
|
||||||
|
if not g.user or not g.user.militante:
|
||||||
|
return False
|
||||||
|
|
||||||
|
militante = Militante.query.get(militante_id)
|
||||||
|
if not militante:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Secretário Geral e Secretário de Organização podem gerenciar qualquer militante
|
||||||
|
if g.user.militante.responsabilidades & (Militante.SECRETARIO_GERAL | Militante.SECRETARIO_ORGANIZACAO):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Secretário de CC pode gerenciar militantes do seu CC
|
||||||
|
if g.user.militante.responsabilidades & Militante.SECRETARIO_CC:
|
||||||
|
if militante.celula.setor.cr.cc_id == g.user.militante.celula.setor.cr.cc_id:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Secretário de CR pode gerenciar militantes do seu CR
|
||||||
|
if g.user.militante.responsabilidades & Militante.SECRETARIO_CR:
|
||||||
|
if militante.celula.setor.cr_id == g.user.militante.celula.setor.cr_id:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Secretário de Setor pode gerenciar militantes do seu setor
|
||||||
|
if g.user.militante.responsabilidades & Militante.SECRETARIO_SETOR:
|
||||||
|
if militante.celula.setor_id == g.user.militante.celula.setor_id:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Secretário de Célula pode gerenciar militantes da sua célula
|
||||||
|
if g.user.militante.responsabilidades & Militante.SECRETARIO_CELULA:
|
||||||
|
if militante.celula_id == g.user.militante.celula_id:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def can_manage_celula(celula_id):
|
||||||
|
"""Verifica se o usuário atual pode gerenciar uma célula específica."""
|
||||||
|
if not g.user or not g.user.militante:
|
||||||
|
return False
|
||||||
|
|
||||||
|
celula = Celula.query.get(celula_id)
|
||||||
|
if not celula:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Secretário Geral e Secretário de Organização podem gerenciar qualquer célula
|
||||||
|
if g.user.militante.responsabilidades & (Militante.SECRETARIO_GERAL | Militante.SECRETARIO_ORGANIZACAO):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Secretário de CC pode gerenciar células do seu CC
|
||||||
|
if g.user.militante.responsabilidades & Militante.SECRETARIO_CC:
|
||||||
|
if celula.setor.cr.cc_id == g.user.militante.celula.setor.cr.cc_id:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Secretário de CR pode gerenciar células do seu CR
|
||||||
|
if g.user.militante.responsabilidades & Militante.SECRETARIO_CR:
|
||||||
|
if celula.setor.cr_id == g.user.militante.celula.setor.cr_id:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Secretário de Setor pode gerenciar células do seu setor
|
||||||
|
if g.user.militante.responsabilidades & Militante.SECRETARIO_SETOR:
|
||||||
|
if celula.setor_id == g.user.militante.celula.setor_id:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def can_manage_setor(setor_id):
|
||||||
|
"""Verifica se o usuário atual pode gerenciar um setor específico."""
|
||||||
|
if not g.user or not g.user.militante:
|
||||||
|
return False
|
||||||
|
|
||||||
|
setor = Setor.query.get(setor_id)
|
||||||
|
if not setor:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Secretário Geral e Secretário de Organização podem gerenciar qualquer setor
|
||||||
|
if g.user.militante.responsabilidades & (Militante.SECRETARIO_GERAL | Militante.SECRETARIO_ORGANIZACAO):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Secretário de CC pode gerenciar setores do seu CC
|
||||||
|
if g.user.militante.responsabilidades & Militante.SECRETARIO_CC:
|
||||||
|
if setor.cr.cc_id == g.user.militante.celula.setor.cr.cc_id:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Secretário de CR pode gerenciar setores do seu CR
|
||||||
|
if g.user.militante.responsabilidades & Militante.SECRETARIO_CR:
|
||||||
|
if setor.cr_id == g.user.militante.celula.setor.cr_id:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def can_manage_cr(cr_id):
|
||||||
|
"""Verifica se o usuário atual pode gerenciar um CR específico."""
|
||||||
|
if not g.user or not g.user.militante:
|
||||||
|
return False
|
||||||
|
|
||||||
|
cr = CR.query.get(cr_id)
|
||||||
|
if not cr:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Secretário Geral e Secretário de Organização podem gerenciar qualquer CR
|
||||||
|
if g.user.militante.responsabilidades & (Militante.SECRETARIO_GERAL | Militante.SECRETARIO_ORGANIZACAO):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Secretário de CC pode gerenciar CRs do seu CC
|
||||||
|
if g.user.militante.responsabilidades & Militante.SECRETARIO_CC:
|
||||||
|
if cr.cc_id == g.user.militante.celula.setor.cr.cc_id:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def can_manage_cc(cc_id):
|
||||||
|
"""Verifica se o usuário atual pode gerenciar um CC específico."""
|
||||||
|
if not g.user or not g.user.militante:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Apenas Secretário Geral e Secretário de Organização podem gerenciar CCs
|
||||||
|
if g.user.militante.responsabilidades & (Militante.SECRETARIO_GERAL | Militante.SECRETARIO_ORGANIZACAO):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def can_manage_financas(instancia_id, tipo_instancia):
|
||||||
|
"""Verifica se o usuário atual pode gerenciar finanças de uma instância específica."""
|
||||||
|
if not g.user or not g.user.militante:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Secretário Geral e Secretário de Organização podem gerenciar finanças de qualquer instância
|
||||||
|
if g.user.militante.responsabilidades & (Militante.SECRETARIO_GERAL | Militante.SECRETARIO_ORGANIZACAO):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Responsável de Finanças da instância pode gerenciar suas finanças
|
||||||
|
if tipo_instancia == 'celula':
|
||||||
|
celula = Celula.query.get(instancia_id)
|
||||||
|
if celula and celula.responsavel_financas_id == g.user.militante.id:
|
||||||
|
return True
|
||||||
|
elif tipo_instancia == 'setor':
|
||||||
|
setor = Setor.query.get(instancia_id)
|
||||||
|
if setor and setor.responsavel_financas_id == g.user.militante.id:
|
||||||
|
return True
|
||||||
|
elif tipo_instancia == 'cr':
|
||||||
|
cr = CR.query.get(instancia_id)
|
||||||
|
if cr and cr.responsavel_financas_id == g.user.militante.id:
|
||||||
|
return True
|
||||||
|
elif tipo_instancia == 'cc':
|
||||||
|
cc = CC.query.get(instancia_id)
|
||||||
|
if cc and cc.responsavel_financas_id == g.user.militante.id:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def can_manage_imprensa(instancia_id, tipo_instancia):
|
||||||
|
"""Verifica se o usuário atual pode gerenciar imprensa de uma instância específica."""
|
||||||
|
if not g.user or not g.user.militante:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Secretário Geral e Secretário de Organização podem gerenciar imprensa de qualquer instância
|
||||||
|
if g.user.militante.responsabilidades & (Militante.SECRETARIO_GERAL | Militante.SECRETARIO_ORGANIZACAO):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Responsável de Imprensa da instância pode gerenciar sua imprensa
|
||||||
|
if tipo_instancia == 'celula':
|
||||||
|
celula = Celula.query.get(instancia_id)
|
||||||
|
if celula and celula.responsavel_imprensa_id == g.user.militante.id:
|
||||||
|
return True
|
||||||
|
elif tipo_instancia == 'setor':
|
||||||
|
setor = Setor.query.get(instancia_id)
|
||||||
|
if setor and setor.responsavel_imprensa_id == g.user.militante.id:
|
||||||
|
return True
|
||||||
|
elif tipo_instancia == 'cr':
|
||||||
|
cr = CR.query.get(instancia_id)
|
||||||
|
if cr and cr.responsavel_imprensa_id == g.user.militante.id:
|
||||||
|
return True
|
||||||
|
elif tipo_instancia == 'cc':
|
||||||
|
cc = CC.query.get(instancia_id)
|
||||||
|
if cc and cc.responsavel_imprensa_id == g.user.militante.id:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def can_manage_responsabilidades(militante_id):
|
||||||
|
"""Verifica se o usuário atual pode gerenciar responsabilidades de um militante específico."""
|
||||||
|
if not g.user or not g.user.militante:
|
||||||
|
return False
|
||||||
|
|
||||||
|
militante = Militante.query.get(militante_id)
|
||||||
|
if not militante:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Secretário Geral e Secretário de Organização podem gerenciar responsabilidades de qualquer militante
|
||||||
|
if g.user.militante.responsabilidades & (Militante.SECRETARIO_GERAL | Militante.SECRETARIO_ORGANIZACAO):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Secretário de CC pode gerenciar responsabilidades de militantes do seu CC
|
||||||
|
if g.user.militante.responsabilidades & Militante.SECRETARIO_CC:
|
||||||
|
if militante.celula.setor.cr.cc_id == g.user.militante.celula.setor.cr.cc_id:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Secretário de CR pode gerenciar responsabilidades de militantes do seu CR
|
||||||
|
if g.user.militante.responsabilidades & Militante.SECRETARIO_CR:
|
||||||
|
if militante.celula.setor.cr_id == g.user.militante.celula.setor.cr_id:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Secretário de Setor pode gerenciar responsabilidades de militantes do seu setor
|
||||||
|
if g.user.militante.responsabilidades & Militante.SECRETARIO_SETOR:
|
||||||
|
if militante.celula.setor_id == g.user.militante.celula.setor_id:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
315
functions/rbac.py
Normal file
315
functions/rbac.py
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
from sqlalchemy import Column, Integer, String, Text, ForeignKey, Table
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
from .base import Base
|
||||||
|
|
||||||
|
# Tabela de mapeamento Role-Permission
|
||||||
|
role_permissions = Table(
|
||||||
|
'role_permissions',
|
||||||
|
Base.metadata,
|
||||||
|
Column('role_id', Integer, ForeignKey('roles.id'), primary_key=True),
|
||||||
|
Column('permission_id', Integer, ForeignKey('permissions.id'), primary_key=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Tabela de mapeamento User-Role
|
||||||
|
user_roles = Table(
|
||||||
|
'user_roles',
|
||||||
|
Base.metadata,
|
||||||
|
Column('user_id', Integer, ForeignKey('usuarios.id'), primary_key=True),
|
||||||
|
Column('role_id', Integer, ForeignKey('roles.id'), primary_key=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
class Role(Base):
|
||||||
|
__tablename__ = 'roles'
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
nome = Column(String(50), unique=True, nullable=False)
|
||||||
|
nivel = Column(Integer, nullable=False) # Nível hierárquico
|
||||||
|
descricao = Column(Text)
|
||||||
|
|
||||||
|
# Relacionamentos
|
||||||
|
permissions = relationship("Permission", secondary=role_permissions, back_populates="roles")
|
||||||
|
users = relationship("Usuario", secondary=user_roles, back_populates="roles")
|
||||||
|
|
||||||
|
# Níveis de role
|
||||||
|
MILITANTE_BASICO = 1
|
||||||
|
SECRETARIO_CELULA = 2
|
||||||
|
MEMBRO_SETOR = 3
|
||||||
|
SECRETARIO_SETOR = 4
|
||||||
|
MEMBRO_CR = 5
|
||||||
|
SECRETARIO_CR = 6
|
||||||
|
MEMBRO_CC = 7
|
||||||
|
SECRETARIO_GERAL = 8
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_roles_list():
|
||||||
|
return [
|
||||||
|
(Role.MILITANTE_BASICO, "Militante Básico"),
|
||||||
|
(Role.SECRETARIO_CELULA, "Secretário de Célula"),
|
||||||
|
(Role.MEMBRO_SETOR, "Membro de Setor"),
|
||||||
|
(Role.SECRETARIO_SETOR, "Secretário de Setor"),
|
||||||
|
(Role.MEMBRO_CR, "Membro de CR"),
|
||||||
|
(Role.SECRETARIO_CR, "Secretário de CR"),
|
||||||
|
(Role.MEMBRO_CC, "Membro do CC"),
|
||||||
|
(Role.SECRETARIO_GERAL, "Secretário Geral")
|
||||||
|
]
|
||||||
|
|
||||||
|
class Permission(Base):
|
||||||
|
__tablename__ = 'permissions'
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
nome = Column(String(50), unique=True, nullable=False)
|
||||||
|
descricao = Column(Text)
|
||||||
|
|
||||||
|
# Relacionamentos
|
||||||
|
roles = relationship("Role", secondary=role_permissions, back_populates="permissions")
|
||||||
|
|
||||||
|
# Permissões básicas
|
||||||
|
VIEW_OWN_DATA = "view_own_data"
|
||||||
|
EDIT_OWN_DATA = "edit_own_data"
|
||||||
|
VIEW_CELL_DATA = "view_cell_data"
|
||||||
|
CREATE_MILITANT = "create_militant" # Nova permissão para criar militantes
|
||||||
|
|
||||||
|
# Permissões de célula
|
||||||
|
MANAGE_CELL_MEMBERS = "manage_cell_members"
|
||||||
|
CREATE_CELL_MEMBER = "create_cell_member"
|
||||||
|
VIEW_CELL_REPORTS = "view_cell_reports"
|
||||||
|
MANAGE_CELL_REPORTS = "manage_cell_reports" # Nova permissão
|
||||||
|
REGISTER_CELL_PAYMENT = "register_cell_payment"
|
||||||
|
|
||||||
|
# Permissões de setor
|
||||||
|
MANAGE_SECTOR_CELLS = "manage_sector_cells"
|
||||||
|
CREATE_SECTOR_CELL = "create_sector_cell"
|
||||||
|
VIEW_SECTOR_REPORTS = "view_sector_reports"
|
||||||
|
REGISTER_SECTOR_PAYMENT = "register_sector_payment"
|
||||||
|
|
||||||
|
# Permissões de CR
|
||||||
|
MANAGE_CR_SECTORS = "manage_cr_sectors"
|
||||||
|
CREATE_CR_SECTOR = "create_cr_sector"
|
||||||
|
VIEW_CR_REPORTS = "view_cr_reports"
|
||||||
|
REGISTER_CR_PAYMENT = "register_cr_payment"
|
||||||
|
|
||||||
|
# Permissões de CC
|
||||||
|
MANAGE_CC_CRS = "manage_cc_crs"
|
||||||
|
CREATE_CC_CR = "create_cc_cr"
|
||||||
|
VIEW_CC_REPORTS = "view_cc_reports"
|
||||||
|
REGISTER_CC_PAYMENT = "register_cc_payment"
|
||||||
|
SYSTEM_CONFIG = "system_config"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_permissions_list():
|
||||||
|
return [
|
||||||
|
# Permissões básicas
|
||||||
|
(Permission.VIEW_OWN_DATA, "Visualizar próprios dados"),
|
||||||
|
(Permission.EDIT_OWN_DATA, "Editar próprios dados"),
|
||||||
|
(Permission.VIEW_CELL_DATA, "Visualizar dados da célula"),
|
||||||
|
(Permission.CREATE_MILITANT, "Criar novos militantes"), # Nova permissão
|
||||||
|
|
||||||
|
# Permissões de célula
|
||||||
|
(Permission.MANAGE_CELL_MEMBERS, "Gerenciar membros da célula"),
|
||||||
|
(Permission.CREATE_CELL_MEMBER, "Criar membros na célula"),
|
||||||
|
(Permission.VIEW_CELL_REPORTS, "Visualizar relatórios da célula"),
|
||||||
|
(Permission.MANAGE_CELL_REPORTS, "Gerenciar relatórios da célula"), # Nova permissão
|
||||||
|
(Permission.REGISTER_CELL_PAYMENT, "Registrar pagamentos da célula"),
|
||||||
|
|
||||||
|
# Permissões de setor
|
||||||
|
(Permission.MANAGE_SECTOR_CELLS, "Gerenciar células do setor"),
|
||||||
|
(Permission.CREATE_SECTOR_CELL, "Criar células no setor"),
|
||||||
|
(Permission.VIEW_SECTOR_REPORTS, "Visualizar relatórios do setor"),
|
||||||
|
(Permission.REGISTER_SECTOR_PAYMENT, "Registrar pagamentos do setor"),
|
||||||
|
|
||||||
|
# Permissões de CR
|
||||||
|
(Permission.MANAGE_CR_SECTORS, "Gerenciar setores do CR"),
|
||||||
|
(Permission.CREATE_CR_SECTOR, "Criar setores no CR"),
|
||||||
|
(Permission.VIEW_CR_REPORTS, "Visualizar relatórios do CR"),
|
||||||
|
(Permission.REGISTER_CR_PAYMENT, "Registrar pagamentos do CR"),
|
||||||
|
|
||||||
|
# Permissões de CC
|
||||||
|
(Permission.MANAGE_CC_CRS, "Gerenciar CRs"),
|
||||||
|
(Permission.CREATE_CC_CR, "Criar CRs"),
|
||||||
|
(Permission.VIEW_CC_REPORTS, "Visualizar relatórios nacionais"),
|
||||||
|
(Permission.REGISTER_CC_PAYMENT, "Registrar pagamentos nacionais"),
|
||||||
|
(Permission.SYSTEM_CONFIG, "Configurar sistema")
|
||||||
|
]
|
||||||
|
|
||||||
|
def init_rbac():
|
||||||
|
"""Inicializa o sistema RBAC com roles e permissões básicas"""
|
||||||
|
from .database import Usuario, get_db_connection
|
||||||
|
session = get_db_connection()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Criar role de administrador primeiro
|
||||||
|
admin_role = session.query(Role).filter_by(nome="Administrador").first()
|
||||||
|
if not admin_role:
|
||||||
|
admin_role = Role(nome="Administrador", nivel=Role.SECRETARIO_GERAL)
|
||||||
|
session.add(admin_role)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
# Criar outras roles
|
||||||
|
for nivel, nome in Role.get_roles_list():
|
||||||
|
if nome != "Administrador": # Pular Administrador pois já foi criado
|
||||||
|
role = session.query(Role).filter_by(nivel=nivel).first()
|
||||||
|
if not role:
|
||||||
|
role = Role(nome=nome, nivel=nivel)
|
||||||
|
session.add(role)
|
||||||
|
|
||||||
|
# Criar permissões
|
||||||
|
for nome, descricao in Permission.get_permissions_list():
|
||||||
|
permission = session.query(Permission).filter_by(nome=nome).first()
|
||||||
|
if not permission:
|
||||||
|
permission = Permission(nome=nome, descricao=descricao)
|
||||||
|
session.add(permission)
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
# Dar todas as permissões para o admin
|
||||||
|
all_permissions = session.query(Permission).all()
|
||||||
|
admin_role.permissions = all_permissions
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
# Buscar usuário admin e atribuir role de administrador
|
||||||
|
admin_user = session.query(Usuario).filter_by(username="admin").first()
|
||||||
|
if admin_user:
|
||||||
|
if admin_role not in admin_user.roles:
|
||||||
|
admin_user.roles = [admin_role] # Substituir roles existentes
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
# Mapear permissões para outros roles
|
||||||
|
for role in session.query(Role).filter(Role.nome != "Administrador").all():
|
||||||
|
# Militante Básico
|
||||||
|
if role.nivel == Role.MILITANTE_BASICO:
|
||||||
|
role.permissions = [
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first()
|
||||||
|
]
|
||||||
|
|
||||||
|
# Secretário de Célula
|
||||||
|
elif role.nivel == Role.SECRETARIO_CELULA:
|
||||||
|
role.permissions = [
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.REGISTER_CELL_PAYMENT).first()
|
||||||
|
]
|
||||||
|
|
||||||
|
# Membro de Setor
|
||||||
|
elif role.nivel == Role.MEMBRO_SETOR:
|
||||||
|
role.permissions = [
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.REGISTER_SECTOR_PAYMENT).first()
|
||||||
|
]
|
||||||
|
|
||||||
|
# Secretário de Setor
|
||||||
|
elif role.nivel == Role.SECRETARIO_SETOR:
|
||||||
|
role.permissions = [
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.REGISTER_SECTOR_PAYMENT).first()
|
||||||
|
]
|
||||||
|
|
||||||
|
# Membro de CR
|
||||||
|
elif role.nivel == Role.MEMBRO_CR:
|
||||||
|
role.permissions = [
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_CR_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.REGISTER_CR_PAYMENT).first()
|
||||||
|
]
|
||||||
|
|
||||||
|
# Secretário de CR
|
||||||
|
elif role.nivel == Role.SECRETARIO_CR:
|
||||||
|
role.permissions = [
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_CR_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_CR_SECTORS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.CREATE_CR_SECTOR).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.REGISTER_CR_PAYMENT).first()
|
||||||
|
]
|
||||||
|
|
||||||
|
# Membro do CC
|
||||||
|
elif role.nivel == Role.MEMBRO_CC:
|
||||||
|
role.permissions = [
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_CR_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_CR_SECTORS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.CREATE_CR_SECTOR).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_CC_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.REGISTER_CC_PAYMENT).first()
|
||||||
|
]
|
||||||
|
|
||||||
|
# Secretário Geral
|
||||||
|
elif role.nivel == Role.SECRETARIO_GERAL:
|
||||||
|
role.permissions = [
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_CR_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_CR_SECTORS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.CREATE_CR_SECTOR).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_CC_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_CC_CRS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.CREATE_CC_CR).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.REGISTER_CC_PAYMENT).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.SYSTEM_CONFIG).first()
|
||||||
|
]
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Erro ao inicializar RBAC: {e}")
|
||||||
|
session.rollback()
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
58
init_system.py
Normal file
58
init_system.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
from create_admin import create_admin
|
||||||
|
from create_test_users import create_test_users
|
||||||
|
from functions.database import get_db_connection, Usuario
|
||||||
|
from functions.rbac import Role
|
||||||
|
|
||||||
|
def init_system():
|
||||||
|
print("=== Inicializando Sistema ===")
|
||||||
|
|
||||||
|
# Criar admin
|
||||||
|
print("\nCriando usuário admin...")
|
||||||
|
create_admin()
|
||||||
|
|
||||||
|
# Criar usuários de teste
|
||||||
|
print("\nCriando usuários de teste...")
|
||||||
|
create_test_users()
|
||||||
|
|
||||||
|
# Verificar configuração
|
||||||
|
print("\n=== Verificando Configuração ===")
|
||||||
|
session = get_db_connection()
|
||||||
|
try:
|
||||||
|
# Verificar admin
|
||||||
|
admin = session.query(Usuario).filter_by(username='admin').first()
|
||||||
|
if admin:
|
||||||
|
print("Admin: OK")
|
||||||
|
print(f"OTP configurado: {'Sim' if admin.otp_secret else 'Não'}")
|
||||||
|
else:
|
||||||
|
print("Admin: FALHOU")
|
||||||
|
|
||||||
|
# Verificar usuários de teste
|
||||||
|
test_users = ['aligner', 'tester', 'deployer']
|
||||||
|
for username in test_users:
|
||||||
|
user = session.query(Usuario).filter_by(username=username).first()
|
||||||
|
if user:
|
||||||
|
print(f"{username}: OK")
|
||||||
|
print(f"OTP configurado: {'Sim' if user.otp_secret else 'Não'}")
|
||||||
|
else:
|
||||||
|
print(f"{username}: FALHOU")
|
||||||
|
|
||||||
|
print("\n=== Instruções ===")
|
||||||
|
print("1. Use o aplicativo autenticador para configurar o OTP de cada usuário")
|
||||||
|
print("2. Faça login com cada usuário para testar")
|
||||||
|
print("3. Altere a senha no primeiro login")
|
||||||
|
print("\nCredenciais:")
|
||||||
|
print("Admin:")
|
||||||
|
print(" Usuário: admin")
|
||||||
|
print(" Senha: admin123")
|
||||||
|
print("\nUsuários de teste:")
|
||||||
|
print(" Usuário: aligner, tester, deployer")
|
||||||
|
print(" Senha: Test123!@#")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Erro ao verificar configuração: {str(e)}")
|
||||||
|
session.rollback()
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
init_system()
|
||||||
64
migrations/versions/add_responsaveis_financas_imprensa.py
Normal file
64
migrations/versions/add_responsaveis_financas_imprensa.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
"""add_responsaveis_financas_imprensa
|
||||||
|
|
||||||
|
Revision ID: add_responsaveis_financas_imprensa
|
||||||
|
Revises: add_aspirante_fields
|
||||||
|
Create Date: 2024-03-19 10:00:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'add_responsaveis_financas_imprensa'
|
||||||
|
down_revision = 'add_aspirante_fields'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# Adicionar colunas na tabela celulas
|
||||||
|
op.add_column('celulas', sa.Column('responsavel_financas_id', sa.Integer(), nullable=True))
|
||||||
|
op.add_column('celulas', sa.Column('responsavel_imprensa_id', sa.Integer(), nullable=True))
|
||||||
|
op.create_foreign_key('fk_celulas_responsavel_financas', 'celulas', 'militantes', ['responsavel_financas_id'], ['id'])
|
||||||
|
op.create_foreign_key('fk_celulas_responsavel_imprensa', 'celulas', 'militantes', ['responsavel_imprensa_id'], ['id'])
|
||||||
|
|
||||||
|
# Adicionar colunas na tabela setores
|
||||||
|
op.add_column('setores', sa.Column('responsavel_financas_id', sa.Integer(), nullable=True))
|
||||||
|
op.add_column('setores', sa.Column('responsavel_imprensa_id', sa.Integer(), nullable=True))
|
||||||
|
op.create_foreign_key('fk_setores_responsavel_financas', 'setores', 'militantes', ['responsavel_financas_id'], ['id'])
|
||||||
|
op.create_foreign_key('fk_setores_responsavel_imprensa', 'setores', 'militantes', ['responsavel_imprensa_id'], ['id'])
|
||||||
|
|
||||||
|
# Adicionar colunas na tabela crs
|
||||||
|
op.add_column('crs', sa.Column('responsavel_financas_id', sa.Integer(), nullable=True))
|
||||||
|
op.add_column('crs', sa.Column('responsavel_imprensa_id', sa.Integer(), nullable=True))
|
||||||
|
op.create_foreign_key('fk_crs_responsavel_financas', 'crs', 'militantes', ['responsavel_financas_id'], ['id'])
|
||||||
|
op.create_foreign_key('fk_crs_responsavel_imprensa', 'crs', 'militantes', ['responsavel_imprensa_id'], ['id'])
|
||||||
|
|
||||||
|
# Adicionar colunas na tabela ccs
|
||||||
|
op.add_column('ccs', sa.Column('responsavel_financas_id', sa.Integer(), nullable=True))
|
||||||
|
op.add_column('ccs', sa.Column('responsavel_imprensa_id', sa.Integer(), nullable=True))
|
||||||
|
op.create_foreign_key('fk_ccs_responsavel_financas', 'ccs', 'militantes', ['responsavel_financas_id'], ['id'])
|
||||||
|
op.create_foreign_key('fk_ccs_responsavel_imprensa', 'ccs', 'militantes', ['responsavel_imprensa_id'], ['id'])
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# Remover foreign keys
|
||||||
|
op.drop_constraint('fk_celulas_responsavel_financas', 'celulas', type_='foreignkey')
|
||||||
|
op.drop_constraint('fk_celulas_responsavel_imprensa', 'celulas', type_='foreignkey')
|
||||||
|
op.drop_constraint('fk_setores_responsavel_financas', 'setores', type_='foreignkey')
|
||||||
|
op.drop_constraint('fk_setores_responsavel_imprensa', 'setores', type_='foreignkey')
|
||||||
|
op.drop_constraint('fk_crs_responsavel_financas', 'crs', type_='foreignkey')
|
||||||
|
op.drop_constraint('fk_crs_responsavel_imprensa', 'crs', type_='foreignkey')
|
||||||
|
op.drop_constraint('fk_ccs_responsavel_financas', 'ccs', type_='foreignkey')
|
||||||
|
op.drop_constraint('fk_ccs_responsavel_imprensa', 'ccs', type_='foreignkey')
|
||||||
|
|
||||||
|
# Remover colunas
|
||||||
|
op.drop_column('celulas', 'responsavel_financas_id')
|
||||||
|
op.drop_column('celulas', 'responsavel_imprensa_id')
|
||||||
|
op.drop_column('setores', 'responsavel_financas_id')
|
||||||
|
op.drop_column('setores', 'responsavel_imprensa_id')
|
||||||
|
op.drop_column('crs', 'responsavel_financas_id')
|
||||||
|
op.drop_column('crs', 'responsavel_imprensa_id')
|
||||||
|
op.drop_column('ccs', 'responsavel_financas_id')
|
||||||
|
op.drop_column('ccs', 'responsavel_imprensa_id')
|
||||||
23
models.py
Normal file
23
models.py
Normal 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
|
||||||
@@ -1,19 +1,17 @@
|
|||||||
black==24.10.0
|
Flask==3.0.2
|
||||||
blinker==1.9.0
|
Flask-SQLAlchemy==3.1.1
|
||||||
click==8.1.7
|
Flask-Login==0.6.3
|
||||||
Flask==3.1.0
|
Flask-WTF==1.2.1
|
||||||
greenlet==3.1.1
|
Flask-Mail==0.9.1
|
||||||
importlib_metadata==8.5.0
|
SQLAlchemy==2.0.27
|
||||||
itsdangerous==2.2.0
|
Werkzeug==3.0.1
|
||||||
Jinja2==3.1.4
|
python-dotenv==1.0.1
|
||||||
MarkupSafe==3.0.2
|
pyotp==2.9.0
|
||||||
mypy-extensions==1.0.0
|
qrcode==7.4.2
|
||||||
mysql-connector-python==9.1.0
|
Pillow==10.2.0
|
||||||
packaging==24.2
|
email-validator==2.1.0.post1
|
||||||
pathspec==0.12.1
|
cryptography==42.0.2
|
||||||
platformdirs==4.3.6
|
bcrypt==4.1.2
|
||||||
SQLAlchemy==2.0.36
|
Bootstrap-Flask==2.3.3
|
||||||
tomli==2.2.1
|
flask-bootstrap5==0.1.dev1
|
||||||
typing_extensions==4.12.2
|
PyJWT==2.8.0
|
||||||
Werkzeug==3.1.3
|
|
||||||
zipp==3.21.0
|
|
||||||
|
|||||||
@@ -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
27
scripts/init_db.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
from functions.database import Role, Permissao, RolePermissao, Base, engine
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
def init_db():
|
||||||
|
Base.metadata.create_all(engine)
|
||||||
|
|
||||||
|
with Session(engine) as session:
|
||||||
|
# Criar roles
|
||||||
|
admin = Role(nome='Administrador', nivel=1)
|
||||||
|
coord = Role(nome='Coordenador', nivel=2)
|
||||||
|
milit = Role(nome='Militante', nivel=3)
|
||||||
|
|
||||||
|
# Criar permissões
|
||||||
|
perm_admin = Permissao(nome='admin', descricao='Acesso total')
|
||||||
|
perm_militantes = Permissao(nome='ver_militantes', descricao='Ver militantes')
|
||||||
|
# ... outras permissões ...
|
||||||
|
|
||||||
|
session.add_all([admin, coord, milit, perm_admin, perm_militantes])
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
# Associar permissões aos roles
|
||||||
|
session.add(RolePermissao(role=admin, permissao=perm_admin))
|
||||||
|
session.add(RolePermissao(role=coord, permissao=perm_militantes))
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
init_db()
|
||||||
32
seed.py
Normal file
32
seed.py
Normal 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
305
seed_data.py
Normal 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
18
setup.py
Normal 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
66
sql/migrate_db.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import os
|
||||||
|
import sqlite3
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Adiciona o diretório raiz ao PYTHONPATH
|
||||||
|
root_dir = str(Path(__file__).parent.parent)
|
||||||
|
sys.path.append(root_dir)
|
||||||
|
|
||||||
|
from functions.base import Base, engine
|
||||||
|
from functions.database import init_database
|
||||||
|
from functions.rbac import init_rbac
|
||||||
|
|
||||||
|
def execute_sql_file(file_path):
|
||||||
|
"""Executa um arquivo SQL"""
|
||||||
|
print(f"Executando arquivo {file_path}...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(file_path, 'r') as sql_file:
|
||||||
|
sql_commands = sql_file.read().split(';')
|
||||||
|
|
||||||
|
conn = sqlite3.connect('database.db')
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
for command in sql_commands:
|
||||||
|
command = command.strip()
|
||||||
|
if command:
|
||||||
|
try:
|
||||||
|
cursor.execute(command)
|
||||||
|
except sqlite3.OperationalError as e:
|
||||||
|
if "already exists" in str(e):
|
||||||
|
print(f"Aviso: {str(e)}")
|
||||||
|
else:
|
||||||
|
raise e
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
print(f"Arquivo {file_path} executado com sucesso!")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Erro ao executar {file_path}: {str(e)}")
|
||||||
|
raise e
|
||||||
|
|
||||||
|
def migrate_database():
|
||||||
|
"""Executa a migração do banco de dados"""
|
||||||
|
print("Inicializando banco de dados...")
|
||||||
|
|
||||||
|
# Criar todas as tabelas
|
||||||
|
Base.metadata.create_all(engine)
|
||||||
|
|
||||||
|
# Executar scripts SQL
|
||||||
|
sql_dir = Path(__file__).parent
|
||||||
|
rbac_tables_sql = sql_dir / 'rbac_tables.sql'
|
||||||
|
|
||||||
|
if rbac_tables_sql.exists():
|
||||||
|
execute_sql_file(rbac_tables_sql)
|
||||||
|
|
||||||
|
# Inicializar RBAC
|
||||||
|
init_rbac()
|
||||||
|
|
||||||
|
# Inicializar banco de dados
|
||||||
|
init_database()
|
||||||
|
|
||||||
|
print("Migração concluída com sucesso!")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
migrate_database()
|
||||||
47
sql/migrate_rbac.py
Normal file
47
sql/migrate_rbac.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
from functions.database import get_db_connection, Usuario
|
||||||
|
from functions.rbac import Role, Permission
|
||||||
|
|
||||||
|
def migrate_existing_users():
|
||||||
|
"""Migra os usuários existentes para o novo sistema RBAC"""
|
||||||
|
session = get_db_connection()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Buscar todos os usuários
|
||||||
|
usuarios = session.query(Usuario).all()
|
||||||
|
|
||||||
|
# Buscar ou criar role de administrador
|
||||||
|
admin_role = session.query(Role).filter_by(nome="Administrador").first()
|
||||||
|
if not admin_role:
|
||||||
|
admin_role = Role(nome="Administrador", nivel=Role.SECRETARIO_GERAL)
|
||||||
|
session.add(admin_role)
|
||||||
|
|
||||||
|
# Buscar ou criar role de militante básico
|
||||||
|
militante_role = session.query(Role).filter_by(nome="Militante Básico").first()
|
||||||
|
if not militante_role:
|
||||||
|
militante_role = Role(nome="Militante Básico", nivel=Role.MILITANTE_BASICO)
|
||||||
|
session.add(militante_role)
|
||||||
|
|
||||||
|
# Atualizar usuários
|
||||||
|
for usuario in usuarios:
|
||||||
|
# Se o usuário já tem roles, pular
|
||||||
|
if usuario.roles:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Atribuir role com base no is_admin
|
||||||
|
if usuario.is_admin:
|
||||||
|
usuario.roles.append(admin_role)
|
||||||
|
else:
|
||||||
|
usuario.roles.append(militante_role)
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
print("Migração de usuários concluída com sucesso!")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
session.rollback()
|
||||||
|
print(f"Erro durante a migração de usuários: {str(e)}")
|
||||||
|
raise e
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
migrate_existing_users()
|
||||||
152
sql/rbac_tables.sql
Normal file
152
sql/rbac_tables.sql
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
-- Tabela de roles
|
||||||
|
CREATE TABLE IF NOT EXISTS roles (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
nome VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
nivel INTEGER NOT NULL,
|
||||||
|
descricao TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Tabela de permissões
|
||||||
|
CREATE TABLE IF NOT EXISTS permissions (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
nome VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
descricao TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Tabela de mapeamento Role-Permission
|
||||||
|
CREATE TABLE IF NOT EXISTS role_permissions (
|
||||||
|
role_id INTEGER NOT NULL,
|
||||||
|
permission_id INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (role_id, permission_id),
|
||||||
|
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Tabela de mapeamento User-Role
|
||||||
|
CREATE TABLE IF NOT EXISTS user_roles (
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
role_id INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (user_id, role_id),
|
||||||
|
FOREIGN KEY (user_id) REFERENCES usuarios(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Inserir roles básicas
|
||||||
|
INSERT OR IGNORE INTO roles (nome, nivel, descricao) VALUES
|
||||||
|
('Militante Básico', 1, 'Militante com permissões básicas'),
|
||||||
|
('Secretário de Célula', 2, 'Responsável por uma célula'),
|
||||||
|
('Membro de Setor', 3, 'Membro de um setor'),
|
||||||
|
('Secretário de Setor', 4, 'Responsável por um setor'),
|
||||||
|
('Membro de CR', 5, 'Membro de um Comitê Regional'),
|
||||||
|
('Secretário de CR', 6, 'Responsável por um Comitê Regional'),
|
||||||
|
('Membro do CC', 7, 'Membro do Comitê Central'),
|
||||||
|
('Secretário Geral', 8, 'Secretário Geral ou de Organização do CC');
|
||||||
|
|
||||||
|
-- Inserir permissões básicas
|
||||||
|
INSERT OR IGNORE INTO permissions (nome, descricao) VALUES
|
||||||
|
-- Permissões básicas
|
||||||
|
('view_own_data', 'Visualizar próprios dados'),
|
||||||
|
('edit_own_data', 'Editar próprios dados'),
|
||||||
|
('view_cell_data', 'Visualizar dados da célula'),
|
||||||
|
('create_militant', 'Criar novos militantes'),
|
||||||
|
|
||||||
|
-- Permissões de célula
|
||||||
|
('manage_cell_members', 'Gerenciar membros da célula'),
|
||||||
|
('create_cell_member', 'Criar membros na célula'),
|
||||||
|
('view_cell_reports', 'Visualizar relatórios da célula'),
|
||||||
|
|
||||||
|
-- Permissões de setor
|
||||||
|
('manage_sector_cells', 'Gerenciar células do setor'),
|
||||||
|
('create_sector_cell', 'Criar células no setor'),
|
||||||
|
('view_sector_reports', 'Visualizar relatórios do setor'),
|
||||||
|
|
||||||
|
-- Permissões de CR
|
||||||
|
('manage_cr_sectors', 'Gerenciar setores do CR'),
|
||||||
|
('create_cr_sector', 'Criar setores no CR'),
|
||||||
|
('view_cr_reports', 'Visualizar relatórios do CR'),
|
||||||
|
|
||||||
|
-- Permissões de CC
|
||||||
|
('manage_cc_crs', 'Gerenciar CRs'),
|
||||||
|
('create_cc_cr', 'Criar CRs'),
|
||||||
|
('view_cc_reports', 'Visualizar relatórios nacionais'),
|
||||||
|
('system_config', 'Configurar sistema');
|
||||||
|
|
||||||
|
-- Mapear permissões para roles
|
||||||
|
-- Militante Básico
|
||||||
|
INSERT OR IGNORE INTO role_permissions (role_id, permission_id)
|
||||||
|
SELECT r.id, p.id
|
||||||
|
FROM roles r, permissions p
|
||||||
|
WHERE r.nome = 'Militante Básico'
|
||||||
|
AND p.nome IN ('view_own_data', 'edit_own_data', 'view_cell_data');
|
||||||
|
|
||||||
|
-- Secretário de Célula
|
||||||
|
INSERT OR IGNORE INTO role_permissions (role_id, permission_id)
|
||||||
|
SELECT r.id, p.id
|
||||||
|
FROM roles r, permissions p
|
||||||
|
WHERE r.nome = 'Secretário de Célula'
|
||||||
|
AND p.nome IN ('view_own_data', 'edit_own_data', 'view_cell_data',
|
||||||
|
'manage_cell_members', 'create_cell_member', 'view_cell_reports',
|
||||||
|
'create_militant');
|
||||||
|
|
||||||
|
-- Membro de Setor
|
||||||
|
INSERT OR IGNORE INTO role_permissions (role_id, permission_id)
|
||||||
|
SELECT r.id, p.id
|
||||||
|
FROM roles r, permissions p
|
||||||
|
WHERE r.nome = 'Membro de Setor'
|
||||||
|
AND p.nome IN ('view_own_data', 'edit_own_data', 'view_cell_data',
|
||||||
|
'manage_cell_members', 'create_cell_member', 'view_cell_reports',
|
||||||
|
'view_sector_reports', 'create_militant');
|
||||||
|
|
||||||
|
-- Secretário de Setor
|
||||||
|
INSERT OR IGNORE INTO role_permissions (role_id, permission_id)
|
||||||
|
SELECT r.id, p.id
|
||||||
|
FROM roles r, permissions p
|
||||||
|
WHERE r.nome = 'Secretário de Setor'
|
||||||
|
AND p.nome IN ('view_own_data', 'edit_own_data', 'view_cell_data',
|
||||||
|
'manage_cell_members', 'create_cell_member', 'view_cell_reports',
|
||||||
|
'view_sector_reports', 'manage_sector_cells', 'create_sector_cell',
|
||||||
|
'create_militant');
|
||||||
|
|
||||||
|
-- Membro de CR
|
||||||
|
INSERT OR IGNORE INTO role_permissions (role_id, permission_id)
|
||||||
|
SELECT r.id, p.id
|
||||||
|
FROM roles r, permissions p
|
||||||
|
WHERE r.nome = 'Membro de CR'
|
||||||
|
AND p.nome IN ('view_own_data', 'edit_own_data', 'view_cell_data',
|
||||||
|
'manage_cell_members', 'create_cell_member', 'view_cell_reports',
|
||||||
|
'view_sector_reports', 'manage_sector_cells', 'create_sector_cell',
|
||||||
|
'view_cr_reports', 'create_militant');
|
||||||
|
|
||||||
|
-- Secretário de CR
|
||||||
|
INSERT OR IGNORE INTO role_permissions (role_id, permission_id)
|
||||||
|
SELECT r.id, p.id
|
||||||
|
FROM roles r, permissions p
|
||||||
|
WHERE r.nome = 'Secretário de CR'
|
||||||
|
AND p.nome IN ('view_own_data', 'edit_own_data', 'view_cell_data',
|
||||||
|
'manage_cell_members', 'create_cell_member', 'view_cell_reports',
|
||||||
|
'view_sector_reports', 'manage_sector_cells', 'create_sector_cell',
|
||||||
|
'view_cr_reports', 'manage_cr_sectors', 'create_cr_sector',
|
||||||
|
'create_militant');
|
||||||
|
|
||||||
|
-- Membro do CC
|
||||||
|
INSERT OR IGNORE INTO role_permissions (role_id, permission_id)
|
||||||
|
SELECT r.id, p.id
|
||||||
|
FROM roles r, permissions p
|
||||||
|
WHERE r.nome = 'Membro do CC'
|
||||||
|
AND p.nome IN ('view_own_data', 'edit_own_data', 'view_cell_data',
|
||||||
|
'manage_cell_members', 'create_cell_member', 'view_cell_reports',
|
||||||
|
'view_sector_reports', 'manage_sector_cells', 'create_sector_cell',
|
||||||
|
'view_cr_reports', 'manage_cr_sectors', 'create_cr_sector',
|
||||||
|
'view_cc_reports', 'create_militant');
|
||||||
|
|
||||||
|
-- Secretário Geral
|
||||||
|
INSERT OR IGNORE INTO role_permissions (role_id, permission_id)
|
||||||
|
SELECT r.id, p.id
|
||||||
|
FROM roles r, permissions p
|
||||||
|
WHERE r.nome = 'Secretário Geral'
|
||||||
|
AND p.nome IN ('view_own_data', 'edit_own_data', 'view_cell_data',
|
||||||
|
'manage_cell_members', 'create_cell_member', 'view_cell_reports',
|
||||||
|
'view_sector_reports', 'manage_sector_cells', 'create_sector_cell',
|
||||||
|
'view_cr_reports', 'manage_cr_sectors', 'create_cr_sector',
|
||||||
|
'view_cc_reports', 'manage_cc_crs', 'create_cc_cr',
|
||||||
|
'system_config', 'create_militant');
|
||||||
563
static/css/components.css
Normal file
563
static/css/components.css
Normal 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
450
static/css/style.css
Normal file
@@ -0,0 +1,450 @@
|
|||||||
|
:root {
|
||||||
|
--primary-color: #E8000C;
|
||||||
|
--primary-dark: #B5000A;
|
||||||
|
--primary-light: #FF1A1A;
|
||||||
|
--secondary-color: #2D2D2D;
|
||||||
|
--secondary-light: #404040;
|
||||||
|
--secondary-dark: #1A1A1A;
|
||||||
|
--background-color: #FFFFFF;
|
||||||
|
--text-color: #2D2D2D;
|
||||||
|
--text-light: #FFFFFF;
|
||||||
|
--hover-color: #FF1A1A;
|
||||||
|
--disabled-color: #999999;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--background-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
font-family: 'Roboto', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
background: linear-gradient(to right, var(--secondary-dark), var(--secondary-color)) !important;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||||
|
border-bottom: 3px solid var(--primary-color);
|
||||||
|
padding: 0.8rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-light) !important;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand img {
|
||||||
|
height: 40px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-logo {
|
||||||
|
height: 32px;
|
||||||
|
width: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-logo {
|
||||||
|
height: 80px;
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
color: rgba(255, 255, 255, 0.85) !important;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link:hover {
|
||||||
|
color: var(--text-light) !important;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link i {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 8px 24px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .card-body {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cards de estatísticas */
|
||||||
|
.card.bg-primary {
|
||||||
|
background: linear-gradient(135deg, #0d6efd, #0a58ca) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card.bg-success {
|
||||||
|
background: linear-gradient(135deg, #198754, #146c43) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card.bg-info {
|
||||||
|
background: linear-gradient(135deg, #0dcaf0, #0aa2c0) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card.bg-warning {
|
||||||
|
background: linear-gradient(135deg, #ffc107, #cc9a06) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .fs-1 {
|
||||||
|
opacity: 0.8;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover .fs-1 {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h2 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h6 {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 400;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card a {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card a:hover {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cards de listagem */
|
||||||
|
.card .card-header {
|
||||||
|
background: linear-gradient(to right, var(--secondary-dark), var(--secondary-color));
|
||||||
|
color: var(--text-light);
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .card-header h5 {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-left: none;
|
||||||
|
border-right: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item:hover {
|
||||||
|
background-color: rgba(0,0,0,0.02);
|
||||||
|
transform: translateX(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item h6 {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item small {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
padding: 0.5em 0.8em;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
border: none;
|
||||||
|
padding: 0.5rem 1.5rem;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: var(--hover-color);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 2px 5px rgba(232, 0, 12, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:disabled {
|
||||||
|
background-color: var(--disabled-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table thead th {
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
color: var(--text-light);
|
||||||
|
font-weight: 500;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table tbody tr:hover {
|
||||||
|
background-color: rgba(232, 0, 12, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(232, 0, 12, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alert styles */
|
||||||
|
.alert {
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
opacity: 1 !important;
|
||||||
|
background-color: rgba(255, 255, 255, 0.98) !important;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert i {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success {
|
||||||
|
color: #155724 !important;
|
||||||
|
background-color: #d4edda !important;
|
||||||
|
border-left: 4px solid #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-danger {
|
||||||
|
color: #721c24 !important;
|
||||||
|
background-color: #f8d7da !important;
|
||||||
|
border-left: 4px solid #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-warning {
|
||||||
|
color: #856404 !important;
|
||||||
|
background-color: #fff3cd !important;
|
||||||
|
border-left: 4px solid #ffc107;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-info {
|
||||||
|
color: #0c5460 !important;
|
||||||
|
background-color: #d1ecf1 !important;
|
||||||
|
border-left: 4px solid #17a2b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animações para feedback */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translate(-50%, -20px); }
|
||||||
|
to { opacity: 1; transform: translate(-50%, 0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsividade */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.navbar-brand {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-logo {
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
margin: 1rem;
|
||||||
|
width: calc(100% - 2rem);
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu {
|
||||||
|
background: linear-gradient(to bottom right, var(--secondary-dark), var(--secondary-color));
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
||||||
|
padding: 0.5rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item {
|
||||||
|
color: rgba(255, 255, 255, 0.85) !important;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 400;
|
||||||
|
padding: 0.6rem 1rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item:hover {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: var(--text-light) !important;
|
||||||
|
transform: translateX(3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-divider {
|
||||||
|
border-top: 1px solid var(--secondary-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilo para o menu mobile */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.navbar-collapse {
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0 0 10px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand img {
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Data styles */
|
||||||
|
.date-header {
|
||||||
|
padding: 1.5rem 0;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
border-bottom: 1px solid rgba(0,0,0,0.1);
|
||||||
|
color: var(--secondary-color);
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navbar styles */
|
||||||
|
.navbar-nav .nav-link {
|
||||||
|
color: rgba(255, 255, 255, 0.85) !important;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-nav .nav-link:hover {
|
||||||
|
color: var(--primary-color) !important;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-nav .dropdown-menu {
|
||||||
|
background: linear-gradient(to bottom right, var(--secondary-dark), var(--secondary-color));
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
||||||
|
padding: 0.5rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item {
|
||||||
|
color: rgba(255, 255, 255, 0.85) !important;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 400;
|
||||||
|
padding: 0.6rem 1rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item:hover {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: var(--text-light) !important;
|
||||||
|
transform: translateX(3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Data styles */
|
||||||
|
.date-header {
|
||||||
|
padding: 1.5rem 0;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
border-bottom: 1px solid rgba(0,0,0,0.1);
|
||||||
|
color: var(--secondary-color);
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.date-header {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-collapse {
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0 0 10px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand img {
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-header {
|
||||||
|
background: linear-gradient(to right, var(--background-color), rgba(232, 0, 12, 0.05));
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-header h2 {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-header h4 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: var(--secondary-color);
|
||||||
|
opacity: 0.8;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
background: linear-gradient(to right, var(--secondary-dark), var(--secondary-color));
|
||||||
|
color: var(--text-light);
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item-action {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item-action:hover {
|
||||||
|
transform: translateX(5px);
|
||||||
|
background-color: rgba(232, 0, 12, 0.05);
|
||||||
|
}
|
||||||
BIN
static/img/logo001-alpha.png
Normal file
BIN
static/img/logo001-alpha.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
BIN
static/img/logo001.png
Normal file
BIN
static/img/logo001.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
BIN
static/img/logo002-alpha.png
Normal file
BIN
static/img/logo002-alpha.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
BIN
static/img/logoComunaTec.jpg
Normal file
BIN
static/img/logoComunaTec.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
127
static/js/cotas.js
Normal file
127
static/js/cotas.js
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
console.log('Carregando script cotas.js...');
|
||||||
|
|
||||||
|
// Configuração do modal de edição
|
||||||
|
const modalEditarCota = document.getElementById('modalEditarCota');
|
||||||
|
if (modalEditarCota) {
|
||||||
|
modalEditarCota.addEventListener('show.bs.modal', function(event) {
|
||||||
|
console.log('Modal de edição sendo exibido');
|
||||||
|
const button = event.relatedTarget;
|
||||||
|
|
||||||
|
if (!button) {
|
||||||
|
console.error('Botão não encontrado!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cotaId = button.getAttribute('data-cota-id');
|
||||||
|
console.log('ID da cota:', cotaId);
|
||||||
|
|
||||||
|
// Dados da cota
|
||||||
|
const dados = {
|
||||||
|
militanteId: button.getAttribute('data-cota-militante'),
|
||||||
|
militanteNome: button.closest('tr').querySelector('td').textContent.trim(),
|
||||||
|
valorAntigo: button.closest('tr').querySelector('td[data-valor_antigo]').getAttribute('data-valor_antigo'),
|
||||||
|
valorNovo: button.closest('tr').querySelector('td[data-valor_novo]').getAttribute('data-valor_novo'),
|
||||||
|
dataAlteracao: button.getAttribute('data-cota-data-alteracao'),
|
||||||
|
dataVencimento: button.getAttribute('data-cota-data-vencimento'),
|
||||||
|
pago: button.getAttribute('data-cota-pago') === 'true'
|
||||||
|
};
|
||||||
|
console.log('Dados da cota:', dados);
|
||||||
|
|
||||||
|
// Preencher campos
|
||||||
|
document.getElementById('editMilitante').value = dados.militanteId;
|
||||||
|
document.getElementById('editMilitanteNome').value = dados.militanteNome;
|
||||||
|
document.getElementById('editValorAntigo').value = dados.valorAntigo;
|
||||||
|
document.getElementById('editValorNovo').value = dados.valorNovo;
|
||||||
|
document.getElementById('editDataAlteracao').value = dados.dataAlteracao;
|
||||||
|
document.getElementById('editDataVencimento').value = dados.dataVencimento;
|
||||||
|
document.getElementById('editPago').checked = dados.pago;
|
||||||
|
|
||||||
|
// Configurar formulário
|
||||||
|
const form = document.getElementById('formEditarCota');
|
||||||
|
if (form) {
|
||||||
|
form.action = `/cotas/editar/${cotaId}`;
|
||||||
|
console.log('Action do formulário:', form.action);
|
||||||
|
|
||||||
|
// Remover listeners antigos para evitar duplicação
|
||||||
|
const newForm = form.cloneNode(true);
|
||||||
|
form.parentNode.replaceChild(newForm, form);
|
||||||
|
|
||||||
|
// Adicionar listener para o submit do formulário
|
||||||
|
newForm.addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
console.log('Formulário submetido');
|
||||||
|
|
||||||
|
// Criar FormData com os dados do formulário
|
||||||
|
const formData = new FormData(this);
|
||||||
|
|
||||||
|
// Adicionar campo pago com o valor correto
|
||||||
|
const isPago = document.getElementById('editPago').checked;
|
||||||
|
formData.set('pago', isPago ? 'true' : 'false');
|
||||||
|
|
||||||
|
// Log dos dados sendo enviados
|
||||||
|
console.log('Dados do formulário:');
|
||||||
|
for (let [key, value] of formData.entries()) {
|
||||||
|
console.log(key + ': ' + value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enviar requisição
|
||||||
|
fetch(this.action, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
console.log('Status da resposta:', response.status);
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
console.log('Resposta:', data);
|
||||||
|
if (data.status === 'success') {
|
||||||
|
// Fechar modal
|
||||||
|
const modal = bootstrap.Modal.getInstance(modalEditarCota);
|
||||||
|
modal.hide();
|
||||||
|
|
||||||
|
// Recarregar página
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Erro ao atualizar cota: ' + data.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Erro:', error);
|
||||||
|
alert('Erro ao atualizar cota. Por favor, tente novamente.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configuração do modal de exclusão
|
||||||
|
const deleteModal = document.getElementById('deleteModal');
|
||||||
|
if (deleteModal) {
|
||||||
|
deleteModal.addEventListener('show.bs.modal', function(event) {
|
||||||
|
console.log('Modal de exclusão sendo exibido');
|
||||||
|
const button = event.relatedTarget;
|
||||||
|
|
||||||
|
if (!button) {
|
||||||
|
console.error('Botão não encontrado!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cotaId = button.getAttribute('data-cota-id');
|
||||||
|
const cotaInfo = button.getAttribute('data-cota-info');
|
||||||
|
console.log('ID da cota:', cotaId);
|
||||||
|
console.log('Info da cota:', cotaInfo);
|
||||||
|
|
||||||
|
// Atualizar texto do modal
|
||||||
|
document.getElementById('cotaInfo').textContent = cotaInfo;
|
||||||
|
|
||||||
|
// Configurar formulário de exclusão
|
||||||
|
const form = document.getElementById('deleteForm');
|
||||||
|
if (form) {
|
||||||
|
form.action = `/cotas/excluir/${cotaId}`;
|
||||||
|
console.log('Action do formulário:', form.action);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
146
static/js/forms.js
Normal file
146
static/js/forms.js
Normal 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
11
static/js/home.js
Normal 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
145
static/js/main.js
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
// Máscaras para campos de formulário
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Máscara para CPF
|
||||||
|
const cpfInputs = document.querySelectorAll('input[name="cpf"]');
|
||||||
|
cpfInputs.forEach(input => {
|
||||||
|
input.addEventListener('input', function(e) {
|
||||||
|
let value = e.target.value.replace(/\D/g, '');
|
||||||
|
if (value.length <= 11) {
|
||||||
|
value = value.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, "$1.$2.$3-$4");
|
||||||
|
e.target.value = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Máscara para telefone
|
||||||
|
const phoneInputs = document.querySelectorAll('input[name="telefone"]');
|
||||||
|
phoneInputs.forEach(input => {
|
||||||
|
input.addEventListener('input', function(e) {
|
||||||
|
let value = e.target.value.replace(/\D/g, '');
|
||||||
|
if (value.length <= 11) {
|
||||||
|
if (value.length === 11) {
|
||||||
|
value = value.replace(/(\d{2})(\d{5})(\d{4})/, "($1) $2-$3");
|
||||||
|
} else {
|
||||||
|
value = value.replace(/(\d{2})(\d{4})(\d{4})/, "($1) $2-$3");
|
||||||
|
}
|
||||||
|
e.target.value = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Formatação de valores monetários
|
||||||
|
const moneyInputs = document.querySelectorAll('input[type="number"][step="0.01"]');
|
||||||
|
moneyInputs.forEach(input => {
|
||||||
|
input.addEventListener('blur', function(e) {
|
||||||
|
const value = parseFloat(e.target.value);
|
||||||
|
if (!isNaN(value)) {
|
||||||
|
e.target.value = value.toFixed(2);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Funções para tabelas
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const tables = document.querySelectorAll('.table');
|
||||||
|
tables.forEach(table => {
|
||||||
|
// Ordenação
|
||||||
|
const headers = table.querySelectorAll('th[data-sort]');
|
||||||
|
headers.forEach(header => {
|
||||||
|
header.addEventListener('click', function() {
|
||||||
|
const column = this.dataset.sort;
|
||||||
|
const asc = this.classList.toggle('sort-asc');
|
||||||
|
const tbody = table.querySelector('tbody');
|
||||||
|
const rows = Array.from(tbody.querySelectorAll('tr'));
|
||||||
|
|
||||||
|
rows.sort((a, b) => {
|
||||||
|
const aVal = a.querySelector(`td[data-${column}]`).dataset[column];
|
||||||
|
const bVal = b.querySelector(`td[data-${column}]`).dataset[column];
|
||||||
|
return asc ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal);
|
||||||
|
});
|
||||||
|
|
||||||
|
rows.forEach(row => tbody.appendChild(row));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filtro
|
||||||
|
const filterInput = document.querySelector(`#filter-${table.id}`);
|
||||||
|
if (filterInput) {
|
||||||
|
filterInput.addEventListener('input', function() {
|
||||||
|
const searchTerm = this.value.toLowerCase();
|
||||||
|
const rows = table.querySelectorAll('tbody tr');
|
||||||
|
|
||||||
|
rows.forEach(row => {
|
||||||
|
const text = row.textContent.toLowerCase();
|
||||||
|
row.style.display = text.includes(searchTerm) ? '' : 'none';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validação de formulários
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const forms = document.querySelectorAll('form');
|
||||||
|
forms.forEach(form => {
|
||||||
|
form.addEventListener('submit', function(e) {
|
||||||
|
if (!form.checkValidity()) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
// Destacar campos inválidos
|
||||||
|
const invalidInputs = form.querySelectorAll(':invalid');
|
||||||
|
invalidInputs.forEach(input => {
|
||||||
|
input.classList.add('is-invalid');
|
||||||
|
|
||||||
|
// Adicionar mensagem de erro
|
||||||
|
const feedback = document.createElement('div');
|
||||||
|
feedback.className = 'invalid-feedback';
|
||||||
|
feedback.textContent = input.validationMessage;
|
||||||
|
input.parentNode.appendChild(feedback);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
form.classList.add('was-validated');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Animações e feedback visual
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Animar cards ao carregar
|
||||||
|
const cards = document.querySelectorAll('.card');
|
||||||
|
cards.forEach((card, index) => {
|
||||||
|
card.style.opacity = '0';
|
||||||
|
card.style.transform = 'translateY(20px)';
|
||||||
|
setTimeout(() => {
|
||||||
|
card.style.transition = 'all 0.3s ease';
|
||||||
|
card.style.opacity = '1';
|
||||||
|
card.style.transform = 'translateY(0)';
|
||||||
|
}, index * 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Feedback visual para ações
|
||||||
|
const actionButtons = document.querySelectorAll('[data-action]');
|
||||||
|
actionButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
button.classList.add('animate__animated', 'animate__pulse');
|
||||||
|
setTimeout(() => {
|
||||||
|
button.classList.remove('animate__animated', 'animate__pulse');
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Confirmações de ações
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const deleteButtons = document.querySelectorAll('[data-confirm]');
|
||||||
|
deleteButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', function(e) {
|
||||||
|
if (!confirm(this.dataset.confirm)) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
742
static/js/militantes.js
Normal file
742
static/js/militantes.js
Normal 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
256
static/js/pagamentos.js
Normal 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
51
templates/alterar_senha.html
Normal file
51
templates/alterar_senha.html
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Alterar Senha{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Alterar Senha</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<form method="POST" action="{{ url_for('alterar_senha') }}">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="senha_atual" class="form-label">Senha Atual</label>
|
||||||
|
<input type="password" class="form-control" id="senha_atual" name="senha_atual" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="nova_senha" class="form-label">Nova Senha</label>
|
||||||
|
<input type="password" class="form-control" id="nova_senha" name="nova_senha" required>
|
||||||
|
<small class="text-muted">
|
||||||
|
A senha deve ter no mínimo 8 caracteres e conter letras e números.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="confirmar_senha" class="form-label">Confirmar Nova Senha</label>
|
||||||
|
<input type="password" class="form-control" id="confirmar_senha" name="confirmar_senha" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary">Alterar Senha</button>
|
||||||
|
<a href="{{ url_for('home') }}" class="btn btn-secondary">Cancelar</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -3,39 +3,625 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>{% block title %}{% endblock %} - Sistema de Gestão</title>
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||||
{{ bootstrap.load_css() }}
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
{% block navbar %}
|
||||||
<div class="container">
|
<nav class="navbar navbar-expand-lg navbar-dark">
|
||||||
<a class="navbar-brand" href="{{ url_for('home') }}">Sistema de Gestão</a>
|
<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">
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
|
{% if session.get('user_id') %}
|
||||||
<div class="collapse navbar-collapse" id="navbarNav">
|
<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">
|
<ul class="navbar-nav">
|
||||||
<li class="nav-item">
|
<li class="nav-item dropdown">
|
||||||
<a class="nav-link" href="{{ url_for('listar_militantes') }}">Militantes</a>
|
<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>
|
||||||
<li class="nav-item">
|
<li><hr class="dropdown-divider"></li>
|
||||||
<a class="nav-link" href="{{ url_for('listar_cotas') }}">Cotas</a>
|
{% endif %}
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="{{ url_for('logout') }}">
|
||||||
|
<i class="fas fa-sign-out-alt"></i>Sair
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
</ul>
|
||||||
<a class="nav-link" href="{{ url_for('listar_pagamentos') }}">Pagamentos</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="{{ url_for('listar_materiais') }}">Materiais</a>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
<div class="container mt-4">
|
<div class="page-wrapper">
|
||||||
|
<div class="container py-4">
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
</div>
|
</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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
111
templates/criar_instancia.html
Normal file
111
templates/criar_instancia.html
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Criar {{ tipo_instancia }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<h1 class="mb-4">Criar {{ tipo_instancia }}</h1>
|
||||||
|
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<form method="POST" class="needs-validation" novalidate>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="nome" class="form-label">Nome</label>
|
||||||
|
<input type="text" class="form-control" id="nome" name="nome" required>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
Por favor, insira o nome da {{ tipo_instancia }}.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if tipo_instancia != 'Célula' %}
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="instancia_superior_id" class="form-label">{{ instancia_superior }}</label>
|
||||||
|
<select class="form-select" id="instancia_superior_id" name="instancia_superior_id" required>
|
||||||
|
<option value="">Selecione uma {{ instancia_superior }}</option>
|
||||||
|
{% for superior in instancias_superiores %}
|
||||||
|
<option value="{{ superior.id }}">{{ superior.nome }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
Por favor, selecione uma {{ instancia_superior }}.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="responsavel_geral_id" class="form-label">Responsável Geral</label>
|
||||||
|
<select class="form-select" id="responsavel_geral_id" name="responsavel_geral_id" required>
|
||||||
|
<option value="">Selecione o responsável geral</option>
|
||||||
|
{% for militante in militantes %}
|
||||||
|
<option value="{{ militante.id }}">{{ militante.nome }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
Por favor, selecione o responsável geral.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="responsavel_financas_id" class="form-label">Responsável de Finanças</label>
|
||||||
|
<select class="form-select" id="responsavel_financas_id" name="responsavel_financas_id">
|
||||||
|
<option value="">Selecione o responsável de finanças</option>
|
||||||
|
{% for militante in militantes %}
|
||||||
|
<option value="{{ militante.id }}">{{ militante.nome }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="responsavel_imprensa_id" class="form-label">Responsável de Imprensa</label>
|
||||||
|
<select class="form-select" id="responsavel_imprensa_id" name="responsavel_imprensa_id">
|
||||||
|
<option value="">Selecione o responsável de imprensa</option>
|
||||||
|
{% for militante in militantes %}
|
||||||
|
<option value="{{ militante.id }}">{{ militante.nome }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between mt-4">
|
||||||
|
<button type="submit" class="btn btn-primary">Criar</button>
|
||||||
|
<a href="{{ url_for('listar_' + tipo_instancia.lower() + 's') }}" class="btn btn-secondary">Cancelar</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Validação do formulário
|
||||||
|
(function () {
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
var forms = document.querySelectorAll('.needs-validation')
|
||||||
|
|
||||||
|
Array.prototype.slice.call(forms)
|
||||||
|
.forEach(function (form) {
|
||||||
|
form.addEventListener('submit', function (event) {
|
||||||
|
if (!form.checkValidity()) {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
}
|
||||||
|
|
||||||
|
form.classList.add('was-validated')
|
||||||
|
}, false)
|
||||||
|
})
|
||||||
|
})()
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
106
templates/criar_militante.html
Normal file
106
templates/criar_militante.html
Normal 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
284
templates/dashboard.html
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Dashboard Administrativo{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="mb-4">Dashboard Administrativo</h1>
|
||||||
|
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">Gerenciamento de Acessos</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Usuário</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Último Login</th>
|
||||||
|
<th>Nível</th>
|
||||||
|
<th>Responsabilidades</th>
|
||||||
|
<th>Ações</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for user in users %}
|
||||||
|
{% if current_user.has_permission('system_config') or
|
||||||
|
(current_user.has_permission('manage_cr_sectors') and user.cr_id == current_user.cr_id) or
|
||||||
|
(current_user.has_permission('manage_sector_cells') and user.setor_id == current_user.setor_id) or
|
||||||
|
(current_user.has_permission('manage_cell_members') and user.celula_id == current_user.celula_id) %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ user.id }}</td>
|
||||||
|
<td>{{ user.username }}</td>
|
||||||
|
<td>{{ user.email }}</td>
|
||||||
|
<td>
|
||||||
|
{% if user.ativo %}
|
||||||
|
<span class="badge bg-success">Ativo</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-danger">Inativo</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ user.ultimo_login.strftime('%d/%m/%Y %H:%M') if user.ultimo_login else 'Nunca' }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-info">{{ user.role }}</span>
|
||||||
|
{% if current_user.has_permission('system_config') or
|
||||||
|
(current_user.has_permission('manage_cr_sectors') and user.cr_id == current_user.cr_id) or
|
||||||
|
(current_user.has_permission('manage_sector_cells') and user.setor_id == current_user.setor_id) %}
|
||||||
|
<select class="form-select form-select-sm d-inline-block w-auto" onchange="alterarNivel({{ user.id }}, this.value)">
|
||||||
|
<option value="">Alterar Nível</option>
|
||||||
|
{% if current_user.has_permission('system_config') %}
|
||||||
|
<option value="militante_basico">Militante Básico</option>
|
||||||
|
<option value="secretario_celula">Secretário de Célula</option>
|
||||||
|
<option value="membro_setor">Membro de Setor</option>
|
||||||
|
<option value="secretario_setor">Secretário de Setor</option>
|
||||||
|
<option value="membro_cr">Membro de CR</option>
|
||||||
|
<option value="secretario_cr">Secretário de CR</option>
|
||||||
|
<option value="membro_cc">Membro do CC</option>
|
||||||
|
<option value="secretario_geral">Secretário Geral</option>
|
||||||
|
{% elif current_user.has_permission('manage_cr_sectors') %}
|
||||||
|
<option value="membro_cr">Membro de CR</option>
|
||||||
|
<option value="secretario_cr">Secretário de CR</option>
|
||||||
|
{% elif current_user.has_permission('manage_sector_cells') %}
|
||||||
|
<option value="membro_setor">Membro de Setor</option>
|
||||||
|
<option value="secretario_setor">Secretário de Setor</option>
|
||||||
|
{% endif %}
|
||||||
|
</select>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if user.militante %}
|
||||||
|
{% if user.militante.quadro_orientador %}
|
||||||
|
<span class="badge bg-primary">Quadro-Orientador</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if user.militante.aspirante %}
|
||||||
|
<span class="badge bg-warning">Aspirante</span>
|
||||||
|
<small class="text-muted">
|
||||||
|
(desde {{ user.militante.data_inicio_aspirante.strftime('%d/%m/%Y') }})
|
||||||
|
</small>
|
||||||
|
{% if user.militante.avaliacao_aspirante %}
|
||||||
|
<button type="button" class="btn btn-sm btn-info"
|
||||||
|
onclick="verAvaliacaoAspirante({{ user.id }})">
|
||||||
|
Ver Avaliação
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% if current_user.has_permission('system_config') or
|
||||||
|
(current_user.has_permission('manage_cr_sectors') and user.cr_id == current_user.cr_id) or
|
||||||
|
(current_user.has_permission('manage_sector_cells') and user.setor_id == current_user.setor_id) %}
|
||||||
|
{% if user.militante.quadro_orientador %}
|
||||||
|
<button type="button" class="btn btn-sm btn-danger"
|
||||||
|
onclick="toggleQuadroOrientador({{ user.id }}, {{ user.militante.quadro_orientador|lower }})">
|
||||||
|
Remover QO
|
||||||
|
</button>
|
||||||
|
{% else %}
|
||||||
|
<button type="button" class="btn btn-sm btn-success"
|
||||||
|
onclick="toggleQuadroOrientador({{ user.id }}, {{ user.militante.quadro_orientador|lower }})">
|
||||||
|
Tornar QO
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{% if user.militante.aspirante %}
|
||||||
|
{% if datetime.utcnow() - user.militante.data_inicio_aspirante >= timedelta(days=90) %}
|
||||||
|
{% if not user.militante.avaliacao_aspirante %}
|
||||||
|
<button type="button" class="btn btn-sm btn-primary"
|
||||||
|
onclick="avaliarAspirante({{ user.id }})">
|
||||||
|
Avaliar Aspirante
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
<button type="button" class="btn btn-sm btn-danger"
|
||||||
|
onclick="toggleAspirante({{ user.id }}, {{ user.militante.aspirante|lower }})">
|
||||||
|
Remover Aspirante
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<button type="button" class="btn btn-sm btn-warning"
|
||||||
|
onclick="toggleAspirante({{ user.id }}, {{ user.militante.aspirante|lower }})">
|
||||||
|
Tornar Aspirante
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
{% if current_user.has_permission('system_config') or
|
||||||
|
(current_user.has_permission('manage_cr_sectors') and user.cr_id == current_user.cr_id) or
|
||||||
|
(current_user.has_permission('manage_sector_cells') and user.setor_id == current_user.setor_id) or
|
||||||
|
(current_user.has_permission('manage_cell_members') and user.celula_id == current_user.celula_id) %}
|
||||||
|
<button type="button" class="btn btn-sm btn-primary"
|
||||||
|
onclick="resetOTP({{ user.id }})">
|
||||||
|
Gerar Novo OTP
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-warning"
|
||||||
|
onclick="resetPassword({{ user.id }})">
|
||||||
|
Resetar Senha
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm {% if user.ativo %}btn-danger{% else %}btn-success{% endif %}"
|
||||||
|
onclick="toggleUserStatus({{ user.id }}, {{ user.ativo|lower }})">
|
||||||
|
{% if user.ativo %}Desativar{% else %}Ativar{% endif %} Login
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function resetOTP(userId) {
|
||||||
|
if (confirm('Tem certeza que deseja gerar um novo OTP para este usuário? O OTP atual será invalidado.')) {
|
||||||
|
fetch(`/usuarios/${userId}/otp/reset`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
alert('Novo OTP gerado com sucesso!');
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Erro ao gerar novo OTP: ' + data.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
alert('Erro ao gerar novo OTP: ' + error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetPassword(userId) {
|
||||||
|
if (confirm('Tem certeza que deseja resetar a senha deste usuário? Uma nova senha será gerada e enviada por email.')) {
|
||||||
|
fetch(`/usuarios/${userId}/password/reset`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
alert('Senha resetada com sucesso! A nova senha foi enviada por email.');
|
||||||
|
} else {
|
||||||
|
alert('Erro ao resetar senha: ' + data.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
alert('Erro ao resetar senha: ' + error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleUserStatus(userId, currentStatus) {
|
||||||
|
const action = currentStatus ? 'desativar' : 'ativar';
|
||||||
|
if (confirm(`Tem certeza que deseja ${action} o login deste usuário?`)) {
|
||||||
|
fetch(`/usuarios/${userId}/toggle_status`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
alert(`Login ${action}do com sucesso!`);
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert(`Erro ao ${action} login: ` + data.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
alert(`Erro ao ${action} login: ` + error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function alterarNivel(userId, novoNivel) {
|
||||||
|
if (!novoNivel) return;
|
||||||
|
|
||||||
|
if (confirm('Tem certeza que deseja alterar o nível deste usuário?')) {
|
||||||
|
fetch(`/usuarios/${userId}/alterar_nivel`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ nivel: novoNivel })
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
alert('Nível do usuário alterado com sucesso!');
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Erro ao alterar nível: ' + data.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
alert('Erro ao alterar nível: ' + error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleQuadroOrientador(userId, isQuadroOrientador) {
|
||||||
|
const action = isQuadroOrientador ? 'remover' : 'adicionar';
|
||||||
|
if (confirm(`Tem certeza que deseja ${action} a responsabilidade de Quadro-Orientador deste militante?`)) {
|
||||||
|
fetch(`/usuarios/${userId}/toggle_quadro_orientador`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
alert(`Responsabilidade de Quadro-Orientador ${action}da com sucesso!`);
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert(`Erro ao ${action} responsabilidade: ` + data.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
alert(`Erro ao ${action} responsabilidade: ` + error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
83
templates/dashboard_admin.html
Normal file
83
templates/dashboard_admin.html
Normal 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 %}
|
||||||
94
templates/editar_celula.html
Normal file
94
templates/editar_celula.html
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Editar Célula{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<h1 class="mb-4">Editar Célula</h1>
|
||||||
|
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<form method="POST" class="needs-validation" novalidate>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="nome" class="form-label">Nome</label>
|
||||||
|
<input type="text" class="form-control" id="nome" name="nome" value="{{ celula.nome }}" required>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
Por favor, insira o nome da célula.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="setor_id" class="form-label">Setor</label>
|
||||||
|
<select class="form-select" id="setor_id" name="setor_id" required>
|
||||||
|
<option value="">Selecione um setor</option>
|
||||||
|
{% for setor in setores %}
|
||||||
|
<option value="{{ setor.id }}" {% if setor.id == celula.setor_id %}selected{% endif %}>{{ setor.nome }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
Por favor, selecione um setor.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="responsavel" class="form-label">Responsável</label>
|
||||||
|
<select class="form-select" id="responsavel" name="responsavel">
|
||||||
|
<option value="">Selecione um responsável</option>
|
||||||
|
{% for militante in militantes %}
|
||||||
|
<option value="{{ militante.id }}" {% if militante.id == celula.responsavel %}selected{% endif %}>{{ militante.nome }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="responsavel_financas" class="form-label">Responsável Finanças</label>
|
||||||
|
<select class="form-select" id="responsavel_financas" name="responsavel_financas">
|
||||||
|
<option value="">Selecione um responsável financeiro</option>
|
||||||
|
{% for militante in militantes %}
|
||||||
|
<option value="{{ militante.id }}" {% if militante.id == celula.responsavel_financas %}selected{% endif %}>{{ militante.nome }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between mt-4">
|
||||||
|
<button type="submit" class="btn btn-primary">Salvar</button>
|
||||||
|
<a href="{{ url_for('listar_celulas') }}" class="btn btn-secondary">Cancelar</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Validação do formulário
|
||||||
|
(function () {
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
var forms = document.querySelectorAll('.needs-validation')
|
||||||
|
|
||||||
|
Array.prototype.slice.call(forms)
|
||||||
|
.forEach(function (form) {
|
||||||
|
form.addEventListener('submit', function (event) {
|
||||||
|
if (!form.checkValidity()) {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
}
|
||||||
|
|
||||||
|
form.classList.add('was-validated')
|
||||||
|
}, false)
|
||||||
|
})
|
||||||
|
})()
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
94
templates/editar_comite.html
Normal file
94
templates/editar_comite.html
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Editar Comitê Regional{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<h1 class="mb-4">Editar Comitê Regional</h1>
|
||||||
|
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<form method="POST" class="needs-validation" novalidate>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="nome" class="form-label">Nome</label>
|
||||||
|
<input type="text" class="form-control" id="nome" name="nome" value="{{ comite.nome }}" required>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
Por favor, insira o nome do comitê regional.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="comite_central_id" class="form-label">Comitê Central</label>
|
||||||
|
<select class="form-select" id="comite_central_id" name="comite_central_id" required>
|
||||||
|
<option value="">Selecione um comitê central</option>
|
||||||
|
{% for comite_central in comites_centrais %}
|
||||||
|
<option value="{{ comite_central.id }}" {% if comite_central.id == comite.comite_central_id %}selected{% endif %}>{{ comite_central.nome }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
Por favor, selecione um comitê central.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="responsavel" class="form-label">Responsável</label>
|
||||||
|
<select class="form-select" id="responsavel" name="responsavel">
|
||||||
|
<option value="">Selecione um responsável</option>
|
||||||
|
{% for militante in militantes %}
|
||||||
|
<option value="{{ militante.id }}" {% if militante.id == comite.responsavel %}selected{% endif %}>{{ militante.nome }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="responsavel_financas" class="form-label">Responsável Finanças</label>
|
||||||
|
<select class="form-select" id="responsavel_financas" name="responsavel_financas">
|
||||||
|
<option value="">Selecione um responsável financeiro</option>
|
||||||
|
{% for militante in militantes %}
|
||||||
|
<option value="{{ militante.id }}" {% if militante.id == comite.responsavel_financas %}selected{% endif %}>{{ militante.nome }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between mt-4">
|
||||||
|
<button type="submit" class="btn btn-primary">Salvar</button>
|
||||||
|
<a href="{{ url_for('listar_comites') }}" class="btn btn-secondary">Cancelar</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Validação do formulário
|
||||||
|
(function () {
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
var forms = document.querySelectorAll('.needs-validation')
|
||||||
|
|
||||||
|
Array.prototype.slice.call(forms)
|
||||||
|
.forEach(function (form) {
|
||||||
|
form.addEventListener('submit', function (event) {
|
||||||
|
if (!form.checkValidity()) {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
}
|
||||||
|
|
||||||
|
form.classList.add('was-validated')
|
||||||
|
}, false)
|
||||||
|
})
|
||||||
|
})()
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
81
templates/editar_comite_central.html
Normal file
81
templates/editar_comite_central.html
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Editar Comitê Central{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<h1 class="mb-4">Editar Comitê Central</h1>
|
||||||
|
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<form method="POST" class="needs-validation" novalidate>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="nome" class="form-label">Nome</label>
|
||||||
|
<input type="text" class="form-control" id="nome" name="nome" value="{{ comite.nome }}" required>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
Por favor, insira o nome do comitê central.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="responsavel" class="form-label">Responsável</label>
|
||||||
|
<select class="form-select" id="responsavel" name="responsavel">
|
||||||
|
<option value="">Selecione um responsável</option>
|
||||||
|
{% for militante in militantes %}
|
||||||
|
<option value="{{ militante.id }}" {% if militante.id == comite.responsavel %}selected{% endif %}>{{ militante.nome }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="responsavel_financas" class="form-label">Responsável Finanças</label>
|
||||||
|
<select class="form-select" id="responsavel_financas" name="responsavel_financas">
|
||||||
|
<option value="">Selecione um responsável financeiro</option>
|
||||||
|
{% for militante in militantes %}
|
||||||
|
<option value="{{ militante.id }}" {% if militante.id == comite.responsavel_financas %}selected{% endif %}>{{ militante.nome }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between mt-4">
|
||||||
|
<button type="submit" class="btn btn-primary">Salvar</button>
|
||||||
|
<a href="{{ url_for('listar_comites_centrais') }}" class="btn btn-secondary">Cancelar</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Validação do formulário
|
||||||
|
(function () {
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
var forms = document.querySelectorAll('.needs-validation')
|
||||||
|
|
||||||
|
Array.prototype.slice.call(forms)
|
||||||
|
.forEach(function (form) {
|
||||||
|
form.addEventListener('submit', function (event) {
|
||||||
|
if (!form.checkValidity()) {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
}
|
||||||
|
|
||||||
|
form.classList.add('was-validated')
|
||||||
|
}, false)
|
||||||
|
})
|
||||||
|
})()
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
29
templates/editar_cota.html
Normal file
29
templates/editar_cota.html
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mt-4">
|
||||||
|
<h2>Editar Cota</h2>
|
||||||
|
<form method="POST" class="needs-validation" novalidate>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="valor_novo" class="form-label">Valor</label>
|
||||||
|
<input type="number" step="0.01" class="form-control" id="valor_novo" name="valor_novo" value="{{ cota.valor_novo }}" required>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
Por favor, insira um valor válido.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="data_vencimento" class="form-label">Data de Vencimento</label>
|
||||||
|
<input type="date" class="form-control" id="data_vencimento" name="data_vencimento" value="{{ cota.data_vencimento }}" required>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
Por favor, selecione uma data de vencimento.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3 form-check">
|
||||||
|
<input type="checkbox" class="form-check-input" id="pago" name="pago" value="true" {% if cota.pago %}checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="pago">Pago</label>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Salvar</button>
|
||||||
|
<a href="{{ url_for('listar_cotas') }}" class="btn btn-secondary">Cancelar</a>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
111
templates/editar_instancia.html
Normal file
111
templates/editar_instancia.html
Normal 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 %}
|
||||||
94
templates/editar_material.html
Normal file
94
templates/editar_material.html
Normal 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 %}
|
||||||
@@ -3,7 +3,10 @@
|
|||||||
{% block title %}Editar Militante{% endblock %}
|
{% block title %}Editar Militante{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% 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) %}
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
@@ -13,22 +16,214 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
<form method="post">
|
<form method="POST" class="needs-validation" novalidate>
|
||||||
Nome: <input type="text" name="nome" required
|
<div class="row">
|
||||||
value="{{ militante.nome }}"><br>
|
<div class="col-md-6 mb-3">
|
||||||
CPF: <input type="text" name="cpf" required
|
<label for="nome" class="form-label">Nome</label>
|
||||||
value="{{ militante.cpf }}"
|
<input type="text" class="form-control" id="nome" name="nome" value="{{ militante.nome }}" required>
|
||||||
pattern="\d{3}\.?\d{3}\.?\d{3}-?\d{2}"
|
<div class="invalid-feedback">
|
||||||
title="Digite um CPF no formato: xxx.xxx.xxx-xx"><br>
|
Por favor, insira o nome do militante.
|
||||||
Email: <input type="email" name="email" required
|
</div>
|
||||||
value="{{ militante.email }}"><br>
|
</div>
|
||||||
Telefone: <input type="text" name="telefone"
|
|
||||||
value="{{ militante.telefone }}"><br>
|
<div class="col-md-6 mb-3">
|
||||||
Endereço: <input type="text" name="endereco"
|
<label for="email" class="form-label">Email</label>
|
||||||
value="{{ militante.endereco }}"><br>
|
<input type="email" class="form-control" id="email" name="email" value="{{ militante.email }}" required>
|
||||||
Filiado: <input type="checkbox" name="filiado"
|
<div class="invalid-feedback">
|
||||||
{% if militante.filiado %}checked{% endif %}><br>
|
Por favor, insira um email válido.
|
||||||
<input type="submit" value="Salvar" class="btn btn-primary">
|
</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>
|
<a href="{{ url_for('listar_militantes') }}" class="btn btn-secondary">Cancelar</a>
|
||||||
|
</div>
|
||||||
</form>
|
</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 %}
|
{% endblock %}
|
||||||
91
templates/editar_relatorio_cotas.html
Normal file
91
templates/editar_relatorio_cotas.html
Normal 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 %}
|
||||||
91
templates/editar_relatorio_pagamentos.html
Normal file
91
templates/editar_relatorio_pagamentos.html
Normal 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 %}
|
||||||
91
templates/editar_relatorio_vendas.html
Normal file
91
templates/editar_relatorio_vendas.html
Normal 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 %}
|
||||||
1
templates/editar_setor.html
Normal file
1
templates/editar_setor.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
74
templates/editar_tipo_material.html
Normal file
74
templates/editar_tipo_material.html
Normal 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
124
templates/editar_venda.html
Normal 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 %}
|
||||||
@@ -1,18 +1,612 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Início{% endblock %}
|
{% block title %}Início{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row g-4">
|
||||||
<div class="col-md-12">
|
<div class="col-12">
|
||||||
<h2>Menu do Sistema</h2>
|
<div class="welcome-header">
|
||||||
<div class="list-group">
|
<h2 class="mb-2">Olá, {{ nome_usuario }}!</h2>
|
||||||
{% for url, endpoint in links %}
|
<h4 class="text-muted">
|
||||||
<a href="{{ url }}" class="list-group-item list-group-item-action">
|
{{ data_atual }}
|
||||||
{{ endpoint|replace('_', ' ')|title }}
|
</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>
|
</a>
|
||||||
|
<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 %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-muted m-3">Nenhum militante cadastrado recentemente.</p>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</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 %}
|
{% endblock %}
|
||||||
@@ -1,35 +1,284 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
{% block title %}Início{% endblock %}
|
{% block title %}Assinaturas{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>Assinaturas Anuais</h1>
|
<div class="container">
|
||||||
<a href="{{ url_for('nova_assinatura') }}">Adicionar Nova Assinatura</a>
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<table border="1">
|
<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>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
<th>Militante</th>
|
||||||
<th>Militante ID</th>
|
|
||||||
<th>Tipo Material</th>
|
|
||||||
<th>Quantidade</th>
|
|
||||||
<th>Valor Total</th>
|
|
||||||
<th>Data Início</th>
|
<th>Data Início</th>
|
||||||
<th>Data Fim</th>
|
<th>Data Fim</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Valor</th>
|
||||||
|
<th>Ações</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for assinatura in assinaturas %}
|
{% for assinatura in assinaturas %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ assinatura.id }}</td>
|
<td>{{ assinatura.militante.nome }}</td>
|
||||||
<td>{{ assinatura.militante_id }}</td>
|
<td>{{ assinatura.data_inicio.strftime('%d/%m/%Y') }}</td>
|
||||||
<td>{{ assinatura.tipo_material_id }}</td>
|
<td>{{ assinatura.data_fim.strftime('%d/%m/%Y') }}</td>
|
||||||
<td>{{ assinatura.quantidade }}</td>
|
<td>
|
||||||
<td>R$ {{ assinatura.valor_total }}</td>
|
{% if assinatura.ativa %}
|
||||||
<td>{{ assinatura.data_inicio }}</td>
|
<span class="badge bg-success">Ativa</span>
|
||||||
<td>{{ assinatura.data_fim }}</td>
|
{% 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>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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 %}
|
{% endblock %}
|
||||||
56
templates/listar_celulas.html
Normal file
56
templates/listar_celulas.html
Normal 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 %}
|
||||||
56
templates/listar_comites.html
Normal file
56
templates/listar_comites.html
Normal 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 %}
|
||||||
54
templates/listar_comites_centrais.html
Normal file
54
templates/listar_comites_centrais.html
Normal 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 %}
|
||||||
@@ -1,31 +1,323 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Listar Militantes{% endblock %}
|
{% block title %}Cotas{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>Cotas Mensais</h1>
|
<div class="row mb-4">
|
||||||
<a href="{{ url_for('nova_cota') }}">Adicionar Nova Cota</a>
|
<div class="col-12">
|
||||||
<table border="1">
|
<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>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
<th data-sort="militante">Militante <i class="fas fa-sort"></i></th>
|
||||||
<th>Militante ID</th>
|
<th data-sort="valor_antigo">Valor Antigo <i class="fas fa-sort"></i></th>
|
||||||
<th>Valor Antigo</th>
|
<th data-sort="valor_novo">Valor Novo <i class="fas fa-sort"></i></th>
|
||||||
<th>Valor Novo</th>
|
<th data-sort="data_alteracao">Data de Alteração <i class="fas fa-sort"></i></th>
|
||||||
<th>Data de Alteração</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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for cota in cotas %}
|
{% for cota in cotas %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ cota.id }}</td>
|
<td data-militante="{{ cota.militante.nome }}">{{ cota.militante.nome }}</td>
|
||||||
<td>{{ cota.militante_id }}</td>
|
<td data-valor_antigo="{{ cota.valor_antigo }}">R$ {{ "%.2f"|format(cota.valor_antigo) }}</td>
|
||||||
<td>R$ {{ cota.valor_antigo }}</td>
|
<td data-valor_novo="{{ cota.valor_novo }}">R$ {{ "%.2f"|format(cota.valor_novo) }}</td>
|
||||||
<td>R$ {{ cota.valor_novo }}</td>
|
<td data-data_alteracao="{{ cota.data_alteracao }}">{{ cota.data_alteracao.strftime('%d/%m/%Y') }}</td>
|
||||||
<td>{{ cota.data_alteracao }}</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>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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 %}
|
{% endblock %}
|
||||||
|
|||||||
79
templates/listar_instancias.html
Normal file
79
templates/listar_instancias.html
Normal 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 %}
|
||||||
@@ -1,33 +1,330 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Listar Militantes{% endblock %}
|
{% block title %}Materiais{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>Materiais Vendidos</h1>
|
<div class="row mb-4">
|
||||||
<a href="{{ url_for('novo_material') }}">Adicionar Novo Material</a>
|
<div class="col-12">
|
||||||
<table border="1">
|
<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>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
<th data-sort="militante">Militante <i class="fas fa-sort"></i></th>
|
||||||
<th>Militante ID</th>
|
<th data-sort="tipo">Tipo <i class="fas fa-sort"></i></th>
|
||||||
<th>Tipo Material</th>
|
<th data-sort="descricao">Descrição <i class="fas fa-sort"></i></th>
|
||||||
<th>Descrição</th>
|
<th data-sort="valor">Valor <i class="fas fa-sort"></i></th>
|
||||||
<th>Valor</th>
|
<th data-sort="data">Data <i class="fas fa-sort"></i></th>
|
||||||
<th>Data da Venda</th>
|
<th class="text-end">Ações</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for material in materiais %}
|
{% for material in materiais %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ material.id }}</td>
|
<td data-militante="{{ material.militante.nome }}">{{ material.militante.nome }}</td>
|
||||||
<td>{{ material.militante_id }}</td>
|
<td data-tipo="{{ material.tipo_material.nome }}">{{ material.tipo_material.nome }}</td>
|
||||||
<td>{{ material.tipo_material_id }}</td>
|
<td data-descricao="{{ material.descricao }}">{{ material.descricao }}</td>
|
||||||
<td>{{ material.descricao }}</td>
|
<td data-valor="{{ material.valor }}">R$ {{ "%.2f"|format(material.valor) }}</td>
|
||||||
<td>R$ {{ material.valor }}</td>
|
<td data-data="{{ material.data_venda }}">{{ material.data_venda.strftime('%d/%m/%Y') }}</td>
|
||||||
<td>{{ material.data_venda }}</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>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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 %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,57 +1,298 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Listar Militantes{% endblock %}
|
{% block title %}Militantes{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row mb-4">
|
||||||
<div class="col-md-12">
|
<div class="col-12">
|
||||||
<h2>Lista de Militantes</h2>
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<a href="{{ url_for('novo_militante') }}" class="btn btn-primary mb-3">Novo Militante</a>
|
<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>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Nome</th>
|
<th data-sort="nome">Nome <i class="fas fa-sort"></i></th>
|
||||||
<th>CPF</th>
|
<th data-sort="cpf">CPF <i class="fas fa-sort"></i></th>
|
||||||
<th>Email</th>
|
<th data-sort="email">Email <i class="fas fa-sort"></i></th>
|
||||||
<th>Telefone</th>
|
<th data-sort="telefone">Telefone <i class="fas fa-sort"></i></th>
|
||||||
<th>Endereço</th>
|
<th data-sort="celula">Célula <i class="fas fa-sort"></i></th>
|
||||||
<th>Filiado</th>
|
<th>Responsabilidades</th>
|
||||||
|
<th class="text-end">Ações</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for militante in militantes %}
|
{% for militante in militantes %}
|
||||||
<tr class="clickable-row" data-href="{{ url_for('editar_militante', id=militante.id) }}">
|
<tr data-militante="{{ militante.id }}" data-filiado="{{ 'sim' if militante.filiado else 'nao' }}">
|
||||||
<td>{{ militante.nome }}</td>
|
<td data-nome="{{ militante.nome }}">{{ militante.nome }}</td>
|
||||||
<td>{{ militante.cpf }}</td>
|
<td data-cpf="{{ militante.cpf }}">{{ militante.cpf }}</td>
|
||||||
<td>{{ militante.email }}</td>
|
<td data-email="{{ militante.email }}">{{ militante.email }}</td>
|
||||||
<td>{{ militante.telefone }}</td>
|
<td data-telefone="{{ militante.telefone }}">{{ militante.telefone }}</td>
|
||||||
<td>{{ militante.endereco }}</td>
|
<td data-celula="{{ militante.celula.nome }}">{{ militante.celula.nome }}</td>
|
||||||
<td>{{ 'Sim' if militante.filiado else 'Não' }}</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>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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>
|
</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>
|
<style>
|
||||||
.clickable-row {
|
/* Estilo para o backdrop com blur em todos os modais */
|
||||||
cursor: pointer;
|
.modal-backdrop.show {
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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;
|
||||||
}
|
}
|
||||||
.clickable-row:hover {
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
}
|
}
|
||||||
</style>
|
</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 %}
|
{% endblock %}
|
||||||
@@ -1,32 +1,203 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Listar Militantes{% endblock %}
|
{% block title %}Pagamentos{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>Pagamentos</h1>
|
<div class="container-fluid mt-3">
|
||||||
<a href="{{ url_for('novo_pagamento') }}">Adicionar Novo Pagamento</a>
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
<table border="1">
|
<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>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
<th>Militante</th>
|
||||||
<th>Militante ID</th>
|
|
||||||
<th>Tipo de Pagamento</th>
|
<th>Tipo de Pagamento</th>
|
||||||
<th>Valor</th>
|
<th>Valor</th>
|
||||||
<th>Data do Pagamento</th>
|
<th>Data do Pagamento</th>
|
||||||
|
<th>Ações</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for pagamento in pagamentos %}
|
{% for pagamento in pagamentos %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ pagamento.id }}</td>
|
<td data-militante="{{ pagamento.militante_id }}">{{ pagamento.militante.nome if pagamento.militante else 'N/A' }}</td>
|
||||||
<td>{{ pagamento.militante_id }}</td>
|
<td data-tipo="{{ pagamento.tipo_pagamento }}">
|
||||||
<td>{{ pagamento.tipo_pagamento_id }}</td>
|
{% if pagamento.tipo_pagamento == 1 %}
|
||||||
<td>R$ {{ pagamento.valor }}</td>
|
Mensalidade
|
||||||
<td>{{ pagamento.data_pagamento }}</td>
|
{% 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>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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 %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +1,57 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
{% block title %}Listar Militantes{% endblock %}
|
{% block title %}Listar Relatórios de Cotas{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>Relatórios de Cotas Mensais</h1>
|
<div class="container">
|
||||||
<a href="{{ url_for('novo_relatorio_cotas') }}">Adicionar Novo Relatório</a>
|
<div class="row">
|
||||||
<table border="1">
|
<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>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
<th>ID</th>
|
||||||
<th>Setor ID</th>
|
<th>Setor</th>
|
||||||
<th>Comitê ID</th>
|
<th>Comitê Central</th>
|
||||||
<th>Total de Cotas</th>
|
<th>Total de Cotas</th>
|
||||||
<th>Data do Relatório</th>
|
<th>Data do Relatório</th>
|
||||||
|
<th>Ações</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for relatorio in relatorios %}
|
{% for relatorio in relatorios %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ relatorio.id }}</td>
|
<td>{{ relatorio.id }}</td>
|
||||||
<td>{{ relatorio.setor_id }}</td>
|
<td>{{ relatorio.setor.nome }}</td>
|
||||||
<td>{{ relatorio.comite_id }}</td>
|
<td>{{ relatorio.comite.nome }}</td>
|
||||||
<td>R$ {{ relatorio.total_cotas }}</td>
|
<td>R$ {{ "%.2f"|format(relatorio.total_cotas) }}</td>
|
||||||
<td>{{ relatorio.data_relatorio }}</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>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<a href="{{ url_for('home') }}">Home</a>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
56
templates/listar_relatorios_pagamentos.html
Normal file
56
templates/listar_relatorios_pagamentos.html
Normal 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 %}
|
||||||
@@ -1,32 +1,56 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
{% block title %}Listar Militantes{% endblock %}
|
{% block title %}Listar Relatórios de Vendas{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>Relatórios de Vendas de Materiais</h1>
|
<div class="container">
|
||||||
<a href="{{ url_for('novo_relatorio_vendas') }}">Adicionar Novo Relatório</a>
|
<div class="row">
|
||||||
<table border="1">
|
<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>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
<th>ID</th>
|
||||||
<th>Setor ID</th>
|
<th>Setor</th>
|
||||||
<th>Comitê ID</th>
|
<th>Comitê Central</th>
|
||||||
<th>Total de Vendas</th>
|
<th>Total de Vendas</th>
|
||||||
<th>Data do Relatório</th>
|
<th>Data do Relatório</th>
|
||||||
|
<th>Ações</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for relatorio in relatorios %}
|
{% for relatorio in relatorios %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ relatorio.id }}</td>
|
<td>{{ relatorio.id }}</td>
|
||||||
<td>{{ relatorio.setor_id }}</td>
|
<td>{{ relatorio.setor.nome }}</td>
|
||||||
<td>{{ relatorio.comite_id }}</td>
|
<td>{{ relatorio.comite.nome }}</td>
|
||||||
<td>R$ {{ relatorio.total_vendas }}</td>
|
<td>R$ {{ "%.2f"|format(relatorio.total_vendas) }}</td>
|
||||||
<td>{{ relatorio.data_relatorio }}</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>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<a href="{{ url_for('home') }}">Home</a>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
56
templates/listar_setores.html
Normal file
56
templates/listar_setores.html
Normal 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 %}
|
||||||
54
templates/listar_tipos_materiais.html
Normal file
54
templates/listar_tipos_materiais.html
Normal 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 %}
|
||||||
58
templates/listar_vendas.html
Normal file
58
templates/listar_vendas.html
Normal 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 %}
|
||||||
@@ -1,32 +1,515 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Listar Militantes{% endblock %}
|
{% block title %}Vendas de Jornais{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>Vendas de Jornais Avulsos</h1>
|
<div class="row mb-4">
|
||||||
<a href="{{ url_for('nova_venda_jornal') }}">Adicionar Nova Venda</a>
|
<div class="col-12">
|
||||||
<table border="1">
|
<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>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
<th data-sort="militante">Militante <i class="fas fa-sort"></i></th>
|
||||||
<th>Militante ID</th>
|
<th data-sort="quantidade">Quantidade <i class="fas fa-sort"></i></th>
|
||||||
<th>Quantidade</th>
|
<th data-sort="valor_total">Valor Total <i class="fas fa-sort"></i></th>
|
||||||
<th>Valor Total</th>
|
<th data-sort="data">Data <i class="fas fa-sort"></i></th>
|
||||||
<th>Data da Venda</th>
|
<th class="text-end">Ações</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for venda in vendas %}
|
{% for venda in vendas %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ venda.id }}</td>
|
<td data-militante="{{ venda.militante.nome }}">{{ venda.militante.nome }}</td>
|
||||||
<td>{{ venda.militante_id }}</td>
|
<td data-quantidade="{{ venda.quantidade }}">{{ venda.quantidade }}</td>
|
||||||
<td>{{ venda.quantidade }}</td>
|
<td data-valor_total="{{ venda.valor_total }}">R$ {{ "%.2f"|format(venda.valor_total) }}</td>
|
||||||
<td>R$ {{ venda.valor_total }}</td>
|
<td data-data="{{ venda.data_venda }}">{{ venda.data_venda.strftime('%d/%m/%Y') }}</td>
|
||||||
<td>{{ venda.data_venda }}</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>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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 %}
|
{% endblock %}
|
||||||
|
|||||||
211
templates/login.html
Normal file
211
templates/login.html
Normal 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
23
templates/militantes.html
Normal 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>
|
||||||
267
templates/modals/militante_editar.html
Normal file
267
templates/modals/militante_editar.html
Normal 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>
|
||||||
23
templates/modals/militante_excluir.html
Normal file
23
templates/modals/militante_excluir.html
Normal 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>
|
||||||
256
templates/modals/militante_novo.html
Normal file
256
templates/modals/militante_novo.html
Normal 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>
|
||||||
41
templates/mostrar_qr_code.html
Normal file
41
templates/mostrar_qr_code.html
Normal 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 %}
|
||||||
94
templates/nova_celula.html
Normal file
94
templates/nova_celula.html
Normal 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
121
templates/nova_venda.html
Normal 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 %}
|
||||||
74
templates/novo_celula.html
Normal file
74
templates/novo_celula.html
Normal 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 %}
|
||||||
94
templates/novo_comite.html
Normal file
94
templates/novo_comite.html
Normal 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 %}
|
||||||
81
templates/novo_comite_central.html
Normal file
81
templates/novo_comite_central.html
Normal 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 %}
|
||||||
54
templates/novo_comite_regional.html
Normal file
54
templates/novo_comite_regional.html
Normal 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 %}
|
||||||
74
templates/novo_endereco.html
Normal file
74
templates/novo_endereco.html
Normal 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 %}
|
||||||
@@ -1,35 +1,94 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
{% block title %}Listar Militantes{% endblock %}
|
{% block title %}Novo Material{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>Registrar Novo Material</h1>
|
<div class="container">
|
||||||
<form method="post">
|
<div class="row">
|
||||||
<div>
|
<div class="col-md-12">
|
||||||
<label for="militante_id">ID do Militante:</label>
|
<h1 class="mb-4">Novo Material</h1>
|
||||||
<input type="number" id="militante_id" name="militante_id" required>
|
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<form method="POST" class="needs-validation" novalidate>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="nome" class="form-label">Nome</label>
|
||||||
|
<input type="text" class="form-control" id="nome" name="nome" required>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
Por favor, insira o nome do material.
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label for="tipo_material_id">Tipo de Material:</label>
|
|
||||||
<input type="number" id="tipo_material_id" name="tipo_material_id" required>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label for="descricao">Descrição:</label>
|
<div class="mb-3">
|
||||||
<input type="text" id="descricao" name="descricao" required>
|
<label for="descricao" class="form-label">Descrição</label>
|
||||||
|
<textarea class="form-control" id="descricao" name="descricao" rows="3" required></textarea>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
Por favor, insira a descrição do material.
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label for="valor">Valor:</label>
|
|
||||||
<input type="number" id="valor" name="valor" step="0.01" required>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label for="data_venda">Data da Venda:</label>
|
<div class="mb-3">
|
||||||
<input type="date" id="data_venda" name="data_venda" required>
|
<label for="preco" class="form-label">Preço</label>
|
||||||
|
<input type="number" class="form-control" id="preco" name="preco" step="0.01" required>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
Por favor, insira o preço do material.
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex gap-2">
|
</div>
|
||||||
<button type="submit" class="btn btn-primary">Registrar</button>
|
|
||||||
<a href="{{ url_for('listar_materiais') }}" class="btn btn-secondary">Voltar</a>
|
<div class="mb-3">
|
||||||
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
|
<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>
|
</div>
|
||||||
</form>
|
</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 %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-8 offset-md-2">
|
<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) %}
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
@@ -19,44 +19,171 @@
|
|||||||
<form method="post" class="mb-4">
|
<form method="post" class="mb-4">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="nome" class="form-label">Nome:</label>
|
<label for="nome" class="form-label">Nome:</label>
|
||||||
<input type="text" class="form-control" id="nome" name="nome" required
|
<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">
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="email" class="form-label">Email:</label>
|
<label for="email" class="form-label">Email:</label>
|
||||||
<input type="email" class="form-control" id="email" name="email" required
|
<input type="email" class="form-control" id="email" name="email" required>
|
||||||
value="{{ dados_anteriores.email if dados_anteriores else '' }}">
|
<small class="form-text text-muted">Este email será usado para login e comunicação do sistema</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="telefone" class="form-label">Telefone:</label>
|
<label for="cpf" class="form-label">CPF:</label>
|
||||||
<input type="text" class="form-control" id="telefone" name="telefone"
|
<input type="text" class="form-control" id="cpf" name="cpf" required>
|
||||||
value="{{ dados_anteriores.telefone if dados_anteriores else '' }}">
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="endereco" class="form-label">Endereço:</label>
|
<label for="titulo_eleitoral" class="form-label">Título Eleitoral:</label>
|
||||||
<input type="text" class="form-control" id="endereco" name="endereco"
|
<input type="text" class="form-control" id="titulo_eleitoral" name="titulo_eleitoral" required>
|
||||||
value="{{ dados_anteriores.endereco if dados_anteriores else '' }}">
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3 form-check">
|
<div class="mb-3">
|
||||||
<input type="checkbox" class="form-check-input" id="filiado" name="filiado"
|
<label for="data_nascimento" class="form-label">Data de Nascimento:</label>
|
||||||
{% if dados_anteriores and dados_anteriores.filiado %}checked{% endif %}>
|
<input type="date" class="form-control" id="data_nascimento" name="data_nascimento" required>
|
||||||
<label class="form-check-label" for="filiado">Filiado</label>
|
</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>
|
||||||
|
|
||||||
<div class="d-flex gap-2">
|
<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('listar_militantes') }}" class="btn btn-secondary">Voltar</a>
|
||||||
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
|
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -64,5 +191,66 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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 %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@@ -6,36 +6,89 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-8 offset-md-2">
|
<div class="col-md-8 offset-md-2">
|
||||||
<h1 class="mb-4">Registrar Novo Pagamento</h1>
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header bg-light">
|
||||||
<form method="post" class="mb-4">
|
<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">
|
<div class="mb-3">
|
||||||
<label for="militante_id" class="form-label">ID do Militante:</label>
|
<label for="militante_id" class="form-label">Militante:</label>
|
||||||
<input type="number" class="form-control" id="militante_id" name="militante_id" required>
|
<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>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="tipo_pagamento_id" class="form-label">Tipo de Pagamento:</label>
|
<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>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="valor" class="form-label">Valor:</label>
|
<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>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="data_pagamento" class="form-label">Data do Pagamento:</label>
|
<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>
|
||||||
|
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<button type="submit" class="btn btn-primary">Registrar</button>
|
<button type="submit" class="btn btn-primary">
|
||||||
<a href="{{ url_for('listar_pagamentos') }}" class="btn btn-secondary">Voltar</a>
|
<i class="fas fa-save me-1"></i>Registrar
|
||||||
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
|
</button>
|
||||||
|
<a href="{{ url_for('listar_pagamentos') }}" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-arrow-left me-1"></i>Voltar
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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 %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,30 +1,92 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
{% block title %}Listar Militantes{% endblock %}
|
{% block title %}Novo Relatório de Cotas{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>Registrar Novo Relatório de Cotas</h1>
|
<div class="container">
|
||||||
<form method="post">
|
<div class="row">
|
||||||
<div>
|
<div class="col-md-12">
|
||||||
<label for="setor_id">ID do Setor:</label>
|
<h1 class="mb-4">Novo Relatório de Cotas</h1>
|
||||||
<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>
|
|
||||||
|
|
||||||
|
{% 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 %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
91
templates/novo_relatorio_pagamentos.html
Normal file
91
templates/novo_relatorio_pagamentos.html
Normal 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 %}
|
||||||
@@ -1,30 +1,91 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
{% block title %}Listar Militantes{% endblock %}
|
{% block title %}Novo Relatório de Vendas{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>Registrar Novo Relatório de Vendas</h1>
|
<div class="container">
|
||||||
<form method="post">
|
<div class="row">
|
||||||
<div>
|
<div class="col-md-12">
|
||||||
<label for="setor_id">ID do Setor:</label>
|
<h1 class="mb-4">Novo Relatório de Vendas</h1>
|
||||||
<input type="number" id="setor_id" name="setor_id" required>
|
|
||||||
|
{% 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>
|
|
||||||
<label for="comite_id">ID do Comitê:</label>
|
|
||||||
<input type="number" id="comite_id" name="comite_id" required>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label for="total_vendas">Total de Vendas:</label>
|
<div class="mb-3">
|
||||||
<input type="number" id="total_vendas" name="total_vendas" step="0.01" required>
|
<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>
|
|
||||||
<label for="data_relatorio">Data do Relatório:</label>
|
|
||||||
<input type="date" id="data_relatorio" name="data_relatorio" required>
|
|
||||||
</div>
|
</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>
|
</form>
|
||||||
<a href="{{ url_for('listar_relatorios_vendas') }}">Voltar para Lista</a>
|
</div>
|
||||||
<a href="{{ url_for('home') }}">Home</a>
|
</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 %}
|
{% endblock %}
|
||||||
|
|||||||
94
templates/novo_setor.html
Normal file
94
templates/novo_setor.html
Normal 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
Reference in New Issue
Block a user