feat: Melhorias de segurança e interface - Segurança: Implementação de CSRF token em formulários, validação no backend, proteção AJAX - QR Code: Preservação do otp_secret, evita geração desnecessária - Interface: Correções visuais, padronização de cores, melhorias em formulários

This commit is contained in:
andersonid
2025-04-04 11:37:48 -03:00
parent 9ffc562357
commit 56b8e7aa54
10 changed files with 1358 additions and 300 deletions

511
app.py
View File

@@ -21,6 +21,7 @@ from functions.database import (
EmailMilitante,
init_database,
EstadoMilitante,
Endereco,
)
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, joinedload
@@ -48,6 +49,7 @@ from flask_login import LoginManager, UserMixin, login_user, logout_user, login_
import random
import string
from sqlalchemy.sql import func
from flask_wtf.csrf import CSRFProtect
load_dotenv()
@@ -55,6 +57,9 @@ app = Flask(__name__)
app.secret_key = os.getenv('SECRET_KEY', secrets.token_hex(16))
bootstrap = Bootstrap5(app)
# Configurar CSRF Protection
csrf = CSRFProtect(app)
# Configurar Flask-Login
login_manager = LoginManager()
login_manager.init_app(app)
@@ -310,79 +315,108 @@ def home():
db.close()
# Rota para criar um novo militante
@app.route('/militantes/criar', methods=['GET', 'POST'])
@app.route("/militantes/criar", methods=["POST"])
@require_login
@require_permission('gerenciar_militantes')
def criar_militante():
if request.method == 'POST':
try:
nome = request.form['nome']
email = request.form['email']
cpf = request.form['cpf']
titulo_eleitoral = request.form['titulo_eleitoral']
data_nascimento = datetime.strptime(request.form['data_nascimento'], '%Y-%m-%d').date() if request.form['data_nascimento'] else None
data_entrada_oci = datetime.strptime(request.form['data_entrada_oci'], '%Y-%m-%d').date() if request.form['data_entrada_oci'] else None
data_efetivacao_oci = datetime.strptime(request.form['data_efetivacao_oci'], '%Y-%m-%d').date() if request.form['data_efetivacao_oci'] else None
telefone1 = request.form['telefone1']
telefone2 = request.form['telefone2']
profissao = request.form['profissao']
regime_trabalho = request.form['regime_trabalho']
empresa = request.form['empresa']
contratante = request.form['contratante']
instituicao_ensino = request.form['instituicao_ensino']
tipo_instituicao = request.form['tipo_instituicao']
sindicato = request.form['sindicato']
cargo_sindical = request.form['cargo_sindical']
dirigente_sindical = 'dirigente_sindical' in request.form
central_sindical = request.form['central_sindical']
setor_id = request.form['setor_id']
celula_id = request.form['celula_id']
"""Cria um novo militante"""
db = get_db_connection()
try:
# Validar CPF
cpf = request.form.get('cpf')
if not validar_cpf(cpf):
return jsonify({
'status': 'error',
'message': 'CPF inválido'
}), 400
# Verificar se já existe militante com este CPF
militante_existente = db.query(Militante).filter(Militante.cpf == cpf).first()
if militante_existente:
return jsonify({
'status': 'error',
'message': 'CPF já cadastrado'
}), 400
# Criar endereço
endereco = Endereco(
cep=request.form.get('cep'),
estado=request.form.get('estado'),
cidade=request.form.get('cidade'),
bairro=request.form.get('bairro'),
logradouro=request.form.get('logradouro'),
numero=request.form.get('numero'),
complemento=request.form.get('complemento')
)
db.add(endereco)
db.flush() # Gerar ID do endereço
# Criar militante
militante = Militante(
# Dados Básicos
nome=request.form.get('nome'),
cpf=cpf,
titulo_eleitoral=request.form.get('titulo_eleitoral'),
data_nascimento=datetime.strptime(request.form.get('data_nascimento'), '%Y-%m-%d') if request.form.get('data_nascimento') else None,
data_entrada_oci=datetime.strptime(request.form.get('data_entrada_oci'), '%Y-%m-%d') if request.form.get('data_entrada_oci') else None,
data_efetivacao_oci=datetime.strptime(request.form.get('data_efetivacao_oci'), '%Y-%m-%d') if request.form.get('data_efetivacao_oci') else None,
# Processar responsabilidades
responsabilidades = 0
if 'responsabilidades' in request.form:
for responsabilidade in request.form.getlist('responsabilidades'):
responsabilidades |= int(responsabilidade)
# Contato
telefone1=request.form.get('telefone1'),
telefone2=request.form.get('telefone2'),
endereco_id=endereco.id,
militante = Militante(
nome=nome,
email=email,
cpf=cpf,
titulo_eleitoral=titulo_eleitoral,
data_nascimento=data_nascimento,
data_entrada_oci=data_entrada_oci,
data_efetivacao_oci=data_efetivacao_oci,
telefone1=telefone1,
telefone2=telefone2,
profissao=profissao,
regime_trabalho=regime_trabalho,
empresa=empresa,
contratante=contratante,
instituicao_ensino=instituicao_ensino,
tipo_instituicao=tipo_instituicao,
sindicato=sindicato,
cargo_sindical=cargo_sindical,
dirigente_sindical=dirigente_sindical,
central_sindical=central_sindical,
setor_id=setor_id,
celula_id=celula_id,
responsabilidades=responsabilidades
)
# Profissional
profissao=request.form.get('profissao'),
regime_trabalho=request.form.get('regime_trabalho'),
empresa=request.form.get('empresa'),
contratante=request.form.get('contratante'),
db_session.add(militante)
db_session.commit()
# Acadêmico
instituicao_ensino=request.form.get('instituicao_ensino'),
tipo_instituicao=request.form.get('tipo_instituicao'),
flash('Militante criado com sucesso!', 'success')
return redirect(url_for('listar_militantes'))
# Sindical
sindicato=request.form.get('sindicato'),
cargo_sindical=request.form.get('cargo_sindical'),
central_sindical=request.form.get('central_sindical'),
dirigente_sindical=request.form.get('dirigente_sindical') == 'on',
except Exception as e:
db_session.rollback()
flash(f'Erro ao criar militante: {str(e)}', 'danger')
return redirect(url_for('criar_militante'))
setores = Setor.query.all()
celulas = Celula.query.all()
return render_template('criar_militante.html', setores=setores, celulas=celulas)
# Organização
estado=EstadoMilitante(request.form.get('estado', 'ATIVO')),
celula_id=request.form.get('celula_id', type=int),
responsabilidades=request.form.get('responsabilidades', type=int, default=0),
# Por padrão, todo novo militante é aspirante
aspirante=True,
data_inicio_aspirante=datetime.now()
)
db.add(militante)
db.flush() # Gerar ID do militante
# Criar email principal
email = EmailMilitante(
email=request.form.get('email'),
principal=True,
militante_id=militante.id
)
db.add(email)
db.commit()
return jsonify({
'status': 'success',
'message': 'Militante criado com sucesso!'
})
except Exception as e:
db.rollback()
return jsonify({
'status': 'error',
'message': f'Erro ao criar militante: {str(e)}'
}), 500
finally:
db.close()
# Rota para listar militantes
@app.route("/militantes")
@@ -904,90 +938,152 @@ def listar_relatorios_vendas():
db.close()
# Rota para editar militante
@app.route("/militantes/editar/<int:id>", methods=["GET", "POST"])
@login_required
@session_timeout
def editar_militante(id):
db = get_db_connection()
try:
militante = db.query(Militante).get(id)
if not militante:
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({'status': 'error', 'message': 'Militante não encontrado'}), 404
flash('Militante não encontrado', 'danger')
return redirect(url_for('listar_militantes'))
if request.method == "POST":
nome = request.form.get("nome")
cpf = request.form.get("cpf")
email = request.form.get("email")
telefone = request.form.get("telefone")
endereco = request.form.get("endereco")
filiado = request.form.get("filiado") == "on"
# Validar CPF
if not validar_cpf(cpf):
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({'status': 'error', 'message': 'CPF inválido'}), 400
flash('CPF inválido', 'danger')
return render_template('editar_militante.html', militante=militante)
# Verificar se já existe outro militante com este CPF
militante_existente = db.query(Militante).filter(
Militante.cpf == cpf,
Militante.id != id
).first()
if militante_existente:
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({'status': 'error', 'message': 'CPF já cadastrado para outro militante'}), 400
flash('CPF já cadastrado para outro militante', 'danger')
return render_template('editar_militante.html', militante=militante)
@app.route("/militantes/editar/<int:militante_id>", methods=["POST"])
@require_login
@require_permission('gerenciar_militantes')
def editar_militante(militante_id):
"""Edita um militante existente"""
print(f"Iniciando edição do militante {militante_id}")
print(f"Dados recebidos: {request.form}")
if request.method == "POST":
db = get_db_connection()
try:
militante = db.query(Militante).options(
joinedload(Militante.endereco),
joinedload(Militante.emails)
).get(militante_id)
if not militante:
print(f"Militante {militante_id} não encontrado")
return jsonify({
'status': 'error',
'message': 'Militante não encontrado'
}), 404
print(f"Militante encontrado: {militante.nome}")
# Dados Básicos
militante.nome = request.form.get('nome')
militante.cpf = request.form.get('cpf')
militante.titulo_eleitoral = request.form.get('titulo_eleitoral')
militante.data_nascimento = datetime.strptime(request.form.get('data_nascimento'), '%Y-%m-%d') if request.form.get('data_nascimento') else None
militante.data_entrada_oci = datetime.strptime(request.form.get('data_entrada_oci'), '%Y-%m-%d') if request.form.get('data_entrada_oci') else None
militante.data_efetivacao_oci = datetime.strptime(request.form.get('data_efetivacao_oci'), '%Y-%m-%d') if request.form.get('data_efetivacao_oci') else None
print("Dados básicos atualizados")
# Contato
militante.telefone1 = request.form.get('telefone1')
militante.telefone2 = request.form.get('telefone2')
# Email (atualizar o principal)
email = request.form.get('email')
if email:
if militante.emails:
militante.emails[0].endereco_email = email
else:
novo_email = EmailMilitante(
endereco_email=email,
militante_id=militante.id,
principal=True
)
db.add(novo_email)
print("Dados de contato atualizados")
# Endereço
if not militante.endereco:
militante.endereco = Endereco()
db.add(militante.endereco)
militante.endereco.cep = request.form.get('cep')
militante.endereco.estado = request.form.get('estado')
militante.endereco.cidade = request.form.get('cidade')
militante.endereco.bairro = request.form.get('bairro')
militante.endereco.rua = request.form.get('rua')
militante.endereco.numero = request.form.get('numero')
militante.endereco.complemento = request.form.get('complemento')
print("Dados de endereço atualizados")
# Profissional
militante.profissao = request.form.get('profissao')
militante.regime_trabalho = request.form.get('regime_trabalho')
militante.empresa = request.form.get('empresa')
militante.contratante = request.form.get('contratante')
# Acadêmico
militante.instituicao_ensino = request.form.get('instituicao_ensino')
militante.tipo_instituicao = request.form.get('tipo_instituicao')
# Sindical
militante.sindicato = request.form.get('sindicato')
militante.cargo_sindical = request.form.get('cargo_sindical')
militante.central_sindical = request.form.get('central_sindical')
militante.dirigente_sindical = request.form.get('dirigente_sindical') == 'on'
print("Dados profissionais e sindicais atualizados")
# Organização
estado_str = request.form.get('estado', 'ATIVO').lower()
print(f"Estado recebido: {estado_str}")
# Converter o estado para o enum
try:
militante.nome = nome
militante.cpf = cpf
militante.email = email
militante.telefone = telefone
militante.endereco = endereco
militante.filiado = filiado
db.commit()
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({
'status': 'success',
'message': 'Militante atualizado com sucesso',
'militante': {
'id': militante.id,
'nome': militante.nome,
'cpf': militante.cpf,
'email': militante.email,
'telefone': militante.telefone,
'endereco': militante.endereco,
'filiado': militante.filiado
}
})
flash('Militante atualizado com sucesso!', 'success')
return redirect(url_for('listar_militantes'))
except Exception as e:
db.rollback()
print(f"Erro ao atualizar militante: {e}")
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({'status': 'error', 'message': 'Erro ao atualizar militante'}), 500
flash('Erro ao atualizar militante', 'danger')
return render_template('editar_militante.html', militante=militante)
return render_template('editar_militante.html', militante=militante)
except Exception as e:
print(f"Erro ao editar militante: {e}")
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({'status': 'error', 'message': 'Erro ao carregar militante'}), 500
flash('Erro ao carregar militante', 'danger')
return redirect(url_for('listar_militantes'))
finally:
db.close()
militante.estado = EstadoMilitante[estado_str.upper()]
print(f"Estado convertido: {militante.estado}")
except (KeyError, ValueError) as e:
print(f"Erro ao converter estado: {str(e)}")
return jsonify({
'status': 'error',
'message': f'Estado inválido: {estado_str}'
}), 400
# Tratar celula_id corretamente
celula_id = request.form.get('celula_id')
if celula_id:
try:
militante.celula_id = int(celula_id)
except (ValueError, TypeError):
militante.celula_id = None
else:
militante.celula_id = None
# Tratar responsabilidades corretamente
responsabilidades = request.form.getlist('responsabilidades')
if responsabilidades:
try:
militante.responsabilidades = sum(int(r) for r in responsabilidades)
except (ValueError, TypeError):
militante.responsabilidades = 0
else:
militante.responsabilidades = 0
print("Dados organizacionais atualizados")
# Se o estado mudou para DESLIGADO, registrar data e motivo
if militante.estado == EstadoMilitante.DESLIGADO:
militante.data_desligamento = datetime.now()
militante.motivo_desligamento = request.form.get('motivo_desligamento')
db.commit()
print("Alterações salvas com sucesso")
return jsonify({
'status': 'success',
'message': f'Militante {militante.nome} atualizado com sucesso!'
})
except Exception as e:
print(f"Erro ao atualizar militante: {str(e)}")
db.rollback()
return jsonify({
'status': 'error',
'message': f'Erro ao atualizar militante: {str(e)}'
}), 500
finally:
db.close()
# Rota para criar um novo usuário
@app.route("/usuarios/novo", methods=["GET", "POST"])
@@ -1459,6 +1555,98 @@ def excluir_assinatura(id):
finally:
db.close()
@app.route("/militantes/<int:militante_id>/dados")
@require_login
@require_permission('gerenciar_militantes')
def get_militante_dados(militante_id):
"""Retorna os dados completos de um militante"""
print(f"Buscando dados do militante {militante_id}")
db = get_db_connection()
try:
militante = db.query(Militante).options(
joinedload(Militante.endereco),
joinedload(Militante.emails),
joinedload(Militante.celula)
).get(militante_id)
if not militante:
print(f"Militante {militante_id} não encontrado")
return jsonify({
'status': 'error',
'message': 'Militante não encontrado'
}), 404
print(f"Militante {militante_id} encontrado: {militante.nome}")
# Formatar datas para o formato YYYY-MM-DD
data_nascimento = militante.data_nascimento.strftime('%Y-%m-%d') if militante.data_nascimento else None
data_entrada = militante.data_entrada_oci.strftime('%Y-%m-%d') if militante.data_entrada_oci else None
data_efetivacao = militante.data_efetivacao_oci.strftime('%Y-%m-%d') if militante.data_efetivacao_oci else None
# Buscar o primeiro email do militante
email_principal = None
if militante.emails and len(militante.emails) > 0:
email_principal = militante.emails[0].endereco_email
dados = {
'id': militante.id,
'nome': militante.nome,
'cpf': militante.cpf,
'titulo_eleitoral': militante.titulo_eleitoral,
'data_nascimento': data_nascimento,
'data_entrada_oci': data_entrada,
'data_efetivacao_oci': data_efetivacao,
# Contato
'telefone1': militante.telefone1,
'telefone2': militante.telefone2,
'email': email_principal,
# Endereço
'endereco': {
'cep': militante.endereco.cep if militante.endereco else None,
'estado': militante.endereco.estado if militante.endereco else None,
'cidade': militante.endereco.cidade if militante.endereco else None,
'bairro': militante.endereco.bairro if militante.endereco else None,
'rua': militante.endereco.rua if militante.endereco else None,
'numero': militante.endereco.numero if militante.endereco else None,
'complemento': militante.endereco.complemento if militante.endereco else None
} if militante.endereco else None,
# Profissional
'profissao': militante.profissao,
'regime_trabalho': militante.regime_trabalho,
'empresa': militante.empresa,
'contratante': militante.contratante,
# Acadêmico
'instituicao_ensino': militante.instituicao_ensino,
'tipo_instituicao': militante.tipo_instituicao,
# Sindical
'sindicato': militante.sindicato,
'cargo_sindical': militante.cargo_sindical,
'central_sindical': militante.central_sindical,
'dirigente_sindical': militante.dirigente_sindical,
# Organização
'estado': militante.estado.value if militante.estado else 'ATIVO',
'celula_id': militante.celula_id,
'responsabilidades': militante.responsabilidades
}
print(f"Dados formatados: {dados}")
return jsonify(dados)
except Exception as e:
print(f"Erro ao buscar dados do militante {militante_id}: {str(e)}")
return jsonify({
'status': 'error',
'message': f'Erro ao buscar dados do militante: {str(e)}'
}), 500
finally:
db.close()
def create_app():
app = Flask(__name__)
# ... existing code ...
@@ -1474,13 +1662,6 @@ def init_system():
print("Inicializando banco de dados...")
init_database()
# Criar admin
create_admin_user()
# Criar usuários de teste
create_test_users()
# Verificar configuração
db = get_db_connection()
try:
# Verificar admin
@@ -1489,10 +1670,15 @@ def init_system():
print("\nAdmin configurado:")
print(f"Username: admin")
print(f"Senha: admin123")
if os.path.exists('admin_qr.png'):
print("OTP: Usando configuração existente do arquivo admin_qr.png")
if admin.otp_secret:
print("OTP: Usando configuração existente")
else:
admin.otp_secret = pyotp.random_base32()
db.commit()
print("OTP: Nova configuração gerada")
else:
# Criar admin se não existir
create_admin_user()
# Verificar usuários de teste
test_users = ['aligner', 'tester', 'deployer']
@@ -1503,14 +1689,19 @@ def init_system():
print(f"Username: {username}")
print(f"Senha: Test123!@#")
if user.otp_secret:
print("OTP: Configurado")
print("OTP: Usando configuração existente")
else:
print("OTP: Não configurado")
user.otp_secret = pyotp.random_base32()
db.commit()
print("OTP: Nova configuração gerada")
else:
# Criar usuário de teste se não existir
create_test_users()
finally:
db.close()
print("\nInstruções:")
print("1. Configure o OTP usando o QR code gerado")
print("1. Configure o OTP usando o QR code gerado (apenas para novos usuários)")
print("2. Faça login com as credenciais fornecidas")
print("3. Altere a senha no primeiro login")

View File

@@ -55,6 +55,10 @@ def create_admin_user():
if admin:
print("\n=== Usuário Admin Encontrado ===")
if not admin.otp_secret:
print("Gerando novo segredo OTP...")
admin.generate_otp_secret()
db.commit()
else:
print("\n=== Criando Novo Usuário Admin ===")
# Criar novo usuário admin
@@ -70,8 +74,16 @@ def create_admin_user():
db.add(admin)
db.commit()
# Gerar QR code uma única vez
qr_path, otp_uri = generate_qr_code(admin)
# Gerar QR code apenas se solicitado ou se for novo usuário
if not os.path.exists('admin_qr.png'):
qr_path, otp_uri = generate_qr_code(admin)
print("\n=== QR Code Gerado ===")
print(f"QR Code salvo em: {qr_path}")
print(f"URI do OTP: {otp_uri}")
else:
print("\n=== QR Code Existente ===")
print("Usando QR Code existente em: admin_qr.png")
qr_path = 'admin_qr.png'
# Mostrar informações
print("\n=== Informações do Admin ===")
@@ -80,10 +92,6 @@ def create_admin_user():
print(f"Senha: admin123")
print(f"Segredo OTP: {admin.otp_secret}")
print("\n=== QR Code Gerado ===")
print(f"QR Code salvo em: {qr_path}")
print(f"URI do OTP: {otp_uri}")
# Gerar código atual para verificação
totp = pyotp.TOTP(admin.otp_secret)
current_code = totp.now()

View File

@@ -56,18 +56,16 @@ def create_test_users():
)
user.set_password(user_data['password'])
user.tipo = "ADMIN" if user_data['is_admin'] else "USUARIO"
db.add(user)
db.commit()
# Se for o usuário teste, usar o mesmo OTP do admin
if user_data['username'] == 'teste' and admin_otp_secret:
user.otp_secret = admin_otp_secret
db.commit()
else:
# Gerar novo OTP para outros usuários
otp_secret = pyotp.random_base32()
user.otp_secret = otp_secret
db.commit()
user.otp_secret = pyotp.random_base32()
db.add(user)
db.commit()
# Atribuir role de Secretário Geral para o usuário teste
if user_data['username'] == 'teste':
@@ -77,6 +75,17 @@ def create_test_users():
db.commit()
print(f"Usuário {user_data['username']} criado com sucesso!")
# Gerar QR code para o novo usuário
qr_path = f"{user_data['username']}_qr.png"
if not os.path.exists(qr_path):
totp = pyotp.TOTP(user.otp_secret)
qr = qrcode.QRCode(version=1, box_size=10, border=5)
qr.add_data(totp.provisioning_uri(user.email, issuer_name="Sistema de Controles"))
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
img.save(qr_path)
print(f"QR Code gerado para {user_data['username']} em: {qr_path}")
else:
print(f"Usuário {user_data['username']} já existe")
@@ -85,6 +94,22 @@ def create_test_users():
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':

View File

@@ -7,6 +7,13 @@
--green: #198754;
--cyan: #0dcaf0;
--yellow: #ffc107;
--primary-color: #dc3545;
--primary-hover: #bb2d3b;
--text-color: #333;
--text-muted: #6c757d;
--bg-hover: #f8f9fa;
--tab-active-color: var(--primary-color);
--tab-hover-color: rgba(220, 53, 69, 0.1);
}
/* Tabelas */
@@ -241,4 +248,171 @@
.welcome-header h4 {
font-size: 1.25rem;
font-weight: 400;
}
/* Tabs */
.nav-tabs {
border-bottom: 2px solid var(--border-color);
margin-bottom: 1rem;
}
.nav-tabs .nav-link,
.nav-tabs .nav-link:focus,
.nav-tabs .nav-link:hover,
.nav-tabs .nav-link.active {
color: var(--primary-color) !important;
border: none;
border-bottom: 2px solid transparent;
padding: 0.75rem 1.5rem;
margin-bottom: -2px;
transition: all 0.2s ease-in-out;
font-weight: 500;
background-color: transparent;
}
.nav-tabs .nav-link:hover {
background-color: var(--tab-hover-color);
border-bottom: 2px solid var(--primary-color);
}
.nav-tabs .nav-link.active {
font-weight: 600;
background-color: var(--tab-hover-color);
border-bottom: 2px solid var(--primary-color);
}
.nav-tabs .nav-link i {
margin-right: 0.5rem;
}
.tab-content {
padding: 1rem 0;
}
.tab-pane {
animation: fadeIn 0.3s ease-in-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(5px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Responsividade das abas */
@media (max-width: 768px) {
.nav-tabs {
flex-wrap: nowrap;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.nav-tabs .nav-link {
white-space: nowrap;
padding: 0.5rem 1rem;
}
}
/* Estilo para botões com largura fixa */
.btn-fixed-width {
min-width: 120px;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.375rem 0.75rem;
text-align: center;
height: 38px;
line-height: 1.5;
vertical-align: middle;
}
.btn-fixed-width i {
margin-right: 8px;
font-size: 0.875rem;
}
/* Estilo para o backdrop com blur em todos os modais */
.modal-backdrop.show {
backdrop-filter: blur(8px);
background-color: rgba(0, 0, 0, 0.7);
}
/* Estilo para o botão de fechar dos modais */
.btn-close {
background-color: transparent;
padding: 0.5rem;
opacity: 0.8;
transition: opacity 0.2s;
filter: invert(1) grayscale(100%) brightness(200%);
}
.btn-close:hover {
opacity: 1;
background-color: transparent;
}
/* Estilos do Modal */
.modal-header {
background-color: #343a40;
color: #fff;
padding: 1rem;
}
.modal-title {
color: #fff;
font-weight: 600;
display: flex;
align-items: center;
}
.modal-header i {
color: #fff;
margin-right: 0.5rem;
}
.modal-header .btn-close {
filter: invert(1) grayscale(100%) brightness(200%);
opacity: 0.8;
}
.modal-header .btn-close:hover {
opacity: 1;
}
/* Estilos globais de formulário */
.form-control:focus,
.form-select:focus,
.form-check-input:focus,
.btn:focus,
.btn-check:focus + .btn {
border-color: var(--primary-color);
box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.25);
}
.form-control:hover,
.form-select:hover {
border-color: var(--primary-color);
}
/* Input group com foco */
.input-group .form-control:focus,
.input-group .form-select:focus {
border-color: var(--primary-color);
}
/* Checkbox e radio */
.form-check-input:checked {
background-color: var(--primary-color);
border-color: var(--primary-color);
}
/* Date picker */
input[type="date"]:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.25);
}

View File

@@ -61,6 +61,8 @@ function filtrarMilitantes() {
// Configurar eventos quando o DOM estiver carregado
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM carregado, configurando eventos...');
// Configurar pesquisa
const searchInput = document.getElementById('searchInput');
if (searchInput) {
@@ -109,7 +111,7 @@ document.addEventListener('DOMContentLoaded', function() {
});
});
console.log('DOM carregado');
console.log('Configurando modal de edição...');
// Configuração do modal de edição
const modalEditarMilitante = document.getElementById('modalEditarMilitante');
@@ -118,6 +120,10 @@ document.addEventListener('DOMContentLoaded', function() {
if (modalEditarMilitante) {
console.log('Modal encontrado, configurando eventos...');
// Criar instância do modal Bootstrap
const modalInstance = new bootstrap.Modal(modalEditarMilitante);
console.log('Instância do modal criada:', modalInstance);
modalEditarMilitante.addEventListener('show.bs.modal', function(event) {
console.log('Modal sendo exibido');
const button = event.relatedTarget;
@@ -129,33 +135,155 @@ document.addEventListener('DOMContentLoaded', function() {
}
const militanteId = button.getAttribute('data-militante-id');
const militanteNome = button.getAttribute('data-militante-nome');
console.log('ID do militante:', militanteId);
console.log('Nome do militante:', militanteNome);
// Dados do militante
const dados = {
nome: button.getAttribute('data-militante-nome'),
cpf: button.getAttribute('data-militante-cpf'),
email: button.getAttribute('data-militante-email'),
telefone: button.getAttribute('data-militante-telefone'),
endereco: button.getAttribute('data-militante-endereco'),
filiado: button.getAttribute('data-militante-filiado')
};
console.log('Dados do militante:', dados);
if (!militanteId) {
console.error('ID do militante não encontrado no botão!');
return;
}
// Preencher campos
document.getElementById('editNome').value = dados.nome || '';
document.getElementById('editCpf').value = dados.cpf || '';
document.getElementById('editEmail').value = dados.email || '';
document.getElementById('editTelefone').value = dados.telefone || '';
document.getElementById('editEndereco').value = dados.endereco || '';
document.getElementById('editFiliado').checked = dados.filiado === 'True';
// Atualizar o título do modal com o nome do militante
const modalTitle = modalEditarMilitante.querySelector('.modal-title');
if (modalTitle) {
modalTitle.innerHTML = `<i class="fas fa-user-edit me-2"></i>Editar ${militanteNome}`;
}
// Configurar formulário
const form = document.getElementById('formEditarMilitante');
if (form) {
form.action = `/militantes/editar/${militanteId}`;
const idInput = document.getElementById('edit_militante_id');
if (idInput) {
idInput.value = militanteId;
}
console.log('Action do formulário:', form.action);
console.log('ID do militante no formulário:', militanteId);
} else {
console.error('Formulário não encontrado!');
return;
}
// Mostrar loading
const modalBody = modalEditarMilitante.querySelector('.modal-body');
if (modalBody) {
modalBody.style.opacity = '0.5';
}
// Buscar dados completos do militante via AJAX
console.log(`Fazendo requisição para /militantes/${militanteId}/dados`);
// Obter o CSRF token
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
console.log('CSRF token:', csrfToken);
fetch(`/militantes/${militanteId}/dados`, {
method: 'GET',
headers: {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
'X-CSRFToken': csrfToken
},
credentials: 'same-origin'
})
.then(response => {
console.log('Resposta recebida:', response);
if (!response.ok) {
return response.json().then(data => {
throw new Error(data.message || `HTTP error! status: ${response.status}`);
});
}
return response.json();
})
.then(dados => {
console.log('Dados do militante recebidos:', dados);
if (!dados) {
throw new Error('Dados do militante não encontrados');
}
try {
console.log('Preenchendo dados básicos...');
// Dados Básicos
document.getElementById('edit_nome').value = dados.nome || '';
document.getElementById('edit_cpf').value = dados.cpf || '';
document.getElementById('edit_titulo_eleitoral').value = dados.titulo_eleitoral || '';
document.getElementById('edit_data_nascimento').value = dados.data_nascimento || '';
document.getElementById('edit_data_entrada').value = dados.data_entrada_oci || '';
document.getElementById('edit_data_efetivacao').value = dados.data_efetivacao_oci || '';
console.log('Dados básicos preenchidos');
console.log('Preenchendo dados de contato...');
// Contato
document.getElementById('edit_telefone1').value = dados.telefone1 || '';
document.getElementById('edit_telefone2').value = dados.telefone2 || '';
document.getElementById('edit_email').value = dados.email || '';
console.log('Dados de contato preenchidos');
console.log('Preenchendo dados de endereço...');
// Endereço
if (dados.endereco) {
document.getElementById('edit_cep').value = dados.endereco.cep || '';
document.getElementById('edit_estado').value = dados.endereco.estado || '';
document.getElementById('edit_cidade').value = dados.endereco.cidade || '';
document.getElementById('edit_bairro').value = dados.endereco.bairro || '';
document.getElementById('edit_logradouro').value = dados.endereco.rua || '';
document.getElementById('edit_numero').value = dados.endereco.numero || '';
document.getElementById('edit_complemento').value = dados.endereco.complemento || '';
}
console.log('Dados de endereço preenchidos');
console.log('Preenchendo dados profissionais...');
// Profissional
document.getElementById('edit_profissao').value = dados.profissao || '';
document.getElementById('edit_regime_trabalho').value = dados.regime_trabalho || '';
document.getElementById('edit_empresa').value = dados.empresa || '';
document.getElementById('edit_contratante').value = dados.contratante || '';
console.log('Dados profissionais preenchidos');
console.log('Preenchendo dados acadêmicos...');
// Acadêmico
document.getElementById('edit_instituicao_ensino').value = dados.instituicao_ensino || '';
document.getElementById('edit_tipo_instituicao').value = dados.tipo_instituicao || '';
console.log('Dados acadêmicos preenchidos');
console.log('Preenchendo dados sindicais...');
// Sindical
document.getElementById('edit_sindicato').value = dados.sindicato || '';
document.getElementById('edit_cargo_sindical').value = dados.cargo_sindical || '';
document.getElementById('edit_central_sindical').value = dados.central_sindical || '';
document.getElementById('edit_dirigente_sindical').checked = dados.dirigente_sindical || false;
console.log('Dados sindicais preenchidos');
console.log('Preenchendo dados organizacionais...');
// Organização
document.getElementById('edit_estado_militante').value = dados.estado || 'ATIVO';
document.getElementById('edit_celula').value = dados.celula_id || '';
// Responsabilidades
const responsabilidades = dados.responsabilidades || 0;
document.querySelectorAll('input[name="responsabilidades"]').forEach(checkbox => {
const valor = parseInt(checkbox.value);
checkbox.checked = (responsabilidades & valor) !== 0;
});
console.log('Dados organizacionais preenchidos');
console.log('Todos os dados preenchidos com sucesso!');
} catch (error) {
console.error('Erro ao preencher os campos:', error);
throw new Error('Erro ao preencher os dados do militante: ' + error.message);
}
})
.catch(error => {
console.error('Erro ao buscar dados do militante:', error);
alert('Erro ao carregar dados do militante: ' + error.message);
})
.finally(() => {
if (modalBody) {
modalBody.style.opacity = '1';
}
});
});
// Envio do formulário de edição via AJAX
@@ -163,53 +291,76 @@ document.addEventListener('DOMContentLoaded', function() {
if (formEditarMilitante) {
formEditarMilitante.addEventListener('submit', function(e) {
e.preventDefault();
console.log('Enviando formulário de edição...');
const formData = new FormData(this);
console.log('Dados do formulário:', Object.fromEntries(formData));
fetch(this.action, {
// Adicionar responsabilidades selecionadas
const responsabilidades = Array.from(this.querySelectorAll('input[name="responsabilidades"]:checked'))
.map(cb => parseInt(cb.value))
.reduce((a, b) => a + b, 0);
formData.set('responsabilidades', responsabilidades);
// Obter o CSRF token
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
console.log('CSRF token:', csrfToken);
// Obter o ID do militante do campo hidden
const militanteId = document.getElementById('edit_militante_id').value;
console.log('ID do militante:', militanteId);
if (!militanteId) {
console.error('ID do militante não encontrado!');
mostrarAlerta('Erro: ID do militante não encontrado', 'danger');
return;
}
// Garantir que o campo de endereço está correto
const logradouro = formData.get('logradouro');
if (logradouro) {
formData.set('rua', logradouro);
formData.delete('logradouro');
}
fetch(`/militantes/editar/${militanteId}`, {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
'X-Requested-With': 'XMLHttpRequest',
'X-CSRFToken': csrfToken
},
credentials: 'same-origin'
})
.then(response => {
console.log('Resposta recebida:', response);
if (!response.ok) {
return response.json().then(data => {
throw new Error(data.message || `HTTP error! status: ${response.status}`);
});
}
return response.json();
})
.then(response => response.json())
.then(data => {
console.log('Resposta processada:', data);
if (data.status === 'success') {
// Fechar o modal
bootstrap.Modal.getInstance(modalEditarMilitante).hide();
// Atualizar a lista
location.reload();
// Mostrar mensagem de sucesso
const alertDiv = document.createElement('div');
alertDiv.className = 'alert alert-success alert-dismissible fade show';
alertDiv.innerHTML = `
${data.message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.querySelector('.container').insertBefore(alertDiv, document.querySelector('.container').firstChild);
mostrarAlerta(data.message, 'success');
// Recarregar a página após um breve delay
setTimeout(() => {
location.reload();
}, 1500);
} else {
// Mostrar erro
const alertDiv = document.createElement('div');
alertDiv.className = 'alert alert-danger alert-dismissible fade show';
alertDiv.innerHTML = `
${data.message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.querySelector('.modal-body').insertBefore(alertDiv, formEditarMilitante);
throw new Error(data.message || 'Erro ao salvar dados');
}
})
.catch(error => {
console.error('Erro:', error);
const alertDiv = document.createElement('div');
alertDiv.className = 'alert alert-danger alert-dismissible fade show';
alertDiv.innerHTML = `
Erro ao atualizar militante. Tente novamente.
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.querySelector('.modal-body').insertBefore(alertDiv, formEditarMilitante);
console.error('Erro ao enviar formulário:', error);
mostrarAlerta(`Erro ao salvar dados: ${error.message}`, 'danger');
});
});
}
@@ -334,32 +485,31 @@ document.addEventListener('DOMContentLoaded', function() {
}
// Máscara para CPF
const cpfInput = document.getElementById('cpf');
if (cpfInput) {
cpfInput.addEventListener('input', function(e) {
const cpfInputs = document.querySelectorAll('input[name="cpf"]');
cpfInputs.forEach(input => {
input.addEventListener('input', function(e) {
let value = e.target.value.replace(/\D/g, '');
if (value.length <= 11) {
value = value.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, '$1.$2.$3-$4');
value = value.replace(/(\d{3})(\d)/, '$1.$2');
value = value.replace(/(\d{3})(\d)/, '$1.$2');
value = value.replace(/(\d{3})(\d{1,2})$/, '$1-$2');
e.target.value = value;
}
});
}
});
// Máscara para telefone
const telefoneInput = document.getElementById('telefone');
if (telefoneInput) {
telefoneInput.addEventListener('input', function(e) {
const telefoneInputs = document.querySelectorAll('input[name="telefone1"], input[name="telefone2"]');
telefoneInputs.forEach(input => {
input.addEventListener('input', function(e) {
let value = e.target.value.replace(/\D/g, '');
if (value.length <= 11) {
if (value.length === 11) {
value = value.replace(/(\d{2})(\d{5})(\d{4})/, '($1) $2-$3');
} else {
value = value.replace(/(\d{2})(\d{4})(\d{4})/, '($1) $2-$3');
}
value = value.replace(/(\d{2})(\d)/, '($1) $2');
value = value.replace(/(\d{4,5})(\d{4})$/, '$1-$2');
e.target.value = value;
}
});
}
});
// Limpar formulário e alertas quando o modal for fechado
const modalNovoMilitante = document.getElementById('modalNovoMilitante');
@@ -444,4 +594,71 @@ document.addEventListener('DOMContentLoaded', function() {
document.body.removeChild(link);
});
}
});
// Configurar máscaras de input
// CEP
const cepInputs = document.querySelectorAll('input[name="cep"]');
cepInputs.forEach(input => {
input.addEventListener('input', function(e) {
let value = e.target.value.replace(/\D/g, '');
if (value.length <= 8) {
value = value.replace(/(\d{5})(\d)/, '$1-$2');
e.target.value = value;
}
});
// Buscar endereço pelo CEP
input.addEventListener('blur', function(e) {
const cep = e.target.value.replace(/\D/g, '');
if (cep.length === 8) {
fetch(`https://viacep.com.br/ws/${cep}/json/`)
.then(response => response.json())
.then(data => {
if (!data.erro) {
const form = input.closest('form');
form.querySelector('input[name="logradouro"]').value = data.logradouro;
form.querySelector('input[name="bairro"]').value = data.bairro;
form.querySelector('input[name="cidade"]').value = data.localidade;
form.querySelector('select[name="estado"]').value = data.uf;
}
});
}
});
});
// Título Eleitoral
const tituloInputs = document.querySelectorAll('input[name="titulo_eleitoral"]');
tituloInputs.forEach(input => {
input.addEventListener('input', function(e) {
let value = e.target.value.replace(/\D/g, '');
if (value.length <= 12) {
value = value.replace(/(\d{4})(\d)/, '$1 $2');
value = value.replace(/(\d{4})(\d)/, '$1 $2');
e.target.value = value;
}
});
});
});
// Função para mostrar alertas
function mostrarAlerta(mensagem, tipo) {
// Criar o elemento de alerta
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${tipo} alert-dismissible fade show position-fixed top-0 start-50 translate-middle-x mt-3`;
alertDiv.style.zIndex = '9999';
alertDiv.innerHTML = `
${mensagem}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
// Adicionar o alerta ao corpo do documento
document.body.appendChild(alertDiv);
// Configurar o Bootstrap alert
const bsAlert = new bootstrap.Alert(alertDiv);
// Remover o alerta após 3 segundos
setTimeout(() => {
bsAlert.close();
}, 3000);
}

View File

@@ -3,12 +3,15 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{% block title %}{% endblock %} - Controles OCI</title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css?v=1" rel="stylesheet">
<!-- Font Awesome 6 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css?v=1">
<!-- Componentes CSS -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/components.css') }}">
<style>
:root {

View File

@@ -24,6 +24,8 @@
{% endwith %}
<form method="POST" action="{{ url_for('login') }}" class="needs-validation" novalidate>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="form-floating mb-3">
<input type="text" class="form-control" id="email" name="email" placeholder="Email ou Usuário" required>
<label for="email">Email ou Usuário</label>

23
templates/militantes.html Normal file
View File

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

View File

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

View File

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