fix: restaura layout da dashboard e corrige exibição das listas

This commit is contained in:
andersonid
2025-04-03 13:48:09 -03:00
parent 10ff9cab3b
commit 417b5c3f96
13 changed files with 3149 additions and 1013 deletions

589
app.py
View File

@@ -26,7 +26,6 @@ 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
from flask_bootstrap import Bootstrap5 from flask_bootstrap import Bootstrap5
from routes.cota import cota_bp
from functions.validations import validar_cpf from functions.validations import validar_cpf
from functools import wraps from functools import wraps
from pathlib import Path from pathlib import Path
@@ -91,9 +90,6 @@ def generate_qr_code(user):
return qr_code return qr_code
# Registrar blueprints
app.register_blueprint(cota_bp)
# Configuração da sessão do SQLAlchemy # Configuração da sessão do SQLAlchemy
db_session = get_db_connection() db_session = get_db_connection()
@@ -182,24 +178,19 @@ def index():
def login(): def login():
"""Rota de login""" """Rota de login"""
if request.method == "POST": if request.method == "POST":
username = request.form.get("username") email = request.form.get("email")
password = request.form.get("password") password = request.form.get("password")
otp_code = request.form.get("otp_code")
if not all([username, password, otp_code]): if not all([email, password]):
flash("Todos os campos são obrigatórios.", "error") flash("Todos os campos são obrigatórios.", "error")
return redirect(url_for("login")) return redirect(url_for("login"))
db = get_db_connection() db = get_db_connection()
try: try:
user = db.query(Usuario).filter_by(username=username).first() user = db.query(Usuario).filter_by(email=email).first()
if not user or not user.check_password(password): if not user or not user.check_password(password):
flash("Usuário ou senha incorretos.", "error") flash("Email ou senha incorretos.", "error")
return redirect(url_for("login"))
if not user.verify_otp(otp_code):
flash("Código OTP inválido.", "error")
return redirect(url_for("login")) return redirect(url_for("login"))
# Atualizar último login # Atualizar último login
@@ -244,76 +235,23 @@ def home():
nome_usuario = usuario.username if usuario else "Usuário" nome_usuario = usuario.username if usuario else "Usuário"
# Formatar data atual em português # Formatar data atual em português
meses = { data_atual = datetime.now().strftime("%d de %B de %Y")
1: 'Janeiro', 2: 'Fevereiro', 3: 'Março', 4: 'Abril',
5: 'Maio', 6: 'Junho', 7: 'Julho', 8: 'Agosto',
9: 'Setembro', 10: 'Outubro', 11: 'Novembro', 12: 'Dezembro'
}
data_atual = datetime.now()
data_formatada = f"{data_atual.day} de {meses[data_atual.month]} de {data_atual.year}"
# Buscar totais # Buscar dados para o dashboard
total_militantes = db.query(Militante).count() total_militantes = db.query(func.count(Militante.id)).scalar()
total_cotas = db.query(func.sum(CotaMensal.valor_novo)).scalar() or 0 total_cotas = db.query(func.sum(CotaMensal.valor)).scalar() or 0
total_materiais = db.query(MaterialVendido).count() total_materiais = db.query(func.sum(MaterialVendido.valor)).scalar() or 0
total_assinaturas = db.query(AssinaturaAnual).filter( total_assinaturas = db.query(func.sum(AssinaturaAnual.valor)).scalar() or 0
AssinaturaAnual.data_fim >= datetime.now()
).count()
# Buscar últimos militantes cadastrados com tratamento de erro return render_template(
try: "home.html",
ultimos_militantes = db.query(Militante)\ nome_usuario=nome_usuario,
.order_by(Militante.id.desc())\ data_atual=data_atual,
.limit(5)\ total_militantes=total_militantes,
.all() total_cotas=total_cotas,
except Exception as e: total_materiais=total_materiais,
print(f"Erro ao buscar últimos militantes: {e}") total_assinaturas=total_assinaturas
ultimos_militantes = [] )
# Buscar últimos pagamentos com tratamento de erro
try:
ultimos_pagamentos = db.query(Pagamento)\
.join(Militante)\
.order_by(Pagamento.data_pagamento.desc())\
.limit(5)\
.all()
except Exception as e:
print(f"Erro ao buscar últimos pagamentos: {e}")
ultimos_pagamentos = []
return render_template('home.html',
nome_usuario=nome_usuario,
data_atual=data_formatada,
total_militantes=total_militantes,
total_cotas="{:.2f}".format(total_cotas),
total_materiais=total_materiais,
total_assinaturas=total_assinaturas,
ultimos_militantes=ultimos_militantes,
ultimos_pagamentos=ultimos_pagamentos)
except Exception as e:
print(f"Erro na página inicial: {e}")
import traceback
traceback.print_exc()
flash('Erro ao carregar a página inicial', 'danger')
# Mesmo com erro, tentar formatar a data e buscar o nome do usuário
try:
data_atual = datetime.now()
data_formatada = f"{data_atual.day} de {meses[data_atual.month]} de {data_atual.year}"
nome_usuario = db.query(Usuario).get(session.get('user_id')).username
except:
data_formatada = datetime.now().strftime("%d/%m/%Y")
nome_usuario = "Usuário"
return render_template('home.html',
nome_usuario=nome_usuario,
data_atual=data_formatada,
total_militantes=db.query(Militante).count(),
total_cotas="0.00",
total_materiais=0,
total_assinaturas=0,
ultimos_militantes=[],
ultimos_pagamentos=[])
finally: finally:
db.close() db.close()
@@ -459,11 +397,25 @@ def nova_cota():
) )
db.add(cotas_mensais) db.add(cotas_mensais)
db.commit() db.commit()
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({
'status': 'success',
'message': 'Cota cadastrada com sucesso!'
})
flash('Cota cadastrada com sucesso!', 'success') flash('Cota cadastrada com sucesso!', 'success')
return redirect(url_for('listar_cotas')) return redirect(url_for('listar_cotas'))
except Exception as e: except Exception as e:
db.rollback() db.rollback()
print(f"Erro ao cadastrar cota: {e}") print(f"Erro ao cadastrar cota: {e}")
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({
'status': 'error',
'message': 'Erro ao cadastrar cota. Verifique os dados e tente novamente.'
}), 400
flash('Erro ao cadastrar cota', 'danger') flash('Erro ao cadastrar cota', 'danger')
return render_template("nova_cota.html") return render_template("nova_cota.html")
finally: finally:
@@ -502,45 +454,68 @@ def listar_cotas():
cota.status = "pendente" cota.status = "pendente"
print(f"Cota {cota.id}: Militante={cota.militante.nome}, Valor={cota.valor_novo}, Status={cota.status}") print(f"Cota {cota.id}: Militante={cota.militante.nome}, Valor={cota.valor_novo}, Status={cota.status}")
return render_template("listar_cotas.html", cotas=cotas) # Buscar militantes para o modal de nova cota
militantes = db.query(Militante).order_by(Militante.nome).all()
return render_template("listar_cotas.html", cotas=cotas, militantes=militantes)
except Exception as e: except Exception as e:
print(f"Erro ao listar cotas: {e}") print(f"Erro ao listar cotas: {e}")
flash('Erro ao listar cotas', 'danger') flash('Erro ao listar cotas', 'danger')
return render_template("listar_cotas.html", cotas=[]) return render_template("listar_cotas.html", cotas=[], militantes=[])
finally: finally:
db.close() db.close()
# Rota para editar cota # Rota para editar cota
@app.route("/cotas/editar/<int:id>", methods=["GET", "POST"]) @app.route('/cotas/editar/<int:id>', methods=['GET', 'POST'])
@login_required @login_required
@session_timeout @session_timeout
def editar_cota(id): def editar_cota(id):
db = get_db_connection() db = get_db_connection()
try: try:
cota = db.query(CotaMensal).filter_by(id=id).first() cota = db.query(CotaMensal).get_or_404(id)
if not cota:
flash('Cota não encontrada', 'danger')
return redirect(url_for('listar_cotas'))
if request.method == "POST": if request.method == 'POST':
valor_novo = float(request.form.get("valor_novo")) try:
data_vencimento = datetime.strptime(request.form.get("data_vencimento"), "%Y-%m-%d").date() cota.militante_id = int(request.form['militante_id'])
pago = request.form.get("pago") == "true" cota.valor_antigo = float(request.form['valor_antigo'])
cota.valor_novo = float(request.form['valor_novo'])
cota.data_alteracao = datetime.strptime(request.form['data_alteracao'], '%Y-%m-%d').date()
cota.data_vencimento = datetime.strptime(request.form['data_vencimento'], '%Y-%m-%d').date()
cota.valor_novo = valor_novo db.commit()
cota.data_vencimento = data_vencimento
cota.pago = pago
cota.data_alteracao = datetime.now().date()
db.commit() if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
flash('Cota atualizada com sucesso!', 'success') return jsonify({
return redirect(url_for('listar_cotas')) 'status': 'success',
'message': 'Cota atualizada com sucesso!'
})
flash('Cota atualizada com sucesso!', 'success')
return redirect(url_for('listar_cotas'))
except Exception as e:
db.rollback()
print(f"Erro ao atualizar cota: {e}")
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({
'status': 'error',
'message': 'Erro ao atualizar cota. Verifique os dados e tente novamente.'
}), 400
flash('Erro ao atualizar cota. Verifique os dados e tente novamente.', 'danger')
return redirect(url_for('editar_cota', id=id))
return render_template('editar_cota.html', cota=cota)
return render_template("editar_cota.html", cota=cota)
except Exception as e: except Exception as e:
db.rollback() print(f"Erro ao carregar cota: {e}")
print(f"Erro ao editar cota: {e}") if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
flash('Erro ao editar cota', 'danger') return jsonify({
'status': 'error',
'message': 'Erro ao carregar cota.'
}), 404
flash('Erro ao carregar cota', 'danger')
return redirect(url_for('listar_cotas')) return redirect(url_for('listar_cotas'))
finally: finally:
db.close() db.close()
@@ -605,39 +580,44 @@ def listar_pagamentos():
@require_login @require_login
@require_permission(Permission.VIEW_CELL_REPORTS) @require_permission(Permission.VIEW_CELL_REPORTS)
def novo_material(): def novo_material():
if request.method == "POST":
militante_id = request.form.get("militante_id")
tipo_material_id = request.form.get("tipo_material_id")
descricao = request.form.get("descricao")
valor = float(request.form.get("valor"))
data_venda = datetime.strptime(request.form.get("data_venda"), "%Y-%m-%d").date()
db = get_db_connection()
try:
materiais_vendidos = MaterialVendido(
militante_id=militante_id,
tipo_material_id=tipo_material_id,
descricao=descricao,
valor=valor,
data_venda=data_venda
)
db.add(materiais_vendidos)
db.commit()
flash('Material vendido cadastrado com sucesso!', 'success')
return redirect(url_for('listar_materiais'))
except Exception as e:
db.rollback()
print(f"Erro ao cadastrar material vendido: {e}")
flash('Erro ao cadastrar material vendido', 'danger')
return render_template("novo_material.html")
finally:
db.close()
db = get_db_connection() db = get_db_connection()
try: try:
militantes = db.query(Militante).order_by(Militante.nome).all() militante_id = request.form.get('militante_id')
tipos_material = db.query(TipoMaterial).order_by(TipoMaterial.descricao).all() tipo_material_id = request.form.get('tipo_material_id')
return render_template("novo_material.html", militantes=militantes, tipos_material=tipos_material) descricao = request.form.get('descricao')
valor = float(request.form.get('valor'))
data_venda = datetime.strptime(request.form.get('data_venda'), '%Y-%m-%d')
material = MaterialVendido(
militante_id=militante_id,
tipo_material_id=tipo_material_id,
descricao=descricao,
valor=valor,
data_venda=data_venda
)
db.add(material)
db.commit()
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({
'status': 'success',
'message': 'Material cadastrado com sucesso!'
})
flash('Material cadastrado com sucesso!', 'success')
return redirect(url_for('listar_materiais'))
except Exception as e:
db.rollback()
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({
'status': 'error',
'message': 'Erro ao cadastrar material. Por favor, tente novamente.'
}), 400
flash('Erro ao cadastrar material. Por favor, tente novamente.', 'error')
return redirect(url_for('listar_materiais'))
finally: finally:
db.close() db.close()
@@ -646,14 +626,15 @@ def novo_material():
@require_login @require_login
@require_permission(Permission.VIEW_CELL_REPORTS) @require_permission(Permission.VIEW_CELL_REPORTS)
def listar_materiais(): def listar_materiais():
db = get_db_connection()
try: try:
db = get_db_connection() materiais = db.query(MaterialVendido).join(Militante).join(TipoMaterial).all()
materiais = db.query(MaterialVendido).order_by(MaterialVendido.data_venda.desc()).all() militantes = db.query(Militante).all()
return render_template("listar_materiais.html", materiais=materiais) tipos_material = db.query(TipoMaterial).all()
except Exception as e: return render_template('listar_materiais.html',
print(f"Erro ao listar materiais: {e}") materiais=materiais,
flash('Erro ao listar materiais', 'danger') militantes=militantes,
return render_template("listar_materiais.html", materiais=[]) tipos_material=tipos_material)
finally: finally:
db.close() db.close()
@@ -662,36 +643,42 @@ def listar_materiais():
@require_login @require_login
@require_permission(Permission.VIEW_CELL_REPORTS) @require_permission(Permission.VIEW_CELL_REPORTS)
def nova_venda_jornal(): def nova_venda_jornal():
if request.method == "POST":
militante_id = request.form.get("militante_id")
quantidade = int(request.form.get("quantidade"))
valor_total = float(request.form.get("valor_total"))
data_venda = datetime.strptime(request.form.get("data_venda"), "%Y-%m-%d").date()
db = get_db_connection()
try:
vendas_jornais_avulsos = VendaJornalAvulso(
militante_id=militante_id,
quantidade=quantidade,
valor_total=valor_total,
data_venda=data_venda
)
db.add(vendas_jornais_avulsos)
db.commit()
flash('Venda de jornal cadastrada com sucesso!', 'success')
return redirect(url_for('listar_vendas_jornal'))
except Exception as e:
db.rollback()
print(f"Erro ao cadastrar venda de jornal: {e}")
flash('Erro ao cadastrar venda de jornal', 'danger')
return render_template("nova_venda_jornal.html")
finally:
db.close()
db = get_db_connection() db = get_db_connection()
try: try:
militantes = db.query(Militante).order_by(Militante.nome).all() militante_id = request.form.get('militante_id')
return render_template("nova_venda_jornal.html", militantes=militantes) quantidade = int(request.form.get('quantidade'))
valor_total = float(request.form.get('valor_total'))
data_venda = datetime.strptime(request.form.get('data_venda'), '%Y-%m-%d')
venda = VendaJornalAvulso(
militante_id=militante_id,
quantidade=quantidade,
valor_total=valor_total,
data_venda=data_venda
)
db.add(venda)
db.commit()
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({
'status': 'success',
'message': 'Venda cadastrada com sucesso!'
})
flash('Venda cadastrada com sucesso!', 'success')
return redirect(url_for('listar_vendas_jornal'))
except Exception as e:
db.rollback()
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({
'status': 'error',
'message': 'Erro ao cadastrar venda. Por favor, tente novamente.'
}), 400
flash('Erro ao cadastrar venda. Por favor, tente novamente.', 'error')
return redirect(url_for('listar_vendas_jornal'))
finally: finally:
db.close() db.close()
@@ -700,73 +687,13 @@ def nova_venda_jornal():
@require_login @require_login
@require_permission(Permission.VIEW_CELL_REPORTS) @require_permission(Permission.VIEW_CELL_REPORTS)
def listar_vendas_jornal(): def listar_vendas_jornal():
try:
db = get_db_connection()
vendas = db.query(VendaJornalAvulso).order_by(VendaJornalAvulso.data_venda.desc()).all()
return render_template("listar_vendas_jornal.html", vendas=vendas)
except Exception as e:
print(f"Erro ao listar vendas de jornal: {e}")
flash('Erro ao listar vendas de jornal', 'danger')
return render_template("listar_vendas_jornal.html", vendas=[])
finally:
db.close()
# Rota para criar uma nova assinatura
@app.route("/assinaturas/novo", methods=["GET", "POST"])
@require_login
@require_permission(Permission.VIEW_CELL_REPORTS)
def nova_assinatura():
if request.method == "POST":
militante_id = request.form.get("militante_id")
tipo_material_id = request.form.get("tipo_material_id")
quantidade = int(request.form.get("quantidade"))
valor_total = float(request.form.get("valor_total"))
data_inicio = datetime.strptime(request.form.get("data_inicio"), "%Y-%m-%d").date()
data_fim = datetime.strptime(request.form.get("data_fim"), "%Y-%m-%d").date()
db = get_db_connection()
try:
assinaturas_anuais = AssinaturaAnual(
militante_id=militante_id,
tipo_material_id=tipo_material_id,
quantidade=quantidade,
valor_total=valor_total,
data_inicio=data_inicio,
data_fim=data_fim
)
db.add(assinaturas_anuais)
db.commit()
flash('Assinatura cadastrada com sucesso!', 'success')
return redirect(url_for('listar_assinaturas'))
except Exception as e:
db.rollback()
print(f"Erro ao cadastrar assinatura: {e}")
flash('Erro ao cadastrar assinatura', 'danger')
return render_template("nova_assinatura.html")
finally:
db.close()
db = get_db_connection() db = get_db_connection()
try: try:
militantes = db.query(Militante).order_by(Militante.nome).all() vendas = db.query(VendaJornalAvulso).join(Militante).all()
tipos_material = db.query(TipoMaterial).order_by(TipoMaterial.descricao).all() militantes = db.query(Militante).all()
return render_template("nova_assinatura.html", militantes=militantes, tipos_material=tipos_material) return render_template('listar_vendas_jornal.html',
finally: vendas=vendas,
db.close() militantes=militantes)
# Rota para listar assinaturas
@app.route("/assinaturas")
@require_login
@require_permission(Permission.VIEW_CELL_REPORTS)
def listar_assinaturas():
try:
db = get_db_connection()
assinaturas = db.query(AssinaturaAnual).order_by(AssinaturaAnual.data_inicio.desc()).all()
return render_template("listar_assinaturas.html", assinaturas=assinaturas)
except Exception as e:
print(f"Erro ao listar assinaturas: {e}")
flash('Erro ao listar assinaturas', 'danger')
return render_template("listar_assinaturas.html", assinaturas=[])
finally: finally:
db.close() db.close()
@@ -889,32 +816,80 @@ def editar_militante(id):
try: try:
militante = db.query(Militante).get(id) militante = db.query(Militante).get(id)
if not militante: if not militante:
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({'status': 'error', 'message': 'Militante não encontrado'}), 404
flash('Militante não encontrado', 'danger') flash('Militante não encontrado', 'danger')
return redirect(url_for('listar_militantes')) return redirect(url_for('listar_militantes'))
if request.method == "POST": if request.method == "POST":
militante.nome = request.form.get("nome") nome = request.form.get("nome")
militante.cpf = request.form.get("cpf") cpf = request.form.get("cpf")
militante.email = request.form.get("email") email = request.form.get("email")
militante.telefone = request.form.get("telefone") telefone = request.form.get("telefone")
militante.endereco = request.form.get("endereco") endereco = request.form.get("endereco")
militante.filiado = request.form.get("filiado") == "on" filiado = request.form.get("filiado") == "on"
if not validar_cpf(militante.cpf): # Validar CPF
if not validar_cpf(cpf):
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({'status': 'error', 'message': 'CPF inválido'}), 400
flash('CPF inválido', 'danger') flash('CPF inválido', 'danger')
return render_template("editar_militante.html", militante=militante) return render_template('editar_militante.html', militante=militante)
# Verificar se já existe outro militante com este CPF
militante_existente = db.query(Militante).filter(
Militante.cpf == cpf,
Militante.id != id
).first()
if militante_existente:
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({'status': 'error', 'message': 'CPF já cadastrado para outro militante'}), 400
flash('CPF já cadastrado para outro militante', 'danger')
return render_template('editar_militante.html', militante=militante)
try: try:
militante.nome = nome
militante.cpf = cpf
militante.email = email
militante.telefone = telefone
militante.endereco = endereco
militante.filiado = filiado
db.commit() db.commit()
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({
'status': 'success',
'message': 'Militante atualizado com sucesso',
'militante': {
'id': militante.id,
'nome': militante.nome,
'cpf': militante.cpf,
'email': militante.email,
'telefone': militante.telefone,
'endereco': militante.endereco,
'filiado': militante.filiado
}
})
flash('Militante atualizado com sucesso!', 'success') flash('Militante atualizado com sucesso!', 'success')
return redirect(url_for('listar_militantes')) return redirect(url_for('listar_militantes'))
except Exception as e: except Exception as e:
db.rollback() db.rollback()
print(f"Erro ao atualizar militante: {e}") print(f"Erro ao atualizar militante: {e}")
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({'status': 'error', 'message': 'Erro ao atualizar militante'}), 500
flash('Erro ao atualizar militante', 'danger') flash('Erro ao atualizar militante', 'danger')
return render_template("editar_militante.html", militante=militante) return render_template('editar_militante.html', militante=militante)
return render_template("editar_militante.html", militante=militante) return render_template('editar_militante.html', militante=militante)
except Exception as e:
print(f"Erro ao editar militante: {e}")
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({'status': 'error', 'message': 'Erro ao carregar militante'}), 500
flash('Erro ao carregar militante', 'danger')
return redirect(url_for('listar_militantes'))
finally: finally:
db.close() db.close()
@@ -1410,69 +1385,6 @@ def has_permission(value, permission):
"""Verifica se o valor contém a permissão especificada.""" """Verifica se o valor contém a permissão especificada."""
return bool(value & permission) return bool(value & permission)
def create_app():
app = Flask(__name__)
# ... existing code ...
# ... existing code ...
return app
def init_system():
"""Inicializa o sistema com todos os usuários necessários"""
print("Inicializando sistema...")
# Inicializar banco de dados
print("Inicializando banco de dados...")
init_database()
# Criar admin
create_admin()
# Criar usuários de teste
create_test_users()
# Verificar configuração
db = get_db_connection()
try:
# Verificar admin
admin = db.query(Usuario).filter_by(username='admin').first()
if admin:
print("\nAdmin configurado:")
print(f"Username: admin")
print(f"Senha: admin123")
if os.path.exists('admin_qr.png'):
print("OTP: Usando configuração existente do arquivo admin_qr.png")
else:
print("OTP: Nova configuração gerada")
# Verificar usuários de teste
test_users = ['aligner', 'tester', 'deployer']
for username in test_users:
user = db.query(Usuario).filter_by(username=username).first()
if user:
print(f"\nUsuário {username} configurado:")
print(f"Username: {username}")
print(f"Senha: Test123!@#")
if user.otp_secret:
print("OTP: Configurado")
else:
print("OTP: Não configurado")
finally:
db.close()
print("\nInstruções:")
print("1. Configure o OTP usando o QR code gerado")
print("2. Faça login com as credenciais fornecidas")
print("3. Altere a senha no primeiro login")
print("\nCredenciais:")
print("Admin:")
print("Username: admin")
print("Senha: admin123")
print("\nUsuários de teste:")
print("Username: aligner, tester, deployer")
print("Senha: Test123!@#")
@app.route('/militante/desligar/<int:id>', methods=['POST']) @app.route('/militante/desligar/<int:id>', methods=['POST'])
@login_required @login_required
def desligar_militante(id): def desligar_militante(id):
@@ -1566,6 +1478,69 @@ def session_timeout():
except Exception as e: except Exception as e:
print(f"Erro ao atualizar última atividade: {e}") print(f"Erro ao atualizar última atividade: {e}")
def create_app():
app = Flask(__name__)
# ... existing code ...
# ... existing code ...
return app
def init_system():
"""Inicializa o sistema com todos os usuários necessários"""
print("Inicializando sistema...")
# Inicializar banco de dados
print("Inicializando banco de dados...")
init_database()
# Criar admin
create_admin()
# Criar usuários de teste
create_test_users()
# Verificar configuração
db = get_db_connection()
try:
# Verificar admin
admin = db.query(Usuario).filter_by(username='admin').first()
if admin:
print("\nAdmin configurado:")
print(f"Username: admin")
print(f"Senha: admin123")
if os.path.exists('admin_qr.png'):
print("OTP: Usando configuração existente do arquivo admin_qr.png")
else:
print("OTP: Nova configuração gerada")
# Verificar usuários de teste
test_users = ['aligner', 'tester', 'deployer']
for username in test_users:
user = db.query(Usuario).filter_by(username=username).first()
if user:
print(f"\nUsuário {username} configurado:")
print(f"Username: {username}")
print(f"Senha: Test123!@#")
if user.otp_secret:
print("OTP: Configurado")
else:
print("OTP: Não configurado")
finally:
db.close()
print("\nInstruções:")
print("1. Configure o OTP usando o QR code gerado")
print("2. Faça login com as credenciais fornecidas")
print("3. Altere a senha no primeiro login")
print("\nCredenciais:")
print("Admin:")
print("Username: admin")
print("Senha: admin123")
print("\nUsuários de teste:")
print("Username: aligner, tester, deployer")
print("Senha: Test123!@#")
if __name__ == '__main__': if __name__ == '__main__':
init_system() init_system()
app.run(debug=True) app.run(debug=True)

View File

@@ -39,10 +39,14 @@ def get_db_connection():
session.commit() session.commit()
raise Exception("Sessão expirada. Por favor, faça login novamente.") raise Exception("Sessão expirada. Por favor, faça login novamente.")
# Testa a conexão usando text()
session.execute(text("SELECT 1"))
return session return session
except Exception as e: except Exception as e:
session.close() print(f"Erro ao conectar ao banco de dados: {str(e)}")
raise e if session:
session.close()
raise
def execute_query(query, params=None): def execute_query(query, params=None):
""" """

View File

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

View File

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

View File

@@ -4,113 +4,469 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{% endblock %} - Controles OCI</title> <title>{% block title %}{% endblock %} - Controles OCI</title>
{{ bootstrap.load_css() }}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css"> <!-- Bootstrap 5 CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css?v=1" rel="stylesheet">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap"> <!-- Font Awesome 6 -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css?v=1">
<link rel="icon" type="image/png" href="{{ url_for('static', filename='img/logo001-alpha.png') }}">
<style>
:root {
--primary-color: #dc3545;
--primary-light: #e35d6a;
--secondary-color: #6c757d;
--secondary-light: #868e96;
--success-color: #198754;
--danger-color: #dc3545;
--warning-color: #ffc107;
--info-color: #0dcaf0;
--background-gradient: linear-gradient(135deg, var(--primary-color) 40%, white 100%);
--navbar-stripe: 4px solid var(--primary-color);
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f8f9fa;
margin: 0;
padding: 0;
}
.navbar {
background: #343a40 !important;
padding: 0.5rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
border-bottom: var(--navbar-stripe);
}
.navbar > .container-fluid {
width: 100%;
max-width: 1140px;
margin: 0 auto;
padding: 0.5rem 1rem;
}
@media (max-width: 1200px) {
.navbar > .container-fluid {
max-width: 960px;
}
}
@media (max-width: 992px) {
.navbar > .container-fluid {
max-width: 720px;
}
}
@media (max-width: 768px) {
.navbar > .container-fluid {
max-width: 540px;
padding: 0.5rem;
}
}
@media (max-width: 576px) {
.navbar > .container-fluid {
width: 100%;
padding: 0.5rem;
}
}
.navbar-brand {
font-weight: bold;
color: var(--primary-color) !important;
display: flex;
align-items: center;
margin-right: 3rem;
white-space: nowrap;
}
.navbar-brand img {
height: 40px;
margin-right: 10px;
}
#navbarNav {
display: flex;
justify-content: center;
flex-grow: 1;
}
.navbar-nav.mx-auto {
margin: 0 auto !important;
}
.navbar-nav:last-child {
margin-left: auto;
}
.nav-link {
color: var(--secondary-color) !important;
transition: color 0.3s ease;
padding: 0.5rem 1rem;
white-space: nowrap;
}
.nav-link:hover {
color: var(--primary-color) !important;
}
.nav-link.active {
color: var(--primary-color) !important;
}
.dropdown-menu {
border: none;
box-shadow: 0 0.5rem 1rem rgba(0,0,0,0.15);
}
.dropdown-item {
padding: 0.5rem 1.5rem;
transition: background-color 0.2s ease;
}
.dropdown-item:hover {
background-color: #f8f9fa;
}
.dropdown-item i {
margin-right: 0.5rem;
width: 1.25rem;
text-align: center;
}
.container {
max-width: 1320px;
margin: 0 auto;
padding: 0 1rem;
}
@media (max-width: 1200px) {
.container {
max-width: 960px;
}
}
@media (max-width: 992px) {
.container {
max-width: 720px;
}
}
@media (max-width: 768px) {
.container {
max-width: 540px;
}
}
@media (max-width: 576px) {
.container {
width: 100%;
padding: 0.5rem;
}
}
/* Cards da Dashboard */
.card {
border: none;
border-radius: 0.5rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
height: 100%;
}
.card-header {
background-color: #f8f9fa;
border-bottom: 1px solid #e9ecef;
padding: 1rem;
}
.card-header .card-title {
margin: 0;
display: flex;
align-items: center;
gap: 0.5rem;
}
.card-header h5 {
margin: 0;
display: flex;
align-items: center;
font-size: 1.1rem;
}
.card-header h5 i {
margin-right: 0.75rem;
color: var(--primary-color);
}
.card-body {
padding: 1.5rem;
}
.card-footer {
background: none;
border-top: 1px solid rgba(0,0,0,0.05);
padding: 1rem 1.5rem;
}
/* Estatísticas da Dashboard */
.stats-card {
position: relative;
padding: 1.5rem;
border-radius: 0.5rem;
color: white;
box-shadow: 0 0.25rem 0.5rem rgba(0,0,0,0.1);
transition: transform 0.2s ease, box-shadow 0.2s ease;
overflow: hidden;
min-height: 140px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.stats-card:hover {
transform: translateY(-3px);
box-shadow: 0 0.5rem 1rem rgba(0,0,0,0.15);
}
.stats-card.blue {
background: linear-gradient(45deg, var(--primary-color), var(--primary-light));
}
.stats-card.green {
background: linear-gradient(45deg, #1cc88a, #13855c);
}
.stats-card.cyan {
background: linear-gradient(45deg, #36b9cc, #258391);
}
.stats-card.yellow {
background: linear-gradient(45deg, #f6c23e, #dda20a);
}
.stats-card .title {
font-size: 0.9rem;
font-weight: bold;
text-transform: uppercase;
margin-bottom: 0.5rem;
}
.stats-card .value {
font-size: 2rem;
font-weight: bold;
margin-bottom: 0.5rem;
}
.stats-card .icon {
position: absolute;
right: 1rem;
top: 1rem;
font-size: 2rem;
opacity: 0.2;
}
/* Tabelas */
.table {
margin-bottom: 0;
}
.table th {
border-top: none;
background-color: #f8f9fa;
font-weight: 600;
text-transform: uppercase;
font-size: 0.8rem;
letter-spacing: 0.5px;
}
.table td {
vertical-align: middle;
}
/* Botões */
.btn {
font-weight: 500;
padding: 0.5rem 1rem;
border-radius: 0.25rem;
transition: all 0.2s ease;
}
.btn-primary {
background-color: var(--primary-color);
border-color: var(--primary-color);
}
.btn-primary:hover {
background-color: var(--primary-light);
border-color: var(--primary-light);
}
.btn-outline-primary {
color: var(--primary-color);
border-color: var(--primary-color);
}
.btn-outline-primary:hover {
background-color: var(--primary-color);
border-color: var(--primary-color);
}
/* Alertas */
.alert {
border: none;
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 1rem;
}
.alert-success {
background-color: #d4edda;
color: #155724;
}
.alert-danger {
background-color: #f8d7da;
color: #721c24;
}
.alert-warning {
background-color: #fff3cd;
color: #856404;
}
.alert-info {
background-color: #d1ecf1;
color: #0c5460;
}
/* Formulários */
.form-control {
border-radius: 0.25rem;
border: 1px solid #ced4da;
padding: 0.5rem 0.75rem;
}
.form-control:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
}
.form-label {
font-weight: 500;
margin-bottom: 0.5rem;
}
/* Modais */
.modal-content {
border: none;
border-radius: 0.5rem;
box-shadow: 0 0.5rem 1rem rgba(0,0,0,0.15);
}
.modal-header {
border-bottom: 1px solid #e9ecef;
padding: 1rem 1.5rem;
}
.modal-body {
padding: 1.5rem;
}
.modal-footer {
border-top: 1px solid #e9ecef;
padding: 1rem 1.5rem;
}
/* Responsividade */
@media (max-width: 768px) {
.navbar-brand {
margin-right: 1rem;
}
.navbar-brand img {
height: 30px;
}
.nav-link {
padding: 0.5rem;
}
.stats-card {
min-height: 120px;
}
.stats-card .value {
font-size: 1.5rem;
}
.stats-card .icon {
font-size: 1.5rem;
}
}
</style>
{% block extra_css %}{% endblock %} {% block extra_css %}{% endblock %}
</head> </head>
<body> <body>
{% if current_user is defined and current_user.is_authenticated %} <!-- Navbar -->
<nav class="navbar navbar-expand-lg navbar-dark"> <nav class="navbar navbar-expand-lg navbar-dark">
<div class="container"> <div class="container-fluid">
<a class="navbar-brand" href="{{ url_for('home') }}"> <a class="navbar-brand" href="{{ url_for('home') }}">
<img src="{{ url_for('static', filename='img/logo002-alpha.png') }}" alt="Logo OCI" class="navbar-logo"> <img src="{{ url_for('static', filename='img/logo.png') }}" alt="Logo">
<span>Controles OCI</span> Controles OCI
</a> </a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
<div class="collapse navbar-collapse" id="navbarNav"> <div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav mx-auto"> <ul class="navbar-nav mx-auto">
{% if current_user.has_permission('view_cell_data') %} <li class="nav-item">
<li class="nav-item dropdown"> <a class="nav-link {% if request.endpoint == 'home' %}active{% endif %}" href="{{ url_for('home') }}">
<a class="nav-link dropdown-toggle" href="#" id="militantesDropdown" role="button" data-bs-toggle="dropdown"> <i class="fas fa-home"></i> Início
<i class="fas fa-users me-1"></i>Militantes
</a> </a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="{{ url_for('listar_militantes') }}">Listar Militantes</a></li>
<li><a class="dropdown-item" href="{{ url_for('novo_militante') }}">Novo Militante</a></li>
</ul>
</li> </li>
{% endif %} <li class="nav-item">
<a class="nav-link {% if request.endpoint == 'listar_militantes' %}active{% endif %}" href="{{ url_for('listar_militantes') }}">
{% if current_user.has_permission('view_cell_reports') %} <i class="fas fa-users"></i> Militantes
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="financeiroDropdown" role="button" data-bs-toggle="dropdown">
<i class="fas fa-dollar-sign me-1"></i>Financeiro
</a> </a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="{{ url_for('listar_cotas') }}">Cotas</a></li>
<li><a class="dropdown-item" href="{{ url_for('listar_pagamentos') }}">Pagamentos</a></li>
</ul>
</li> </li>
<li class="nav-item dropdown"> <li class="nav-item">
<a class="nav-link dropdown-toggle" href="#" id="materiaisDropdown" role="button" data-bs-toggle="dropdown"> <a class="nav-link {% if request.endpoint == 'listar_materiais' %}active{% endif %}" href="{{ url_for('listar_materiais') }}">
<i class="fas fa-book me-1"></i>Materiais <i class="fas fa-box"></i> Materiais
</a> </a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="{{ url_for('listar_materiais') }}">Listar Materiais</a></li>
<li><a class="dropdown-item" href="{{ url_for('listar_vendas_jornal') }}">Vendas de Jornal</a></li>
<li><a class="dropdown-item" href="{{ url_for('listar_assinaturas') }}">Assinaturas</a></li>
</ul>
</li> </li>
{% endif %} <li class="nav-item">
<a class="nav-link {% if request.endpoint == 'listar_pagamentos' %}active{% endif %}" href="{{ url_for('listar_pagamentos') }}">
{% if current_user.has_permission('view_cell_reports') or current_user.has_permission('view_sector_reports') or current_user.has_permission('view_cr_reports') %} <i class="fas fa-money-bill"></i> Pagamentos
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="relatoriosDropdown" role="button" data-bs-toggle="dropdown">
<i class="fas fa-chart-bar me-1"></i>Relatórios
</a> </a>
<ul class="dropdown-menu">
{% if current_user.has_permission('view_cell_reports') %}
<li><a class="dropdown-item" href="{{ url_for('listar_relatorios_cotas') }}">Relatórios de Cotas</a></li>
<li><a class="dropdown-item" href="{{ url_for('listar_relatorios_vendas') }}">Relatórios de Vendas</a></li>
{% endif %}
</ul>
</li> </li>
{% endif %}
</ul> </ul>
<ul class="navbar-nav"> <ul class="navbar-nav">
{% if current_user.is_admin %} <li class="nav-item dropdown">
<li class="nav-item"> <a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-bs-toggle="dropdown">
<a class="nav-link" href="{{ url_for('dashboard_admin') }}"> <i class="fas fa-user"></i> {{ current_user.username }}
<i class="fas fa-user-shield me-1"></i>Dashboard Admin
</a>
</li>
{% endif %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('logout') }}">
<i class="fas fa-sign-out-alt me-1"></i>Sair
</a> </a>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<a class="dropdown-item" href="{{ url_for('logout') }}">
<i class="fas fa-sign-out-alt"></i> Sair
</a>
</li>
</ul>
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
</nav> </nav>
{% endif %}
<!-- Conteúdo -->
<div class="container mt-4"> <div class="container mt-4">
{% 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 %}
{% if not (category == 'success' and message == 'Login realizado com sucesso!') %} <div class="alert alert-{{ category }} alert-dismissible fade show">
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{% if category == 'success' %}
<i class="fas fa-check-circle"></i>
{% elif category == 'danger' %}
<i class="fas fa-exclamation-circle"></i>
{% elif category == 'warning' %}
<i class="fas fa-exclamation-triangle"></i>
{% elif category == 'info' %}
<i class="fas fa-info-circle"></i>
{% endif %}
{{ message }} {{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div> </div>
{% endif %}
{% endfor %} {% endfor %}
{% endif %} {% endif %}
{% endwith %} {% endwith %}
@@ -118,47 +474,34 @@
{% block content %}{% endblock %} {% block content %}{% endblock %}
</div> </div>
{{ bootstrap.load_js() }} <!-- Scripts -->
<script src="{{ url_for('static', filename='js/main.js') }}"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="{{ url_for('static', filename='js/forms.js') }}"></script>
{% block extra_js %}{% endblock %}
<script> <script>
// Auto-dismiss para alertas após 5 segundos // Auto-dismiss alerts after 5 seconds
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const alerts = document.querySelectorAll('.alert'); setTimeout(function() {
alerts.forEach(function(alert) { var alerts = document.querySelectorAll('.alert');
setTimeout(function() { alerts.forEach(function(alert) {
if (alert) { var bsAlert = new bootstrap.Alert(alert);
bootstrap.Alert.getOrCreateInstance(alert).close(); bsAlert.close();
} });
}, 5000); }, 5000);
});
}); });
{% if current_user is defined and current_user.is_authenticated %} // Check session timeout
// Verificar a sessão a cada 5 minutos
function checkSession() { function checkSession() {
fetch('/check_session') fetch('/check_session')
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
if (data.expired) { if (data.status !== 'active') {
window.location.href = '/login'; window.location.href = '/login';
} }
}) });
.catch(error => console.error('Erro ao verificar sessão:', error));
} }
// Verificar a cada 5 minutos // Check session every minute
setInterval(checkSession, 5 * 60 * 1000); setInterval(checkSession, 60000);
// Verificar também quando a página ganha foco
document.addEventListener('visibilitychange', function() {
if (document.visibilityState === 'visible') {
checkSession();
}
});
{% endif %}
</script> </script>
{% block scripts %}{% endblock %}
</body> </body>
</html> </html>

View File

@@ -0,0 +1,60 @@
{% extends "base.html" %}
{% block title %}Editar Pagamento{% endblock %}
{% block content %}
<div class="container mt-4">
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-edit me-2"></i>Editar Pagamento
</h5>
</div>
<div class="card-body">
<form method="post">
<div class="row">
<div class="col-md-6 mb-3">
<label for="valor" class="form-label">Valor</label>
<div class="input-group">
<span class="input-group-text">R$</span>
<input type="number" step="0.01" class="form-control" id="valor" name="valor" value="{{ pagamento.valor }}" required>
</div>
</div>
<div class="col-md-6 mb-3">
<label for="data_pagamento" class="form-label">Data do Pagamento</label>
<input type="date" class="form-control" id="data_pagamento" name="data_pagamento" value="{{ pagamento.data_pagamento.strftime('%Y-%m-%d') }}" required>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="tipo_pagamento" class="form-label">Tipo de Pagamento</label>
<select class="form-select" id="tipo_pagamento" name="tipo_pagamento" required>
{% for tipo in tipos_pagamento %}
<option value="{{ tipo.id }}" {% if tipo.id == pagamento.tipo_pagamento_id %}selected{% endif %}>
{{ tipo.nome }}
</option>
{% endfor %}
</select>
</div>
<div class="col-md-6 mb-3">
<label for="observacao" class="form-label">Observação</label>
<textarea class="form-control" id="observacao" name="observacao" rows="3">{{ pagamento.observacao }}</textarea>
</div>
</div>
<div class="d-flex justify-content-end gap-2">
<a href="{{ url_for('listar_pagamentos') }}" class="btn btn-secondary">
<i class="fas fa-times me-2"></i>Cancelar
</a>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-2"></i>Salvar Alterações
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -13,159 +13,126 @@
</div> </div>
</div> </div>
{% if current_user.has_permission('view_cell_data') %} <!-- Cards de Estatísticas -->
<!-- Card de Militantes -->
<div class="col-md-6 col-lg-3"> <div class="col-md-6 col-lg-3">
<div class="card bg-primary text-white"> <div class="stats-card blue">
<div class="card-body"> <div class="title">Total de Militantes</div>
<div class="d-flex justify-content-between align-items-center"> <div class="value">{{ total_militantes }}</div>
<div> <a href="{{ url_for('listar_militantes') }}" class="link">
<h6 class="card-title mb-0">Total de Militantes</h6> Ver detalhes <i class="fas fa-arrow-right"></i>
<h2 class="my-2">{{ total_militantes }}</h2> </a>
</div> <div class="icon">
<div class="fs-1"> <i class="fas fa-users"></i>
<i class="fas fa-users"></i>
</div>
</div>
<a href="{{ url_for('listar_militantes') }}" class="text-white text-decoration-none">
Ver detalhes <i class="fas fa-arrow-right ms-1"></i>
</a>
</div>
</div>
</div>
{% endif %}
{% if current_user.has_permission('view_cell_reports') %}
<!-- Card de Cotas -->
<div class="col-md-6 col-lg-3">
<div class="card bg-success text-white">
<div class="card-body">
<h6 class="card-title mb-2">Total de Cotas</h6>
<div class="d-flex justify-content-between align-items-start">
<div class="valor-container">
<h2 class="valor-cota mb-0">R$ {{ total_cotas }}</h2>
</div>
<div class="icon-container">
<i class="fas fa-dollar-sign"></i>
</div>
</div>
<a href="{{ url_for('listar_cotas') }}" class="text-white text-decoration-none mt-2 d-block">
Ver detalhes <i class="fas fa-arrow-right ms-1"></i>
</a>
</div> </div>
</div> </div>
</div> </div>
<!-- Card de Materiais -->
<div class="col-md-6 col-lg-3"> <div class="col-md-6 col-lg-3">
<div class="card bg-info text-white"> <div class="stats-card green">
<div class="card-body"> <div class="title">Total de Cotas</div>
<div class="d-flex justify-content-between align-items-center"> <div class="value">R$ {{ total_cotas }}</div>
<div> <a href="{{ url_for('listar_cotas') }}" class="link">
<h6 class="card-title mb-0">Materiais Vendidos</h6> Ver detalhes <i class="fas fa-arrow-right"></i>
<h2 class="my-2">{{ total_materiais }}</h2> </a>
</div> <div class="icon">
<div class="fs-1"> <i class="fas fa-dollar-sign"></i>
<i class="fas fa-book"></i>
</div>
</div>
<a href="{{ url_for('listar_materiais') }}" class="text-white text-decoration-none">
Ver detalhes <i class="fas fa-arrow-right ms-1"></i>
</a>
</div> </div>
</div> </div>
</div> </div>
<!-- Card de Assinaturas -->
<div class="col-md-6 col-lg-3"> <div class="col-md-6 col-lg-3">
<div class="card bg-warning text-white"> <div class="stats-card cyan">
<div class="card-body"> <div class="title">Materiais Vendidos</div>
<div class="d-flex justify-content-between align-items-center"> <div class="value">{{ total_materiais }}</div>
<div> <a href="{{ url_for('listar_materiais') }}" class="link">
<h6 class="card-title mb-0">Assinaturas Ativas</h6> Ver detalhes <i class="fas fa-arrow-right"></i>
<h2 class="my-2">{{ total_assinaturas }}</h2> </a>
</div> <div class="icon">
<div class="fs-1"> <i class="fas fa-book"></i>
<i class="fas fa-newspaper"></i> </div>
</div> </div>
</div> </div>
<a href="{{ url_for('listar_assinaturas') }}" class="text-white text-decoration-none">
Ver detalhes <i class="fas fa-arrow-right ms-1"></i> <div class="col-md-6 col-lg-3">
</a> <div class="stats-card yellow">
<div class="title">Assinaturas Ativas</div>
<div class="value">{{ total_assinaturas }}</div>
<a href="{{ url_for('listar_assinaturas') }}" class="link">
Ver detalhes <i class="fas fa-arrow-right"></i>
</a>
<div class="icon">
<i class="fas fa-newspaper"></i>
</div> </div>
</div> </div>
</div> </div>
{% endif %}
</div> </div>
<div class="row mt-4"> <div class="row mt-4">
{% if current_user.has_permission('view_cell_data') %}
<!-- Últimos Militantes --> <!-- Últimos Militantes -->
<div class="col-md-6 mb-4"> <div class="col-md-6 mb-4">
<div class="card h-100"> <div class="card h-100">
<div class="card-header"> <div class="card-header">
<h5 class="card-title mb-0"> <h5 class="card-title">
<i class="fas fa-user-plus me-2"></i>Últimos Militantes Cadastrados <i class="fas fa-user-plus"></i>Últimos Militantes Cadastrados
</h5> </h5>
</div> </div>
<div class="card-body"> <div class="card-body p-0">
{% if ultimos_militantes %} {% if ultimos_militantes %}
<div class="list-group list-group-flush"> <div class="list-group list-group-flush">
{% for militante in ultimos_militantes %} {% for militante in ultimos_militantes %}
<a href="#" class="list-group-item list-group-item-action" data-militante-id="{{ militante.id }}"> <div class="list-group-item" onclick="window.location='{{ url_for('editar_militante', id=militante.id) }}'">
<div class="d-flex w-100 justify-content-between"> <div class="militante-info">
<h6 class="mb-1">{{ militante.nome }}</h6> <h6 class="mb-1">{{ militante.nome }}</h6>
<small class="text-muted">#{{ militante.id }}</small> <small>{{ militante.email }}</small>
</div> </div>
<small class="text-muted">{{ militante.email }}</small> <i class="fas fa-chevron-right text-muted"></i>
</a>
{% endfor %}
</div>
{% else %}
<p class="text-muted mb-0">Nenhum militante cadastrado recentemente.</p>
{% endif %}
</div>
</div>
</div>
{% endif %}
{% if current_user.has_permission('view_cell_reports') %}
<!-- Últimos Pagamentos -->
<div class="col-md-6 mb-4">
<div class="card h-100">
<div class="card-header bg-light">
<h5 class="card-title mb-0">
<i class="fas fa-money-bill-wave me-2"></i>Últimos Pagamentos
</h5>
</div>
<div class="card-body">
{% if ultimos_pagamentos %}
<div class="list-group list-group-flush">
{% for pagamento in ultimos_pagamentos %}
<div class="list-group-item">
<div class="d-flex w-100 justify-content-between">
<h6 class="mb-1">{{ pagamento.militante.nome }}</h6>
<span class="badge bg-success">R$ {{ "%.2f"|format(pagamento.valor) }}</span>
</div>
<small class="text-muted">{{ pagamento.data_pagamento.strftime('%d/%m/%Y') }}</small>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
{% else %} {% else %}
<p class="text-muted mb-0">Nenhum pagamento registrado recentemente.</p> <p class="text-muted m-3">Nenhum militante cadastrado recentemente.</p>
{% endif %}
</div>
</div>
</div>
<!-- Últimos Pagamentos -->
<div class="col-md-6 mb-4">
<div class="card h-100">
<div class="card-header">
<h5 class="card-title">
<i class="fas fa-money-bill-wave"></i>Últimos Pagamentos
</h5>
</div>
<div class="card-body p-0">
{% if ultimos_pagamentos %}
<div class="list-group list-group-flush">
{% for pagamento in ultimos_pagamentos %}
<div class="list-group-item" onclick="window.location='{{ url_for('editar_pagamento', id=pagamento.id) }}'">
<div class="militante-info">
<h6 class="mb-1">{{ pagamento.militante.nome }}</h6>
<small>{{ pagamento.data_pagamento.strftime('%d/%m/%Y') }}</small>
</div>
<span class="badge bg-success">R$ {{ "%.2f"|format(pagamento.valor) }}</span>
</div>
{% endfor %}
</div>
{% else %}
<p class="text-muted m-3">Nenhum pagamento registrado recentemente.</p>
{% endif %} {% endif %}
</div> </div>
</div> </div>
</div> </div>
{% endif %}
</div> </div>
<!-- Modal de Detalhes do Militante --> <!-- Modal de Detalhes do Militante -->
<div class="modal fade" id="militanteModal" tabindex="-1" aria-labelledby="militanteModalLabel" aria-hidden="true"> <div class="modal fade" id="militanteModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered"> <div class="modal-dialog modal-dialog-centered">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="militanteModalLabel">Detalhes do Militante</h5> <h5 class="modal-title">
<i class="fas fa-user me-2"></i>Detalhes do Militante
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Fechar"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Fechar"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
@@ -195,212 +162,193 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<a href="#" id="btnEditarMilitante" class="btn btn-primary" style="background-color: var(--bs-primary) !important; border-color: var(--bs-primary) !important; color: white !important;"> <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalEditarMilitante">
<i class="fas fa-edit me-1"></i>Editar <i class="fas fa-edit me-2"></i>Editar
</a>
<button type="button" id="btnExcluirMilitante" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#confirmDeleteModal">
<i class="fas fa-trash me-1"></i>Excluir
</button> </button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Fechar</button> <button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#deleteModal">
<i class="fas fa-trash me-2"></i>Excluir
</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Modal de Confirmação de Exclusão --> <!-- Modal de Edição -->
<div class="modal fade" id="confirmDeleteModal" tabindex="-1" aria-hidden="true"> <div class="modal fade" id="modalEditarMilitante" tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-user-edit me-2"></i>Editar Militante
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="formEditarMilitante" method="post">
<div class="row">
<div class="col-md-6 mb-3">
<label for="editNome" class="form-label">Nome:</label>
<input type="text" class="form-control" id="editNome" name="nome" required>
</div>
<div class="col-md-6 mb-3">
<label for="editCpf" class="form-label">CPF:</label>
<input type="text" class="form-control" id="editCpf" name="cpf" required>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="editEmail" class="form-label">Email:</label>
<input type="email" class="form-control" id="editEmail" name="email" required>
</div>
<div class="col-md-6 mb-3">
<label for="editTelefone" class="form-label">Telefone:</label>
<input type="text" class="form-control" id="editTelefone" name="telefone">
</div>
</div>
<div class="mb-3">
<label for="editEndereco" class="form-label">Endereço:</label>
<input type="text" class="form-control" id="editEndereco" name="endereco">
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="editFiliado" name="filiado">
<label class="form-check-label" for="editFiliado">Filiado</label>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" form="formEditarMilitante" class="btn btn-success">
<i class="fas fa-save me-2"></i>Salvar
</button>
</div>
</div>
</div>
</div>
<!-- Modal de Exclusão -->
<div class="modal fade" id="deleteModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered"> <div class="modal-dialog modal-dialog-centered">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">Confirmar Exclusão</h5> <h5 class="modal-title">Confirmar Exclusão</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Fechar"></button> <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<p>Tem certeza que deseja excluir o militante <strong id="militanteExcluirNome"></strong>?</p> <p>Tem certeza que deseja excluir este militante?</p>
<p class="text-danger mb-0">Esta ação não pode ser desfeita.</p> <p class="text-danger">Esta ação não pode ser desfeita.</p>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<form id="formExcluirMilitante" method="POST"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button> <button type="button" class="btn btn-danger" id="confirmDelete">
<button type="submit" class="btn btn-danger"> <i class="fas fa-trash me-2"></i>Excluir
<i class="fas fa-trash me-1"></i>Excluir </button>
</button>
</form>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endblock %}
<style> {% block scripts %}
.welcome-header {
background: linear-gradient(to right, var(--background-color), rgba(232, 0, 12, 0.05));
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 2rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
.welcome-header h2 {
font-size: 2rem;
margin-bottom: 0.5rem;
color: var(--primary-color);
}
.welcome-header h4 {
font-size: 1.2rem;
color: var(--secondary-color);
opacity: 0.8;
margin: 0;
}
.valor-container {
flex: 1;
min-width: 0; /* Permite que o texto quebre corretamente */
}
.valor-cota {
font-size: calc(1.2rem + 0.8vw);
line-height: 1.2;
word-break: break-word;
margin-right: 0.5rem;
}
.icon-container {
font-size: 1.5rem;
opacity: 0.8;
margin-left: 8px;
margin-top: 4px;
}
/* Estilo para o backdrop do modal com blur */
.modal-backdrop.show {
backdrop-filter: blur(5px);
background-color: rgba(0, 0, 0, 0.5);
}
/* Estilo para o modal */
.modal-content {
border: none;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
}
.modal-header {
background: linear-gradient(to right, var(--secondary-dark), var(--secondary-color));
color: white;
border-radius: 12px 12px 0 0;
border-bottom: none;
padding: 1rem;
}
.modal-body {
padding: 1.5rem;
}
.modal-footer {
border-top: 1px solid #eee;
padding: 1rem;
flex-wrap: wrap;
gap: 0.5rem;
}
/* Ajustes para mobile */
@media (max-width: 576px) {
.modal-footer {
justify-content: center;
}
.modal-footer .btn {
min-width: 120px;
}
.modal-body label {
font-size: 0.95rem;
}
}
.modal-body label {
color: var(--secondary-color);
font-size: 0.9rem;
}
.modal-body p {
font-size: 1rem;
color: var(--text-color);
}
/* Garantir que o modal de confirmação mantenha o backdrop quando aberto */
#confirmDeleteModal {
background-color: rgba(0, 0, 0, 0.1);
}
#confirmDeleteModal.show {
backdrop-filter: blur(5px);
}
/* Ajuste para garantir que o botão de editar fique azul */
.btn-primary,
.btn-primary:hover,
.btn-primary:focus,
.btn-primary:active {
background-color: var(--bs-primary) !important;
border-color: var(--bs-primary) !important;
color: white !important;
}
</style>
{% block extra_js %}
<script> <script>
document.addEventListener('DOMContentLoaded', function() { // Função para formatar CPF
// Dados dos militantes para o modal function formatCPF(cpf) {
const militantes = {{ ultimos_militantes|tojson|safe }}; return cpf.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, "$1.$2.$3-$4");
// Referência ao modal principal
const militanteModal = document.getElementById('militanteModal');
// Garantir que o backdrop seja removido ao fechar o modal
militanteModal.addEventListener('hidden.bs.modal', function () {
const backdrop = document.querySelector('.modal-backdrop');
if (backdrop) {
backdrop.remove();
}
document.body.classList.remove('modal-open');
document.body.style.overflow = '';
document.body.style.paddingRight = '';
});
// Função para abrir o modal com os dados do militante
function showMilitanteModal(militanteId) {
const militante = militantes.find(m => m.id === militanteId);
if (!militante) return;
// Preencher os dados no modal
document.getElementById('militanteNome').textContent = militante.nome;
document.getElementById('militanteCPF').textContent = militante.cpf;
document.getElementById('militanteEmail').textContent = militante.email;
document.getElementById('militanteTelefone').textContent = militante.telefone;
document.getElementById('militanteEndereco').textContent = militante.endereco;
document.getElementById('militanteFiliado').textContent = militante.filiado ? 'Filiado' : 'Não Filiado';
// Configurar botões
document.getElementById('btnEditarMilitante').href = `/militantes/editar/${militante.id}`;
document.getElementById('formExcluirMilitante').action = `/militantes/excluir/${militante.id}`;
document.getElementById('militanteExcluirNome').textContent = militante.nome;
// Abrir o modal
const modal = new bootstrap.Modal(document.getElementById('militanteModal'));
modal.show();
} }
// Adicionar evento de clique nos itens da lista // Função para formatar telefone
document.querySelectorAll('[data-militante-id]').forEach(item => { function formatPhone(phone) {
item.addEventListener('click', function(e) { return phone.replace(/(\d{2})(\d{5})(\d{4})/, "($1) $2-$3");
}
// Função para carregar detalhes do militante
function loadMilitanteDetails(id) {
fetch(`/api/militante/${id}`)
.then(response => response.json())
.then(data => {
document.getElementById('militanteNome').textContent = data.nome;
document.getElementById('militanteCPF').textContent = formatCPF(data.cpf);
document.getElementById('militanteEmail').textContent = data.email;
document.getElementById('militanteTelefone').textContent = formatPhone(data.telefone);
document.getElementById('militanteEndereco').textContent = data.endereco;
document.getElementById('militanteFiliado').textContent = data.filiado ? 'Filiado' : 'Não Filiado';
// Preencher formulário de edição
document.getElementById('editNome').value = data.nome;
document.getElementById('editCpf').value = data.cpf;
document.getElementById('editEmail').value = data.email;
document.getElementById('editTelefone').value = data.telefone;
document.getElementById('editEndereco').value = data.endereco;
document.getElementById('editFiliado').checked = data.filiado;
});
}
// Função para excluir militante
function deleteMilitante(id) {
fetch(`/api/militante/${id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
window.location.reload();
} else {
alert('Erro ao excluir militante: ' + data.message);
}
});
}
// Event listeners
document.addEventListener('DOMContentLoaded', function() {
// Máscara para CPF
const cpfInput = document.getElementById('editCpf');
cpfInput.addEventListener('input', function(e) {
let value = e.target.value.replace(/\D/g, '');
if (value.length > 11) value = value.slice(0, 11);
e.target.value = formatCPF(value);
});
// Máscara para telefone
const phoneInput = document.getElementById('editTelefone');
phoneInput.addEventListener('input', function(e) {
let value = e.target.value.replace(/\D/g, '');
if (value.length > 11) value = value.slice(0, 11);
e.target.value = formatPhone(value);
});
// Submit do formulário de edição
const formEditar = document.getElementById('formEditarMilitante');
formEditar.addEventListener('submit', function(e) {
e.preventDefault(); e.preventDefault();
const militanteId = parseInt(this.getAttribute('data-militante-id')); const formData = new FormData(formEditar);
showMilitanteModal(militanteId); const data = Object.fromEntries(formData.entries());
fetch('/api/militante/' + data.id, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(data => {
if (data.success) {
window.location.reload();
} else {
alert('Erro ao editar militante: ' + data.message);
}
});
});
// Confirmação de exclusão
const confirmDelete = document.getElementById('confirmDelete');
confirmDelete.addEventListener('click', function() {
const id = this.dataset.militanteId;
deleteMilitante(id);
}); });
}); });
});
</script> </script>
{% endblock %} {% endblock %}
{% endblock %}

View File

@@ -1,35 +1,551 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block title %}Início{% endblock %} {% block title %}Assinaturas{% endblock %}
{% block content %} {% block content %}
<h1>Assinaturas Anuais</h1> <div class="row mb-4">
<a href="{{ url_for('nova_assinatura') }}">Adicionar Nova Assinatura</a> <div class="col-12">
<table border="1"> <div class="d-flex justify-content-between align-items-center">
<thead> <h1 class="mb-0">
<tr> <i class="fas fa-file-signature me-2"></i>Assinaturas
<th>ID</th> </h1>
<th>Militante ID</th> <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalNovaAssinatura">
<th>Tipo Material</th> <i class="fas fa-plus me-2"></i>Nova Assinatura
<th>Quantidade</th> </button>
<th>Valor Total</th> </div>
<th>Data Início</th> </div>
<th>Data Fim</th> </div>
</tr>
</thead> <div class="card shadow-sm">
<tbody> <div class="card-body">
{% for assinatura in assinaturas %} <div class="row mb-4">
<tr> <div class="col-md-6">
<td>{{ assinatura.id }}</td> <div class="input-group">
<td>{{ assinatura.militante_id }}</td> <span class="input-group-text">
<td>{{ assinatura.tipo_material_id }}</td> <i class="fas fa-search"></i>
<td>{{ assinatura.quantidade }}</td> </span>
<td>R$ {{ assinatura.valor_total }}</td> <input type="text" class="form-control" id="searchInput" placeholder="Pesquisar assinaturas...">
<td>{{ assinatura.data_inicio }}</td> </div>
<td>{{ assinatura.data_fim }}</td> </div>
</tr> <div class="col-md-6 text-end">
{% endfor %} <button id="btnExportar" class="btn btn-outline-primary">
</tbody> <i class="fas fa-download me-2"></i>Exportar
</table> </button>
<a href="{{ url_for('home') }}">Home</a> </div>
</div>
<div class="table-responsive">
<table class="table table-hover" id="assinaturasTable">
<thead>
<tr>
<th data-sort="militante">Militante <i class="fas fa-sort"></i></th>
<th data-sort="tipo">Tipo de Material <i class="fas fa-sort"></i></th>
<th data-sort="quantidade">Quantidade <i class="fas fa-sort"></i></th>
<th data-sort="valor_total">Valor Total <i class="fas fa-sort"></i></th>
<th data-sort="data_inicio">Data de Início <i class="fas fa-sort"></i></th>
<th data-sort="data_fim">Data de Fim <i class="fas fa-sort"></i></th>
<th class="text-end">Ações</th>
</tr>
</thead>
<tbody>
{% for assinatura in assinaturas %}
<tr>
<td data-militante="{{ assinatura.militante.nome }}">{{ assinatura.militante.nome }}</td>
<td data-tipo="{{ assinatura.tipo_material.nome }}">{{ assinatura.tipo_material.nome }}</td>
<td data-quantidade="{{ assinatura.quantidade }}">{{ assinatura.quantidade }}</td>
<td data-valor_total="{{ assinatura.valor_total }}">R$ {{ "%.2f"|format(assinatura.valor_total) }}</td>
<td data-data_inicio="{{ assinatura.data_inicio }}">{{ assinatura.data_inicio.strftime('%d/%m/%Y') }}</td>
<td data-data_fim="{{ assinatura.data_fim }}">{{ assinatura.data_fim.strftime('%d/%m/%Y') }}</td>
<td class="text-end">
<div class="btn-group">
<button type="button"
class="btn btn-sm btn-outline-primary"
data-bs-toggle="modal"
data-bs-target="#modalEditarAssinatura"
data-assinatura-id="{{ assinatura.id }}"
data-assinatura-militante="{{ assinatura.militante_id }}"
data-assinatura-tipo="{{ assinatura.tipo_material_id }}"
data-assinatura-quantidade="{{ assinatura.quantidade }}"
data-assinatura-valor-total="{{ assinatura.valor_total }}"
data-assinatura-data-inicio="{{ assinatura.data_inicio.strftime('%Y-%m-%d') }}"
data-assinatura-data-fim="{{ assinatura.data_fim.strftime('%Y-%m-%d') }}"
title="Editar">
<i class="fas fa-edit"></i>
</button>
<button type="button"
class="btn btn-sm btn-outline-danger"
data-bs-toggle="modal"
data-bs-target="#deleteModal"
data-assinatura-id="{{ assinatura.id }}"
data-assinatura-info="{{ assinatura.militante.nome }} - {{ assinatura.tipo_material.nome }}"
title="Excluir">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Modal Nova Assinatura -->
<div class="modal fade" id="modalNovaAssinatura" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-plus me-2"></i>Nova Assinatura
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="formNovaAssinatura" method="post" action="{{ url_for('nova_assinatura') }}">
<div class="mb-3">
<label for="militante_id" class="form-label">Militante:</label>
<select class="form-select" id="militante_id" name="militante_id" required>
<option value="">Selecione um militante</option>
{% for militante in militantes %}
<option value="{{ militante.id }}">{{ militante.nome }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="tipo_material_id" class="form-label">Tipo de Material:</label>
<select class="form-select" id="tipo_material_id" name="tipo_material_id" required>
<option value="">Selecione um tipo</option>
{% for tipo in tipos_material %}
<option value="{{ tipo.id }}">{{ tipo.nome }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="quantidade" class="form-label">Quantidade:</label>
<input type="number" class="form-control" id="quantidade" name="quantidade" required>
</div>
<div class="mb-3">
<label for="valor_total" class="form-label">Valor Total:</label>
<input type="number" step="0.01" class="form-control" id="valor_total" name="valor_total" required>
</div>
<div class="mb-3">
<label for="data_inicio" class="form-label">Data de Início:</label>
<input type="date" class="form-control" id="data_inicio" name="data_inicio" required>
</div>
<div class="mb-3">
<label for="data_fim" class="form-label">Data de Fim:</label>
<input type="date" class="form-control" id="data_fim" name="data_fim" required>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" form="formNovaAssinatura" class="btn btn-success">
<i class="fas fa-save me-2"></i>Salvar
</button>
</div>
</div>
</div>
</div>
<!-- Modal de Edição -->
<div class="modal fade" id="modalEditarAssinatura" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-edit me-2"></i>Editar Assinatura
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="formEditarAssinatura" method="post">
<div class="mb-3">
<label for="editMilitante" class="form-label">Militante:</label>
<select class="form-select" id="editMilitante" name="militante_id" required>
<option value="">Selecione um militante</option>
{% for militante in militantes %}
<option value="{{ militante.id }}">{{ militante.nome }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="editTipo" class="form-label">Tipo de Material:</label>
<select class="form-select" id="editTipo" name="tipo_material_id" required>
<option value="">Selecione um tipo</option>
{% for tipo in tipos_material %}
<option value="{{ tipo.id }}">{{ tipo.nome }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="editQuantidade" class="form-label">Quantidade:</label>
<input type="number" class="form-control" id="editQuantidade" name="quantidade" required>
</div>
<div class="mb-3">
<label for="editValorTotal" class="form-label">Valor Total:</label>
<input type="number" step="0.01" class="form-control" id="editValorTotal" name="valor_total" required>
</div>
<div class="mb-3">
<label for="editDataInicio" class="form-label">Data de Início:</label>
<input type="date" class="form-control" id="editDataInicio" name="data_inicio" required>
</div>
<div class="mb-3">
<label for="editDataFim" class="form-label">Data de Fim:</label>
<input type="date" class="form-control" id="editDataFim" name="data_fim" required>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" form="formEditarAssinatura" class="btn btn-success">
<i class="fas fa-save me-2"></i>Salvar
</button>
</div>
</div>
</div>
</div>
<!-- Modal de Exclusão -->
<div class="modal fade" id="deleteModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Confirmar Exclusão</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>Tem certeza que deseja excluir a assinatura de <strong id="assinaturaInfo"></strong>?</p>
<p class="text-danger mb-0">Esta ação não pode ser desfeita.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<form action="" method="POST" id="deleteForm" class="d-inline">
<button type="submit" class="btn btn-danger">
<i class="fas fa-trash me-2"></i>Excluir
</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Configuração do modal de exclusão
const deleteModal = document.getElementById('deleteModal');
deleteModal.addEventListener('show.bs.modal', function(event) {
const button = event.relatedTarget;
const assinaturaId = button.getAttribute('data-assinatura-id');
const assinaturaInfo = button.getAttribute('data-assinatura-info');
document.getElementById('assinaturaInfo').textContent = assinaturaInfo;
document.getElementById('deleteForm').action = `/assinaturas/excluir/${assinaturaId}`;
});
// Envio do formulário de nova assinatura via AJAX
const formNovaAssinatura = document.getElementById('formNovaAssinatura');
formNovaAssinatura.addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
fetch(this.action, {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
// Fechar o modal
bootstrap.Modal.getInstance(document.getElementById('modalNovaAssinatura')).hide();
// Atualizar a lista
location.reload();
// Mostrar mensagem de sucesso
const alertDiv = document.createElement('div');
alertDiv.className = 'alert alert-success alert-dismissible fade show';
alertDiv.innerHTML = `
${data.message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.querySelector('.container').insertBefore(alertDiv, document.querySelector('.container').firstChild);
} else {
// Mostrar erro
const alertDiv = document.createElement('div');
alertDiv.className = 'alert alert-danger alert-dismissible fade show';
alertDiv.innerHTML = `
${data.message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.querySelector('.modal-body').insertBefore(alertDiv, formNovaAssinatura);
}
})
.catch(error => {
console.error('Erro:', error);
const alertDiv = document.createElement('div');
alertDiv.className = 'alert alert-danger alert-dismissible fade show';
alertDiv.innerHTML = `
Erro ao cadastrar assinatura. Tente novamente.
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.querySelector('.modal-body').insertBefore(alertDiv, formNovaAssinatura);
});
});
// Configuração do modal de edição
const modalEditarAssinatura = document.getElementById('modalEditarAssinatura');
modalEditarAssinatura.addEventListener('show.bs.modal', function(event) {
const button = event.relatedTarget;
const assinaturaId = button.getAttribute('data-assinatura-id');
// Preencher o formulário com os dados da assinatura
document.getElementById('editMilitante').value = button.getAttribute('data-assinatura-militante');
document.getElementById('editTipo').value = button.getAttribute('data-assinatura-tipo');
document.getElementById('editQuantidade').value = button.getAttribute('data-assinatura-quantidade');
document.getElementById('editValorTotal').value = button.getAttribute('data-assinatura-valor-total');
document.getElementById('editDataInicio').value = button.getAttribute('data-assinatura-data-inicio');
document.getElementById('editDataFim').value = button.getAttribute('data-assinatura-data-fim');
// Configurar a action do formulário
document.getElementById('formEditarAssinatura').action = `/assinaturas/editar/${assinaturaId}`;
});
// Envio do formulário de edição via AJAX
const formEditarAssinatura = document.getElementById('formEditarAssinatura');
formEditarAssinatura.addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
fetch(this.action, {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
// Fechar o modal
bootstrap.Modal.getInstance(modalEditarAssinatura).hide();
// Atualizar a lista
location.reload();
// Mostrar mensagem de sucesso
const alertDiv = document.createElement('div');
alertDiv.className = 'alert alert-success alert-dismissible fade show';
alertDiv.innerHTML = `
${data.message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.querySelector('.container').insertBefore(alertDiv, document.querySelector('.container').firstChild);
} else {
// Mostrar erro
const alertDiv = document.createElement('div');
alertDiv.className = 'alert alert-danger alert-dismissible fade show';
alertDiv.innerHTML = `
${data.message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.querySelector('.modal-body').insertBefore(alertDiv, formEditarAssinatura);
}
})
.catch(error => {
console.error('Erro:', error);
const alertDiv = document.createElement('div');
alertDiv.className = 'alert alert-danger alert-dismissible fade show';
alertDiv.innerHTML = `
Erro ao atualizar assinatura. Tente novamente.
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.querySelector('.modal-body').insertBefore(alertDiv, formEditarAssinatura);
});
});
// Limpar alertas quando os modais forem fechados
[modalEditarAssinatura, document.getElementById('modalNovaAssinatura')].forEach(modal => {
modal.addEventListener('hidden.bs.modal', function () {
const alerts = this.querySelectorAll('.alert');
alerts.forEach(alert => alert.remove());
});
});
// Pesquisa em tempo real
const searchInput = document.getElementById('searchInput');
searchInput.addEventListener('input', function() {
const searchTerm = this.value.toLowerCase();
const rows = document.querySelectorAll('#assinaturasTable tbody tr');
rows.forEach(row => {
const text = row.textContent.toLowerCase();
row.style.display = text.includes(searchTerm) ? '' : 'none';
});
});
// Ordenação
const headers = document.querySelectorAll('#assinaturasTable th[data-sort]');
headers.forEach(header => {
header.addEventListener('click', function() {
const column = this.getAttribute('data-sort');
const tbody = document.querySelector('#assinaturasTable tbody');
const rows = Array.from(tbody.querySelectorAll('tr'));
const isAsc = !this.classList.contains('sort-asc');
// Remover classes de ordenação de todos os headers
headers.forEach(h => {
h.classList.remove('sort-asc', 'sort-desc');
h.querySelector('i').className = 'fas fa-sort';
});
// Adicionar classe de ordenação ao header clicado
this.classList.add(isAsc ? 'sort-asc' : 'sort-desc');
this.querySelector('i').className = `fas fa-sort-${isAsc ? 'up' : 'down'}`;
// Ordenar linhas
rows.sort((a, b) => {
const aVal = a.querySelector(`td[data-${column}]`).getAttribute(`data-${column}`);
const bVal = b.querySelector(`td[data-${column}]`).getAttribute(`data-${column}`);
return isAsc ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal);
});
// Reposicionar linhas
rows.forEach(row => tbody.appendChild(row));
});
});
// Exportar para CSV
document.getElementById('btnExportar').addEventListener('click', function() {
const rows = document.querySelectorAll('#assinaturasTable tbody tr:not([style*="display: none"])');
const headers = ['Militante', 'Tipo de Material', 'Quantidade', 'Valor Total', 'Data de Início', 'Data de Fim'];
let csv = headers.join(',') + '\n';
rows.forEach(row => {
const cols = row.querySelectorAll('td');
const values = [
cols[0].textContent,
cols[1].textContent,
cols[2].textContent,
cols[3].textContent,
cols[4].textContent,
cols[5].textContent
].map(val => `"${val}"`);
csv += values.join(',') + '\n';
});
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.setAttribute('download', 'assinaturas.csv');
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});
});
</script>
{% endblock %}
{% block extra_css %}
<style>
/* Estilo para colunas ordenáveis */
th[data-sort] {
cursor: pointer;
user-select: none;
}
th[data-sort] i {
margin-left: 5px;
color: #ccc;
}
th[data-sort].sort-asc i,
th[data-sort].sort-desc i {
color: var(--primary-color);
}
/* Animação para linhas da tabela */
#assinaturasTable tbody tr {
transition: all 0.3s ease;
}
#assinaturasTable tbody tr:hover {
background-color: rgba(0,0,0,0.02);
transform: translateX(5px);
}
/* Estilo para botões de ação */
.btn-group .btn {
padding: 0.25rem 0.5rem;
}
.btn-group .btn i {
width: 16px;
text-align: center;
}
/* Responsividade */
@media (max-width: 768px) {
.btn-group {
display: flex;
margin-top: 1rem;
}
.btn-group .btn {
flex: 1;
}
}
/* Estilo para o backdrop com blur em todos os modais */
.modal-backdrop.show {
backdrop-filter: blur(8px);
background-color: rgba(0, 0, 0, 0.7);
}
/* Estilo para o botão de fechar dos modais */
.btn-close {
background-color: transparent;
padding: 0.5rem;
opacity: 0.8;
transition: opacity 0.2s;
filter: invert(1) grayscale(100%) brightness(200%);
}
.btn-close:hover {
opacity: 1;
background-color: transparent;
}
/* Estilo para modais */
.modal-content {
border: none;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
}
.modal-header {
background: linear-gradient(to right, var(--bs-gray-dark), var(--bs-gray));
color: white;
border-radius: 12px 12px 0 0;
border-bottom: none;
padding: 1rem;
}
.modal-body {
padding: 1.5rem;
}
.modal-footer {
border-top: 1px solid #eee;
padding: 1rem;
}
</style>
{% endblock %} {% endblock %}

View File

@@ -7,11 +7,11 @@
<div class="col-12"> <div class="col-12">
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<h1 class="mb-0"> <h1 class="mb-0">
<i class="fas fa-dollar-sign me-2"></i>Cotas <i class="fas fa-money-bill me-2"></i>Cotas
</h1> </h1>
<a href="{{ url_for('nova_cota') }}" class="btn btn-primary"> <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalNovaCota">
<i class="fas fa-plus me-2"></i>Nova Cota <i class="fas fa-plus me-2"></i>Nova Cota
</a> </button>
</div> </div>
</div> </div>
</div> </div>
@@ -27,23 +27,10 @@
<input type="text" class="form-control" id="searchInput" placeholder="Pesquisar cotas..."> <input type="text" class="form-control" id="searchInput" placeholder="Pesquisar cotas...">
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6 text-end">
<div class="d-flex justify-content-end gap-2"> <button id="btnExportar" class="btn btn-outline-primary">
<div class="btn-group"> <i class="fas fa-download me-2"></i>Exportar
<button type="button" class="btn btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown"> </button>
<i class="fas fa-filter me-1"></i>Filtrar
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" data-filter="todos">Todas</a></li>
<li><a class="dropdown-item" href="#" data-filter="pagas">Pagas</a></li>
<li><a class="dropdown-item" href="#" data-filter="pendentes">Pendentes</a></li>
<li><a class="dropdown-item" href="#" data-filter="atrasadas">Atrasadas</a></li>
</ul>
</div>
<button type="button" class="btn btn-outline-secondary" id="btnExportar">
<i class="fas fa-download me-1"></i>Exportar
</button>
</div>
</div> </div>
</div> </div>
@@ -52,30 +39,36 @@
<thead> <thead>
<tr> <tr>
<th data-sort="militante">Militante <i class="fas fa-sort"></i></th> <th data-sort="militante">Militante <i class="fas fa-sort"></i></th>
<th data-sort="valor">Valor <i class="fas fa-sort"></i></th> <th data-sort="valor_antigo">Valor Antigo <i class="fas fa-sort"></i></th>
<th data-sort="data_vencimento">Vencimento <i class="fas fa-sort"></i></th> <th data-sort="valor_novo">Valor Novo <i class="fas fa-sort"></i></th>
<th data-sort="status">Status <i class="fas fa-sort"></i></th> <th data-sort="data_alteracao">Data de Alteração <i class="fas fa-sort"></i></th>
<th data-sort="data_vencimento">Data de Vencimento <i class="fas fa-sort"></i></th>
<th class="text-end">Ações</th> <th class="text-end">Ações</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for cota in cotas %} {% for cota in cotas %}
<tr data-cota="{{ cota.id }}" data-status="{{ cota.status }}"> <tr>
<td data-militante="{{ cota.militante.nome }}">{{ cota.militante.nome }}</td> <td data-militante="{{ cota.militante.nome }}">{{ cota.militante.nome }}</td>
<td data-valor="{{ cota.valor_novo }}">R$ {{ "%.2f"|format(cota.valor_novo) }}</td> <td data-valor_antigo="{{ cota.valor_antigo }}">R$ {{ "%.2f"|format(cota.valor_antigo) }}</td>
<td data-valor_novo="{{ cota.valor_novo }}">R$ {{ "%.2f"|format(cota.valor_novo) }}</td>
<td data-data_alteracao="{{ cota.data_alteracao }}">{{ cota.data_alteracao.strftime('%d/%m/%Y') }}</td>
<td data-data_vencimento="{{ cota.data_vencimento }}">{{ cota.data_vencimento.strftime('%d/%m/%Y') }}</td> <td data-data_vencimento="{{ cota.data_vencimento }}">{{ cota.data_vencimento.strftime('%d/%m/%Y') }}</td>
<td data-status="{{ cota.status }}">
<span class="badge {{ 'bg-success' if cota.status == 'paga' else 'bg-warning' if cota.status == 'pendente' else 'bg-danger' }}">
{{ cota.status.title() }}
</span>
</td>
<td class="text-end"> <td class="text-end">
<div class="btn-group"> <div class="btn-group">
<a href="{{ url_for('editar_cota', id=cota.id) }}" <button type="button"
class="btn btn-sm btn-outline-primary" class="btn btn-sm btn-outline-primary"
title="Editar"> data-bs-toggle="modal"
data-bs-target="#modalEditarCota"
data-cota-id="{{ cota.id }}"
data-cota-militante="{{ cota.militante_id }}"
data-cota-valor-antigo="{{ cota.valor_antigo }}"
data-cota-valor-novo="{{ cota.valor_novo }}"
data-cota-data-alteracao="{{ cota.data_alteracao.strftime('%Y-%m-%d') }}"
data-cota-data-vencimento="{{ cota.data_vencimento.strftime('%Y-%m-%d') }}"
title="Editar">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</a> </button>
<button type="button" <button type="button"
class="btn btn-sm btn-outline-danger" class="btn btn-sm btn-outline-danger"
data-bs-toggle="modal" data-bs-toggle="modal"
@@ -92,29 +85,108 @@
</tbody> </tbody>
</table> </table>
</div> </div>
</div>
</div>
<div class="d-flex justify-content-between align-items-center mt-4"> <!-- Modal Nova Cota -->
<div class="text-muted"> <div class="modal fade" id="modalNovaCota" tabindex="-1">
Mostrando <span id="countCotas">{{ cotas|length }}</span> cotas <div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-plus me-2"></i>Nova Cota
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="formNovaCota" method="post" action="{{ url_for('nova_cota') }}">
<div class="mb-3">
<label for="militante_id" class="form-label">Militante:</label>
<select class="form-select" id="militante_id" name="militante_id" required>
<option value="">Selecione um militante</option>
{% for militante in militantes %}
<option value="{{ militante.id }}">{{ militante.nome }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="valor_antigo" class="form-label">Valor Antigo:</label>
<input type="number" step="0.01" class="form-control" id="valor_antigo" name="valor_antigo" required>
</div>
<div class="mb-3">
<label for="valor_novo" class="form-label">Valor Novo:</label>
<input type="number" step="0.01" class="form-control" id="valor_novo" name="valor_novo" required>
</div>
<div class="mb-3">
<label for="data_alteracao" class="form-label">Data de Alteração:</label>
<input type="date" class="form-control" id="data_alteracao" name="data_alteracao" required>
</div>
<div class="mb-3">
<label for="data_vencimento" class="form-label">Data de Vencimento:</label>
<input type="date" class="form-control" id="data_vencimento" name="data_vencimento" required>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" form="formNovaCota" class="btn btn-success">
<i class="fas fa-save me-2"></i>Salvar
</button>
</div> </div>
<nav aria-label="Navegação de páginas">
<ul class="pagination mb-0">
<li class="page-item disabled" id="prevPage">
<a class="page-link" href="#"><i class="fas fa-chevron-left"></i></a>
</li>
<li class="page-item active"><a class="page-link" href="#">1</a></li>
<li class="page-item"><a class="page-link" href="#">2</a></li>
<li class="page-item"><a class="page-link" href="#">3</a></li>
<li class="page-item" id="nextPage">
<a class="page-link" href="#"><i class="fas fa-chevron-right"></i></a>
</li>
</ul>
</nav>
</div> </div>
</div> </div>
</div> </div>
<!-- Modal de Confirmação de Exclusão --> <!-- Modal de Edição -->
<div class="modal fade" id="modalEditarCota" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-edit me-2"></i>Editar Cota
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="formEditarCota" method="post">
<div class="mb-3">
<label for="editMilitante" class="form-label">Militante:</label>
<select class="form-select" id="editMilitante" name="militante_id" required>
<option value="">Selecione um militante</option>
{% for militante in militantes %}
<option value="{{ militante.id }}">{{ militante.nome }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="editValorAntigo" class="form-label">Valor Antigo:</label>
<input type="number" step="0.01" class="form-control" id="editValorAntigo" name="valor_antigo" required>
</div>
<div class="mb-3">
<label for="editValorNovo" class="form-label">Valor Novo:</label>
<input type="number" step="0.01" class="form-control" id="editValorNovo" name="valor_novo" required>
</div>
<div class="mb-3">
<label for="editDataAlteracao" class="form-label">Data de Alteração:</label>
<input type="date" class="form-control" id="editDataAlteracao" name="data_alteracao" required>
</div>
<div class="mb-3">
<label for="editDataVencimento" class="form-label">Data de Vencimento:</label>
<input type="date" class="form-control" id="editDataVencimento" name="data_vencimento" required>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" form="formEditarCota" class="btn btn-success">
<i class="fas fa-save me-2"></i>Salvar
</button>
</div>
</div>
</div>
</div>
<!-- Modal de Exclusão -->
<div class="modal fade" id="deleteModal" tabindex="-1"> <div class="modal fade" id="deleteModal" tabindex="-1">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
@@ -153,49 +225,130 @@ document.addEventListener('DOMContentLoaded', function() {
document.getElementById('deleteForm').action = `/cotas/excluir/${cotaId}`; document.getElementById('deleteForm').action = `/cotas/excluir/${cotaId}`;
}); });
// Envio do formulário de nova cota via AJAX
const formNovaCota = document.getElementById('formNovaCota');
formNovaCota.addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
fetch(this.action, {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
// Fechar o modal
bootstrap.Modal.getInstance(document.getElementById('modalNovaCota')).hide();
// Atualizar a lista
location.reload();
// Mostrar mensagem de sucesso
const alertDiv = document.createElement('div');
alertDiv.className = 'alert alert-success alert-dismissible fade show';
alertDiv.innerHTML = `
${data.message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.querySelector('.container').insertBefore(alertDiv, document.querySelector('.container').firstChild);
} else {
// Mostrar erro
const alertDiv = document.createElement('div');
alertDiv.className = 'alert alert-danger alert-dismissible fade show';
alertDiv.innerHTML = `
${data.message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.querySelector('.modal-body').insertBefore(alertDiv, formNovaCota);
}
})
.catch(error => {
console.error('Erro:', error);
const alertDiv = document.createElement('div');
alertDiv.className = 'alert alert-danger alert-dismissible fade show';
alertDiv.innerHTML = `
Erro ao cadastrar cota. Tente novamente.
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.querySelector('.modal-body').insertBefore(alertDiv, formNovaCota);
});
});
// Configuração do modal de edição
const modalEditarCota = document.getElementById('modalEditarCota');
modalEditarCota.addEventListener('show.bs.modal', function(event) {
const button = event.relatedTarget;
const cotaId = button.getAttribute('data-cota-id');
const militanteId = button.getAttribute('data-cota-militante');
const valorAntigo = button.getAttribute('data-cota-valor-antigo');
const valorNovo = button.getAttribute('data-cota-valor-novo');
const dataAlteracao = button.getAttribute('data-cota-data-alteracao');
const dataVencimento = button.getAttribute('data-cota-data-vencimento');
const form = document.getElementById('formEditarCota');
form.action = `/cotas/editar/${cotaId}`;
document.getElementById('editMilitante').value = militanteId;
document.getElementById('editValorAntigo').value = valorAntigo;
document.getElementById('editValorNovo').value = valorNovo;
document.getElementById('editDataAlteracao').value = dataAlteracao;
document.getElementById('editDataVencimento').value = dataVencimento;
});
// Envio do formulário de edição via AJAX
const formEditarCota = document.getElementById('formEditarCota');
formEditarCota.addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
fetch(this.action, {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
// Fechar o modal
bootstrap.Modal.getInstance(document.getElementById('modalEditarCota')).hide();
// Atualizar a lista
location.reload();
} else {
alert(data.message || 'Erro ao editar cota');
}
})
.catch(error => {
console.error('Erro:', error);
alert('Erro ao editar cota');
});
});
// Limpar alertas quando os modais forem fechados
[modalEditarCota, document.getElementById('modalNovaCota')].forEach(modal => {
modal.addEventListener('hidden.bs.modal', function () {
const alerts = this.querySelectorAll('.alert');
alerts.forEach(alert => alert.remove());
});
});
// Pesquisa em tempo real // Pesquisa em tempo real
const searchInput = document.getElementById('searchInput'); const searchInput = document.getElementById('searchInput');
searchInput.addEventListener('input', function() { searchInput.addEventListener('input', function() {
const searchTerm = this.value.toLowerCase(); const searchTerm = this.value.toLowerCase();
const rows = document.querySelectorAll('#cotasTable tbody tr'); const rows = document.querySelectorAll('#cotasTable tbody tr');
let visibleCount = 0;
rows.forEach(row => { rows.forEach(row => {
const text = row.textContent.toLowerCase(); const text = row.textContent.toLowerCase();
const isVisible = text.includes(searchTerm); row.style.display = text.includes(searchTerm) ? '' : 'none';
row.style.display = isVisible ? '' : 'none';
if (isVisible) visibleCount++;
});
document.getElementById('countCotas').textContent = visibleCount;
});
// Filtros
const filterButtons = document.querySelectorAll('[data-filter]');
filterButtons.forEach(button => {
button.addEventListener('click', function(e) {
e.preventDefault();
const filter = this.getAttribute('data-filter');
const rows = document.querySelectorAll('#cotasTable tbody tr');
let visibleCount = 0;
rows.forEach(row => {
const status = row.getAttribute('data-status');
let isVisible = true;
if (filter === 'pagas') {
isVisible = status === 'paga';
} else if (filter === 'pendentes') {
isVisible = status === 'pendente';
} else if (filter === 'atrasadas') {
isVisible = status === 'atrasada';
}
row.style.display = isVisible ? '' : 'none';
if (isVisible) visibleCount++;
});
document.getElementById('countCotas').textContent = visibleCount;
}); });
}); });
@@ -233,7 +386,7 @@ document.addEventListener('DOMContentLoaded', function() {
// Exportar para CSV // Exportar para CSV
document.getElementById('btnExportar').addEventListener('click', function() { document.getElementById('btnExportar').addEventListener('click', function() {
const rows = document.querySelectorAll('#cotasTable tbody tr:not([style*="display: none"])'); const rows = document.querySelectorAll('#cotasTable tbody tr:not([style*="display: none"])');
const headers = ['Militante', 'Valor', 'Vencimento', 'Status']; const headers = ['Militante', 'Valor Antigo', 'Valor Novo', 'Data de Alteração', 'Data de Vencimento'];
let csv = headers.join(',') + '\n'; let csv = headers.join(',') + '\n';
rows.forEach(row => { rows.forEach(row => {
@@ -242,7 +395,8 @@ document.addEventListener('DOMContentLoaded', function() {
cols[0].textContent, cols[0].textContent,
cols[1].textContent, cols[1].textContent,
cols[2].textContent, cols[2].textContent,
cols[3].textContent.trim() cols[3].textContent,
cols[4].textContent
].map(val => `"${val}"`); ].map(val => `"${val}"`);
csv += values.join(',') + '\n'; csv += values.join(',') + '\n';
}); });
@@ -287,12 +441,6 @@ th[data-sort].sort-desc i {
transform: translateX(5px); transform: translateX(5px);
} }
/* Estilo para badges */
.badge {
font-weight: 500;
padding: 0.5em 0.8em;
}
/* Estilo para botões de ação */ /* Estilo para botões de ação */
.btn-group .btn { .btn-group .btn {
padding: 0.25rem 0.5rem; padding: 0.25rem 0.5rem;
@@ -314,5 +462,49 @@ th[data-sort].sort-desc i {
flex: 1; flex: 1;
} }
} }
/* Estilo para o backdrop com blur em todos os modais */
.modal-backdrop.show {
backdrop-filter: blur(8px);
background-color: rgba(0, 0, 0, 0.7);
}
/* Estilo para o botão de fechar dos modais */
.btn-close {
background-color: transparent;
padding: 0.5rem;
opacity: 0.8;
transition: opacity 0.2s;
filter: invert(1) grayscale(100%) brightness(200%);
}
.btn-close:hover {
opacity: 1;
background-color: transparent;
}
/* Estilo para modais */
.modal-content {
border: none;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
}
.modal-header {
background: linear-gradient(to right, var(--bs-gray-dark), var(--bs-gray));
color: white;
border-radius: 12px 12px 0 0;
border-bottom: none;
padding: 1rem;
}
.modal-body {
padding: 1.5rem;
}
.modal-footer {
border-top: 1px solid #eee;
padding: 1rem;
}
</style> </style>
{% endblock %} {% endblock %}

View File

@@ -1,58 +1,330 @@
{% extends 'base.html' %} {% extends "base.html" %}
{% block title %}Listar Materiais{% endblock %} {% block title %}Materiais{% endblock %}
{% block content %} {% block content %}
<div class="container"> <div class="row mb-4">
<div class="row"> <div class="col-12">
<div class="col-md-12"> <div class="d-flex justify-content-between align-items-center">
<h1 class="mb-4">Lista de Materiais</h1> <h1 class="mb-0">
<i class="fas fa-box me-2"></i>Materiais
</h1>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalNovoMaterial">
<i class="fas fa-plus me-2"></i>Novo Material
</button>
</div>
</div>
</div>
{% with messages = get_flashed_messages(with_categories=true) %} <div class="card shadow-sm">
{% if messages %} <div class="card-body">
{% for category, message in messages %} <div class="row mb-4">
<div class="alert alert-{{ category }}">{{ message }}</div> <div class="col-md-6">
{% endfor %} <div class="input-group">
{% endif %} <span class="input-group-text">
{% endwith %} <i class="fas fa-search"></i>
</span>
<div class="d-flex justify-content-between mb-4"> <input type="text" class="form-control" id="searchInput" placeholder="Pesquisar materiais...">
<a href="{{ url_for('novo_material') }}" class="btn btn-success">Novo Material</a> </div>
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
</div> </div>
<div class="col-md-6 text-end">
<button id="btnExportar" class="btn btn-outline-primary">
<i class="fas fa-download me-2"></i>Exportar
</button>
</div>
</div>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped table-hover"> <table class="table table-hover" id="materiaisTable">
<thead> <thead>
<tr> <tr>
<th>ID</th> <th data-sort="militante">Militante <i class="fas fa-sort"></i></th>
<th>Nome</th> <th data-sort="tipo">Tipo <i class="fas fa-sort"></i></th>
<th>Descrição</th> <th data-sort="descricao">Descrição <i class="fas fa-sort"></i></th>
<th>Preço</th> <th data-sort="valor">Valor <i class="fas fa-sort"></i></th>
<th>Quantidade</th> <th data-sort="data">Data <i class="fas fa-sort"></i></th>
<th>Tipo</th> <th class="text-end">Ações</th>
<th>Ações</th> </tr>
</tr> </thead>
</thead> <tbody>
<tbody> {% for material in materiais %}
{% for material in materiais %} <tr>
<tr> <td data-militante="{{ material.militante.nome }}">{{ material.militante.nome }}</td>
<td>{{ material.id }}</td> <td data-tipo="{{ material.tipo_material.nome }}">{{ material.tipo_material.nome }}</td>
<td>{{ material.nome }}</td> <td data-descricao="{{ material.descricao }}">{{ material.descricao }}</td>
<td>{{ material.descricao }}</td> <td data-valor="{{ material.valor }}">R$ {{ "%.2f"|format(material.valor) }}</td>
<td>R$ {{ "%.2f"|format(material.preco) }}</td> <td data-data="{{ material.data_venda }}">{{ material.data_venda.strftime('%d/%m/%Y') }}</td>
<td>{{ material.quantidade }}</td> <td class="text-end">
<td>{{ material.tipo.nome }}</td> <div class="btn-group">
<td> <button type="button"
<a href="{{ url_for('editar_material', id=material.id) }}" class="btn btn-primary btn-sm">Editar</a> class="btn btn-sm btn-outline-primary"
<a href="{{ url_for('deletar_material', id=material.id) }}" class="btn btn-danger btn-sm" onclick="return confirm('Tem certeza que deseja excluir este material?')">Excluir</a> data-bs-toggle="modal"
</td> data-bs-target="#modalEditarMaterial"
</tr> data-material-id="{{ material.id }}"
{% endfor %} data-material-militante="{{ material.militante_id }}"
</tbody> data-material-tipo="{{ material.tipo_material_id }}"
</table> data-material-descricao="{{ material.descricao }}"
data-material-valor="{{ material.valor }}"
data-material-data="{{ material.data_venda.strftime('%Y-%m-%d') }}"
title="Editar">
<i class="fas fa-edit"></i>
</button>
<button type="button"
class="btn btn-sm btn-outline-danger"
data-bs-toggle="modal"
data-bs-target="#deleteModal"
data-material-id="{{ material.id }}"
data-material-info="{{ material.militante.nome }} - {{ material.tipo_material.nome }}"
title="Excluir">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Modal Novo Material -->
<div class="modal fade" id="modalNovoMaterial" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-plus me-2"></i>Novo Material
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="formNovoMaterial" method="post" action="{{ url_for('novo_material') }}">
<div class="mb-3">
<label for="militante_id" class="form-label">Militante:</label>
<select class="form-select" id="militante_id" name="militante_id" required>
<option value="">Selecione um militante</option>
{% for militante in militantes %}
<option value="{{ militante.id }}">{{ militante.nome }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="tipo_material_id" class="form-label">Tipo de Material:</label>
<select class="form-select" id="tipo_material_id" name="tipo_material_id" required>
<option value="">Selecione um tipo</option>
{% for tipo in tipos_material %}
<option value="{{ tipo.id }}">{{ tipo.nome }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="descricao" class="form-label">Descrição:</label>
<input type="text" class="form-control" id="descricao" name="descricao" required>
</div>
<div class="mb-3">
<label for="valor" class="form-label">Valor:</label>
<input type="number" step="0.01" class="form-control" id="valor" name="valor" required>
</div>
<div class="mb-3">
<label for="data_venda" class="form-label">Data da Venda:</label>
<input type="date" class="form-control" id="data_venda" name="data_venda" required>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" form="formNovoMaterial" class="btn btn-success">
<i class="fas fa-save me-2"></i>Salvar
</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Modal de Edição -->
<div class="modal fade" id="modalEditarMaterial" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-edit me-2"></i>Editar Material
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="formEditarMaterial" method="post">
<div class="mb-3">
<label for="editMilitante" class="form-label">Militante:</label>
<select class="form-select" id="editMilitante" name="militante_id" required>
<option value="">Selecione um militante</option>
{% for militante in militantes %}
<option value="{{ militante.id }}">{{ militante.nome }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="editTipo" class="form-label">Tipo de Material:</label>
<select class="form-select" id="editTipo" name="tipo_material_id" required>
<option value="">Selecione um tipo</option>
{% for tipo in tipos_material %}
<option value="{{ tipo.id }}">{{ tipo.nome }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="editDescricao" class="form-label">Descrição:</label>
<input type="text" class="form-control" id="editDescricao" name="descricao" required>
</div>
<div class="mb-3">
<label for="editValor" class="form-label">Valor:</label>
<input type="number" step="0.01" class="form-control" id="editValor" name="valor" required>
</div>
<div class="mb-3">
<label for="editData" class="form-label">Data da Venda:</label>
<input type="date" class="form-control" id="editData" name="data_venda" required>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" form="formEditarMaterial" class="btn btn-success">
<i class="fas fa-save me-2"></i>Salvar
</button>
</div>
</div>
</div>
</div>
<!-- Modal de Confirmação de Exclusão -->
<div class="modal fade" id="deleteModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-exclamation-triangle me-2 text-danger"></i>Confirmar Exclusão
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>Tem certeza que deseja excluir o material <strong id="materialInfo"></strong>?</p>
<p class="text-danger mb-0">Esta ação não pode ser desfeita.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<form id="formDeleteMaterial" method="post" style="display: inline;">
<button type="submit" class="btn btn-danger">
<i class="fas fa-trash me-2"></i>Excluir
</button>
</form>
</div>
</div>
</div>
</div>
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Configuração da tabela
const table = document.getElementById('materiaisTable');
const searchInput = document.getElementById('searchInput');
const exportBtn = document.getElementById('btnExportar');
// Função de pesquisa
searchInput.addEventListener('input', function() {
const searchTerm = this.value.toLowerCase();
const rows = table.getElementsByTagName('tbody')[0].getElementsByTagName('tr');
Array.from(rows).forEach(row => {
const text = row.textContent.toLowerCase();
row.style.display = text.includes(searchTerm) ? '' : 'none';
});
});
// Função de ordenação
const headers = table.getElementsByTagName('th');
Array.from(headers).forEach(header => {
if (header.dataset.sort) {
header.addEventListener('click', () => {
const column = header.dataset.sort;
const tbody = table.getElementsByTagName('tbody')[0];
const rows = Array.from(tbody.getElementsByTagName('tr'));
rows.sort((a, b) => {
const aValue = a.querySelector(`td[data-${column}]`).dataset[column];
const bValue = b.querySelector(`td[data-${column}]`).dataset[column];
if (column === 'valor') {
return parseFloat(aValue) - parseFloat(bValue);
} else if (column === 'data') {
return new Date(aValue) - new Date(bValue);
}
return aValue.localeCompare(bValue);
});
if (header.classList.contains('asc')) {
rows.reverse();
header.classList.remove('asc');
header.classList.add('desc');
} else {
header.classList.remove('desc');
header.classList.add('asc');
}
tbody.innerHTML = '';
rows.forEach(row => tbody.appendChild(row));
});
}
});
// Configuração do modal de edição
const editModal = document.getElementById('modalEditarMaterial');
editModal.addEventListener('show.bs.modal', function(event) {
const button = event.relatedTarget;
const materialId = button.dataset.materialId;
const form = this.querySelector('form');
form.action = `/editar_material/${materialId}`;
document.getElementById('editMilitante').value = button.dataset.materialMilitante;
document.getElementById('editTipo').value = button.dataset.materialTipo;
document.getElementById('editDescricao').value = button.dataset.materialDescricao;
document.getElementById('editValor').value = button.dataset.materialValor;
document.getElementById('editData').value = button.dataset.materialData;
});
// Configuração do modal de exclusão
const deleteModal = document.getElementById('deleteModal');
deleteModal.addEventListener('show.bs.modal', function(event) {
const button = event.relatedTarget;
const materialId = button.dataset.materialId;
const materialInfo = button.dataset.materialInfo;
document.getElementById('materialInfo').textContent = materialInfo;
document.getElementById('formDeleteMaterial').action = `/deletar_material/${materialId}`;
});
// Configuração do botão de exportação
exportBtn.addEventListener('click', function() {
const rows = Array.from(table.getElementsByTagName('tbody')[0].getElementsByTagName('tr'));
const csv = [
['Militante', 'Tipo', 'Descrição', 'Valor', 'Data'],
...rows.map(row => [
row.cells[0].textContent,
row.cells[1].textContent,
row.cells[2].textContent,
row.cells[3].textContent,
row.cells[4].textContent
])
].map(row => row.join(',')).join('\n');
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = 'materiais.csv';
link.click();
});
});
</script>
{% endblock %}
{% endblock %} {% endblock %}

View File

@@ -10,9 +10,9 @@
<i class="fas fa-users me-2"></i>Militantes <i class="fas fa-users me-2"></i>Militantes
</h1> </h1>
{% if current_user.has_permission('gerenciar_militantes') %} {% if current_user.has_permission('gerenciar_militantes') %}
<a href="{{ url_for('criar_militante') }}" class="btn btn-primary"> <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalNovoMilitante">
<i class="fas fa-user-plus me-2"></i>Novo Militante <i class="fas fa-user-plus me-2"></i>Novo Militante
</a> </button>
{% endif %} {% endif %}
</div> </div>
</div> </div>
@@ -83,11 +83,20 @@
<td class="text-end"> <td class="text-end">
<div class="btn-group"> <div class="btn-group">
{% if current_user.has_permission('gerenciar_militantes') %} {% if current_user.has_permission('gerenciar_militantes') %}
<a href="{{ url_for('editar_militante', id=militante.id) }}" <button type="button"
class="btn btn-sm btn-outline-primary" class="btn btn-sm btn-outline-primary"
data-bs-toggle="modal"
data-bs-target="#modalEditarMilitante"
data-militante-id="{{ militante.id }}"
data-militante-nome="{{ militante.nome }}"
data-militante-cpf="{{ militante.cpf }}"
data-militante-email="{{ militante.email }}"
data-militante-telefone="{{ militante.telefone }}"
data-militante-endereco="{{ militante.endereco }}"
data-militante-filiado="{{ militante.filiado }}"
title="Editar"> title="Editar">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</a> </button>
<button type="button" <button type="button"
class="btn btn-sm btn-outline-danger" class="btn btn-sm btn-outline-danger"
data-bs-toggle="modal" data-bs-toggle="modal"
@@ -141,101 +150,464 @@
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<form action="" method="POST" id="deleteForm" class="d-inline"> <button type="button" class="btn btn-danger" id="confirmDelete">
<button type="submit" class="btn btn-danger"> <i class="fas fa-trash me-2"></i>Excluir
<i class="fas fa-trash me-2"></i>Excluir </button>
</button> </div>
</div>
</div>
</div>
<!-- Modal de Novo Militante -->
<div class="modal fade" id="modalNovoMilitante" tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-user-plus me-2"></i>Novo Militante
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="formNovoMilitante" method="post" action="{{ url_for('novo_militante') }}">
<div class="row">
<div class="col-md-6 mb-3">
<label for="nome" class="form-label">Nome:</label>
<input type="text" class="form-control" id="nome" name="nome" required>
</div>
<div class="col-md-6 mb-3">
<label for="cpf" class="form-label">CPF:</label>
<input type="text" class="form-control" id="cpf" name="cpf" required
pattern="\d{3}\.?\d{3}\.?\d{3}-?\d{2}"
title="Digite um CPF no formato: xxx.xxx.xxx-xx">
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="email" class="form-label">Email:</label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
<div class="col-md-6 mb-3">
<label for="telefone" class="form-label">Telefone:</label>
<input type="text" class="form-control" id="telefone" name="telefone">
</div>
</div>
<div class="mb-3">
<label for="endereco" class="form-label">Endereço:</label>
<input type="text" class="form-control" id="endereco" name="endereco">
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="filiado" name="filiado">
<label class="form-check-label" for="filiado">Filiado</label>
</div>
</form> </form>
</div> </div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" form="formNovoMilitante" class="btn btn-primary">
<i class="fas fa-save me-2"></i>Salvar
</button>
</div>
</div>
</div>
</div>
<!-- Modal de Edição -->
<div class="modal fade" id="modalEditarMilitante" tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-user-edit me-2"></i>Editar Militante
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="formEditarMilitante" method="post">
<div class="row">
<div class="col-md-6 mb-3">
<label for="editNome" class="form-label">Nome:</label>
<input type="text" class="form-control" id="editNome" name="nome" required>
</div>
<div class="col-md-6 mb-3">
<label for="editCpf" class="form-label">CPF:</label>
<input type="text" class="form-control" id="editCpf" name="cpf" required>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="editEmail" class="form-label">Email:</label>
<input type="email" class="form-control" id="editEmail" name="email" required>
</div>
<div class="col-md-6 mb-3">
<label for="editTelefone" class="form-label">Telefone:</label>
<input type="text" class="form-control" id="editTelefone" name="telefone">
</div>
</div>
<div class="mb-3">
<label for="editEndereco" class="form-label">Endereço:</label>
<input type="text" class="form-control" id="editEndereco" name="endereco">
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="editFiliado" name="filiado">
<label class="form-check-label" for="editFiliado">Filiado</label>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" form="formEditarMilitante" class="btn btn-primary">
<i class="fas fa-save me-2"></i>Salvar
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block extra_js %} {% block scripts %}
<script> <script>
document.addEventListener('DOMContentLoaded', function() { // Função para formatar CPF
// Configuração do modal de exclusão function formatCPF(cpf) {
const deleteModal = document.getElementById('deleteModal'); return cpf.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, "$1.$2.$3-$4");
deleteModal.addEventListener('show.bs.modal', function(event) { }
const button = event.relatedTarget;
const militanteId = button.getAttribute('data-militante-id');
const militanteNome = button.getAttribute('data-militante-nome');
document.getElementById('militanteNome').textContent = militanteNome; // Função para formatar telefone
document.getElementById('deleteForm').action = `/militantes/excluir/${militanteId}`; function formatPhone(phone) {
}); return phone.replace(/(\d{2})(\d{5})(\d{4})/, "($1) $2-$3");
}
// Pesquisa em tempo real // Função para carregar detalhes do militante
const searchInput = document.getElementById('searchInput'); function loadMilitanteDetails(id) {
searchInput.addEventListener('input', function() { fetch(`/api/militante/${id}`)
const searchTerm = this.value.toLowerCase(); .then(response => response.json())
const rows = document.querySelectorAll('#militantesTable tbody tr'); .then(data => {
let visibleCount = 0; document.getElementById('militanteNome').textContent = data.nome;
document.getElementById('editNome').value = data.nome;
document.getElementById('editCpf').value = data.cpf;
document.getElementById('editEmail').value = data.email;
document.getElementById('editTelefone').value = data.telefone;
document.getElementById('editEndereco').value = data.endereco;
document.getElementById('editFiliado').checked = data.filiado;
});
}
rows.forEach(row => { // Função para excluir militante
const text = row.textContent.toLowerCase(); function deleteMilitante(id) {
const isVisible = text.includes(searchTerm); fetch(`/api/militante/${id}`, {
row.style.display = isVisible ? '' : 'none'; method: 'DELETE',
if (isVisible) visibleCount++; headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
window.location.reload();
} else {
alert('Erro ao excluir militante: ' + data.message);
}
});
}
// Event listeners
document.addEventListener('DOMContentLoaded', function() {
// Máscara para CPF
const cpfInput = document.getElementById('cpf');
cpfInput.addEventListener('input', function(e) {
let value = e.target.value.replace(/\D/g, '');
if (value.length > 11) value = value.slice(0, 11);
e.target.value = formatCPF(value);
}); });
document.getElementById('countMilitantes').textContent = visibleCount; // Máscara para telefone
}); const phoneInput = document.getElementById('telefone');
phoneInput.addEventListener('input', function(e) {
let value = e.target.value.replace(/\D/g, '');
if (value.length > 11) value = value.slice(0, 11);
e.target.value = formatPhone(value);
});
// Filtros // Máscara para CPF de edição
const filterButtons = document.querySelectorAll('[data-filter]'); const editCpfInput = document.getElementById('editCpf');
filterButtons.forEach(button => { editCpfInput.addEventListener('input', function(e) {
button.addEventListener('click', function(e) { let value = e.target.value.replace(/\D/g, '');
if (value.length > 11) value = value.slice(0, 11);
e.target.value = formatCPF(value);
});
// Máscara para telefone de edição
const editPhoneInput = document.getElementById('editTelefone');
editPhoneInput.addEventListener('input', function(e) {
let value = e.target.value.replace(/\D/g, '');
if (value.length > 11) value = value.slice(0, 11);
e.target.value = formatPhone(value);
});
// Submit do formulário de edição
const formEditar = document.getElementById('formEditarMilitante');
formEditar.addEventListener('submit', function(e) {
e.preventDefault(); e.preventDefault();
const filter = this.getAttribute('data-filter'); const formData = new FormData(formEditar);
const data = Object.fromEntries(formData.entries());
fetch('/api/militante/' + data.id, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(data => {
if (data.success) {
window.location.reload();
} else {
alert('Erro ao editar militante: ' + data.message);
}
});
});
// Confirmação de exclusão
const confirmDelete = document.getElementById('confirmDelete');
confirmDelete.addEventListener('click', function() {
const id = this.dataset.militanteId;
deleteMilitante(id);
});
// Pesquisa
const searchInput = document.getElementById('searchInput');
searchInput.addEventListener('input', function(e) {
const searchTerm = e.target.value.toLowerCase();
const rows = document.querySelectorAll('#militantesTable tbody tr'); const rows = document.querySelectorAll('#militantesTable tbody tr');
let visibleCount = 0;
rows.forEach(row => { rows.forEach(row => {
const filiado = row.getAttribute('data-filiado'); const nome = row.querySelector('[data-nome]').textContent.toLowerCase();
let isVisible = true; const cpf = row.querySelector('[data-cpf]').textContent.toLowerCase();
const email = row.querySelector('[data-email]').textContent.toLowerCase();
const telefone = row.querySelector('[data-telefone]').textContent.toLowerCase();
const celula = row.querySelector('[data-celula]').textContent.toLowerCase();
if (filter === 'filiados') { const matches = nome.includes(searchTerm) ||
isVisible = filiado === 'sim'; cpf.includes(searchTerm) ||
} else if (filter === 'nao-filiados') { email.includes(searchTerm) ||
isVisible = filiado === 'nao'; telefone.includes(searchTerm) ||
} celula.includes(searchTerm);
row.style.display = isVisible ? '' : 'none'; row.style.display = matches ? '' : 'none';
if (isVisible) visibleCount++;
}); });
document.getElementById('countMilitantes').textContent = visibleCount; updateCount();
});
// Filtros
const filterButtons = document.querySelectorAll('[data-filter]');
filterButtons.forEach(button => {
button.addEventListener('click', function(e) {
e.preventDefault();
const filter = this.dataset.filter;
const rows = document.querySelectorAll('#militantesTable tbody tr');
rows.forEach(row => {
const filiado = row.dataset.filiado;
if (filter === 'todos' ||
(filter === 'filiados' && filiado === 'sim') ||
(filter === 'nao-filiados' && filiado === 'nao')) {
row.style.display = '';
} else {
row.style.display = 'none';
}
});
updateCount();
});
});
// Exportar
const btnExportar = document.getElementById('btnExportar');
btnExportar.addEventListener('click', function() {
const rows = document.querySelectorAll('#militantesTable tbody tr:not([style*="display: none"])');
const data = [];
rows.forEach(row => {
data.push({
nome: row.querySelector('[data-nome]').textContent,
cpf: row.querySelector('[data-cpf]').textContent,
email: row.querySelector('[data-email]').textContent,
telefone: row.querySelector('[data-telefone]').textContent,
celula: row.querySelector('[data-celula]').textContent
});
});
const csv = convertToCSV(data);
downloadCSV(csv, 'militantes.csv');
});
// Ordenação
const headers = document.querySelectorAll('#militantesTable th[data-sort]');
headers.forEach(header => {
header.addEventListener('click', function() {
const sortBy = this.dataset.sort;
const rows = Array.from(document.querySelectorAll('#militantesTable tbody tr'));
const direction = this.classList.contains('asc') ? -1 : 1;
rows.sort((a, b) => {
const aValue = a.querySelector(`[data-${sortBy}]`).textContent;
const bValue = b.querySelector(`[data-${sortBy}]`).textContent;
return direction * aValue.localeCompare(bValue);
});
const tbody = document.querySelector('#militantesTable tbody');
rows.forEach(row => tbody.appendChild(row));
headers.forEach(h => h.classList.remove('asc', 'desc'));
this.classList.add(direction === 1 ? 'asc' : 'desc');
});
}); });
}); });
// Exportar para CSV // Funções auxiliares
document.getElementById('btnExportar').addEventListener('click', function() { function updateCount() {
const rows = document.querySelectorAll('#militantesTable tbody tr:not([style*="display: none"])'); const visibleRows = document.querySelectorAll('#militantesTable tbody tr:not([style*="display: none"])');
let csv = 'Nome,CPF,Email,Telefone,Célula\n'; document.getElementById('countMilitantes').textContent = visibleRows.length;
}
rows.forEach(row => { function convertToCSV(data) {
const nome = row.querySelector('[data-nome]').getAttribute('data-nome'); const headers = ['Nome', 'CPF', 'Email', 'Telefone', 'Célula'];
const cpf = row.querySelector('[data-cpf]').getAttribute('data-cpf'); const rows = data.map(item => [
const email = row.querySelector('[data-email]').getAttribute('data-email'); item.nome,
const telefone = row.querySelector('[data-telefone]').getAttribute('data-telefone'); item.cpf,
const celula = row.querySelector('[data-celula]').getAttribute('data-celula'); item.email,
item.telefone,
item.celula
]);
csv += `"${nome}","${cpf}","${email}","${telefone}","${celula}"\n`; return [headers, ...rows]
}); .map(row => row.map(cell => `"${cell}"`).join(','))
.join('\n');
}
function downloadCSV(csv, filename) {
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a'); const link = document.createElement('a');
link.setAttribute('href', url); link.href = URL.createObjectURL(blob);
link.setAttribute('download', 'militantes.csv'); link.download = filename;
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click(); link.click();
document.body.removeChild(link); }
});
});
</script> </script>
{% endblock %} {% endblock %}
{% block extra_css %}
<style>
/* Estilo para o backdrop com blur em todos os modais */
.modal-backdrop.show {
backdrop-filter: blur(8px);
background-color: rgba(0, 0, 0, 0.7);
}
/* Estilo para o botão de fechar dos modais */
.btn-close {
background-color: transparent;
padding: 0.5rem;
opacity: 0.8;
transition: opacity 0.2s;
filter: invert(1) grayscale(100%) brightness(200%);
}
.btn-close:hover {
opacity: 1;
background-color: transparent;
}
/* Estilo para colunas ordenáveis */
th[data-sort] {
cursor: pointer;
user-select: none;
}
th[data-sort] i {
margin-left: 5px;
color: #ccc;
}
th[data-sort].sort-asc i,
th[data-sort].sort-desc i {
color: var(--primary-color);
}
/* Animação para linhas da tabela */
#militantesTable tbody tr {
transition: all 0.3s ease;
}
#militantesTable tbody tr:hover {
background-color: rgba(0,0,0,0.02);
transform: translateX(5px);
}
/* Estilo para badges */
.badge {
font-weight: 500;
padding: 0.5em 0.8em;
}
/* Estilo para botões de ação */
.btn-group .btn {
padding: 0.25rem 0.5rem;
}
.btn-group .btn i {
width: 16px;
text-align: center;
}
/* Estilo para modais */
.modal-content {
border: none;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
}
.modal-header {
background: linear-gradient(to right, var(--bs-gray-dark), var(--bs-gray));
color: white;
border-radius: 12px 12px 0 0;
border-bottom: none;
padding: 1rem;
}
.modal-body {
padding: 1.5rem;
}
.modal-footer {
border-top: 1px solid #eee;
padding: 1rem;
}
/* Responsividade */
@media (max-width: 768px) {
.btn-group {
display: flex;
margin-top: 1rem;
}
.btn-group .btn {
flex: 1;
}
.modal-footer {
justify-content: center;
}
.modal-footer .btn {
min-width: 120px;
}
}
</style>
{% endblock %} {% endblock %}

View File

@@ -1,32 +1,515 @@
{% extends 'base.html' %} {% extends "base.html" %}
{% block title %}Listar Militantes{% endblock %} {% block title %}Vendas de Jornais{% endblock %}
{% block content %} {% block content %}
<h1>Vendas de Jornais Avulsos</h1> <div class="row mb-4">
<a href="{{ url_for('nova_venda_jornal') }}">Adicionar Nova Venda</a> <div class="col-12">
<table border="1"> <div class="d-flex justify-content-between align-items-center">
<thead> <h1 class="mb-0">
<tr> <i class="fas fa-newspaper me-2"></i>Vendas de Jornais
<th>ID</th> </h1>
<th>Militante ID</th> <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalNovaVenda">
<th>Quantidade</th> <i class="fas fa-plus me-2"></i>Nova Venda
<th>Valor Total</th> </button>
<th>Data da Venda</th> </div>
</tr> </div>
</thead> </div>
<tbody>
{% for venda in vendas %}
<tr>
<td>{{ venda.id }}</td>
<td>{{ venda.militante_id }}</td>
<td>{{ venda.quantidade }}</td>
<td>R$ {{ venda.valor_total }}</td>
<td>{{ venda.data_venda }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<a href="{{ url_for('home') }}">Home</a>
<div class="card shadow-sm">
<div class="card-body">
<div class="row mb-4">
<div class="col-md-6">
<div class="input-group">
<span class="input-group-text">
<i class="fas fa-search"></i>
</span>
<input type="text" class="form-control" id="searchInput" placeholder="Pesquisar vendas...">
</div>
</div>
<div class="col-md-6 text-end">
<button id="btnExportar" class="btn btn-outline-primary">
<i class="fas fa-download me-2"></i>Exportar
</button>
</div>
</div>
<div class="table-responsive">
<table class="table table-hover" id="vendasTable">
<thead>
<tr>
<th data-sort="militante">Militante <i class="fas fa-sort"></i></th>
<th data-sort="quantidade">Quantidade <i class="fas fa-sort"></i></th>
<th data-sort="valor_total">Valor Total <i class="fas fa-sort"></i></th>
<th data-sort="data">Data <i class="fas fa-sort"></i></th>
<th class="text-end">Ações</th>
</tr>
</thead>
<tbody>
{% for venda in vendas %}
<tr>
<td data-militante="{{ venda.militante.nome }}">{{ venda.militante.nome }}</td>
<td data-quantidade="{{ venda.quantidade }}">{{ venda.quantidade }}</td>
<td data-valor_total="{{ venda.valor_total }}">R$ {{ "%.2f"|format(venda.valor_total) }}</td>
<td data-data="{{ venda.data_venda }}">{{ venda.data_venda.strftime('%d/%m/%Y') }}</td>
<td class="text-end">
<div class="btn-group">
<button type="button"
class="btn btn-sm btn-outline-primary"
data-bs-toggle="modal"
data-bs-target="#modalEditarVenda"
data-venda-id="{{ venda.id }}"
data-venda-militante="{{ venda.militante_id }}"
data-venda-quantidade="{{ venda.quantidade }}"
data-venda-valor-total="{{ venda.valor_total }}"
data-venda-data="{{ venda.data_venda.strftime('%Y-%m-%d') }}"
title="Editar">
<i class="fas fa-edit"></i>
</button>
<button type="button"
class="btn btn-sm btn-outline-danger"
data-bs-toggle="modal"
data-bs-target="#deleteModal"
data-venda-id="{{ venda.id }}"
data-venda-info="{{ venda.militante.nome }} - {{ venda.quantidade }} jornais"
title="Excluir">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Modal Nova Venda -->
<div class="modal fade" id="modalNovaVenda" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-plus me-2"></i>Nova Venda de Jornal
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="formNovaVenda" method="post" action="{{ url_for('nova_venda_jornal') }}">
<div class="mb-3">
<label for="militante_id" class="form-label">Militante:</label>
<select class="form-select" id="militante_id" name="militante_id" required>
<option value="">Selecione um militante</option>
{% for militante in militantes %}
<option value="{{ militante.id }}">{{ militante.nome }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="quantidade" class="form-label">Quantidade:</label>
<input type="number" class="form-control" id="quantidade" name="quantidade" required>
</div>
<div class="mb-3">
<label for="valor_total" class="form-label">Valor Total:</label>
<input type="number" step="0.01" class="form-control" id="valor_total" name="valor_total" required>
</div>
<div class="mb-3">
<label for="data_venda" class="form-label">Data da Venda:</label>
<input type="date" class="form-control" id="data_venda" name="data_venda" required>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" form="formNovaVenda" class="btn btn-success">
<i class="fas fa-save me-2"></i>Salvar
</button>
</div>
</div>
</div>
</div>
<!-- Modal de Edição -->
<div class="modal fade" id="modalEditarVenda" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-edit me-2"></i>Editar Venda de Jornal
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="formEditarVenda" method="post">
<div class="mb-3">
<label for="editMilitante" class="form-label">Militante:</label>
<select class="form-select" id="editMilitante" name="militante_id" required>
<option value="">Selecione um militante</option>
{% for militante in militantes %}
<option value="{{ militante.id }}">{{ militante.nome }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="editQuantidade" class="form-label">Quantidade:</label>
<input type="number" class="form-control" id="editQuantidade" name="quantidade" required>
</div>
<div class="mb-3">
<label for="editValorTotal" class="form-label">Valor Total:</label>
<input type="number" step="0.01" class="form-control" id="editValorTotal" name="valor_total" required>
</div>
<div class="mb-3">
<label for="editData" class="form-label">Data da Venda:</label>
<input type="date" class="form-control" id="editData" name="data_venda" required>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" form="formEditarVenda" class="btn btn-success">
<i class="fas fa-save me-2"></i>Salvar
</button>
</div>
</div>
</div>
</div>
<!-- Modal de Exclusão -->
<div class="modal fade" id="deleteModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Confirmar Exclusão</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>Tem certeza que deseja excluir a venda de <strong id="vendaInfo"></strong>?</p>
<p class="text-danger mb-0">Esta ação não pode ser desfeita.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<form action="" method="POST" id="deleteForm" class="d-inline">
<button type="submit" class="btn btn-danger">
<i class="fas fa-trash me-2"></i>Excluir
</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Configuração do modal de exclusão
const deleteModal = document.getElementById('deleteModal');
deleteModal.addEventListener('show.bs.modal', function(event) {
const button = event.relatedTarget;
const vendaId = button.getAttribute('data-venda-id');
const vendaInfo = button.getAttribute('data-venda-info');
document.getElementById('vendaInfo').textContent = vendaInfo;
document.getElementById('deleteForm').action = `/jornais/excluir/${vendaId}`;
});
// Envio do formulário de nova venda via AJAX
const formNovaVenda = document.getElementById('formNovaVenda');
formNovaVenda.addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
fetch(this.action, {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
// Fechar o modal
bootstrap.Modal.getInstance(document.getElementById('modalNovaVenda')).hide();
// Atualizar a lista
location.reload();
// Mostrar mensagem de sucesso
const alertDiv = document.createElement('div');
alertDiv.className = 'alert alert-success alert-dismissible fade show';
alertDiv.innerHTML = `
${data.message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.querySelector('.container').insertBefore(alertDiv, document.querySelector('.container').firstChild);
} else {
// Mostrar erro
const alertDiv = document.createElement('div');
alertDiv.className = 'alert alert-danger alert-dismissible fade show';
alertDiv.innerHTML = `
${data.message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.querySelector('.modal-body').insertBefore(alertDiv, formNovaVenda);
}
})
.catch(error => {
console.error('Erro:', error);
const alertDiv = document.createElement('div');
alertDiv.className = 'alert alert-danger alert-dismissible fade show';
alertDiv.innerHTML = `
Erro ao cadastrar venda. Tente novamente.
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.querySelector('.modal-body').insertBefore(alertDiv, formNovaVenda);
});
});
// Configuração do modal de edição
const modalEditarVenda = document.getElementById('modalEditarVenda');
modalEditarVenda.addEventListener('show.bs.modal', function(event) {
const button = event.relatedTarget;
const vendaId = button.getAttribute('data-venda-id');
// Preencher o formulário com os dados da venda
document.getElementById('editMilitante').value = button.getAttribute('data-venda-militante');
document.getElementById('editQuantidade').value = button.getAttribute('data-venda-quantidade');
document.getElementById('editValorTotal').value = button.getAttribute('data-venda-valor-total');
document.getElementById('editData').value = button.getAttribute('data-venda-data');
// Configurar a action do formulário
document.getElementById('formEditarVenda').action = `/jornais/editar/${vendaId}`;
});
// Envio do formulário de edição via AJAX
const formEditarVenda = document.getElementById('formEditarVenda');
formEditarVenda.addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
fetch(this.action, {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
// Fechar o modal
bootstrap.Modal.getInstance(modalEditarVenda).hide();
// Atualizar a lista
location.reload();
// Mostrar mensagem de sucesso
const alertDiv = document.createElement('div');
alertDiv.className = 'alert alert-success alert-dismissible fade show';
alertDiv.innerHTML = `
${data.message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.querySelector('.container').insertBefore(alertDiv, document.querySelector('.container').firstChild);
} else {
// Mostrar erro
const alertDiv = document.createElement('div');
alertDiv.className = 'alert alert-danger alert-dismissible fade show';
alertDiv.innerHTML = `
${data.message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.querySelector('.modal-body').insertBefore(alertDiv, formEditarVenda);
}
})
.catch(error => {
console.error('Erro:', error);
const alertDiv = document.createElement('div');
alertDiv.className = 'alert alert-danger alert-dismissible fade show';
alertDiv.innerHTML = `
Erro ao atualizar venda. Tente novamente.
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.querySelector('.modal-body').insertBefore(alertDiv, formEditarVenda);
});
});
// Limpar alertas quando os modais forem fechados
[modalEditarVenda, document.getElementById('modalNovaVenda')].forEach(modal => {
modal.addEventListener('hidden.bs.modal', function () {
const alerts = this.querySelectorAll('.alert');
alerts.forEach(alert => alert.remove());
});
});
// Pesquisa em tempo real
const searchInput = document.getElementById('searchInput');
searchInput.addEventListener('input', function() {
const searchTerm = this.value.toLowerCase();
const rows = document.querySelectorAll('#vendasTable tbody tr');
rows.forEach(row => {
const text = row.textContent.toLowerCase();
row.style.display = text.includes(searchTerm) ? '' : 'none';
});
});
// Ordenação
const headers = document.querySelectorAll('#vendasTable th[data-sort]');
headers.forEach(header => {
header.addEventListener('click', function() {
const column = this.getAttribute('data-sort');
const tbody = document.querySelector('#vendasTable tbody');
const rows = Array.from(tbody.querySelectorAll('tr'));
const isAsc = !this.classList.contains('sort-asc');
// Remover classes de ordenação de todos os headers
headers.forEach(h => {
h.classList.remove('sort-asc', 'sort-desc');
h.querySelector('i').className = 'fas fa-sort';
});
// Adicionar classe de ordenação ao header clicado
this.classList.add(isAsc ? 'sort-asc' : 'sort-desc');
this.querySelector('i').className = `fas fa-sort-${isAsc ? 'up' : 'down'}`;
// Ordenar linhas
rows.sort((a, b) => {
const aVal = a.querySelector(`td[data-${column}]`).getAttribute(`data-${column}`);
const bVal = b.querySelector(`td[data-${column}]`).getAttribute(`data-${column}`);
return isAsc ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal);
});
// Reposicionar linhas
rows.forEach(row => tbody.appendChild(row));
});
});
// Exportar para CSV
document.getElementById('btnExportar').addEventListener('click', function() {
const rows = document.querySelectorAll('#vendasTable tbody tr:not([style*="display: none"])');
const headers = ['Militante', 'Quantidade', 'Valor Total', 'Data'];
let csv = headers.join(',') + '\n';
rows.forEach(row => {
const cols = row.querySelectorAll('td');
const values = [
cols[0].textContent,
cols[1].textContent,
cols[2].textContent,
cols[3].textContent
].map(val => `"${val}"`);
csv += values.join(',') + '\n';
});
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.setAttribute('download', 'vendas_jornal.csv');
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});
});
</script>
{% endblock %}
{% block extra_css %}
<style>
/* Estilo para colunas ordenáveis */
th[data-sort] {
cursor: pointer;
user-select: none;
}
th[data-sort] i {
margin-left: 5px;
color: #ccc;
}
th[data-sort].sort-asc i,
th[data-sort].sort-desc i {
color: var(--primary-color);
}
/* Animação para linhas da tabela */
#vendasTable tbody tr {
transition: all 0.3s ease;
}
#vendasTable tbody tr:hover {
background-color: rgba(0,0,0,0.02);
transform: translateX(5px);
}
/* Estilo para botões de ação */
.btn-group .btn {
padding: 0.25rem 0.5rem;
}
.btn-group .btn i {
width: 16px;
text-align: center;
}
/* Responsividade */
@media (max-width: 768px) {
.btn-group {
display: flex;
margin-top: 1rem;
}
.btn-group .btn {
flex: 1;
}
}
/* Estilo para o backdrop com blur em todos os modais */
.modal-backdrop.show {
backdrop-filter: blur(8px);
background-color: rgba(0, 0, 0, 0.7);
}
/* Estilo para o botão de fechar dos modais */
.btn-close {
background-color: transparent;
padding: 0.5rem;
opacity: 0.8;
transition: opacity 0.2s;
filter: invert(1) grayscale(100%) brightness(200%);
}
.btn-close:hover {
opacity: 1;
background-color: transparent;
}
/* Estilo para modais */
.modal-content {
border: none;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
}
.modal-header {
background: linear-gradient(to right, var(--bs-gray-dark), var(--bs-gray));
color: white;
border-radius: 12px 12px 0 0;
border-bottom: none;
padding: 1rem;
}
.modal-body {
padding: 1.5rem;
}
.modal-footer {
border-top: 1px solid #eee;
padding: 1rem;
}
</style>
{% endblock %} {% endblock %}