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 b6c6a0e..4b2081a 100644 Binary files a/functions/__pycache__/database.cpython-312.pyc and b/functions/__pycache__/database.cpython-312.pyc differ 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 %} -