49 Commits

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

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

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

- Estilos:
  - Adicionadas classes valor-container e icon-container
  - Melhorado responsividade dos valores monetários
  - Ajustado gradiente no header de boas-vindas
  - Refinado espaçamento e margens dos componentes
2025-04-03 17:59:41 -03:00
andersonid
d4869dcfaa feat: adiciona script de dados fictícios e padroniza tratamento de erros 2025-04-03 17:56:44 -03:00
andersonid
06e7c79488 refactor: padroniza tratamento de erro na listagem de militantes 2025-04-03 17:54:29 -03:00
andersonid
0a2d5c1d23 fix: corrige tratamento de erro na listagem de cotas 2025-04-03 17:54:29 -03:00
andersonid
855f97c72b feat: moderniza a página de listagem de cotas 2025-04-03 17:54:25 -03:00
andersonid
8e6ccb70e9 fix: corrige blocos não fechados no template de listagem de militantes 2025-04-03 17:53:24 -03:00
andersonid
65406276ae feat: adiciona hover vermelho nos menus e centraliza data no mobile 2025-04-03 17:53:24 -03:00
andersonid
b1acc2fdfc fix: corrige referências dos logos de acordo com o fundo 2025-04-03 17:53:24 -03:00
andersonid
c44ce94bef refactor: simplifica e moderniza a tela de login 2025-04-03 17:53:17 -03:00
andersonid
ce3b5a4231 refactor: melhorias na UI - formulário de pagamento e cards 2025-04-03 17:52:30 -03:00
andersonid
f0faf4270b refactor: melhorias na UI - navbar, logo e layout da data 2025-04-03 17:51:27 -03:00
andersonid
178a58bb00 fix: correções no logo, remoção de mensagem de login e auto-dismiss de alertas 2025-04-03 17:49:42 -03:00
andersonid
e9c1f3aedf fix: correções no logo, degradê e alertas 2025-04-03 17:48:47 -03:00
andersonid
1ff8e97bbc fix: ajustes visuais - degradê, card branco, logo e barra vermelha 2025-04-03 17:48:41 -03:00
andersonid
b815f77240 refactor: melhorias na interface de login e alertas, foco em mobile-first 2025-04-03 17:47:05 -03:00
andersonid
ba4f6d6de3 fix: ajustes no logo e nome do sistema em todas as interfaces 2025-04-03 17:44:44 -03:00
andersonid
ac461ce800 fix: removido Flask-Moment e adicionada data formatada em português 2025-04-03 17:41:29 -03:00
andersonid
4f781b2a0e refactor: ajustes na interface - logo, nome do sistema e layout da home 2025-04-03 17:41:29 -03:00
andersonid
32cd4b70c1 feat: atualização da identidade visual com cores e logo da OCI 2025-04-03 17:41:29 -03:00
andersonid
54261e455c feat: melhorias na interface e estrutura do frontend 2025-04-03 17:41:29 -03:00
44 changed files with 8137 additions and 1385 deletions

7
.gitignore vendored
View File

@@ -263,3 +263,10 @@ database.db
admin_qr.png admin_qr.png
# End of https://www.toptal.com/developers/gitignore/api/python,flask # End of https://www.toptal.com/developers/gitignore/api/python,flask
# Documentação temporária
docs/alteracoes_db_connection.md
# QR Codes
*_qr.png
*_qr.txt

View File

@@ -5,8 +5,14 @@ clean:
rm -rf ~/.local/share/controles/database.db rm -rf ~/.local/share/controles/database.db
rm -f admin_qr.png rm -f admin_qr.png
run: clean 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 reset-admin: clean
python create_admin.py python create_admin.py

View File

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

1570
app.py

File diff suppressed because it is too large Load Diff

View File

@@ -1,91 +1,142 @@
import os from functions.database import init_database, Usuario, Role, get_db_connection
from functions.database import get_db_connection, Usuario
from functions.rbac import Role
import pyotp
import qrcode import qrcode
import base64 import os
from io import BytesIO from pathlib import Path
import pyotp
def create_admin(): def generate_qr_code(user):
"""Cria o usuário admin se não existir""" """
db = get_db_connection() 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: try:
# Verificar se o admin já existe # Inicializar banco de dados
admin = db.query(Usuario).filter_by(username='admin').first() init_database()
if admin: # Criar sessão
print("Usuário admin já existe") db = get_db_connection()
# Verificar se o arquivo admin_qr.png existe try:
if os.path.exists('admin_qr.png'): # Verificar se já existe um usuário admin
print("Usando OTP existente do arquivo admin_qr.png") admin = db.query(Usuario).filter_by(username="admin").first()
# Extrair o OTP secret do QR code existente
with open('admin_qr.png', 'rb') as f: if admin:
qr_data = f.read() print("\n=== Usuário Admin Encontrado ===")
# Aqui você precisaria implementar a lógica para extrair o OTP secret do QR code if not admin.otp_secret:
# Por enquanto, vamos apenas manter o OTP existente print("Gerando novo segredo OTP...")
return admin.generate_otp_secret()
db.commit()
else: else:
print("Gerando novo OTP para o admin...") print("\n=== Criando Novo Usuário Admin ===")
# Gerar novo OTP # Criar novo usuário admin
otp_secret = pyotp.random_base32() admin = Usuario(
admin.otp_secret = otp_secret username="admin",
db.commit() email="admin@example.com",
else: is_admin=True
print("Criando usuário admin...") )
# Criar usuário admin admin.set_password("admin123")
admin = Usuario( admin.generate_otp_secret()
username='admin',
password='admin123',
is_admin=True
)
admin.email = 'admin@controles.com'
db.add(admin)
db.commit()
# Gerar OTP # Adicionar e fazer commit
otp_secret = pyotp.random_base32() db.add(admin)
admin.otp_secret = otp_secret
db.commit()
# Atribuir role de Secretário Geral
admin_role = db.query(Role).filter_by(nivel=Role.SECRETARIO_GERAL).first()
if admin_role:
admin.roles.append(admin_role)
db.commit() db.commit()
# Gerar QR code # Gerar QR code apenas se solicitado ou se for novo usuário
totp = pyotp.TOTP(otp_secret) if not os.path.exists('admin_qr.png'):
provisioning_uri = totp.provisioning_uri(admin.username, issuer_name="Sistema de Controles") 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'
qr = qrcode.QRCode(version=1, box_size=10, border=5) # Mostrar informações
qr.add_data(provisioning_uri) print("\n=== Informações do Admin ===")
qr.make(fit=True) print(f"Username: {admin.username}")
print(f"Email: {admin.email}")
print(f"Senha: admin123")
print(f"Segredo OTP: {admin.otp_secret}")
img = qr.make_image(fill_color="black", back_color="white") # 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)}")
# Salvar QR code como base64 print("\n=== Instruções para Configuração ===")
buffered = BytesIO() print("1. Instale um aplicativo autenticador no seu celular")
img.save(buffered, format="PNG") print(" (Google Authenticator, Microsoft Authenticator, etc)")
qr_base64 = base64.b64encode(buffered.getvalue()).decode() 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")
# Salvar QR code como arquivo # Verificação final
img.save('admin_qr.png') 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'}")
print("\nConfiguração do OTP para o admin:") if not is_valid:
print(f"OTP Secret: {otp_secret}") print("\nALERTA: Verificação do OTP falhou!")
print("\nInstruções:") print("Por favor, verifique se o segredo OTP está correto.")
print("1. Use um aplicativo autenticador (como Google Authenticator ou Authy)")
print("2. Escaneie o QR code ou insira o OTP Secret manualmente") # Fazer commit final para garantir que tudo foi salvo
print("3. Use o código gerado para fazer login") db.commit()
print("\nQR code salvo em 'admin_qr.png'")
except Exception as e:
db.rollback()
raise e
finally:
db.close()
except Exception as e: except Exception as e:
print(f"Erro ao criar admin: {str(e)}") print(f"\nErro durante a execução: {e}")
db.rollback() import traceback
raise traceback.print_exc()
finally:
db.close()
if __name__ == '__main__': if __name__ == "__main__":
create_admin() create_admin_user()

View File

@@ -51,22 +51,21 @@ def create_test_users():
# Criar usuário # Criar usuário
user = Usuario( user = Usuario(
username=user_data['username'], username=user_data['username'],
password=user_data['password'], email=user_data['email'],
is_admin=user_data['is_admin'] is_admin=user_data['is_admin']
) )
user.email = user_data['email'] user.set_password(user_data['password'])
db.add(user) user.tipo = "ADMIN" if user_data['is_admin'] else "USUARIO"
db.commit()
# Se for o usuário teste, usar o mesmo OTP do admin # Se for o usuário teste, usar o mesmo OTP do admin
if user_data['username'] == 'teste' and admin_otp_secret: if user_data['username'] == 'teste' and admin_otp_secret:
user.otp_secret = admin_otp_secret user.otp_secret = admin_otp_secret
db.commit()
else: else:
# Gerar novo OTP para outros usuários # Gerar novo OTP para outros usuários
otp_secret = pyotp.random_base32() user.otp_secret = pyotp.random_base32()
user.otp_secret = otp_secret
db.commit() db.add(user)
db.commit()
# Atribuir role de Secretário Geral para o usuário teste # Atribuir role de Secretário Geral para o usuário teste
if user_data['username'] == 'teste': if user_data['username'] == 'teste':
@@ -76,6 +75,17 @@ def create_test_users():
db.commit() db.commit()
print(f"Usuário {user_data['username']} criado com sucesso!") 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: else:
print(f"Usuário {user_data['username']} já existe") print(f"Usuário {user_data['username']} já existe")
@@ -84,6 +94,22 @@ def create_test_users():
user.otp_secret = admin_otp_secret user.otp_secret = admin_otp_secret
db.commit() db.commit()
print(f"OTP do usuário teste atualizado para o mesmo do admin") 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 # Verificar se o usuário teste tem a role de Secretário Geral
if user_data['username'] == 'teste': if user_data['username'] == 'teste':

View File

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

View File

@@ -1,14 +1,26 @@
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from pathlib import Path
import os import os
# Configuração do banco de dados # Configurar caminho do banco de dados
DATABASE_URL = os.getenv('DATABASE_URL', 'sqlite:///database.db') db_dir = Path.home() / '.local' / 'share' / 'controles'
engine = create_engine(DATABASE_URL) db_dir.mkdir(parents=True, exist_ok=True)
Session = sessionmaker(bind=engine) db_path = db_dir / 'database.db'
# Base declarativa do SQLAlchemy # 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() Base = declarative_base()
def get_db_connection(): def get_db_connection():

View File

@@ -1,6 +1,6 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.security import generate_password_hash, check_password_hash
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text, Numeric, Date, Enum from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text, Numeric, Date, Enum, create_engine, text
from sqlalchemy.orm import sessionmaker, relationship, backref from sqlalchemy.orm import sessionmaker, relationship, backref
import os import os
import pyotp import pyotp
@@ -13,6 +13,7 @@ import enum
from flask_login import UserMixin from flask_login import UserMixin
from .rbac import Role, Permission, role_permissions, user_roles from .rbac import Role, Permission, role_permissions, user_roles
from .base import Base, engine, Session from .base import Base, engine, Session
import logging
# Configurar caminho do banco de dados # Configurar caminho do banco de dados
db_dir = Path.home() / '.local' / 'share' / 'controles' db_dir = Path.home() / '.local' / 'share' / 'controles'
@@ -22,26 +23,16 @@ db_path = db_dir / 'database.db'
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 SQLite e verifica timeout db = SessionLocal()
"""
session = SessionLocal()
try: try:
# Verificar timeout para usuários logados # Configurar SQLite para melhor tratamento de concorrência
usuario_atual = session.query(Usuario).filter( db.execute(text("PRAGMA journal_mode=WAL"))
Usuario.ultimo_login.isnot(None), db.execute(text("PRAGMA busy_timeout=5000"))
Usuario.ultimo_logout.is_(None) return db
).first() except:
db.close()
if usuario_atual and usuario_atual.check_session_timeout(): raise
usuario_atual.logout()
session.commit()
raise Exception("Sessão expirada. Por favor, faça login novamente.")
return session
except Exception as e:
session.close()
raise e
def execute_query(query, params=None): def execute_query(query, params=None):
""" """
@@ -58,6 +49,12 @@ 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): class Celula(Base):
__tablename__ = 'celulas' __tablename__ = 'celulas'
@@ -177,6 +174,11 @@ class Militante(Base):
avaliacao_aspirante = Column(Text) avaliacao_aspirante = Column(Text)
data_avaliacao_aspirante = Column(DateTime) 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 # 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")
@@ -296,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")
@@ -456,31 +460,19 @@ class Usuario(Base, UserMixin):
cr = relationship('ComiteRegional', back_populates='usuarios') cr = relationship('ComiteRegional', back_populates='usuarios')
celula = relationship('Celula', back_populates='usuarios') celula = relationship('Celula', back_populates='usuarios')
def get_id(self): def __init__(self, username, email=None, is_admin=False):
return str(self.id)
@property
def is_authenticated(self):
return True
@property
def is_active(self):
return self.ativo
@property
def is_anonymous(self):
return False
def __init__(self, username, password, is_admin=False, email=None, tipo="USUARIO"):
self.username = username self.username = username
self.password_hash = generate_password_hash(password) self.email = email
self.is_admin = is_admin self.is_admin = is_admin
self.email = email self.email = email
self.ativo = True self.ativo = True
self.session_timeout = 30 self.session_timeout = 30
self.tipo = tipo self.tipo = "USUARIO"
self.ultima_atividade = datetime.utcnow() self.ultima_atividade = datetime.utcnow()
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password): def check_password(self, password):
return check_password_hash(self.password_hash, password) return check_password_hash(self.password_hash, password)
@@ -501,7 +493,11 @@ class Usuario(Base, UserMixin):
return time_diff.total_seconds() > (self.session_timeout * 60) return time_diff.total_seconds() > (self.session_timeout * 60)
def has_permission(self, permission_name): def has_permission(self, permission_name):
"""Verifica se o usuário tem uma determinada permissão""" """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 role in self.roles:
for permission in role.permissions: for permission in role.permissions:
if permission.nome == permission_name: if permission.nome == permission_name:
@@ -621,86 +617,47 @@ class TransacaoPIX(Base):
pagamento = relationship("Pagamento", back_populates="transacoes_pix") pagamento = relationship("Pagamento", back_populates="transacoes_pix")
# Remover o banco de dados existente (se existir)
if os.path.exists(db_path):
os.remove(db_path)
def init_rbac():
"""Inicializa o sistema RBAC"""
print("Inicializando sistema RBAC...")
session = SessionLocal()
try:
# Verificar se já existe um admin
admin = session.query(Usuario).filter_by(username="admin").first()
if not admin:
print("Criando role de administrador...")
# Criar role de admin
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()
print("Criando usuário admin...")
# Criar usuário admin
admin = Usuario(
username="admin",
password="admin123",
is_admin=True
)
admin.email = "admin@example.com"
admin.role_id = admin_role.id
# Adicionar apenas a permissão de system_config ao admin
permission = session.query(Permission).filter_by(nome='system_config').first()
if permission and permission not in admin_role.permissions:
admin_role.permissions.append(permission)
session.add(admin)
session.commit()
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}")
else:
print("Usuário admin já existe")
# Garantir que o admin tenha apenas a permissão de system_config
admin_role = session.query(Role).filter_by(nome="Administrador").first()
if admin_role:
# Remover todas as permissões atuais
admin_role.permissions = []
# Adicionar apenas a permissão de system_config
permission = session.query(Permission).filter_by(nome='system_config').first()
if permission:
admin_role.permissions.append(permission)
session.commit()
except Exception as e:
print(f"Erro na inicialização do sistema RBAC: {e}")
session.rollback()
raise
finally:
session.close()
def init_database(): def init_database():
"""Inicializa o banco de dados com dados básicos""" """Inicializa o banco de dados com dados básicos"""
print("Inicializando banco de dados...") print("Inicializando banco de dados...")
# Criar todas as tabelas session = get_db_connection()
Base.metadata.drop_all(engine) # Remover todas as tabelas existentes
Base.metadata.create_all(engine)
session = SessionLocal()
try: try:
# Criar role de administrador # Configurar SQLite para melhor tratamento de concorrência
admin_role = Role(nome="Administrador", nivel=Role.SECRETARIO_GERAL) session.execute(text("PRAGMA journal_mode=WAL"))
session.add(admin_role) session.execute(text("PRAGMA busy_timeout=5000"))
# Criar todas as tabelas
Base.metadata.drop_all(engine) # Remover todas as tabelas existentes
Base.metadata.create_all(engine)
# Criar roles padrão
roles = [
("Administrador", Role.SECRETARIO_GERAL),
("Secretário", Role.SECRETARIO_CELULA),
("Militante", Role.MILITANTE_BASICO)
]
for nome, nivel in roles:
if not session.query(Role).filter_by(nome=nome).first():
role = Role(nome=nome, nivel=nivel)
session.add(role)
session.commit()
# Criar setores padrão
setores = ["Setor 1", "Setor 2", "Setor 3"]
for nome in setores:
if not session.query(Setor).filter_by(nome=nome).first():
setor = Setor(nome=nome)
session.add(setor)
session.commit()
# Criar comitês padrão
comites = ["Comitê 1", "Comitê 2", "Comitê 3"]
for nome in comites:
if not session.query(ComiteCentral).filter_by(nome=nome).first():
comite = ComiteCentral(nome=nome)
session.add(comite)
session.commit() session.commit()
# Verificar se existe um QR code salvo # Verificar se existe um QR code salvo
@@ -708,12 +665,10 @@ def init_database():
admin_otp_secret = None admin_otp_secret = None
if qr_path.exists(): if qr_path.exists():
# Extrair o segredo OTP do nome do arquivo temporário dentro do QR
try: try:
import re import re
with open('admin_qr.txt', 'r') as f: with open('admin_qr.txt', 'r') as f:
qr_content = f.read() qr_content = f.read()
# O segredo OTP está no formato otpauth://totp/admin?secret=XXXXX&issuer=Sistema%20de%20Controles
match = re.search(r'secret=([A-Z0-9]+)&', qr_content) match = re.search(r'secret=([A-Z0-9]+)&', qr_content)
if match: if match:
admin_otp_secret = match.group(1) admin_otp_secret = match.group(1)
@@ -726,15 +681,19 @@ def init_database():
print(f"Novo OTP gerado: {admin_otp_secret}") print(f"Novo OTP gerado: {admin_otp_secret}")
# Criar usuário admin # Criar usuário admin
admin_role = session.query(Role).filter_by(nome="Administrador").first()
setor = session.query(Setor).first()
admin = Usuario( admin = Usuario(
username="admin", username="admin",
password="admin123",
is_admin=True,
email="admin@example.com", email="admin@example.com",
tipo="ADMIN" is_admin=True
) )
admin.role_id = admin_role.id admin.set_password("admin123")
admin.tipo = "ADMIN"
admin.otp_secret = admin_otp_secret admin.otp_secret = admin_otp_secret
admin.roles.append(admin_role)
admin.setor = setor
session.add(admin) session.add(admin)
session.commit() session.commit()
@@ -743,11 +702,9 @@ def init_database():
totp = pyotp.totp.TOTP(admin_otp_secret) totp = pyotp.totp.TOTP(admin_otp_secret)
provisioning_uri = totp.provisioning_uri("admin", issuer_name="Sistema de Controles") provisioning_uri = totp.provisioning_uri("admin", issuer_name="Sistema de Controles")
# Salvar a URI em um arquivo texto para referência futura
with open('admin_qr.txt', 'w') as f: with open('admin_qr.txt', 'w') as f:
f.write(provisioning_uri) f.write(provisioning_uri)
# Gerar QR code
import qrcode import qrcode
qr = qrcode.QRCode(version=1, box_size=10, border=5) qr = qrcode.QRCode(version=1, box_size=10, border=5)
qr.add_data(provisioning_uri) qr.add_data(provisioning_uri)
@@ -762,6 +719,12 @@ def init_database():
print(f"OTP Secret: {admin.otp_secret}") print(f"OTP Secret: {admin.otp_secret}")
print(f"QR Code: {qr_path}") 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: except Exception as e:
print(f"Erro na inicialização do banco: {e}") print(f"Erro na inicialização do banco: {e}")
session.rollback() session.rollback()
@@ -769,12 +732,5 @@ def init_database():
finally: finally:
session.close() session.close()
# Inicializar o sistema RBAC
init_rbac()
# Inicializar o banco de dados automaticamente quando o módulo for importado
init_database()
# Executar a criação dos dados iniciais
if __name__ == "__main__": if __name__ == "__main__":
init_database() init_database()

View File

@@ -10,7 +10,7 @@ def require_login(f):
@wraps(f) @wraps(f)
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
if not current_user.is_authenticated: if not current_user.is_authenticated:
flash('Por favor, faça login para acessar esta página.', 'error') flash('Por favor, faça login para acessar esta página.', 'danger')
return redirect(url_for('login')) return redirect(url_for('login'))
db = get_db_connection() db = get_db_connection()
@@ -21,7 +21,7 @@ def require_login(f):
).get(current_user.id) ).get(current_user.id)
if not user: if not user:
flash('Usuário não encontrado.', 'error') flash('Usuário não encontrado.', 'danger')
return redirect(url_for('login')) return redirect(url_for('login'))
# Atualiza timestamp da última atividade # Atualiza timestamp da última atividade
@@ -39,11 +39,11 @@ def require_permission(permission_name):
@wraps(f) @wraps(f)
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
if not current_user.is_authenticated: if not current_user.is_authenticated:
flash('Por favor, faça login para acessar esta página.', 'error') flash('Por favor, faça login para acessar esta página.', 'danger')
return redirect(url_for('login')) return redirect(url_for('login'))
if not current_user.has_permission(permission_name): if not current_user.has_permission(permission_name):
flash('Você não tem permissão para acessar esta página.', 'error') flash('Você não tem permissão para acessar esta página.', 'danger')
return redirect(url_for('home')) return redirect(url_for('home'))
return f(*args, **kwargs) return f(*args, **kwargs)

View File

@@ -73,6 +73,7 @@ class Permission(Base):
MANAGE_CELL_MEMBERS = "manage_cell_members" MANAGE_CELL_MEMBERS = "manage_cell_members"
CREATE_CELL_MEMBER = "create_cell_member" CREATE_CELL_MEMBER = "create_cell_member"
VIEW_CELL_REPORTS = "view_cell_reports" VIEW_CELL_REPORTS = "view_cell_reports"
MANAGE_CELL_REPORTS = "manage_cell_reports" # Nova permissão
REGISTER_CELL_PAYMENT = "register_cell_payment" REGISTER_CELL_PAYMENT = "register_cell_payment"
# Permissões de setor # Permissões de setor
@@ -107,6 +108,7 @@ class Permission(Base):
(Permission.MANAGE_CELL_MEMBERS, "Gerenciar membros da célula"), (Permission.MANAGE_CELL_MEMBERS, "Gerenciar membros da célula"),
(Permission.CREATE_CELL_MEMBER, "Criar membros na célula"), (Permission.CREATE_CELL_MEMBER, "Criar membros na célula"),
(Permission.VIEW_CELL_REPORTS, "Visualizar relatórios da 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"), (Permission.REGISTER_CELL_PAYMENT, "Registrar pagamentos da célula"),
# Permissões de setor # Permissões de setor
@@ -131,18 +133,26 @@ class Permission(Base):
def init_rbac(): def init_rbac():
"""Inicializa o sistema RBAC com roles e permissões básicas""" """Inicializa o sistema RBAC com roles e permissões básicas"""
from .database import get_db_connection from .database import Usuario, get_db_connection
session = get_db_connection() session = get_db_connection()
try: try:
# Criar roles se não existirem # Criar role de administrador primeiro
for nivel, nome in Role.get_roles_list(): admin_role = session.query(Role).filter_by(nome="Administrador").first()
role = session.query(Role).filter_by(nivel=nivel).first() if not admin_role:
if not role: admin_role = Role(nome="Administrador", nivel=Role.SECRETARIO_GERAL)
role = Role(nome=nome, nivel=nivel) session.add(admin_role)
session.add(role) session.commit()
# Criar permissões se não existirem # 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(): for nome, descricao in Permission.get_permissions_list():
permission = session.query(Permission).filter_by(nome=nome).first() permission = session.query(Permission).filter_by(nome=nome).first()
if not permission: if not permission:
@@ -151,8 +161,20 @@ def init_rbac():
session.commit() session.commit()
# Mapear permissões para roles # Dar todas as permissões para o admin
for role in session.query(Role).all(): 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 # Militante Básico
if role.nivel == Role.MILITANTE_BASICO: if role.nivel == Role.MILITANTE_BASICO:
role.permissions = [ role.permissions = [
@@ -170,6 +192,7 @@ def init_rbac():
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).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.CREATE_CELL_MEMBER).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).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() session.query(Permission).filter_by(nome=Permission.REGISTER_CELL_PAYMENT).first()
] ]
@@ -182,6 +205,7 @@ def init_rbac():
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).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.CREATE_CELL_MEMBER).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).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.VIEW_SECTOR_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.REGISTER_SECTOR_PAYMENT).first() session.query(Permission).filter_by(nome=Permission.REGISTER_SECTOR_PAYMENT).first()
] ]
@@ -195,6 +219,7 @@ def init_rbac():
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).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.CREATE_CELL_MEMBER).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).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.VIEW_SECTOR_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).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.CREATE_SECTOR_CELL).first(),
@@ -210,6 +235,7 @@ def init_rbac():
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).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.CREATE_CELL_MEMBER).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).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.VIEW_SECTOR_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).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.CREATE_SECTOR_CELL).first(),
@@ -226,6 +252,7 @@ def init_rbac():
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).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.CREATE_CELL_MEMBER).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).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.VIEW_SECTOR_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).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.CREATE_SECTOR_CELL).first(),
@@ -244,6 +271,7 @@ def init_rbac():
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).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.CREATE_CELL_MEMBER).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).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.VIEW_SECTOR_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).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.CREATE_SECTOR_CELL).first(),
@@ -263,6 +291,7 @@ def init_rbac():
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).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.CREATE_CELL_MEMBER).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).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.VIEW_SECTOR_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).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.CREATE_SECTOR_CELL).first(),
@@ -276,12 +305,6 @@ def init_rbac():
session.query(Permission).filter_by(nome=Permission.SYSTEM_CONFIG).first() session.query(Permission).filter_by(nome=Permission.SYSTEM_CONFIG).first()
] ]
# Administrador
elif role.nome == "Administrador":
role.permissions = [
session.query(Permission).filter_by(nome=Permission.SYSTEM_CONFIG).first()
]
session.commit() session.commit()
except Exception as e: except Exception as e:

23
models.py Normal file
View File

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

View File

@@ -14,3 +14,4 @@ cryptography==42.0.2
bcrypt==4.1.2 bcrypt==4.1.2
Bootstrap-Flask==2.3.3 Bootstrap-Flask==2.3.3
flask-bootstrap5==0.1.dev1 flask-bootstrap5==0.1.dev1
PyJWT==2.8.0

View File

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

32
seed.py Normal file
View File

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

305
seed_data.py Normal file
View File

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

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

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

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
static/img/logo001.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

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

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

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

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

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

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

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

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

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

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

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

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

View File

@@ -1,156 +1,627 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="pt-BR"> <html lang="pt-br">
<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 Controle OCI</title> <meta name="csrf-token" content="{{ csrf_token() }}">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <title>{% block title %}{% endblock %} - Controles OCI</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" rel="stylesheet">
<!-- 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> <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 { body {
padding-top: 56px; 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 { .navbar-brand {
font-weight: bold; flex: 0 0 auto;
} margin-right: 2rem;
.nav-link {
font-weight: 500; 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 { .card {
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); border: none;
border-radius: 0.5rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
height: 100%;
} }
.card-header { .card-header {
background-color: #f8f9fa; background-color: #f8f9fa;
border-bottom: 1px solid #dee2e6; border-bottom: 1px solid #e9ecef;
padding: 1rem;
} }
.btn-primary {
background-color: #0d6efd; .card-header .card-title {
border-color: #0d6efd; margin: 0;
display: flex;
align-items: center;
gap: 0.5rem;
} }
.btn-success {
background-color: #198754; .card-header h5 {
border-color: #198754; margin: 0;
display: flex;
align-items: center;
font-size: 1.1rem;
} }
.btn-secondary {
background-color: #6c757d; .card-header h5 i {
border-color: #6c757d; margin-right: 0.75rem;
color: var(--primary-color);
} }
.btn-outline-primary {
color: #0d6efd; .card-body {
border-color: #0d6efd; padding: 1.5rem;
} }
.btn-outline-primary:hover {
background-color: #0d6efd; .card-footer {
color: #fff; 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 { .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; border-radius: 0.25rem;
font-weight: 500;
} }
.form-control:focus {
border-color: #0d6efd; .btn-primary {
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); background-color: var(--primary-color);
border-color: var(--primary-color);
} }
.form-select:focus {
border-color: #0d6efd; .btn-success {
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); 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> </style>
{% block extra_css %}{% endblock %}
</head> </head>
<body> <body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top"> {% block navbar %}
<div class="container"> <nav class="navbar navbar-expand-lg navbar-dark">
<a class="navbar-brand" href="{{ url_for('home') }}">Sistema de Controle OCI</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 me-auto"> <ul class="navbar-nav mx-auto">
{% if current_user is defined and current_user.is_authenticated %} <li class="nav-item dropdown">
{% if current_user.is_admin %} <a class="nav-link" href="#" data-bs-toggle="dropdown">
<li class="nav-item"> <i class="fas fa-users me-1"></i>Militantes
<a class="nav-link" href="{{ url_for('dashboard_admin') }}">Dashboard Admin</a> </a>
</li> <ul class="dropdown-menu">
{% else %} <li>
<li class="nav-item"> <a class="dropdown-item" href="{{ url_for('listar_militantes') }}">
<a class="nav-link" href="{{ url_for('home') }}">Início</a> <i class="fas fa-list"></i>Listar Militantes
</li> </a>
</li>
{% if current_user.has_permission('view_cell_data') %} </ul>
<li class="nav-item"> </li>
<a class="nav-link" href="{{ url_for('listar_militantes') }}">Militantes</a> <li class="nav-item dropdown">
</li> <a class="nav-link" href="#" data-bs-toggle="dropdown">
{% endif %} <i class="fas fa-dollar-sign me-1"></i>Financeiro
</a>
{% if current_user.has_permission('view_cell_reports') %} <ul class="dropdown-menu">
<li class="nav-item"> <li>
<a class="nav-link" href="{{ url_for('listar_pagamentos') }}">Pagamentos</a> <a class="dropdown-item" href="{{ url_for('listar_cotas') }}">
</li> <i class="fas fa-money-bill-wave"></i>Cotas
<li class="nav-item"> </a>
<a class="nav-link" href="{{ url_for('listar_materiais') }}">Materiais</a> </li>
</li> <li>
<li class="nav-item"> <a class="dropdown-item" href="{{ url_for('listar_pagamentos') }}">
<a class="nav-link" href="{{ url_for('listar_relatorios_vendas') }}">Vendas</a> <i class="fas fa-receipt"></i>Pagamentos
</li> </a>
{% endif %} </li>
</ul>
{% if current_user.has_permission('view_cell_reports') or current_user.has_permission('view_sector_reports') or current_user.has_permission('view_cr_reports') %} </li>
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown"> <a class="nav-link" href="#" data-bs-toggle="dropdown">
Relatórios <i class="fas fa-box me-1"></i>Materiais
</a> </a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
{% if current_user.has_permission('view_cell_reports') %} <li>
<li><a class="dropdown-item" href="{{ url_for('listar_relatorios_cotas') }}">Relatórios de Cotas</a></li> <a class="dropdown-item" href="{{ url_for('listar_materiais') }}">
<li><a class="dropdown-item" href="{{ url_for('listar_relatorios_vendas') }}">Relatórios de Vendas</a></li> <i class="fas fa-box"></i>Listar Materiais
{% endif %} </a>
</ul> </li>
</li> <li>
{% endif %} <a class="dropdown-item" href="{{ url_for('listar_vendas_jornal') }}">
{% endif %} <i class="fas fa-newspaper"></i>Vendas de Jornais
{% endif %} </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>
<ul class="navbar-nav"> <ul class="navbar-nav">
{% if current_user is defined and current_user.is_authenticated %} <li class="nav-item dropdown">
<li class="nav-item"> <a class="nav-link" href="#" data-bs-toggle="dropdown">
<a class="nav-link" href="{{ url_for('logout') }}">Sair</a> <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><hr class="dropdown-divider"></li>
{% endif %}
<li>
<a class="dropdown-item" href="{{ url_for('logout') }}">
<i class="fas fa-sign-out-alt"></i>Sair
</a>
</li>
</ul>
</li> </li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('login') }}">Login</a>
</li>
{% endif %}
</ul> </ul>
</div> </div>
{% endif %}
</div> </div>
</nav> </nav>
{% endblock %}
<div class="container mt-4"> <div class="page-wrapper">
{% block content %}{% endblock %} <div class="container py-4">
{% block content %}{% endblock %}
</div>
</div> </div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <!-- Bootstrap 5 JS Bundle with Popper -->
<script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
// Verificar status da sessão a cada 5 minutos {% block scripts %}{% endblock %}
function checkSession() {
fetch('/check_session')
.then(response => response.json())
.then(data => {
if (data.expired) {
window.location.href = '/login';
}
})
.catch(error => console.error('Erro ao verificar sessão:', error));
}
// Verificar a cada 5 minutos
setInterval(checkSession, 5 * 60 * 1000);
// Verificar também quando a página ganha foco
document.addEventListener('visibilitychange', function() {
if (document.visibilityState === 'visible') {
checkSession();
}
});
</script>
</body> </body>
</html> </html>

View File

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

View File

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

View File

@@ -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>
<thead> <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalNovaAssinatura">
<tr> <i class="fas fa-plus me-2"></i>Nova Assinatura
<th>ID</th> </button>
<th>Militante ID</th> </div>
<th>Tipo Material</th>
<th>Quantidade</th> <div class="card">
<th>Valor Total</th> <div class="card-body p-0">
<th>Data Início</th> {% if assinaturas %}
<th>Data Fim</th> <div class="table-responsive">
</tr> <table class="table table-hover">
</thead> <thead>
<tbody> <tr>
{% for assinatura in assinaturas %} <th>Militante</th>
<tr> <th>Data Início</th>
<td>{{ assinatura.id }}</td> <th>Data Fim</th>
<td>{{ assinatura.militante_id }}</td> <th>Status</th>
<td>{{ assinatura.tipo_material_id }}</td> <th>Valor</th>
<td>{{ assinatura.quantidade }}</td> <th>Ações</th>
<td>R$ {{ assinatura.valor_total }}</td> </tr>
<td>{{ assinatura.data_inicio }}</td> </thead>
<td>{{ assinatura.data_fim }}</td> <tbody>
</tr> {% for assinatura in assinaturas %}
{% endfor %} <tr>
</tbody> <td>{{ assinatura.militante.nome }}</td>
</table> <td>{{ assinatura.data_inicio.strftime('%d/%m/%Y') }}</td>
<a href="{{ url_for('home') }}">Home</a> <td>{{ assinatura.data_fim.strftime('%d/%m/%Y') }}</td>
<td>
{% if assinatura.ativa %}
<span class="badge bg-success">Ativa</span>
{% else %}
<span class="badge bg-danger">Inativa</span>
{% endif %}
</td>
<td>R$ {{ "%.2f"|format(assinatura.valor) }}</td>
<td>
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-primary"
onclick="editarAssinatura({{ assinatura.id }})">
<i class="fas fa-edit"></i>
</button>
<button type="button" class="btn btn-sm btn-outline-danger"
onclick="confirmarExclusao({{ assinatura.id }})">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</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 %}

View File

@@ -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">
<thead> <h1 class="mb-0">
<tr> <i class="fas fa-money-bill me-2"></i>Cotas
<th>ID</th> </h1>
<th>Militante ID</th> <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalNovaCota">
<th>Valor Antigo</th> <i class="fas fa-plus me-2"></i>Nova Cota
<th>Valor Novo</th> </button>
<th>Data de Alteração</th> </div>
</tr> </div>
</thead> </div>
<tbody>
{% for cota in cotas %} <div class="card shadow-sm">
<tr> <div class="card-body">
<td>{{ cota.id }}</td> <div class="row mb-4">
<td>{{ cota.militante_id }}</td> <div class="col-md-6">
<td>R$ {{ cota.valor_antigo }}</td> <div class="input-group">
<td>R$ {{ cota.valor_novo }}</td> <span class="input-group-text">
<td>{{ cota.data_alteracao }}</td> <i class="fas fa-search"></i>
</tr> </span>
{% endfor %} <input type="text" class="form-control" id="searchInput" placeholder="Pesquisar cotas...">
</tbody> </div>
</table> </div>
<a href="{{ url_for('home') }}">Home</a> <div class="col-md-6 text-end">
<button id="btnExportar" class="btn btn-outline-primary">
<i class="fas fa-download me-2"></i>Exportar
</button>
</div>
</div>
<div class="table-responsive">
<table class="table table-hover" id="cotasTable">
<thead>
<tr>
<th data-sort="militante">Militante <i class="fas fa-sort"></i></th>
<th data-sort="valor_antigo">Valor Antigo <i class="fas fa-sort"></i></th>
<th data-sort="valor_novo">Valor Novo <i class="fas fa-sort"></i></th>
<th data-sort="data_alteracao">Data de Alteração <i class="fas fa-sort"></i></th>
<th data-sort="data_vencimento">Data de Vencimento <i class="fas fa-sort"></i></th>
<th data-sort="status">Status <i class="fas fa-sort"></i></th>
<th class="text-end">Ações</th>
</tr>
</thead>
<tbody>
{% for cota in cotas %}
<tr>
<td data-militante="{{ cota.militante.nome }}">{{ cota.militante.nome }}</td>
<td data-valor_antigo="{{ cota.valor_antigo }}">R$ {{ "%.2f"|format(cota.valor_antigo) }}</td>
<td data-valor_novo="{{ cota.valor_novo }}">R$ {{ "%.2f"|format(cota.valor_novo) }}</td>
<td data-data_alteracao="{{ cota.data_alteracao }}">{{ cota.data_alteracao.strftime('%d/%m/%Y') }}</td>
<td data-data_vencimento="{{ cota.data_vencimento }}">{{ cota.data_vencimento.strftime('%d/%m/%Y') }}</td>
<td data-status="{{ cota.status }}">
{% if cota.status == 'paga' %}
<span class="badge bg-success">Paga</span>
{% elif cota.status == 'atrasada' %}
<span class="badge bg-danger">Atrasada</span>
{% else %}
<span class="badge bg-warning text-dark">Pendente</span>
{% endif %}
</td>
<td class="text-end">
<div class="btn-group">
<button type="button"
class="btn btn-sm btn-outline-primary"
data-bs-toggle="modal"
data-bs-target="#modalEditarCota"
data-cota-id="{{ cota.id }}"
data-cota-militante="{{ cota.militante_id }}"
data-cota-valor-antigo="{{ cota.valor_antigo }}"
data-cota-valor-novo="{{ cota.valor_novo }}"
data-cota-data-alteracao="{{ cota.data_alteracao.strftime('%Y-%m-%d') }}"
data-cota-data-vencimento="{{ cota.data_vencimento.strftime('%Y-%m-%d') }}"
data-cota-pago="{{ 'true' if cota.pago else 'false' }}"
title="Editar">
<i class="fas fa-edit"></i>
</button>
<button type="button"
class="btn btn-sm btn-outline-danger"
data-bs-toggle="modal"
data-bs-target="#deleteModal"
data-cota-id="{{ cota.id }}"
data-cota-info="{{ cota.militante.nome }} - R$ {{ "%.2f"|format(cota.valor_novo) }}"
title="Excluir">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</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 %}

View File

@@ -1,58 +1,330 @@
{% extends 'base.html' %} {% extends "base.html" %}
{% block title %}Listar Materiais{% endblock %} {% block title %}Materiais{% endblock %}
{% block content %} {% block content %}
<div class="container"> <div class="row mb-4">
<div class="row"> <div class="col-12">
<div class="col-md-12"> <div class="d-flex justify-content-between align-items-center">
<h1 class="mb-4">Lista de Materiais</h1> <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>
{% with messages = get_flashed_messages(with_categories=true) %} <div class="card shadow-sm">
{% if messages %} <div class="card-body">
{% for category, message in messages %} <div class="row mb-4">
<div class="alert alert-{{ category }}">{{ message }}</div> <div class="col-md-6">
{% endfor %} <div class="input-group">
{% endif %} <span class="input-group-text">
{% endwith %} <i class="fas fa-search"></i>
</span>
<div class="d-flex justify-content-between mb-4"> <input type="text" class="form-control" id="searchInput" placeholder="Pesquisar materiais...">
<a href="{{ url_for('novo_material') }}" class="btn btn-success">Novo Material</a> </div>
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
</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"> <div class="table-responsive">
<table class="table table-striped table-hover"> <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>Nome</th> <th data-sort="tipo">Tipo <i class="fas fa-sort"></i></th>
<th>Descrição</th> <th data-sort="descricao">Descrição <i class="fas fa-sort"></i></th>
<th>Preço</th> <th data-sort="valor">Valor <i class="fas fa-sort"></i></th>
<th>Quantidade</th> <th data-sort="data">Data <i class="fas fa-sort"></i></th>
<th>Tipo</th> <th class="text-end">Ações</th>
<th>Ações</th> </tr>
</tr> </thead>
</thead> <tbody>
<tbody> {% for material in materiais %}
{% for material in materiais %} <tr>
<tr> <td data-militante="{{ material.militante.nome }}">{{ material.militante.nome }}</td>
<td>{{ material.id }}</td> <td data-tipo="{{ material.tipo_material.nome }}">{{ material.tipo_material.nome }}</td>
<td>{{ material.nome }}</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$ {{ "%.2f"|format(material.preco) }}</td> <td data-data="{{ material.data_venda }}">{{ material.data_venda.strftime('%d/%m/%Y') }}</td>
<td>{{ material.quantidade }}</td> <td class="text-end">
<td>{{ material.tipo.nome }}</td> <div class="btn-group">
<td> <button type="button"
<a href="{{ url_for('editar_material', id=material.id) }}" class="btn btn-primary btn-sm">Editar</a> class="btn btn-sm btn-outline-primary"
<a href="{{ url_for('deletar_material', id=material.id) }}" class="btn btn-danger btn-sm" onclick="return confirm('Tem certeza que deseja excluir este material?')">Excluir</a> data-bs-toggle="modal"
</td> data-bs-target="#modalEditarMaterial"
</tr> data-material-id="{{ material.id }}"
{% endfor %} data-material-militante="{{ material.militante_id }}"
</tbody> data-material-tipo="{{ material.tipo_material_id }}"
</table> data-material-descricao="{{ material.descricao }}"
data-material-valor="{{ material.valor }}"
data-material-data="{{ material.data_venda.strftime('%Y-%m-%d') }}"
title="Editar">
<i class="fas fa-edit"></i>
</button>
<button type="button"
class="btn btn-sm btn-outline-danger"
data-bs-toggle="modal"
data-bs-target="#deleteModal"
data-material-id="{{ material.id }}"
data-material-info="{{ material.militante.nome }} - {{ material.tipo_material.nome }}"
title="Excluir">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</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>
</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 %}

View File

@@ -1,71 +1,298 @@
{% extends 'base.html' %} {% extends "base.html" %}
{% block title %}Lista de Militantes{% endblock %} {% block title %}Militantes{% endblock %}
{% block content %} {% block content %}
<div class="container"> <div class="row mb-4">
<div class="row"> <div class="col-12">
<div class="col-md-12"> <div class="d-flex justify-content-between align-items-center">
<h1 class="mb-4">Lista de Militantes</h1> <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>
{% with messages = get_flashed_messages(with_categories=true) %} <div class="card">
{% if messages %} <div class="card-body">
{% for category, message in messages %} <div class="row mb-4">
<div class="alert alert-{{ category }}">{{ message }}</div> <div class="col-md-6">
{% endfor %} <div class="input-group">
{% endif %} <span class="input-group-text">
{% endwith %} <i class="fas fa-search"></i>
</span>
<div class="d-flex justify-content-between mb-4"> <input type="text" class="form-control" id="searchInput" placeholder="Pesquisar militantes...">
<a href="{{ url_for('criar_militante') }}" class="btn btn-primary">Novo Militante</a> </div>
</div> </div>
<div class="col-md-6 text-end">
<div class="table-responsive"> <div class="btn-group me-2">
<table class="table table-striped"> <button type="button" class="btn btn-outline-secondary btn-fixed-width dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<thead> <i class="fas fa-filter me-2"></i>Filtrar
<tr> </button>
<th>Nome</th> <ul class="dropdown-menu">
<th>Email</th> <li><h6 class="dropdown-header">Status</h6></li>
<th>Célula</th> <li><a class="dropdown-item" href="#" data-filter="todos">Todos</a></li>
<th>Responsabilidades</th> <li><hr class="dropdown-divider"></li>
<th>Ações</th> <li><h6 class="dropdown-header">Responsabilidades</h6></li>
</tr> <li><a class="dropdown-item" href="#" data-filter="financas">Finanças</a></li>
</thead> <li><a class="dropdown-item" href="#" data-filter="imprensa">Imprensa</a></li>
<tbody> <li><a class="dropdown-item" href="#" data-filter="quadro-orientador">Quadro-Orientador</a></li>
{% for militante in militantes %} <li><hr class="dropdown-divider"></li>
<tr> <li><h6 class="dropdown-header">Célula</h6></li>
<td>{{ militante.nome }}</td> {% for celula in celulas %}
<td>{{ militante.email }}</td> <li><a class="dropdown-item" href="#" data-filter="celula" data-celula="{{ celula.nome }}">{{ celula.nome }}</a></li>
<td>{{ militante.celula.nome }}</td>
<td>
{% if militante.responsabilidades & Militante.RESPONSAVEL_FINANCAS %}
<span class="badge bg-primary">Finanças</span>
{% endif %}
{% if militante.responsabilidades & Militante.RESPONSAVEL_IMPRENSA %}
<span class="badge bg-info">Imprensa</span>
{% endif %}
{% if militante.responsabilidades & Militante.QUADRO_ORIENTADOR %}
<span class="badge bg-success">Quadro-Orientador</span>
{% endif %}
</td>
<td>
<a href="{{ url_for('editar_militante', id=militante.id) }}" class="btn btn-sm btn-warning">Editar</a>
<button type="button" class="btn btn-sm btn-danger" onclick="confirmarExclusao({{ militante.id }})">Excluir</button>
</td>
</tr>
{% endfor %} {% endfor %}
</tbody> </ul>
</table> </div>
<button class="btn btn-outline-primary btn-fixed-width" type="button" id="btnExportar">
<i class="fas fa-file-export me-2"></i>Exportar
</button>
</div>
</div>
<div class="table-responsive">
<table class="table table-hover" id="militantesTable">
<thead>
<tr>
<th data-sort="nome">Nome <i class="fas fa-sort"></i></th>
<th data-sort="cpf">CPF <i class="fas fa-sort"></i></th>
<th data-sort="email">Email <i class="fas fa-sort"></i></th>
<th data-sort="telefone">Telefone <i class="fas fa-sort"></i></th>
<th data-sort="celula">Célula <i class="fas fa-sort"></i></th>
<th>Responsabilidades</th>
<th class="text-end">Ações</th>
</tr>
</thead>
<tbody>
{% for militante in militantes %}
<tr data-militante="{{ militante.id }}" data-filiado="{{ 'sim' if militante.filiado else 'nao' }}">
<td data-nome="{{ militante.nome }}">{{ militante.nome }}</td>
<td data-cpf="{{ militante.cpf }}">{{ militante.cpf }}</td>
<td data-email="{{ militante.email }}">{{ militante.email }}</td>
<td data-telefone="{{ militante.telefone }}">{{ militante.telefone }}</td>
<td data-celula="{{ militante.celula.nome }}">{{ militante.celula.nome }}</td>
<td>
{% if militante.responsabilidades|bitwise_and(Militante.RESPONSAVEL_FINANCAS) %}
<span class="badge bg-primary">Finanças</span>
{% endif %}
{% if militante.responsabilidades|bitwise_and(Militante.RESPONSAVEL_IMPRENSA) %}
<span class="badge bg-info">Imprensa</span>
{% endif %}
{% if militante.responsabilidades|bitwise_and(Militante.QUADRO_ORIENTADOR) %}
<span class="badge bg-success">Quadro-Orientador</span>
{% endif %}
</td>
<td class="text-end">
<div class="btn-group">
{% if current_user.has_permission('gerenciar_militantes') %}
<button type="button"
class="btn btn-sm btn-outline-primary"
data-bs-toggle="modal"
data-bs-target="#modalEditarMilitante"
data-militante-id="{{ militante.id }}"
data-militante-nome="{{ militante.nome }}"
data-militante-cpf="{{ militante.cpf }}"
data-militante-email="{{ militante.email }}"
data-militante-telefone="{{ militante.telefone }}"
data-militante-endereco="{{ militante.endereco }}"
data-militante-filiado="{{ militante.filiado }}"
title="Editar">
<i class="fas fa-edit"></i>
</button>
<button type="button"
class="btn btn-sm btn-outline-danger"
data-bs-toggle="modal"
data-bs-target="#deleteModal"
data-militante-id="{{ militante.id }}"
data-militante-nome="{{ militante.nome }}"
title="Excluir">
<i class="fas fa-trash"></i>
</button>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="pagination-container d-flex justify-content-between align-items-center">
<div class="text-muted">
Mostrando <span id="countMilitantes">{{ militantes|length }}</span> militantes
</div>
<div class="d-flex align-items-center gap-3">
<div class="d-flex align-items-center">
<span class="me-2">Mostrar</span>
<select class="form-select form-select-sm me-2" id="rowsPerPage" style="width: auto;">
<option value="10">10</option>
<option value="20" selected>20</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
<span>linhas</span>
</div>
<nav aria-label="Navegação de páginas">
<ul class="pagination mb-0">
<li class="page-item disabled" id="prevPage">
<a class="page-link" href="#"><i class="fas fa-chevron-left"></i></a>
</li>
<li class="page-item active"><a class="page-link" href="#">1</a></li>
<li class="page-item"><a class="page-link" href="#">2</a></li>
<li class="page-item"><a class="page-link" href="#">3</a></li>
<li class="page-item" id="nextPage">
<a class="page-link" href="#"><i class="fas fa-chevron-right"></i></a>
</li>
</ul>
</nav>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<script> <!-- Modais -->
function confirmarExclusao(id) { {% include 'modals/militante_novo.html' %}
if (confirm('Tem certeza que deseja excluir este militante?')) { {% include 'modals/militante_editar.html' %}
window.location.href = "{{ url_for('excluir_militante', id=0) }}".replace('0', id); {% include 'modals/militante_excluir.html' %}
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/militantes.js') }}"></script>
{% endblock %}
{% block extra_css %}
<style>
/* 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 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;
} }
} }
</script> </style>
{% endblock %} {% endblock %}

View File

@@ -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>
<thead> <div>
<tr> <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalNovoPagamento">
<th>ID</th> <i class="fas fa-plus"></i> Novo Pagamento
<th>Militante ID</th> </button>
<th>Tipo de Pagamento</th> <button type="button" class="btn btn-outline-primary" id="btnExportar">
<th>Valor</th> <i class="fas fa-file-export"></i> Exportar
<th>Data do Pagamento</th> </button>
</tr> </div>
</thead> </div>
<tbody>
{% for pagamento in pagamentos %} <div class="card">
<tr> <div class="card-body">
<td>{{ pagamento.id }}</td> <div class="table-responsive">
<td>{{ pagamento.militante_id }}</td> <table class="table table-striped table-hover" id="tabelaPagamentos">
<td>{{ pagamento.tipo_pagamento_id }}</td> <thead>
<td>R$ {{ pagamento.valor }}</td> <tr>
<td>{{ pagamento.data_pagamento }}</td> <th>Militante</th>
</tr> <th>Tipo de Pagamento</th>
{% endfor %} <th>Valor</th>
</tbody> <th>Data do Pagamento</th>
</table> <th>Ações</th>
<a href="{{ url_for('home') }}">Home</a> </tr>
</thead>
<tbody>
{% for pagamento in pagamentos %}
<tr>
<td data-militante="{{ pagamento.militante_id }}">{{ pagamento.militante.nome if pagamento.militante else 'N/A' }}</td>
<td data-tipo="{{ pagamento.tipo_pagamento }}">
{% if pagamento.tipo_pagamento == 1 %}
Mensalidade
{% elif pagamento.tipo_pagamento == 2 %}
Contribuição Extra
{% elif pagamento.tipo_pagamento == 3 %}
Doação
{% elif pagamento.tipo_pagamento == 4 %}
Taxa de Evento
{% elif pagamento.tipo_pagamento == 5 %}
Outros
{% else %}
Não Definido
{% endif %}
</td>
<td data-valor="{{ pagamento.valor }}">R$ {{ "%.2f"|format(pagamento.valor) }}</td>
<td data-data="{{ pagamento.data_pagamento }}">{{ pagamento.data_pagamento.strftime('%d/%m/%Y') }}</td>
<td>
<button type="button"
class="btn btn-sm btn-outline-primary"
data-bs-toggle="modal"
data-bs-target="#modalEditarPagamento"
data-pagamento-id="{{ pagamento.id }}"
data-militante-id="{{ pagamento.militante_id }}"
data-tipo-pagamento="{{ pagamento.tipo_pagamento }}"
data-valor="{{ pagamento.valor }}"
data-data-pagamento="{{ pagamento.data_pagamento.strftime('%Y-%m-%d') }}"
title="Editar">
<i class="fas fa-edit"></i>
</button>
<button type="button"
class="btn btn-sm btn-outline-danger"
data-bs-toggle="modal"
data-bs-target="#modalExcluirPagamento"
data-pagamento-id="{{ pagamento.id }}"
data-pagamento-info="Pagamento de {{ pagamento.militante.nome if pagamento.militante else 'N/A' }} - R$ {{ "%.2f"|format(pagamento.valor) }}"
title="Excluir">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</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 %}

View File

@@ -1,32 +1,515 @@
{% extends 'base.html' %} {% extends "base.html" %}
{% block title %}Listar Militantes{% endblock %} {% block title %}Vendas de Jornais{% endblock %}
{% block content %} {% 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">
<thead> <h1 class="mb-0">
<tr> <i class="fas fa-newspaper me-2"></i>Vendas de Jornais
<th>ID</th> </h1>
<th>Militante ID</th> <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalNovaVenda">
<th>Quantidade</th> <i class="fas fa-plus me-2"></i>Nova Venda
<th>Valor Total</th> </button>
<th>Data da Venda</th> </div>
</tr> </div>
</thead> </div>
<tbody>
{% for venda in vendas %}
<tr>
<td>{{ venda.id }}</td>
<td>{{ venda.militante_id }}</td>
<td>{{ venda.quantidade }}</td>
<td>R$ {{ venda.valor_total }}</td>
<td>{{ venda.data_venda }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<a href="{{ url_for('home') }}">Home</a>
<div class="card shadow-sm">
<div class="card-body">
<div class="row mb-4">
<div class="col-md-6">
<div class="input-group">
<span class="input-group-text">
<i class="fas fa-search"></i>
</span>
<input type="text" class="form-control" id="searchInput" placeholder="Pesquisar vendas...">
</div>
</div>
<div class="col-md-6 text-end">
<button id="btnExportar" class="btn btn-outline-primary">
<i class="fas fa-download me-2"></i>Exportar
</button>
</div>
</div>
<div class="table-responsive">
<table class="table table-hover" id="vendasTable">
<thead>
<tr>
<th data-sort="militante">Militante <i class="fas fa-sort"></i></th>
<th data-sort="quantidade">Quantidade <i class="fas fa-sort"></i></th>
<th data-sort="valor_total">Valor Total <i class="fas fa-sort"></i></th>
<th data-sort="data">Data <i class="fas fa-sort"></i></th>
<th class="text-end">Ações</th>
</tr>
</thead>
<tbody>
{% for venda in vendas %}
<tr>
<td data-militante="{{ venda.militante.nome }}">{{ venda.militante.nome }}</td>
<td data-quantidade="{{ venda.quantidade }}">{{ venda.quantidade }}</td>
<td data-valor_total="{{ venda.valor_total }}">R$ {{ "%.2f"|format(venda.valor_total) }}</td>
<td data-data="{{ venda.data_venda }}">{{ venda.data_venda.strftime('%d/%m/%Y') }}</td>
<td class="text-end">
<div class="btn-group">
<button type="button"
class="btn btn-sm btn-outline-primary"
data-bs-toggle="modal"
data-bs-target="#modalEditarVenda"
data-venda-id="{{ venda.id }}"
data-venda-militante="{{ venda.militante_id }}"
data-venda-quantidade="{{ venda.quantidade }}"
data-venda-valor-total="{{ venda.valor_total }}"
data-venda-data="{{ venda.data_venda.strftime('%Y-%m-%d') }}"
title="Editar">
<i class="fas fa-edit"></i>
</button>
<button type="button"
class="btn btn-sm btn-outline-danger"
data-bs-toggle="modal"
data-bs-target="#deleteModal"
data-venda-id="{{ venda.id }}"
data-venda-info="{{ venda.militante.nome }} - {{ venda.quantidade }} jornais"
title="Excluir">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</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 %}

View File

@@ -1,48 +1,211 @@
{% extends 'base.html' %} {% extends "base.html" %}
{% block title %}Login{% endblock %} {% block title %}Login{% endblock %}
{% block navbar %}{% endblock %}
{% block content %} {% block content %}
<div class="container"> <div class="login-container">
<div class="row justify-content-center"> <div class="login-content">
<div class="col-md-6"> <div class="login-header">
<div class="card"> <img src="{{ url_for('static', filename='img/logo001-alpha.png') }}" alt="Logo OCI" class="login-logo">
<div class="card-header"> <h4 class="login-title">Controles OCI</h4>
<h3 class="card-title">Login</h3> </div>
</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('login') }}"> {% with messages = get_flashed_messages(with_categories=true) %}
<div class="mb-3"> {% if messages %}
<label for="username" class="form-label">Usuário</label> {% for category, message in messages %}
<input type="text" class="form-control" id="username" name="username" required> <div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
</div> {{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="mb-3"> <form method="POST" action="{{ url_for('login') }}" class="needs-validation" novalidate>
<label for="password" class="form-label">Senha</label> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="password" class="form-control" id="password" name="password" required>
</div>
<div class="mb-3"> <div class="form-floating mb-3">
<label for="otp_code" class="form-label">Código OTP</label> <input type="text" class="form-control" id="email" name="email" placeholder="Email ou Usuário" required>
<input type="text" class="form-control" id="otp_code" name="otp_code" required> <label for="email">Email ou Usuário</label>
<small class="text-muted">Digite o código gerado pelo seu aplicativo autenticador</small> <div class="invalid-feedback">
</div> Por favor, informe seu email ou nome de usuário.
<div class="d-grid">
<button type="submit" class="btn btn-primary">Entrar</button>
</div>
</form>
</div> </div>
</div> </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>
</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 %} {% endblock %}

23
templates/militantes.html Normal file
View File

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

View File

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

View File

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

View File

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

View File

@@ -6,80 +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">Novo Pagamento</h1> <div class="card shadow-sm">
<div class="card-header bg-light">
{% with messages = get_flashed_messages(with_categories=true) %} <h4 class="card-title mb-0">
{% if messages %} <i class="fas fa-money-bill-wave me-2"></i>Registrar Novo Pagamento
{% for category, message in messages %} </h4>
<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>
<div class="card-body">
<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 um militante.
</div>
</div>
<div class="mb-3"> <div class="mb-3">
<label for="tipo_pagamento" class="form-label">Tipo de Pagamento:</label> <label for="tipo_pagamento_id" class="form-label">Tipo de Pagamento:</label>
<select class="form-select" id="tipo_pagamento" name="tipo_pagamento" required> <select class="form-select" id="tipo_pagamento_id" name="tipo_pagamento_id" required>
<option value="">Selecione o tipo</option> <option value="">Selecione o tipo de pagamento</option>
<option value="cota">Cota</option> {% for tipo in tipos_pagamento %}
<option value="jornal">Jornal</option> <option value="{{ tipo.id }}">{{ tipo.descricao }}</option>
<option value="assinatura">Assinatura</option> {% endfor %}
<option value="campanha">Campanha Financeira</option> </select>
</select> <div class="invalid-feedback">
</div> Por favor, selecione o tipo de pagamento.
</div>
</div>
<div class="mb-3"> <div class="mb-3">
<label for="mes_referencia" class="form-label">Mês de Referência:</label> <label for="valor" class="form-label">Valor:</label>
<input type="month" class="form-control" id="mes_referencia" name="mes_referencia" required> <div class="input-group">
</div> <span class="input-group-text">R$</span>
<input type="text" class="form-control money" id="valor" name="valor" required>
</div>
<div class="invalid-feedback">
Por favor, informe um valor válido.
</div>
</div>
<div class="mb-3"> <div class="mb-3">
<label for="numero_jornal" class="form-label">Número do Jornal:</label> <label for="data_pagamento" class="form-label">Data do Pagamento:</label>
<input type="number" class="form-control" id="numero_jornal" name="numero_jornal"> <input type="date" class="form-control" id="data_pagamento" name="data_pagamento"
</div> required max="{{ hoje }}">
<div class="invalid-feedback">
Por favor, informe uma data válida.
</div>
</div>
<div class="mb-3"> <div class="d-flex gap-2">
<label for="numero_inicial_assinatura" class="form-label">Número Inicial da Assinatura:</label> <button type="submit" class="btn btn-primary">
<input type="number" class="form-control" id="numero_inicial_assinatura" name="numero_inicial_assinatura"> <i class="fas fa-save me-1"></i>Registrar
</button>
<a href="{{ url_for('listar_pagamentos') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left me-1"></i>Voltar
</a>
</div>
</form>
</div> </div>
</div>
<div class="mb-3">
<label for="numero_final_assinatura" class="form-label">Número Final da Assinatura:</label>
<input type="number" class="form-control" id="numero_final_assinatura" name="numero_final_assinatura">
</div>
<div class="mb-3">
<label for="campanha_financeira" class="form-label">Campanha Financeira:</label>
<input type="text" class="form-control" id="campanha_financeira" name="campanha_financeira">
</div>
<div class="mb-3">
<label for="valor" class="form-label">Valor:</label>
<input type="number" class="form-control" id="valor" name="valor" step="0.01" required>
</div>
<div class="mb-3">
<label for="data_pagamento" class="form-label">Data do Pagamento:</label>
<input type="date" class="form-control" id="data_pagamento" name="data_pagamento" required>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">Registrar</button>
<a href="{{ url_for('listar_pagamentos') }}" class="btn btn-secondary">Voltar</a>
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
</div>
</form>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% 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 %}