diff --git a/app.py b/app.py index ff4077d..1021cdf 100644 --- a/app.py +++ b/app.py @@ -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/", 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/", 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//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") diff --git a/create_admin.py b/create_admin.py index b9dbaaa..9d70e6d 100644 --- a/create_admin.py +++ b/create_admin.py @@ -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() diff --git a/create_test_users.py b/create_test_users.py index a56c102..88dd5e5 100644 --- a/create_test_users.py +++ b/create_test_users.py @@ -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': diff --git a/static/css/components.css b/static/css/components.css index ac97a90..8e57e30 100644 --- a/static/css/components.css +++ b/static/css/components.css @@ -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); } \ No newline at end of file diff --git a/static/js/militantes.js b/static/js/militantes.js index b7e1e2a..0f84327 100644 --- a/static/js/militantes.js +++ b/static/js/militantes.js @@ -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 = `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} - - `; - 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} - - `; - 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. - - `; - 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); }); } -}); \ No newline at end of file + + // 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} + + `; + + // 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); +} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index 5a90ba6..b2a3cbd 100644 --- a/templates/base.html +++ b/templates/base.html @@ -3,12 +3,15 @@ + {% block title %}{% endblock %} - Controles OCI + +