From 8803c971e4f86fe52fce5114bcc4d969172f9502 Mon Sep 17 00:00:00 2001 From: andersonid Date: Wed, 2 Apr 2025 21:20:48 -0300 Subject: [PATCH] feat: Melhorias no Dashboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Interface: - Adicionada saudação personalizada com nome do usuário - Melhorado formato da data em português - Ajustado layout do header com gradiente e sombra - Corrigida categoria de mensagens flash de 'error' para 'danger' - Card de Cotas: - Reorganizado layout para melhor exibição de valores grandes - Ajustado tamanho da fonte usando calc(1.2rem + 0.8vw) - Adicionado container específico para valor com min-width: 0 - Redimensionado e reposicionado ícone - Melhorado espaçamento e alinhamento - Lista de Militantes: - Ajustada query para ordenar por ID - Removida dependência da coluna created_at - Adicionado ID do militante na listagem - Estilos: - Adicionadas classes valor-container e icon-container - Melhorado responsividade dos valores monetários - Ajustado gradiente no header de boas-vindas - Refinado espaçamento e margens dos componentes --- .gitignore | 3 + Makefile | 4 +- app.py | 793 ++++++++++-------- create_admin.py | 201 +++-- .../__pycache__/database.cpython-312.pyc | Bin 15652 -> 16297 bytes functions/database.py | 103 +-- seed_data.py | 126 ++- static/css/style.css | 81 +- templates/editar_cota.html | 29 + templates/home.html | 67 +- 10 files changed, 831 insertions(+), 576 deletions(-) create mode 100644 templates/editar_cota.html diff --git a/.gitignore b/.gitignore index 79f62d2..c60a1ff 100644 --- a/.gitignore +++ b/.gitignore @@ -263,3 +263,6 @@ database.db admin_qr.png # End of https://www.toptal.com/developers/gitignore/api/python,flask + +# Documentação temporária +docs/alteracoes_db_connection.md diff --git a/Makefile b/Makefile index 969466b..977f3b9 100644 --- a/Makefile +++ b/Makefile @@ -5,8 +5,8 @@ clean: rm -rf ~/.local/share/controles/database.db rm -f admin_qr.png -run: clean +run: python app.py reset-admin: clean - python create_admin.py + python create_admin.py diff --git a/app.py b/app.py index 4c9c296..c38880f 100644 --- a/app.py +++ b/app.py @@ -237,38 +237,52 @@ def logout(): @require_login def home(): """Página inicial do sistema com dashboard""" + db = get_db_connection() try: + # Buscar nome do usuário + usuario = db.query(Usuario).get(session.get('user_id')) + nome_usuario = usuario.username if usuario else "Usuário" + # Formatar data atual em português meses = { - 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' + 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 - total_militantes = db_session.query(Militante).count() - total_cotas = db_session.query(func.sum(CotaMensal.valor_novo)).scalar() or 0 - total_materiais = db_session.query(MaterialVendido).count() - total_assinaturas = db_session.query(AssinaturaAnual).filter( + total_militantes = db.query(Militante).count() + total_cotas = db.query(func.sum(CotaMensal.valor_novo)).scalar() or 0 + total_materiais = db.query(MaterialVendido).count() + total_assinaturas = db.query(AssinaturaAnual).filter( AssinaturaAnual.data_fim >= datetime.now() ).count() - # Buscar últimos militantes cadastrados - ultimos_militantes = db_session.query(Militante)\ - .order_by(Militante.id.desc())\ - .limit(5)\ - .all() + # Buscar últimos militantes cadastrados com tratamento de erro + try: + ultimos_militantes = db.query(Militante)\ + .order_by(Militante.id.desc())\ + .limit(5)\ + .all() + except Exception as e: + print(f"Erro ao buscar últimos militantes: {e}") + ultimos_militantes = [] - # Buscar últimos pagamentos - ultimos_pagamentos = db_session.query(Pagamento)\ - .join(Militante)\ - .order_by(Pagamento.data_pagamento.desc())\ - .limit(5)\ - .all() + # 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), @@ -280,15 +294,28 @@ def home(): print(f"Erro na página inicial: {e}") import traceback traceback.print_exc() - flash('Erro ao carregar a página inicial', 'error') + 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', - data_atual=datetime.now().strftime("%d/%m/%Y"), - total_militantes=0, + 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: + db.close() # Rota para criar um novo militante @app.route('/militantes/criar', methods=['GET', 'POST']) @@ -371,167 +398,207 @@ def criar_militante(): @require_permission(Permission.VIEW_CELL_DATA) def listar_militantes(): try: - militantes = db_session.query(Militante).order_by(Militante.nome).all() + print("Buscando militantes...") + db = get_db_connection() + militantes = db.query(Militante).order_by(Militante.nome).all() + print(f"Total de militantes encontrados: {len(militantes)}") + for militante in militantes: + print(f"Militante: {militante.nome} (ID: {militante.id})") return render_template("listar_militantes.html", militantes=militantes) except Exception as e: - print(f"Erro ao listar militantes: {str(e)}") - flash("Erro ao carregar lista de militantes. Por favor, tente novamente.", "danger") + print(f"Erro ao listar militantes: {e}") + flash('Erro ao listar militantes', 'danger') return render_template("listar_militantes.html", militantes=[]) + finally: + db.close() +# Rota para excluir militante @app.route("/militantes/excluir/", methods=["POST"]) @login_required @session_timeout def excluir_militante(id): try: - militante = db_session.query(Militante).get(id) + db = get_db_connection() + militante = db.query(Militante).get(id) if not militante: - flash('Militante não encontrado.', 'error') + flash('Militante não encontrado', 'danger') return redirect(url_for('listar_militantes')) - - # Verificar se existem registros relacionados - tem_cotas = db_session.query(CotaMensal).filter_by(militante_id=id).first() is not None - tem_pagamentos = db_session.query(Pagamento).filter_by(militante_id=id).first() is not None - tem_materiais = db_session.query(MaterialVendido).filter_by(militante_id=id).first() is not None - tem_vendas = db_session.query(VendaJornalAvulso).filter_by(militante_id=id).first() is not None - tem_assinaturas = db_session.query(AssinaturaAnual).filter_by(militante_id=id).first() is not None - - if any([tem_cotas, tem_pagamentos, tem_materiais, tem_vendas, tem_assinaturas]): - flash('Não é possível excluir o militante pois existem registros relacionados.', 'error') - return redirect(url_for('listar_militantes')) - - db_session.delete(militante) - db_session.commit() + + db.delete(militante) + db.commit() flash('Militante excluído com sucesso!', 'success') except Exception as e: + db.rollback() print(f"Erro ao excluir militante: {e}") - db_session.rollback() - flash('Erro ao excluir militante.', 'error') - + flash('Erro ao excluir militante', 'danger') + finally: + db.close() return redirect(url_for('listar_militantes')) -# Rota para criar uma nova cota mensal +# Rota para criar uma nova cota @app.route("/cotas/novo", methods=["GET", "POST"]) @require_login @require_permission(Permission.VIEW_CELL_REPORTS) def nova_cota(): if request.method == "POST": - cotas_mensais = CotaMensal( - militante_id=request.form["militante_id"], - valor_antigo=request.form["valor_antigo"], - valor_novo=request.form["valor_novo"], - data_alteracao=datetime.strptime(request.form["data_alteracao"], "%Y-%m-%d") - ) + militante_id = request.form.get("militante_id") + valor_antigo = float(request.form.get("valor_antigo")) + valor_novo = float(request.form.get("valor_novo")) + data_alteracao = datetime.strptime(request.form.get("data_alteracao"), "%Y-%m-%d").date() + data_vencimento = datetime.strptime(request.form.get("data_vencimento"), "%Y-%m-%d").date() - db_session.add(cotas_mensais) + db = get_db_connection() try: - db_session.commit() - return redirect(url_for("listar_cotas")) + cotas_mensais = CotaMensal( + militante_id=militante_id, + valor_antigo=valor_antigo, + valor_novo=valor_novo, + data_alteracao=data_alteracao, + data_vencimento=data_vencimento, + pago=False + ) + db.add(cotas_mensais) + db.commit() + flash('Cota cadastrada com sucesso!', 'success') + return redirect(url_for('listar_cotas')) except Exception as e: - print(e) - db_session.rollback() - flash('Erro ao cadastrar cota mensal. Verifique se os dados fornecidos são válidos.', 'error') + db.rollback() + print(f"Erro ao cadastrar cota: {e}") + flash('Erro ao cadastrar cota', 'danger') return render_template("nova_cota.html") + finally: + db.close() - return render_template("nova_cota.html") + db = get_db_connection() + try: + militantes = db.query(Militante).order_by(Militante.nome).all() + return render_template("nova_cota.html", militantes=militantes) + finally: + db.close() -# Rota para listar cotas mensais +# Rota para listar cotas @app.route("/cotas") @require_login @require_permission(Permission.VIEW_CELL_REPORTS) def listar_cotas(): - """Rota para listar cotas mensais""" try: + print("Buscando cotas...") # Buscar cotas com informações do militante - cotas = db_session.query(CotaMensal)\ + db = get_db_connection() + cotas = db.query(CotaMensal)\ .join(Militante)\ .order_by(CotaMensal.data_vencimento.desc())\ .all() - - # Calcular status das cotas - hoje = datetime.now().date() + + print(f"Total de cotas encontradas: {len(cotas)}") + + # Calcular status de cada cota for cota in cotas: if cota.pago: - cota.status = 'paga' - elif cota.data_vencimento < hoje: - cota.status = 'atrasada' + cota.status = "paga" + elif cota.data_vencimento < datetime.now().date(): + cota.status = "atrasada" else: - cota.status = 'pendente' + cota.status = "pendente" + print(f"Cota {cota.id}: Militante={cota.militante.nome}, Valor={cota.valor_novo}, Status={cota.status}") return render_template("listar_cotas.html", cotas=cotas) - except Exception as e: - print(f"Erro ao listar cotas: {str(e)}") - flash("Erro ao carregar lista de cotas. Por favor, tente novamente.", "danger") + print(f"Erro ao listar cotas: {e}") + flash('Erro ao listar cotas', 'danger') return render_template("listar_cotas.html", cotas=[]) + finally: + db.close() + +# Rota para editar cota +@app.route("/cotas/editar/", methods=["GET", "POST"]) +@login_required +@session_timeout +def editar_cota(id): + db = get_db_connection() + try: + cota = db.query(CotaMensal).filter_by(id=id).first() + if not cota: + flash('Cota não encontrada', 'danger') + return redirect(url_for('listar_cotas')) + + if request.method == "POST": + valor_novo = float(request.form.get("valor_novo")) + data_vencimento = datetime.strptime(request.form.get("data_vencimento"), "%Y-%m-%d").date() + pago = request.form.get("pago") == "true" + + cota.valor_novo = valor_novo + cota.data_vencimento = data_vencimento + cota.pago = pago + cota.data_alteracao = datetime.now().date() + + db.commit() + flash('Cota atualizada com sucesso!', 'success') + return redirect(url_for('listar_cotas')) + + return render_template("editar_cota.html", cota=cota) + except Exception as e: + db.rollback() + print(f"Erro ao editar cota: {e}") + flash('Erro ao editar cota', 'danger') + return redirect(url_for('listar_cotas')) + finally: + db.close() # Rota para criar um novo pagamento @app.route("/pagamentos/novo", methods=["GET", "POST"]) @require_login def novo_pagamento(): - """Rota para criar um novo pagamento""" - user = current_user - - # Verificar se o usuário tem permissão para registrar pagamentos em alguma instância - if not (user.has_permission(Permission.REGISTER_CELL_PAYMENT) or - user.has_permission(Permission.REGISTER_SECTOR_PAYMENT) or - user.has_permission(Permission.REGISTER_CR_PAYMENT) or - user.has_permission(Permission.REGISTER_CC_PAYMENT)): - flash('Você não tem permissão para registrar pagamentos.', 'error') - return redirect(url_for('home')) - if request.method == "POST": + militante_id = request.form.get("militante_id") + tipo_pagamento_id = request.form.get("tipo_pagamento_id") + valor = float(request.form.get("valor")) + data_pagamento = datetime.strptime(request.form.get("data_pagamento"), "%Y-%m-%d").date() + + # Criar novo pagamento + db = get_db_connection() try: - militante_id = request.form.get("militante_id") - tipo_pagamento_id = request.form.get("tipo_pagamento_id") - valor = request.form.get("valor") - data_pagamento = request.form.get("data_pagamento") - - # Validações - if not all([militante_id, tipo_pagamento_id, valor, data_pagamento]): - flash("Todos os campos são obrigatórios.", "danger") - return redirect(url_for("novo_pagamento")) - - # Criar novo pagamento pagamento = Pagamento( militante_id=militante_id, tipo_pagamento_id=tipo_pagamento_id, - valor=float(valor), - data_pagamento=datetime.strptime(data_pagamento, "%Y-%m-%d") + valor=valor, + data_pagamento=data_pagamento ) - - db_session.add(pagamento) - db_session.commit() - - flash("Pagamento registrado com sucesso!", "success") - return redirect(url_for("listar_pagamentos")) - + db.add(pagamento) + db.commit() + flash('Pagamento cadastrado com sucesso!', 'success') + return redirect(url_for('listar_pagamentos')) except Exception as e: - db_session.rollback() - app.logger.error(f"Erro ao registrar pagamento: {str(e)}") - flash("Erro ao registrar pagamento. Por favor, tente novamente.", "danger") - return redirect(url_for("novo_pagamento")) - + db.rollback() + print(f"Erro ao cadastrar pagamento: {e}") + flash('Erro ao cadastrar pagamento', 'danger') + return render_template("novo_pagamento.html") + finally: + db.close() + # GET - Renderizar formulário - militantes = db_session.query(Militante).order_by(Militante.nome).all() - tipos_pagamento = db_session.query(TipoPagamento).order_by(TipoPagamento.descricao).all() - hoje = datetime.now().strftime("%Y-%m-%d") - - return render_template("novo_pagamento.html", - militantes=militantes, - tipos_pagamento=tipos_pagamento, - hoje=hoje) + db = get_db_connection() + try: + militantes = db.query(Militante).order_by(Militante.nome).all() + tipos_pagamento = db.query(TipoPagamento).order_by(TipoPagamento.descricao).all() + return render_template("novo_pagamento.html", militantes=militantes, tipos_pagamento=tipos_pagamento) + finally: + db.close() # Rota para listar pagamentos @app.route("/pagamentos") @require_login def listar_pagamentos(): try: - pagamentos = db_session.query(Pagamento).order_by(Pagamento.data_pagamento.desc()).all() + db = get_db_connection() + pagamentos = db.query(Pagamento).order_by(Pagamento.data_pagamento.desc()).all() return render_template("listar_pagamentos.html", pagamentos=pagamentos) except Exception as e: - print(f"Erro ao listar pagamentos: {str(e)}") - flash("Erro ao carregar lista de pagamentos. Por favor, tente novamente.", "danger") + print(f"Erro ao listar pagamentos: {e}") + flash('Erro ao listar pagamentos', 'danger') return render_template("listar_pagamentos.html", pagamentos=[]) + finally: + db.close() # Rota para criar um novo material vendido @app.route("/materiais/novo", methods=["GET", "POST"]) @@ -539,25 +606,40 @@ def listar_pagamentos(): @require_permission(Permission.VIEW_CELL_REPORTS) def novo_material(): if request.method == "POST": - materiais_vendidos = MaterialVendido( - militante_id=request.form["militante_id"], - tipo_material_id=request.form["tipo_material_id"], - descricao=request.form["descricao"], - valor=request.form["valor"], - data_venda=datetime.strptime(request.form["data_venda"], "%Y-%m-%d"), - ) + 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_session.add(materiais_vendidos) + db = get_db_connection() try: - db_session.commit() - return redirect(url_for("listar_materiais")) + 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: - print(e) - db_session.rollback() - flash('Erro ao cadastrar material vendido. Verifique se os dados fornecidos são válidos.', 'error') + 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() - return render_template("novo_material.html") + db = get_db_connection() + try: + militantes = db.query(Militante).order_by(Militante.nome).all() + tipos_material = db.query(TipoMaterial).order_by(TipoMaterial.descricao).all() + return render_template("novo_material.html", militantes=militantes, tipos_material=tipos_material) + finally: + db.close() # Rota para listar materiais vendidos @app.route("/materiais") @@ -565,314 +647,351 @@ def novo_material(): @require_permission(Permission.VIEW_CELL_REPORTS) def listar_materiais(): try: - materiais = db_session.query(MaterialVendido).order_by(MaterialVendido.data_venda.desc()).all() + db = get_db_connection() + materiais = db.query(MaterialVendido).order_by(MaterialVendido.data_venda.desc()).all() return render_template("listar_materiais.html", materiais=materiais) except Exception as e: - print(f"Erro ao listar materiais: {str(e)}") - flash("Erro ao carregar lista de materiais. Por favor, tente novamente.", "danger") + print(f"Erro ao listar materiais: {e}") + flash('Erro ao listar materiais', 'danger') return render_template("listar_materiais.html", materiais=[]) + finally: + db.close() -# Rota para criar uma nova venda de jornais avulsos +# Rota para criar uma nova venda de jornal @app.route("/jornais/novo", methods=["GET", "POST"]) @require_login @require_permission(Permission.VIEW_CELL_REPORTS) def nova_venda_jornal(): if request.method == "POST": - vendas_jornais_avulsos = VendaJornalAvulso( - militante_id=request.form["militante_id"], - quantidade=request.form["quantidade"], - valor_total=request.form["valor_total"], - data_venda=datetime.strptime(request.form["data_venda"], "%Y-%m-%d"), - ) + 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_session.add(vendas_jornais_avulsos) + db = get_db_connection() try: - db_session.commit() - return redirect(url_for("listar_vendas_jornal")) + 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: - print(e) - db_session.rollback() - flash('Erro ao cadastrar venda de jornal avulso. Verifique se os dados fornecidos são válidos.', 'error') + 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() - return render_template("nova_venda_jornal.html") + db = get_db_connection() + try: + militantes = db.query(Militante).order_by(Militante.nome).all() + return render_template("nova_venda_jornal.html", militantes=militantes) + finally: + db.close() -# Rota para listar vendas de jornais avulsos +# Rota para listar vendas de jornal @app.route("/jornais") @require_login @require_permission(Permission.VIEW_CELL_REPORTS) def listar_vendas_jornal(): try: - vendas = db_session.query(VendaJornalAvulso).order_by(VendaJornalAvulso.data_venda.desc()).all() + 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 jornais: {str(e)}") - flash("Erro ao carregar lista de vendas de jornais. Por favor, tente novamente.", "danger") + 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 anual +# 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": - assinaturas_anuais = AssinaturaAnual( - militante_id=request.form["militante_id"], - tipo_material_id=request.form["tipo_material_id"], - quantidade=request.form["quantidade"], - valor_total=request.form["valor_total"], - data_inicio=datetime.strptime(request.form["data_inicio"], "%Y-%m-%d"), - data_fim=datetime.strptime(request.form["data_fim"], "%Y-%m-%d") - ) + 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_session.add(assinaturas_anuais) + db = get_db_connection() try: - db_session.commit() - return redirect(url_for("listar_assinaturas")) + 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: - print(e) - db_session.rollback() - flash('Erro ao cadastrar assinatura anual. Verifique se os dados fornecidos são válidos.', 'error') + db.rollback() + print(f"Erro ao cadastrar assinatura: {e}") + flash('Erro ao cadastrar assinatura', 'danger') return render_template("nova_assinatura.html") + finally: + db.close() - return render_template("nova_assinatura.html") + db = get_db_connection() + try: + militantes = db.query(Militante).order_by(Militante.nome).all() + tipos_material = db.query(TipoMaterial).order_by(TipoMaterial.descricao).all() + return render_template("nova_assinatura.html", militantes=militantes, tipos_material=tipos_material) + finally: + db.close() -# Rota para listar assinaturas anuais +# Rota para listar assinaturas @app.route("/assinaturas") @require_login @require_permission(Permission.VIEW_CELL_REPORTS) def listar_assinaturas(): try: - assinaturas = db_session.query(AssinaturaAnual).order_by(AssinaturaAnual.data_inicio.desc()).all() + 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: {str(e)}") - flash("Erro ao carregar lista de assinaturas. Por favor, tente novamente.", "danger") + print(f"Erro ao listar assinaturas: {e}") + flash('Erro ao listar assinaturas', 'danger') return render_template("listar_assinaturas.html", assinaturas=[]) + finally: + db.close() -# Rota para criar um novo relatório de cotas mensais +# Rota para criar um novo relatório de cotas @app.route("/relatorios/cotas/novo", methods=["GET", "POST"]) @require_login @require_permission(Permission.VIEW_CELL_REPORTS) def novo_relatorio_cotas(): if request.method == "POST": - relatorio_cotas_mensais = RelatorioCotasMensais( - setor_id=request.form["setor_id"], - comite_id=request.form["comite_id"], - total_cotas=request.form["total_cotas"], - data_relatorio=datetime.strptime(request.form["data_relatorio"], "%Y-%m-%d") - ) + setor_id = request.form.get("setor_id") + comite_id = request.form.get("comite_id") + total_cotas = float(request.form.get("total_cotas")) + data_relatorio = datetime.strptime(request.form.get("data_relatorio"), "%Y-%m-%d").date() - db_session.add(relatorio_cotas_mensais) + db = get_db_connection() try: - db_session.commit() - return redirect(url_for("listar_relatorios_cotas")) + relatorio_cotas_mensais = RelatorioCotasMensais( + setor_id=setor_id, + comite_id=comite_id, + total_cotas=total_cotas, + data_relatorio=data_relatorio + ) + db.add(relatorio_cotas_mensais) + db.commit() + flash('Relatório de cotas cadastrado com sucesso!', 'success') + return redirect(url_for('listar_relatorios_cotas')) except Exception as e: - print(e) - db_session.rollback() - flash('Erro ao cadastrar relatório de cotas mensais. Verifique se os dados fornecidos são válidos.', 'error') + db.rollback() + print(f"Erro ao cadastrar relatório de cotas: {e}") + flash('Erro ao cadastrar relatório de cotas', 'danger') return render_template("novo_relatorio_cotas.html") + finally: + db.close() - return render_template("novo_relatorio_cotas.html") + db = get_db_connection() + try: + setores = db.query(Setor).order_by(Setor.nome).all() + comites = db.query(ComiteCentral).order_by(ComiteCentral.nome).all() + return render_template("novo_relatorio_cotas.html", setores=setores, comites=comites) + finally: + db.close() -# Rota para listar relatórios de cotas mensais +# Rota para listar relatórios de cotas @app.route("/relatorios/cotas") @require_login @require_permission(Permission.VIEW_CELL_REPORTS) def listar_relatorios_cotas(): try: - relatorios = db_session.query(RelatorioCotasMensais).order_by(RelatorioCotasMensais.data_relatorio.desc()).all() + db = get_db_connection() + relatorios = db.query(RelatorioCotasMensais).order_by(RelatorioCotasMensais.data_relatorio.desc()).all() return render_template("listar_relatorios_cotas.html", relatorios=relatorios) except Exception as e: - print(f"Erro ao listar relatórios de cotas: {str(e)}") - flash("Erro ao carregar lista de relatórios de cotas. Por favor, tente novamente.", "danger") + print(f"Erro ao listar relatórios de cotas: {e}") + flash('Erro ao listar relatórios de cotas', 'danger') return render_template("listar_relatorios_cotas.html", relatorios=[]) + finally: + db.close() -# Rota para criar um novo relatório de vendas de materiais +# Rota para criar um novo relatório de vendas @app.route("/relatorios/vendas/novo", methods=["GET", "POST"]) @require_login @require_permission(Permission.VIEW_CELL_REPORTS) def novo_relatorio_vendas(): if request.method == "POST": - relatorio_vendas_materiais = RelatorioVendasMateriais( - setor_id=request.form["setor_id"], - comite_id=request.form["comite_id"], - total_vendas=request.form["total_vendas"], - data_relatorio=datetime.strptime(request.form["data_relatorio"], "%Y-%m-%d") - ) + setor_id = request.form.get("setor_id") + comite_id = request.form.get("comite_id") + total_vendas = float(request.form.get("total_vendas")) + data_relatorio = datetime.strptime(request.form.get("data_relatorio"), "%Y-%m-%d").date() - db_session.add(relatorio_vendas_materiais) + db = get_db_connection() try: - db_session.commit() - return redirect(url_for("listar_relatorios_vendas")) + relatorio_vendas_materiais = RelatorioVendasMateriais( + setor_id=setor_id, + comite_id=comite_id, + total_vendas=total_vendas, + data_relatorio=data_relatorio + ) + db.add(relatorio_vendas_materiais) + db.commit() + flash('Relatório de vendas cadastrado com sucesso!', 'success') + return redirect(url_for('listar_relatorios_vendas')) except Exception as e: - print(e) - db_session.rollback() - flash('Erro ao cadastrar relatório de vendas de materiais. Verifique se os dados fornecidos são válidos.', 'error') + db.rollback() + print(f"Erro ao cadastrar relatório de vendas: {e}") + flash('Erro ao cadastrar relatório de vendas', 'danger') return render_template("novo_relatorio_vendas.html") + finally: + db.close() - return render_template("novo_relatorio_vendas.html") + db = get_db_connection() + try: + setores = db.query(Setor).order_by(Setor.nome).all() + comites = db.query(ComiteCentral).order_by(ComiteCentral.nome).all() + return render_template("novo_relatorio_vendas.html", setores=setores, comites=comites) + finally: + db.close() -# Rota para listar relatórios de vendas de materiais +# Rota para listar relatórios de vendas @app.route("/relatorios/vendas") @require_login @require_permission(Permission.VIEW_CELL_REPORTS) def listar_relatorios_vendas(): try: - relatorios = db_session.query(RelatorioVendasMateriais).order_by(RelatorioVendasMateriais.data_relatorio.desc()).all() + db = get_db_connection() + relatorios = db.query(RelatorioVendasMateriais).order_by(RelatorioVendasMateriais.data_relatorio.desc()).all() return render_template("listar_relatorios_vendas.html", relatorios=relatorios) except Exception as e: - print(f"Erro ao listar relatórios de vendas: {str(e)}") - flash("Erro ao carregar lista de relatórios de vendas. Por favor, tente novamente.", "danger") + print(f"Erro ao listar relatórios de vendas: {e}") + flash('Erro ao listar relatórios de vendas', 'danger') return render_template("listar_relatorios_vendas.html", relatorios=[]) + finally: + db.close() -@app.route('/militantes//editar', methods=['GET', 'POST']) -@require_login -@require_permission(Permission.MANAGE_CELL_MEMBERS) +# Rota para editar militante +@app.route("/militantes/editar/", methods=["GET", "POST"]) +@login_required +@session_timeout def editar_militante(id): - militante = db_session.query(Militante).get(id) - if not militante: - flash('Militante não encontrado.', 'error') - return redirect(url_for('listar_militantes')) - - if request.method == "POST": - cpf = request.form["cpf"] - if cpf != militante.cpf and not validar_cpf(cpf): # Só valida se o CPF foi alterado - flash('CPF inválido. Por favor, verifique o número informado.', 'error') - return render_template("editar_militante.html", militante=militante) - - try: - militante.nome = request.form["nome"] - militante.cpf = cpf - militante.email = request.form["email"] - militante.telefone = request.form["telefone"] - militante.endereco = request.form["endereco"] - militante.filiado = bool(request.form.get("filiado", False)) - - db_session.commit() - flash('Militante atualizado com sucesso!', 'success') + db = get_db_connection() + try: + militante = db.query(Militante).get(id) + if not militante: + flash('Militante não encontrado', 'danger') return redirect(url_for('listar_militantes')) - except Exception as e: - db_session.rollback() - flash('Erro ao atualizar militante. Verifique se o CPF ou email já não estão cadastrados.', 'error') - return render_template("editar_militante.html", militante=militante) - - return render_template("editar_militante.html", militante=militante) -@app.route('/dash') -@require_login -def dashboard_admin(): - """ - Rota para o dashboard administrativo - """ - db = get_db_connection() - try: - # Carregar usuários com seus relacionamentos - users = db.query(Usuario).options( - joinedload(Usuario.roles), - joinedload(Usuario.militante) - ).all() - - # Filtrar usuários baseado nas permissões - if not current_user.has_permission('system_config'): - if current_user.has_permission('manage_cr_sectors'): - users = [u for u in users if u.cr_id == current_user.cr_id] - elif current_user.has_permission('manage_sector_cells'): - users = [u for u in users if u.setor_id == current_user.setor_id] - elif current_user.has_permission('manage_cell_members'): - users = [u for u in users if u.celula_id == current_user.celula_id] - else: - users = [] - - # Carregar dados necessários antes de fechar a sessão - for user in users: - if user.militante: - user.militante.get_responsabilidades_list() - - return render_template('dashboard.html', users=users) + if request.method == "POST": + militante.nome = request.form.get("nome") + militante.cpf = request.form.get("cpf") + militante.email = request.form.get("email") + militante.telefone = request.form.get("telefone") + militante.endereco = request.form.get("endereco") + militante.filiado = request.form.get("filiado") == "on" + + if not validar_cpf(militante.cpf): + flash('CPF inválido', 'danger') + return render_template("editar_militante.html", militante=militante) + + try: + db.commit() + flash('Militante atualizado com sucesso!', 'success') + return redirect(url_for('listar_militantes')) + except Exception as e: + db.rollback() + print(f"Erro ao atualizar militante: {e}") + flash('Erro ao atualizar militante', 'danger') + return render_template("editar_militante.html", militante=militante) + + return render_template("editar_militante.html", militante=militante) finally: db.close() -@app.route("/usuarios//otp/reset", methods=["POST"]) -@require_login -@require_permission('system_config') -def reset_otp(user_id): - """Resetar OTP de um usuário""" - db = get_db_connection() - try: - user = db.query(Usuario).get(user_id) - if not user: - return jsonify({'success': False, 'message': 'Usuário não encontrado'}), 404 - - # Gerar novo OTP - user.otp_secret = pyotp.random_base32() - db.commit() - - return jsonify({'success': True, 'message': 'OTP resetado com sucesso'}) - finally: - db.close() +# Rota para criar um novo usuário +@app.route("/usuarios/novo", methods=["GET", "POST"]) +@login_required +def novo_usuario(): + if request.method == "POST": + username = request.form.get("username") + password = request.form.get("password") + email = request.form.get("email") + role_id = request.form.get("role_id") + setor_id = request.form.get("setor_id") -@app.route("/usuarios//password/reset", methods=["POST"]) -@require_login -@require_permission('system_config') -def reset_password(user_id): - """Resetar senha de um usuário""" - db = get_db_connection() - try: - user = db.query(Usuario).get(user_id) - if not user: - return jsonify({'success': False, 'message': 'Usuário não encontrado'}), 404 - - # Gerar nova senha aleatória - new_password = ''.join(random.choices(string.ascii_letters + string.digits, k=8)) - user.password_hash = generate_password_hash(new_password) - db.commit() - - # Enviar email com a nova senha + # Verificar se usuário já existe + db = get_db_connection() try: - msg = Message( - 'Sua senha foi resetada', - recipients=[user.email], - body=f''' - Olá {user.username}, - - Sua senha foi resetada pelo administrador do sistema. - - Sua nova senha é: {new_password} - - Por favor, altere sua senha após o primeiro login. - - Atenciosamente, - Sistema de Controle OCI - ''' + if db.query(Usuario).filter_by(username=username).first(): + flash('Nome de usuário já existe.', 'danger') + return render_template("novo_usuario.html") + + novo_usuario = Usuario( + username=username, + email=email, + role_id=role_id, + setor_id=setor_id ) - mail.send(msg) - return jsonify({'success': True, 'message': 'Senha resetada e enviada por email'}) + novo_usuario.set_password(password) + novo_usuario.otp_secret = pyotp.random_base32() + + db.add(novo_usuario) + db.commit() + flash('Usuário cadastrado com sucesso!', 'success') + return redirect(url_for('listar_usuarios')) except Exception as e: db.rollback() - return jsonify({'success': False, 'message': f'Erro ao enviar email: {str(e)}'}), 500 - + print(f"Erro ao cadastrar usuário: {e}") + flash('Erro ao cadastrar usuário', 'danger') + return render_template("novo_usuario.html") + finally: + db.close() + + db = get_db_connection() + try: + roles = db.query(Role).order_by(Role.nome).all() + setores = db.query(Setor).order_by(Setor.nome).all() + return render_template("novo_usuario.html", roles=roles, setores=setores) finally: db.close() +# Rota para verificar status da sessão @app.route('/check_session') def check_session(): - if not current_user.is_authenticated: + if 'user_id' not in session: return jsonify({'status': 'expired'}) - - db = get_db_connection() - try: - if current_user.is_session_expired(): - current_user.logout() - db.commit() - logout_user() - return jsonify({'status': 'expired'}) - current_user.update_last_activity() - db.commit() - return jsonify({'status': 'active'}) - finally: - db.close() + if 'last_activity' in session: + last_activity = datetime.fromtimestamp(session['last_activity']) + now = datetime.now() + + if now - last_activity > timedelta(hours=2): + # Registrar o logout por timeout + try: + db = get_db_connection() + user = db.query(Usuario).get(session.get('user_id')) + if user: + user.ultimo_logout = datetime.now() + user.motivo_logout = "Timeout de sessão" + db.commit() + db.close() + except Exception as e: + print(f"Erro ao registrar logout por timeout: {e}") + + session.clear() + return jsonify({'status': 'expired'}) + + return jsonify({'status': 'active'}) @app.route("/qr/") def get_qr_code(token): diff --git a/create_admin.py b/create_admin.py index c994bf7..b9dbaaa 100644 --- a/create_admin.py +++ b/create_admin.py @@ -1,91 +1,134 @@ -import os -from functions.database import get_db_connection, Usuario -from functions.rbac import Role -import pyotp +from functions.database import init_database, Usuario, Role, get_db_connection import qrcode -import base64 -from io import BytesIO +import os +from pathlib import Path +import pyotp -def create_admin(): - """Cria o usuário admin se não existir""" - db = get_db_connection() +def generate_qr_code(user): + """ + Gera o QR code para um usuário específico + + Args: + user: Instância do modelo Usuario + + Returns: + Path: Caminho do arquivo QR code gerado + """ + # Gerar QR Code apenas na raiz do projeto + qr_path = Path('admin_qr.png') + + # Remover arquivo antigo se existir + if qr_path.exists(): + os.remove(str(qr_path)) + + # Gerar e salvar QR Code + qr = qrcode.QRCode(version=1, box_size=10, border=5) + + # Gerar URI do OTP + totp = pyotp.TOTP(user.otp_secret) + otp_uri = totp.provisioning_uri( + name=user.username, + issuer_name="Sistema de Controles" + ) + + qr.add_data(otp_uri) + qr.make(fit=True) + img = qr.make_image(fill_color="black", back_color="white") + img.save(str(qr_path)) + + print(f"\nQR Code gerado em: {os.path.abspath(qr_path)}") + + return qr_path, otp_uri + +def create_admin_user(): + """Cria ou atualiza o usuário admin""" try: - # Verificar se o admin já existe - admin = db.query(Usuario).filter_by(username='admin').first() + # Inicializar banco de dados + init_database() - if admin: - print("Usuário admin já existe") + # Criar sessão + db = get_db_connection() + + try: + # Verificar se já existe um usuário admin + admin = db.query(Usuario).filter_by(username="admin").first() - # Verificar se o arquivo admin_qr.png existe - if os.path.exists('admin_qr.png'): - print("Usando OTP existente do arquivo admin_qr.png") - # Extrair o OTP secret do QR code existente - with open('admin_qr.png', 'rb') as f: - qr_data = f.read() - # Aqui você precisaria implementar a lógica para extrair o OTP secret do QR code - # Por enquanto, vamos apenas manter o OTP existente - return + if admin: + print("\n=== Usuário Admin Encontrado ===") else: - print("Gerando novo OTP para o admin...") - # Gerar novo OTP - otp_secret = pyotp.random_base32() - admin.otp_secret = otp_secret + print("\n=== Criando Novo Usuário Admin ===") + # Criar novo usuário admin + admin = Usuario( + username="admin", + email="admin@example.com", + is_admin=True + ) + admin.set_password("admin123") + admin.generate_otp_secret() + + # Adicionar e fazer commit + db.add(admin) db.commit() - else: - print("Criando usuário admin...") - # Criar usuário admin - admin = Usuario( - username='admin', - password='admin123', - is_admin=True - ) - admin.email = 'admin@controles.com' - db.add(admin) + + # Gerar QR code uma única vez + qr_path, otp_uri = generate_qr_code(admin) + + # Mostrar informações + print("\n=== Informações do Admin ===") + print(f"Username: {admin.username}") + print(f"Email: {admin.email}") + print(f"Senha: admin123") + print(f"Segredo OTP: {admin.otp_secret}") + + print("\n=== QR Code Gerado ===") + print(f"QR Code salvo em: {qr_path}") + print(f"URI do OTP: {otp_uri}") + + # Gerar código atual para verificação + totp = pyotp.TOTP(admin.otp_secret) + current_code = totp.now() + print("\n=== Verificação do OTP ===") + print(f"Código OTP atual: {current_code}") + print(f"Verificação do código: {totp.verify(current_code)}") + + print("\n=== Instruções para Configuração ===") + print("1. Instale um aplicativo autenticador no seu celular") + print(" (Google Authenticator, Microsoft Authenticator, etc)") + print("2. Abra o aplicativo") + print("3. Selecione a opção para adicionar uma nova conta") + print("4. Escaneie o QR Code salvo em:", qr_path) + print("\nOU configure manualmente:") + print(f"- Nome da conta: {admin.username}") + print(f"- Segredo: {admin.otp_secret}") + print("- Tipo: Baseado em tempo (TOTP)") + print("- Algoritmo: SHA1") + print("- Dígitos: 6") + print("- Intervalo: 30 segundos") + + # Verificação final + print("\n=== Teste de Verificação ===") + test_code = totp.now() + print(f"Código de teste: {test_code}") + is_valid = admin.verify_otp(test_code) + print(f"Verificação do código: {'Sucesso' if is_valid else 'Falha'}") + + if not is_valid: + print("\nALERTA: Verificação do OTP falhou!") + print("Por favor, verifique se o segredo OTP está correto.") + + # Fazer commit final para garantir que tudo foi salvo db.commit() - # Gerar OTP - otp_secret = pyotp.random_base32() - admin.otp_secret = otp_secret - db.commit() + except Exception as e: + db.rollback() + raise e + finally: + db.close() - # Atribuir role de Secretário Geral - admin_role = db.query(Role).filter_by(nivel=Role.SECRETARIO_GERAL).first() - if admin_role: - admin.roles.append(admin_role) - db.commit() - - # Gerar QR code - totp = pyotp.TOTP(otp_secret) - provisioning_uri = totp.provisioning_uri(admin.username, issuer_name="Sistema de Controles") - - qr = qrcode.QRCode(version=1, box_size=10, border=5) - qr.add_data(provisioning_uri) - qr.make(fit=True) - - img = qr.make_image(fill_color="black", back_color="white") - - # Salvar QR code como base64 - buffered = BytesIO() - img.save(buffered, format="PNG") - qr_base64 = base64.b64encode(buffered.getvalue()).decode() - - # Salvar QR code como arquivo - img.save('admin_qr.png') - - print("\nConfiguração do OTP para o admin:") - print(f"OTP Secret: {otp_secret}") - print("\nInstruções:") - print("1. Use um aplicativo autenticador (como Google Authenticator ou Authy)") - print("2. Escaneie o QR code ou insira o OTP Secret manualmente") - print("3. Use o código gerado para fazer login") - print("\nQR code salvo em 'admin_qr.png'") - except Exception as e: - print(f"Erro ao criar admin: {str(e)}") - db.rollback() - raise - finally: - db.close() + print(f"\nErro durante a execução: {e}") + import traceback + traceback.print_exc() -if __name__ == '__main__': - create_admin() \ No newline at end of file +if __name__ == "__main__": + create_admin_user() diff --git a/functions/__pycache__/database.cpython-312.pyc b/functions/__pycache__/database.cpython-312.pyc index 4b2081ab65b6f26ef19121cd37e79887645cbf9d..72b50420ff87b1f00c737f6de99d2a9d073f684b 100644 GIT binary patch delta 5793 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 R9}R^M+&rx(2Ddow{tFE +

Editar Cota

+
+
+ + +
+ Por favor, insira um valor válido. +
+
+
+ + +
+ Por favor, selecione uma data de vencimento. +
+
+
+ + +
+ + Cancelar +
+ +{% endblock %} \ No newline at end of file diff --git a/templates/home.html b/templates/home.html index 3db7d27..2d52ce9 100644 --- a/templates/home.html +++ b/templates/home.html @@ -5,9 +5,12 @@ {% block content %}
-

- {{ data_atual }} -

+
+

Olá, {{ nome_usuario }}!

+

+ {{ data_atual }} +

+
{% if current_user.has_permission('view_cell_data') %} @@ -37,16 +40,16 @@
-
-
-
Total de Cotas
-

R$ {{ total_cotas }}

+
Total de Cotas
+
+
+

R$ {{ total_cotas }}

-
+
- + Ver detalhes
@@ -100,7 +103,7 @@
-
+ {% endif %}
+ + {% endblock %} \ No newline at end of file