feat: Melhorias no Dashboard
- 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
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -263,3 +263,6 @@ database.db
|
|||||||
admin_qr.png
|
admin_qr.png
|
||||||
|
|
||||||
# End of https://www.toptal.com/developers/gitignore/api/python,flask
|
# End of https://www.toptal.com/developers/gitignore/api/python,flask
|
||||||
|
|
||||||
|
# Documentação temporária
|
||||||
|
docs/alteracoes_db_connection.md
|
||||||
|
|||||||
4
Makefile
4
Makefile
@@ -5,8 +5,8 @@ clean:
|
|||||||
rm -rf ~/.local/share/controles/database.db
|
rm -rf ~/.local/share/controles/database.db
|
||||||
rm -f admin_qr.png
|
rm -f admin_qr.png
|
||||||
|
|
||||||
run: clean
|
run:
|
||||||
python app.py
|
python app.py
|
||||||
|
|
||||||
reset-admin: clean
|
reset-admin: clean
|
||||||
python create_admin.py
|
python create_admin.py
|
||||||
|
|||||||
201
create_admin.py
201
create_admin.py
@@ -1,91 +1,134 @@
|
|||||||
import os
|
from functions.database import init_database, Usuario, Role, get_db_connection
|
||||||
from functions.database import get_db_connection, Usuario
|
|
||||||
from functions.rbac import Role
|
|
||||||
import pyotp
|
|
||||||
import qrcode
|
import qrcode
|
||||||
import base64
|
import os
|
||||||
from io import BytesIO
|
from pathlib import Path
|
||||||
|
import pyotp
|
||||||
|
|
||||||
def create_admin():
|
def generate_qr_code(user):
|
||||||
"""Cria o usuário admin se não existir"""
|
"""
|
||||||
db = get_db_connection()
|
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:
|
try:
|
||||||
# Verificar se o admin já existe
|
# Inicializar banco de dados
|
||||||
admin = db.query(Usuario).filter_by(username='admin').first()
|
init_database()
|
||||||
|
|
||||||
if admin:
|
# Criar sessão
|
||||||
print("Usuário admin já existe")
|
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 admin:
|
||||||
if os.path.exists('admin_qr.png'):
|
print("\n=== Usuário Admin Encontrado ===")
|
||||||
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
|
|
||||||
else:
|
else:
|
||||||
print("Gerando novo OTP para o admin...")
|
print("\n=== Criando Novo Usuário Admin ===")
|
||||||
# Gerar novo OTP
|
# Criar novo usuário admin
|
||||||
otp_secret = pyotp.random_base32()
|
admin = Usuario(
|
||||||
admin.otp_secret = otp_secret
|
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()
|
db.commit()
|
||||||
else:
|
|
||||||
print("Criando usuário admin...")
|
# Gerar QR code uma única vez
|
||||||
# Criar usuário admin
|
qr_path, otp_uri = generate_qr_code(admin)
|
||||||
admin = Usuario(
|
|
||||||
username='admin',
|
# Mostrar informações
|
||||||
password='admin123',
|
print("\n=== Informações do Admin ===")
|
||||||
is_admin=True
|
print(f"Username: {admin.username}")
|
||||||
)
|
print(f"Email: {admin.email}")
|
||||||
admin.email = 'admin@controles.com'
|
print(f"Senha: admin123")
|
||||||
db.add(admin)
|
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()
|
db.commit()
|
||||||
|
|
||||||
# Gerar OTP
|
except Exception as e:
|
||||||
otp_secret = pyotp.random_base32()
|
db.rollback()
|
||||||
admin.otp_secret = otp_secret
|
raise e
|
||||||
db.commit()
|
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:
|
except Exception as e:
|
||||||
print(f"Erro ao criar admin: {str(e)}")
|
print(f"\nErro durante a execução: {e}")
|
||||||
db.rollback()
|
import traceback
|
||||||
raise
|
traceback.print_exc()
|
||||||
finally:
|
|
||||||
db.close()
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
create_admin()
|
create_admin_user()
|
||||||
|
|||||||
Binary file not shown.
@@ -1,6 +1,6 @@
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from werkzeug.security import generate_password_hash, check_password_hash
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text, Numeric, Date, Enum
|
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text, Numeric, Date, Enum, create_engine, text
|
||||||
from sqlalchemy.orm import sessionmaker, relationship, backref
|
from sqlalchemy.orm import sessionmaker, relationship, backref
|
||||||
import os
|
import os
|
||||||
import pyotp
|
import pyotp
|
||||||
@@ -13,6 +13,7 @@ import enum
|
|||||||
from flask_login import UserMixin
|
from flask_login import UserMixin
|
||||||
from .rbac import Role, Permission, role_permissions, user_roles
|
from .rbac import Role, Permission, role_permissions, user_roles
|
||||||
from .base import Base, engine, Session
|
from .base import Base, engine, Session
|
||||||
|
import logging
|
||||||
|
|
||||||
# Configurar caminho do banco de dados
|
# Configurar caminho do banco de dados
|
||||||
db_dir = Path.home() / '.local' / 'share' / 'controles'
|
db_dir = Path.home() / '.local' / 'share' / 'controles'
|
||||||
@@ -296,6 +297,8 @@ class CotaMensal(Base):
|
|||||||
valor_antigo = Column(Numeric(10, 2), nullable=False)
|
valor_antigo = Column(Numeric(10, 2), nullable=False)
|
||||||
valor_novo = Column(Numeric(10, 2), nullable=False)
|
valor_novo = Column(Numeric(10, 2), nullable=False)
|
||||||
data_alteracao = Column(Date, nullable=False)
|
data_alteracao = Column(Date, nullable=False)
|
||||||
|
data_vencimento = Column(Date, nullable=False)
|
||||||
|
pago = Column(Boolean, default=False)
|
||||||
|
|
||||||
militante = relationship("Militante", back_populates="cotas_mensais")
|
militante = relationship("Militante", back_populates="cotas_mensais")
|
||||||
|
|
||||||
@@ -456,31 +459,19 @@ class Usuario(Base, UserMixin):
|
|||||||
cr = relationship('ComiteRegional', back_populates='usuarios')
|
cr = relationship('ComiteRegional', back_populates='usuarios')
|
||||||
celula = relationship('Celula', back_populates='usuarios')
|
celula = relationship('Celula', back_populates='usuarios')
|
||||||
|
|
||||||
def get_id(self):
|
def __init__(self, username, email=None, is_admin=False):
|
||||||
return str(self.id)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_authenticated(self):
|
|
||||||
return True
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_active(self):
|
|
||||||
return self.ativo
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_anonymous(self):
|
|
||||||
return False
|
|
||||||
|
|
||||||
def __init__(self, username, password, is_admin=False, email=None, tipo="USUARIO"):
|
|
||||||
self.username = username
|
self.username = username
|
||||||
self.password_hash = generate_password_hash(password)
|
self.email = email
|
||||||
self.is_admin = is_admin
|
self.is_admin = is_admin
|
||||||
self.email = email
|
self.email = email
|
||||||
self.ativo = True
|
self.ativo = True
|
||||||
self.session_timeout = 30
|
self.session_timeout = 30
|
||||||
self.tipo = tipo
|
self.tipo = "USUARIO"
|
||||||
self.ultima_atividade = datetime.utcnow()
|
self.ultima_atividade = datetime.utcnow()
|
||||||
|
|
||||||
|
def set_password(self, password):
|
||||||
|
self.password_hash = generate_password_hash(password)
|
||||||
|
|
||||||
def check_password(self, password):
|
def check_password(self, password):
|
||||||
return check_password_hash(self.password_hash, password)
|
return check_password_hash(self.password_hash, password)
|
||||||
|
|
||||||
@@ -621,73 +612,6 @@ class TransacaoPIX(Base):
|
|||||||
|
|
||||||
pagamento = relationship("Pagamento", back_populates="transacoes_pix")
|
pagamento = relationship("Pagamento", back_populates="transacoes_pix")
|
||||||
|
|
||||||
# Remover o banco de dados existente (se existir)
|
|
||||||
if os.path.exists(db_path):
|
|
||||||
os.remove(db_path)
|
|
||||||
|
|
||||||
def init_rbac():
|
|
||||||
"""Inicializa o sistema RBAC"""
|
|
||||||
print("Inicializando sistema RBAC...")
|
|
||||||
|
|
||||||
session = SessionLocal()
|
|
||||||
try:
|
|
||||||
# Verificar se já existe um admin
|
|
||||||
admin = session.query(Usuario).filter_by(username="admin").first()
|
|
||||||
|
|
||||||
if not admin:
|
|
||||||
print("Criando role de administrador...")
|
|
||||||
# Criar role de admin
|
|
||||||
admin_role = session.query(Role).filter_by(nome="Administrador").first()
|
|
||||||
if not admin_role:
|
|
||||||
admin_role = Role(nome="Administrador", nivel=Role.SECRETARIO_GERAL)
|
|
||||||
session.add(admin_role)
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
print("Criando usuário admin...")
|
|
||||||
# Criar usuário admin
|
|
||||||
admin = Usuario(
|
|
||||||
username="admin",
|
|
||||||
password="admin123",
|
|
||||||
is_admin=True
|
|
||||||
)
|
|
||||||
admin.email = "admin@example.com"
|
|
||||||
admin.role_id = admin_role.id
|
|
||||||
|
|
||||||
# Adicionar apenas a permissão de system_config ao admin
|
|
||||||
permission = session.query(Permission).filter_by(nome='system_config').first()
|
|
||||||
if permission and permission not in admin_role.permissions:
|
|
||||||
admin_role.permissions.append(permission)
|
|
||||||
|
|
||||||
session.add(admin)
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
print("=== Usuário Admin Criado ===")
|
|
||||||
print(f"Username: admin")
|
|
||||||
print(f"Senha: admin123")
|
|
||||||
print(f"Email: {admin.email}")
|
|
||||||
print(f"OTP Secret: {admin.otp_secret}")
|
|
||||||
else:
|
|
||||||
print("Usuário admin já existe")
|
|
||||||
# Garantir que o admin tenha apenas a permissão de system_config
|
|
||||||
admin_role = session.query(Role).filter_by(nome="Administrador").first()
|
|
||||||
if admin_role:
|
|
||||||
# Remover todas as permissões atuais
|
|
||||||
admin_role.permissions = []
|
|
||||||
|
|
||||||
# Adicionar apenas a permissão de system_config
|
|
||||||
permission = session.query(Permission).filter_by(nome='system_config').first()
|
|
||||||
if permission:
|
|
||||||
admin_role.permissions.append(permission)
|
|
||||||
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Erro na inicialização do sistema RBAC: {e}")
|
|
||||||
session.rollback()
|
|
||||||
raise
|
|
||||||
finally:
|
|
||||||
session.close()
|
|
||||||
|
|
||||||
def init_database():
|
def init_database():
|
||||||
"""Inicializa o banco de dados com dados básicos"""
|
"""Inicializa o banco de dados com dados básicos"""
|
||||||
print("Inicializando banco de dados...")
|
print("Inicializando banco de dados...")
|
||||||
@@ -769,12 +693,5 @@ def init_database():
|
|||||||
finally:
|
finally:
|
||||||
session.close()
|
session.close()
|
||||||
|
|
||||||
# Inicializar o sistema RBAC
|
|
||||||
init_rbac()
|
|
||||||
|
|
||||||
# Inicializar o banco de dados automaticamente quando o módulo for importado
|
|
||||||
init_database()
|
|
||||||
|
|
||||||
# Executar a criação dos dados iniciais
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
init_database()
|
init_database()
|
||||||
126
seed_data.py
126
seed_data.py
@@ -2,7 +2,8 @@ from datetime import datetime, timedelta
|
|||||||
from functions.database import (
|
from functions.database import (
|
||||||
Base, Militante, CotaMensal, TipoPagamento, Pagamento,
|
Base, Militante, CotaMensal, TipoPagamento, Pagamento,
|
||||||
MaterialVendido, TipoMaterial, VendaJornalAvulso, AssinaturaAnual,
|
MaterialVendido, TipoMaterial, VendaJornalAvulso, AssinaturaAnual,
|
||||||
RelatorioCotasMensais, RelatorioVendasMateriais, engine, get_db_connection
|
RelatorioCotasMensais, RelatorioVendasMateriais, engine, get_db_connection,
|
||||||
|
Setor, ComiteCentral, Usuario, Role
|
||||||
)
|
)
|
||||||
import random
|
import random
|
||||||
from faker import Faker
|
from faker import Faker
|
||||||
@@ -38,26 +39,47 @@ def criar_tipos_material():
|
|||||||
db_session.add(TipoMaterial(descricao=tipo))
|
db_session.add(TipoMaterial(descricao=tipo))
|
||||||
db_session.commit()
|
db_session.commit()
|
||||||
|
|
||||||
def criar_militantes(quantidade=50):
|
def criar_militantes(num_militantes):
|
||||||
"""Cria militantes fictícios"""
|
print(f"\nCriando {num_militantes} militantes...")
|
||||||
militantes = []
|
militantes = []
|
||||||
for _ in range(quantidade):
|
emails_usados = set() # Conjunto para rastrear emails já usados
|
||||||
|
|
||||||
|
for i in range(num_militantes):
|
||||||
|
nome = fake.name()
|
||||||
|
cpf = fake.cpf()
|
||||||
|
|
||||||
|
# Gerar email único
|
||||||
|
while True:
|
||||||
|
email = fake.email()
|
||||||
|
if email not in emails_usados:
|
||||||
|
emails_usados.add(email)
|
||||||
|
break
|
||||||
|
|
||||||
|
telefone = fake.phone_number()
|
||||||
|
endereco = fake.address()
|
||||||
|
filiado = fake.boolean()
|
||||||
|
|
||||||
|
print(f"Criando militante {i+1}: {nome} (CPF: {cpf})")
|
||||||
|
|
||||||
militante = Militante(
|
militante = Militante(
|
||||||
nome=fake.name(),
|
nome=nome,
|
||||||
cpf=fake.cpf(),
|
cpf=cpf,
|
||||||
email=fake.email(),
|
email=email,
|
||||||
telefone=fake.phone_number(),
|
telefone=telefone,
|
||||||
endereco=fake.address(),
|
endereco=endereco,
|
||||||
filiado=random.choice([True, False])
|
filiado=filiado
|
||||||
)
|
)
|
||||||
db_session.add(militante)
|
|
||||||
militantes.append(militante)
|
militantes.append(militante)
|
||||||
|
|
||||||
|
db_session.add_all(militantes)
|
||||||
db_session.commit()
|
db_session.commit()
|
||||||
return militantes
|
return militantes
|
||||||
|
|
||||||
def criar_cotas(militantes, quantidade_por_militante=3):
|
def criar_cotas(militantes, quantidade_por_militante=3):
|
||||||
"""Cria cotas mensais fictícias"""
|
"""Cria cotas mensais fictícias"""
|
||||||
|
print(f"Criando {quantidade_por_militante} cotas para cada um dos {len(militantes)} militantes...")
|
||||||
for militante in militantes:
|
for militante in militantes:
|
||||||
|
print(f"Criando cotas para militante {militante.nome}")
|
||||||
for i in range(quantidade_por_militante):
|
for i in range(quantidade_por_militante):
|
||||||
data_base = datetime.now() - timedelta(days=30 * i)
|
data_base = datetime.now() - timedelta(days=30 * i)
|
||||||
valor = random.uniform(50, 200)
|
valor = random.uniform(50, 200)
|
||||||
@@ -70,7 +92,9 @@ def criar_cotas(militantes, quantidade_por_militante=3):
|
|||||||
pago=random.choice([True, False])
|
pago=random.choice([True, False])
|
||||||
)
|
)
|
||||||
db_session.add(cota)
|
db_session.add(cota)
|
||||||
|
print(f" Cota criada: valor={valor:.2f}, vencimento={data_base + timedelta(days=30)}")
|
||||||
db_session.commit()
|
db_session.commit()
|
||||||
|
print("Cotas criadas com sucesso!")
|
||||||
|
|
||||||
def criar_pagamentos(militantes):
|
def criar_pagamentos(militantes):
|
||||||
"""Cria pagamentos fictícios"""
|
"""Cria pagamentos fictícios"""
|
||||||
@@ -155,38 +179,82 @@ def criar_relatorios():
|
|||||||
|
|
||||||
db_session.commit()
|
db_session.commit()
|
||||||
|
|
||||||
|
def criar_setores():
|
||||||
|
"""Cria setores padrão"""
|
||||||
|
setores = [
|
||||||
|
"Setor 1",
|
||||||
|
"Setor 2",
|
||||||
|
"Setor 3",
|
||||||
|
"Setor 4",
|
||||||
|
"Setor 5"
|
||||||
|
]
|
||||||
|
for setor in setores:
|
||||||
|
if not db_session.query(Setor).filter_by(nome=setor).first():
|
||||||
|
db_session.add(Setor(nome=setor))
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
def criar_comites():
|
||||||
|
"""Cria comitês padrão"""
|
||||||
|
comites = [
|
||||||
|
"Comitê 1",
|
||||||
|
"Comitê 2",
|
||||||
|
"Comitê 3"
|
||||||
|
]
|
||||||
|
for comite in comites:
|
||||||
|
if not db_session.query(ComiteCentral).filter_by(nome=comite).first():
|
||||||
|
db_session.add(ComiteCentral(nome=comite))
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
def criar_roles():
|
||||||
|
"""Cria roles padrão"""
|
||||||
|
roles = [
|
||||||
|
("admin", 1), # Nível 1: Administrador
|
||||||
|
("gestor", 2), # Nível 2: Gestor
|
||||||
|
("usuario", 3) # Nível 3: Usuário comum
|
||||||
|
]
|
||||||
|
for nome, nivel in roles:
|
||||||
|
if not db_session.query(Role).filter_by(nome=nome).first():
|
||||||
|
db_session.add(Role(nome=nome, nivel=nivel))
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
def criar_usuario_admin():
|
||||||
|
"""Cria usuário admin inicial"""
|
||||||
|
if not db_session.query(Usuario).filter_by(username='admin').first():
|
||||||
|
role_admin = db_session.query(Role).filter_by(nome='admin').first()
|
||||||
|
setor = db_session.query(Setor).first()
|
||||||
|
|
||||||
|
admin = Usuario(
|
||||||
|
username='admin',
|
||||||
|
email='admin@example.com',
|
||||||
|
is_admin=True,
|
||||||
|
ativo=True,
|
||||||
|
role_id=role_admin.id if role_admin else None,
|
||||||
|
setor_id=setor.id if setor else None
|
||||||
|
)
|
||||||
|
admin.set_password('admin123') # Método que deve existir na classe Usuario
|
||||||
|
db_session.add(admin)
|
||||||
|
db_session.commit()
|
||||||
|
print("Usuário admin criado com sucesso!")
|
||||||
|
|
||||||
def seed_database():
|
def seed_database():
|
||||||
"""Função principal para popular o banco de dados com dados fictícios"""
|
"""Função principal para popular o banco de dados com dados fictícios"""
|
||||||
print("Iniciando população do banco de dados com dados fictícios...")
|
print("Populando banco de dados com dados fictícios...")
|
||||||
|
|
||||||
print("Criando tipos de pagamento...")
|
|
||||||
criar_tipos_pagamento()
|
criar_tipos_pagamento()
|
||||||
|
|
||||||
print("Criando tipos de material...")
|
|
||||||
criar_tipos_material()
|
criar_tipos_material()
|
||||||
|
criar_setores()
|
||||||
|
criar_comites()
|
||||||
|
criar_roles()
|
||||||
|
|
||||||
print("Criando militantes...")
|
|
||||||
militantes = criar_militantes(50)
|
militantes = criar_militantes(50)
|
||||||
|
|
||||||
print("Criando cotas mensais...")
|
|
||||||
criar_cotas(militantes)
|
criar_cotas(militantes)
|
||||||
|
|
||||||
print("Criando pagamentos...")
|
|
||||||
criar_pagamentos(militantes)
|
criar_pagamentos(militantes)
|
||||||
|
|
||||||
print("Criando materiais vendidos...")
|
|
||||||
criar_materiais_vendidos(militantes)
|
criar_materiais_vendidos(militantes)
|
||||||
|
|
||||||
print("Criando vendas de jornal...")
|
|
||||||
criar_vendas_jornal(militantes)
|
criar_vendas_jornal(militantes)
|
||||||
|
|
||||||
print("Criando assinaturas...")
|
|
||||||
criar_assinaturas(militantes)
|
criar_assinaturas(militantes)
|
||||||
|
|
||||||
print("Criando relatórios...")
|
|
||||||
criar_relatorios()
|
criar_relatorios()
|
||||||
|
|
||||||
print("Banco de dados populado com sucesso!")
|
print("Dados fictícios criados com sucesso!")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
seed_database()
|
seed_database()
|
||||||
@@ -227,47 +227,41 @@ body {
|
|||||||
|
|
||||||
/* Alert styles */
|
/* Alert styles */
|
||||||
.alert {
|
.alert {
|
||||||
position: fixed;
|
|
||||||
top: 1rem;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
z-index: 1050;
|
|
||||||
min-width: 300px;
|
|
||||||
max-width: 90%;
|
|
||||||
text-align: center;
|
|
||||||
border: none;
|
border: none;
|
||||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
border-radius: 8px;
|
||||||
display: flex;
|
padding: 1rem 1.5rem;
|
||||||
align-items: center;
|
margin-bottom: 1rem;
|
||||||
justify-content: center;
|
opacity: 1 !important;
|
||||||
gap: 10px;
|
background-color: rgba(255, 255, 255, 0.98) !important;
|
||||||
padding: 1rem;
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
backdrop-filter: blur(5px);
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert i {
|
.alert i {
|
||||||
font-size: 1.2rem;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-success {
|
.alert-success {
|
||||||
background: rgba(40, 167, 69, 0.95);
|
color: #155724 !important;
|
||||||
color: white;
|
background-color: #d4edda !important;
|
||||||
|
border-left: 4px solid #28a745;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-danger {
|
.alert-danger {
|
||||||
background: rgba(220, 53, 69, 0.95);
|
color: #721c24 !important;
|
||||||
color: white;
|
background-color: #f8d7da !important;
|
||||||
|
border-left: 4px solid #dc3545;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-warning {
|
.alert-warning {
|
||||||
background: rgba(255, 193, 7, 0.95);
|
color: #856404 !important;
|
||||||
color: #333;
|
background-color: #fff3cd !important;
|
||||||
|
border-left: 4px solid #ffc107;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-info {
|
.alert-info {
|
||||||
background: rgba(23, 162, 184, 0.95);
|
color: #0c5460 !important;
|
||||||
color: white;
|
background-color: #d1ecf1 !important;
|
||||||
|
border-left: 4px solid #17a2b8;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Animações para feedback */
|
/* Animações para feedback */
|
||||||
@@ -416,4 +410,41 @@ body {
|
|||||||
.navbar-brand img {
|
.navbar-brand img {
|
||||||
height: 30px;
|
height: 30px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-header {
|
||||||
|
background: linear-gradient(to right, var(--background-color), rgba(232, 0, 12, 0.05));
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-header h2 {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-header h4 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: var(--secondary-color);
|
||||||
|
opacity: 0.8;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
background: linear-gradient(to right, var(--secondary-dark), var(--secondary-color));
|
||||||
|
color: var(--text-light);
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item-action {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item-action:hover {
|
||||||
|
transform: translateX(5px);
|
||||||
|
background-color: rgba(232, 0, 12, 0.05);
|
||||||
}
|
}
|
||||||
29
templates/editar_cota.html
Normal file
29
templates/editar_cota.html
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mt-4">
|
||||||
|
<h2>Editar Cota</h2>
|
||||||
|
<form method="POST" class="needs-validation" novalidate>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="valor_novo" class="form-label">Valor</label>
|
||||||
|
<input type="number" step="0.01" class="form-control" id="valor_novo" name="valor_novo" value="{{ cota.valor_novo }}" required>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
Por favor, insira um valor válido.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="data_vencimento" class="form-label">Data de Vencimento</label>
|
||||||
|
<input type="date" class="form-control" id="data_vencimento" name="data_vencimento" value="{{ cota.data_vencimento }}" required>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
Por favor, selecione uma data de vencimento.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3 form-check">
|
||||||
|
<input type="checkbox" class="form-check-input" id="pago" name="pago" value="true" {% if cota.pago %}checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="pago">Pago</label>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Salvar</button>
|
||||||
|
<a href="{{ url_for('listar_cotas') }}" class="btn btn-secondary">Cancelar</a>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -5,9 +5,12 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row g-4">
|
<div class="row g-4">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<h3 class="date-header">
|
<div class="welcome-header">
|
||||||
{{ data_atual }}
|
<h2 class="mb-2">Olá, {{ nome_usuario }}!</h2>
|
||||||
</h3>
|
<h4 class="text-muted">
|
||||||
|
{{ data_atual }}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if current_user.has_permission('view_cell_data') %}
|
{% if current_user.has_permission('view_cell_data') %}
|
||||||
@@ -37,16 +40,16 @@
|
|||||||
<div class="col-md-6 col-lg-3">
|
<div class="col-md-6 col-lg-3">
|
||||||
<div class="card bg-success text-white">
|
<div class="card bg-success text-white">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<h6 class="card-title mb-2">Total de Cotas</h6>
|
||||||
<div>
|
<div class="d-flex justify-content-between align-items-start">
|
||||||
<h6 class="card-title mb-0">Total de Cotas</h6>
|
<div class="valor-container">
|
||||||
<h2 class="my-2">R$ {{ total_cotas }}</h2>
|
<h2 class="valor-cota mb-0">R$ {{ total_cotas }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="fs-1">
|
<div class="icon-container">
|
||||||
<i class="fas fa-dollar-sign"></i>
|
<i class="fas fa-dollar-sign"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<a href="{{ url_for('listar_cotas') }}" class="text-white text-decoration-none">
|
<a href="{{ url_for('listar_cotas') }}" class="text-white text-decoration-none mt-2 d-block">
|
||||||
Ver detalhes <i class="fas fa-arrow-right ms-1"></i>
|
Ver detalhes <i class="fas fa-arrow-right ms-1"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -100,7 +103,7 @@
|
|||||||
<!-- Últimos Militantes -->
|
<!-- Últimos Militantes -->
|
||||||
<div class="col-md-6 mb-4">
|
<div class="col-md-6 mb-4">
|
||||||
<div class="card h-100">
|
<div class="card h-100">
|
||||||
<div class="card-header bg-light">
|
<div class="card-header">
|
||||||
<h5 class="card-title mb-0">
|
<h5 class="card-title mb-0">
|
||||||
<i class="fas fa-user-plus me-2"></i>Últimos Militantes Cadastrados
|
<i class="fas fa-user-plus me-2"></i>Últimos Militantes Cadastrados
|
||||||
</h5>
|
</h5>
|
||||||
@@ -112,7 +115,7 @@
|
|||||||
<a href="{{ url_for('editar_militante', id=militante.id) }}" class="list-group-item list-group-item-action">
|
<a href="{{ url_for('editar_militante', id=militante.id) }}" class="list-group-item list-group-item-action">
|
||||||
<div class="d-flex w-100 justify-content-between">
|
<div class="d-flex w-100 justify-content-between">
|
||||||
<h6 class="mb-1">{{ militante.nome }}</h6>
|
<h6 class="mb-1">{{ militante.nome }}</h6>
|
||||||
<small class="text-muted">{{ militante.created_at.strftime('%d/%m/%Y') }}</small>
|
<small class="text-muted">#{{ militante.id }}</small>
|
||||||
</div>
|
</div>
|
||||||
<small class="text-muted">{{ militante.email }}</small>
|
<small class="text-muted">{{ militante.email }}</small>
|
||||||
</a>
|
</a>
|
||||||
@@ -156,4 +159,46 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.welcome-header {
|
||||||
|
background: linear-gradient(to right, var(--background-color), rgba(232, 0, 12, 0.05));
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-header h2 {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-header h4 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: var(--secondary-color);
|
||||||
|
opacity: 0.8;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.valor-container {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0; /* Permite que o texto quebre corretamente */
|
||||||
|
}
|
||||||
|
|
||||||
|
.valor-cota {
|
||||||
|
font-size: calc(1.2rem + 0.8vw);
|
||||||
|
line-height: 1.2;
|
||||||
|
word-break: break-word;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-container {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
margin-left: 8px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user