From 54261e455ca69977fda725fc4ed2b2c5435be27a Mon Sep 17 00:00:00 2001 From: andersonid Date: Wed, 2 Apr 2025 14:14:37 -0300 Subject: [PATCH] feat: melhorias na interface e estrutura do frontend --- app.py | 141 +++++---- .../__pycache__/database.cpython-312.pyc | Bin 1708 -> 15652 bytes static/css/style.css | 113 +++++++ static/js/forms.js | 146 +++++++++ static/js/main.js | 145 +++++++++ templates/base.html | 190 ++++++------ templates/home.html | 173 +++++++++-- templates/listar_militantes.html | 287 ++++++++++++++---- templates/login.html | 88 ++++-- 9 files changed, 1009 insertions(+), 274 deletions(-) create mode 100644 static/css/style.css create mode 100644 static/js/forms.js create mode 100644 static/js/main.js diff --git a/app.py b/app.py index 1b0aedd..b116d47 100644 --- a/app.py +++ b/app.py @@ -48,6 +48,7 @@ from werkzeug.security import generate_password_hash, check_password_hash from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user import random import string +from sqlalchemy.sql import func load_dotenv() @@ -235,44 +236,48 @@ def logout(): @app.route("/home") @require_login def home(): - """Página inicial""" - links = [] - - # Links básicos para todos os usuários - links.append({ - 'text': 'Alterar Senha', - 'url': url_for('alterar_senha') - }) - - # Links específicos baseados em permissões - if current_user.has_permission('view_cell_data'): - links.append({ - 'text': 'Militantes', - 'url': url_for('listar_militantes') - }) - - if current_user.has_permission('view_cell_reports'): - links.append({ - 'text': 'Pagamentos', - 'url': url_for('listar_pagamentos') - }) - links.append({ - 'text': 'Materiais', - 'url': url_for('listar_materiais') - }) - links.append({ - 'text': 'Vendas', - 'url': url_for('listar_relatorios_vendas') - }) - - # Links para admin - if current_user.is_admin: - links.append({ - 'text': 'Dashboard Admin', - 'url': url_for('dashboard_admin') - }) - - return render_template('home.html', links=links) + """Página inicial do sistema com dashboard""" + try: + # 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( + 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 pagamentos + ultimos_pagamentos = db_session.query(Pagamento)\ + .join(Militante)\ + .order_by(Pagamento.data_pagamento.desc())\ + .limit(5)\ + .all() + + return render_template('home.html', + 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', 'error') + return render_template('home.html', + total_militantes=0, + total_cotas="0.00", + total_materiais=0, + total_assinaturas=0, + ultimos_militantes=[], + ultimos_pagamentos=[]) # Rota para criar um novo militante @app.route('/militantes/criar', methods=['GET', 'POST']) @@ -354,36 +359,44 @@ def criar_militante(): @require_login @require_permission(Permission.VIEW_CELL_DATA) def listar_militantes(): - """Lista todos os militantes""" - db = get_db_connection() try: - # Adicionar opção de filtro por estado - estado = request.args.get('estado', 'todos') + militantes = db_session.query(Militante).order_by(Militante.nome).all() + return render_template("listar_militantes.html", militantes=militantes) + except Exception as e: + print(f"Erro ao listar militantes: {e}") + flash('Erro ao carregar a lista de militantes', 'error') + return render_template("listar_militantes.html", militantes=[]) + +@app.route("/militantes/excluir/", methods=["POST"]) +@login_required +@session_timeout +def excluir_militante(id): + try: + militante = db_session.query(Militante).get(id) + if not militante: + flash('Militante não encontrado.', 'error') + return redirect(url_for('listar_militantes')) - query = db.query(Militante) - if estado != 'todos': - query = query.filter(Militante.estado == estado) + # 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 - militantes = query.all() + 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')) - # Precomputar todas as responsabilidades antes de renderizar - for militante in militantes: - militante.is_responsavel_financas = bool(militante.responsabilidades & Militante.RESPONSAVEL_FINANCAS) - militante.is_responsavel_imprensa = bool(militante.responsabilidades & Militante.RESPONSAVEL_IMPRENSA) - militante.is_quadro_orientador = bool(militante.responsabilidades & Militante.QUADRO_ORIENTADOR) - - return render_template( - "listar_militantes.html", - militantes=militantes, - estado_atual=estado, - estados=[ - ('todos', 'Todos'), - (EstadoMilitante.ATIVO.value, 'Ativos'), - (EstadoMilitante.DESLIGADO.value, 'Desligados') - ] - ) - finally: - db.close() + db_session.delete(militante) + db_session.commit() + flash('Militante excluído com sucesso!', 'success') + except Exception as e: + print(f"Erro ao excluir militante: {e}") + db_session.rollback() + flash('Erro ao excluir militante.', 'error') + + return redirect(url_for('listar_militantes')) # Rota para criar uma nova cota mensal @app.route("/cotas/novo", methods=["GET", "POST"]) diff --git a/functions/__pycache__/database.cpython-312.pyc b/functions/__pycache__/database.cpython-312.pyc index b6c6a0e36f761298581b30aa088ae1ac765f4e87..4b2081ab65b6f26ef19121cd37e79887645cbf9d 100644 GIT binary patch literal 15652 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 literal 1708 zcmbtU&1)M+6o0d`E3LHp@Wyr;)h3Y@r(xq@K}-V;CB=wRT7oX=CAtVjyLM%yU3+#F zM?p$W$svJKx0kw53Oy9+;vD)fxH`{p-q z-kW(}zsKWIK$@Alu&7XgXM7Qm;0t?~P}l$+bW()^!LLw-notl3$7q$-#Da*jcn=Da z9@3?Iq!4NlE&Mk(kk^PSeaM)_P>AR^i)G7Ps4SL!D~IWb|}c$Y*)PUKx)!^|}hfrV;a>V%JCYidVQL*0}T5pofgxM!D{_&`RB4jVozQ z_R7``mFvvrT{F-y5U^^sisQyAHXj|l#tq$-%2mrYyl!HAf@zW~d6wMJa*35{HsdK} zDrY<~Hm{>JCl*J^jr-<{-XV4hF^=1RAzOo8HS>o$z4l2jthU3cUUIZADAJjpI zI%;N9Z*zsY`=OqkSWi4id|m%;p(~&3$tQN?R7XyA<%wQ$ydQCeN1rO=uVLe$AHgm? z=*zF?a^KeQq3iF?Uxw|=k(^AoKS<;#{Vgu$6nI1>L>|eM%gS8tD%D6{3%f~gw9WcS zuo&ZgHyCIm2;xwm9gg?Gu;YWsi!I}mc@O_E2*3P(&C=^t<0_j(jE`TNr=U*>A&=qY W6PS7e?>rYnWbDOAlAK`^xcLu?qg;^y diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..c2f6239 --- /dev/null +++ b/static/css/style.css @@ -0,0 +1,113 @@ +:root { + --primary-color: #1a237e; + --secondary-color: #0d47a1; + --accent-color: #2962ff; + --background-color: #f5f5f5; + --text-color: #212121; +} + +body { + background-color: var(--background-color); + color: var(--text-color); +} + +.navbar { + background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)) !important; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.navbar-brand { + font-weight: 600; + font-size: 1.4rem; +} + +.nav-link { + font-weight: 500; + transition: all 0.3s ease; +} + +.nav-link:hover { + color: #fff !important; + transform: translateY(-2px); +} + +.card { + border: none; + border-radius: 10px; + box-shadow: 0 2px 10px rgba(0,0,0,0.05); + transition: transform 0.3s ease; +} + +.card:hover { + transform: translateY(-5px); +} + +.btn-primary { + background-color: var(--accent-color); + border: none; + padding: 0.5rem 1.5rem; + border-radius: 5px; + font-weight: 500; +} + +.btn-primary:hover { + background-color: var(--secondary-color); + transform: translateY(-2px); + box-shadow: 0 2px 5px rgba(0,0,0,0.2); +} + +.table { + background: white; + border-radius: 10px; + overflow: hidden; + box-shadow: 0 2px 10px rgba(0,0,0,0.05); +} + +.table thead th { + background-color: var(--primary-color); + color: white; + font-weight: 500; + border: none; +} + +.form-control { + border-radius: 5px; + border: 1px solid #e0e0e0; + padding: 0.75rem; +} + +.form-control:focus { + border-color: var(--accent-color); + box-shadow: 0 0 0 0.2rem rgba(41, 98, 255, 0.25); +} + +.alert { + border-radius: 10px; + border: none; + box-shadow: 0 2px 5px rgba(0,0,0,0.05); +} + +/* Animações para feedback */ +@keyframes fadeIn { + from { opacity: 0; transform: translateY(-10px); } + to { opacity: 1; transform: translateY(0); } +} + +.alert { + animation: fadeIn 0.3s ease; +} + +/* Responsividade */ +@media (max-width: 768px) { + .navbar-brand { + font-size: 1.2rem; + } + + .container { + padding: 1rem; + } + + .card { + margin-bottom: 1rem; + } +} \ No newline at end of file diff --git a/static/js/forms.js b/static/js/forms.js new file mode 100644 index 0000000..f44128c --- /dev/null +++ b/static/js/forms.js @@ -0,0 +1,146 @@ +// Validação de CPF +function validarCPF(cpf) { + cpf = cpf.replace(/[^\d]/g, ''); + + if (cpf.length !== 11) return false; + + // Verifica se todos os dígitos são iguais + if (/^(\d)\1{10}$/.test(cpf)) return false; + + // Validação do primeiro dígito verificador + let soma = 0; + for (let i = 0; i < 9; i++) { + soma += parseInt(cpf.charAt(i)) * (10 - i); + } + let resto = 11 - (soma % 11); + let dv1 = resto > 9 ? 0 : resto; + + if (dv1 !== parseInt(cpf.charAt(9))) return false; + + // Validação do segundo dígito verificador + soma = 0; + for (let i = 0; i < 10; i++) { + soma += parseInt(cpf.charAt(i)) * (11 - i); + } + resto = 11 - (soma % 11); + let dv2 = resto > 9 ? 0 : resto; + + if (dv2 !== parseInt(cpf.charAt(10))) return false; + + return true; +} + +// Validação de email +function validarEmail(email) { + const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return re.test(email); +} + +// Validação de telefone +function validarTelefone(telefone) { + telefone = telefone.replace(/[^\d]/g, ''); + return telefone.length >= 10 && telefone.length <= 11; +} + +// Inicialização dos formulários +document.addEventListener('DOMContentLoaded', function() { + // Validação personalizada para CPF + const cpfInputs = document.querySelectorAll('input[name="cpf"]'); + cpfInputs.forEach(input => { + input.addEventListener('blur', function() { + const cpf = this.value; + if (!validarCPF(cpf)) { + this.setCustomValidity('CPF inválido'); + this.classList.add('is-invalid'); + } else { + this.setCustomValidity(''); + this.classList.remove('is-invalid'); + } + }); + }); + + // Validação personalizada para email + const emailInputs = document.querySelectorAll('input[type="email"]'); + emailInputs.forEach(input => { + input.addEventListener('blur', function() { + const email = this.value; + if (!validarEmail(email)) { + this.setCustomValidity('Email inválido'); + this.classList.add('is-invalid'); + } else { + this.setCustomValidity(''); + this.classList.remove('is-invalid'); + } + }); + }); + + // Validação personalizada para telefone + const phoneInputs = document.querySelectorAll('input[name="telefone"]'); + phoneInputs.forEach(input => { + input.addEventListener('blur', function() { + const telefone = this.value; + if (!validarTelefone(telefone)) { + this.setCustomValidity('Telefone inválido'); + this.classList.add('is-invalid'); + } else { + this.setCustomValidity(''); + this.classList.remove('is-invalid'); + } + }); + }); + + // Validação de campos monetários + const moneyInputs = document.querySelectorAll('input[type="number"][step="0.01"]'); + moneyInputs.forEach(input => { + input.addEventListener('blur', function() { + const value = parseFloat(this.value); + if (isNaN(value) || value < 0) { + this.setCustomValidity('Valor inválido'); + this.classList.add('is-invalid'); + } else { + this.setCustomValidity(''); + this.classList.remove('is-invalid'); + this.value = value.toFixed(2); + } + }); + }); + + // Validação de datas + const dateInputs = document.querySelectorAll('input[type="date"]'); + dateInputs.forEach(input => { + input.addEventListener('change', function() { + const date = new Date(this.value); + const today = new Date(); + + if (this.hasAttribute('min')) { + const minDate = new Date(this.getAttribute('min')); + if (date < minDate) { + this.setCustomValidity(`A data não pode ser anterior a ${minDate.toLocaleDateString()}`); + this.classList.add('is-invalid'); + return; + } + } + + if (this.hasAttribute('max')) { + const maxDate = new Date(this.getAttribute('max')); + if (date > maxDate) { + this.setCustomValidity(`A data não pode ser posterior a ${maxDate.toLocaleDateString()}`); + this.classList.add('is-invalid'); + return; + } + } + + this.setCustomValidity(''); + this.classList.remove('is-invalid'); + }); + }); + + // Feedback visual para campos obrigatórios + const requiredInputs = document.querySelectorAll('input[required], select[required], textarea[required]'); + requiredInputs.forEach(input => { + const label = input.previousElementSibling; + if (label && label.tagName === 'LABEL') { + label.innerHTML += ' *'; + } + }); +}); \ No newline at end of file diff --git a/static/js/main.js b/static/js/main.js new file mode 100644 index 0000000..fa31277 --- /dev/null +++ b/static/js/main.js @@ -0,0 +1,145 @@ +// Máscaras para campos de formulário +document.addEventListener('DOMContentLoaded', function() { + // Máscara para CPF + const cpfInputs = document.querySelectorAll('input[name="cpf"]'); + cpfInputs.forEach(input => { + input.addEventListener('input', function(e) { + let value = e.target.value.replace(/\D/g, ''); + if (value.length <= 11) { + value = value.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, "$1.$2.$3-$4"); + e.target.value = value; + } + }); + }); + + // Máscara para telefone + const phoneInputs = document.querySelectorAll('input[name="telefone"]'); + phoneInputs.forEach(input => { + input.addEventListener('input', function(e) { + let value = e.target.value.replace(/\D/g, ''); + if (value.length <= 11) { + if (value.length === 11) { + value = value.replace(/(\d{2})(\d{5})(\d{4})/, "($1) $2-$3"); + } else { + value = value.replace(/(\d{2})(\d{4})(\d{4})/, "($1) $2-$3"); + } + e.target.value = value; + } + }); + }); + + // Formatação de valores monetários + const moneyInputs = document.querySelectorAll('input[type="number"][step="0.01"]'); + moneyInputs.forEach(input => { + input.addEventListener('blur', function(e) { + const value = parseFloat(e.target.value); + if (!isNaN(value)) { + e.target.value = value.toFixed(2); + } + }); + }); +}); + +// Funções para tabelas +document.addEventListener('DOMContentLoaded', function() { + const tables = document.querySelectorAll('.table'); + tables.forEach(table => { + // Ordenação + const headers = table.querySelectorAll('th[data-sort]'); + headers.forEach(header => { + header.addEventListener('click', function() { + const column = this.dataset.sort; + const asc = this.classList.toggle('sort-asc'); + const tbody = table.querySelector('tbody'); + const rows = Array.from(tbody.querySelectorAll('tr')); + + rows.sort((a, b) => { + const aVal = a.querySelector(`td[data-${column}]`).dataset[column]; + const bVal = b.querySelector(`td[data-${column}]`).dataset[column]; + return asc ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal); + }); + + rows.forEach(row => tbody.appendChild(row)); + }); + }); + + // Filtro + const filterInput = document.querySelector(`#filter-${table.id}`); + if (filterInput) { + filterInput.addEventListener('input', function() { + const searchTerm = this.value.toLowerCase(); + const rows = table.querySelectorAll('tbody tr'); + + rows.forEach(row => { + const text = row.textContent.toLowerCase(); + row.style.display = text.includes(searchTerm) ? '' : 'none'; + }); + }); + } + }); +}); + +// Validação de formulários +document.addEventListener('DOMContentLoaded', function() { + const forms = document.querySelectorAll('form'); + forms.forEach(form => { + form.addEventListener('submit', function(e) { + if (!form.checkValidity()) { + e.preventDefault(); + e.stopPropagation(); + + // Destacar campos inválidos + const invalidInputs = form.querySelectorAll(':invalid'); + invalidInputs.forEach(input => { + input.classList.add('is-invalid'); + + // Adicionar mensagem de erro + const feedback = document.createElement('div'); + feedback.className = 'invalid-feedback'; + feedback.textContent = input.validationMessage; + input.parentNode.appendChild(feedback); + }); + } + + form.classList.add('was-validated'); + }); + }); +}); + +// Animações e feedback visual +document.addEventListener('DOMContentLoaded', function() { + // Animar cards ao carregar + const cards = document.querySelectorAll('.card'); + cards.forEach((card, index) => { + card.style.opacity = '0'; + card.style.transform = 'translateY(20px)'; + setTimeout(() => { + card.style.transition = 'all 0.3s ease'; + card.style.opacity = '1'; + card.style.transform = 'translateY(0)'; + }, index * 100); + }); + + // Feedback visual para ações + const actionButtons = document.querySelectorAll('[data-action]'); + actionButtons.forEach(button => { + button.addEventListener('click', function() { + button.classList.add('animate__animated', 'animate__pulse'); + setTimeout(() => { + button.classList.remove('animate__animated', 'animate__pulse'); + }, 1000); + }); + }); +}); + +// Confirmações de ações +document.addEventListener('DOMContentLoaded', function() { + const deleteButtons = document.querySelectorAll('[data-confirm]'); + deleteButtons.forEach(button => { + button.addEventListener('click', function(e) { + if (!confirm(this.dataset.confirm)) { + e.preventDefault(); + } + }); + }); +}); \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index ab18881..9d9858b 100644 --- a/templates/base.html +++ b/templates/base.html @@ -3,132 +3,113 @@ - {% block title %}{% endblock %} - Sistema de Controle OCI - - - + {% block title %}{% endblock %} - Sistema de Gestão + {{ bootstrap.load_css() }} + + + + {% block extra_css %}{% endblock %} -