30 Commits

Author SHA1 Message Date
andersonid
2da8dec63f Primeira parte da tela de administração de usuários do sistema. 2025-04-25 18:47:57 -03:00
andersonid
e6057cd566 fix(#11): Corrige inicialização do sistema para não recriar usuários a cada execução 2025-04-15 15:10:21 -03:00
andersonid
8255f1d933 feat(#11): Adiciona módulo create_test_users 2025-04-15 15:09:03 -03:00
andersonid
0f32eae5cf refactor(#11): Integra listagem de usuários no dashboard 2025-04-15 14:26:02 -03:00
andersonid
47f13e7c18 feat(#11): Implementa estrutura inicial da área administrativa - Cria blueprint administrativo com rotas básicas - Implementa templates base para área administrativa - Adiciona dashboard administrativo - Implementa gerenciamento de usuários - Organiza rotas em pacote separado 2025-04-15 10:49:15 -03:00
andersonid
53769cf080 Adicionado parâmetro de versão aleatório no CSS para evitar cache 2025-04-15 10:21:41 -03:00
andersonid
92bc21dbd8 Tela inicial de administração desenvolvida. 2025-04-15 10:19:59 -03:00
andersonid
5057802220 tela de administração em ajustes 2025-04-13 22:48:27 -03:00
andersonid
e43b089155 fix: Correções na página de administração e suas dependências 2025-04-13 22:30:05 -03:00
LS
295a433d59 fix: remove debug duplicado na chamada de app.run() 2025-04-09 11:23:48 -03:00
LS
203751deeb feat: adiciona classes Controle, Notificacao e Relatorio para gerenciamento do sistema 2025-04-09 11:21:45 -03:00
LS
71f926e6be fix: corrige erro de sintaxe na chamada de app.run() 2025-04-09 10:12:05 -03:00
LS
8cef19576e fix: adiciona use_alter=True e nomes específicos para chaves estrangeiras circulares 2025-04-09 10:09:31 -03:00
andersonid
abc46704c3 Corrigir atualização de dados na tabela de militantes 2025-04-09 09:59:41 -03:00
andersonid
c640a756df chore: remove arquivos não utilizados do projeto - Remoção de scripts obsoletos (seed.py, create_test_users.py) e arquivos de configuração não utilizados (setup.py, models.py) após análise completa de dependências 2025-04-09 09:59:41 -03:00
andersonid
3f2e6e3022 fix: corrige validação e salvamento do formulário de edição de militante - Corrige validação do email, ajusta conversão de datas, corrige CSRF token e melhora feedback visual 2025-04-09 09:59:41 -03:00
LS
179ea3cad0 resolvido merge com nova ui 2025-04-09 09:59:12 -03:00
andersonid
b47c9efc21 Melhorias na lógica de ativação de badges e atualização de responsabilidades 2025-04-09 09:54:59 -03:00
andersonid
97711d30c7 fix: corrige campos de data no modal de novo militante 2025-04-09 09:54:59 -03:00
andersonid
50ef370c2b fix: corrige comportamento dos campos de data para manter calendário e formato brasileiro 2025-04-09 09:54:59 -03:00
andersonid
53594517c0 fix: ajusta formato de data para padrão brasileiro (DD/MM/AAAA) 2025-04-09 09:54:59 -03:00
andersonid
874df1d340 feat: melhora visualização do formato de data nos formulários 2025-04-09 09:54:59 -03:00
LS
b170f94058 fix: adiciona Faker como dependência para geração de dados de teste 2025-04-04 18:11:24 -03:00
LS
786040162b fix: configura Flask para produção com gunicorn e ajusta Dockerfile para Coolify 2025-04-04 18:07:04 -03:00
LS
daaa7fd462 feat: atualiza Dockerfile para incluir dependências necessárias 2025-04-04 18:04:23 -03:00
LS
ad0ea2f259 refactor: atualiza Dockerfile para usar Alpine Linux e corrige instalação do Python 2025-04-04 17:55:58 -03:00
Levy Sant'Anna
74e5a1f7e3 Update Dockerfile
changed fedora version to latest
2025-04-04 17:50:17 -03:00
LS
d07a227e80 docker compose 2025-04-04 17:43:34 -03:00
Levy Sant'Anna
0635003485 Update Dockerfile 2025-04-04 17:30:43 -03:00
LS
d931fb4b5e Dockerfile 2025-04-04 15:21:39 -03:00
50 changed files with 5981 additions and 2775 deletions

50
.dockerignore Normal file
View File

@@ -0,0 +1,50 @@
# Arquivos e diretórios do Git
.git
.gitignore
# Arquivos do Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# Arquivos de ambiente
.env
.venv
venv/
ENV/
# Arquivos de IDE
.idea/
.vscode/
*.swp
*.swo
# Arquivos de log
*.log
# Arquivos de banco de dados
*.db
*.sqlite3
# Arquivos temporários
*.tmp
*.bak
*.swp
*~

39
Dockerfile Normal file
View File

@@ -0,0 +1,39 @@
FROM alpine:latest
# Instalar dependências do sistema
RUN apk update && \
apk add --no-cache \
python3 \
py3-pip \
make \
git \
gcc \
python3-dev \
musl-dev \
linux-headers
# Criar link simbólico para python3
RUN ln -sf python3 /usr/bin/python
# Definir diretório de trabalho
WORKDIR /app
# Copiar arquivos do projeto
COPY . .
# Criar e ativar ambiente virtual
RUN python -m venv /venv && \
. /venv/bin/activate && \
pip install --upgrade pip && \
pip install -r requirements.txt
# Expor a porta que o Flask usa
EXPOSE 5000
# Definir o ambiente virtual como padrão
ENV PATH="/venv/bin:$PATH"
ENV FLASK_APP=app.py
ENV FLASK_ENV=production
# Comando para rodar a aplicação
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]

View File

@@ -2,17 +2,22 @@ install:
pip install -r requirements.txt pip install -r requirements.txt
clean: 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: init-db: clean
python app.py python init_db.py
seed: seed: init-db
python seed.py python seed.py
run-with-seed: clean init:
python app.py & sleep 5 && python seed.py python app.py --init
run:
python app.py
run-with-seed: seed init run
reset-admin: clean reset-admin: clean
python create_admin.py python create_admin.py

3030
app.py

File diff suppressed because it is too large Load Diff

View File

@@ -70,6 +70,15 @@ def create_admin_user():
admin.set_password("admin123") admin.set_password("admin123")
admin.generate_otp_secret() admin.generate_otp_secret()
# Buscar ou criar role de admin
admin_role = db.query(Role).filter_by(nome="admin").first()
if not admin_role:
admin_role = Role(nome="admin", nivel=0) # Nível 0 é o mais alto
db.add(admin_role)
# Adicionar role ao usuário
admin.roles.append(admin_role)
# Adicionar e fazer commit # Adicionar e fazer commit
db.add(admin) db.add(admin)
db.commit() db.commit()

View File

@@ -1,130 +1,56 @@
from functions.database import get_db_connection, Usuario from functions.database import get_db_connection, Usuario, Role
from functions.rbac import Role from werkzeug.security import generate_password_hash
import pyotp
import qrcode
import os
import base64
from io import BytesIO
def create_test_users(): def create_test_users():
"""Cria usuários de teste se não existirem""" """Cria usuários de teste"""
db = get_db_connection() db = get_db_connection()
try: try:
# Usuários de teste # Lista de usuários de teste
test_users = [ test_users = [
{
'username': 'teste',
'password': 'admin123', # Mesma senha do admin
'email': 'teste@controles.com',
'is_admin': True
},
{ {
'username': 'aligner', 'username': 'aligner',
'email': 'aligner@test.com',
'password': 'Test123!@#', 'password': 'Test123!@#',
'email': 'aligner@controles.com',
'is_admin': False 'is_admin': False
}, },
{ {
'username': 'tester', 'username': 'tester',
'email': 'tester@test.com',
'password': 'Test123!@#', 'password': 'Test123!@#',
'email': 'tester@controles.com',
'is_admin': False 'is_admin': False
}, },
{ {
'username': 'deployer', 'username': 'deployer',
'email': 'deployer@test.com',
'password': 'Test123!@#', 'password': 'Test123!@#',
'email': 'deployer@controles.com',
'is_admin': False 'is_admin': False
} }
] ]
# Obter o OTP secret do admin se existir # Criar cada usuário
admin = db.query(Usuario).filter_by(username='admin').first()
admin_otp_secret = admin.otp_secret if admin else None
for user_data in test_users: for user_data in test_users:
# Verificar se o usuário já existe
user = db.query(Usuario).filter_by(username=user_data['username']).first() user = db.query(Usuario).filter_by(username=user_data['username']).first()
if not user: if not user:
print(f"Criando usuário {user_data['username']}...")
# Criar usuário
user = Usuario( user = Usuario(
username=user_data['username'], username=user_data['username'],
email=user_data['email'], email=user_data['email'],
is_admin=user_data['is_admin'] is_admin=user_data['is_admin']
) )
user.set_password(user_data['password']) user.set_password(user_data['password'])
user.tipo = "ADMIN" if user_data['is_admin'] else "USUARIO"
# Se for o usuário teste, usar o mesmo OTP do admin
if user_data['username'] == 'teste' and admin_otp_secret:
user.otp_secret = admin_otp_secret
else:
# Gerar novo OTP para outros usuários
user.otp_secret = pyotp.random_base32()
db.add(user) db.add(user)
db.commit() print(f"Usuário {user_data['username']} criado")
# Atribuir role de Secretário Geral para o usuário teste
if user_data['username'] == 'teste':
admin_role = db.query(Role).filter_by(nivel=Role.SECRETARIO_GERAL).first()
if admin_role:
user.roles.append(admin_role)
db.commit()
print(f"Usuário {user_data['username']} criado com sucesso!")
# Gerar QR code para o novo usuário
qr_path = f"{user_data['username']}_qr.png"
if not os.path.exists(qr_path):
totp = pyotp.TOTP(user.otp_secret)
qr = qrcode.QRCode(version=1, box_size=10, border=5)
qr.add_data(totp.provisioning_uri(user.email, issuer_name="Sistema de Controles"))
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
img.save(qr_path)
print(f"QR Code gerado para {user_data['username']} em: {qr_path}")
else: else:
print(f"Usuário {user_data['username']} já existe") print(f"Usuário {user_data['username']} já existe")
# Se for o usuário teste e não tiver o OTP do admin, atualizar db.commit()
if user_data['username'] == 'teste' and admin_otp_secret and user.otp_secret != admin_otp_secret: print("Usuários de teste criados com sucesso")
user.otp_secret = admin_otp_secret
db.commit()
print(f"OTP do usuário teste atualizado para o mesmo do admin")
elif not user.otp_secret:
# Se não tiver OTP, gerar um novo
user.otp_secret = pyotp.random_base32()
db.commit()
print(f"Novo OTP gerado para {user_data['username']}")
# Gerar QR code
qr_path = f"{user_data['username']}_qr.png"
if not os.path.exists(qr_path):
totp = pyotp.TOTP(user.otp_secret)
qr = qrcode.QRCode(version=1, box_size=10, border=5)
qr.add_data(totp.provisioning_uri(user.email, issuer_name="Sistema de Controles"))
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
img.save(qr_path)
print(f"QR Code gerado para {user_data['username']} em: {qr_path}")
# Verificar se o usuário teste tem a role de Secretário Geral
if user_data['username'] == 'teste':
admin_role = db.query(Role).filter_by(nivel=Role.SECRETARIO_GERAL).first()
if admin_role and admin_role not in user.roles:
user.roles.append(admin_role)
db.commit()
print(f"Role de Secretário Geral atribuída ao usuário teste")
except Exception as e: except Exception as e:
print(f"Erro ao criar usuários de teste: {str(e)}") print(f"Erro ao criar usuários de teste: {str(e)}")
db.rollback() db.rollback()
raise
finally: finally:
db.close() db.close()
if __name__ == '__main__': if __name__ == "__main__":
create_test_users() create_test_users()

14
docker-compose.yml Normal file
View File

@@ -0,0 +1,14 @@
version: '3.8'
services:
app:
build: .
ports:
- "5000:5000"
volumes:
- .:/app
- ~/.local/share/controles:/root/.local/share/controles
environment:
- FLASK_ENV=development
- FLASK_APP=app.py
restart: unless-stopped

84
functions/controle.py Normal file
View File

@@ -0,0 +1,84 @@
from datetime import datetime, UTC
from sqlalchemy.exc import SQLAlchemyError
from functions.database import get_db_connection, Controle as ControleModel
class Controle:
def __init__(self):
self.db = get_db_connection()
def registrar_controle(self, militante_id: int, tipo: str, valor: float, observacao: str = None) -> bool:
"""
Registra um novo controle no sistema
Args:
militante_id: ID do militante
tipo: Tipo do controle (ex: 'pagamento', 'cota')
valor: Valor do controle
observacao: Observação opcional sobre o controle
Returns:
bool: True se o controle foi registrado com sucesso, False caso contrário
"""
try:
data_registro = datetime.now(UTC)
novo_controle = ControleModel(
militante_id=militante_id,
tipo=tipo,
valor=valor,
data_registro=data_registro,
observacao=observacao
)
self.db.add(novo_controle)
self.db.commit()
return True
except SQLAlchemyError as e:
self.db.rollback()
print(f"Erro ao registrar controle: {str(e)}")
return False
finally:
self.db.close()
def listar_controles(self, militante_id: int = None) -> list:
"""
Lista os controles registrados no sistema
Args:
militante_id: ID do militante para filtrar (opcional)
Returns:
list: Lista de controles encontrados
"""
try:
query = self.db.query(ControleModel)
if militante_id:
query = query.filter(ControleModel.militante_id == militante_id)
return query.all()
except SQLAlchemyError as e:
print(f"Erro ao listar controles: {str(e)}")
return []
finally:
self.db.close()
def buscar_controle(self, controle_id: int) -> ControleModel:
"""
Busca um controle específico pelo ID
Args:
controle_id: ID do controle
Returns:
ControleModel: Objeto do controle encontrado ou None
"""
try:
return self.db.query(ControleModel).filter(ControleModel.id == controle_id).first()
except SQLAlchemyError as e:
print(f"Erro ao buscar controle: {str(e)}")
return None
finally:
self.db.close()

View File

@@ -20,11 +20,15 @@ db_dir = Path.home() / '.local' / 'share' / 'controles'
db_dir.mkdir(parents=True, exist_ok=True) db_dir.mkdir(parents=True, exist_ok=True)
db_path = db_dir / 'database.db' db_path = db_dir / 'database.db'
SessionLocal = sessionmaker(bind=engine) DATABASE_URL = f"sqlite:///{db_path}"
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def get_db_connection(): def get_db_connection():
"""Retorna uma nova conexão com o banco de dados""" """Retorna uma nova sessão do banco de dados"""
db = SessionLocal() Session = sessionmaker(bind=engine)
db = Session()
try: try:
# Configurar SQLite para melhor tratamento de concorrência # Configurar SQLite para melhor tratamento de concorrência
db.execute(text("PRAGMA journal_mode=WAL")) db.execute(text("PRAGMA journal_mode=WAL"))
@@ -60,10 +64,10 @@ class Celula(Base):
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
nome = Column(String(100), nullable=False) nome = Column(String(100), nullable=False)
setor_id = Column(Integer, ForeignKey('setores.id')) setor_id = Column(Integer, ForeignKey('setores.id', use_alter=True, name='fk_celula_setor'))
cr_id = Column(Integer, ForeignKey('comites_regionais.id')) cr_id = Column(Integer, ForeignKey('comites_regionais.id', use_alter=True, name='fk_celula_cr'))
secretario = Column(Integer, ForeignKey('militantes.id')) secretario = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_celula_secretario'))
responsavel_financas = Column(Integer, ForeignKey('militantes.id')) responsavel_financas = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_celula_responsavel_financas'))
quadro_orientador = Column(String(255)) quadro_orientador = Column(String(255))
# Relacionamentos # Relacionamentos
@@ -80,10 +84,10 @@ class ComiteRegional(Base):
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
nome = Column(String(100), nullable=False) nome = Column(String(100), nullable=False)
responsavel_financas = Column(Integer, ForeignKey('militantes.id')) responsavel_financas = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_cr_responsavel_financas'))
responsavel_formacao = Column(Integer, ForeignKey('militantes.id')) responsavel_formacao = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_cr_responsavel_formacao'))
secretario_organizacao = Column(Integer, ForeignKey('militantes.id')) secretario_organizacao = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_cr_secretario_organizacao'))
correspondente_jornal = Column(Integer, ForeignKey('militantes.id')) correspondente_jornal = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_cr_correspondente_jornal'))
# Relacionamentos # Relacionamentos
responsavel_financas_rel = relationship("Militante", foreign_keys=[responsavel_financas]) responsavel_financas_rel = relationship("Militante", foreign_keys=[responsavel_financas])
@@ -141,7 +145,7 @@ class Militante(Base):
# Relacionamento para múltiplos emails # Relacionamento para múltiplos emails
emails = relationship("EmailMilitante", back_populates="militante") emails = relationship("EmailMilitante", back_populates="militante")
# Endereço # Endereço
endereco_id = Column(Integer, ForeignKey('enderecos.id')) endereco_id = Column(Integer, ForeignKey('enderecos.id', use_alter=True, name='fk_militante_endereco'))
endereco = relationship("Endereco", back_populates="militantes") endereco = relationship("Endereco", back_populates="militantes")
# Redes sociais # Redes sociais
redes_sociais = relationship("RedeSocial", back_populates="militante") redes_sociais = relationship("RedeSocial", back_populates="militante")
@@ -159,9 +163,9 @@ class Militante(Base):
dirigente_sindical = Column(Boolean) dirigente_sindical = Column(Boolean)
central_sindical = Column(String(100)) central_sindical = Column(String(100))
# Responsável pelo cadastro # Responsável pelo cadastro
registrado_por = Column(Integer, ForeignKey('militantes.id')) registrado_por = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_militante_registrado_por'))
# Campos existentes # Campos existentes
celula_id = Column(Integer, ForeignKey('celulas.id')) celula_id = Column(Integer, ForeignKey('celulas.id', use_alter=True, name='fk_militante_celula'))
responsabilidades = Column(Integer, default=0) responsabilidades = Column(Integer, default=0)
otp_secret = Column(String(32)) otp_secret = Column(String(32))
temp_token = Column(String(64)) temp_token = Column(String(64))
@@ -378,9 +382,9 @@ class Setor(Base):
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
nome = Column(String(100), nullable=False) nome = Column(String(100), nullable=False)
cr_id = Column(Integer, ForeignKey('comites_regionais.id')) cr_id = Column(Integer, ForeignKey('comites_regionais.id', use_alter=True, name='fk_setor_cr'))
responsavel = Column(Integer, ForeignKey('militantes.id')) responsavel = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_setor_responsavel'))
responsavel_financas = Column(Integer, ForeignKey('militantes.id')) responsavel_financas = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_setor_responsavel_financas'))
# Relacionamentos # Relacionamentos
cr = relationship("ComiteRegional", back_populates="setores") cr = relationship("ComiteRegional", back_populates="setores")
@@ -437,6 +441,7 @@ class Usuario(Base, UserMixin):
username = Column(String(50), unique=True, nullable=False) username = Column(String(50), unique=True, nullable=False)
password_hash = Column(String(255), nullable=False) password_hash = Column(String(255), nullable=False)
email = Column(String(100), unique=True, nullable=False) email = Column(String(100), unique=True, nullable=False)
nome = Column(String(100)) # Nome completo do usuário
otp_secret = Column(String(32)) otp_secret = Column(String(32))
role_id = Column(Integer, ForeignKey('roles.id')) role_id = Column(Integer, ForeignKey('roles.id'))
setor_id = Column(Integer, ForeignKey('setores.id')) setor_id = Column(Integer, ForeignKey('setores.id'))
@@ -460,11 +465,11 @@ 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 __init__(self, username, email=None, is_admin=False): def __init__(self, username, email=None, is_admin=False, nome=None):
self.username = username self.username = username
self.email = email self.email = email
self.is_admin = is_admin self.is_admin = is_admin
self.email = email self.nome = nome
self.ativo = True self.ativo = True
self.session_timeout = 30 self.session_timeout = 30
self.tipo = "USUARIO" self.tipo = "USUARIO"
@@ -545,6 +550,10 @@ class Usuario(Base, UserMixin):
self.motivo_logout = "Logout manual" self.motivo_logout = "Logout manual"
self.ultima_atividade = None self.ultima_atividade = None
def is_admin_user(self):
"""Verifica se o usuário é admin"""
return self.is_admin or any(role.nome == "admin" for role in self.roles)
class PagamentoCelula(Base): class PagamentoCelula(Base):
__tablename__ = 'pagamentos_celula' __tablename__ = 'pagamentos_celula'
@@ -623,10 +632,6 @@ def init_database():
session = get_db_connection() session = get_db_connection()
try: try:
# Configurar SQLite para melhor tratamento de concorrência
session.execute(text("PRAGMA journal_mode=WAL"))
session.execute(text("PRAGMA busy_timeout=5000"))
# Criar todas as tabelas # Criar todas as tabelas
Base.metadata.drop_all(engine) # Remover todas as tabelas existentes Base.metadata.drop_all(engine) # Remover todas as tabelas existentes
Base.metadata.create_all(engine) Base.metadata.create_all(engine)
@@ -660,25 +665,9 @@ def init_database():
session.add(comite) session.add(comite)
session.commit() session.commit()
# Verificar se existe um QR code salvo # Gerar OTP para admin
qr_path = Path('admin_qr.png') admin_otp_secret = pyotp.random_base32()
admin_otp_secret = None print(f"Novo OTP gerado: {admin_otp_secret}")
if qr_path.exists():
try:
import re
with open('admin_qr.txt', 'r') as f:
qr_content = f.read()
match = re.search(r'secret=([A-Z0-9]+)&', qr_content)
if match:
admin_otp_secret = match.group(1)
print(f"Usando OTP existente: {admin_otp_secret}")
except Exception as e:
print(f"Erro ao ler OTP existente: {e}")
if not admin_otp_secret:
admin_otp_secret = pyotp.random_base32()
print(f"Novo OTP gerado: {admin_otp_secret}")
# Criar usuário admin # Criar usuário admin
admin_role = session.query(Role).filter_by(nome="Administrador").first() admin_role = session.query(Role).filter_by(nome="Administrador").first()
@@ -697,27 +686,23 @@ def init_database():
session.add(admin) session.add(admin)
session.commit() session.commit()
# Gerar novo QR code se não existir # Gerar QR code
if not qr_path.exists(): 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")
import qrcode
with open('admin_qr.txt', 'w') as f: qr = qrcode.QRCode(version=1, box_size=10, border=5)
f.write(provisioning_uri) qr.add_data(provisioning_uri)
qr.make(fit=True)
import qrcode img = qr.make_image(fill_color="black", back_color="white")
qr = qrcode.QRCode(version=1, box_size=10, border=5) img.save('admin_qr.png')
qr.add_data(provisioning_uri)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
img.save('admin_qr.png')
print("=== Usuário Admin Criado ===") print("=== Usuário Admin Criado ===")
print(f"Username: admin") print(f"Username: admin")
print(f"Senha: admin123") print(f"Senha: admin123")
print(f"Email: {admin.email}") print(f"Email: {admin.email}")
print(f"OTP Secret: {admin.otp_secret}") print(f"OTP Secret: {admin_otp_secret}")
print(f"QR Code: {qr_path}") print(f"QR Code: admin_qr.png")
# Importar e executar o seed após criar todas as dependências # Importar e executar o seed após criar todas as dependências
from seed_data import seed_database from seed_data import seed_database

View File

@@ -2,7 +2,7 @@ from functools import wraps
from flask import session, redirect, url_for, flash from flask import session, redirect, url_for, flash
from flask_login import current_user, login_required from flask_login import current_user, login_required
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
from .database import get_db_connection, Usuario from .database import get_db_connection, Usuario, Role
from .rbac import Permission from .rbac import Permission
def require_login(f): def require_login(f):
@@ -15,9 +15,13 @@ def require_login(f):
db = get_db_connection() db = get_db_connection()
try: try:
# Carregar o usuário com suas roles # Carregar o usuário com suas roles e permissões
user = db.query(Usuario).options( user = db.query(Usuario).options(
joinedload(Usuario.roles) joinedload(Usuario.roles).joinedload(Role.permissions),
joinedload(Usuario.militante),
joinedload(Usuario.cr),
joinedload(Usuario.setor),
joinedload(Usuario.celula)
).get(current_user.id) ).get(current_user.id)
if not user: if not user:
@@ -28,7 +32,15 @@ def require_login(f):
user.update_last_activity() user.update_last_activity()
db.commit() db.commit()
# Substituir o current_user pelo usuário carregado
setattr(current_user, '_get_current_object', lambda: user)
# Executar a função com o usuário carregado
return f(*args, **kwargs) return f(*args, **kwargs)
except Exception as e:
db.rollback()
flash('Erro ao carregar dados do usuário.', 'danger')
return redirect(url_for('login'))
finally: finally:
db.close() db.close()
return decorated_function return decorated_function
@@ -39,14 +51,38 @@ 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.', 'danger') flash('Você precisa estar logado para acessar esta página.', 'error')
return redirect(url_for('login')) return redirect(url_for('login'))
if not current_user.has_permission(permission_name): db = get_db_connection()
flash('Você não tem permissão para acessar esta página.', 'danger') try:
return redirect(url_for('home')) # Carregar o usuário com suas roles e permissões
user = db.query(Usuario).options(
return f(*args, **kwargs) joinedload(Usuario.roles).joinedload(Role.permissions),
joinedload(Usuario.militante),
joinedload(Usuario.cr),
joinedload(Usuario.setor),
joinedload(Usuario.celula)
).get(current_user.id)
if not user:
flash('Usuário não encontrado.', 'error')
return redirect(url_for('login'))
if not user.has_permission(permission_name):
flash('Você não tem permissão para acessar esta página.', 'error')
return redirect(url_for('index'))
# Atualiza timestamp da última atividade
user.update_last_activity()
db.commit()
# Substituir o current_user pelo usuário carregado
setattr(current_user, '_get_current_object', lambda: user)
return f(*args, **kwargs)
finally:
db.close()
return decorated_function return decorated_function
return decorator return decorator

1
functions/notificacao.py Normal file
View File

@@ -0,0 +1 @@

1
functions/relatorio.py Normal file
View File

@@ -0,0 +1 @@

19
init_db.py Normal file
View File

@@ -0,0 +1,19 @@
from functions.database import init_database
from functions.rbac import init_rbac
from create_admin import create_admin_user
from create_test_users import create_test_users
def init_system():
print("Inicializando banco de dados...")
init_database()
print("Inicializando sistema RBAC...")
init_rbac()
print("Criando usuários iniciais...")
create_admin_user()
create_test_users()
if __name__ == "__main__":
init_system()
print("Sistema inicializado com sucesso!")

View File

@@ -1,23 +0,0 @@
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

5
pytest.ini Normal file
View File

@@ -0,0 +1,5 @@
[pytest]
pythonpath = .
testpaths = tests
python_files = test_*.py
addopts = -v --cov=. --cov-report=term-missing

View File

@@ -15,3 +15,5 @@ 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 PyJWT==2.8.0
gunicorn==21.2.0
Faker==19.13.0

2
routes/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
# Este arquivo está intencionalmente vazio
# Ele é usado para marcar o diretório como um pacote Python

128
routes/admin.py Normal file
View File

@@ -0,0 +1,128 @@
from flask import Blueprint, render_template, flash, redirect, url_for, request, jsonify
from functions.database import Usuario, get_db_connection
from functions.decorators import require_permission, require_role, require_minimum_role
from flask_login import login_required, current_user
from sqlalchemy.orm import joinedload
import pyotp
from werkzeug.security import generate_password_hash
import secrets
from functools import wraps
from sqlalchemy.exc import SQLAlchemyError
import logging
from datetime import datetime
logger = logging.getLogger(__name__)
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
def admin_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not current_user.is_admin:
flash('Acesso não autorizado.', 'danger')
return redirect(url_for('main.index'))
return f(*args, **kwargs)
return decorated_function
@admin_bp.route('/')
@login_required
@admin_required
def dashboard():
"""Dashboard principal da área administrativa com lista de usuários"""
db = get_db_connection()
try:
now = datetime.now()
# Carregar estatísticas relevantes
total_users = db.query(Usuario).count()
active_users = db.query(Usuario).filter(Usuario.is_active == True).count()
inactive_users = total_users - active_users
# Carregar lista de usuários
users = db.query(Usuario).options(
joinedload(Usuario.roles),
joinedload(Usuario.militante)
).all()
return render_template(
'admin/dashboard.html',
total_users=total_users,
active_users=active_users,
inactive_users=inactive_users,
users=users,
now=now
)
except SQLAlchemyError as e:
logger.error(f"Erro ao buscar dados do dashboard: {str(e)}")
flash('Erro ao carregar dados. Por favor, tente novamente.', 'danger')
return render_template('admin/dashboard.html',
total_users=0,
active_users=0,
inactive_users=0,
users=[])
finally:
db.close()
@admin_bp.route('/users/<int:user_id>/reset-otp', methods=['POST'])
@login_required
@require_role('ADMIN')
def reset_user_otp(user_id):
"""Reseta o OTP de um usuário"""
db = get_db_connection()
try:
user = db.query(Usuario).get(user_id)
if not user:
flash('Usuário não encontrado.', 'danger')
return redirect(url_for('admin.dashboard'))
# Gerar novo segredo OTP
user.otp_secret = pyotp.random_base32()
db.commit()
flash(f'OTP resetado com sucesso para {user.email}.', 'success')
return redirect(url_for('admin.dashboard'))
finally:
db.close()
@admin_bp.route('/users/<int:user_id>/reset-password', methods=['POST'])
@login_required
@require_role('ADMIN')
def reset_user_password(user_id):
"""Reseta a senha de um usuário"""
db = get_db_connection()
try:
user = db.query(Usuario).get(user_id)
if not user:
flash('Usuário não encontrado.', 'danger')
return redirect(url_for('admin.dashboard'))
# Gerar nova senha aleatória
new_password = secrets.token_urlsafe(8)
user.password = generate_password_hash(new_password)
db.commit()
flash(f'Senha resetada com sucesso. Nova senha: {new_password}', 'success')
return redirect(url_for('admin.dashboard'))
finally:
db.close()
@admin_bp.route('/users/<int:user_id>/toggle-status', methods=['POST'])
@login_required
@require_role('ADMIN')
def toggle_user_status(user_id):
"""Ativa/desativa um usuário"""
db = get_db_connection()
try:
user = db.query(Usuario).get(user_id)
if not user:
flash('Usuário não encontrado.', 'danger')
return redirect(url_for('admin.dashboard'))
user.is_active = not user.is_active
db.commit()
status = 'ativado' if user.is_active else 'desativado'
flash(f'Usuário {status} com sucesso.', 'success')
return redirect(url_for('admin.dashboard'))
finally:
db.close()

17
run_tests.sh Executable file
View File

@@ -0,0 +1,17 @@
#!/bin/bash
# Criar e ativar ambiente virtual
python -m venv venv
source venv/bin/activate
# Instalar dependências de teste
pip install -r tests/requirements-test.txt
# Instalar o projeto em modo de desenvolvimento
pip install -e .
# Executar testes
python -m pytest
# Desativar ambiente virtual
deactivate

32
seed.py
View File

@@ -1,32 +0,0 @@
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.")

View File

@@ -3,16 +3,60 @@ from functions.database import (
Base, Militante, CotaMensal, TipoPagamento, Pagamento, Base, Militante, CotaMensal, TipoPagamento, Pagamento,
MaterialVendido, TipoMaterial, VendaJornalAvulso, AssinaturaAnual, MaterialVendido, TipoMaterial, VendaJornalAvulso, AssinaturaAnual,
RelatorioCotasMensais, RelatorioVendasMateriais, engine, SessionLocal, RelatorioCotasMensais, RelatorioVendasMateriais, engine, SessionLocal,
Setor, ComiteCentral, Usuario, Role, EmailMilitante, Endereco Setor, ComiteCentral, Usuario, Role, EmailMilitante, Endereco,
ComiteRegional, Celula, EstadoMilitante
) )
import random import random
from faker import Faker from faker import Faker
import time import time
from werkzeug.security import generate_password_hash
fake = Faker('pt_BR') fake = Faker('pt_BR')
def criar_estrutura_organizacional(session):
"""Cria a estrutura organizacional básica"""
print("\nCriando estrutura organizacional...")
# Criar Comitê Central
cc = ComiteCentral(nome="Comitê Central SP")
session.add(cc)
session.flush()
# Criar Comitês Regionais
crs = []
for nome in ["CR São Paulo", "CR ABC", "CR Campinas"]:
cr = ComiteRegional(nome=nome)
session.add(cr)
session.flush()
crs.append(cr)
# Criar Setores para cada CR
setores = []
for cr in crs:
for i in range(2): # 2 setores por CR
setor = Setor(
nome=f"Setor {i+1} - {cr.nome}",
cr_id=cr.id
)
session.add(setor)
session.flush()
setores.append(setor)
# Criar Células para cada Setor
for setor in setores:
for i in range(2): # 2 células por setor
celula = Celula(
nome=f"Célula {i+1} - {setor.nome}",
setor_id=setor.id
)
session.add(celula)
session.commit()
return crs, setores
def criar_tipos_pagamento(session): def criar_tipos_pagamento(session):
"""Cria tipos de pagamento padrão""" """Cria tipos de pagamento padrão"""
print("\nCriando tipos de pagamento...")
tipos = [ tipos = [
"Dinheiro", "Dinheiro",
"PIX", "PIX",
@@ -27,6 +71,7 @@ def criar_tipos_pagamento(session):
def criar_tipos_material(session): def criar_tipos_material(session):
"""Cria tipos de material padrão""" """Cria tipos de material padrão"""
print("\nCriando tipos de material...")
tipos = [ tipos = [
"Jornal", "Jornal",
"Revista", "Revista",
@@ -39,42 +84,66 @@ def criar_tipos_material(session):
session.add(TipoMaterial(descricao=tipo)) session.add(TipoMaterial(descricao=tipo))
session.commit() session.commit()
def criar_militantes(session, num_militantes): def criar_militantes(session, num_militantes, setores):
"""Cria militantes com todos os dados necessários"""
print(f"\nCriando {num_militantes} militantes...") print(f"\nCriando {num_militantes} militantes...")
militantes = [] militantes = []
emails_usados = set() 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): for i in range(num_militantes):
try: try:
# Dados básicos
nome = fake.name() nome = fake.name()
cpf = fake.cpf() cpf = fake.cpf()
# Email único
while True: while True:
email = fake.email() email = fake.email()
if email not in emails_usados: if email not in emails_usados:
emails_usados.add(email) emails_usados.add(email)
break break
# Criar endereço
endereco = Endereco( endereco = Endereco(
cep=fake.postcode(),
estado=fake.estado_sigla(), estado=fake.estado_sigla(),
cidade=fake.city(), cidade=fake.city(),
bairro=fake.bairro(), bairro=fake.bairro(),
rua=fake.street_name(), rua=fake.street_name(),
numero=str(random.randint(1, 999)), 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, 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.add(endereco)
session.flush() session.flush()
print(f"Criando militante {i+1}: {nome} (CPF: {cpf})") # Selecionar setor e célula aleatórios
setor = random.choice(setores)
celula = random.choice(session.query(Celula).filter_by(setor_id=setor.id).all())
# Definir responsabilidades
responsabilidades = 0
if random.random() < 0.2: # 20% chance de ser Responsável de Finanças
responsabilidades |= Militante.RESPONSAVEL_FINANCAS
if random.random() < 0.2: # 20% chance de ser Responsável de Imprensa
responsabilidades |= Militante.RESPONSAVEL_IMPRENSA
if random.random() < 0.2: # 20% chance de ser Quadro-Orientador
responsabilidades |= Militante.QUADRO_ORIENTADOR
if random.random() < 0.2: # 20% chance de ser Secretário
responsabilidades |= Militante.SECRETARIO
if random.random() < 0.2: # 20% chance de ser MPS
responsabilidades |= Militante.MPS
if random.random() < 0.2: # 20% chance de ser Tesoureiro
responsabilidades |= Militante.TESOUREIRO
if random.random() < 0.2: # 20% chance de ser MNS
responsabilidades |= Militante.MNS
if random.random() < 0.2: # 20% chance de ser da Juventude
responsabilidades |= Militante.JUVENTUDE
if random.random() < 0.3: # 30% chance de ser Aspirante
responsabilidades |= Militante.ASPIRANTE
print(f"Criando militante {i+1}: {nome}")
# Criar militante com todos os dados
militante = Militante( militante = Militante(
nome=nome, nome=nome,
cpf=cpf, cpf=cpf,
@@ -95,11 +164,14 @@ def criar_militantes(session, num_militantes):
dirigente_sindical=random.random() < 0.2, dirigente_sindical=random.random() < 0.2,
central_sindical=random.choice(['CUT', 'CSP-Conlutas', 'CTB', 'Força Sindical']) if random.random() < 0.4 else None, central_sindical=random.choice(['CUT', 'CSP-Conlutas', 'CTB', 'Força Sindical']) if random.random() < 0.4 else None,
endereco_id=endereco.id, endereco_id=endereco.id,
responsabilidades=random.randint(0, 1023) celula_id=celula.id,
responsabilidades=responsabilidades,
estado=random.choice(list(EstadoMilitante))
) )
session.add(militante) session.add(militante)
session.flush() session.flush()
# Criar email do militante
email_militante = EmailMilitante( email_militante = EmailMilitante(
militante_id=militante.id, militante_id=militante.id,
endereco_email=email endereco_email=email
@@ -107,8 +179,6 @@ def criar_militantes(session, num_militantes):
session.add(email_militante) session.add(email_militante)
militantes.append(militante) militantes.append(militante)
# Commit a cada militante para evitar transações muito longas
session.commit() session.commit()
except Exception as e: except Exception as e:
@@ -118,12 +188,13 @@ def criar_militantes(session, num_militantes):
return militantes return militantes
def criar_cotas(session, militantes, quantidade_por_militante=3): def criar_cotas(session, militantes):
print(f"Criando {quantidade_por_militante} cotas para cada um dos {len(militantes)} militantes...") """Cria cotas mensais para os militantes"""
print("\nCriando cotas mensais...")
for militante in militantes: for militante in militantes:
try: try:
print(f"Criando cotas para militante {militante.nome}") # Criar 12 cotas (1 ano) para cada militante
for i in range(quantidade_por_militante): for i in range(12):
data_base = datetime.now() - timedelta(days=30 * i) data_base = datetime.now() - timedelta(days=30 * i)
valor = random.uniform(50, 200) valor = random.uniform(50, 200)
cota = CotaMensal( cota = CotaMensal(
@@ -139,164 +210,123 @@ def criar_cotas(session, militantes, quantidade_por_militante=3):
except Exception as e: except Exception as e:
print(f"Erro ao criar cotas para militante {militante.nome}: {e}") print(f"Erro ao criar cotas para militante {militante.nome}: {e}")
session.rollback() session.rollback()
continue
print("Cotas criadas com sucesso!")
def criar_pagamentos(militantes): def criar_pagamentos(session, militantes):
"""Cria pagamentos fictícios""" """Cria pagamentos para os militantes"""
tipos_pagamento = ["Cota", "Jornal", "Assinatura", "Campanha Financeira"] print("\nCriando pagamentos...")
for militante in militantes: tipos_pagamento = session.query(TipoPagamento).all()
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() for militante in militantes:
try:
# Criar entre 3 e 8 pagamentos por militante
for _ in range(random.randint(3, 8)):
tipo = random.choice(tipos_pagamento)
pagamento = Pagamento(
militante_id=militante.id,
tipo_pagamento=tipo.descricao, # Usando a descrição do tipo
valor=random.uniform(50, 500),
data_pagamento=fake.date_between(start_date='-1y', end_date='today')
)
session.add(pagamento)
session.commit()
except Exception as e:
print(f"Erro ao criar pagamentos para militante {militante.nome}: {e}")
session.rollback()
def criar_setores(): def criar_materiais_vendidos(session, militantes):
"""Cria setores padrão""" """Cria registros de materiais vendidos"""
setores = [ print("\nCriando materiais vendidos...")
"Setor 1", tipos_material = session.query(TipoMaterial).all()
"Setor 2",
"Setor 3", for militante in militantes:
"Setor 4", try:
"Setor 5" # Criar entre 2 e 5 materiais vendidos por militante
] for _ in range(random.randint(2, 5)):
for setor in setores: material = MaterialVendido(
if not db_session.query(Setor).filter_by(nome=setor).first(): militante_id=militante.id,
db_session.add(Setor(nome=setor)) tipo_material_id=random.choice(tipos_material).id,
db_session.commit() descricao=fake.sentence(),
valor=random.uniform(20, 100),
data_venda=fake.date_time_between(start_date='-1y', end_date='now')
)
session.add(material)
session.commit()
except Exception as e:
print(f"Erro ao criar materiais vendidos para militante {militante.nome}: {e}")
session.rollback()
def criar_comites(): def criar_vendas_jornal(session, militantes):
"""Cria comitês padrão""" """Cria vendas de jornal avulso"""
comites = [ print("\nCriando vendas de jornal...")
"Comitê 1", for militante in militantes:
"Comitê 2", try:
"Comitê 3" # Criar entre 2 e 6 vendas de jornal por militante
] for _ in range(random.randint(2, 6)):
for comite in comites: quantidade = random.randint(1, 10)
if not db_session.query(ComiteCentral).filter_by(nome=comite).first(): valor_unitario = random.uniform(5, 15)
db_session.add(ComiteCentral(nome=comite)) venda = VendaJornalAvulso(
db_session.commit() militante_id=militante.id,
quantidade=quantidade,
valor_total=quantidade * valor_unitario,
data_venda=fake.date_time_between(start_date='-1y', end_date='now')
)
session.add(venda)
session.commit()
except Exception as e:
print(f"Erro ao criar vendas de jornal para militante {militante.nome}: {e}")
session.rollback()
def criar_roles(): def criar_assinaturas(session, militantes):
"""Cria roles padrão""" """Cria assinaturas anuais"""
roles = [ print("\nCriando assinaturas anuais...")
("admin", 1), # Nível 1: Administrador tipos_material = session.query(TipoMaterial).all()
("gestor", 2), # Nível 2: Gestor
("usuario", 3) # Nível 3: Usuário comum for militante in militantes:
] try:
for nome, nivel in roles: # 30% de chance de ter assinatura
if not db_session.query(Role).filter_by(nome=nome).first(): if random.random() < 0.3:
db_session.add(Role(nome=nome, nivel=nivel)) data_inicio = fake.date_time_between(start_date='-1y', end_date='now')
db_session.commit() assinatura = AssinaturaAnual(
militante_id=militante.id,
def criar_usuario_admin(): tipo_material_id=random.choice(tipos_material).id,
"""Cria usuário admin inicial""" quantidade=random.randint(1, 3),
if not db_session.query(Usuario).filter_by(username='admin').first(): valor_total=random.uniform(100, 500),
role_admin = db_session.query(Role).filter_by(nome='admin').first() data_inicio=data_inicio,
setor = db_session.query(Setor).first() data_fim=data_inicio + timedelta(days=365)
)
admin = Usuario( session.add(assinatura)
username='admin', session.commit()
email='admin@example.com', except Exception as e:
is_admin=True, print(f"Erro ao criar assinatura para militante {militante.nome}: {e}")
ativo=True, session.rollback()
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(): def seed_database():
"""Função principal para popular o banco de dados com dados fictícios""" """Função principal para popular o banco de dados"""
print("Populando banco de dados com dados fictícios...")
session = SessionLocal() session = SessionLocal()
try: try:
print("Iniciando população do banco de dados...")
# Criar estrutura organizacional
crs, setores = criar_estrutura_organizacional(session)
# Criar tipos básicos
criar_tipos_pagamento(session) criar_tipos_pagamento(session)
criar_tipos_material(session) criar_tipos_material(session)
militantes = criar_militantes(session, 50) # Criar militantes (30 militantes para teste)
if militantes: militantes = criar_militantes(session, 30, setores)
criar_cotas(session, militantes)
print("Dados fictícios criados com sucesso!") # Criar dados financeiros e materiais
criar_cotas(session, militantes)
criar_pagamentos(session, militantes)
criar_materiais_vendidos(session, militantes)
criar_vendas_jornal(session, militantes)
criar_assinaturas(session, militantes)
print("\nBanco de dados populado com sucesso!")
except Exception as e: except Exception as e:
print(f"Erro ao popular banco de dados: {e}") print(f"Erro durante a população do banco: {e}")
session.rollback() session.rollback()
finally: finally:
session.close() session.close()

View File

@@ -2,17 +2,17 @@ from setuptools import setup, find_packages
setup( setup(
name="controles", name="controles",
version="0.1.0", version="0.1",
packages=find_packages(), packages=find_packages(),
include_package_data=True,
install_requires=[ install_requires=[
"fastapi", 'flask',
"uvicorn", 'flask-login',
"sqlalchemy", 'flask-sqlalchemy',
"python-jose[cryptography]", 'flask-wtf',
"passlib[bcrypt]", 'flask-mail',
"python-multipart", 'python-dotenv',
"qrcode", 'pyotp',
"pillow", 'qrcode',
"python-dotenv"
], ],
) )

View File

@@ -20,6 +20,10 @@
--bs-success-dark: #157347; --bs-success-dark: #157347;
--bs-secondary: #6c757d; --bs-secondary: #6c757d;
--bs-secondary-dark: #565e64; --bs-secondary-dark: #565e64;
/* Variáveis para status */
--status-active: #28a745;
--status-inactive: #dc3545;
} }
/* Tabelas */ /* Tabelas */
@@ -560,4 +564,63 @@ input.btn-secondary:hover,
border-color: #0b5ed7 !important; border-color: #0b5ed7 !important;
color: white !important; color: white !important;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important;
}
/* Estilos para alertas */
.alert {
position: fixed;
top: 1rem;
left: 50%;
transform: translateX(-50%);
z-index: 9999;
min-width: 300px;
max-width: 600px;
text-align: center;
padding: 1rem 2.5rem 1rem 1rem;
margin: 0;
border-radius: 0.5rem;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
}
.alert .btn-close {
position: absolute;
top: 50%;
right: 1rem;
transform: translateY(-50%);
padding: 0.5rem;
}
.alert-success {
color: #0f5132;
background-color: #d1e7dd;
border-color: #badbcc;
}
.alert-danger {
color: #842029;
background-color: #f8d7da;
border-color: #f5c2c7;
}
.alert-warning {
color: #664d03;
background-color: #fff3cd;
border-color: #ffecb5;
}
.alert-info {
color: #055160;
background-color: #cff4fc;
border-color: #b6effb;
}
/* Status styles */
.status-active {
color: var(--status-active);
font-weight: 500;
}
.status-inactive {
color: var(--status-inactive);
font-weight: 500;
} }

53
static/css/styles.css Normal file
View File

@@ -0,0 +1,53 @@
/* Estilos globais para alertas do sistema */
.alert {
position: relative;
margin-bottom: 1rem;
}
/* Estilo base para o botão de fechar */
.alert .btn-close {
filter: none;
opacity: 1;
}
/* Alert Success */
.alert-success .btn-close {
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23198754'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;
}
/* Alert Danger */
.alert-danger .btn-close {
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23842029'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;
}
/* Alert Warning */
.alert-warning .btn-close {
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23997404'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;
}
/* Alert Info */
.alert-info .btn-close {
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23055160'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;
}
/* Efeito hover para todos os botões de fechar */
.alert .btn-close:hover {
opacity: 0.75;
}
/* Estilo das abas do modal */
.nav-tabs .nav-link {
/* remover estilos */
}
.nav-tabs .nav-link.active {
/* remover estilos */
}
.nav-tabs .nav-link:hover:not(.active) {
/* remover estilos */
}
.nav-tabs .nav-link i {
/* remover estilos */
}

1
static/img/favicon.ico Normal file
View File

@@ -0,0 +1 @@

View File

@@ -106,30 +106,87 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
// Validação de datas // Validação de datas
const dateInputs = document.querySelectorAll('input[type="date"]'); const dateInputs = document.querySelectorAll('input[type="date"], input.date-mask');
dateInputs.forEach(input => { dateInputs.forEach(input => {
input.addEventListener('change', function() { input.addEventListener('change', function() {
const date = new Date(this.value); console.log('Validando data:', this.value);
const today = new Date();
if (this.hasAttribute('min')) { let dataValida = true;
const minDate = new Date(this.getAttribute('min')); let mensagemErro = '';
if (date < minDate) {
this.setCustomValidity(`A data não pode ser anterior a ${minDate.toLocaleDateString()}`); // Se for um campo com máscara, validar o formato
this.classList.add('is-invalid'); if (this.classList.contains('date-mask')) {
return; if (!validarData(this.value)) {
dataValida = false;
mensagemErro = 'Por favor, insira uma data válida no formato DD/MM/AAAA';
}
} else {
// Para campos type="date", converter para Date
const date = new Date(this.value);
if (isNaN(date.getTime())) {
dataValida = false;
mensagemErro = 'Data inválida';
} }
} }
if (this.hasAttribute('max')) { // Validar limites de data
const maxDate = new Date(this.getAttribute('max')); if (dataValida) {
if (date > maxDate) { const hoje = new Date();
this.setCustomValidity(`A data não pode ser posterior a ${maxDate.toLocaleDateString()}`); hoje.setHours(0, 0, 0, 0);
this.classList.add('is-invalid');
return; let dataComparacao;
if (this.classList.contains('date-mask')) {
const [dia, mes, ano] = this.value.split('/').map(Number);
dataComparacao = new Date(ano, mes - 1, dia);
} else {
dataComparacao = new Date(this.value);
}
// Verificar data mínima
if (this.hasAttribute('min')) {
const minDate = new Date(this.getAttribute('min'));
if (dataComparacao < minDate) {
dataValida = false;
mensagemErro = `A data não pode ser anterior a ${minDate.toLocaleDateString()}`;
}
}
// Verificar data máxima
if (this.hasAttribute('max')) {
const maxDate = new Date(this.getAttribute('max'));
if (dataComparacao > maxDate) {
dataValida = false;
mensagemErro = `A data não pode ser posterior a ${maxDate.toLocaleDateString()}`;
}
}
// Verificar se é data futura (quando não permitido)
if (this.hasAttribute('data-no-future') && dataComparacao > hoje) {
dataValida = false;
mensagemErro = 'A data não pode ser futura';
} }
} }
// Atualizar validação do campo
if (!dataValida) {
console.warn('Data inválida:', this.value, mensagemErro);
this.setCustomValidity(mensagemErro);
this.classList.add('is-invalid');
// Atualizar mensagem de feedback
const feedback = this.nextElementSibling;
if (feedback && feedback.classList.contains('invalid-feedback')) {
feedback.textContent = mensagemErro;
}
} else {
console.log('Data válida:', this.value);
this.setCustomValidity('');
this.classList.remove('is-invalid');
}
});
// Limpar validação ao começar a digitar
input.addEventListener('input', function() {
this.setCustomValidity(''); this.setCustomValidity('');
this.classList.remove('is-invalid'); this.classList.remove('is-invalid');
}); });

File diff suppressed because it is too large Load Diff

View File

@@ -6,10 +6,30 @@ document.addEventListener('DOMContentLoaded', function() {
language: { language: {
url: '//cdn.datatables.net/plug-ins/1.13.7/i18n/pt-BR.json' url: '//cdn.datatables.net/plug-ins/1.13.7/i18n/pt-BR.json'
}, },
order: [[3, 'desc']], // Ordenar por data de pagamento (decrescente)
columnDefs: [ columnDefs: [
{ targets: -1, orderable: false } // Desabilitar ordenação na coluna de ações {
] targets: 3, // Coluna de data
type: 'date-br',
render: function(data, type, row) {
if (type === 'sort') {
return data.split('/').reverse().join('');
}
return data;
}
},
{
targets: 2, // Coluna de valor
type: 'numeric',
render: function(data, type, row) {
if (type === 'sort') {
return parseFloat(data.replace('R$ ', '').replace(',', '.'));
}
return data;
}
},
{ targets: -1, orderable: false } // Coluna de ações
],
order: [[3, 'desc']] // Ordenar por data decrescente por padrão
}); });
// Configuração do modal de edição // Configuração do modal de edição
@@ -253,4 +273,44 @@ document.addEventListener('DOMContentLoaded', function() {
} }
}); });
} }
// Funções de validação e formatação de datas
function validarData(data) {
if (!data) return false;
const dataObj = new Date(data);
if (isNaN(dataObj.getTime())) return false;
const hoje = new Date();
hoje.setHours(0, 0, 0, 0);
return dataObj <= hoje;
}
function formatarData(data) {
if (!data) return '';
const dataObj = new Date(data);
if (isNaN(dataObj.getTime())) return '';
return dataObj.toLocaleDateString('pt-BR');
}
// Configurar campos de data
const camposData = document.querySelectorAll('input[type="date"]');
camposData.forEach(campo => {
// Definir data máxima como hoje
const hoje = new Date().toISOString().split('T')[0];
campo.setAttribute('max', hoje);
campo.addEventListener('change', function() {
if (!validarData(this.value)) {
this.setCustomValidity('Data inválida ou futura');
this.classList.add('is-invalid');
} else {
this.setCustomValidity('');
this.classList.remove('is-invalid');
}
});
});
}); });

200
static/js/table_sort.js Normal file
View File

@@ -0,0 +1,200 @@
// Função para converter data DD/MM/YYYY para objeto Date
function converterDataParaComparacao(dataStr) {
console.log('Convertendo data para comparação:', dataStr);
if (!dataStr) return null;
try {
// Se já estiver no formato ISO
if (/^\d{4}-\d{2}-\d{2}/.test(dataStr)) {
const data = new Date(dataStr);
console.log('Data ISO convertida:', data);
return data;
}
// Se estiver no formato DD/MM/YYYY
if (/^\d{2}\/\d{2}\/\d{4}/.test(dataStr)) {
const [dia, mes, ano] = dataStr.split('/').map(Number);
const data = new Date(ano, mes - 1, dia);
console.log('Data DD/MM/YYYY convertida:', data);
return data;
}
console.warn('Formato de data não reconhecido:', dataStr);
return null;
} catch (error) {
console.error('Erro ao converter data:', error, 'Data:', dataStr);
return null;
}
}
// Função para ordenar tabelas
function configurarOrdenacaoTabela(tabelaId) {
console.log('Configurando ordenação para tabela:', tabelaId);
const table = document.getElementById(tabelaId);
if (!table) {
console.warn('Tabela não encontrada:', tabelaId);
return;
}
const headers = table.querySelectorAll('th[data-sort]');
headers.forEach(header => {
if (header.dataset.sort) {
header.addEventListener('click', () => {
const column = header.dataset.sort;
const tbody = table.getElementsByTagName('tbody')[0];
const rows = Array.from(tbody.getElementsByTagName('tr'));
console.log('Ordenando coluna:', column);
rows.sort((a, b) => {
const aValue = a.querySelector(`td[data-${column}]`).dataset[column];
const bValue = b.querySelector(`td[data-${column}]`).dataset[column];
// Ordenação por data
if (column === 'data' ||
column === 'data_vencimento' ||
column === 'data_alteracao' ||
column === 'data_pagamento' ||
column === 'data_venda' ||
column === 'data_relatorio') {
const aDate = converterDataParaComparacao(aValue);
const bDate = converterDataParaComparacao(bValue);
// Se alguma data for inválida
if (!aDate && !bDate) return 0;
if (!aDate) return 1;
if (!bDate) return -1;
return aDate - bDate;
}
// Ordenação por valor monetário
if (column === 'valor' ||
column === 'valor_total' ||
column === 'valor_antigo' ||
column === 'valor_novo') {
const aNum = parseFloat(aValue.replace(/[^\d,-]/g, '').replace(',', '.'));
const bNum = parseFloat(bValue.replace(/[^\d,-]/g, '').replace(',', '.'));
return aNum - bNum;
}
// Ordenação padrão para texto
return aValue.localeCompare(bValue);
});
// Alternar direção da ordenação
if (header.classList.contains('asc')) {
rows.reverse();
header.classList.remove('asc');
header.classList.add('desc');
console.log('Ordenação descendente');
} else {
header.classList.remove('desc');
header.classList.add('asc');
console.log('Ordenação ascendente');
}
// Atualizar tabela
tbody.innerHTML = '';
rows.forEach(row => tbody.appendChild(row));
});
}
});
}
// Configurar ordenação para todas as tabelas que precisam
document.addEventListener('DOMContentLoaded', function() {
console.log('Configurando ordenação para todas as tabelas...');
const tabelas = [
'materiaisTable',
'vendasTable',
'cotasTable',
'pagamentosTable'
];
tabelas.forEach(tabelaId => {
configurarOrdenacaoTabela(tabelaId);
});
});
document.addEventListener('DOMContentLoaded', function() {
console.log('Carregando script table_sort.js...');
// Função para comparar datas no formato DD/MM/YYYY
function compararDatas(a, b) {
if (!a || !b) return 0;
const [diaA, mesA, anoA] = a.split('/').map(Number);
const [diaB, mesB, anoB] = b.split('/').map(Number);
const dataA = new Date(anoA, mesA - 1, diaA);
const dataB = new Date(anoB, mesB - 1, diaB);
return dataA - dataB;
}
// Função para comparar valores monetários
function compararValores(a, b) {
const valorA = parseFloat(a.replace('R$ ', '').replace('.', '').replace(',', '.'));
const valorB = parseFloat(b.replace('R$ ', '').replace('.', '').replace(',', '.'));
if (isNaN(valorA)) return -1;
if (isNaN(valorB)) return 1;
return valorA - valorB;
}
// Configurar ordenação para todas as tabelas com classe 'table-sort'
document.querySelectorAll('table.table-sort').forEach(tabela => {
const tbody = tabela.querySelector('tbody');
const headers = tabela.querySelectorAll('th[data-sort]');
headers.forEach(header => {
const tipoOrdenacao = header.dataset.sort;
header.addEventListener('click', () => {
const rows = Array.from(tbody.querySelectorAll('tr'));
const colIndex = Array.from(header.parentNode.children).indexOf(header);
rows.sort((rowA, rowB) => {
const cellA = rowA.children[colIndex].dataset[tipoOrdenacao] || rowA.children[colIndex].textContent.trim();
const cellB = rowB.children[colIndex].dataset[tipoOrdenacao] || rowB.children[colIndex].textContent.trim();
switch (tipoOrdenacao) {
case 'data':
return compararDatas(cellA, cellB);
case 'valor':
return compararValores(cellA, cellB);
case 'numero':
return parseFloat(cellA) - parseFloat(cellB);
default:
return cellA.localeCompare(cellB);
}
});
if (header.classList.contains('asc')) {
rows.reverse();
header.classList.remove('asc');
header.classList.add('desc');
} else {
header.classList.remove('desc');
header.classList.add('asc');
}
// Remover classes de ordenação de outros headers
headers.forEach(h => {
if (h !== header) {
h.classList.remove('asc', 'desc');
}
});
// Atualizar tabela
tbody.innerHTML = '';
rows.forEach(row => tbody.appendChild(row));
});
});
});
});

284
static/js/testes.js Normal file
View File

@@ -0,0 +1,284 @@
// Testes para o formulário de edição de militantes
console.log('Iniciando testes do formulário de edição...');
// Lista de campos que devem existir no formulário
const camposEsperados = {
'edit_militante_id': { tipo: 'hidden', obrigatorio: true },
'edit_nome': { tipo: 'text', obrigatorio: true },
'edit_cpf': { tipo: 'text', obrigatorio: true },
'edit_titulo_eleitoral': { tipo: 'text', obrigatorio: false },
'edit_data_nascimento': { tipo: 'text', obrigatorio: false },
'edit_data_entrada_oci': { tipo: 'text', obrigatorio: false },
'edit_data_efetivacao_oci': { tipo: 'text', obrigatorio: false },
'edit_email': { tipo: 'email', obrigatorio: true },
'edit_telefone1': { tipo: 'text', obrigatorio: false },
'edit_telefone2': { tipo: 'text', obrigatorio: false },
'edit_cep': { tipo: 'text', obrigatorio: false },
'edit_estado': { tipo: 'select', obrigatorio: false },
'edit_cidade': { tipo: 'text', obrigatorio: false },
'edit_bairro': { tipo: 'text', obrigatorio: false },
'edit_rua': { tipo: 'text', obrigatorio: false },
'edit_numero': { tipo: 'text', obrigatorio: false },
'edit_complemento': { tipo: 'text', obrigatorio: false },
'edit_empresa': { tipo: 'text', obrigatorio: false },
'edit_contratante': { tipo: 'text', obrigatorio: false },
'edit_instituicao_ensino': { tipo: 'text', obrigatorio: false },
'edit_tipo_instituicao': { tipo: 'select', obrigatorio: false },
'edit_sindicato': { tipo: 'text', obrigatorio: false },
'edit_cargo_sindical': { tipo: 'text', obrigatorio: false },
'edit_central_sindical': { tipo: 'text', obrigatorio: false },
'edit_celula': { tipo: 'select', obrigatorio: false },
'responsabilidades_values': { tipo: 'hidden', obrigatorio: false }
};
// Função para testar a existência e configuração dos campos
function testarCamposFormulario() {
console.log('Testando campos do formulário...');
const form = document.getElementById('formEditarMilitante');
const erros = [];
if (!form) {
console.error('Formulário não encontrado!');
return false;
}
// Testar cada campo esperado
for (const [id, config] of Object.entries(camposEsperados)) {
const campo = document.getElementById(id);
if (!campo) {
erros.push(`Campo ${id} não encontrado`);
continue;
}
// Verificar tipo
if (campo.type !== config.tipo && config.tipo !== 'select') {
erros.push(`Campo ${id} tem tipo ${campo.type}, esperado ${config.tipo}`);
}
// Verificar obrigatoriedade
if (config.obrigatorio && !campo.hasAttribute('required')) {
erros.push(`Campo ${id} deveria ser obrigatório`);
}
// Verificar se o campo tem name attribute
if (!campo.hasAttribute('name')) {
erros.push(`Campo ${id} não tem atributo name`);
}
}
// Reportar erros encontrados
if (erros.length > 0) {
console.error('Erros encontrados nos campos:', erros);
return false;
}
console.log('Todos os campos estão configurados corretamente');
return true;
}
// Função para testar o carregamento de dados
async function testarCarregamentoDados(militanteId) {
console.log('Testando carregamento de dados...');
try {
const response = await fetch(`/militantes/dados/${militanteId}`);
if (!response.ok) {
throw new Error(`Erro HTTP: ${response.status}`);
}
const data = await response.json();
console.log('Dados recebidos:', data);
// Verificar se os dados foram carregados corretamente
const erros = [];
// Verificar campos básicos
if (!data.nome) erros.push('Nome não carregado');
if (!data.cpf) erros.push('CPF não carregado');
// Verificar se os campos foram preenchidos
for (const [id, config] of Object.entries(camposEsperados)) {
const campo = document.getElementById(id);
if (!campo) continue;
// Mapear campos do servidor para campos do formulário
let valorEsperado = '';
switch(id) {
case 'edit_nome': valorEsperado = data.nome; break;
case 'edit_cpf': valorEsperado = data.cpf; break;
case 'edit_email': valorEsperado = data.emails?.[0]; break;
case 'edit_telefone1': valorEsperado = data.telefone1; break;
case 'edit_celula': valorEsperado = data.celula_id?.toString(); break;
case 'edit_cargo_sindical': valorEsperado = data.cargo_sindical; break;
case 'edit_central_sindical': valorEsperado = data.central_sindical; break;
case 'edit_sindicato': valorEsperado = data.sindicato; break;
// Adicione mais campos conforme necessário
}
if (config.obrigatorio && !valorEsperado) {
erros.push(`Campo obrigatório ${id} não tem valor no servidor`);
}
if (valorEsperado && campo.value !== valorEsperado) {
erros.push(`Campo ${id} tem valor diferente do servidor. Esperado: ${valorEsperado}, Atual: ${campo.value}`);
}
}
if (erros.length > 0) {
console.error('Erros no carregamento:', erros);
return false;
}
console.log('Dados carregados corretamente');
return true;
} catch (error) {
console.error('Erro ao carregar dados:', error);
return false;
}
}
// Função para testar o salvamento de dados
async function testarSalvamentoDados(militanteId) {
console.log('Testando salvamento de dados...');
try {
const form = document.getElementById('formEditarMilitante');
const formData = new FormData(form);
// Guardar valores originais para comparação
const valoresOriginais = {
nome: formData.get('nome'),
cpf: formData.get('cpf'),
email: formData.get('email'),
celula: formData.get('celula'),
cargo_sindical: formData.get('cargo_sindical'),
central_sindical: formData.get('central_sindical'),
sindicato: formData.get('sindicato'),
responsabilidades: formData.get('responsabilidades_values')
};
const response = await fetch(`/militantes/editar/${militanteId}`, {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')
},
body: formData
});
if (!response.ok) {
throw new Error(`Erro HTTP: ${response.status}`);
}
const data = await response.json();
console.log('Resposta do servidor:', data);
// Verificar se os dados foram salvos corretamente
const row = document.querySelector(`tr[data-militante="${militanteId}"]`);
if (!row) {
console.error('Linha da tabela não encontrada após salvamento');
return false;
}
const erros = [];
// Verificar dados básicos na tabela
const nome = row.querySelector('td[data-nome]')?.textContent;
const cpf = row.querySelector('td[data-cpf]')?.textContent;
const email = row.querySelector('td[data-email]')?.textContent;
if (nome !== valoresOriginais.nome) erros.push(`Nome não atualizado na tabela. Esperado: ${valoresOriginais.nome}, Atual: ${nome}`);
if (cpf !== valoresOriginais.cpf) erros.push(`CPF não atualizado na tabela. Esperado: ${valoresOriginais.cpf}, Atual: ${cpf}`);
if (email !== valoresOriginais.email) erros.push(`Email não atualizado na tabela. Esperado: ${valoresOriginais.email}, Atual: ${email}`);
// Verificar atributos para filtros
const celulaId = row.getAttribute('data-celula-id');
const responsabilidades = row.getAttribute('data-responsabilidades');
if (celulaId !== valoresOriginais.celula) erros.push(`Célula não atualizada na tabela. Esperado: ${valoresOriginais.celula}, Atual: ${celulaId}`);
if (responsabilidades !== valoresOriginais.responsabilidades) erros.push(`Responsabilidades não atualizadas na tabela. Esperado: ${valoresOriginais.responsabilidades}, Atual: ${responsabilidades}`);
// Verificar botão de edição
const btnEditar = row.querySelector('button[data-bs-target="#modalEditarMilitante"]');
if (btnEditar) {
if (btnEditar.getAttribute('data-militante-nome') !== valoresOriginais.nome) {
erros.push('Nome não atualizado no botão de edição');
}
if (btnEditar.getAttribute('data-celula-id') !== valoresOriginais.celula) {
erros.push('Célula não atualizada no botão de edição');
}
}
if (erros.length > 0) {
console.error('Erros no salvamento:', erros);
return false;
}
console.log('Dados salvos e atualizados corretamente');
return true;
} catch (error) {
console.error('Erro ao salvar dados:', error);
return false;
}
}
// Função principal de teste
async function testarFormularioEdicao(militanteId) {
console.log('Iniciando teste completo do formulário...');
// Testar campos do formulário
if (!testarCamposFormulario()) {
console.error('Teste dos campos falhou');
return false;
}
// Testar carregamento de dados
if (!await testarCarregamentoDados(militanteId)) {
console.error('Teste de carregamento falhou');
return false;
}
// Testar salvamento de dados
if (!await testarSalvamentoDados(militanteId)) {
console.error('Teste de salvamento falhou');
return false;
}
console.log('Todos os testes passaram com sucesso!');
return true;
}
// Executar testes quando o documento estiver carregado
document.addEventListener('DOMContentLoaded', function() {
// Adicionar botão de teste na interface
const btnTeste = document.createElement('button');
btnTeste.className = 'btn btn-info me-2';
btnTeste.innerHTML = '<i class="fas fa-vial me-2"></i>Testar Formulário';
btnTeste.onclick = function() {
// Pegar ID do primeiro militante da lista
const primeiraLinha = document.querySelector('#militantesTable tbody tr');
if (!primeiraLinha) {
mostrarAlerta('danger', 'Nenhum militante encontrado para teste');
return;
}
const militanteId = primeiraLinha.getAttribute('data-militante');
if (!militanteId) {
mostrarAlerta('danger', 'ID do militante não encontrado');
return;
}
// Executar testes
testarFormularioEdicao(militanteId).then(sucesso => {
if (sucesso) {
mostrarAlerta('success', 'Testes concluídos com sucesso!');
} else {
mostrarAlerta('danger', 'Alguns testes falharam. Verifique o console para mais detalhes.');
}
});
};
// Adicionar botão ao lado do botão de exportar
const btnExportar = document.querySelector('.btn-exportar');
if (btnExportar && btnExportar.parentNode) {
btnExportar.parentNode.insertBefore(btnTeste, btnExportar);
}
});

119
static/js/vendas.js Normal file
View File

@@ -0,0 +1,119 @@
document.addEventListener('DOMContentLoaded', function() {
console.log('Carregando script vendas.js...');
// Funções de validação e formatação de datas
function validarData(data) {
if (!data) return false;
const dataObj = new Date(data);
if (isNaN(dataObj.getTime())) return false;
const hoje = new Date();
hoje.setHours(0, 0, 0, 0);
return dataObj <= hoje;
}
function formatarData(data) {
if (!data) return '';
const dataObj = new Date(data);
if (isNaN(dataObj.getTime())) return '';
return dataObj.toLocaleDateString('pt-BR');
}
// Configurar campos de data
const camposData = document.querySelectorAll('input[type="date"]');
camposData.forEach(campo => {
// Definir data máxima como hoje
const hoje = new Date().toISOString().split('T')[0];
campo.setAttribute('max', hoje);
campo.addEventListener('change', function() {
if (!validarData(this.value)) {
this.setCustomValidity('Data inválida ou futura');
this.classList.add('is-invalid');
} else {
this.setCustomValidity('');
this.classList.remove('is-invalid');
}
});
});
// Configurar tabela de vendas
const tabelaVendas = $('#vendasTable').DataTable({
language: {
url: '//cdn.datatables.net/plug-ins/1.13.7/i18n/pt-BR.json'
},
columnDefs: [
{
targets: 3, // Coluna de data
type: 'date-br',
render: function(data, type, row) {
if (type === 'sort') {
return data.split('/').reverse().join('');
}
return data;
}
},
{
targets: 2, // Coluna de valor
type: 'numeric',
render: function(data, type, row) {
if (type === 'sort') {
return parseFloat(data.replace('R$ ', '').replace(',', '.'));
}
return data;
}
},
{ targets: -1, orderable: false } // Coluna de ações
],
order: [[3, 'desc']] // Ordenar por data decrescente por padrão
});
// Atualizar valor total ao mudar quantidade ou material
const campoQuantidade = document.getElementById('quantidade');
const campoMaterial = document.getElementById('material_id');
const campoValorTotal = document.getElementById('valor_total');
function atualizarValorTotal() {
if (!campoQuantidade || !campoMaterial || !campoValorTotal) return;
const quantidade = parseInt(campoQuantidade.value) || 0;
const materialSelecionado = campoMaterial.options[campoMaterial.selectedIndex];
const preco = materialSelecionado ? parseFloat(materialSelecionado.dataset.preco) || 0 : 0;
campoValorTotal.value = (quantidade * preco).toFixed(2);
}
if (campoQuantidade) {
campoQuantidade.addEventListener('change', atualizarValorTotal);
}
if (campoMaterial) {
campoMaterial.addEventListener('change', atualizarValorTotal);
}
// Configurar modal de edição
const modalEditarVenda = document.getElementById('modalEditarVenda');
if (modalEditarVenda) {
modalEditarVenda.addEventListener('show.bs.modal', function(event) {
const button = event.relatedTarget;
if (!button) return;
const vendaId = button.getAttribute('data-venda-id');
const militanteId = button.getAttribute('data-militante-id');
const materialId = button.getAttribute('data-material-id');
const quantidade = button.getAttribute('data-quantidade');
const valorTotal = button.getAttribute('data-valor-total');
const dataVenda = button.getAttribute('data-data-venda');
document.getElementById('editVendaId').value = vendaId;
document.getElementById('editMilitanteId').value = militanteId;
document.getElementById('editMaterialId').value = materialId;
document.getElementById('editQuantidade').value = quantidade;
document.getElementById('editValorTotal').value = valorTotal;
document.getElementById('editDataVenda').value = dataVenda;
});
}
});

102
templates/admin/base.html Normal file
View File

@@ -0,0 +1,102 @@
{% extends "base.html" %}
{% block title %}Área Administrativa{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row">
<!-- Sidebar -->
<nav id="sidebar" class="col-md-3 col-lg-2 d-md-block bg-light sidebar">
<div class="position-sticky pt-3">
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'admin.dashboard' %}active{% endif %}"
href="{{ url_for('admin.dashboard') }}">
<i class="fas fa-tachometer-alt me-2"></i>
Dashboard
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'admin.list_users' %}active{% endif %}"
href="{{ url_for('admin.list_users') }}">
<i class="fas fa-users me-2"></i>
Usuários
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('home') }}">
<i class="fas fa-arrow-left me-2"></i>
Voltar ao Sistema
</a>
</li>
</ul>
</div>
</nav>
<!-- Main content -->
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">{% block admin_title %}{% endblock %}</h1>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block admin_content %}{% endblock %}
</main>
</div>
</div>
{% endblock %}
{% block extra_css %}
<style>
.sidebar {
position: fixed;
top: 0;
bottom: 0;
left: 0;
z-index: 100;
padding: 48px 0 0;
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
}
.sidebar .nav-link {
font-weight: 500;
color: #333;
}
.sidebar .nav-link.active {
color: #2470dc;
}
.sidebar-heading {
font-size: .75rem;
text-transform: uppercase;
}
main {
padding-top: 48px;
}
@media (max-width: 767.98px) {
.sidebar {
position: static;
padding-top: 0;
}
main {
padding-top: 0;
}
}
</style>
{% endblock %}
{% block extra_scripts %}{% endblock %}

View File

@@ -0,0 +1,227 @@
{% extends "admin/base.html" %}
{% block title %}Dashboard Administrativo{% endblock %}
{% block extra_css %}
<style>
.card {
border: none;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
transition: all 0.3s ease;
overflow: hidden;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 24px rgba(0,0,0,0.1);
}
.bg-primary {
background: linear-gradient(135deg, #0d6efd, #0a58ca) !important;
}
.bg-success {
background: linear-gradient(135deg, #198754, #146c43) !important;
}
.bg-danger {
background: linear-gradient(135deg, #dc3545, #b02a37) !important;
}
.card .opacity-50 {
opacity: 0.2 !important;
transition: all 0.3s ease;
}
.card:hover .opacity-50 {
opacity: 0.3 !important;
transform: scale(1.1);
}
.card-title {
font-size: 0.9rem;
font-weight: 600;
margin-bottom: 1rem;
text-transform: uppercase;
color: rgba(255,255,255,0.8);
}
.display-4 {
font-size: 2.5rem;
font-weight: 600;
margin: 0.5rem 0;
}
.btn-group {
gap: 0.25rem;
}
/* Estilo da lista de usuários */
.card.lista-usuarios {
border-radius: 0;
box-shadow: none;
transition: none;
border: 1px solid #dee2e6;
}
.card.lista-usuarios:hover {
transform: none;
box-shadow: none;
}
.card.lista-usuarios .card-header {
background: linear-gradient(to right, var(--secondary-dark), var(--secondary-color));
color: white;
border: none;
padding: 1rem 1.5rem;
}
.card.lista-usuarios .card-header h5 {
margin: 0;
font-size: 1.1rem;
font-weight: 500;
}
.card.lista-usuarios .table {
margin-bottom: 0;
}
.card.lista-usuarios .table th {
border-top: none;
font-weight: 600;
padding: 1rem;
background-color: #f8f9fa;
}
.card.lista-usuarios .table td {
padding: 1rem;
vertical-align: middle;
}
.card.lista-usuarios .badge {
padding: 0.5em 0.8em;
font-weight: 500;
}
.btn-group .btn {
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
}
</style>
{% endblock %}
{% block content %}
<h2 class="mb-4">
<i class="fas fa-users-cog"></i>
Administração de Usuários
</h2>
<div class="row mb-4">
<div class="col-md-4">
<div class="card bg-primary text-white">
<div class="card-body">
<h5 class="card-title text-uppercase">Total de Usuários</h5>
<div class="d-flex justify-content-between align-items-center">
<h2 class="display-4 mb-0">{{ total_users }}</h2>
<i class="fas fa-users fa-3x opacity-50"></i>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card bg-success text-white">
<div class="card-body">
<h5 class="card-title text-uppercase">Usuários Ativos</h5>
<div class="d-flex justify-content-between align-items-center">
<h2 class="display-4 mb-0">{{ active_users }}</h2>
<i class="fas fa-user-check fa-3x opacity-50"></i>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card bg-danger text-white">
<div class="card-body">
<h5 class="card-title text-uppercase">Usuários Inativos</h5>
<div class="d-flex justify-content-between align-items-center">
<h2 class="display-4 mb-0">{{ inactive_users }}</h2>
<i class="fas fa-user-times fa-3x opacity-50"></i>
</div>
</div>
</div>
</div>
</div>
<div class="card lista-usuarios">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">
<i class="fas fa-users me-2"></i>
Lista de Usuários
</h5>
</div>
<div class="card-body p-0">
<table id="users-table" class="table table-striped table-hover">
<thead>
<tr>
<th>Nome</th>
<th>Email</th>
<th>Status</th>
<th>Último Login</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{ user.name }}</td>
<td>{{ user.email }}</td>
<td>
<span class="badge {% if user.is_active %}bg-success{% else %}bg-danger{% endif %}">
{{ "Ativo" if user.is_active else "Inativo" }}
</span>
</td>
<td>{{ user.last_login.strftime('%d/%m/%Y %H:%M') if user.last_login else 'Nunca' }}</td>
<td>
<div class="btn-group">
<form action="{{ url_for('admin.reset_user_otp', user_id=user.id) }}" method="post" class="d-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-warning btn-sm" title="Reset OTP" onclick="return confirm('Confirma o reset do OTP deste usuário?')">
<i class="fas fa-key"></i>
</button>
</form>
<form action="{{ url_for('admin.reset_user_password', user_id=user.id) }}" method="post" class="d-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-info btn-sm" title="Reset Senha" onclick="return confirm('Confirma o reset da senha deste usuário?')">
<i class="fas fa-lock"></i>
</button>
</form>
<form action="{{ url_for('admin.toggle_user_status', user_id=user.id) }}" method="post" class="d-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-{{ 'danger' if user.is_active else 'success' }} btn-sm" title="{{ 'Desativar' if user.is_active else 'Ativar' }} Usuário">
<i class="fas fa-{{ 'user-times' if user.is_active else 'user-check' }}"></i>
</button>
</form>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
$(document).ready(function() {
$('#users-table').DataTable({
language: {
url: '//cdn.datatables.net/plug-ins/1.13.7/i18n/pt-BR.json'
},
order: [[0, 'asc']],
pageLength: 25
});
});
</script>
{% endblock %}

View File

@@ -4,14 +4,15 @@
<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">
<meta name="csrf-token" content="{{ csrf_token() }}"> <meta name="csrf-token" content="{{ csrf_token() }}">
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='img/favicon.ico') }}">
<title>{% block title %}{% endblock %} - Controles OCI</title> <title>{% block title %}{% endblock %} - Controles OCI</title>
<!-- Bootstrap 5 CSS --> <!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css?v=1" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css?v=1" rel="stylesheet">
<!-- Font Awesome 6 --> <!-- Font Awesome 6 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css?v=1"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<!-- Componentes CSS --> <!-- Componentes CSS -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/components.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/components.css') }}?v={{ range(1, 10000) | random }}">
<style> <style>
:root { :root {
@@ -598,6 +599,11 @@
<i class="fas fa-user-plus"></i>Novo Usuário <i class="fas fa-user-plus"></i>Novo Usuário
</a> </a>
</li> </li>
<li>
<a class="dropdown-item" href="{{ url_for('admin.dashboard') }}">
<i class="fas fa-cog fa fa-cog fa-solid fa-cog" style="display: inline-block !important; visibility: visible !important;"></i>Administração
</a>
</li>
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
{% endif %} {% endif %}
<li> <li>

View File

@@ -17,6 +17,7 @@
{% endwith %} {% endwith %}
<form method="POST" class="needs-validation" novalidate> <form method="POST" class="needs-validation" novalidate>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="row"> <div class="row">
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label for="nome" class="form-label">Nome</label> <label for="nome" class="form-label">Nome</label>

View File

@@ -1,63 +1,69 @@
{% extends 'base.html' %} {% extends "base.html" %}
{% block title %}Dashboard Administrativo{% endblock %} {% block title %}Dashboard Administrativo{% endblock %}
{% block content %} {% block content %}
<div class="container"> <div class="container mt-4">
<h1 class="mb-4">Dashboard Administrativo</h1> <h2 class="mb-4"><i class="fas fa-users-cog"></i> Administração de Usuários</h2>
<div class="card">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title mb-0">Gerenciamento de Usuários</h5>
</div>
<div class="card-body"> <div class="card-body">
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped"> <table class="table table-hover">
<thead> <thead class="thead-light">
<tr> <tr>
<th>ID</th>
<th>Usuário</th> <th>Usuário</th>
<th>Email</th> <th>Email</th>
<th>Admin</th> <th>Nome</th>
<th>OTP Configurado</th> <th>Último Acesso</th>
<th>Status</th>
<th>Nível</th>
<th>Ações</th> <th>Ações</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for usuario in usuarios %} {% for usuario in usuarios %}
<tr> <tr>
<td>{{ usuario.id }}</td>
<td>{{ usuario.username }}</td> <td>{{ usuario.username }}</td>
<td>{{ usuario.email }}</td> <td>{{ usuario.email }}</td>
<td>{{ usuario.nome }}</td>
<td>{{ usuario.last_login }}</td>
<td>
<span class="badge {% if usuario.ativo %}bg-success{% else %}bg-danger{% endif %}">
{{ "Ativo" if usuario.ativo else "Inativo" }}
</span>
</td>
<td> <td>
{% if usuario.is_admin %} {% if usuario.is_admin %}
<span class="badge bg-success">Sim</span> Administrador
{% else %} {% else %}
<span class="badge bg-secondary">Não</span> {{ usuario.nivel }}
{% endif %} {% endif %}
</td> </td>
<td> <td>
{% if usuario.otp_secret %} <div class="btn-group" role="group">
<span class="badge bg-success">Sim</span> <button class="btn btn-sm btn-outline-primary"
{% else %} onclick="toggleStatus('{{ usuario.id }}')"
<span class="badge bg-danger">Não</span> data-toggle="tooltip"
{% endif %} title="{{ 'Desativar' if usuario.ativo else 'Ativar' }} usuário">
</td> <i class="fas {% if usuario.ativo %}fa-user-times{% else %}fa-user-check{% endif %}"></i>
<td>
<form action="{{ url_for('reset_otp', user_id=usuario.id) }}" method="POST" class="d-inline">
<button type="submit" class="btn btn-warning btn-sm"
onclick="return confirm('Tem certeza que deseja resetar o OTP deste usuário?')">
Resetar OTP
</button> </button>
</form>
<button class="btn btn-sm btn-outline-warning"
onclick="resetarSenha('{{ usuario.id }}')"
data-toggle="tooltip"
title="Resetar senha">
<i class="fas fa-key"></i>
</button>
{% if not usuario.is_admin %}
<button class="btn btn-sm btn-outline-info"
onclick="alterarNivel('{{ usuario.id }}')"
data-toggle="tooltip"
title="Alterar nível">
<i class="fas fa-level-up-alt"></i>
</button>
{% endif %}
</div>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
@@ -66,18 +72,127 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="card">
<div class="card-header"> <!-- Modal de Feedback -->
<h5 class="card-title mb-0">Ações Rápidas</h5> <div class="modal fade" id="feedbackModal" tabindex="-1" role="dialog">
</div> <div class="modal-dialog" role="document">
<div class="card-body"> <div class="modal-content">
<div class="d-grid gap-2"> <div class="modal-header">
<a href="{{ url_for('novo_usuario') }}" class="btn btn-primary"> <h5 class="modal-title">Aviso</h5>
Criar Novo Usuário <button type="button" class="close" data-dismiss="modal">
</a> <span>&times;</span>
</button>
</div>
<div class="modal-body">
<p id="feedbackMessage"></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Fechar</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endblock %}
{% block scripts %}
<script>
function showFeedback(message, type = 'info') {
const modal = document.getElementById('feedbackModal');
const messageElement = document.getElementById('feedbackMessage');
messageElement.textContent = message;
messageElement.className = `alert alert-${type}`;
$(modal).modal('show');
}
function handleResponse(response) {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}
function toggleStatus(userId) {
if (!confirm('Tem certeza que deseja alterar o status deste usuário?')) {
return;
}
fetch(`/usuarios/${userId}/toggle_status`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').content
}
})
.then(handleResponse)
.then(data => {
showFeedback(data.message || 'Status alterado com sucesso!', data.success ? 'success' : 'danger');
if (data.success) {
setTimeout(() => location.reload(), 1500);
}
})
.catch(error => {
console.error('Error:', error);
showFeedback('Erro ao alterar status do usuário. Por favor, tente novamente.', 'danger');
});
}
function resetarSenha(userId) {
if (!confirm('Tem certeza que deseja resetar a senha deste usuário?')) {
return;
}
fetch(`/reset_password/${userId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').content
}
})
.then(handleResponse)
.then(data => {
showFeedback(data.message || 'Senha resetada com sucesso!', data.success ? 'success' : 'danger');
})
.catch(error => {
console.error('Error:', error);
showFeedback('Erro ao resetar senha. Por favor, tente novamente.', 'danger');
});
}
function alterarNivel(userId) {
const novoNivel = prompt('Digite o novo nível do usuário (1-5):');
if (!novoNivel) return;
if (!/^[1-5]$/.test(novoNivel)) {
showFeedback('Por favor, insira um nível válido entre 1 e 5.', 'warning');
return;
}
fetch(`/usuarios/${userId}/alterar_nivel`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({ nivel: parseInt(novoNivel) })
})
.then(handleResponse)
.then(data => {
showFeedback(data.message || 'Nível alterado com sucesso!', data.success ? 'success' : 'danger');
if (data.success) {
setTimeout(() => location.reload(), 1500);
}
})
.catch(error => {
console.error('Error:', error);
showFeedback('Erro ao alterar nível. Por favor, tente novamente.', 'danger');
});
}
// Inicializa os tooltips do Bootstrap
$(function () {
$('[data-toggle="tooltip"]').tooltip();
});
</script>
{% endblock %} {% endblock %}

View File

@@ -16,7 +16,9 @@
{% endif %} {% endif %}
{% endwith %} {% endwith %}
<form method="POST" class="needs-validation" novalidate> <form id="formEditarMilitante" method="POST" class="needs-validation" novalidate>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="militante_id" value="{{ militante.id }}">
<div class="row"> <div class="row">
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label for="nome" class="form-label">Nome</label> <label for="nome" class="form-label">Nome</label>
@@ -28,7 +30,7 @@
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label for="email" class="form-label">Email</label> <label for="email" class="form-label">Email</label>
<input type="email" class="form-control" id="email" name="email" value="{{ militante.email }}" required> <input type="email" class="form-control" id="email" name="email" value="{{ militante.emails[0].endereco_email if militante.emails else '' }}" required>
<div class="invalid-feedback"> <div class="invalid-feedback">
Por favor, insira um email válido. Por favor, insira um email válido.
</div> </div>
@@ -209,21 +211,43 @@
<script> <script>
// Validação do formulário // Validação do formulário
(function () { (function () {
'use strict' 'use strict';
var forms = document.querySelectorAll('.needs-validation') var forms = document.querySelectorAll('.needs-validation');
Array.prototype.slice.call(forms) Array.prototype.slice.call(forms)
.forEach(function (form) { .forEach(function (form) {
form.addEventListener('submit', function (event) { form.addEventListener('submit', function (event) {
event.preventDefault();
if (!form.checkValidity()) { if (!form.checkValidity()) {
event.preventDefault() event.stopPropagation();
event.stopPropagation() } else {
salvarAlteracoesMilitante({{ militante.id }});
} }
form.classList.add('was-validated') form.classList.add('was-validated');
}, false) }, false);
}) });
})() })();
// Função para mostrar alertas
function mostrarAlerta(mensagem, tipo) {
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${tipo} alert-dismissible fade show`;
alertDiv.role = 'alert';
alertDiv.innerHTML = `
${mensagem}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
`;
const container = document.querySelector('.container');
container.insertBefore(alertDiv, container.firstChild);
// Remover o alerta após 5 segundos
setTimeout(() => {
alertDiv.remove();
}, 5000);
}
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -87,7 +87,7 @@
data-militante-nome="{{ militante.nome }}"> data-militante-nome="{{ militante.nome }}">
<div class="militante-info"> <div class="militante-info">
<h6 class="mb-1">{{ militante.nome }}</h6> <h6 class="mb-1">{{ militante.nome }}</h6>
<small>{{ militante.email }}</small> <small>{{ militante.emails[0].endereco_email if militante.emails else '' }}</small>
</div> </div>
<i class="fas fa-chevron-right text-muted"></i> <i class="fas fa-chevron-right text-muted"></i>
</div> </div>

View File

@@ -1,155 +1,189 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block head %}
<!-- Bootstrap Datepicker CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datepicker/1.9.0/css/bootstrap-datepicker.min.css">
{% endblock %}
{% block title %}Militantes{% endblock %} {% block title %}Militantes{% endblock %}
{% block content %} {% block content %}
<div class="row mb-4"> <div class="container-fluid">
<div class="col-12"> <div class="row mb-4">
<div class="d-flex justify-content-between align-items-center"> <div class="col-12">
<h1 class="h3 mb-0"> <div class="d-flex justify-content-between align-items-center">
<i class="fas fa-users me-2"></i>Militantes <h1 class="h3 mb-0">
</h1> <i class="fas fa-users me-2"></i>Militantes
{% if current_user.has_permission('gerenciar_militantes') %} </h1>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalNovoMilitante"> <div>
<i class="fas fa-user-plus me-2"></i>Novo Militante <button type="button" class="btn btn-outline-primary me-2" id="btnExportar">
</button> <i class="fas fa-file-export me-2"></i>Exportar
{% endif %} </button>
<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>
</div>
</div>
</div> </div>
</div> </div>
</div>
<div class="card"> <div class="row">
<div class="card-body"> <div class="col-12">
<div class="row mb-4"> <div class="card">
<div class="col-md-6"> <div class="card-body">
<div class="input-group"> <div class="row mb-4">
<span class="input-group-text"> <div class="col-md-6">
<i class="fas fa-search"></i> <div class="input-group">
</span> <span class="input-group-text">
<input type="text" class="form-control" id="searchInput" placeholder="Pesquisar militantes..."> <i class="fas fa-search"></i>
</div> </span>
</div> <input type="text" class="form-control" id="searchInput" placeholder="Pesquisar militantes...">
<div class="col-md-6 text-end">
<div class="btn-group me-2">
<button type="button" class="btn btn-outline-secondary btn-fixed-width dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fas fa-filter me-2"></i>Filtrar
</button>
<ul class="dropdown-menu">
<li><h6 class="dropdown-header">Status</h6></li>
<li><a class="dropdown-item" href="#" data-filter="todos">Todos</a></li>
<li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">Responsabilidades</h6></li>
<li><a class="dropdown-item" href="#" data-filter="financas">Finanças</a></li>
<li><a class="dropdown-item" href="#" data-filter="imprensa">Imprensa</a></li>
<li><a class="dropdown-item" href="#" data-filter="quadro-orientador">Quadro-Orientador</a></li>
<li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">Célula</h6></li>
{% for celula in celulas %}
<li><a class="dropdown-item" href="#" data-filter="celula" data-celula="{{ celula.nome }}">{{ celula.nome }}</a></li>
{% endfor %}
</ul>
</div>
<button class="btn btn-outline-primary btn-fixed-width" type="button" id="btnExportar">
<i class="fas fa-file-export me-2"></i>Exportar
</button>
</div>
</div>
<div class="table-responsive">
<table class="table table-hover" id="militantesTable">
<thead>
<tr>
<th 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> </div>
</td> </div>
</tr> <div class="col-md-6 text-end">
{% endfor %} <div class="btn-group me-2">
</tbody> <button type="button" class="btn btn-outline-secondary btn-fixed-width dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
</table> <i class="fas fa-filter me-2"></i>Filtrar
</div> </button>
<ul class="dropdown-menu">
<li><h6 class="dropdown-header">Status</h6></li>
<li><a class="dropdown-item" href="#" data-filter="todos">Todos</a></li>
<li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">Responsabilidades</h6></li>
<li><a class="dropdown-item" href="#" data-filter="responsavel-financas">Responsável de Finanças</a></li>
<li><a class="dropdown-item" href="#" data-filter="responsavel-imprensa">Responsável de Imprensa</a></li>
<li><a class="dropdown-item" href="#" data-filter="quadro-orientador">Quadro-Orientador</a></li>
<li><a class="dropdown-item" href="#" data-filter="secretario">Secretário</a></li>
<li><a class="dropdown-item" href="#" data-filter="tesoureiro">Tesoureiro</a></li>
<li><a class="dropdown-item" href="#" data-filter="imprensa">Imprensa</a></li>
<li><a class="dropdown-item" href="#" data-filter="mns">MNS</a></li>
<li><a class="dropdown-item" href="#" data-filter="mps">MPS</a></li>
<li><a class="dropdown-item" href="#" data-filter="juventude">Juventude</a></li>
<li><a class="dropdown-item" href="#" data-filter="aspirante">Aspirante</a></li>
<li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">Célula</h6></li>
{% for celula in celulas %}
<li><a class="dropdown-item" href="#" data-filter="celula" data-celula="{{ celula.id }}">{{ celula.nome }}</a></li>
{% endfor %}
</ul>
</div>
</div>
</div>
<div class="pagination-container d-flex justify-content-between align-items-center"> <div class="table-responsive">
<div class="text-muted"> <table class="table table-hover" id="militantesTable">
Mostrando <span id="countMilitantes">{{ militantes|length }}</span> militantes <thead>
</div> <tr>
<div class="d-flex align-items-center gap-3"> <th data-sort="nome">Nome <i class="fas fa-sort"></i></th>
<div class="d-flex align-items-center"> <th data-sort="cpf">CPF <i class="fas fa-sort"></i></th>
<span class="me-2">Mostrar</span> <th data-sort="email">Email <i class="fas fa-sort"></i></th>
<select class="form-select form-select-sm me-2" id="rowsPerPage" style="width: auto;"> <th data-sort="telefone">Telefone <i class="fas fa-sort"></i></th>
<option value="10">10</option> <th>Responsabilidades</th>
<option value="20" selected>20</option> <th class="text-end">Ações</th>
<option value="50">50</option> </tr>
<option value="100">100</option> </thead>
</select> <tbody>
<span>linhas</span> {% for militante in militantes %}
<tr data-militante="{{ militante.id }}"
data-celula-id="{{ militante.celula_id }}"
data-responsabilidades="{{ militante.responsabilidades }}">
<td data-nome="{{ militante.nome }}">{{ militante.nome }}</td>
<td data-cpf="{{ militante.cpf }}">{{ militante.cpf }}</td>
<td data-email="{{ militante.emails[0].endereco_email if militante.emails else '' }}">{{ militante.emails[0].endereco_email if militante.emails else '' }}</td>
<td data-telefone="{{ militante.telefone1 }}">{{ militante.telefone1 }}</td>
<td>
{% if militante.responsabilidades is defined and militante.responsabilidades %}
{% if militante.responsabilidades|bitwise_and(Militante.RESPONSAVEL_FINANCAS) %}
<span class="badge bg-primary" title="Responsável de Finanças">RFI</span>
{% endif %}
{% if militante.responsabilidades|bitwise_and(Militante.RESPONSAVEL_IMPRENSA) %}
<span class="badge bg-info" title="Responsável de Imprensa">RIM</span>
{% endif %}
{% if militante.responsabilidades|bitwise_and(Militante.QUADRO_ORIENTADOR) %}
<span class="badge bg-success" title="Quadro-Orientador">QOR</span>
{% endif %}
{% if militante.responsabilidades|bitwise_and(Militante.SECRETARIO) %}
<span class="badge bg-secondary" title="Secretário">SEC</span>
{% endif %}
{% if militante.responsabilidades|bitwise_and(Militante.TESOUREIRO) %}
<span class="badge bg-warning" title="Tesoureiro">TES</span>
{% endif %}
{% if militante.responsabilidades|bitwise_and(Militante.IMPRENSA) %}
<span class="badge bg-danger" title="Imprensa">IMP</span>
{% endif %}
{% if militante.responsabilidades|bitwise_and(Militante.MNS) %}
<span class="badge bg-purple" title="MNS">MNS</span>
{% endif %}
{% if militante.responsabilidades|bitwise_and(Militante.MPS) %}
<span class="badge bg-teal" title="MPS">MPS</span>
{% endif %}
{% if militante.responsabilidades|bitwise_and(Militante.JUVENTUDE) %}
<span class="badge bg-orange" title="Juventude">JUV</span>
{% endif %}
{% if militante.responsabilidades|bitwise_and(Militante.ASPIRANTE) %}
<span class="badge bg-dark" title="Aspirante">ASP</span>
{% endif %}
{% 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="#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>
</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>
<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>
@@ -162,11 +196,70 @@
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script src="{{ url_for('static', filename='js/militantes.js') }}"></script> <!-- jQuery -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<!-- jQuery Mask Plugin -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.mask/1.14.16/jquery.mask.min.js"></script>
<!-- Nosso script -->
<script src="{{ url_for('static', filename='js/militantes.js') }}"></script>
{% endblock %} {% endblock %}
{% block extra_css %} {% block extra_css %}
<style> <style>
/* Estilo para o botão Novo Militante */
.btn-primary {
background-color: var(--bs-danger);
border-color: var(--bs-danger);
}
.btn-primary:hover,
.btn-primary:focus,
.btn-primary:active {
background-color: var(--bs-danger-dark, #b02a37) !important;
border-color: var(--bs-danger-dark, #b02a37) !important;
}
/* Estilo para os switches */
.form-check-input {
background-color: #fff;
border-color: rgba(220, 53, 69, 0.5);
}
.form-check-input:checked {
background-color: var(--bs-danger);
border-color: var(--bs-danger);
}
.form-check-input:focus {
border-color: var(--bs-danger);
box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.25);
}
.form-switch .form-check-input {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28220, 53, 69, 0.85%29'/%3e%3c/svg%3e");
background-position: left center;
border-radius: 2em;
transition: background-position .15s ease-in-out;
}
.form-switch .form-check-input:checked {
background-position: right center;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e");
}
.form-check {
min-height: 1.5rem;
padding-left: 2.8em;
margin-bottom: 0.5rem;
}
.form-check-label {
cursor: pointer;
user-select: none;
}
/* Estilo para o backdrop com blur em todos os modais */ /* Estilo para o backdrop com blur em todos os modais */
.modal-backdrop.show { .modal-backdrop.show {
backdrop-filter: blur(8px); backdrop-filter: blur(8px);
@@ -238,7 +331,41 @@ th[data-sort].sort-desc i {
/* Estilo para badges */ /* Estilo para badges */
.badge { .badge {
font-weight: 500; font-weight: 500;
padding: 0.5em 0.8em; padding: 0.4em 0.6em;
font-size: 0.75rem;
margin-right: 0.3rem;
min-width: 2em;
text-align: center;
border-radius: 4px;
cursor: help;
}
.badge:last-child {
margin-right: 0;
}
/* Cores personalizadas para badges */
.bg-purple { background-color: #6f42c1 !important; color: white !important; }
.bg-teal { background-color: #20c997 !important; color: white !important; }
.bg-orange { background-color: #fd7e14 !important; color: white !important; }
.bg-indigo { background-color: #6610f2 !important; color: white !important; }
.bg-pink { background-color: #d63384 !important; color: white !important; }
/* Cores do Bootstrap que vamos usar */
.badge.bg-primary { background-color: #0d6efd !important; }
.badge.bg-info { background-color: #0dcaf0 !important; }
.badge.bg-success { background-color: #198754 !important; }
.badge.bg-danger { background-color: #dc3545 !important; }
.badge.bg-dark { background-color: #212529 !important; }
/* Tooltip personalizado */
.tooltip {
font-size: 0.875rem;
}
.tooltip .tooltip-inner {
padding: 0.5rem 0.75rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
} }
/* Estilo para botões de ação */ /* Estilo para botões de ação */
@@ -294,5 +421,91 @@ th[data-sort].sort-desc i {
min-width: 120px; min-width: 120px;
} }
} }
/* Estilos personalizados para o Bootstrap Datepicker */
.datepicker {
padding: 4px;
border-radius: 4px;
border: 1px solid #dee2e6;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
font-size: 0.875rem;
background-color: white !important;
color: #212529 !important;
}
.datepicker table {
background-color: white !important;
}
.datepicker table tr td,
.datepicker table tr th {
text-align: center;
width: 30px;
height: 30px;
border-radius: 2px;
color: #212529 !important;
background-color: white !important;
}
.datepicker table tr td.day:hover,
.datepicker table tr td.focused {
background: #f8f9fa !important;
color: #212529 !important;
}
.datepicker table tr td.active,
.datepicker table tr td.active:hover {
background-color: var(--bs-primary) !important;
color: white !important;
}
.datepicker table tr td.today {
background-color: #e9ecef !important;
color: #212529 !important;
}
.datepicker .datepicker-switch,
.datepicker .prev,
.datepicker .next {
font-weight: normal;
padding: 4px;
color: #212529 !important;
background-color: white !important;
}
.datepicker .dow {
font-weight: normal;
padding: 4px;
color: #212529 !important;
background-color: white !important;
}
.datepicker-dropdown:after {
border-bottom-color: white !important;
}
.datepicker-dropdown.datepicker-orient-top:after {
border-top-color: white !important;
}
/* Estilo para os campos de data */
.datepicker-input {
background-color: white !important;
color: #212529 !important;
cursor: pointer;
}
.datepicker-clear-btn {
color: #6c757d !important;
background-color: white !important;
padding: 5px 10px;
font-size: 12px;
border-radius: 4px;
}
.datepicker-clear-btn:hover {
background-color: #f8f9fa !important;
color: #495057 !important;
}
</style> </style>
{% endblock %} {% endblock %}

View File

@@ -3,47 +3,47 @@
{% block title %}Listar Relatórios de Cotas{% endblock %} {% block title %}Listar Relatórios de Cotas{% endblock %}
{% block content %} {% block content %}
<div class="container"> <div class="container mt-4">
<div class="row"> <div class="card">
<div class="col-md-12"> <div class="card-header d-flex justify-content-between align-items-center">
<h1 class="mb-4">Lista de Relatórios de Cotas</h1> <h5 class="mb-0"><i class="fas fa-file-invoice-dollar me-2"></i>Relatórios de Cotas</h5>
<a href="{{ url_for('novo_relatorio_cotas') }}" class="btn btn-success">
{% with messages = get_flashed_messages(with_categories=true) %} <i class="fas fa-plus me-2"></i>Novo Relatório
{% if messages %} </a>
{% for category, message in messages %} </div>
<div class="alert alert-{{ category }}">{{ message }}</div> <div class="card-body">
{% endfor %}
{% endif %}
{% endwith %}
<div class="d-flex justify-content-between mb-4">
<a href="{{ url_for('novo_relatorio_cotas') }}" class="btn btn-success">Novo Relatório</a>
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
</div>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped table-hover"> <table class="table table-hover" id="relatoriosTable">
<thead> <thead>
<tr> <tr>
<th>ID</th> <th data-sort="id">ID <i class="fas fa-sort"></i></th>
<th>Setor</th> <th data-sort="setor">Setor <i class="fas fa-sort"></i></th>
<th>Comitê Central</th> <th data-sort="comite">Comitê Central <i class="fas fa-sort"></i></th>
<th>Total de Cotas</th> <th data-sort="total">Total de Cotas <i class="fas fa-sort"></i></th>
<th>Data do Relatório</th> <th data-sort="data">Data do Relatório <i class="fas fa-sort"></i></th>
<th>Ações</th> <th class="text-end">Ações</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for relatorio in relatorios %} {% for relatorio in relatorios %}
<tr> <tr>
<td>{{ relatorio.id }}</td> <td data-id="{{ relatorio.id }}">{{ relatorio.id }}</td>
<td>{{ relatorio.setor.nome }}</td> <td data-setor="{{ relatorio.setor.nome }}">{{ relatorio.setor.nome }}</td>
<td>{{ relatorio.comite.nome }}</td> <td data-comite="{{ relatorio.comite.nome }}">{{ relatorio.comite.nome }}</td>
<td>R$ {{ "%.2f"|format(relatorio.total_cotas) }}</td> <td data-total="{{ relatorio.total_cotas }}">R$ {{ "%.2f"|format(relatorio.total_cotas) }}</td>
<td>{{ relatorio.data_relatorio.strftime('%d/%m/%Y') }}</td> <td data-data="{{ relatorio.data_relatorio }}">{{ relatorio.data_relatorio.strftime('%d/%m/%Y') }}</td>
<td> <td class="text-end">
<a href="{{ url_for('editar_relatorio_cotas', id=relatorio.id) }}" class="btn btn-primary btn-sm">Editar</a> <a href="{{ url_for('editar_relatorio_cotas', id=relatorio.id) }}"
<a href="{{ url_for('deletar_relatorio_cotas', id=relatorio.id) }}" class="btn btn-danger btn-sm" onclick="return confirm('Tem certeza que deseja excluir este relatório?')">Excluir</a> class="btn btn-primary btn-sm"
title="Editar">
<i class="fas fa-edit"></i>
</a>
<a href="{{ url_for('deletar_relatorio_cotas', id=relatorio.id) }}"
class="btn btn-danger btn-sm"
onclick="return confirm('Tem certeza que deseja excluir este relatório?')"
title="Excluir">
<i class="fas fa-trash"></i>
</a>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
@@ -53,5 +53,7 @@
</div> </div>
</div> </div>
</div> </div>
<script src="{{ url_for('static', filename='js/table_sort.js') }}"></script>
{% endblock %} {% endblock %}

View File

@@ -3,47 +3,47 @@
{% block title %}Listar Relatórios de Vendas{% endblock %} {% block title %}Listar Relatórios de Vendas{% endblock %}
{% block content %} {% block content %}
<div class="container"> <div class="container mt-4">
<div class="row"> <div class="card">
<div class="col-md-12"> <div class="card-header d-flex justify-content-between align-items-center">
<h1 class="mb-4">Lista de Relatórios de Vendas</h1> <h5 class="mb-0"><i class="fas fa-file-invoice me-2"></i>Relatórios de Vendas</h5>
<a href="{{ url_for('novo_relatorio_vendas') }}" class="btn btn-success">
{% with messages = get_flashed_messages(with_categories=true) %} <i class="fas fa-plus me-2"></i>Novo Relatório
{% if messages %} </a>
{% for category, message in messages %} </div>
<div class="alert alert-{{ category }}">{{ message }}</div> <div class="card-body">
{% endfor %}
{% endif %}
{% endwith %}
<div class="d-flex justify-content-between mb-4">
<a href="{{ url_for('novo_relatorio_vendas') }}" class="btn btn-success">Novo Relatório</a>
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
</div>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped table-hover"> <table class="table table-hover" id="relatoriosTable">
<thead> <thead>
<tr> <tr>
<th>ID</th> <th data-sort="id">ID <i class="fas fa-sort"></i></th>
<th>Setor</th> <th data-sort="setor">Setor <i class="fas fa-sort"></i></th>
<th>Comitê Central</th> <th data-sort="comite">Comitê Central <i class="fas fa-sort"></i></th>
<th>Total de Vendas</th> <th data-sort="total">Total de Vendas <i class="fas fa-sort"></i></th>
<th>Data do Relatório</th> <th data-sort="data">Data do Relatório <i class="fas fa-sort"></i></th>
<th>Ações</th> <th class="text-end">Ações</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for relatorio in relatorios %} {% for relatorio in relatorios %}
<tr> <tr>
<td>{{ relatorio.id }}</td> <td data-id="{{ relatorio.id }}">{{ relatorio.id }}</td>
<td>{{ relatorio.setor.nome }}</td> <td data-setor="{{ relatorio.setor.nome }}">{{ relatorio.setor.nome }}</td>
<td>{{ relatorio.comite.nome }}</td> <td data-comite="{{ relatorio.comite.nome }}">{{ relatorio.comite.nome }}</td>
<td>R$ {{ "%.2f"|format(relatorio.total_vendas) }}</td> <td data-total="{{ relatorio.total_vendas }}">R$ {{ "%.2f"|format(relatorio.total_vendas) }}</td>
<td>{{ relatorio.data_relatorio.strftime('%d/%m/%Y') }}</td> <td data-data="{{ relatorio.data_relatorio }}">{{ relatorio.data_relatorio.strftime('%d/%m/%Y') }}</td>
<td> <td class="text-end">
<a href="{{ url_for('editar_relatorio_vendas', id=relatorio.id) }}" class="btn btn-primary btn-sm">Editar</a> <a href="{{ url_for('editar_relatorio_vendas', id=relatorio.id) }}"
<a href="{{ url_for('deletar_relatorio_vendas', id=relatorio.id) }}" class="btn btn-danger btn-sm" onclick="return confirm('Tem certeza que deseja excluir este relatório?')">Excluir</a> class="btn btn-primary btn-sm"
title="Editar">
<i class="fas fa-edit"></i>
</a>
<a href="{{ url_for('deletar_relatorio_vendas', id=relatorio.id) }}"
class="btn btn-danger btn-sm"
onclick="return confirm('Tem certeza que deseja excluir este relatório?')"
title="Excluir">
<i class="fas fa-trash"></i>
</a>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
@@ -53,4 +53,6 @@
</div> </div>
</div> </div>
</div> </div>
<script src="{{ url_for('static', filename='js/table_sort.js') }}"></script>
{% endblock %} {% endblock %}

View File

@@ -20,4 +20,16 @@
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</button> </button>
</div> </div>
</td> </td>
{% include 'modals/militante_editar.html' %}
{% include 'modals/militante_excluir.html' %}
<!-- Scripts -->
<script src="{{ url_for('static', filename='js/militantes.js') }}"></script>
{% if config.DEBUG %}
<script src="{{ url_for('static', filename='js/tests/militantes.test.js') }}"></script>
<script>ativarTestesMilitantes();</script>
{% endif %}
</body>
</html>

View File

@@ -1,255 +1,275 @@
<!-- Modal de Editar Militante --> <!-- Modal de Editar Militante -->
<div class="modal fade" id="modalEditarMilitante" tabindex="-1"> <div class="modal fade" id="modalEditarMilitante" tabindex="-1" aria-labelledby="modalEditarMilitanteLabel" aria-hidden="true" data-bs-backdrop="true" data-bs-keyboard="true">
<div class="modal-dialog modal-lg modal-dialog-centered"> <div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title"> <h5 class="modal-title" id="modalEditarMilitanteLabel">
<i class="fas fa-user-edit me-2"></i>Editar Militante <i class="fas fa-user-edit me-2"></i>Editar Militante
</h5> </h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Fechar"></button>
</div> </div>
<form id="formEditarMilitante" method="POST"> <form id="formEditarMilitante" method="POST" action="/militantes/editar/" novalidate>
<input type="hidden" id="edit_militante_id" name="militante_id"> <input type="hidden" id="edit_militante_id" name="militante_id" value="">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" id="responsabilidades_values" name="responsabilidades_valor" value="0">
<div class="modal-body"> <!-- Tabs de navegação -->
<!-- Nav tabs --> <ul class="nav nav-tabs nav-fill" role="tablist">
<ul class="nav nav-tabs nav-fill mb-3" role="tablist"> <li class="nav-item" role="presentation">
<li class="nav-item" role="presentation"> <button class="nav-link active" data-bs-toggle="tab" data-bs-target="#edit-dados-basicos" type="button" role="tab">
<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>Dadossicos
<i class="fas fa-user me-2"></i>Dados Básicos </button>
</button> </li>
</li> <li class="nav-item" role="presentation">
<li class="nav-item" role="presentation"> <button class="nav-link" data-bs-toggle="tab" data-bs-target="#edit-contato" type="button" role="tab">
<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
<i class="fas fa-address-book me-2"></i>Contato </button>
</button> </li>
</li> <li class="nav-item" role="presentation">
<li class="nav-item" role="presentation"> <button class="nav-link" data-bs-toggle="tab" data-bs-target="#edit-profissional" type="button" role="tab">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#edit-profissional" type="button"> <i class="fas fa-briefcase me-2"></i>Profissional
<i class="fas fa-briefcase me-2"></i>Profissional </button>
</button> </li>
</li> <li class="nav-item" role="presentation">
<li class="nav-item" role="presentation"> <button class="nav-link" data-bs-toggle="tab" data-bs-target="#edit-organizacao" type="button" role="tab">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#edit-organizacao" type="button"> <i class="fas fa-sitemap me-2"></i>Organização
<i class="fas fa-users me-2"></i>Organização </button>
</button> </li>
</li> </ul>
</ul>
<!-- Tab content --> <!-- Conteúdo das tabs -->
<div class="tab-content"> <div class="tab-content p-3">
<!-- Dados Básicos --> <!-- Dados Básicos -->
<div class="tab-pane fade show active" id="edit-dados-basicos"> <div class="tab-pane fade show active" id="edit-dados-basicos">
<div class="row"> <div class="row">
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label for="edit_nome" class="form-label">Nome</label> <label for="edit_nome" class="form-label">Nome</label>
<input type="text" class="form-control" id="edit_nome" name="nome" required> <input type="text" class="form-control" id="edit_nome" name="nome" required>
</div> <div class="invalid-feedback">
<div class="col-md-6 mb-3"> Por favor, insira o nome do militante.
<label for="edit_cpf" class="form-label">CPF</label>
<input type="text" class="form-control" id="edit_cpf" name="cpf" required>
</div> </div>
</div> </div>
<div class="row"> <div class="col-md-6 mb-3">
<div class="col-md-6 mb-3"> <label for="edit_cpf" class="form-label">CPF</label>
<label for="edit_titulo_eleitoral" class="form-label">Título Eleitoral</label> <input type="text" class="form-control" id="edit_cpf" name="cpf" required>
<input type="text" class="form-control" id="edit_titulo_eleitoral" name="titulo_eleitoral"> <div class="invalid-feedback">
</div> Por favor, insira um CPF válido.
<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> </div>
</div> </div>
<div class="row">
<!-- Contato --> <div class="col-md-6 mb-3">
<div class="tab-pane fade" id="edit-contato"> <label for="edit_titulo_eleitoral" class="form-label">Título Eleitoral</label>
<div class="row"> <input type="text" class="form-control" id="edit_titulo_eleitoral" name="titulo_eleitoral">
<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> </div>
<div class="col-md-6 mb-3">
<!-- Email Principal --> <label for="edit_data_nascimento" class="form-label">Data de Nascimento</label>
<div class="mb-3"> <input type="text"
<label for="edit_email" class="form-label">Email Principal</label> class="form-control date-mask"
<input type="email" class="form-control" id="edit_email" name="email" required> id="edit_data_nascimento"
</div> name="data_nascimento"
placeholder="DD/MM/AAAA"
<!-- Endereço --> maxlength="10"
<div class="endereco-container"> pattern="\d{2}/\d{2}/\d{4}"
<div class="row"> title="Data no formato DD/MM/AAAA">
<div class="col-md-4 mb-3"> <div class="invalid-feedback">
<label for="edit_cep" class="form-label">CEP</label> Por favor, insira uma data válida no formato DD/MM/AAAA.
<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> </div>
</div> </div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="edit_data_entrada_oci" class="form-label">Data de Entrada na OCI</label>
<input type="text"
class="form-control date-mask"
id="edit_data_entrada_oci"
name="data_entrada_oci"
placeholder="DD/MM/AAAA"
maxlength="10"
pattern="\d{2}/\d{2}/\d{4}"
title="Data no formato DD/MM/AAAA">
<div class="invalid-feedback">
Por favor, insira uma data válida no formato DD/MM/AAAA.
</div>
</div>
<div class="col-md-6 mb-3">
<label for="edit_data_efetivacao_oci" class="form-label">Data de Efetivação na OCI</label>
<input type="text"
class="form-control date-mask"
id="edit_data_efetivacao_oci"
name="data_efetivacao_oci"
placeholder="DD/MM/AAAA"
maxlength="10"
pattern="\d{2}/\d{2}/\d{4}"
title="Data no formato DD/MM/AAAA">
<div class="invalid-feedback">
Por favor, insira uma data válida no formato DD/MM/AAAA.
</div>
</div>
</div>
</div>
<!-- Profissional --> <!-- Contato -->
<div class="tab-pane fade" id="edit-profissional"> <div class="tab-pane fade" id="edit-contato">
<div class="row"> <div class="row">
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label for="edit_profissao" class="form-label">Profissão</label> <label for="edit_telefone1" class="form-label">Telefone Principal</label>
<input type="text" class="form-control" id="edit_profissao" name="profissao"> <input type="text" class="form-control" id="edit_telefone1" name="telefone1">
</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>
<div class="row"> <div class="col-md-6 mb-3">
<div class="col-md-6 mb-3"> <label for="edit_telefone2" class="form-label">Telefone Alternativo</label>
<label for="edit_empresa" class="form-label">Empresa</label> <input type="text" class="form-control" id="edit_telefone2" name="telefone2">
<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> </div>
<hr> </div>
<!-- Dados Acadêmicos -->
<!-- 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 class="invalid-feedback">
Por favor, insira um email válido.
</div>
</div>
<!-- Endereço -->
<div class="endereco-container">
<div class="row"> <div class="row">
<div class="col-md-8 mb-3"> <div class="col-md-4 mb-3">
<label for="edit_instituicao_ensino" class="form-label">Instituição de Ensino</label> <label for="edit_cep" class="form-label">CEP</label>
<input type="text" class="form-control" id="edit_instituicao_ensino" name="instituicao_ensino"> <input type="text" class="form-control" id="edit_cep" name="cep">
</div> </div>
<div class="col-md-4 mb-3"> <div class="col-md-4 mb-3">
<label for="edit_tipo_instituicao" class="form-label">Tipo</label> <label for="edit_estado" class="form-label">Estado</label>
<select class="form-select" id="edit_tipo_instituicao" name="tipo_instituicao"> <select class="form-select" id="edit_estado" name="estado">
<option value="">Selecione...</option> <option value="">Selecione...</option>
<option value="Federal">Federal</option> <!-- Estados serão carregados via JavaScript -->
<option value="Estadual">Estadual</option>
<option value="Municipal">Municipal</option>
<option value="Privada">Privada</option>
</select> </select>
</div> </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_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>
</div> </div>
<hr>
<!-- Organização --> <!-- Estado na Organização -->
<div class="tab-pane fade" id="edit-organizacao"> <div class="row">
<!-- Dados Sindicais --> <div class="col-md-6 mb-3">
<div class="row"> <label for="edit_estado_militante" class="form-label">Estado</label>
<div class="col-md-6 mb-3"> <select class="form-select" id="edit_estado_militante" name="estado">
<label for="edit_sindicato" class="form-label">Sindicato</label> <option value="ATIVO">Ativo</option>
<input type="text" class="form-control" id="edit_sindicato" name="sindicato"> <option value="LICENCIADO">Licenciado</option>
</div> <option value="SUSPENSO">Suspenso</option>
<div class="col-md-6 mb-3"> <option value="DESLIGADO">Desligado</option>
<label for="edit_cargo_sindical" class="form-label">Cargo Sindical</label> </select>
<input type="text" class="form-control" id="edit_cargo_sindical" name="cargo_sindical">
</div>
</div> </div>
<div class="row"> <div class="col-md-6 mb-3">
<div class="col-md-6 mb-3"> <label for="edit_celula" class="form-label">Célula</label>
<label for="edit_central_sindical" class="form-label">Central Sindical</label> <select class="form-select" id="edit_celula" name="celula_id">
<input type="text" class="form-control" id="edit_central_sindical" name="central_sindical"> <option value="">Selecione...</option>
</div> {% for celula in celulas %}
<div class="col-md-6 mb-3 d-flex align-items-center"> <option value="{{ celula.id }}">{{ celula.nome }}</option>
<div class="form-check"> {% endfor %}
<input type="checkbox" class="form-check-input" id="edit_dirigente_sindical" name="dirigente_sindical"> </select>
<label class="form-check-label" for="edit_dirigente_sindical">Dirigente Sindical</label>
</div>
</div>
</div> </div>
<hr> </div>
<!-- Estado na Organização --> <!-- Responsabilidades -->
<div class="row"> <div class="row">
<div class="col-md-6 mb-3"> <div class="col-12">
<label for="edit_estado_militante" class="form-label">Estado</label> <label class="form-label">Responsabilidades</label>
<select class="form-select" id="edit_estado_militante" name="estado"> <div class="d-flex flex-wrap gap-2">
<option value="ATIVO">Ativo</option> <span class="badge badge-clickable bg-secondary" data-value="{{ Militante.SECRETARIO }}" data-original-class="bg-secondary" title="Secretário">SEC</span>
<option value="LICENCIADO">Licenciado</option> <span class="badge badge-clickable bg-warning" data-value="{{ Militante.TESOUREIRO }}" data-original-class="bg-warning" title="Tesoureiro">TES</span>
<option value="SUSPENSO">Suspenso</option> <span class="badge badge-clickable bg-danger" data-value="{{ Militante.IMPRENSA }}" data-original-class="bg-danger" title="Imprensa">IMP</span>
<option value="DESLIGADO">Desligado</option> <span class="badge badge-clickable bg-purple" data-value="{{ Militante.MNS }}" data-original-class="bg-purple" title="MNS">MNS</span>
</select> <span class="badge badge-clickable bg-teal" data-value="{{ Militante.MPS }}" data-original-class="bg-teal" title="MPS">MPS</span>
</div> <span class="badge badge-clickable bg-orange" data-value="{{ Militante.JUVENTUDE }}" data-original-class="bg-orange" title="Juventude">JUV</span>
<div class="col-md-6 mb-3"> <span class="badge badge-clickable bg-success" data-value="{{ Militante.QUADRO_ORIENTADOR }}" data-original-class="bg-success" title="Quadro-Orientador">QOR</span>
<label for="edit_celula" class="form-label">Célula</label> <span class="badge badge-clickable bg-primary" data-value="{{ Militante.RESPONSAVEL_FINANCAS }}" data-original-class="bg-primary" title="Responsável de Finanças">RFI</span>
<select class="form-select" id="edit_celula" name="celula_id"> <span class="badge badge-clickable bg-info" data-value="{{ Militante.RESPONSAVEL_IMPRENSA }}" data-original-class="bg-info" title="Responsável de Imprensa">RIM</span>
<option value="">Selecione...</option> <span class="badge badge-clickable bg-dark" data-value="{{ Militante.ASPIRANTE }}" data-original-class="bg-dark" title="Aspirante">ASP</span>
{% 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>
@@ -264,4 +284,151 @@
</form> </form>
</div> </div>
</div> </div>
</div> </div>
<style>
/* Estilo para badges clicáveis */
.badge-clickable {
cursor: pointer;
transition: all 0.2s ease-in-out;
opacity: 0.7;
font-size: 0.8rem;
padding: 0.5rem 0.75rem;
min-width: 50px;
text-align: center;
}
.badge-clickable:hover {
opacity: 1;
transform: translateY(-1px);
}
.badge-clickable.active {
opacity: 1;
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
/* Cores personalizadas para badges */
.bg-purple {
background-color: #6f42c1;
}
.bg-teal {
background-color: #20c997;
}
.bg-orange {
background-color: #fd7e14;
}
.responsabilidades-container {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
padding: 1rem;
border: 1px solid #dee2e6;
border-radius: 0.375rem;
background-color: #f8f9fa;
}
/* Cores personalizadas para badges */
.bg-purple { background-color: #6f42c1 !important; color: white !important; }
.bg-teal { background-color: #20c997 !important; color: white !important; }
.bg-orange { background-color: #fd7e14 !important; color: white !important; }
.bg-indigo { background-color: #6610f2 !important; color: white !important; }
.bg-pink { background-color: #d63384 !important; color: white !important; }
/* Cores do Bootstrap que vamos usar */
.active.bg-primary { background-color: #0d6efd !important; color: white !important; }
.active.bg-success { background-color: #198754 !important; color: white !important; }
.active.bg-info { background-color: #0dcaf0 !important; color: white !important; }
.active.bg-danger { background-color: #dc3545 !important; color: white !important; }
.active.bg-dark { background-color: #212529 !important; color: white !important; }
/* Estilos para as tabs */
.nav-tabs {
border-bottom: none;
}
.nav-tabs .nav-link {
border: none;
color: var(--bs-danger);
padding: 0.75rem 1rem;
text-align: center;
}
.nav-tabs .nav-link:hover {
border: none;
color: var(--bs-danger);
background-color: rgba(var(--bs-danger-rgb), 0.1);
}
.nav-tabs .nav-link.active {
color: var(--bs-danger);
background-color: rgba(var(--bs-danger-rgb), 0.1);
border-bottom: 2px solid var(--bs-danger);
}
/* Adicionar nav-fill para distribuir as abas igualmente */
.nav-tabs {
display: flex;
}
.nav-tabs .nav-item {
flex: 1;
text-align: center;
}
/* Estilos para o conteúdo das tabs */
.tab-content {
background-color: #fff;
border-radius: 0 0 0.25rem 0.25rem;
}
.tab-pane {
padding: 1rem;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
const modalEditarMilitante = document.getElementById('modalEditarMilitante');
if (modalEditarMilitante) {
modalEditarMilitante.addEventListener('hidden.bs.modal', function() {
// Limpar formulário
const form = this.querySelector('form');
if (form) {
form.reset();
}
// Limpar campos hidden
document.getElementById('edit_militante_id').value = '';
document.getElementById('responsabilidades_values').value = '0';
// Resetar badges
this.querySelectorAll('.badge-clickable').forEach(badge => {
badge.classList.remove('active');
const originalClass = badge.getAttribute('data-original-class');
if (originalClass) {
badge.className = `badge badge-clickable ${originalClass}`;
}
});
// Limpar mensagens de erro
this.querySelectorAll('.is-invalid').forEach(field => {
field.classList.remove('is-invalid');
});
this.querySelectorAll('.invalid-feedback').forEach(feedback => {
feedback.style.display = 'none';
});
// Voltar para a primeira aba
const firstTab = this.querySelector('button[data-bs-target="#edit-dados-basicos"]');
if (firstTab) {
firstTab.click();
}
});
}
});
</script>

View File

@@ -57,17 +57,20 @@
</div> </div>
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label for="data_nascimento" class="form-label">Data de Nascimento</label> <label for="data_nascimento" class="form-label">Data de Nascimento</label>
<input type="date" class="form-control" id="data_nascimento" name="data_nascimento"> <input type="text" class="form-control date-mask" id="data_nascimento" name="data_nascimento"
placeholder="DD/MM/AAAA" maxlength="10">
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label for="data_entrada" class="form-label">Data de Entrada OCI</label> <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"> <input type="text" class="form-control date-mask" id="data_entrada" name="data_entrada_oci"
placeholder="DD/MM/AAAA" maxlength="10">
</div> </div>
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label for="data_efetivacao" class="form-label">Data de Efetivação</label> <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"> <input type="text" class="form-control date-mask" id="data_efetivacao" name="data_efetivacao_oci"
placeholder="DD/MM/AAAA" maxlength="10">
</div> </div>
</div> </div>
</div> </div>
@@ -227,18 +230,32 @@
</select> </select>
</div> </div>
</div> </div>
<div class="mb-3"> <div class="row">
<label class="form-label d-block">Responsabilidades</label> <div class="col-12">
<div class="row g-3"> <label class="form-label">Responsabilidades</label>
{% for valor, nome in Militante.get_responsabilidades_list() %} <div class="responsabilidades-container">
<div class="col-md-6"> <input type="hidden" name="responsabilidades" id="novo_responsabilidades_values" value="0">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="resp_{{ valor }}" <span class="badge badge-clickable bg-secondary" data-value="{{ Militante.SECRETARIO }}" data-bs-toggle="tooltip" title="Clique para alternar">Secretário</span>
name="responsabilidades" value="{{ valor }}">
<label class="form-check-label" for="resp_{{ valor }}">{{ nome }}</label> <span class="badge badge-clickable bg-info" data-value="{{ Militante.RESPONSAVEL_IMPRENSA }}" data-bs-toggle="tooltip" title="Clique para alternar">Responsável de Imprensa</span>
</div>
<span class="badge badge-clickable bg-warning text-dark" data-value="{{ Militante.IMPRENSA }}" data-bs-toggle="tooltip" title="Clique para alternar">Imprensa</span>
<span class="badge badge-clickable bg-warning text-dark" data-value="{{ Militante.MPS }}" data-bs-toggle="tooltip" title="Clique para alternar">MPS</span>
<span class="badge badge-clickable bg-success" data-value="{{ Militante.QUADRO_ORIENTADOR }}" data-bs-toggle="tooltip" title="Clique para alternar">Quadro-Orientador</span>
<span class="badge badge-clickable bg-primary" data-value="{{ Militante.RESPONSAVEL_FINANCAS }}" data-bs-toggle="tooltip" title="Clique para alternar">Responsável de Finanças</span>
<span class="badge badge-clickable bg-dark" data-value="{{ Militante.TESOUREIRO }}" data-bs-toggle="tooltip" title="Clique para alternar">Tesoureiro</span>
<span class="badge badge-clickable bg-info" data-value="{{ Militante.MNS }}" data-bs-toggle="tooltip" title="Clique para alternar">MNS</span>
<span class="badge badge-clickable bg-danger" data-value="{{ Militante.JUVENTUDE }}" data-bs-toggle="tooltip" title="Clique para alternar">Juventude</span>
<span class="badge badge-clickable bg-light text-dark border" data-value="{{ Militante.ASPIRANTE }}" data-bs-toggle="tooltip" title="Clique para alternar">Aspirante</span>
</div> </div>
{% endfor %}
</div> </div>
</div> </div>
</div> </div>
@@ -253,4 +270,34 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<style>
.badge-clickable {
font-size: 0.9rem;
padding: 0.5rem 1rem;
margin: 0.3rem;
cursor: pointer;
transition: all 0.2s ease;
opacity: 0.5;
user-select: none;
}
.badge-clickable:hover {
opacity: 0.8;
}
.badge-clickable.active {
opacity: 1;
}
.responsabilidades-container {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
padding: 1rem;
border: 1px solid #dee2e6;
border-radius: 0.375rem;
background-color: #f8f9fa;
}
</style>

View File

@@ -3,11 +3,12 @@
{% block title %}Novo Relatório de Cotas{% endblock %} {% block title %}Novo Relatório de Cotas{% endblock %}
{% block content %} {% block content %}
<div class="container"> <div class="container mt-4">
<div class="row"> <div class="card">
<div class="col-md-12"> <div class="card-header">
<h1 class="mb-4">Novo Relatório de Cotas</h1> <h5 class="mb-0"><i class="fas fa-file-invoice-dollar me-2"></i>Novo Relatório de Cotas</h5>
</div>
<div class="card-body">
{% with messages = get_flashed_messages(with_categories=true) %} {% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %} {% if messages %}
{% for category, message in messages %} {% for category, message in messages %}
@@ -20,7 +21,7 @@
<div class="mb-3"> <div class="mb-3">
<label for="setor_id" class="form-label">Setor</label> <label for="setor_id" class="form-label">Setor</label>
<select class="form-select" id="setor_id" name="setor_id" required> <select class="form-select" id="setor_id" name="setor_id" required>
<option value="">Selecione um setor</option> <option value="">Selecione o setor</option>
{% for setor in setores %} {% for setor in setores %}
<option value="{{ setor.id }}">{{ setor.nome }}</option> <option value="{{ setor.id }}">{{ setor.nome }}</option>
{% endfor %} {% endfor %}
@@ -33,35 +34,53 @@
<div class="mb-3"> <div class="mb-3">
<label for="comite_id" class="form-label">Comitê Central</label> <label for="comite_id" class="form-label">Comitê Central</label>
<select class="form-select" id="comite_id" name="comite_id" required> <select class="form-select" id="comite_id" name="comite_id" required>
<option value="">Selecione um comitê</option> <option value="">Selecione o comitê</option>
{% for comite in comites %} {% for comite in comites %}
<option value="{{ comite.id }}">{{ comite.nome }}</option> <option value="{{ comite.id }}">{{ comite.nome }}</option>
{% endfor %} {% endfor %}
</select> </select>
<div class="invalid-feedback"> <div class="invalid-feedback">
Por favor, selecione o comitê central. Por favor, selecione o comitê.
</div> </div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="total_cotas" class="form-label">Total de Cotas</label> <label for="total_cotas" class="form-label">Total de Cotas</label>
<input type="number" class="form-control" id="total_cotas" name="total_cotas" step="0.01" required> <div class="input-group">
<span class="input-group-text">R$</span>
<input type="number"
class="form-control"
id="total_cotas"
name="total_cotas"
step="0.01"
min="0.01"
required>
</div>
<div class="invalid-feedback"> <div class="invalid-feedback">
Por favor, insira o total de cotas. Por favor, insira um valor válido para o total de cotas.
</div> </div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="data_relatorio" class="form-label">Data do Relatório</label> <label for="data_relatorio" class="form-label">Data do Relatório</label>
<input type="date" class="form-control" id="data_relatorio" name="data_relatorio" required> <input type="date"
class="form-control"
id="data_relatorio"
name="data_relatorio"
max="{{ hoje }}"
required>
<div class="invalid-feedback"> <div class="invalid-feedback">
Por favor, insira a data do relatório. Por favor, insira uma data válida não futura.
</div> </div>
</div> </div>
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<button type="submit" class="btn btn-success">Registrar</button> <button type="submit" class="btn btn-success">
<a href="{{ url_for('listar_relatorios_cotas') }}" class="btn btn-outline-secondary">Voltar</a> <i class="fas fa-save me-2"></i>Registrar
</button>
<a href="{{ url_for('listar_relatorios_cotas') }}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-2"></i>Voltar
</a>
</div> </div>
</form> </form>
</div> </div>
@@ -73,20 +92,51 @@
(function () { (function () {
'use strict' 'use strict'
var forms = document.querySelectorAll('.needs-validation') const forms = document.querySelectorAll('.needs-validation');
Array.prototype.slice.call(forms) forms.forEach(form => {
.forEach(function (form) { form.addEventListener('submit', event => {
form.addEventListener('submit', function (event) { if (!form.checkValidity()) {
if (!form.checkValidity()) { event.preventDefault();
event.preventDefault() event.stopPropagation();
event.stopPropagation() }
}
// Validar valor mínimo
form.classList.add('was-validated') const totalCotas = form.querySelector('#total_cotas');
}, false) if (totalCotas.value <= 0) {
}) totalCotas.setCustomValidity('O valor deve ser maior que zero');
})() event.preventDefault();
event.stopPropagation();
} else {
totalCotas.setCustomValidity('');
}
// Validar data não futura
const dataRelatorio = form.querySelector('#data_relatorio');
const hoje = new Date();
hoje.setHours(0, 0, 0, 0);
const dataSelecionada = new Date(dataRelatorio.value);
if (dataSelecionada > hoje) {
dataRelatorio.setCustomValidity('A data não pode ser futura');
event.preventDefault();
event.stopPropagation();
} else {
dataRelatorio.setCustomValidity('');
}
form.classList.add('was-validated');
}, false);
// Limpar validação ao mudar valor
const inputs = form.querySelectorAll('input, select');
inputs.forEach(input => {
input.addEventListener('input', () => {
input.setCustomValidity('');
});
});
});
})();
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -3,11 +3,12 @@
{% block title %}Novo Relatório de Vendas{% endblock %} {% block title %}Novo Relatório de Vendas{% endblock %}
{% block content %} {% block content %}
<div class="container"> <div class="container mt-4">
<div class="row"> <div class="card">
<div class="col-md-12"> <div class="card-header">
<h1 class="mb-4">Novo Relatório de Vendas</h1> <h5 class="mb-0"><i class="fas fa-file-invoice me-2"></i>Novo Relatório de Vendas</h5>
</div>
<div class="card-body">
{% with messages = get_flashed_messages(with_categories=true) %} {% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %} {% if messages %}
{% for category, message in messages %} {% for category, message in messages %}
@@ -20,7 +21,7 @@
<div class="mb-3"> <div class="mb-3">
<label for="setor_id" class="form-label">Setor</label> <label for="setor_id" class="form-label">Setor</label>
<select class="form-select" id="setor_id" name="setor_id" required> <select class="form-select" id="setor_id" name="setor_id" required>
<option value="">Selecione um setor</option> <option value="">Selecione o setor</option>
{% for setor in setores %} {% for setor in setores %}
<option value="{{ setor.id }}">{{ setor.nome }}</option> <option value="{{ setor.id }}">{{ setor.nome }}</option>
{% endfor %} {% endfor %}
@@ -33,35 +34,53 @@
<div class="mb-3"> <div class="mb-3">
<label for="comite_id" class="form-label">Comitê Central</label> <label for="comite_id" class="form-label">Comitê Central</label>
<select class="form-select" id="comite_id" name="comite_id" required> <select class="form-select" id="comite_id" name="comite_id" required>
<option value="">Selecione um comitê</option> <option value="">Selecione o comitê</option>
{% for comite in comites %} {% for comite in comites %}
<option value="{{ comite.id }}">{{ comite.nome }}</option> <option value="{{ comite.id }}">{{ comite.nome }}</option>
{% endfor %} {% endfor %}
</select> </select>
<div class="invalid-feedback"> <div class="invalid-feedback">
Por favor, selecione o comitê central. Por favor, selecione o comitê.
</div> </div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="total_vendas" class="form-label">Total de Vendas</label> <label for="total_vendas" class="form-label">Total de Vendas</label>
<input type="number" class="form-control" id="total_vendas" name="total_vendas" step="0.01" required> <div class="input-group">
<span class="input-group-text">R$</span>
<input type="number"
class="form-control"
id="total_vendas"
name="total_vendas"
step="0.01"
min="0.01"
required>
</div>
<div class="invalid-feedback"> <div class="invalid-feedback">
Por favor, insira o total de vendas. Por favor, insira um valor válido para o total de vendas.
</div> </div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="data_relatorio" class="form-label">Data do Relatório</label> <label for="data_relatorio" class="form-label">Data do Relatório</label>
<input type="date" class="form-control" id="data_relatorio" name="data_relatorio" required> <input type="date"
class="form-control"
id="data_relatorio"
name="data_relatorio"
max="{{ hoje }}"
required>
<div class="invalid-feedback"> <div class="invalid-feedback">
Por favor, insira a data do relatório. Por favor, insira uma data válida não futura.
</div> </div>
</div> </div>
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<button type="submit" class="btn btn-success">Registrar</button> <button type="submit" class="btn btn-success">
<a href="{{ url_for('listar_relatorios_vendas') }}" class="btn btn-outline-secondary">Voltar</a> <i class="fas fa-save me-2"></i>Registrar
</button>
<a href="{{ url_for('listar_relatorios_vendas') }}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-2"></i>Voltar
</a>
</div> </div>
</form> </form>
</div> </div>
@@ -73,19 +92,50 @@
(function () { (function () {
'use strict' 'use strict'
var forms = document.querySelectorAll('.needs-validation') const forms = document.querySelectorAll('.needs-validation');
Array.prototype.slice.call(forms) forms.forEach(form => {
.forEach(function (form) { form.addEventListener('submit', event => {
form.addEventListener('submit', function (event) { if (!form.checkValidity()) {
if (!form.checkValidity()) { event.preventDefault();
event.preventDefault() event.stopPropagation();
event.stopPropagation() }
}
// Validar valor mínimo
form.classList.add('was-validated') const totalVendas = form.querySelector('#total_vendas');
}, false) if (totalVendas.value <= 0) {
}) totalVendas.setCustomValidity('O valor deve ser maior que zero');
})() event.preventDefault();
event.stopPropagation();
} else {
totalVendas.setCustomValidity('');
}
// Validar data não futura
const dataRelatorio = form.querySelector('#data_relatorio');
const hoje = new Date();
hoje.setHours(0, 0, 0, 0);
const dataSelecionada = new Date(dataRelatorio.value);
if (dataSelecionada > hoje) {
dataRelatorio.setCustomValidity('A data não pode ser futura');
event.preventDefault();
event.stopPropagation();
} else {
dataRelatorio.setCustomValidity('');
}
form.classList.add('was-validated');
}, false);
// Limpar validação ao mudar valor
const inputs = form.querySelectorAll('input, select');
inputs.forEach(input => {
input.addEventListener('input', () => {
input.setCustomValidity('');
});
});
});
})();
</script> </script>
{% endblock %} {% endblock %}

33
tests/conftest.py Normal file
View File

@@ -0,0 +1,33 @@
import pytest
from app import create_app
from functions.database import init_database, get_db_connection
@pytest.fixture
def app():
"""Cria uma instância do app para testes"""
app = create_app()
app.config['TESTING'] = True
app.config['WTF_CSRF_ENABLED'] = False
# Inicializar banco de dados de teste
init_database()
yield app
# Limpar banco após os testes
db = get_db_connection()
try:
db.execute('DROP TABLE IF EXISTS usuarios CASCADE')
db.commit()
finally:
db.close()
@pytest.fixture
def client(app):
"""Cria um cliente de teste"""
return app.test_client()
@pytest.fixture
def runner(app):
"""Cria um runner de CLI para testes"""
return app.test_cli_runner()

View File

@@ -0,0 +1,4 @@
pytest==7.4.3
pytest-cov==4.1.0
pytest-flask==1.3.0
coverage==7.3.2

100
tests/test_admin_routes.py Normal file
View File

@@ -0,0 +1,100 @@
import pytest
from flask import url_for
from functions.database import Usuario, get_db_connection
from werkzeug.security import generate_password_hash
import json
@pytest.fixture
def admin_user(client):
"""Fixture que cria um usuário admin para testes"""
db = get_db_connection()
try:
admin = Usuario(
username='admin_test',
email='admin@test.com',
password_hash=generate_password_hash('admin123'),
is_admin=True,
is_active=True
)
db.add(admin)
db.commit()
return admin
finally:
db.close()
@pytest.fixture
def auth_admin_client(client, admin_user):
"""Fixture que retorna um cliente autenticado como admin"""
client.post('/login', data={
'email': 'admin@test.com',
'password': 'admin123'
})
return client
def test_dashboard_access_sem_login(client):
"""Testa acesso ao dashboard sem login"""
response = client.get('/admin/')
assert response.status_code == 302
assert '/login' in response.headers['Location']
def test_dashboard_access_com_login(auth_admin_client):
"""Testa acesso ao dashboard com login de admin"""
response = auth_admin_client.get('/admin/')
assert response.status_code == 200
assert b'Dashboard Administrativo' in response.data
def test_lista_usuarios(auth_admin_client):
"""Testa listagem de usuários"""
response = auth_admin_client.get('/admin/users')
assert response.status_code == 200
assert b'Lista de' in response.data
assert b'admin_test' in response.data
def test_reset_otp(auth_admin_client, admin_user):
"""Testa reset de OTP"""
response = auth_admin_client.post(f'/admin/users/{admin_user.id}/reset-otp')
assert response.status_code == 302
assert 'success' in response.headers['Location']
def test_reset_password(auth_admin_client, admin_user):
"""Testa reset de senha"""
response = auth_admin_client.post(f'/admin/users/{admin_user.id}/reset-password')
assert response.status_code == 302
assert 'success' in response.headers['Location']
def test_toggle_status(auth_admin_client, admin_user):
"""Testa alteração de status do usuário"""
response = auth_admin_client.post(
f'/admin/users/{admin_user.id}/toggle-status',
headers={'Content-Type': 'application/json'}
)
data = json.loads(response.data)
assert response.status_code == 200
assert data['success'] is True
def test_acesso_nao_admin(client):
"""Testa acesso de usuário não admin"""
db = get_db_connection()
try:
# Criar usuário normal
user = Usuario(
username='normal_user',
email='user@test.com',
password_hash=generate_password_hash('user123'),
is_admin=False,
is_active=True
)
db.add(user)
db.commit()
# Login
client.post('/login', data={
'email': 'user@test.com',
'password': 'user123'
})
# Tentar acessar área admin
response = client.get('/admin/')
assert response.status_code == 403
finally:
db.close()

171
utils/date_utils.py Normal file
View File

@@ -0,0 +1,171 @@
from datetime import datetime, date
import logging
logger = logging.getLogger(__name__)
def validar_data(data_str: str, formato: str = '%Y-%m-%d') -> bool:
"""
Valida se uma string representa uma data válida no formato especificado.
Args:
data_str: String contendo a data
formato: Formato esperado da data (default: YYYY-MM-DD)
Returns:
bool: True se a data é válida, False caso contrário
"""
if not data_str:
return True
try:
datetime.strptime(data_str, formato)
return True
except ValueError as e:
logger.warning(f"Data inválida: {data_str} (formato esperado: {formato}). Erro: {e}")
return False
def converter_data(data_str: str, formato_entrada: str = '%Y-%m-%d', formato_saida: str = None) -> date:
"""
Converte uma string de data para um objeto date.
Args:
data_str: String contendo a data
formato_entrada: Formato da data de entrada (default: YYYY-MM-DD)
formato_saida: Se especificado, retorna a data como string neste formato
Returns:
date: Objeto date se formato_saida=None, string formatada caso contrário
Raises:
ValueError: Se a data for inválida
"""
if not data_str:
return None
try:
data = datetime.strptime(data_str, formato_entrada).date()
if formato_saida:
return data.strftime(formato_saida)
return data
except ValueError as e:
logger.error(f"Erro ao converter data '{data_str}': {e}")
raise ValueError(f"Data inválida: {data_str}. Use o formato {formato_entrada}")
def validar_sequencia_datas(data_nascimento: date = None,
data_entrada: date = None,
data_efetivacao: date = None) -> None:
"""
Valida a sequência lógica entre datas.
Args:
data_nascimento: Data de nascimento
data_entrada: Data de entrada na OCI
data_efetivacao: Data de efetivação na OCI
Raises:
ValueError: Se houver inconsistência entre as datas
"""
hoje = date.today()
# Validar datas futuras
for nome, data in [
("Data de nascimento", data_nascimento),
("Data de entrada", data_entrada),
("Data de efetivação", data_efetivacao)
]:
if data and data > hoje:
logger.warning(f"{nome} no futuro: {data}")
raise ValueError(f"{nome} não pode ser no futuro")
# Validar sequência
if data_nascimento and data_entrada and data_nascimento > data_entrada:
logger.warning(f"Data de entrada ({data_entrada}) anterior à data de nascimento ({data_nascimento})")
raise ValueError("Data de entrada na OCI não pode ser anterior à data de nascimento")
if data_entrada and data_efetivacao and data_entrada > data_efetivacao:
logger.warning(f"Data de efetivação ({data_efetivacao}) anterior à data de entrada ({data_entrada})")
raise ValueError("Data de efetivação não pode ser anterior à data de entrada")
def calcular_idade(data_nascimento: date) -> int:
"""
Calcula a idade com base na data de nascimento.
Args:
data_nascimento: Data de nascimento
Returns:
int: Idade em anos
"""
if not data_nascimento:
return None
hoje = date.today()
idade = hoje.year - data_nascimento.year
# Ajustar se ainda não fez aniversário este ano
if hoje.month < data_nascimento.month or \
(hoje.month == data_nascimento.month and hoje.day < data_nascimento.day):
idade -= 1
return idade
def converter_data_br(data_str):
"""Converte string de data no formato DD/MM/YYYY para objeto date"""
if not data_str:
return None
try:
dia, mes, ano = map(int, data_str.split('/'))
return date(ano, mes, dia)
except (ValueError, TypeError) as e:
return None
def converter_data_iso(data_str):
"""Converte string de data no formato YYYY-MM-DD para objeto date"""
if not data_str:
return None
try:
return datetime.strptime(data_str, '%Y-%m-%d').date()
except (ValueError, TypeError) as e:
return None
def formatar_data_br(data):
"""Formata objeto date para string no formato DD/MM/YYYY"""
if not data:
return ''
if isinstance(data, str):
data = converter_data_iso(data) or converter_data_br(data)
if not data:
return ''
return data.strftime('%d/%m/%Y')
def formatar_data_iso(data):
"""Formata objeto date para string no formato YYYY-MM-DD"""
if not data:
return ''
if isinstance(data, str):
data = converter_data_br(data) or converter_data_iso(data)
if not data:
return ''
return data.strftime('%Y-%m-%d')
def validar_data(data, data_maxima=None, data_minima=None):
"""Valida se a data está dentro do intervalo permitido"""
if not data:
return True
if isinstance(data, str):
data = converter_data_br(data) or converter_data_iso(data)
if not data:
return False
hoje = date.today()
if data_maxima and data > data_maxima:
return False
if data_minima and data < data_minima:
return False
if data > hoje:
return False
return True