From 241543ea63f525d18ceca7861ac2158dba45ad8e Mon Sep 17 00:00:00 2001 From: andersonid Date: Thu, 3 Apr 2025 20:58:02 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20melhorias=20na=20interface=20da=20home?= =?UTF-8?q?=20e=20navbar=20-=20Ajustes=20no=20layout=20da=20navbar=20e=20m?= =?UTF-8?q?enu=20-=20Corre=C3=A7=C3=A3o=20do=20logo=20e=20nome=20do=20sist?= =?UTF-8?q?ema=20-=20Melhorias=20no=20estilo=20dos=20cards=20da=20dashboar?= =?UTF-8?q?d=20-=20Ajustes=20nas=20permiss=C3=B5es=20e=20autentica=C3=A7?= =?UTF-8?q?=C3=A3o=20-=20Corre=C3=A7=C3=A3o=20de=20bugs=20na=20exibi=C3=A7?= =?UTF-8?q?=C3=A3o=20de=20mensagens?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin_qr.txt | 2 +- app.py | 388 ++++--------- create_test_users.py | 5 +- .../__pycache__/database.cpython-312.pyc | Bin 16026 -> 34805 bytes functions/database.py | 17 +- functions/decorators.py | 8 +- functions/rbac.py | 39 +- models.py | 23 + seed_data.py | 57 +- static/css/components.css | 80 +++ templates/base.html | 157 +++--- templates/listar_assinaturas.html | 509 +++++------------- 12 files changed, 506 insertions(+), 779 deletions(-) create mode 100644 models.py diff --git a/admin_qr.txt b/admin_qr.txt index 543118b..dce4a09 100644 --- a/admin_qr.txt +++ b/admin_qr.txt @@ -1 +1 @@ -otpauth://totp/Sistema%20de%20Controles:admin?secret=27NESPSPWKWIXVIDBUJPTK7MPAKGF4WG&issuer=Sistema%20de%20Controles \ No newline at end of file +otpauth://totp/Sistema%20de%20Controles:admin?secret=OXLVOR5LJVOIPRFPQTWQDJKTBVVR242U&issuer=Sistema%20de%20Controles \ No newline at end of file diff --git a/app.py b/app.py index 5161599..9edcc3c 100644 --- a/app.py +++ b/app.py @@ -41,7 +41,7 @@ import pyotp import qrcode import base64 from io import BytesIO -from create_admin import create_admin +from create_admin import create_admin_user from create_test_users import create_test_users from werkzeug.security import generate_password_hash, check_password_hash from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user @@ -112,7 +112,7 @@ init_rbac() # Criar admin e usuários de teste print("Criando usuários iniciais...") -create_admin() +create_admin_user() create_test_users() # Decorator para verificar se o usuário está logado @@ -178,27 +178,44 @@ def index(): def login(): """Rota de login""" if request.method == "POST": - email = request.form.get("email") + email_or_username = request.form.get("email") password = request.form.get("password") + otp = request.form.get("otp") - if not all([email, password]): - flash("Todos os campos são obrigatórios.", "error") + if not all([email_or_username, password]): + flash("Email/usuário e senha são obrigatórios.", "danger") return redirect(url_for("login")) db = get_db_connection() try: - user = db.query(Usuario).filter_by(email=email).first() + # Tenta encontrar o usuário por email ou username + user = db.query(Usuario).filter( + (Usuario.email == email_or_username) | + (Usuario.username == email_or_username) + ).first() if not user or not user.check_password(password): - flash("Email ou senha incorretos.", "error") + flash("Email/usuário ou senha incorretos.", "danger") + return redirect(url_for("login")) + + # Verificar OTP se o usuário tiver configurado + if user.otp_secret and not otp: + flash("Código OTP é obrigatório para sua conta.", "danger") + return redirect(url_for("login")) + + if user.otp_secret and not user.verify_otp(otp): + flash("Código OTP inválido.", "danger") return redirect(url_for("login")) # Atualizar último login user.ultimo_login = datetime.utcnow() db.commit() - # Fazer login + # Fazer login e setar sessão login_user(user) + session['user_id'] = user.id + session['username'] = user.username + session['is_admin'] = user.is_admin # Redirecionar para home return redirect(url_for("home")) @@ -238,22 +255,11 @@ def home(): data_atual = datetime.now().strftime("%d de %B de %Y") # Buscar dados para o dashboard - total_militantes = db.query(func.count(Militante.id)).scalar() - total_cotas = db.query(func.sum(CotaMensal.valor)).scalar() or 0 - total_materiais = db.query(func.sum(MaterialVendido.valor)).scalar() or 0 - total_assinaturas = db.query(func.sum(AssinaturaAnual.valor)).scalar() or 0 + 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).count() -<<<<<<< HEAD - return render_template( - "home.html", - nome_usuario=nome_usuario, - data_atual=data_atual, - total_militantes=total_militantes, - total_cotas=total_cotas, - total_materiais=total_materiais, - total_assinaturas=total_assinaturas - ) -======= # Buscar últimos militantes cadastrados ultimos_militantes = db.query(Militante)\ .order_by(Militante.id.desc())\ @@ -272,7 +278,7 @@ def home(): return render_template('home.html', nome_usuario=nome_usuario, - data_atual=data_formatada, + data_atual=data_atual, total_militantes=total_militantes, total_cotas="{:.2f}".format(total_cotas), total_materiais=total_materiais, @@ -294,7 +300,6 @@ def home(): total_assinaturas=0, ultimos_militantes=[], ultimos_pagamentos=[]) ->>>>>>> 324660d (refactor: melhorias na interface e funcionalidades - Atualização do layout do dashboard com Bootstrap 5 - Remoção do template editar_pagamento.html (integrado ao modal) - Melhorias no template home.html com cards estatísticos - Ajustes nos estilos e responsividade - Correções nas rotas e conexões do banco de dados - Implementação do modal de edição de pagamentos - Adição de efeitos hover e melhorias visuais) finally: db.close() @@ -1329,273 +1334,6 @@ def toggle_quadro_orientador(user_id): finally: db.close() -<<<<<<< HEAD -@app.route('/usuarios//toggle_aspirante', methods=['POST']) -@require_login -def toggle_aspirante(user_id): - 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 - - # Verificar permissões - if not (current_user.has_permission('system_config') or - (current_user.has_permission('manage_cr_sectors') and user.cr_id == current_user.cr_id) or - (current_user.has_permission('manage_sector_cells') and user.setor_id == current_user.setor_id)): - return jsonify({'success': False, 'message': 'Você não tem permissão para alterar este status'}), 403 - - # Verificar se o usuário tem um militante associado - if not user.militante: - return jsonify({'success': False, 'message': 'Usuário não tem um militante associado'}), 400 - - # Se estiver tentando remover o status de aspirante - if user.militante.aspirante: - # Verificar se já passaram 3 meses - if datetime.utcnow() - user.militante.data_inicio_aspirante < timedelta(days=90): - return jsonify({ - 'success': False, - 'message': 'Não é possível remover o status de aspirante antes de 3 meses de integração' - }), 400 - - # Verificar se há avaliação - if not user.militante.avaliacao_aspirante: - return jsonify({ - 'success': False, - 'message': 'É necessário registrar uma avaliação antes de remover o status de aspirante' - }), 400 - - # Alternar o status de Aspirante - user.militante.aspirante = not user.militante.aspirante - - # Atualizar a responsabilidade no campo responsabilidades - if user.militante.aspirante: - user.militante.responsabilidades |= Militante.ASPIRANTE - user.militante.data_inicio_aspirante = datetime.utcnow() - user.militante.avaliacao_aspirante = None - user.militante.data_avaliacao_aspirante = None - else: - user.militante.responsabilidades &= ~Militante.ASPIRANTE - user.militante.data_avaliacao_aspirante = datetime.utcnow() - - db.commit() - return jsonify({ - 'success': True, - 'message': f'Status de Aspirante {"adicionado" if user.militante.aspirante else "removido"} com sucesso' - }) - except Exception as e: - db.rollback() - return jsonify({'success': False, 'message': str(e)}), 500 - finally: - db.close() - -@app.route('/usuarios//avaliar_aspirante', methods=['POST']) -@require_login -def avaliar_aspirante(user_id): - 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 - - # Verificar permissões - if not (current_user.has_permission('system_config') or - (current_user.has_permission('manage_cr_sectors') and user.cr_id == current_user.cr_id) or - (current_user.has_permission('manage_sector_cells') and user.setor_id == current_user.setor_id)): - return jsonify({'success': False, 'message': 'Você não tem permissão para avaliar este aspirante'}), 403 - - # Verificar se o usuário tem um militante associado e é aspirante - if not user.militante or not user.militante.aspirante: - return jsonify({'success': False, 'message': 'Usuário não é um aspirante'}), 400 - - # Verificar se já passaram 3 meses - if datetime.utcnow() - user.militante.data_inicio_aspirante < timedelta(days=90): - return jsonify({ - 'success': False, - 'message': 'Não é possível avaliar o aspirante antes de 3 meses de integração' - }), 400 - - # Obter a avaliação do corpo da requisição - avaliacao = request.json.get('avaliacao') - if not avaliacao: - return jsonify({'success': False, 'message': 'A avaliação é obrigatória'}), 400 - - # Atualizar a avaliação - user.militante.avaliacao_aspirante = avaliacao - user.militante.data_avaliacao_aspirante = datetime.utcnow() - - db.commit() - return jsonify({ - 'success': True, - 'message': 'Avaliação registrada com sucesso' - }) - except Exception as e: - db.rollback() - return jsonify({'success': False, 'message': str(e)}), 500 - finally: - db.close() - -@app.template_filter('has_permission') -def has_permission(value, permission): - """Verifica se o valor contém a permissão especificada.""" - return bool(value & permission) - -@app.route('/militante/desligar/', methods=['POST']) -@login_required -def desligar_militante(id): - """Desliga um militante e desativa seu usuário associado""" - militante = db_session.query(Militante).get(id) - if militante is None: - flash('Militante não encontrado.', 'danger') - return redirect(url_for('listar_militantes')) - - motivo = request.form.get('motivo') - if not motivo: - flash('É necessário informar o motivo do desligamento.', 'danger') - return redirect(url_for('listar_militantes')) - - # Atualizar estado do militante - militante.estado = EstadoMilitante.DESLIGADO.value - militante.data_desligamento = datetime.utcnow() - militante.motivo_desligamento = motivo - - # Desativar usuário associado se existir - if militante.usuario: - militante.usuario.ativo = False - militante.usuario.ultimo_logout = datetime.utcnow() - militante.usuario.motivo_logout = "Desligamento do militante" - - db_session.commit() - flash('Militante desligado com sucesso.', 'success') - return redirect(url_for('listar_militantes')) - -@app.route('/editar_pagamento/', methods=['GET', 'POST']) -@require_login -def editar_pagamento(id): - user = current_user - - # Verificar permissões do usuário - if not (user.has_permission(Permission.EDIT_CELL_PAYMENT) or - user.has_permission(Permission.EDIT_SECTOR_PAYMENT) or - user.has_permission(Permission.EDIT_CR_PAYMENT) or - user.has_permission(Permission.EDIT_CC_PAYMENT)): - flash('Você não tem permissão para editar pagamentos.', 'error') - return redirect(url_for('home')) - - pagamento = db_session.query(Pagamento).get(id) - if not pagamento: - flash('Pagamento não encontrado.', 'error') - return redirect(url_for('listar_pagamentos')) - - if request.method == 'POST': - pagamento.valor = request.form['valor'] - pagamento.data_pagamento = datetime.strptime(request.form['data_pagamento'], '%Y-%m-%d') - - db_session.commit() - flash('Pagamento atualizado com sucesso!', 'success') - return redirect(url_for('listar_pagamentos')) - - return render_template('editar_pagamento.html', pagamento=pagamento) - -@app.before_request -def session_timeout(): - """Verifica se a sessão expirou""" - if current_user.is_authenticated: - if 'last_activity' not in session: - session['last_activity'] = time() - return - - last_activity = datetime.fromtimestamp(session['last_activity']) - now = datetime.now() - - # Se passaram mais de 30 minutos (configurável) - timeout_minutes = 30 - if now - last_activity > timedelta(minutes=timeout_minutes): - # Registrar o logout por timeout - try: - current_user.ultimo_logout = datetime.now() - current_user.motivo_logout = "Timeout de sessão" - db_session.commit() - except Exception as e: - print(f"Erro ao registrar logout por timeout: {e}") - - session.clear() - flash('Sua sessão expirou. Por favor, faça login novamente.', 'warning') - return redirect(url_for('login')) - - # Atualizar timestamp de último acesso - session['last_activity'] = time() - - # Atualizar também no banco de dados - try: - current_user.update_last_activity() - db_session.commit() - except Exception as e: - print(f"Erro ao atualizar última atividade: {e}") -======= -# API Routes para os modais -@app.route('/api/pagamentos/', methods=['GET']) -@login_required -def get_pagamento(id): - db = get_db_connection() - try: - pagamento = db.query(Pagamento).get(id) - if not pagamento: - return jsonify({'success': False, 'message': 'Pagamento não encontrado'}), 404 - - return jsonify({ - 'id': pagamento.id, - 'valor': pagamento.valor, - 'data_pagamento': pagamento.data_pagamento.strftime('%Y-%m-%d'), - 'tipo_pagamento_id': pagamento.tipo_pagamento_id, - 'observacao': pagamento.observacao - }) - finally: - db.close() - -@app.route('/api/pagamentos/', methods=['PUT']) -@login_required -def update_pagamento(id): - db = get_db_connection() - try: - pagamento = db.query(Pagamento).get(id) - if not pagamento: - return jsonify({'success': False, 'message': 'Pagamento não encontrado'}), 404 - - try: - pagamento.valor = float(request.form['valor']) - pagamento.data_pagamento = datetime.strptime(request.form['data_pagamento'], '%Y-%m-%d') - pagamento.tipo_pagamento_id = int(request.form['tipo_pagamento']) - pagamento.observacao = request.form['observacao'] - - db.commit() - return jsonify({'success': True}) - except Exception as e: - db.rollback() - return jsonify({'success': False, 'message': str(e)}), 400 - finally: - db.close() - -@app.route('/api/pagamentos/', methods=['DELETE']) -@login_required -def delete_pagamento(id): - db = get_db_connection() - try: - pagamento = db.query(Pagamento).get(id) - if not pagamento: - return jsonify({'success': False, 'message': 'Pagamento não encontrado'}), 404 - - try: - db.delete(pagamento) - db.commit() - return jsonify({'success': True}) - except Exception as e: - db.rollback() - return jsonify({'success': False, 'message': str(e)}), 400 - finally: - db.close() ->>>>>>> 324660d (refactor: melhorias na interface e funcionalidades - Atualização do layout do dashboard com Bootstrap 5 - Remoção do template editar_pagamento.html (integrado ao modal) - Melhorias no template home.html com cards estatísticos - Ajustes nos estilos e responsividade - Correções nas rotas e conexões do banco de dados - Implementação do modal de edição de pagamentos - Adição de efeitos hover e melhorias visuais) - @app.route("/cotas/excluir/", methods=["POST"]) @login_required @session_timeout @@ -1620,6 +1358,70 @@ def excluir_cota(id): db.close() return redirect(url_for('listar_cotas')) +@app.route("/assinaturas") +@require_login +@require_permission(Permission.MANAGE_CELL_REPORTS) +def listar_assinaturas(): + db = get_db_connection() + try: + assinaturas = db.query(AssinaturaAnual).join(Militante).all() + militantes = db.query(Militante).all() + return render_template('listar_assinaturas.html', + assinaturas=assinaturas, + militantes=militantes) + finally: + db.close() + +@app.route("/assinaturas/novo", methods=['POST']) +@require_login +@require_permission(Permission.MANAGE_CELL_REPORTS) +def nova_assinatura(): + db = get_db_connection() + try: + data = request.form + + # Validar dados + if not all(k in data for k in ['militante_id', 'data_inicio', 'data_fim', 'valor']): + return jsonify({'success': False, 'message': 'Todos os campos são obrigatórios'}) + + # Criar nova assinatura + assinatura = AssinaturaAnual( + militante_id=data['militante_id'], + data_inicio=datetime.strptime(data['data_inicio'], '%Y-%m-%d'), + data_fim=datetime.strptime(data['data_fim'], '%Y-%m-%d'), + valor=float(data['valor']) + ) + + db.add(assinatura) + db.commit() + + return jsonify({'success': True}) + except Exception as e: + db.rollback() + return jsonify({'success': False, 'message': str(e)}) + finally: + db.close() + +@app.route("/assinaturas/excluir/", methods=['POST']) +@require_login +@require_permission(Permission.MANAGE_CELL_REPORTS) +def excluir_assinatura(id): + db = get_db_connection() + try: + assinatura = db.query(AssinaturaAnual).get(id) + if not assinatura: + return jsonify({'success': False, 'message': 'Assinatura não encontrada'}) + + db.delete(assinatura) + db.commit() + + return jsonify({'success': True}) + except Exception as e: + db.rollback() + return jsonify({'success': False, 'message': str(e)}) + finally: + db.close() + def create_app(): app = Flask(__name__) # ... existing code ... @@ -1636,7 +1438,7 @@ def init_system(): init_database() # Criar admin - create_admin() + create_admin_user() # Criar usuários de teste create_test_users() diff --git a/create_test_users.py b/create_test_users.py index 51e0e39..a56c102 100644 --- a/create_test_users.py +++ b/create_test_users.py @@ -51,10 +51,11 @@ def create_test_users(): # Criar usuário user = Usuario( username=user_data['username'], - password=user_data['password'], + email=user_data['email'], is_admin=user_data['is_admin'] ) - user.email = user_data['email'] + user.set_password(user_data['password']) + user.tipo = "ADMIN" if user_data['is_admin'] else "USUARIO" db.add(user) db.commit() diff --git a/functions/__pycache__/database.cpython-312.pyc b/functions/__pycache__/database.cpython-312.pyc index 3a339b70346946e8e6d3767ce4ea297500cf0f3d..7fefa1e9f0cc0400e285ebaa1f4a50298b20647b 100644 GIT binary patch literal 34805 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 .container-fluid { width: 100%; - max-width: 1140px; + max-width: 1320px; 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; - } + padding: 0; + display: flex; + justify-content: space-between; + align-items: center; } .navbar-brand { - font-weight: bold; - color: var(--primary-color) !important; + flex: 0 0 auto; + margin-right: 2rem; + font-weight: 500; + color: #fff !important; display: flex; align-items: center; - margin-right: 3rem; white-space: nowrap; + font-size: 1.2rem; } .navbar-brand img { - height: 40px; - margin-right: 10px; + height: 35px; + margin-right: 0.75rem; } #navbarNav { display: flex; justify-content: center; - flex-grow: 1; + flex: 1; } - .navbar-nav.me-auto { - margin: 0 auto !important; + .navbar-nav.mx-auto { + margin: 0 auto; } .navbar-nav:last-child { - margin-left: auto; + flex: 0 0 auto; + margin-left: 2rem; } .nav-link { - color: #fff !important; - transition: color 0.3s ease; - padding: 0.5rem 1rem; + color: rgba(255,255,255,0.85) !important; + transition: all 0.2s ease; + padding: 0.75rem 1rem; white-space: nowrap; + font-size: 0.95rem; + font-weight: 400; + letter-spacing: 0.3px; } .nav-link:hover { - color: rgba(255, 255, 255, 0.8) !important; + color: #fff !important; + background-color: var(--primary-color); + border-radius: 4px; } - .nav-link.active { - color: rgba(255, 255, 255, 0.9) !important; - font-weight: 500; + .nav-link i { + font-size: 0.9rem; + opacity: 0.9; + margin-right: 0.5rem; } .dropdown-menu { - border: none; - box-shadow: 0 0.5rem 1rem rgba(0,0,0,0.15); + background-color: #343a40; + border: 1px solid rgba(255,255,255,0.1); + box-shadow: 0 4px 15px rgba(0,0,0,0.2); + padding: 0.5rem; + margin-top: 0.5rem; + border-radius: 8px; + min-width: 200px; } .dropdown-item { - padding: 0.5rem 1.5rem; - transition: background-color 0.2s ease; + color: rgba(255,255,255,0.85) !important; + font-size: 0.9rem; + font-weight: 400; + padding: 0.6rem 1rem; + transition: all 0.2s ease; + border-radius: 6px; } .dropdown-item:hover { - background-color: #f8f9fa; + background-color: var(--primary-color); + color: #fff !important; + transform: translateX(3px); } .dropdown-item i { - margin-right: 0.5rem; + margin-right: 0.75rem; width: 1.25rem; text-align: center; + font-size: 0.9rem; + opacity: 0.9; + } + + .dropdown-divider { + border-top: 1px solid rgba(255,255,255,0.1); + margin: 0.5rem 0; + } + + /* Estilo para o menu mobile */ + @media (max-width: 768px) { + .navbar-collapse { + background-color: #343a40; + padding: 1rem; + border-radius: 0 0 10px 10px; + margin-top: 0.5rem; + } + + .navbar-brand img { + height: 30px; + } + + .dropdown-menu { + background-color: rgba(0,0,0,0.2); + margin-left: 1rem; + min-width: auto; + } + + .nav-link { + padding: 0.5rem 1rem; + } + + .navbar-nav { + flex-direction: column; + align-items: stretch; + } } .container { @@ -474,9 +507,9 @@ {% if session.get('user_id') %}