Corrigir atualização de dados na tabela de militantes

This commit is contained in:
andersonid
2025-04-09 09:20:07 -03:00
committed by LS
parent c640a756df
commit abc46704c3
19 changed files with 2582 additions and 974 deletions

433
app.py
View File

@@ -25,7 +25,7 @@ from functions.database import (
) )
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, joinedload from sqlalchemy.orm import sessionmaker, joinedload
from datetime import datetime, timedelta from datetime import datetime, timedelta, date
from flask_bootstrap import Bootstrap5 from flask_bootstrap import Bootstrap5
from functions.validations import validar_cpf from functions.validations import validar_cpf
from functools import wraps from functools import wraps
@@ -43,7 +43,6 @@ import qrcode
import base64 import base64
from io import BytesIO from io import BytesIO
from create_admin import create_admin_user from create_admin import create_admin_user
from create_test_users import create_test_users
from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user
import random import random
@@ -51,6 +50,7 @@ import string
from sqlalchemy.sql import func from sqlalchemy.sql import func
from flask_wtf.csrf import CSRFProtect from flask_wtf.csrf import CSRFProtect
import json import json
from utils.date_utils import validar_data, converter_data, validar_sequencia_datas, calcular_idade
load_dotenv() load_dotenv()
@@ -615,14 +615,17 @@ def create_app():
@require_login @require_login
def novo_pagamento(): def novo_pagamento():
if request.method == "POST": if request.method == "POST":
militante_id = request.form.get("militante_id")
tipo_pagamento_id = request.form.get("tipo_pagamento_id")
valor = float(request.form.get("valor"))
data_pagamento = datetime.strptime(request.form.get("data_pagamento"), "%Y-%m-%d").date()
# Criar novo pagamento
db = get_db_connection()
try: try:
militante_id = request.form.get("militante_id")
tipo_pagamento_id = request.form.get("tipo_pagamento_id")
valor = float(request.form.get("valor"))
data_pagamento = converter_data(request.form.get("data_pagamento"))
if not validar_data(data_pagamento):
flash('Data de pagamento inválida ou futura', 'danger')
return redirect(url_for('novo_pagamento'))
db = get_db_connection()
pagamento = Pagamento( pagamento = Pagamento(
militante_id=militante_id, militante_id=militante_id,
tipo_pagamento_id=tipo_pagamento_id, tipo_pagamento_id=tipo_pagamento_id,
@@ -635,9 +638,9 @@ def create_app():
return redirect(url_for('listar_pagamentos')) return redirect(url_for('listar_pagamentos'))
except Exception as e: except Exception as e:
db.rollback() db.rollback()
print(f"Erro ao cadastrar pagamento: {e}") app.logger.error(f"Erro ao cadastrar pagamento: {e}")
flash('Erro ao cadastrar pagamento', 'danger') flash('Erro ao cadastrar pagamento', 'danger')
return render_template("novo_pagamento.html") return redirect(url_for('novo_pagamento'))
finally: finally:
db.close() db.close()
@@ -687,7 +690,7 @@ def create_app():
militante_id = request.form.get("militante_id") militante_id = request.form.get("militante_id")
tipo_pagamento = request.form.get("tipo_pagamento") tipo_pagamento = request.form.get("tipo_pagamento")
valor = float(request.form.get("valor")) valor = float(request.form.get("valor"))
data_pagamento = datetime.strptime(request.form.get("data_pagamento"), "%Y-%m-%d").date() data_pagamento = converter_data(request.form.get("data_pagamento"))
db = get_db_connection() db = get_db_connection()
pagamento = Pagamento( pagamento = Pagamento(
@@ -718,7 +721,7 @@ def create_app():
tipo_material_id = request.form.get('tipo_material_id') tipo_material_id = request.form.get('tipo_material_id')
descricao = request.form.get('descricao') descricao = request.form.get('descricao')
valor = float(request.form.get('valor')) valor = float(request.form.get('valor'))
data_venda = datetime.strptime(request.form.get('data_venda'), '%Y-%m-%d') data_venda = converter_data(request.form.get('data_venda'))
material = MaterialVendido( material = MaterialVendido(
militante_id=militante_id, militante_id=militante_id,
@@ -775,44 +778,51 @@ def create_app():
@require_login @require_login
@require_permission(Permission.VIEW_CELL_REPORTS) @require_permission(Permission.VIEW_CELL_REPORTS)
def nova_venda_jornal(): def nova_venda_jornal():
db = get_db_connection() if request.method == "POST":
try: try:
militante_id = request.form.get('militante_id') militante_id = request.form.get('militante_id')
quantidade = int(request.form.get('quantidade')) quantidade = int(request.form.get('quantidade'))
valor_total = float(request.form.get('valor_total')) valor_total = float(request.form.get('valor_total'))
data_venda = datetime.strptime(request.form.get('data_venda'), '%Y-%m-%d') data_venda = converter_data(request.form.get('data_venda'))
venda = VendaJornalAvulso( if not validar_data(data_venda):
militante_id=militante_id, if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
quantidade=quantidade, return jsonify({
valor_total=valor_total, 'status': 'error',
data_venda=data_venda 'message': 'Data de venda inválida ou futura'
) }), 400
flash('Data de venda inválida ou futura', 'danger')
db.add(venda) return redirect(url_for('nova_venda_jornal'))
db.commit()
db = get_db_connection()
if request.headers.get('X-Requested-With') == 'XMLHttpRequest': venda = VendaJornalAvulso(
return jsonify({ militante_id=militante_id,
'status': 'success', quantidade=quantidade,
'message': 'Venda cadastrada com sucesso!' valor_total=valor_total,
}) data_venda=data_venda
)
flash('Venda cadastrada com sucesso!', 'success') db.add(venda)
return redirect(url_for('listar_vendas_jornal')) db.commit()
except Exception as e: if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
db.rollback() return jsonify({
if request.headers.get('X-Requested-With') == 'XMLHttpRequest': 'status': 'success',
return jsonify({ 'message': 'Venda cadastrada com sucesso!'
'status': 'error', })
'message': 'Erro ao cadastrar venda. Por favor, tente novamente.' flash('Venda cadastrada com sucesso!', 'success')
}), 400 return redirect(url_for('listar_vendas_jornal'))
except Exception as e:
flash('Erro ao cadastrar venda. Por favor, tente novamente.', 'error') db.rollback()
return redirect(url_for('listar_vendas_jornal')) app.logger.error(f"Erro ao cadastrar venda: {e}")
finally: if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
db.close() return jsonify({
'status': 'error',
'message': 'Erro ao cadastrar venda'
}), 400
flash('Erro ao cadastrar venda', 'danger')
return redirect(url_for('nova_venda_jornal'))
finally:
db.close()
# Rota para listar vendas de jornal # Rota para listar vendas de jornal
@app.route("/jornais") @app.route("/jornais")
@@ -835,36 +845,56 @@ def create_app():
@require_permission(Permission.VIEW_CELL_REPORTS) @require_permission(Permission.VIEW_CELL_REPORTS)
def novo_relatorio_cotas(): def novo_relatorio_cotas():
if request.method == "POST": if request.method == "POST":
setor_id = request.form.get("setor_id")
comite_id = request.form.get("comite_id")
total_cotas = float(request.form.get("total_cotas"))
data_relatorio = datetime.strptime(request.form.get("data_relatorio"), "%Y-%m-%d").date()
db = get_db_connection()
try: try:
relatorio_cotas_mensais = RelatorioCotasMensais( setor_id = request.form.get("setor_id")
setor_id=setor_id, comite_id = request.form.get("comite_id")
comite_id=comite_id, total_cotas = float(request.form.get("total_cotas"))
total_cotas=total_cotas, data_relatorio = request.form.get("data_relatorio")
data_relatorio=data_relatorio
) # Validar data
db.add(relatorio_cotas_mensais) if not validar_data(data_relatorio):
db.commit() flash('Data do relatório inválida', 'danger')
flash('Relatório de cotas cadastrado com sucesso!', 'success') return render_template("novo_relatorio_cotas.html")
return redirect(url_for('listar_relatorios_cotas'))
except Exception as e: # Converter data
db.rollback() data_relatorio = converter_data(data_relatorio)
print(f"Erro ao cadastrar relatório de cotas: {e}")
flash('Erro ao cadastrar relatório de cotas', 'danger') # Validar data futura
if data_relatorio > date.today():
flash('A data do relatório não pode ser futura', 'danger')
return render_template("novo_relatorio_cotas.html")
db = get_db_connection()
try:
relatorio_cotas_mensais = RelatorioCotasMensais(
setor_id=setor_id,
comite_id=comite_id,
total_cotas=total_cotas,
data_relatorio=data_relatorio
)
db.add(relatorio_cotas_mensais)
db.commit()
flash('Relatório de cotas cadastrado com sucesso!', 'success')
return redirect(url_for('listar_relatorios_cotas'))
except Exception as e:
db.rollback()
app.logger.error(f"Erro ao cadastrar relatório de cotas: {e}")
flash('Erro ao cadastrar relatório de cotas', 'danger')
return render_template("novo_relatorio_cotas.html")
finally:
db.close()
except ValueError as e:
flash(str(e), 'danger')
return render_template("novo_relatorio_cotas.html") return render_template("novo_relatorio_cotas.html")
finally:
db.close()
db = get_db_connection() db = get_db_connection()
try: try:
setores = db.query(Setor).order_by(Setor.nome).all() setores = db.query(Setor).order_by(Setor.nome).all()
comites = db.query(ComiteCentral).order_by(ComiteCentral.nome).all() comites = db.query(ComiteCentral).order_by(ComiteCentral.nome).all()
return render_template("novo_relatorio_cotas.html", setores=setores, comites=comites) return render_template("novo_relatorio_cotas.html",
setores=setores,
comites=comites,
hoje=date.today().strftime('%Y-%m-%d'))
finally: finally:
db.close() db.close()
@@ -890,36 +920,56 @@ def create_app():
@require_permission(Permission.VIEW_CELL_REPORTS) @require_permission(Permission.VIEW_CELL_REPORTS)
def novo_relatorio_vendas(): def novo_relatorio_vendas():
if request.method == "POST": if request.method == "POST":
setor_id = request.form.get("setor_id")
comite_id = request.form.get("comite_id")
total_vendas = float(request.form.get("total_vendas"))
data_relatorio = datetime.strptime(request.form.get("data_relatorio"), "%Y-%m-%d").date()
db = get_db_connection()
try: try:
relatorio_vendas_materiais = RelatorioVendasMateriais( setor_id = request.form.get("setor_id")
setor_id=setor_id, comite_id = request.form.get("comite_id")
comite_id=comite_id, total_vendas = float(request.form.get("total_vendas"))
total_vendas=total_vendas, data_relatorio = request.form.get("data_relatorio")
data_relatorio=data_relatorio
) # Validar data
db.add(relatorio_vendas_materiais) if not validar_data(data_relatorio):
db.commit() flash('Data do relatório inválida', 'danger')
flash('Relatório de vendas cadastrado com sucesso!', 'success') return render_template("novo_relatorio_vendas.html")
return redirect(url_for('listar_relatorios_vendas'))
except Exception as e: # Converter data
db.rollback() data_relatorio = converter_data(data_relatorio)
print(f"Erro ao cadastrar relatório de vendas: {e}")
flash('Erro ao cadastrar relatório de vendas', 'danger') # Validar data futura
if data_relatorio > date.today():
flash('A data do relatório não pode ser futura', 'danger')
return render_template("novo_relatorio_vendas.html")
db = get_db_connection()
try:
relatorio_vendas_materiais = RelatorioVendasMateriais(
setor_id=setor_id,
comite_id=comite_id,
total_vendas=total_vendas,
data_relatorio=data_relatorio
)
db.add(relatorio_vendas_materiais)
db.commit()
flash('Relatório de vendas cadastrado com sucesso!', 'success')
return redirect(url_for('listar_relatorios_vendas'))
except Exception as e:
db.rollback()
app.logger.error(f"Erro ao cadastrar relatório de vendas: {e}")
flash('Erro ao cadastrar relatório de vendas', 'danger')
return render_template("novo_relatorio_vendas.html")
finally:
db.close()
except ValueError as e:
flash(str(e), 'danger')
return render_template("novo_relatorio_vendas.html") return render_template("novo_relatorio_vendas.html")
finally:
db.close()
db = get_db_connection() db = get_db_connection()
try: try:
setores = db.query(Setor).order_by(Setor.nome).all() setores = db.query(Setor).order_by(Setor.nome).all()
comites = db.query(ComiteCentral).order_by(ComiteCentral.nome).all() comites = db.query(ComiteCentral).order_by(ComiteCentral.nome).all()
return render_template("novo_relatorio_vendas.html", setores=setores, comites=comites) return render_template("novo_relatorio_vendas.html",
setores=setores,
comites=comites,
hoje=date.today().strftime('%Y-%m-%d'))
finally: finally:
db.close() db.close()
@@ -945,8 +995,8 @@ def create_app():
@require_permission('gerenciar_militantes') @require_permission('gerenciar_militantes')
def editar_militante(militante_id): def editar_militante(militante_id):
try: try:
# Verificar se o militante existe db = get_db_connection()
militante = db_session.query(Militante).get(militante_id) militante = db.query(Militante).get(militante_id)
if not militante: if not militante:
return jsonify({ return jsonify({
'status': 'error', 'status': 'error',
@@ -964,15 +1014,29 @@ def create_app():
telefone2 = request.form.get('telefone2') telefone2 = request.form.get('telefone2')
email = request.form.get('email') email = request.form.get('email')
# Converter datas para objetos date # Validar e converter datas
data_nascimento = datetime.strptime(data_nascimento, '%Y-%m-%d').date() if data_nascimento else None try:
data_entrada_oci = datetime.strptime(data_entrada_oci, '%Y-%m-%d').date() if data_entrada_oci else None data_nascimento = converter_data(data_nascimento) if data_nascimento else None
data_efetivacao_oci = datetime.strptime(data_efetivacao_oci, '%Y-%m-%d').date() if data_efetivacao_oci else None data_entrada_oci = converter_data(data_entrada_oci) if data_entrada_oci else None
data_efetivacao_oci = converter_data(data_efetivacao_oci) if data_efetivacao_oci else None
# Validar sequência lógica das datas
validar_sequencia_datas(
data_nascimento=data_nascimento,
data_entrada=data_entrada_oci,
data_efetivacao=data_efetivacao_oci
)
except ValueError as e:
return jsonify({
'status': 'error',
'message': str(e)
}), 400
# Atualizar dados básicos # Atualizar dados básicos
militante.nome = nome if nome: militante.nome = nome
militante.cpf = cpf if cpf: militante.cpf = cpf
militante.titulo_eleitoral = titulo_eleitoral if titulo_eleitoral: militante.titulo_eleitoral = titulo_eleitoral
militante.data_nascimento = data_nascimento militante.data_nascimento = data_nascimento
militante.data_entrada_oci = data_entrada_oci militante.data_entrada_oci = data_entrada_oci
militante.data_efetivacao_oci = data_efetivacao_oci militante.data_efetivacao_oci = data_efetivacao_oci
@@ -981,70 +1045,45 @@ def create_app():
# Atualizar email # Atualizar email
if email: if email:
# Verificar se já existe um email para este militante # Verificar se já existe email
email_existente = db_session.query(EmailMilitante).filter_by(militante_id=militante_id).first() email_existente = db.query(EmailMilitante).filter_by(militante_id=militante_id).first()
if email_existente: if email_existente:
email_existente.endereco_email = email email_existente.endereco_email = email
else: else:
novo_email = EmailMilitante(endereco_email=email, militante_id=militante_id) novo_email = EmailMilitante(endereco_email=email, militante_id=militante_id)
db_session.add(novo_email) db.add(novo_email)
# Atualizar endereço # Calcular idade
endereco = militante.endereco or Endereco(militante_id=militante_id) militante.idade = calcular_idade(data_nascimento) if data_nascimento else None
endereco.cep = request.form.get('cep')
endereco.estado = request.form.get('estado')
endereco.cidade = request.form.get('cidade')
endereco.bairro = request.form.get('bairro')
endereco.rua = request.form.get('rua')
endereco.numero = request.form.get('numero')
endereco.complemento = request.form.get('complemento')
if not militante.endereco:
militante.endereco = endereco
db_session.add(endereco)
# Atualizar dados profissionais
militante.empresa = request.form.get('empresa')
militante.contratante = request.form.get('contratante')
militante.instituicao_ensino = request.form.get('instituicao_ensino')
militante.tipo_instituicao = request.form.get('tipo_instituicao')
# Atualizar dados sindicais
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') == 'true'
# Atualizar responsabilidades
try: try:
responsabilidades_valor = request.form.get('responsabilidades_valor') db.commit()
if responsabilidades_valor: return jsonify({
militante.responsabilidades = int(responsabilidades_valor) 'status': 'success',
print(f"Responsabilidades atualizadas para: {militante.responsabilidades}") 'message': 'Militante atualizado com sucesso',
else: 'data': {
militante.responsabilidades = 0 'nome': militante.nome,
print("Nenhuma responsabilidade definida") 'cpf': militante.cpf,
except (ValueError, TypeError) as e: 'idade': militante.idade,
print(f"Erro ao processar responsabilidades: {e}") 'emails': [e.endereco_email for e in militante.emails],
militante.responsabilidades = 0 'telefone1': militante.telefone1,
'celula_id': str(militante.celula_id) if militante.celula_id else None,
# Salvar alterações 'responsabilidades_valor': militante.responsabilidades
db_session.commit() }
print("Alterações salvas com sucesso!") })
except Exception as e:
# Retornar resposta db.rollback()
return jsonify({ app.logger.error(f"Erro ao salvar militante: {e}")
'status': 'success', return jsonify({
'message': 'Militante atualizado com sucesso!', 'status': 'error',
'responsabilidades': militante.get_responsabilidades() 'message': 'Erro ao salvar alterações no banco de dados'
}) }), 500
except Exception as e: except Exception as e:
db_session.rollback() app.logger.error(f"Erro ao editar militante: {e}")
print(f"Erro ao salvar alterações: {e}")
return jsonify({ return jsonify({
'status': 'error', 'status': 'error',
'message': str(e) 'message': 'Erro interno do servidor'
}), 500 }), 500
# Rota para criar um novo usuário # Rota para criar um novo usuário
@@ -1521,64 +1560,64 @@ def create_app():
@require_login @require_login
@require_permission('gerenciar_militantes') @require_permission('gerenciar_militantes')
def buscar_dados_militante(militante_id): def buscar_dados_militante(militante_id):
"""Retorna os dados de um militante""" """Busca os dados de um militante específico"""
db = get_db_connection() db = get_db_connection()
try: try:
militante = db.query(Militante).options( militante = db.query(Militante).get(militante_id)
joinedload(Militante.endereco),
joinedload(Militante.emails)
).get(militante_id)
if not militante: if not militante:
print(f"Militante não encontrado: ID {militante_id}")
return jsonify({ return jsonify({
'status': 'error', 'status': 'error',
'message': 'Militante não encontrado' 'message': 'Militante não encontrado'
}), 404 }), 404
# Preparar dados do militante # Função auxiliar para formatar data com validação
dados = { def formatar_data_segura(data):
try:
if not data:
return None
return data.strftime('%Y-%m-%d')
except Exception as e:
print(f"Erro ao formatar data: {str(e)}, valor: {data}")
return None
# Formatar datas com validação
data_nascimento = formatar_data_segura(militante.data_nascimento)
data_entrada_oci = formatar_data_segura(militante.data_entrada_oci)
data_efetivacao_oci = formatar_data_segura(militante.data_efetivacao_oci)
print(f"Dados do militante {militante_id} recuperados com sucesso")
print(f"Datas formatadas: nascimento={data_nascimento}, entrada={data_entrada_oci}, efetivação={data_efetivacao_oci}")
return jsonify({
'status': 'success',
'id': militante.id, 'id': militante.id,
'nome': militante.nome, 'nome': militante.nome,
'cpf': militante.cpf, 'cpf': militante.cpf,
'titulo_eleitoral': militante.titulo_eleitoral, 'titulo_eleitoral': militante.titulo_eleitoral,
'data_nascimento': militante.data_nascimento.strftime('%Y-%m-%d') if militante.data_nascimento else None, 'data_nascimento': data_nascimento,
'data_entrada_oci': militante.data_entrada_oci.strftime('%Y-%m-%d') if militante.data_entrada_oci else None, 'data_entrada_oci': data_entrada_oci,
'data_efetivacao_oci': militante.data_efetivacao_oci.strftime('%Y-%m-%d') if militante.data_efetivacao_oci else None, 'data_efetivacao_oci': data_efetivacao_oci,
'emails': [email.endereco_email for email in militante.emails] if militante.emails else [],
'telefone1': militante.telefone1, 'telefone1': militante.telefone1,
'telefone2': militante.telefone2, 'telefone2': militante.telefone2,
'emails': [email.endereco_email for email in militante.emails], 'celula_id': militante.celula_id,
'profissao': militante.profissao, 'responsabilidades_valor': militante.responsabilidades,
'regime_trabalho': militante.regime_trabalho,
'empresa': militante.empresa,
'contratante': militante.contratante,
'instituicao_ensino': militante.instituicao_ensino,
'tipo_instituicao': militante.tipo_instituicao,
'sindicato': militante.sindicato, 'sindicato': militante.sindicato,
'cargo_sindical': militante.cargo_sindical, 'cargo_sindical': militante.cargo_sindical,
'dirigente_sindical': militante.dirigente_sindical,
'central_sindical': militante.central_sindical, 'central_sindical': militante.central_sindical,
'celula_id': militante.celula_id, 'dirigente_sindical': militante.dirigente_sindical
'estado': militante.estado.name if militante.estado else None, })
'responsabilidades_valor': militante.responsabilidades,
'endereco': {
'cep': militante.endereco.cep 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,
'bairro': militante.endereco.bairro if militante.endereco else None,
'cidade': militante.endereco.cidade if militante.endereco else None,
'estado': militante.endereco.estado if militante.endereco else None
}
}
return jsonify(dados)
except Exception as e: except Exception as e:
print(f"Erro ao buscar dados do militante: {str(e)}") import traceback
print(f"Erro ao buscar dados do militante {militante_id}:")
print(traceback.format_exc())
return jsonify({ return jsonify({
'status': 'error', 'status': 'error',
'message': 'Erro ao buscar dados do militante' 'message': f'Erro ao buscar dados do militante: {str(e)}'
}), 500 }), 500
finally:
db.close()
return app return app
@@ -1624,7 +1663,7 @@ def init_system():
print("OTP: Nova configuração gerada") print("OTP: Nova configuração gerada")
else: else:
# Criar usuário de teste se não existir # Criar usuário de teste se não existir
create_test_users() create_admin_user()
finally: finally:
db.close() db.close()

View File

@@ -560,4 +560,52 @@ input.btn-secondary:hover,
border-color: #0b5ed7 !important; border-color: #0b5ed7 !important;
color: white !important; color: white !important;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important;
}
/* Estilos para alertas */
.alert {
position: fixed;
top: 1rem;
left: 50%;
transform: translateX(-50%);
z-index: 9999;
min-width: 300px;
max-width: 600px;
text-align: center;
padding: 1rem 2.5rem 1rem 1rem;
margin: 0;
border-radius: 0.5rem;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
}
.alert .btn-close {
position: absolute;
top: 50%;
right: 1rem;
transform: translateY(-50%);
padding: 0.5rem;
}
.alert-success {
color: #0f5132;
background-color: #d1e7dd;
border-color: #badbcc;
}
.alert-danger {
color: #842029;
background-color: #f8d7da;
border-color: #f5c2c7;
}
.alert-warning {
color: #664d03;
background-color: #fff3cd;
border-color: #ffecb5;
}
.alert-info {
color: #055160;
background-color: #cff4fc;
border-color: #b6effb;
} }

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

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

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

@@ -0,0 +1 @@

View File

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

File diff suppressed because it is too large Load Diff

View File

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

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

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

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

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

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

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

View File

@@ -4,6 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="{{ csrf_token() }}"> <meta name="csrf-token" content="{{ csrf_token() }}">
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='img/favicon.ico') }}">
<title>{% block title %}{% endblock %} - Controles OCI</title> <title>{% block title %}{% endblock %} - Controles OCI</title>
<!-- Bootstrap 5 CSS --> <!-- Bootstrap 5 CSS -->

View File

@@ -63,7 +63,7 @@
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">Célula</h6></li> <li><h6 class="dropdown-header">Célula</h6></li>
{% for celula in celulas %} {% for celula in celulas %}
<li><a class="dropdown-item" href="#" data-filter="celula" data-celula="{{ celula.nome }}">{{ celula.nome }}</a></li> <li><a class="dropdown-item" href="#" data-filter="celula" data-celula="{{ celula.id }}">{{ celula.nome }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

171
utils/date_utils.py Normal file
View File

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