Compare commits
78 Commits
feat/rbac
...
pagamentos
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ff58cc51e | ||
|
|
813c968efd | ||
|
|
bdab631f0f | ||
|
|
91369f670f | ||
|
|
494b6262bf | ||
|
|
e01764ab40 | ||
|
|
279924a43c | ||
|
|
54191b8dde | ||
|
|
295a433d59 | ||
|
|
203751deeb | ||
|
|
71f926e6be | ||
|
|
8cef19576e | ||
|
|
abc46704c3 | ||
|
|
c640a756df | ||
|
|
3f2e6e3022 | ||
|
|
179ea3cad0 | ||
|
|
b47c9efc21 | ||
|
|
97711d30c7 | ||
|
|
50ef370c2b | ||
|
|
53594517c0 | ||
|
|
874df1d340 | ||
|
|
b170f94058 | ||
|
|
786040162b | ||
|
|
daaa7fd462 | ||
|
|
ad0ea2f259 | ||
|
|
74e5a1f7e3 | ||
|
|
d07a227e80 | ||
|
|
0635003485 | ||
|
|
d931fb4b5e | ||
|
|
a302a259a6 | ||
|
|
75ba696355 | ||
|
|
7f4fe77711 | ||
|
|
c29eed0c69 | ||
|
|
52a6bf9eb0 | ||
|
|
d468f8ff39 | ||
|
|
5527db8729 | ||
|
|
56b8e7aa54 | ||
|
|
9ffc562357 | ||
|
|
3ed3002410 | ||
|
|
f58c340235 | ||
|
|
9158a86655 | ||
|
|
6b23adcb34 | ||
|
|
c7c3b95f0b | ||
|
|
9bb62c81a7 | ||
|
|
c17a3eaa0f | ||
|
|
07605797d1 | ||
|
|
745803fef3 | ||
|
|
241543ea63 | ||
|
|
50516664e4 | ||
|
|
0447524a91 | ||
|
|
77cf5ad99c | ||
|
|
9cc3f408f8 | ||
|
|
758dbdb26d | ||
|
|
83ae798033 | ||
|
|
742f820bc2 | ||
|
|
a28f543478 | ||
|
|
417b5c3f96 | ||
|
|
10ff9cab3b | ||
|
|
8803c971e4 | ||
|
|
d4869dcfaa | ||
|
|
06e7c79488 | ||
|
|
0a2d5c1d23 | ||
|
|
855f97c72b | ||
|
|
8e6ccb70e9 | ||
|
|
65406276ae | ||
|
|
b1acc2fdfc | ||
|
|
c44ce94bef | ||
|
|
ce3b5a4231 | ||
|
|
f0faf4270b | ||
|
|
178a58bb00 | ||
|
|
e9c1f3aedf | ||
|
|
1ff8e97bbc | ||
|
|
b815f77240 | ||
|
|
ba4f6d6de3 | ||
|
|
ac461ce800 | ||
|
|
4f781b2a0e | ||
|
|
32cd4b70c1 | ||
|
|
54261e455c |
50
.dockerignore
Normal file
50
.dockerignore
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# Arquivos e diretórios do Git
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Arquivos do Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
env/
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# Arquivos de ambiente
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
|
||||||
|
# Arquivos de IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Arquivos de log
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Arquivos de banco de dados
|
||||||
|
*.db
|
||||||
|
*.sqlite3
|
||||||
|
|
||||||
|
# Arquivos temporários
|
||||||
|
*.tmp
|
||||||
|
*.bak
|
||||||
|
*.swp
|
||||||
|
*~
|
||||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -263,3 +263,10 @@ 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
|
||||||
|
|
||||||
|
# QR Codes
|
||||||
|
*_qr.png
|
||||||
|
*_qr.txt
|
||||||
|
|||||||
39
Dockerfile
Normal file
39
Dockerfile
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
# Instalar dependências do sistema
|
||||||
|
RUN apk update && \
|
||||||
|
apk add --no-cache \
|
||||||
|
python3 \
|
||||||
|
py3-pip \
|
||||||
|
make \
|
||||||
|
git \
|
||||||
|
gcc \
|
||||||
|
python3-dev \
|
||||||
|
musl-dev \
|
||||||
|
linux-headers
|
||||||
|
|
||||||
|
# Criar link simbólico para python3
|
||||||
|
RUN ln -sf python3 /usr/bin/python
|
||||||
|
|
||||||
|
# Definir diretório de trabalho
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copiar arquivos do projeto
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Criar e ativar ambiente virtual
|
||||||
|
RUN python -m venv /venv && \
|
||||||
|
. /venv/bin/activate && \
|
||||||
|
pip install --upgrade pip && \
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Expor a porta que o Flask usa
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
# Definir o ambiente virtual como padrão
|
||||||
|
ENV PATH="/venv/bin:$PATH"
|
||||||
|
ENV FLASK_APP=app.py
|
||||||
|
ENV FLASK_ENV=production
|
||||||
|
|
||||||
|
# Comando para rodar a aplicação
|
||||||
|
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]
|
||||||
34
Makefile
34
Makefile
@@ -1,12 +1,38 @@
|
|||||||
|
.PHONY: install run test clean refresh
|
||||||
|
|
||||||
install:
|
install:
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
|
pip install pytest pytest-cov
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf ~/.local/share/controles/database.db
|
find . -type d -name "__pycache__" -exec rm -r {} +
|
||||||
|
find . -type f -name "*.pyc" -delete
|
||||||
|
find . -type f -name "*.pyo" -delete
|
||||||
|
find . -type f -name "*.pyd" -delete
|
||||||
|
find . -type f -name ".coverage" -delete
|
||||||
|
find . -type d -name "*.egg-info" -exec rm -r {} +
|
||||||
|
find . -type d -name "*.egg" -exec rm -r {} +
|
||||||
|
find . -type d -name ".pytest_cache" -exec rm -r {} +
|
||||||
|
find . -type d -name "htmlcov" -exec rm -r {} +
|
||||||
|
rm -rf ~/.local/share/controles/database.db*
|
||||||
rm -f admin_qr.png
|
rm -f admin_qr.png
|
||||||
|
|
||||||
run: clean
|
init-db: clean
|
||||||
python app.py
|
python init_db.py
|
||||||
|
|
||||||
|
seed: init-db
|
||||||
|
python seed.py
|
||||||
|
|
||||||
|
run:
|
||||||
|
python app.py
|
||||||
|
|
||||||
|
run-with-seed: seed run
|
||||||
|
|
||||||
reset-admin: clean
|
reset-admin: clean
|
||||||
python create_admin.py
|
python create_admin.py
|
||||||
|
|
||||||
|
test:
|
||||||
|
pytest tests/ --cov=app --cov=functions --cov-report=term-missing
|
||||||
|
|
||||||
|
refresh: clean install test
|
||||||
|
python app.py
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
otpauth://totp/Sistema%20de%20Controles:admin?secret=27NESPSPWKWIXVIDBUJPTK7MPAKGF4WG&issuer=Sistema%20de%20Controles
|
|
||||||
148
create_admin.py
148
create_admin.py
@@ -1,91 +1,77 @@
|
|||||||
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
|
||||||
try:
|
|
||||||
# Verificar se o admin já existe
|
Args:
|
||||||
admin = db.query(Usuario).filter_by(username='admin').first()
|
user: Instância do modelo Usuario
|
||||||
|
|
||||||
if admin:
|
Returns:
|
||||||
print("Usuário admin já existe")
|
Path: Caminho do arquivo QR code gerado
|
||||||
|
"""
|
||||||
# Verificar se o arquivo admin_qr.png existe
|
# Gerar QR Code apenas na raiz do projeto
|
||||||
if os.path.exists('admin_qr.png'):
|
qr_path = Path('admin_qr.png')
|
||||||
print("Usando OTP existente do arquivo admin_qr.png")
|
|
||||||
# Extrair o OTP secret do QR code existente
|
# Remover arquivo antigo se existir
|
||||||
with open('admin_qr.png', 'rb') as f:
|
if qr_path.exists():
|
||||||
qr_data = f.read()
|
os.remove(str(qr_path))
|
||||||
# Aqui você precisaria implementar a lógica para extrair o OTP secret do QR code
|
|
||||||
# Por enquanto, vamos apenas manter o OTP existente
|
# Gerar e salvar QR Code
|
||||||
return
|
qr = qrcode.QRCode(version=1, box_size=10, border=5)
|
||||||
else:
|
|
||||||
print("Gerando novo OTP para o admin...")
|
# Gerar URI do OTP
|
||||||
# Gerar novo OTP
|
totp = pyotp.TOTP(user.otp_secret)
|
||||||
otp_secret = pyotp.random_base32()
|
otp_uri = totp.provisioning_uri(
|
||||||
admin.otp_secret = otp_secret
|
name=user.username,
|
||||||
db.commit()
|
issuer_name="Sistema de Controles"
|
||||||
else:
|
)
|
||||||
print("Criando usuário admin...")
|
|
||||||
# Criar usuário admin
|
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 o usuário admin do sistema"""
|
||||||
|
session = get_db_connection()
|
||||||
|
try:
|
||||||
|
# Buscar role de administrador
|
||||||
|
admin_role = session.query(Role).filter_by(nome="Administrador").first()
|
||||||
|
if not admin_role:
|
||||||
|
print("Role de administrador não encontrada!")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Verificar se o usuário admin já existe
|
||||||
|
if not session.query(Usuario).filter_by(username="admin").first():
|
||||||
admin = Usuario(
|
admin = Usuario(
|
||||||
username='admin',
|
username="admin",
|
||||||
password='admin123',
|
email="admin@example.com",
|
||||||
is_admin=True
|
is_admin=True
|
||||||
)
|
)
|
||||||
admin.email = 'admin@controles.com'
|
admin.set_password("admin123")
|
||||||
db.add(admin)
|
admin.tipo = "ADMIN"
|
||||||
db.commit()
|
admin.roles.append(admin_role)
|
||||||
|
session.add(admin)
|
||||||
# Gerar OTP
|
session.commit()
|
||||||
otp_secret = pyotp.random_base32()
|
print("Usuário admin criado com sucesso!")
|
||||||
admin.otp_secret = otp_secret
|
else:
|
||||||
db.commit()
|
print("Usuário admin já existe!")
|
||||||
|
|
||||||
# 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"Erro ao criar usuário admin: {e}")
|
||||||
db.rollback()
|
session.rollback()
|
||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
session.close()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
create_admin()
|
create_admin_user()
|
||||||
|
|||||||
@@ -1,104 +1,65 @@
|
|||||||
from functions.database import get_db_connection, Usuario
|
from functions.database import Usuario, Role, get_db_connection
|
||||||
from functions.rbac import Role
|
|
||||||
import pyotp
|
|
||||||
import qrcode
|
|
||||||
import os
|
|
||||||
import base64
|
|
||||||
from io import BytesIO
|
|
||||||
|
|
||||||
def create_test_users():
|
def create_test_users():
|
||||||
"""Cria usuários de teste se não existirem"""
|
"""Cria usuários de teste para o sistema"""
|
||||||
db = get_db_connection()
|
session = get_db_connection()
|
||||||
try:
|
try:
|
||||||
# Usuários de teste
|
# Buscar roles
|
||||||
test_users = [
|
secretario_celula = session.query(Role).filter_by(nivel=Role.SECRETARIO_CELULA).first()
|
||||||
|
secretario_setor = session.query(Role).filter_by(nivel=Role.SECRETARIO_SETOR).first()
|
||||||
|
secretario_cr = session.query(Role).filter_by(nivel=Role.SECRETARIO_CR).first()
|
||||||
|
secretario_geral = session.query(Role).filter_by(nivel=Role.SECRETARIO_GERAL).first()
|
||||||
|
|
||||||
|
# Criar usuários de teste
|
||||||
|
usuarios = [
|
||||||
{
|
{
|
||||||
'username': 'teste',
|
'username': 'celula',
|
||||||
'password': 'admin123', # Mesma senha do admin
|
'email': 'celula@example.com',
|
||||||
'email': 'teste@controles.com',
|
'password': 'celula123',
|
||||||
'is_admin': True
|
'role': secretario_celula,
|
||||||
|
'tipo': 'SECRETARIO_CELULA'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'username': 'aligner',
|
'username': 'setor',
|
||||||
'password': 'Test123!@#',
|
'email': 'setor@example.com',
|
||||||
'email': 'aligner@controles.com',
|
'password': 'setor123',
|
||||||
'is_admin': False
|
'role': secretario_setor,
|
||||||
|
'tipo': 'SECRETARIO_SETOR'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'username': 'tester',
|
'username': 'cr',
|
||||||
'password': 'Test123!@#',
|
'email': 'cr@example.com',
|
||||||
'email': 'tester@controles.com',
|
'password': 'cr123',
|
||||||
'is_admin': False
|
'role': secretario_cr,
|
||||||
|
'tipo': 'SECRETARIO_CR'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'username': 'deployer',
|
'username': 'geral',
|
||||||
'password': 'Test123!@#',
|
'email': 'geral@example.com',
|
||||||
'email': 'deployer@controles.com',
|
'password': 'geral123',
|
||||||
'is_admin': False
|
'role': secretario_geral,
|
||||||
|
'tipo': 'SECRETARIO_GERAL'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
# Obter o OTP secret do admin se existir
|
for user_data in usuarios:
|
||||||
admin = db.query(Usuario).filter_by(username='admin').first()
|
|
||||||
admin_otp_secret = admin.otp_secret if admin else None
|
|
||||||
|
|
||||||
for user_data in test_users:
|
|
||||||
# Verificar se o usuário já existe
|
# Verificar se o usuário já existe
|
||||||
user = db.query(Usuario).filter_by(username=user_data['username']).first()
|
if not session.query(Usuario).filter_by(username=user_data['username']).first():
|
||||||
|
|
||||||
if not user:
|
|
||||||
print(f"Criando usuário {user_data['username']}...")
|
|
||||||
# Criar usuário
|
|
||||||
user = Usuario(
|
user = Usuario(
|
||||||
username=user_data['username'],
|
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'])
|
||||||
db.add(user)
|
user.tipo = user_data['tipo']
|
||||||
db.commit()
|
user.roles.append(user_data['role'])
|
||||||
|
session.add(user)
|
||||||
# Se for o usuário teste, usar o mesmo OTP do admin
|
|
||||||
if user_data['username'] == 'teste' and admin_otp_secret:
|
session.commit()
|
||||||
user.otp_secret = admin_otp_secret
|
print("Usuários de teste criados com sucesso!")
|
||||||
db.commit()
|
|
||||||
else:
|
|
||||||
# Gerar novo OTP para outros usuários
|
|
||||||
otp_secret = pyotp.random_base32()
|
|
||||||
user.otp_secret = otp_secret
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
# Atribuir role de Secretário Geral para o usuário teste
|
|
||||||
if user_data['username'] == 'teste':
|
|
||||||
admin_role = db.query(Role).filter_by(nivel=Role.SECRETARIO_GERAL).first()
|
|
||||||
if admin_role:
|
|
||||||
user.roles.append(admin_role)
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
print(f"Usuário {user_data['username']} criado com sucesso!")
|
|
||||||
else:
|
|
||||||
print(f"Usuário {user_data['username']} já existe")
|
|
||||||
|
|
||||||
# Se for o usuário teste e não tiver o OTP do admin, atualizar
|
|
||||||
if user_data['username'] == 'teste' and admin_otp_secret and user.otp_secret != admin_otp_secret:
|
|
||||||
user.otp_secret = admin_otp_secret
|
|
||||||
db.commit()
|
|
||||||
print(f"OTP do usuário teste atualizado para o mesmo do admin")
|
|
||||||
|
|
||||||
# Verificar se o usuário teste tem a role de Secretário Geral
|
|
||||||
if user_data['username'] == 'teste':
|
|
||||||
admin_role = db.query(Role).filter_by(nivel=Role.SECRETARIO_GERAL).first()
|
|
||||||
if admin_role and admin_role not in user.roles:
|
|
||||||
user.roles.append(admin_role)
|
|
||||||
db.commit()
|
|
||||||
print(f"Role de Secretário Geral atribuída ao usuário teste")
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Erro ao criar usuários de teste: {str(e)}")
|
print(f"Erro ao criar usuários de teste: {e}")
|
||||||
db.rollback()
|
session.rollback()
|
||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
session.close()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
create_test_users()
|
|
||||||
14
docker-compose.yml
Normal file
14
docker-compose.yml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "5000:5000"
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
- ~/.local/share/controles:/root/.local/share/controles
|
||||||
|
environment:
|
||||||
|
- FLASK_ENV=development
|
||||||
|
- FLASK_APP=app.py
|
||||||
|
restart: unless-stopped
|
||||||
54
docs/alteracoes_db_connection.md
Normal file
54
docs/alteracoes_db_connection.md
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# Alterações no Gerenciamento de Conexões com o Banco de Dados
|
||||||
|
|
||||||
|
## Commit
|
||||||
|
- ID: [ID do commit será adicionado após o commit]
|
||||||
|
- Data: [Data do commit]
|
||||||
|
- Autor: [Nome do autor]
|
||||||
|
|
||||||
|
## Contexto
|
||||||
|
O sistema estava utilizando uma única sessão global do SQLAlchemy (`db_session`) que era criada no início da aplicação. Isso poderia causar problemas de concorrência e vazamento de recursos.
|
||||||
|
|
||||||
|
## Alterações Realizadas
|
||||||
|
|
||||||
|
### 1. Remoção da Sessão Global
|
||||||
|
- Removida a linha `db_session = get_db_connection()` do início do arquivo
|
||||||
|
- Todas as rotas agora criam sua própria sessão
|
||||||
|
|
||||||
|
### 2. Novo Padrão de Gerenciamento de Sessão
|
||||||
|
Em cada rota, implementamos o seguinte padrão:
|
||||||
|
```python
|
||||||
|
db = get_db_connection()
|
||||||
|
try:
|
||||||
|
# Operações com o banco
|
||||||
|
db.commit()
|
||||||
|
except Exception as e:
|
||||||
|
db.rollback()
|
||||||
|
# Tratamento de erro
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Melhorias no Tratamento de Erros
|
||||||
|
- Adicionado `db.rollback()` em caso de exceção
|
||||||
|
- Melhoradas as mensagens de erro
|
||||||
|
- Garantido que a sessão seja fechada mesmo em caso de erro
|
||||||
|
|
||||||
|
### 4. Padronização de Código
|
||||||
|
- Uso de `request.form.get()` ao invés de acessar diretamente o dicionário
|
||||||
|
- Conversão explícita de tipos (float, int, date)
|
||||||
|
- Validação de dados antes de criar objetos
|
||||||
|
- Mensagens de feedback mais claras para o usuário
|
||||||
|
|
||||||
|
## Impacto no Frontend
|
||||||
|
Não houve alterações necessárias nos templates, pois as mudanças foram apenas na forma como o backend gerencia as conexões com o banco de dados.
|
||||||
|
|
||||||
|
## Benefícios
|
||||||
|
1. Maior segurança (evita vazamentos de recursos)
|
||||||
|
2. Maior robustez (melhor tratamento de erros)
|
||||||
|
3. Código mais fácil de manter (padronização)
|
||||||
|
4. Maior eficiência (sessões são fechadas adequadamente)
|
||||||
|
|
||||||
|
## Observações
|
||||||
|
- Esta alteração foi feita para melhorar a arquitetura do sistema
|
||||||
|
- Não afeta a funcionalidade existente
|
||||||
|
- Recomenda-se seguir este padrão em novas implementações
|
||||||
26
docs/rbac.md
26
docs/rbac.md
@@ -109,22 +109,26 @@ CREATE TABLE user_roles (
|
|||||||
- `manage_cell_members`: Gerenciar membros da célula
|
- `manage_cell_members`: Gerenciar membros da célula
|
||||||
- `create_cell_member`: Criar novos membros na célula
|
- `create_cell_member`: Criar novos membros na célula
|
||||||
- `view_cell_reports`: Visualizar relatórios da célula
|
- `view_cell_reports`: Visualizar relatórios da célula
|
||||||
|
- `REGISTER_CELL_RECEIPT`: Registrar comprovantes da célula
|
||||||
|
|
||||||
### Permissões de Setor
|
### Permissões de Setor
|
||||||
- `manage_sector_cells`: Gerenciar células do setor
|
- `manage_sector_cells`: Gerenciar células do setor
|
||||||
- `create_sector_cell`: Criar novas células no setor
|
- `create_sector_cell`: Criar novas células no setor
|
||||||
- `view_sector_reports`: Visualizar relatórios do setor
|
- `view_sector_reports`: Visualizar relatórios do setor
|
||||||
|
- `REGISTER_SECTOR_RECEIPT`: Registrar comprovantes do setor
|
||||||
|
|
||||||
### Permissões de CR
|
### Permissões de CR
|
||||||
- `manage_cr_sectors`: Gerenciar setores do CR
|
- `manage_cr_sectors`: Gerenciar setores do CR
|
||||||
- `create_cr_sector`: Criar novos setores no CR
|
- `create_cr_sector`: Criar novos setores no CR
|
||||||
- `view_cr_reports`: Visualizar relatórios do CR
|
- `view_cr_reports`: Visualizar relatórios do CR
|
||||||
|
- `REGISTER_CR_RECEIPT`: Registrar comprovantes do CR
|
||||||
|
|
||||||
### Permissões de CC
|
### Permissões de CC
|
||||||
- `manage_cc_crs`: Gerenciar CRs
|
- `manage_cc_crs`: Gerenciar CRs
|
||||||
- `create_cc_cr`: Criar novos CRs
|
- `create_cc_cr`: Criar novos CRs
|
||||||
- `view_cc_reports`: Visualizar relatórios nacionais
|
- `view_cc_reports`: Visualizar relatórios nacionais
|
||||||
- `system_config`: Configurar o sistema
|
- `system_config`: Configurar o sistema
|
||||||
|
- `REGISTER_CC_RECEIPT`: Registrar comprovantes do CC
|
||||||
|
|
||||||
## Uso no Código
|
## Uso no Código
|
||||||
|
|
||||||
@@ -166,12 +170,12 @@ O sistema possui uma estrutura hierárquica com os seguintes níveis:
|
|||||||
- `MANAGE_CELL_MEMBERS`: Gerenciar membros da célula
|
- `MANAGE_CELL_MEMBERS`: Gerenciar membros da célula
|
||||||
- `VIEW_CELL_DATA`: Visualizar dados da célula
|
- `VIEW_CELL_DATA`: Visualizar dados da célula
|
||||||
- `VIEW_CELL_REPORTS`: Visualizar relatórios da célula
|
- `VIEW_CELL_REPORTS`: Visualizar relatórios da célula
|
||||||
- `REGISTER_CELL_PAYMENT`: Registrar pagamentos da célula
|
- `REGISTER_CELL_RECEIPT`: Registrar comprovantes da célula
|
||||||
|
|
||||||
- **Tesoureiro(a)**:
|
- **Tesoureiro(a)**:
|
||||||
- `VIEW_CELL_DATA`: Visualizar dados da célula
|
- `VIEW_CELL_DATA`: Visualizar dados da célula
|
||||||
- `VIEW_CELL_REPORTS`: Visualizar relatórios da célula
|
- `VIEW_CELL_REPORTS`: Visualizar relatórios da célula
|
||||||
- `REGISTER_CELL_PAYMENT`: Registrar pagamentos da célula
|
- `REGISTER_CELL_RECEIPT`: Registrar comprovantes da célula
|
||||||
|
|
||||||
- **Militante**:
|
- **Militante**:
|
||||||
- `VIEW_OWN_DATA`: Visualizar apenas seus próprios dados
|
- `VIEW_OWN_DATA`: Visualizar apenas seus próprios dados
|
||||||
@@ -180,32 +184,32 @@ O sistema possui uma estrutura hierárquica com os seguintes níveis:
|
|||||||
- **Secretário(a)**:
|
- **Secretário(a)**:
|
||||||
- `MANAGE_SECTOR_CELLS`: Gerenciar células do setor
|
- `MANAGE_SECTOR_CELLS`: Gerenciar células do setor
|
||||||
- `VIEW_SECTOR_REPORTS`: Visualizar relatórios do setor
|
- `VIEW_SECTOR_REPORTS`: Visualizar relatórios do setor
|
||||||
- `REGISTER_SECTOR_PAYMENT`: Registrar pagamentos do setor
|
- `REGISTER_SECTOR_RECEIPT`: Registrar comprovantes do setor
|
||||||
|
|
||||||
- **Tesoureiro(a)**:
|
- **Tesoureiro(a)**:
|
||||||
- `VIEW_SECTOR_REPORTS`: Visualizar relatórios do setor
|
- `VIEW_SECTOR_REPORTS`: Visualizar relatórios do setor
|
||||||
- `REGISTER_SECTOR_PAYMENT`: Registrar pagamentos do setor
|
- `REGISTER_SECTOR_RECEIPT`: Registrar comprovantes do setor
|
||||||
|
|
||||||
### CR
|
### CR
|
||||||
- **Secretário(a)**:
|
- **Secretário(a)**:
|
||||||
- `MANAGE_CR_SECTORS`: Gerenciar setores do CR
|
- `MANAGE_CR_SECTORS`: Gerenciar setores do CR
|
||||||
- `VIEW_CR_REPORTS`: Visualizar relatórios do CR
|
- `VIEW_CR_REPORTS`: Visualizar relatórios do CR
|
||||||
- `REGISTER_CR_PAYMENT`: Registrar pagamentos do CR
|
- `REGISTER_CR_RECEIPT`: Registrar comprovantes do CR
|
||||||
|
|
||||||
- **Tesoureiro(a)**:
|
- **Tesoureiro(a)**:
|
||||||
- `VIEW_CR_REPORTS`: Visualizar relatórios do CR
|
- `VIEW_CR_REPORTS`: Visualizar relatórios do CR
|
||||||
- `REGISTER_CR_PAYMENT`: Registrar pagamentos do CR
|
- `REGISTER_CR_RECEIPT`: Registrar comprovantes do CR
|
||||||
|
|
||||||
### CC
|
### CC
|
||||||
- **Secretário(a)**:
|
- **Secretário(a)**:
|
||||||
- `MANAGE_CC_CRS`: Gerenciar CRs
|
- `MANAGE_CC_CRS`: Gerenciar CRs
|
||||||
- `VIEW_CC_REPORTS`: Visualizar relatórios do CC
|
- `VIEW_CC_REPORTS`: Visualizar relatórios do CC
|
||||||
- `REGISTER_CC_PAYMENT`: Registrar pagamentos do CC
|
- `REGISTER_CC_RECEIPT`: Registrar comprovantes do CC
|
||||||
- `SYSTEM_CONFIG`: Configurar o sistema
|
- `SYSTEM_CONFIG`: Configurar o sistema
|
||||||
|
|
||||||
- **Tesoureiro(a)**:
|
- **Tesoureiro(a)**:
|
||||||
- `VIEW_CC_REPORTS`: Visualizar relatórios do CC
|
- `VIEW_CC_REPORTS`: Visualizar relatórios do CC
|
||||||
- `REGISTER_CC_PAYMENT`: Registrar pagamentos do CC
|
- `REGISTER_CC_RECEIPT`: Registrar comprovantes do CC
|
||||||
|
|
||||||
## Regras de Acesso a Dados
|
## Regras de Acesso a Dados
|
||||||
|
|
||||||
@@ -214,10 +218,10 @@ O sistema possui uma estrutura hierárquica com os seguintes níveis:
|
|||||||
- Secretários e tesoureiros podem ver dados de sua instância
|
- Secretários e tesoureiros podem ver dados de sua instância
|
||||||
- O CC tem acesso a todos os dados
|
- O CC tem acesso a todos os dados
|
||||||
|
|
||||||
2. **Registro de Pagamentos**:
|
2. **Registro de Comprovantes**:
|
||||||
- Apenas tesoureiros e secretários podem registrar pagamentos
|
- Apenas tesoureiros e secretários podem registrar comprovantes
|
||||||
- O registro é restrito à instância do usuário
|
- O registro é restrito à instância do usuário
|
||||||
- O CC pode registrar pagamentos em qualquer nível
|
- O CC pode registrar comprovantes em qualquer nível
|
||||||
|
|
||||||
## Implementação Técnica
|
## Implementação Técnica
|
||||||
|
|
||||||
|
|||||||
94
docs/regras_comprovantes.md
Normal file
94
docs/regras_comprovantes.md
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
# Regras de Negócio - Comprovantes
|
||||||
|
|
||||||
|
## 1. Estrutura do Comprovante
|
||||||
|
|
||||||
|
### 1.1 Dados Básicos
|
||||||
|
- Todo comprovante deve ter:
|
||||||
|
- Militante associado (obrigatório)
|
||||||
|
- Data do comprovante (obrigatório)
|
||||||
|
- Forma de pagamento (obrigatório)
|
||||||
|
- Campanha financeira (opcional)
|
||||||
|
|
||||||
|
### 1.2 Formas de Pagamento
|
||||||
|
- As formas de pagamento aceitas são:
|
||||||
|
- PIX
|
||||||
|
- Transferência/DOC
|
||||||
|
- Depósito
|
||||||
|
- Maquininha
|
||||||
|
|
||||||
|
## 2. Centralizações
|
||||||
|
|
||||||
|
### 2.1 Tipos de Centralização
|
||||||
|
- Cada comprovante pode ter uma ou mais centralizações
|
||||||
|
- Os tipos de centralização são:
|
||||||
|
- Cota
|
||||||
|
- Jornal
|
||||||
|
- Assinatura
|
||||||
|
|
||||||
|
### 2.2 Valores
|
||||||
|
- Cada centralização deve ter:
|
||||||
|
- Tipo (obrigatório)
|
||||||
|
- Valor (obrigatório, maior que zero)
|
||||||
|
|
||||||
|
## 3. Transações PIX
|
||||||
|
|
||||||
|
### 3.1 Dados da Transação
|
||||||
|
- Para pagamentos via PIX, o comprovante deve incluir:
|
||||||
|
- Chave PIX
|
||||||
|
- Valor
|
||||||
|
- Data de geração
|
||||||
|
- Data de pagamento
|
||||||
|
- Status (Pendente, Pago, Expirado)
|
||||||
|
- QR Code (quando aplicável)
|
||||||
|
|
||||||
|
## 4. Validações
|
||||||
|
|
||||||
|
### 4.1 Obrigatoriedades
|
||||||
|
- Um comprovante deve ter pelo menos uma centralização
|
||||||
|
- O valor total do comprovante deve ser igual à soma das centralizações
|
||||||
|
- A data do comprovante não pode ser futura
|
||||||
|
|
||||||
|
### 4.2 Restrições
|
||||||
|
- Não é permitido excluir comprovantes com centralizações já registradas
|
||||||
|
- Não é permitido alterar valores de centralizações após confirmação
|
||||||
|
- O militante associado deve estar ativo no sistema
|
||||||
|
|
||||||
|
## 5. Permissões
|
||||||
|
|
||||||
|
### 5.1 Acesso
|
||||||
|
- Apenas usuários com permissão `MANAGE_MATERIALS` podem:
|
||||||
|
- Criar comprovantes
|
||||||
|
- Editar comprovantes
|
||||||
|
- Excluir comprovantes
|
||||||
|
- Visualizar lista de comprovantes
|
||||||
|
|
||||||
|
### 5.2 Restrições
|
||||||
|
- Usuários só podem editar comprovantes de sua própria célula/setor/CR
|
||||||
|
- Apenas administradores podem editar comprovantes de qualquer nível
|
||||||
|
|
||||||
|
## 6. Relacionamentos
|
||||||
|
|
||||||
|
### 6.1 Militante
|
||||||
|
- Todo comprovante deve estar associado a um militante
|
||||||
|
- O militante deve estar ativo no sistema
|
||||||
|
- O militante deve pertencer a uma célula/setor/CR válido
|
||||||
|
|
||||||
|
### 6.2 Campanha Financeira
|
||||||
|
- O comprovante pode estar associado a uma campanha financeira
|
||||||
|
- A campanha deve estar ativa no período do comprovante
|
||||||
|
- O valor do comprovante é contabilizado no total da campanha
|
||||||
|
|
||||||
|
## 7. Histórico
|
||||||
|
|
||||||
|
### 7.1 Registro
|
||||||
|
- Todas as alterações em comprovantes devem ser registradas
|
||||||
|
- O sistema mantém histórico de:
|
||||||
|
- Data de criação
|
||||||
|
- Usuário que criou
|
||||||
|
- Data de alteração
|
||||||
|
- Usuário que alterou
|
||||||
|
|
||||||
|
### 7.2 Auditoria
|
||||||
|
- Os comprovantes são auditáveis
|
||||||
|
- O sistema mantém logs de todas as operações
|
||||||
|
- As alterações podem ser rastreadas por usuário e data
|
||||||
Binary file not shown.
@@ -1,14 +1,26 @@
|
|||||||
from sqlalchemy.ext.declarative import declarative_base
|
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
from pathlib import Path
|
||||||
import os
|
import os
|
||||||
|
|
||||||
# Configuração do banco de dados
|
# Configurar caminho do banco de dados
|
||||||
DATABASE_URL = os.getenv('DATABASE_URL', 'sqlite:///database.db')
|
db_dir = Path.home() / '.local' / 'share' / 'controles'
|
||||||
engine = create_engine(DATABASE_URL)
|
db_dir.mkdir(parents=True, exist_ok=True)
|
||||||
Session = sessionmaker(bind=engine)
|
db_path = db_dir / 'database.db'
|
||||||
|
|
||||||
# Base declarativa do SQLAlchemy
|
# Configurar SQLite com opções para melhor concorrência
|
||||||
|
engine = create_engine(
|
||||||
|
f'sqlite:///{db_path}',
|
||||||
|
connect_args={
|
||||||
|
'timeout': 30, # Tempo de espera em segundos
|
||||||
|
'check_same_thread': False # Permite acesso de múltiplas threads
|
||||||
|
},
|
||||||
|
pool_pre_ping=True, # Verifica conexão antes de usar
|
||||||
|
pool_recycle=3600 # Recicla conexões após 1 hora
|
||||||
|
)
|
||||||
|
|
||||||
|
Session = sessionmaker(bind=engine)
|
||||||
Base = declarative_base()
|
Base = declarative_base()
|
||||||
|
|
||||||
def get_db_connection():
|
def get_db_connection():
|
||||||
|
|||||||
84
functions/controle.py
Normal file
84
functions/controle.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
from datetime import datetime, UTC
|
||||||
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
from functions.database import get_db_connection, Controle as ControleModel
|
||||||
|
|
||||||
|
class Controle:
|
||||||
|
def __init__(self):
|
||||||
|
self.db = get_db_connection()
|
||||||
|
|
||||||
|
def registrar_controle(self, militante_id: int, tipo: str, valor: float, observacao: str = None) -> bool:
|
||||||
|
"""
|
||||||
|
Registra um novo controle no sistema
|
||||||
|
|
||||||
|
Args:
|
||||||
|
militante_id: ID do militante
|
||||||
|
tipo: Tipo do controle (ex: 'pagamento', 'cota')
|
||||||
|
valor: Valor do controle
|
||||||
|
observacao: Observação opcional sobre o controle
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True se o controle foi registrado com sucesso, False caso contrário
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
data_registro = datetime.now(UTC)
|
||||||
|
|
||||||
|
novo_controle = ControleModel(
|
||||||
|
militante_id=militante_id,
|
||||||
|
tipo=tipo,
|
||||||
|
valor=valor,
|
||||||
|
data_registro=data_registro,
|
||||||
|
observacao=observacao
|
||||||
|
)
|
||||||
|
|
||||||
|
self.db.add(novo_controle)
|
||||||
|
self.db.commit()
|
||||||
|
return True
|
||||||
|
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
self.db.rollback()
|
||||||
|
print(f"Erro ao registrar controle: {str(e)}")
|
||||||
|
return False
|
||||||
|
finally:
|
||||||
|
self.db.close()
|
||||||
|
|
||||||
|
def listar_controles(self, militante_id: int = None) -> list:
|
||||||
|
"""
|
||||||
|
Lista os controles registrados no sistema
|
||||||
|
|
||||||
|
Args:
|
||||||
|
militante_id: ID do militante para filtrar (opcional)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: Lista de controles encontrados
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
query = self.db.query(ControleModel)
|
||||||
|
|
||||||
|
if militante_id:
|
||||||
|
query = query.filter(ControleModel.militante_id == militante_id)
|
||||||
|
|
||||||
|
return query.all()
|
||||||
|
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
print(f"Erro ao listar controles: {str(e)}")
|
||||||
|
return []
|
||||||
|
finally:
|
||||||
|
self.db.close()
|
||||||
|
|
||||||
|
def buscar_controle(self, controle_id: int) -> ControleModel:
|
||||||
|
"""
|
||||||
|
Busca um controle específico pelo ID
|
||||||
|
|
||||||
|
Args:
|
||||||
|
controle_id: ID do controle
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ControleModel: Objeto do controle encontrado ou None
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return self.db.query(ControleModel).filter(ControleModel.id == controle_id).first()
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
print(f"Erro ao buscar controle: {str(e)}")
|
||||||
|
return None
|
||||||
|
finally:
|
||||||
|
self.db.close()
|
||||||
@@ -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,35 +13,33 @@ 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
|
||||||
|
import qrcode
|
||||||
|
from PIL import Image
|
||||||
|
import re
|
||||||
|
|
||||||
# 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'
|
||||||
db_dir.mkdir(parents=True, exist_ok=True)
|
db_dir.mkdir(parents=True, exist_ok=True)
|
||||||
db_path = db_dir / 'database.db'
|
db_path = db_dir / 'database.db'
|
||||||
|
|
||||||
SessionLocal = sessionmaker(bind=engine)
|
DATABASE_URL = f"sqlite:///{db_path}"
|
||||||
|
engine = create_engine(DATABASE_URL)
|
||||||
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
|
||||||
def get_db_connection():
|
def get_db_connection():
|
||||||
"""
|
"""Retorna uma nova sessão do banco de dados"""
|
||||||
Retorna uma nova sessão do banco de dados SQLite e verifica timeout
|
Session = sessionmaker(bind=engine)
|
||||||
"""
|
db = Session()
|
||||||
session = SessionLocal()
|
|
||||||
try:
|
try:
|
||||||
# Verificar timeout para usuários logados
|
# Configurar SQLite para melhor tratamento de concorrência
|
||||||
usuario_atual = session.query(Usuario).filter(
|
db.execute(text("PRAGMA journal_mode=WAL"))
|
||||||
Usuario.ultimo_login.isnot(None),
|
db.execute(text("PRAGMA busy_timeout=5000"))
|
||||||
Usuario.ultimo_logout.is_(None)
|
return db
|
||||||
).first()
|
except:
|
||||||
|
db.close()
|
||||||
if usuario_atual and usuario_atual.check_session_timeout():
|
raise
|
||||||
usuario_atual.logout()
|
|
||||||
session.commit()
|
|
||||||
raise Exception("Sessão expirada. Por favor, faça login novamente.")
|
|
||||||
|
|
||||||
return session
|
|
||||||
except Exception as e:
|
|
||||||
session.close()
|
|
||||||
raise e
|
|
||||||
|
|
||||||
def execute_query(query, params=None):
|
def execute_query(query, params=None):
|
||||||
"""
|
"""
|
||||||
@@ -58,15 +56,21 @@ def execute_query(query, params=None):
|
|||||||
finally:
|
finally:
|
||||||
session.close()
|
session.close()
|
||||||
|
|
||||||
|
class EstadoMilitante(enum.Enum):
|
||||||
|
ATIVO = 'ativo'
|
||||||
|
DESLIGADO = 'desligado'
|
||||||
|
SUSPENSO = 'suspenso'
|
||||||
|
AFASTADO = 'afastado'
|
||||||
|
|
||||||
class Celula(Base):
|
class Celula(Base):
|
||||||
__tablename__ = 'celulas'
|
__tablename__ = 'celulas'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
nome = Column(String(100), nullable=False)
|
nome = Column(String(100), nullable=False)
|
||||||
setor_id = Column(Integer, ForeignKey('setores.id'))
|
setor_id = Column(Integer, ForeignKey('setores.id', use_alter=True, name='fk_celula_setor'))
|
||||||
cr_id = Column(Integer, ForeignKey('comites_regionais.id'))
|
cr_id = Column(Integer, ForeignKey('comites_regionais.id', use_alter=True, name='fk_celula_cr'))
|
||||||
secretario = Column(Integer, ForeignKey('militantes.id'))
|
secretario = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_celula_secretario'))
|
||||||
responsavel_financas = Column(Integer, ForeignKey('militantes.id'))
|
responsavel_financas = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_celula_responsavel_financas'))
|
||||||
quadro_orientador = Column(String(255))
|
quadro_orientador = Column(String(255))
|
||||||
|
|
||||||
# Relacionamentos
|
# Relacionamentos
|
||||||
@@ -83,10 +87,10 @@ class ComiteRegional(Base):
|
|||||||
|
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
nome = Column(String(100), nullable=False)
|
nome = Column(String(100), nullable=False)
|
||||||
responsavel_financas = Column(Integer, ForeignKey('militantes.id'))
|
responsavel_financas = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_cr_responsavel_financas'))
|
||||||
responsavel_formacao = Column(Integer, ForeignKey('militantes.id'))
|
responsavel_formacao = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_cr_responsavel_formacao'))
|
||||||
secretario_organizacao = Column(Integer, ForeignKey('militantes.id'))
|
secretario_organizacao = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_cr_secretario_organizacao'))
|
||||||
correspondente_jornal = Column(Integer, ForeignKey('militantes.id'))
|
correspondente_jornal = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_cr_correspondente_jornal'))
|
||||||
|
|
||||||
# Relacionamentos
|
# Relacionamentos
|
||||||
responsavel_financas_rel = relationship("Militante", foreign_keys=[responsavel_financas])
|
responsavel_financas_rel = relationship("Militante", foreign_keys=[responsavel_financas])
|
||||||
@@ -144,7 +148,7 @@ class Militante(Base):
|
|||||||
# Relacionamento para múltiplos emails
|
# Relacionamento para múltiplos emails
|
||||||
emails = relationship("EmailMilitante", back_populates="militante")
|
emails = relationship("EmailMilitante", back_populates="militante")
|
||||||
# Endereço
|
# Endereço
|
||||||
endereco_id = Column(Integer, ForeignKey('enderecos.id'))
|
endereco_id = Column(Integer, ForeignKey('enderecos.id', use_alter=True, name='fk_militante_endereco'))
|
||||||
endereco = relationship("Endereco", back_populates="militantes")
|
endereco = relationship("Endereco", back_populates="militantes")
|
||||||
# Redes sociais
|
# Redes sociais
|
||||||
redes_sociais = relationship("RedeSocial", back_populates="militante")
|
redes_sociais = relationship("RedeSocial", back_populates="militante")
|
||||||
@@ -162,9 +166,9 @@ class Militante(Base):
|
|||||||
dirigente_sindical = Column(Boolean)
|
dirigente_sindical = Column(Boolean)
|
||||||
central_sindical = Column(String(100))
|
central_sindical = Column(String(100))
|
||||||
# Responsável pelo cadastro
|
# Responsável pelo cadastro
|
||||||
registrado_por = Column(Integer, ForeignKey('militantes.id'))
|
registrado_por = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_militante_registrado_por'))
|
||||||
# Campos existentes
|
# Campos existentes
|
||||||
celula_id = Column(Integer, ForeignKey('celulas.id'))
|
celula_id = Column(Integer, ForeignKey('celulas.id', use_alter=True, name='fk_militante_celula'))
|
||||||
responsabilidades = Column(Integer, default=0)
|
responsabilidades = Column(Integer, default=0)
|
||||||
otp_secret = Column(String(32))
|
otp_secret = Column(String(32))
|
||||||
temp_token = Column(String(64))
|
temp_token = Column(String(64))
|
||||||
@@ -176,6 +180,11 @@ class Militante(Base):
|
|||||||
data_inicio_aspirante = Column(DateTime, default=datetime.utcnow)
|
data_inicio_aspirante = Column(DateTime, default=datetime.utcnow)
|
||||||
avaliacao_aspirante = Column(Text)
|
avaliacao_aspirante = Column(Text)
|
||||||
data_avaliacao_aspirante = Column(DateTime)
|
data_avaliacao_aspirante = Column(DateTime)
|
||||||
|
|
||||||
|
# Campos para estado do militante
|
||||||
|
estado = Column(Enum(EstadoMilitante), default=EstadoMilitante.ATIVO)
|
||||||
|
data_desligamento = Column(DateTime)
|
||||||
|
motivo_desligamento = Column(Text)
|
||||||
|
|
||||||
# Relacionamentos existentes
|
# Relacionamentos existentes
|
||||||
cotas_mensais = relationship("CotaMensal", back_populates="militante")
|
cotas_mensais = relationship("CotaMensal", back_populates="militante")
|
||||||
@@ -184,6 +193,7 @@ class Militante(Base):
|
|||||||
vendas_jornais = relationship("VendaJornalAvulso", back_populates="militante")
|
vendas_jornais = relationship("VendaJornalAvulso", back_populates="militante")
|
||||||
assinaturas = relationship("AssinaturaAnual", back_populates="militante")
|
assinaturas = relationship("AssinaturaAnual", back_populates="militante")
|
||||||
celula = relationship("Celula", back_populates="militantes", foreign_keys=[celula_id])
|
celula = relationship("Celula", back_populates="militantes", foreign_keys=[celula_id])
|
||||||
|
comprovantes = relationship("Comprovante", back_populates="militante")
|
||||||
|
|
||||||
# Constantes para responsabilidades
|
# Constantes para responsabilidades
|
||||||
SECRETARIO = 1
|
SECRETARIO = 1
|
||||||
@@ -296,6 +306,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")
|
||||||
|
|
||||||
@@ -320,7 +332,6 @@ class Pagamento(Base):
|
|||||||
data_pagamento = Column(Date, nullable=False)
|
data_pagamento = Column(Date, nullable=False)
|
||||||
|
|
||||||
militante = relationship("Militante", back_populates="pagamentos")
|
militante = relationship("Militante", back_populates="pagamentos")
|
||||||
transacoes_pix = relationship("TransacaoPIX", back_populates="pagamento")
|
|
||||||
|
|
||||||
class TipoMaterial(Base):
|
class TipoMaterial(Base):
|
||||||
__tablename__ = 'tipos_materiais'
|
__tablename__ = 'tipos_materiais'
|
||||||
@@ -374,9 +385,9 @@ class Setor(Base):
|
|||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
nome = Column(String(100), nullable=False)
|
nome = Column(String(100), nullable=False)
|
||||||
cr_id = Column(Integer, ForeignKey('comites_regionais.id'))
|
cr_id = Column(Integer, ForeignKey('comites_regionais.id', use_alter=True, name='fk_setor_cr'))
|
||||||
responsavel = Column(Integer, ForeignKey('militantes.id'))
|
responsavel = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_setor_responsavel'))
|
||||||
responsavel_financas = Column(Integer, ForeignKey('militantes.id'))
|
responsavel_financas = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_setor_responsavel_financas'))
|
||||||
|
|
||||||
# Relacionamentos
|
# Relacionamentos
|
||||||
cr = relationship("ComiteRegional", back_populates="setores")
|
cr = relationship("ComiteRegional", back_populates="setores")
|
||||||
@@ -456,31 +467,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)
|
||||||
|
|
||||||
@@ -501,7 +500,11 @@ class Usuario(Base, UserMixin):
|
|||||||
return time_diff.total_seconds() > (self.session_timeout * 60)
|
return time_diff.total_seconds() > (self.session_timeout * 60)
|
||||||
|
|
||||||
def has_permission(self, permission_name):
|
def has_permission(self, permission_name):
|
||||||
"""Verifica se o usuário tem uma determinada permissão"""
|
"""Verifica se o usuário tem uma permissão específica"""
|
||||||
|
if self.is_admin: # Se for admin, tem todas as permissões
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Verifica se o usuário tem a permissão através de suas roles
|
||||||
for role in self.roles:
|
for role in self.roles:
|
||||||
for permission in role.permissions:
|
for permission in role.permissions:
|
||||||
if permission.nome == permission_name:
|
if permission.nome == permission_name:
|
||||||
@@ -607,6 +610,49 @@ class Relatorio(Base):
|
|||||||
setor = relationship("Setor", foreign_keys=[setor_id])
|
setor = relationship("Setor", foreign_keys=[setor_id])
|
||||||
cr = relationship("ComiteRegional", foreign_keys=[cr_id])
|
cr = relationship("ComiteRegional", foreign_keys=[cr_id])
|
||||||
|
|
||||||
|
class CampanhaFinanceira(Base):
|
||||||
|
__tablename__ = 'campanhas_financeiras'
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
nome = Column(String(100), nullable=False)
|
||||||
|
descricao = Column(Text)
|
||||||
|
data_inicio = Column(Date, nullable=False)
|
||||||
|
data_fim = Column(Date, nullable=False)
|
||||||
|
meta = Column(Numeric(10, 2), nullable=False)
|
||||||
|
valor_arrecadado = Column(Numeric(10, 2), default=0)
|
||||||
|
status = Column(String(20), default='Em andamento') # Em andamento, Concluída, Cancelada
|
||||||
|
|
||||||
|
comprovantes = relationship("Comprovante", back_populates="campanha")
|
||||||
|
|
||||||
|
class TipoComprovante(Base):
|
||||||
|
__tablename__ = 'tipos_comprovante'
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
descricao = Column(String(50), nullable=False)
|
||||||
|
valor = Column(Numeric(10, 2), nullable=False)
|
||||||
|
|
||||||
|
class CentralizacaoComprovante(Base):
|
||||||
|
__tablename__ = 'centralizacoes_comprovante'
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
comprovante_id = Column(Integer, ForeignKey('comprovantes.id'), nullable=False)
|
||||||
|
tipo_comprovante = Column(String(50), nullable=False) # Cota, Jornal, Assinatura, etc.
|
||||||
|
valor = Column(Numeric(10, 2), nullable=False)
|
||||||
|
|
||||||
|
comprovante = relationship("Comprovante", back_populates="centralizacoes")
|
||||||
|
|
||||||
|
class Comprovante(Base):
|
||||||
|
__tablename__ = 'comprovantes'
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
militante_id = Column(Integer, ForeignKey('militantes.id'), nullable=False)
|
||||||
|
data_comprovante = Column(Date, nullable=False)
|
||||||
|
forma_pagamento = Column(String(20), nullable=False) # PIX, transferência/DOC, depósito, maquininha
|
||||||
|
campanha_id = Column(Integer, ForeignKey('campanhas_financeiras.id'))
|
||||||
|
|
||||||
|
militante = relationship("Militante", back_populates="comprovantes")
|
||||||
|
transacoes_pix = relationship("TransacaoPIX", back_populates="comprovante")
|
||||||
|
campanha = relationship("CampanhaFinanceira", back_populates="comprovantes")
|
||||||
|
centralizacoes = relationship("CentralizacaoComprovante", back_populates="comprovante", cascade="all, delete-orphan")
|
||||||
|
|
||||||
class TransacaoPIX(Base):
|
class TransacaoPIX(Base):
|
||||||
__tablename__ = 'transacoes_pix'
|
__tablename__ = 'transacoes_pix'
|
||||||
|
|
||||||
@@ -617,151 +663,115 @@ class TransacaoPIX(Base):
|
|||||||
data_pagamento = Column(DateTime)
|
data_pagamento = Column(DateTime)
|
||||||
status = Column(String(20)) # Pendente, Pago, Expirado
|
status = Column(String(20)) # Pendente, Pago, Expirado
|
||||||
qr_code = Column(Text)
|
qr_code = Column(Text)
|
||||||
pagamento_id = Column(Integer, ForeignKey('pagamentos.id'))
|
comprovante_id = Column(Integer, ForeignKey('comprovantes.id'))
|
||||||
|
|
||||||
pagamento = relationship("Pagamento", back_populates="transacoes_pix")
|
comprovante = relationship("Comprovante", 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...")
|
||||||
|
|
||||||
# Criar todas as tabelas
|
session = get_db_connection()
|
||||||
Base.metadata.drop_all(engine) # Remover todas as tabelas existentes
|
|
||||||
Base.metadata.create_all(engine)
|
|
||||||
|
|
||||||
session = SessionLocal()
|
|
||||||
try:
|
try:
|
||||||
# Criar role de administrador
|
# Criar todas as tabelas
|
||||||
admin_role = Role(nome="Administrador", nivel=Role.SECRETARIO_GERAL)
|
Base.metadata.drop_all(engine) # Remover todas as tabelas existentes
|
||||||
session.add(admin_role)
|
Base.metadata.create_all(engine)
|
||||||
|
|
||||||
|
# Criar roles padrão
|
||||||
|
roles = [
|
||||||
|
("Administrador", Role.SECRETARIO_GERAL),
|
||||||
|
("Secretário", Role.SECRETARIO_CELULA),
|
||||||
|
("Militante", Role.MILITANTE_BASICO)
|
||||||
|
]
|
||||||
|
|
||||||
|
for nome, nivel in roles:
|
||||||
|
if not session.query(Role).filter_by(nome=nome).first():
|
||||||
|
role = Role(nome=nome, nivel=nivel)
|
||||||
|
session.add(role)
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
# Verificar se existe um QR code salvo
|
# Criar setores padrão
|
||||||
qr_path = Path('admin_qr.png')
|
setores = ["Setor 1", "Setor 2", "Setor 3"]
|
||||||
admin_otp_secret = None
|
for nome in setores:
|
||||||
|
if not session.query(Setor).filter_by(nome=nome).first():
|
||||||
|
setor = Setor(nome=nome)
|
||||||
|
session.add(setor)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
if qr_path.exists():
|
# Criar comitês padrão
|
||||||
# Extrair o segredo OTP do nome do arquivo temporário dentro do QR
|
comites = ["Comitê 1", "Comitê 2", "Comitê 3"]
|
||||||
|
for nome in comites:
|
||||||
|
if not session.query(ComiteCentral).filter_by(nome=nome).first():
|
||||||
|
comite = ComiteCentral(nome=nome)
|
||||||
|
session.add(comite)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
# Verificar se existe QR code do admin
|
||||||
|
admin_otp_secret = None
|
||||||
|
qr_path = 'admin_qr.png'
|
||||||
|
|
||||||
|
if os.path.exists(qr_path):
|
||||||
try:
|
try:
|
||||||
import re
|
# Tentar ler o QR code existente
|
||||||
with open('admin_qr.txt', 'r') as f:
|
from pyzbar.pyzbar import decode
|
||||||
qr_content = f.read()
|
qr_data = decode(Image.open(qr_path))
|
||||||
# O segredo OTP está no formato otpauth://totp/admin?secret=XXXXX&issuer=Sistema%20de%20Controles
|
if qr_data:
|
||||||
match = re.search(r'secret=([A-Z0-9]+)&', qr_content)
|
# O URI do OTP está no formato: otpauth://totp/Sistema%20de%20Controles:admin?secret=XXXXX&issuer=Sistema%20de%20Controles
|
||||||
|
uri = qr_data[0].data.decode('utf-8')
|
||||||
|
# Extrair o secret do URI
|
||||||
|
match = re.search(r'secret=([A-Z0-9]+)', uri)
|
||||||
if match:
|
if match:
|
||||||
admin_otp_secret = match.group(1)
|
admin_otp_secret = match.group(1)
|
||||||
print(f"Usando OTP existente: {admin_otp_secret}")
|
print("OTP existente encontrado no QR code")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Erro ao ler OTP existente: {e}")
|
print(f"Erro ao ler QR code existente: {e}")
|
||||||
|
|
||||||
if not admin_otp_secret:
|
if not admin_otp_secret:
|
||||||
|
# Se não conseguiu ler o QR code ou ele não existe, gera um novo
|
||||||
admin_otp_secret = pyotp.random_base32()
|
admin_otp_secret = pyotp.random_base32()
|
||||||
print(f"Novo OTP gerado: {admin_otp_secret}")
|
print(f"Novo OTP gerado: {admin_otp_secret}")
|
||||||
|
|
||||||
# Criar usuário admin
|
# Criar usuário admin
|
||||||
|
admin_role = session.query(Role).filter_by(nome="Administrador").first()
|
||||||
|
setor = session.query(Setor).first()
|
||||||
|
|
||||||
admin = Usuario(
|
admin = Usuario(
|
||||||
username="admin",
|
username="admin",
|
||||||
password="admin123",
|
|
||||||
is_admin=True,
|
|
||||||
email="admin@example.com",
|
email="admin@example.com",
|
||||||
tipo="ADMIN"
|
is_admin=True
|
||||||
)
|
)
|
||||||
admin.role_id = admin_role.id
|
admin.set_password("admin123")
|
||||||
|
admin.tipo = "ADMIN"
|
||||||
admin.otp_secret = admin_otp_secret
|
admin.otp_secret = admin_otp_secret
|
||||||
|
admin.roles.append(admin_role)
|
||||||
|
admin.setor = setor
|
||||||
session.add(admin)
|
session.add(admin)
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
# Gerar novo QR code se não existir
|
# Gerar QR code apenas se não existir
|
||||||
if not qr_path.exists():
|
if not os.path.exists(qr_path):
|
||||||
totp = pyotp.totp.TOTP(admin_otp_secret)
|
totp = pyotp.totp.TOTP(admin_otp_secret)
|
||||||
provisioning_uri = totp.provisioning_uri("admin", issuer_name="Sistema de Controles")
|
provisioning_uri = totp.provisioning_uri("admin", issuer_name="Sistema de Controles")
|
||||||
|
|
||||||
# Salvar a URI em um arquivo texto para referência futura
|
|
||||||
with open('admin_qr.txt', 'w') as f:
|
|
||||||
f.write(provisioning_uri)
|
|
||||||
|
|
||||||
# Gerar QR code
|
|
||||||
import qrcode
|
|
||||||
qr = qrcode.QRCode(version=1, box_size=10, border=5)
|
qr = qrcode.QRCode(version=1, box_size=10, border=5)
|
||||||
qr.add_data(provisioning_uri)
|
qr.add_data(provisioning_uri)
|
||||||
qr.make(fit=True)
|
qr.make(fit=True)
|
||||||
img = qr.make_image(fill_color="black", back_color="white")
|
img = qr.make_image(fill_color="black", back_color="white")
|
||||||
img.save('admin_qr.png')
|
img.save(qr_path)
|
||||||
|
|
||||||
print("=== Usuário Admin Criado ===")
|
print("=== Usuário Admin Criado ===")
|
||||||
print(f"Username: admin")
|
print(f"Username: admin")
|
||||||
print(f"Senha: admin123")
|
print(f"Senha: admin123")
|
||||||
print(f"Email: {admin.email}")
|
print(f"Email: {admin.email}")
|
||||||
print(f"OTP Secret: {admin.otp_secret}")
|
print(f"OTP Secret: {admin_otp_secret}")
|
||||||
print(f"QR Code: {qr_path}")
|
print(f"QR Code: {qr_path}")
|
||||||
|
|
||||||
|
# Importar e executar o seed após criar todas as dependências
|
||||||
|
from seed_data import seed_database
|
||||||
|
print("\nPopulando banco de dados com dados de teste...")
|
||||||
|
seed_database()
|
||||||
|
print("Dados de teste criados com sucesso!")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Erro na inicialização do banco: {e}")
|
print(f"Erro na inicialização do banco: {e}")
|
||||||
session.rollback()
|
session.rollback()
|
||||||
@@ -769,12 +779,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()
|
||||||
@@ -10,7 +10,7 @@ def require_login(f):
|
|||||||
@wraps(f)
|
@wraps(f)
|
||||||
def decorated_function(*args, **kwargs):
|
def decorated_function(*args, **kwargs):
|
||||||
if not current_user.is_authenticated:
|
if not current_user.is_authenticated:
|
||||||
flash('Por favor, faça login para acessar esta página.', 'error')
|
flash('Por favor, faça login para acessar esta página.', 'danger')
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
db = get_db_connection()
|
db = get_db_connection()
|
||||||
@@ -21,7 +21,7 @@ def require_login(f):
|
|||||||
).get(current_user.id)
|
).get(current_user.id)
|
||||||
|
|
||||||
if not user:
|
if not user:
|
||||||
flash('Usuário não encontrado.', 'error')
|
flash('Usuário não encontrado.', 'danger')
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
# Atualiza timestamp da última atividade
|
# Atualiza timestamp da última atividade
|
||||||
@@ -39,11 +39,11 @@ def require_permission(permission_name):
|
|||||||
@wraps(f)
|
@wraps(f)
|
||||||
def decorated_function(*args, **kwargs):
|
def decorated_function(*args, **kwargs):
|
||||||
if not current_user.is_authenticated:
|
if not current_user.is_authenticated:
|
||||||
flash('Por favor, faça login para acessar esta página.', 'error')
|
flash('Por favor, faça login para acessar esta página.', 'danger')
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
if not current_user.has_permission(permission_name):
|
if not current_user.has_permission(permission_name):
|
||||||
flash('Você não tem permissão para acessar esta página.', 'error')
|
flash('Você não tem permissão para acessar esta página.', 'danger')
|
||||||
return redirect(url_for('home'))
|
return redirect(url_for('home'))
|
||||||
|
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
|
|||||||
1
functions/notificacao.py
Normal file
1
functions/notificacao.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -68,11 +68,14 @@ class Permission(Base):
|
|||||||
EDIT_OWN_DATA = "edit_own_data"
|
EDIT_OWN_DATA = "edit_own_data"
|
||||||
VIEW_CELL_DATA = "view_cell_data"
|
VIEW_CELL_DATA = "view_cell_data"
|
||||||
CREATE_MILITANT = "create_militant" # Nova permissão para criar militantes
|
CREATE_MILITANT = "create_militant" # Nova permissão para criar militantes
|
||||||
|
MANAGE_MATERIALS = "manage_materials" # Nova permissão para gerenciar materiais
|
||||||
|
MANAGE_REPORTS = "manage_reports" # Nova permissão para gerenciar relatórios
|
||||||
|
|
||||||
# Permissões de célula
|
# Permissões de célula
|
||||||
MANAGE_CELL_MEMBERS = "manage_cell_members"
|
MANAGE_CELL_MEMBERS = "manage_cell_members"
|
||||||
CREATE_CELL_MEMBER = "create_cell_member"
|
CREATE_CELL_MEMBER = "create_cell_member"
|
||||||
VIEW_CELL_REPORTS = "view_cell_reports"
|
VIEW_CELL_REPORTS = "view_cell_reports"
|
||||||
|
MANAGE_CELL_REPORTS = "manage_cell_reports" # Nova permissão
|
||||||
REGISTER_CELL_PAYMENT = "register_cell_payment"
|
REGISTER_CELL_PAYMENT = "register_cell_payment"
|
||||||
|
|
||||||
# Permissões de setor
|
# Permissões de setor
|
||||||
@@ -101,12 +104,15 @@ class Permission(Base):
|
|||||||
(Permission.VIEW_OWN_DATA, "Visualizar próprios dados"),
|
(Permission.VIEW_OWN_DATA, "Visualizar próprios dados"),
|
||||||
(Permission.EDIT_OWN_DATA, "Editar próprios dados"),
|
(Permission.EDIT_OWN_DATA, "Editar próprios dados"),
|
||||||
(Permission.VIEW_CELL_DATA, "Visualizar dados da célula"),
|
(Permission.VIEW_CELL_DATA, "Visualizar dados da célula"),
|
||||||
(Permission.CREATE_MILITANT, "Criar novos militantes"), # Nova permissão
|
(Permission.CREATE_MILITANT, "Criar novos militantes"),
|
||||||
|
(Permission.MANAGE_MATERIALS, "Gerenciar materiais"),
|
||||||
|
(Permission.MANAGE_REPORTS, "Gerenciar relatórios"),
|
||||||
|
|
||||||
# Permissões de célula
|
# Permissões de célula
|
||||||
(Permission.MANAGE_CELL_MEMBERS, "Gerenciar membros da célula"),
|
(Permission.MANAGE_CELL_MEMBERS, "Gerenciar membros da célula"),
|
||||||
(Permission.CREATE_CELL_MEMBER, "Criar membros na célula"),
|
(Permission.CREATE_CELL_MEMBER, "Criar membros na célula"),
|
||||||
(Permission.VIEW_CELL_REPORTS, "Visualizar relatórios da célula"),
|
(Permission.VIEW_CELL_REPORTS, "Visualizar relatórios da célula"),
|
||||||
|
(Permission.MANAGE_CELL_REPORTS, "Gerenciar relatórios da célula"),
|
||||||
(Permission.REGISTER_CELL_PAYMENT, "Registrar pagamentos da célula"),
|
(Permission.REGISTER_CELL_PAYMENT, "Registrar pagamentos da célula"),
|
||||||
|
|
||||||
# Permissões de setor
|
# Permissões de setor
|
||||||
@@ -131,18 +137,26 @@ class Permission(Base):
|
|||||||
|
|
||||||
def init_rbac():
|
def init_rbac():
|
||||||
"""Inicializa o sistema RBAC com roles e permissões básicas"""
|
"""Inicializa o sistema RBAC com roles e permissões básicas"""
|
||||||
from .database import get_db_connection
|
from .database import Usuario, get_db_connection
|
||||||
session = get_db_connection()
|
session = get_db_connection()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Criar roles se não existirem
|
# Criar role de administrador primeiro
|
||||||
for nivel, nome in Role.get_roles_list():
|
admin_role = session.query(Role).filter_by(nome="Administrador").first()
|
||||||
role = session.query(Role).filter_by(nivel=nivel).first()
|
if not admin_role:
|
||||||
if not role:
|
admin_role = Role(nome="Administrador", nivel=Role.SECRETARIO_GERAL)
|
||||||
role = Role(nome=nome, nivel=nivel)
|
session.add(admin_role)
|
||||||
session.add(role)
|
session.commit()
|
||||||
|
|
||||||
# Criar permissões se não existirem
|
# Criar outras roles
|
||||||
|
for nivel, nome in Role.get_roles_list():
|
||||||
|
if nome != "Administrador": # Pular Administrador pois já foi criado
|
||||||
|
role = session.query(Role).filter_by(nivel=nivel).first()
|
||||||
|
if not role:
|
||||||
|
role = Role(nome=nome, nivel=nivel)
|
||||||
|
session.add(role)
|
||||||
|
|
||||||
|
# Criar permissões
|
||||||
for nome, descricao in Permission.get_permissions_list():
|
for nome, descricao in Permission.get_permissions_list():
|
||||||
permission = session.query(Permission).filter_by(nome=nome).first()
|
permission = session.query(Permission).filter_by(nome=nome).first()
|
||||||
if not permission:
|
if not permission:
|
||||||
@@ -151,8 +165,20 @@ def init_rbac():
|
|||||||
|
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
# Mapear permissões para roles
|
# Dar todas as permissões para o admin
|
||||||
for role in session.query(Role).all():
|
all_permissions = session.query(Permission).all()
|
||||||
|
admin_role.permissions = all_permissions
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
# Buscar usuário admin e atribuir role de administrador
|
||||||
|
admin_user = session.query(Usuario).filter_by(username="admin").first()
|
||||||
|
if admin_user:
|
||||||
|
if admin_role not in admin_user.roles:
|
||||||
|
admin_user.roles = [admin_role] # Substituir roles existentes
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
# Mapear permissões para outros roles
|
||||||
|
for role in session.query(Role).filter(Role.nome != "Administrador").all():
|
||||||
# Militante Básico
|
# Militante Básico
|
||||||
if role.nivel == Role.MILITANTE_BASICO:
|
if role.nivel == Role.MILITANTE_BASICO:
|
||||||
role.permissions = [
|
role.permissions = [
|
||||||
@@ -170,7 +196,9 @@ def init_rbac():
|
|||||||
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
|
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
|
||||||
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
|
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
|
||||||
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
|
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
|
||||||
session.query(Permission).filter_by(nome=Permission.REGISTER_CELL_PAYMENT).first()
|
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.REGISTER_CELL_PAYMENT).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_MATERIALS).first()
|
||||||
]
|
]
|
||||||
|
|
||||||
# Membro de Setor
|
# Membro de Setor
|
||||||
@@ -182,8 +210,10 @@ def init_rbac():
|
|||||||
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
|
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
|
||||||
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
|
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
|
||||||
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
|
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
|
||||||
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
|
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
|
||||||
session.query(Permission).filter_by(nome=Permission.REGISTER_SECTOR_PAYMENT).first()
|
session.query(Permission).filter_by(nome=Permission.REGISTER_SECTOR_PAYMENT).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_MATERIALS).first()
|
||||||
]
|
]
|
||||||
|
|
||||||
# Secretário de Setor
|
# Secretário de Setor
|
||||||
@@ -195,10 +225,12 @@ def init_rbac():
|
|||||||
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
|
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
|
||||||
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
|
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
|
||||||
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
|
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
|
||||||
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
|
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
|
||||||
session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(),
|
session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(),
|
||||||
session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(),
|
session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(),
|
||||||
session.query(Permission).filter_by(nome=Permission.REGISTER_SECTOR_PAYMENT).first()
|
session.query(Permission).filter_by(nome=Permission.REGISTER_SECTOR_PAYMENT).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_MATERIALS).first()
|
||||||
]
|
]
|
||||||
|
|
||||||
# Membro de CR
|
# Membro de CR
|
||||||
@@ -210,11 +242,13 @@ def init_rbac():
|
|||||||
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
|
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
|
||||||
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
|
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
|
||||||
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
|
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
|
||||||
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
|
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
|
||||||
session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(),
|
session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(),
|
||||||
session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(),
|
session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(),
|
||||||
session.query(Permission).filter_by(nome=Permission.VIEW_CR_REPORTS).first(),
|
session.query(Permission).filter_by(nome=Permission.VIEW_CR_REPORTS).first(),
|
||||||
session.query(Permission).filter_by(nome=Permission.REGISTER_CR_PAYMENT).first()
|
session.query(Permission).filter_by(nome=Permission.REGISTER_CR_PAYMENT).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_MATERIALS).first()
|
||||||
]
|
]
|
||||||
|
|
||||||
# Secretário de CR
|
# Secretário de CR
|
||||||
@@ -226,13 +260,15 @@ def init_rbac():
|
|||||||
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
|
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
|
||||||
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
|
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
|
||||||
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
|
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
|
||||||
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
|
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
|
||||||
session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(),
|
session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(),
|
||||||
session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(),
|
session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(),
|
||||||
session.query(Permission).filter_by(nome=Permission.VIEW_CR_REPORTS).first(),
|
session.query(Permission).filter_by(nome=Permission.VIEW_CR_REPORTS).first(),
|
||||||
session.query(Permission).filter_by(nome=Permission.MANAGE_CR_SECTORS).first(),
|
session.query(Permission).filter_by(nome=Permission.MANAGE_CR_SECTORS).first(),
|
||||||
session.query(Permission).filter_by(nome=Permission.CREATE_CR_SECTOR).first(),
|
session.query(Permission).filter_by(nome=Permission.CREATE_CR_SECTOR).first(),
|
||||||
session.query(Permission).filter_by(nome=Permission.REGISTER_CR_PAYMENT).first()
|
session.query(Permission).filter_by(nome=Permission.REGISTER_CR_PAYMENT).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_MATERIALS).first()
|
||||||
]
|
]
|
||||||
|
|
||||||
# Membro do CC
|
# Membro do CC
|
||||||
@@ -244,6 +280,7 @@ def init_rbac():
|
|||||||
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
|
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
|
||||||
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
|
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
|
||||||
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
|
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
|
||||||
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
|
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
|
||||||
session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(),
|
session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(),
|
||||||
session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(),
|
session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(),
|
||||||
@@ -251,7 +288,8 @@ def init_rbac():
|
|||||||
session.query(Permission).filter_by(nome=Permission.MANAGE_CR_SECTORS).first(),
|
session.query(Permission).filter_by(nome=Permission.MANAGE_CR_SECTORS).first(),
|
||||||
session.query(Permission).filter_by(nome=Permission.CREATE_CR_SECTOR).first(),
|
session.query(Permission).filter_by(nome=Permission.CREATE_CR_SECTOR).first(),
|
||||||
session.query(Permission).filter_by(nome=Permission.VIEW_CC_REPORTS).first(),
|
session.query(Permission).filter_by(nome=Permission.VIEW_CC_REPORTS).first(),
|
||||||
session.query(Permission).filter_by(nome=Permission.REGISTER_CC_PAYMENT).first()
|
session.query(Permission).filter_by(nome=Permission.REGISTER_CC_PAYMENT).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_MATERIALS).first()
|
||||||
]
|
]
|
||||||
|
|
||||||
# Secretário Geral
|
# Secretário Geral
|
||||||
@@ -263,6 +301,7 @@ def init_rbac():
|
|||||||
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
|
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
|
||||||
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
|
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
|
||||||
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
|
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
|
||||||
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
|
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
|
||||||
session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(),
|
session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(),
|
||||||
session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(),
|
session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(),
|
||||||
@@ -273,13 +312,8 @@ def init_rbac():
|
|||||||
session.query(Permission).filter_by(nome=Permission.MANAGE_CC_CRS).first(),
|
session.query(Permission).filter_by(nome=Permission.MANAGE_CC_CRS).first(),
|
||||||
session.query(Permission).filter_by(nome=Permission.CREATE_CC_CR).first(),
|
session.query(Permission).filter_by(nome=Permission.CREATE_CC_CR).first(),
|
||||||
session.query(Permission).filter_by(nome=Permission.REGISTER_CC_PAYMENT).first(),
|
session.query(Permission).filter_by(nome=Permission.REGISTER_CC_PAYMENT).first(),
|
||||||
session.query(Permission).filter_by(nome=Permission.SYSTEM_CONFIG).first()
|
session.query(Permission).filter_by(nome=Permission.SYSTEM_CONFIG).first(),
|
||||||
]
|
session.query(Permission).filter_by(nome=Permission.MANAGE_MATERIALS).first()
|
||||||
|
|
||||||
# Administrador
|
|
||||||
elif role.nome == "Administrador":
|
|
||||||
role.permissions = [
|
|
||||||
session.query(Permission).filter_by(nome=Permission.SYSTEM_CONFIG).first()
|
|
||||||
]
|
]
|
||||||
|
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|||||||
1
functions/relatorio.py
Normal file
1
functions/relatorio.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
23
functions/usuario.py
Normal file
23
functions/usuario.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
def get_permissoes_por_cargo(cargo_id):
|
||||||
|
permissoes = {
|
||||||
|
1: [ # Secretário Geral
|
||||||
|
'gerenciar_relatorios_celula',
|
||||||
|
'visualizar_relatorios_celula',
|
||||||
|
'gerenciar_militantes',
|
||||||
|
'gerenciar_tipos_comprovante'
|
||||||
|
],
|
||||||
|
2: [ # Admin
|
||||||
|
'gerenciar_relatorios_celula',
|
||||||
|
'visualizar_relatorios_celula',
|
||||||
|
'gerenciar_militantes',
|
||||||
|
'gerenciar_tipos_comprovante'
|
||||||
|
],
|
||||||
|
3: [ # Secretário Financeiro do Comitê Central
|
||||||
|
'gerenciar_relatorios_celula',
|
||||||
|
'visualizar_relatorios_celula',
|
||||||
|
'gerenciar_militantes',
|
||||||
|
'gerenciar_tipos_comprovante'
|
||||||
|
],
|
||||||
|
# ... existing code ...
|
||||||
|
}
|
||||||
|
return permissoes.get(cargo_id, [])
|
||||||
19
init_db.py
Normal file
19
init_db.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from functions.database import init_database
|
||||||
|
from functions.rbac import init_rbac
|
||||||
|
from create_admin import create_admin_user
|
||||||
|
from create_test_users import create_test_users
|
||||||
|
|
||||||
|
def init_system():
|
||||||
|
print("Inicializando banco de dados...")
|
||||||
|
init_database()
|
||||||
|
|
||||||
|
print("Inicializando sistema RBAC...")
|
||||||
|
init_rbac()
|
||||||
|
|
||||||
|
print("Criando usuários iniciais...")
|
||||||
|
create_admin_user()
|
||||||
|
create_test_users()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
init_system()
|
||||||
|
print("Sistema inicializado com sucesso!")
|
||||||
@@ -14,3 +14,9 @@ cryptography==42.0.2
|
|||||||
bcrypt==4.1.2
|
bcrypt==4.1.2
|
||||||
Bootstrap-Flask==2.3.3
|
Bootstrap-Flask==2.3.3
|
||||||
flask-bootstrap5==0.1.dev1
|
flask-bootstrap5==0.1.dev1
|
||||||
|
PyJWT==2.8.0
|
||||||
|
gunicorn==21.2.0
|
||||||
|
Faker==19.13.0
|
||||||
|
pytest==8.0.0
|
||||||
|
pytest-cov==4.1.0
|
||||||
|
pyzbar==0.1.9
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
from flask import Blueprint, request, jsonify
|
|
||||||
from models.integracao import calcular_cota
|
|
||||||
|
|
||||||
cota_bp = Blueprint('cota', __name__)
|
|
||||||
|
|
||||||
@cota_bp.route('/calculate_cota', methods=['POST'])
|
|
||||||
def calculate_cota():
|
|
||||||
try:
|
|
||||||
data = request.get_json()
|
|
||||||
|
|
||||||
# Extrair dados do request
|
|
||||||
salary = float(data.get('salary', 0))
|
|
||||||
num_children = int(data.get('num_children', 0))
|
|
||||||
pays_school = bool(data.get('pays_school', False))
|
|
||||||
pays_rent = bool(data.get('pays_rent', False))
|
|
||||||
num_parents = int(data.get('num_parents', 0))
|
|
||||||
|
|
||||||
# Calcular a cota (implemente sua lógica de cálculo aqui)
|
|
||||||
cota = calcular_cota(
|
|
||||||
salary=salary,
|
|
||||||
num_children=num_children,
|
|
||||||
pays_school=pays_school,
|
|
||||||
pays_rent=pays_rent,
|
|
||||||
num_parents=num_parents
|
|
||||||
)
|
|
||||||
|
|
||||||
return jsonify({'cota': cota})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({'error': str(e)}), 400
|
|
||||||
355
seed_data.py
Normal file
355
seed_data.py
Normal file
@@ -0,0 +1,355 @@
|
|||||||
|
from datetime import datetime, timedelta
|
||||||
|
from functions.database import (
|
||||||
|
Base, Militante, CotaMensal, TipoComprovante, Comprovante,
|
||||||
|
MaterialVendido, TipoMaterial, VendaJornalAvulso, AssinaturaAnual,
|
||||||
|
RelatorioCotasMensais, RelatorioVendasMateriais, engine, SessionLocal,
|
||||||
|
Setor, ComiteCentral, Usuario, Role, EmailMilitante, Endereco,
|
||||||
|
ComiteRegional, Celula, EstadoMilitante, get_db_connection,
|
||||||
|
init_database, CentralizacaoComprovante
|
||||||
|
)
|
||||||
|
import random
|
||||||
|
from faker import Faker
|
||||||
|
import time
|
||||||
|
from werkzeug.security import generate_password_hash
|
||||||
|
|
||||||
|
fake = Faker('pt_BR')
|
||||||
|
|
||||||
|
def criar_estrutura_organizacional(session):
|
||||||
|
"""Cria a estrutura organizacional básica"""
|
||||||
|
print("\nCriando estrutura organizacional...")
|
||||||
|
|
||||||
|
# Criar Comitê Central
|
||||||
|
cc = ComiteCentral(nome="Comitê Central SP")
|
||||||
|
session.add(cc)
|
||||||
|
session.flush()
|
||||||
|
|
||||||
|
# Criar Comitês Regionais
|
||||||
|
crs = []
|
||||||
|
for nome in ["CR São Paulo", "CR ABC", "CR Campinas"]:
|
||||||
|
cr = ComiteRegional(nome=nome)
|
||||||
|
session.add(cr)
|
||||||
|
session.flush()
|
||||||
|
crs.append(cr)
|
||||||
|
|
||||||
|
# Criar Setores para cada CR
|
||||||
|
setores = []
|
||||||
|
for cr in crs:
|
||||||
|
for i in range(2): # 2 setores por CR
|
||||||
|
setor = Setor(
|
||||||
|
nome=f"Setor {i+1} - {cr.nome}",
|
||||||
|
cr_id=cr.id
|
||||||
|
)
|
||||||
|
session.add(setor)
|
||||||
|
session.flush()
|
||||||
|
setores.append(setor)
|
||||||
|
|
||||||
|
# Criar Células para cada Setor
|
||||||
|
for setor in setores:
|
||||||
|
for i in range(2): # 2 células por setor
|
||||||
|
celula = Celula(
|
||||||
|
nome=f"Célula {i+1} - {setor.nome}",
|
||||||
|
setor_id=setor.id
|
||||||
|
)
|
||||||
|
session.add(celula)
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
return crs, setores
|
||||||
|
|
||||||
|
def criar_tipos_comprovante(session):
|
||||||
|
"""Cria tipos de comprovante padrão"""
|
||||||
|
print("\nCriando tipos de comprovante...")
|
||||||
|
tipos = [
|
||||||
|
("Comprovante Padrão", 50.00),
|
||||||
|
("Comprovante Especial", 100.00),
|
||||||
|
("Comprovante Extraordinário", 200.00),
|
||||||
|
("Jornal Avulso", 5.00),
|
||||||
|
("Assinatura de Jornal", 30.00),
|
||||||
|
("Campanha Financeira", 0.00) # Valor variável
|
||||||
|
]
|
||||||
|
|
||||||
|
for descricao, valor in tipos:
|
||||||
|
if not session.query(TipoComprovante).filter_by(descricao=descricao).first():
|
||||||
|
session.add(TipoComprovante(descricao=descricao, valor=valor))
|
||||||
|
|
||||||
|
try:
|
||||||
|
session.commit()
|
||||||
|
print("Tipos de comprovante criados com sucesso!")
|
||||||
|
except Exception as e:
|
||||||
|
session.rollback()
|
||||||
|
print(f"Erro ao criar tipos de comprovante: {e}")
|
||||||
|
|
||||||
|
def criar_tipos_material(session):
|
||||||
|
"""Cria tipos de material padrão"""
|
||||||
|
print("\nCriando tipos de material...")
|
||||||
|
tipos = [
|
||||||
|
"Jornal",
|
||||||
|
"Revista",
|
||||||
|
"Livro",
|
||||||
|
"Panfleto",
|
||||||
|
"Cartilha"
|
||||||
|
]
|
||||||
|
for tipo in tipos:
|
||||||
|
if not session.query(TipoMaterial).filter_by(descricao=tipo).first():
|
||||||
|
session.add(TipoMaterial(descricao=tipo))
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
def criar_militantes(session, num_militantes, setores):
|
||||||
|
"""Cria militantes com todos os dados necessários"""
|
||||||
|
print(f"\nCriando {num_militantes} militantes...")
|
||||||
|
militantes = []
|
||||||
|
emails_usados = set()
|
||||||
|
|
||||||
|
for i in range(num_militantes):
|
||||||
|
try:
|
||||||
|
# Dados básicos
|
||||||
|
nome = fake.name()
|
||||||
|
cpf = fake.cpf()
|
||||||
|
|
||||||
|
# Email único
|
||||||
|
while True:
|
||||||
|
email = fake.email()
|
||||||
|
if email not in emails_usados:
|
||||||
|
emails_usados.add(email)
|
||||||
|
break
|
||||||
|
|
||||||
|
# Criar endereço
|
||||||
|
endereco = Endereco(
|
||||||
|
cep=fake.postcode(),
|
||||||
|
estado=fake.estado_sigla(),
|
||||||
|
cidade=fake.city(),
|
||||||
|
bairro=fake.bairro(),
|
||||||
|
rua=fake.street_name(),
|
||||||
|
numero=str(random.randint(1, 999)),
|
||||||
|
complemento=f"Bloco {random.randint(1, 10)}, Apto {random.randint(1, 999)}" if random.random() < 0.3 else None
|
||||||
|
)
|
||||||
|
session.add(endereco)
|
||||||
|
session.flush()
|
||||||
|
|
||||||
|
# Selecionar setor e célula aleatórios
|
||||||
|
setor = random.choice(setores)
|
||||||
|
celula = random.choice(session.query(Celula).filter_by(setor_id=setor.id).all())
|
||||||
|
|
||||||
|
# Definir responsabilidades
|
||||||
|
responsabilidades = 0
|
||||||
|
if random.random() < 0.2: # 20% chance de ser Responsável de Finanças
|
||||||
|
responsabilidades |= Militante.RESPONSAVEL_FINANCAS
|
||||||
|
if random.random() < 0.2: # 20% chance de ser Responsável de Imprensa
|
||||||
|
responsabilidades |= Militante.RESPONSAVEL_IMPRENSA
|
||||||
|
if random.random() < 0.2: # 20% chance de ser Quadro-Orientador
|
||||||
|
responsabilidades |= Militante.QUADRO_ORIENTADOR
|
||||||
|
if random.random() < 0.2: # 20% chance de ser Secretário
|
||||||
|
responsabilidades |= Militante.SECRETARIO
|
||||||
|
if random.random() < 0.2: # 20% chance de ser MPS
|
||||||
|
responsabilidades |= Militante.MPS
|
||||||
|
if random.random() < 0.2: # 20% chance de ser Tesoureiro
|
||||||
|
responsabilidades |= Militante.TESOUREIRO
|
||||||
|
if random.random() < 0.2: # 20% chance de ser MNS
|
||||||
|
responsabilidades |= Militante.MNS
|
||||||
|
if random.random() < 0.2: # 20% chance de ser da Juventude
|
||||||
|
responsabilidades |= Militante.JUVENTUDE
|
||||||
|
if random.random() < 0.3: # 30% chance de ser Aspirante
|
||||||
|
responsabilidades |= Militante.ASPIRANTE
|
||||||
|
|
||||||
|
print(f"Criando militante {i+1}: {nome}")
|
||||||
|
|
||||||
|
# Criar militante com todos os dados
|
||||||
|
militante = Militante(
|
||||||
|
nome=nome,
|
||||||
|
cpf=cpf,
|
||||||
|
titulo_eleitoral=str(random.randint(100000000000, 999999999999)),
|
||||||
|
data_nascimento=fake.date_of_birth(minimum_age=18, maximum_age=65),
|
||||||
|
data_entrada_oci=fake.date_between(start_date='-5y', end_date='today'),
|
||||||
|
data_efetivacao_oci=fake.date_between(start_date='-4y', end_date='today'),
|
||||||
|
telefone1=fake.phone_number(),
|
||||||
|
telefone2=fake.phone_number() if random.random() < 0.3 else None,
|
||||||
|
profissao=fake.job(),
|
||||||
|
regime_trabalho=random.choice(['CLT', 'PJ', 'Estatutário', 'Autônomo']),
|
||||||
|
empresa=fake.company(),
|
||||||
|
contratante=fake.company() if random.random() < 0.2 else None,
|
||||||
|
instituicao_ensino=fake.company() if random.random() < 0.4 else None,
|
||||||
|
tipo_instituicao=random.choice(['Federal', 'Estadual', 'Municipal', 'Privada']) if random.random() < 0.4 else None,
|
||||||
|
sindicato=fake.company() if random.random() < 0.6 else None,
|
||||||
|
cargo_sindical=random.choice(['Diretor', 'Delegado', 'Conselheiro']) if random.random() < 0.3 else None,
|
||||||
|
dirigente_sindical=random.random() < 0.2,
|
||||||
|
central_sindical=random.choice(['CUT', 'CSP-Conlutas', 'CTB', 'Força Sindical']) if random.random() < 0.4 else None,
|
||||||
|
endereco_id=endereco.id,
|
||||||
|
celula_id=celula.id,
|
||||||
|
responsabilidades=responsabilidades,
|
||||||
|
estado=random.choice(list(EstadoMilitante))
|
||||||
|
)
|
||||||
|
session.add(militante)
|
||||||
|
session.flush()
|
||||||
|
|
||||||
|
# Criar email do militante
|
||||||
|
email_militante = EmailMilitante(
|
||||||
|
militante_id=militante.id,
|
||||||
|
endereco_email=email
|
||||||
|
)
|
||||||
|
session.add(email_militante)
|
||||||
|
|
||||||
|
militantes.append(militante)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Erro ao criar militante {i+1}: {e}")
|
||||||
|
session.rollback()
|
||||||
|
continue
|
||||||
|
|
||||||
|
return militantes
|
||||||
|
|
||||||
|
def criar_cotas(session, militantes):
|
||||||
|
"""Cria cotas mensais para os militantes"""
|
||||||
|
print("\nCriando cotas mensais...")
|
||||||
|
for militante in militantes:
|
||||||
|
try:
|
||||||
|
# Criar 12 cotas (1 ano) para cada militante
|
||||||
|
for i in range(12):
|
||||||
|
data_base = datetime.now() - timedelta(days=30 * i)
|
||||||
|
valor = random.uniform(50, 200)
|
||||||
|
cota = CotaMensal(
|
||||||
|
militante_id=militante.id,
|
||||||
|
valor_antigo=valor,
|
||||||
|
valor_novo=valor * 1.1,
|
||||||
|
data_alteracao=data_base,
|
||||||
|
data_vencimento=data_base + timedelta(days=30),
|
||||||
|
pago=random.choice([True, False])
|
||||||
|
)
|
||||||
|
session.add(cota)
|
||||||
|
session.commit()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Erro ao criar cotas para militante {militante.nome}: {e}")
|
||||||
|
session.rollback()
|
||||||
|
|
||||||
|
def criar_comprovantes(session, militantes):
|
||||||
|
"""Cria comprovantes para os militantes"""
|
||||||
|
print("\nCriando comprovantes...")
|
||||||
|
tipos_comprovante = session.query(TipoComprovante).all()
|
||||||
|
|
||||||
|
for militante in militantes:
|
||||||
|
try:
|
||||||
|
# Criar entre 3 e 8 comprovantes por militante
|
||||||
|
for _ in range(random.randint(3, 8)):
|
||||||
|
# Criar o comprovante base
|
||||||
|
comprovante = Comprovante(
|
||||||
|
militante_id=militante.id,
|
||||||
|
data_comprovante=fake.date_between(start_date='-1y', end_date='today'),
|
||||||
|
forma_pagamento=random.choice(['PIX', 'transferência/DOC', 'depósito', 'maquininha'])
|
||||||
|
)
|
||||||
|
session.add(comprovante)
|
||||||
|
session.flush() # Para obter o ID do comprovante
|
||||||
|
|
||||||
|
# Criar a centralização para o comprovante
|
||||||
|
tipo = random.choice(tipos_comprovante)
|
||||||
|
valor = random.uniform(10, 1000)
|
||||||
|
centralizacao = CentralizacaoComprovante(
|
||||||
|
comprovante_id=comprovante.id,
|
||||||
|
tipo_comprovante=tipo.descricao,
|
||||||
|
valor=valor
|
||||||
|
)
|
||||||
|
session.add(centralizacao)
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
except Exception as e:
|
||||||
|
session.rollback()
|
||||||
|
print(f"Erro ao criar comprovantes para militante {militante.nome}: {e}")
|
||||||
|
|
||||||
|
def criar_materiais_vendidos(session, militantes):
|
||||||
|
"""Cria registros de materiais vendidos"""
|
||||||
|
print("\nCriando materiais vendidos...")
|
||||||
|
tipos_material = session.query(TipoMaterial).all()
|
||||||
|
|
||||||
|
for militante in militantes:
|
||||||
|
try:
|
||||||
|
# Criar entre 2 e 5 materiais vendidos por militante
|
||||||
|
for _ in range(random.randint(2, 5)):
|
||||||
|
material = MaterialVendido(
|
||||||
|
militante_id=militante.id,
|
||||||
|
tipo_material_id=random.choice(tipos_material).id,
|
||||||
|
descricao=fake.sentence(),
|
||||||
|
valor=random.uniform(20, 100),
|
||||||
|
data_venda=fake.date_time_between(start_date='-1y', end_date='now')
|
||||||
|
)
|
||||||
|
session.add(material)
|
||||||
|
session.commit()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Erro ao criar materiais vendidos para militante {militante.nome}: {e}")
|
||||||
|
session.rollback()
|
||||||
|
|
||||||
|
def criar_vendas_jornal(session, militantes):
|
||||||
|
"""Cria vendas de jornal avulso"""
|
||||||
|
print("\nCriando vendas de jornal...")
|
||||||
|
for militante in militantes:
|
||||||
|
try:
|
||||||
|
# Criar entre 2 e 6 vendas de jornal por militante
|
||||||
|
for _ in range(random.randint(2, 6)):
|
||||||
|
quantidade = random.randint(1, 10)
|
||||||
|
valor_unitario = random.uniform(5, 15)
|
||||||
|
venda = VendaJornalAvulso(
|
||||||
|
militante_id=militante.id,
|
||||||
|
quantidade=quantidade,
|
||||||
|
valor_total=quantidade * valor_unitario,
|
||||||
|
data_venda=fake.date_time_between(start_date='-1y', end_date='now')
|
||||||
|
)
|
||||||
|
session.add(venda)
|
||||||
|
session.commit()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Erro ao criar vendas de jornal para militante {militante.nome}: {e}")
|
||||||
|
session.rollback()
|
||||||
|
|
||||||
|
def criar_assinaturas(session, militantes):
|
||||||
|
"""Cria assinaturas anuais"""
|
||||||
|
print("\nCriando assinaturas anuais...")
|
||||||
|
tipos_material = session.query(TipoMaterial).all()
|
||||||
|
|
||||||
|
for militante in militantes:
|
||||||
|
try:
|
||||||
|
# 30% de chance de ter assinatura
|
||||||
|
if random.random() < 0.3:
|
||||||
|
data_inicio = fake.date_time_between(start_date='-1y', end_date='now')
|
||||||
|
assinatura = AssinaturaAnual(
|
||||||
|
militante_id=militante.id,
|
||||||
|
tipo_material_id=random.choice(tipos_material).id,
|
||||||
|
quantidade=random.randint(1, 3),
|
||||||
|
valor_total=random.uniform(100, 500),
|
||||||
|
data_inicio=data_inicio,
|
||||||
|
data_fim=data_inicio + timedelta(days=365)
|
||||||
|
)
|
||||||
|
session.add(assinatura)
|
||||||
|
session.commit()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Erro ao criar assinatura para militante {militante.nome}: {e}")
|
||||||
|
session.rollback()
|
||||||
|
|
||||||
|
def seed_database():
|
||||||
|
"""Função principal para popular o banco de dados"""
|
||||||
|
session = get_db_connection()
|
||||||
|
try:
|
||||||
|
print("Iniciando população do banco de dados...")
|
||||||
|
|
||||||
|
# Criar estrutura organizacional
|
||||||
|
crs, setores = criar_estrutura_organizacional(session)
|
||||||
|
|
||||||
|
# Criar tipos básicos
|
||||||
|
criar_tipos_comprovante(session)
|
||||||
|
criar_tipos_material(session)
|
||||||
|
|
||||||
|
# Criar militantes (30 militantes para teste)
|
||||||
|
militantes = criar_militantes(session, 30, setores)
|
||||||
|
|
||||||
|
# Criar dados financeiros e materiais
|
||||||
|
criar_cotas(session, militantes)
|
||||||
|
criar_comprovantes(session, militantes)
|
||||||
|
criar_materiais_vendidos(session, militantes)
|
||||||
|
criar_vendas_jornal(session, militantes)
|
||||||
|
criar_assinaturas(session, militantes)
|
||||||
|
|
||||||
|
print("\nBanco de dados populado com sucesso!")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Erro durante a população do banco: {e}")
|
||||||
|
session.rollback()
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
seed_database()
|
||||||
18
setup.py
18
setup.py
@@ -1,18 +0,0 @@
|
|||||||
from setuptools import setup, find_packages
|
|
||||||
|
|
||||||
setup(
|
|
||||||
name="controles",
|
|
||||||
version="0.1.0",
|
|
||||||
packages=find_packages(),
|
|
||||||
install_requires=[
|
|
||||||
"fastapi",
|
|
||||||
"uvicorn",
|
|
||||||
"sqlalchemy",
|
|
||||||
"python-jose[cryptography]",
|
|
||||||
"passlib[bcrypt]",
|
|
||||||
"python-multipart",
|
|
||||||
"qrcode",
|
|
||||||
"pillow",
|
|
||||||
"python-dotenv"
|
|
||||||
],
|
|
||||||
)
|
|
||||||
611
static/css/components.css
Normal file
611
static/css/components.css
Normal file
@@ -0,0 +1,611 @@
|
|||||||
|
/* Variáveis globais */
|
||||||
|
:root {
|
||||||
|
--table-header-bg: #d8dde2;
|
||||||
|
--table-hover-bg: rgba(0, 0, 0, 0.02);
|
||||||
|
--border-color: #dee2e6;
|
||||||
|
--blue: #0d6efd;
|
||||||
|
--green: #198754;
|
||||||
|
--cyan: #0dcaf0;
|
||||||
|
--yellow: #ffc107;
|
||||||
|
--primary-color: #dc3545;
|
||||||
|
--primary-hover: #bb2d3b;
|
||||||
|
--text-color: #333;
|
||||||
|
--text-muted: #6c757d;
|
||||||
|
--bg-hover: #f8f9fa;
|
||||||
|
--tab-active-color: var(--primary-color);
|
||||||
|
--tab-hover-color: rgba(220, 53, 69, 0.1);
|
||||||
|
|
||||||
|
/* Variáveis para os botões */
|
||||||
|
--bs-success: #198754;
|
||||||
|
--bs-success-dark: #157347;
|
||||||
|
--bs-secondary: #6c757d;
|
||||||
|
--bs-secondary-dark: #565e64;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tabelas */
|
||||||
|
.table-container {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table thead {
|
||||||
|
background-color: var(--table-header-bg) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table thead th {
|
||||||
|
border-bottom: none;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 1rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table tbody td {
|
||||||
|
padding: 1rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-hover tbody tr:hover {
|
||||||
|
background-color: var(--table-hover-bg) !important;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-hover tbody tr {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Botões de ação */
|
||||||
|
.btn-group-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group-actions .btn {
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Botões padrão */
|
||||||
|
.btn-outline-primary {
|
||||||
|
color: #0d6efd;
|
||||||
|
border-color: #0d6efd;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-primary:hover {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #0d6efd;
|
||||||
|
border-color: #0d6efd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cabeçalho de listagem */
|
||||||
|
.list-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Barra de pesquisa e filtros */
|
||||||
|
.search-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input-group {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input-group .input-group-text {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input-group .form-control {
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input-group .form-control:focus {
|
||||||
|
box-shadow: none;
|
||||||
|
border-color: #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Badges */
|
||||||
|
.badge {
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 0.5em 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge.bg-success {
|
||||||
|
background-color: #198754 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge.bg-secondary {
|
||||||
|
background-color: #6c757d !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Paginação */
|
||||||
|
.pagination-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-link {
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsividade */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.search-bar {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input-group {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-actions {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group-actions {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cards do Dashboard */
|
||||||
|
.stats-card {
|
||||||
|
position: relative;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card .title {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #6c757d;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card .value {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card .link {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card .icon {
|
||||||
|
position: absolute;
|
||||||
|
top: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card.blue {
|
||||||
|
background: linear-gradient(135deg, var(--blue) 0%, #0a58ca 100%);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card.green {
|
||||||
|
background: linear-gradient(135deg, var(--green) 0%, #146c43 100%);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card.cyan {
|
||||||
|
background: linear-gradient(135deg, var(--cyan) 0%, #0aa2c0 100%);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card.yellow {
|
||||||
|
background: linear-gradient(135deg, var(--yellow) 0%, #cc9a06 100%);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Welcome Header */
|
||||||
|
.welcome-header {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-header h2 {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-header h4 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tabs */
|
||||||
|
.nav-tabs {
|
||||||
|
border-bottom: 2px solid var(--border-color);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs .nav-link,
|
||||||
|
.nav-tabs .nav-link:focus,
|
||||||
|
.nav-tabs .nav-link:hover,
|
||||||
|
.nav-tabs .nav-link.active {
|
||||||
|
color: var(--primary-color) !important;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
margin-bottom: -2px;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
font-weight: 500;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs .nav-link:hover {
|
||||||
|
background-color: var(--tab-hover-color);
|
||||||
|
border-bottom: 2px solid var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs .nav-link.active {
|
||||||
|
font-weight: 600;
|
||||||
|
background-color: var(--tab-hover-color);
|
||||||
|
border-bottom: 2px solid var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs .nav-link i {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-pane {
|
||||||
|
animation: fadeIn 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(5px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsividade das abas */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.nav-tabs {
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs .nav-link {
|
||||||
|
white-space: nowrap;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilo para botões com largura fixa */
|
||||||
|
.btn-fixed-width {
|
||||||
|
min-width: 120px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
text-align: center;
|
||||||
|
height: 38px;
|
||||||
|
line-height: 1.5;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-fixed-width i {
|
||||||
|
margin-right: 8px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilo para o backdrop com blur em todos os modais */
|
||||||
|
.modal-backdrop.show {
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilo para o botão de fechar dos modais */
|
||||||
|
.btn-close {
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 0.5rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
filter: invert(1) grayscale(100%) brightness(200%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-close:hover {
|
||||||
|
opacity: 1;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilos do Modal */
|
||||||
|
.modal-header {
|
||||||
|
background-color: #343a40;
|
||||||
|
color: #fff;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-title {
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header i {
|
||||||
|
color: #fff;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header .btn-close {
|
||||||
|
filter: invert(1) grayscale(100%) brightness(200%);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header .btn-close:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilos globais de formulário */
|
||||||
|
.form-control:focus,
|
||||||
|
.form-select:focus,
|
||||||
|
.form-check-input:focus,
|
||||||
|
.btn:focus,
|
||||||
|
.btn-check:focus + .btn {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:hover,
|
||||||
|
.form-select:hover {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Input group com foco */
|
||||||
|
.input-group .form-control:focus,
|
||||||
|
.input-group .form-select:focus {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Checkbox e radio */
|
||||||
|
.form-check-input:checked {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Date picker */
|
||||||
|
input[type="date"]:focus {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilo para colunas ordenáveis */
|
||||||
|
th[data-sort] {
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
th[data-sort] i {
|
||||||
|
margin-left: 5px;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
th[data-sort].sort-asc i,
|
||||||
|
th[data-sort].sort-desc i {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animação para linhas da tabela */
|
||||||
|
#militantesTable tbody tr {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilos globais para botões */
|
||||||
|
.btn-success,
|
||||||
|
.modal-footer .btn-success,
|
||||||
|
button.btn-success,
|
||||||
|
input.btn-success,
|
||||||
|
.btn-success.active,
|
||||||
|
.btn-success:active,
|
||||||
|
.show > .btn-success.dropdown-toggle {
|
||||||
|
background-color: #198754 !important;
|
||||||
|
border-color: #198754 !important;
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success:hover,
|
||||||
|
.modal-footer .btn-success:hover,
|
||||||
|
button.btn-success:hover,
|
||||||
|
input.btn-success:hover,
|
||||||
|
.btn-success:focus,
|
||||||
|
.btn-success:active,
|
||||||
|
.modal-footer .btn-success:focus,
|
||||||
|
.modal-footer .btn-success:active,
|
||||||
|
.btn-success:not(:disabled):not(.disabled):active,
|
||||||
|
.btn-success:not(:disabled):not(.disabled).active,
|
||||||
|
.show > .btn-success.dropdown-toggle:hover {
|
||||||
|
background-color: #146c43 !important;
|
||||||
|
border-color: #146c43 !important;
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary,
|
||||||
|
.modal-footer .btn-secondary,
|
||||||
|
button.btn-secondary,
|
||||||
|
input.btn-secondary,
|
||||||
|
.btn-secondary.active,
|
||||||
|
.btn-secondary:active,
|
||||||
|
.show > .btn-secondary.dropdown-toggle {
|
||||||
|
background-color: #6c757d !important;
|
||||||
|
border-color: #6c757d !important;
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover,
|
||||||
|
.modal-footer .btn-secondary:hover,
|
||||||
|
button.btn-secondary:hover,
|
||||||
|
input.btn-secondary:hover,
|
||||||
|
.btn-secondary:focus,
|
||||||
|
.btn-secondary:active,
|
||||||
|
.modal-footer .btn-secondary:focus,
|
||||||
|
.modal-footer .btn-secondary:active,
|
||||||
|
.btn-secondary:not(:disabled):not(.disabled):active,
|
||||||
|
.btn-secondary:not(:disabled):not(.disabled).active,
|
||||||
|
.show > .btn-secondary.dropdown-toggle:hover {
|
||||||
|
background-color: #5c636a !important;
|
||||||
|
border-color: #5c636a !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:not(:disabled):not(.disabled).active {
|
||||||
|
background-color: #4b545c !important;
|
||||||
|
border-color: #4b545c !important;
|
||||||
|
color: white !important;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilos para botões nos modais */
|
||||||
|
.modal .btn,
|
||||||
|
.modal-footer .btn {
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 0.5rem 1.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal .btn:hover,
|
||||||
|
.modal-footer .btn:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Garantir que o botão primário mantenha suas cores */
|
||||||
|
.modal .btn-primary,
|
||||||
|
.modal-footer .btn-primary,
|
||||||
|
.modal .btn-primary.active,
|
||||||
|
.modal .btn-primary:active,
|
||||||
|
.modal-footer .btn-primary.active,
|
||||||
|
.modal-footer .btn-primary:active,
|
||||||
|
.modal .btn-primary:not(:disabled):not(.disabled):active,
|
||||||
|
.modal .btn-primary:not(:disabled):not(.disabled).active,
|
||||||
|
.modal-footer .btn-primary:not(:disabled):not(.disabled):active,
|
||||||
|
.modal-footer .btn-primary:not(:disabled):not(.disabled).active,
|
||||||
|
.show > .modal .btn-primary.dropdown-toggle,
|
||||||
|
.show > .modal-footer .btn-primary.dropdown-toggle {
|
||||||
|
background-color: #0d6efd !important;
|
||||||
|
border-color: #0d6efd !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal .btn-primary:hover,
|
||||||
|
.modal-footer .btn-primary:hover,
|
||||||
|
.modal .btn-primary:focus,
|
||||||
|
.modal-footer .btn-primary:focus,
|
||||||
|
.modal .btn-primary:active,
|
||||||
|
.modal-footer .btn-primary:active,
|
||||||
|
.modal .btn-primary:not(:disabled):not(.disabled):active:focus,
|
||||||
|
.modal .btn-primary:not(:disabled):not(.disabled).active:focus,
|
||||||
|
.modal-footer .btn-primary:not(:disabled):not(.disabled):active:focus,
|
||||||
|
.modal-footer .btn-primary:not(:disabled):not(.disabled).active:focus {
|
||||||
|
background-color: #0b5ed7 !important;
|
||||||
|
border-color: #0b5ed7 !important;
|
||||||
|
color: white !important;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilos para alertas */
|
||||||
|
.alert {
|
||||||
|
position: fixed;
|
||||||
|
top: 1rem;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 9999;
|
||||||
|
min-width: 300px;
|
||||||
|
max-width: 600px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 1rem 2.5rem 1rem 1rem;
|
||||||
|
margin: 0;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert .btn-close {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
right: 1rem;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success {
|
||||||
|
color: #0f5132;
|
||||||
|
background-color: #d1e7dd;
|
||||||
|
border-color: #badbcc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-danger {
|
||||||
|
color: #842029;
|
||||||
|
background-color: #f8d7da;
|
||||||
|
border-color: #f5c2c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-warning {
|
||||||
|
color: #664d03;
|
||||||
|
background-color: #fff3cd;
|
||||||
|
border-color: #ffecb5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-info {
|
||||||
|
color: #055160;
|
||||||
|
background-color: #cff4fc;
|
||||||
|
border-color: #b6effb;
|
||||||
|
}
|
||||||
450
static/css/style.css
Normal file
450
static/css/style.css
Normal file
@@ -0,0 +1,450 @@
|
|||||||
|
:root {
|
||||||
|
--primary-color: #E8000C;
|
||||||
|
--primary-dark: #B5000A;
|
||||||
|
--primary-light: #FF1A1A;
|
||||||
|
--secondary-color: #2D2D2D;
|
||||||
|
--secondary-light: #404040;
|
||||||
|
--secondary-dark: #1A1A1A;
|
||||||
|
--background-color: #FFFFFF;
|
||||||
|
--text-color: #2D2D2D;
|
||||||
|
--text-light: #FFFFFF;
|
||||||
|
--hover-color: #FF1A1A;
|
||||||
|
--disabled-color: #999999;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--background-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
font-family: 'Roboto', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
background: linear-gradient(to right, var(--secondary-dark), var(--secondary-color)) !important;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||||
|
border-bottom: 3px solid var(--primary-color);
|
||||||
|
padding: 0.8rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-light) !important;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand img {
|
||||||
|
height: 40px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-logo {
|
||||||
|
height: 32px;
|
||||||
|
width: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-logo {
|
||||||
|
height: 80px;
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
color: rgba(255, 255, 255, 0.85) !important;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link:hover {
|
||||||
|
color: var(--text-light) !important;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link i {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 8px 24px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .card-body {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cards de estatísticas */
|
||||||
|
.card.bg-primary {
|
||||||
|
background: linear-gradient(135deg, #0d6efd, #0a58ca) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card.bg-success {
|
||||||
|
background: linear-gradient(135deg, #198754, #146c43) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card.bg-info {
|
||||||
|
background: linear-gradient(135deg, #0dcaf0, #0aa2c0) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card.bg-warning {
|
||||||
|
background: linear-gradient(135deg, #ffc107, #cc9a06) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .fs-1 {
|
||||||
|
opacity: 0.8;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover .fs-1 {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h2 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h6 {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 400;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card a {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card a:hover {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cards de listagem */
|
||||||
|
.card .card-header {
|
||||||
|
background: linear-gradient(to right, var(--secondary-dark), var(--secondary-color));
|
||||||
|
color: var(--text-light);
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .card-header h5 {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-left: none;
|
||||||
|
border-right: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item:hover {
|
||||||
|
background-color: rgba(0,0,0,0.02);
|
||||||
|
transform: translateX(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item h6 {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item small {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
padding: 0.5em 0.8em;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
border: none;
|
||||||
|
padding: 0.5rem 1.5rem;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: var(--hover-color);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 2px 5px rgba(232, 0, 12, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:disabled {
|
||||||
|
background-color: var(--disabled-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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(--secondary-color);
|
||||||
|
color: var(--text-light);
|
||||||
|
font-weight: 500;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table tbody tr:hover {
|
||||||
|
background-color: rgba(232, 0, 12, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(232, 0, 12, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alert styles */
|
||||||
|
.alert {
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
opacity: 1 !important;
|
||||||
|
background-color: rgba(255, 255, 255, 0.98) !important;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert i {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success {
|
||||||
|
color: #155724 !important;
|
||||||
|
background-color: #d4edda !important;
|
||||||
|
border-left: 4px solid #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-danger {
|
||||||
|
color: #721c24 !important;
|
||||||
|
background-color: #f8d7da !important;
|
||||||
|
border-left: 4px solid #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-warning {
|
||||||
|
color: #856404 !important;
|
||||||
|
background-color: #fff3cd !important;
|
||||||
|
border-left: 4px solid #ffc107;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-info {
|
||||||
|
color: #0c5460 !important;
|
||||||
|
background-color: #d1ecf1 !important;
|
||||||
|
border-left: 4px solid #17a2b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animações para feedback */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translate(-50%, -20px); }
|
||||||
|
to { opacity: 1; transform: translate(-50%, 0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsividade */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.navbar-brand {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-logo {
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
margin: 1rem;
|
||||||
|
width: calc(100% - 2rem);
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu {
|
||||||
|
background: linear-gradient(to bottom right, var(--secondary-dark), var(--secondary-color));
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item {
|
||||||
|
color: rgba(255, 255, 255, 0.85) !important;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 400;
|
||||||
|
padding: 0.6rem 1rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item:hover {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: var(--text-light) !important;
|
||||||
|
transform: translateX(3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-divider {
|
||||||
|
border-top: 1px solid var(--secondary-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilo para o menu mobile */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.navbar-collapse {
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0 0 10px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand img {
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Data styles */
|
||||||
|
.date-header {
|
||||||
|
padding: 1.5rem 0;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
border-bottom: 1px solid rgba(0,0,0,0.1);
|
||||||
|
color: var(--secondary-color);
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navbar styles */
|
||||||
|
.navbar-nav .nav-link {
|
||||||
|
color: rgba(255, 255, 255, 0.85) !important;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-nav .nav-link:hover {
|
||||||
|
color: var(--primary-color) !important;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-nav .dropdown-menu {
|
||||||
|
background: linear-gradient(to bottom right, var(--secondary-dark), var(--secondary-color));
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item {
|
||||||
|
color: rgba(255, 255, 255, 0.85) !important;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 400;
|
||||||
|
padding: 0.6rem 1rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item:hover {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: var(--text-light) !important;
|
||||||
|
transform: translateX(3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Data styles */
|
||||||
|
.date-header {
|
||||||
|
padding: 1.5rem 0;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
border-bottom: 1px solid rgba(0,0,0,0.1);
|
||||||
|
color: var(--secondary-color);
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.date-header {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-collapse {
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0 0 10px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand img {
|
||||||
|
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);
|
||||||
|
}
|
||||||
53
static/css/styles.css
Normal file
53
static/css/styles.css
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
/* Estilos globais para alertas do sistema */
|
||||||
|
.alert {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilo base para o botão de fechar */
|
||||||
|
.alert .btn-close {
|
||||||
|
filter: none;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alert Success */
|
||||||
|
.alert-success .btn-close {
|
||||||
|
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23198754'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alert Danger */
|
||||||
|
.alert-danger .btn-close {
|
||||||
|
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23842029'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alert Warning */
|
||||||
|
.alert-warning .btn-close {
|
||||||
|
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23997404'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alert Info */
|
||||||
|
.alert-info .btn-close {
|
||||||
|
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23055160'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Efeito hover para todos os botões de fechar */
|
||||||
|
.alert .btn-close:hover {
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilo das abas do modal */
|
||||||
|
.nav-tabs .nav-link {
|
||||||
|
/* remover estilos */
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs .nav-link.active {
|
||||||
|
/* remover estilos */
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs .nav-link:hover:not(.active) {
|
||||||
|
/* remover estilos */
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs .nav-link i {
|
||||||
|
/* remover estilos */
|
||||||
|
}
|
||||||
1
static/img/favicon.ico
Normal file
1
static/img/favicon.ico
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
BIN
static/img/logo001-alpha.png
Normal file
BIN
static/img/logo001-alpha.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
BIN
static/img/logo001.png
Normal file
BIN
static/img/logo001.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
BIN
static/img/logo002-alpha.png
Normal file
BIN
static/img/logo002-alpha.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
BIN
static/img/logoComunaTec.jpg
Normal file
BIN
static/img/logoComunaTec.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
51
static/js/comprovantes.js
Normal file
51
static/js/comprovantes.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
$(document).ready(function() {
|
||||||
|
// Inicialização da tabela
|
||||||
|
$('#tabelaComprovantes').DataTable({
|
||||||
|
language: {
|
||||||
|
url: '//cdn.datatables.net/plug-ins/1.13.7/i18n/pt-BR.json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Modal de edição
|
||||||
|
$('#modalEditarComprovante').on('show.bs.modal', function(event) {
|
||||||
|
var button = $(event.relatedTarget);
|
||||||
|
var comprovanteId = button.data('comprovante-id');
|
||||||
|
var militanteId = button.data('militante-id');
|
||||||
|
var militanteNome = button.data('militante-nome');
|
||||||
|
var tipoComprovante = button.data('tipo-comprovante');
|
||||||
|
var valor = button.data('valor');
|
||||||
|
var dataComprovante = button.data('data-comprovante');
|
||||||
|
|
||||||
|
var modal = $(this);
|
||||||
|
modal.find('#editMilitante').val(militanteId);
|
||||||
|
modal.find('#editMilitanteNome').val(militanteNome);
|
||||||
|
modal.find('#editTipoComprovante').val(tipoComprovante);
|
||||||
|
modal.find('#editValor').val(valor);
|
||||||
|
modal.find('#editDataComprovante').val(dataComprovante);
|
||||||
|
|
||||||
|
modal.find('form').attr('action', '/comprovantes/editar/' + comprovanteId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Modal de exclusão
|
||||||
|
$('#modalExcluirComprovante').on('show.bs.modal', function(event) {
|
||||||
|
var button = $(event.relatedTarget);
|
||||||
|
var comprovanteId = button.data('comprovante-id');
|
||||||
|
var comprovanteInfo = button.data('comprovante-info');
|
||||||
|
|
||||||
|
var modal = $(this);
|
||||||
|
modal.find('#comprovanteInfo').text(comprovanteInfo);
|
||||||
|
modal.find('form').attr('action', '/comprovantes/excluir/' + comprovanteId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Formatação de valores monetários
|
||||||
|
$('.money').mask('000.000.000.000.000,00', {reverse: true});
|
||||||
|
|
||||||
|
// Validação de formulários
|
||||||
|
$('form').on('submit', function(e) {
|
||||||
|
if (!this.checkValidity()) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
$(this).addClass('was-validated');
|
||||||
|
});
|
||||||
|
});
|
||||||
127
static/js/cotas.js
Normal file
127
static/js/cotas.js
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
console.log('Carregando script cotas.js...');
|
||||||
|
|
||||||
|
// Configuração do modal de edição
|
||||||
|
const modalEditarCota = document.getElementById('modalEditarCota');
|
||||||
|
if (modalEditarCota) {
|
||||||
|
modalEditarCota.addEventListener('show.bs.modal', function(event) {
|
||||||
|
console.log('Modal de edição sendo exibido');
|
||||||
|
const button = event.relatedTarget;
|
||||||
|
|
||||||
|
if (!button) {
|
||||||
|
console.error('Botão não encontrado!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cotaId = button.getAttribute('data-cota-id');
|
||||||
|
console.log('ID da cota:', cotaId);
|
||||||
|
|
||||||
|
// Dados da cota
|
||||||
|
const dados = {
|
||||||
|
militanteId: button.getAttribute('data-cota-militante'),
|
||||||
|
militanteNome: button.closest('tr').querySelector('td').textContent.trim(),
|
||||||
|
valorAntigo: button.closest('tr').querySelector('td[data-valor_antigo]').getAttribute('data-valor_antigo'),
|
||||||
|
valorNovo: button.closest('tr').querySelector('td[data-valor_novo]').getAttribute('data-valor_novo'),
|
||||||
|
dataAlteracao: button.getAttribute('data-cota-data-alteracao'),
|
||||||
|
dataVencimento: button.getAttribute('data-cota-data-vencimento'),
|
||||||
|
pago: button.getAttribute('data-cota-pago') === 'true'
|
||||||
|
};
|
||||||
|
console.log('Dados da cota:', dados);
|
||||||
|
|
||||||
|
// Preencher campos
|
||||||
|
document.getElementById('editMilitante').value = dados.militanteId;
|
||||||
|
document.getElementById('editMilitanteNome').value = dados.militanteNome;
|
||||||
|
document.getElementById('editValorAntigo').value = dados.valorAntigo;
|
||||||
|
document.getElementById('editValorNovo').value = dados.valorNovo;
|
||||||
|
document.getElementById('editDataAlteracao').value = dados.dataAlteracao;
|
||||||
|
document.getElementById('editDataVencimento').value = dados.dataVencimento;
|
||||||
|
document.getElementById('editPago').checked = dados.pago;
|
||||||
|
|
||||||
|
// Configurar formulário
|
||||||
|
const form = document.getElementById('formEditarCota');
|
||||||
|
if (form) {
|
||||||
|
form.action = `/cotas/editar/${cotaId}`;
|
||||||
|
console.log('Action do formulário:', form.action);
|
||||||
|
|
||||||
|
// Remover listeners antigos para evitar duplicação
|
||||||
|
const newForm = form.cloneNode(true);
|
||||||
|
form.parentNode.replaceChild(newForm, form);
|
||||||
|
|
||||||
|
// Adicionar listener para o submit do formulário
|
||||||
|
newForm.addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
console.log('Formulário submetido');
|
||||||
|
|
||||||
|
// Criar FormData com os dados do formulário
|
||||||
|
const formData = new FormData(this);
|
||||||
|
|
||||||
|
// Adicionar campo pago com o valor correto
|
||||||
|
const isPago = document.getElementById('editPago').checked;
|
||||||
|
formData.set('pago', isPago ? 'true' : 'false');
|
||||||
|
|
||||||
|
// Log dos dados sendo enviados
|
||||||
|
console.log('Dados do formulário:');
|
||||||
|
for (let [key, value] of formData.entries()) {
|
||||||
|
console.log(key + ': ' + value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enviar requisição
|
||||||
|
fetch(this.action, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
console.log('Status da resposta:', response.status);
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
console.log('Resposta:', data);
|
||||||
|
if (data.status === 'success') {
|
||||||
|
// Fechar modal
|
||||||
|
const modal = bootstrap.Modal.getInstance(modalEditarCota);
|
||||||
|
modal.hide();
|
||||||
|
|
||||||
|
// Recarregar página
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Erro ao atualizar cota: ' + data.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Erro:', error);
|
||||||
|
alert('Erro ao atualizar cota. Por favor, tente novamente.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configuração do modal de exclusão
|
||||||
|
const deleteModal = document.getElementById('deleteModal');
|
||||||
|
if (deleteModal) {
|
||||||
|
deleteModal.addEventListener('show.bs.modal', function(event) {
|
||||||
|
console.log('Modal de exclusão sendo exibido');
|
||||||
|
const button = event.relatedTarget;
|
||||||
|
|
||||||
|
if (!button) {
|
||||||
|
console.error('Botão não encontrado!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cotaId = button.getAttribute('data-cota-id');
|
||||||
|
const cotaInfo = button.getAttribute('data-cota-info');
|
||||||
|
console.log('ID da cota:', cotaId);
|
||||||
|
console.log('Info da cota:', cotaInfo);
|
||||||
|
|
||||||
|
// Atualizar texto do modal
|
||||||
|
document.getElementById('cotaInfo').textContent = cotaInfo;
|
||||||
|
|
||||||
|
// Configurar formulário de exclusão
|
||||||
|
const form = document.getElementById('deleteForm');
|
||||||
|
if (form) {
|
||||||
|
form.action = `/cotas/excluir/${cotaId}`;
|
||||||
|
console.log('Action do formulário:', form.action);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
203
static/js/forms.js
Normal file
203
static/js/forms.js
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
// 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"], input.date-mask');
|
||||||
|
dateInputs.forEach(input => {
|
||||||
|
input.addEventListener('change', function() {
|
||||||
|
console.log('Validando data:', this.value);
|
||||||
|
|
||||||
|
let dataValida = true;
|
||||||
|
let mensagemErro = '';
|
||||||
|
|
||||||
|
// Se for um campo com máscara, validar o formato
|
||||||
|
if (this.classList.contains('date-mask')) {
|
||||||
|
if (!validarData(this.value)) {
|
||||||
|
dataValida = false;
|
||||||
|
mensagemErro = 'Por favor, insira uma data válida no formato DD/MM/AAAA';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Para campos type="date", converter para Date
|
||||||
|
const date = new Date(this.value);
|
||||||
|
if (isNaN(date.getTime())) {
|
||||||
|
dataValida = false;
|
||||||
|
mensagemErro = 'Data inválida';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validar limites de data
|
||||||
|
if (dataValida) {
|
||||||
|
const hoje = new Date();
|
||||||
|
hoje.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
let dataComparacao;
|
||||||
|
if (this.classList.contains('date-mask')) {
|
||||||
|
const [dia, mes, ano] = this.value.split('/').map(Number);
|
||||||
|
dataComparacao = new Date(ano, mes - 1, dia);
|
||||||
|
} else {
|
||||||
|
dataComparacao = new Date(this.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar data mínima
|
||||||
|
if (this.hasAttribute('min')) {
|
||||||
|
const minDate = new Date(this.getAttribute('min'));
|
||||||
|
if (dataComparacao < minDate) {
|
||||||
|
dataValida = false;
|
||||||
|
mensagemErro = `A data não pode ser anterior a ${minDate.toLocaleDateString()}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar data máxima
|
||||||
|
if (this.hasAttribute('max')) {
|
||||||
|
const maxDate = new Date(this.getAttribute('max'));
|
||||||
|
if (dataComparacao > maxDate) {
|
||||||
|
dataValida = false;
|
||||||
|
mensagemErro = `A data não pode ser posterior a ${maxDate.toLocaleDateString()}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar se é data futura (quando não permitido)
|
||||||
|
if (this.hasAttribute('data-no-future') && dataComparacao > hoje) {
|
||||||
|
dataValida = false;
|
||||||
|
mensagemErro = 'A data não pode ser futura';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Atualizar validação do campo
|
||||||
|
if (!dataValida) {
|
||||||
|
console.warn('Data inválida:', this.value, mensagemErro);
|
||||||
|
this.setCustomValidity(mensagemErro);
|
||||||
|
this.classList.add('is-invalid');
|
||||||
|
|
||||||
|
// Atualizar mensagem de feedback
|
||||||
|
const feedback = this.nextElementSibling;
|
||||||
|
if (feedback && feedback.classList.contains('invalid-feedback')) {
|
||||||
|
feedback.textContent = mensagemErro;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('Data válida:', this.value);
|
||||||
|
this.setCustomValidity('');
|
||||||
|
this.classList.remove('is-invalid');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Limpar validação ao começar a digitar
|
||||||
|
input.addEventListener('input', function() {
|
||||||
|
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 += ' <span class="text-danger">*</span>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
11
static/js/home.js
Normal file
11
static/js/home.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Configurar clique nos itens da lista de comprovantes
|
||||||
|
document.querySelectorAll('.list-group-item[onclick*="carregarDadosComprovante"]').forEach(item => {
|
||||||
|
item.addEventListener('click', function(e) {
|
||||||
|
const comprovanteId = this.getAttribute('data-comprovante-id');
|
||||||
|
if (comprovanteId) {
|
||||||
|
carregarDadosComprovante(comprovanteId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
157
static/js/main.js
Normal file
157
static/js/main.js
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
// Configuração do token CSRF para requisições AJAX
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
||||||
|
$.ajaxSetup({
|
||||||
|
beforeSend: function(xhr, settings) {
|
||||||
|
if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) {
|
||||||
|
xhr.setRequestHeader("X-CSRFToken", csrfToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
1461
static/js/militantes.js
Normal file
1461
static/js/militantes.js
Normal file
File diff suppressed because it is too large
Load Diff
208
static/js/table_sort.js
Normal file
208
static/js/table_sort.js
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
// Função para converter data DD/MM/YYYY para objeto Date
|
||||||
|
function converterDataParaComparacao(dataStr) {
|
||||||
|
console.log('Convertendo data para comparação:', dataStr);
|
||||||
|
|
||||||
|
if (!dataStr) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Se já estiver no formato ISO
|
||||||
|
if (/^\d{4}-\d{2}-\d{2}/.test(dataStr)) {
|
||||||
|
const data = new Date(dataStr);
|
||||||
|
console.log('Data ISO convertida:', data);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Se estiver no formato DD/MM/YYYY
|
||||||
|
if (/^\d{2}\/\d{2}\/\d{4}/.test(dataStr)) {
|
||||||
|
const [dia, mes, ano] = dataStr.split('/').map(Number);
|
||||||
|
const data = new Date(ano, mes - 1, dia);
|
||||||
|
console.log('Data DD/MM/YYYY convertida:', data);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn('Formato de data não reconhecido:', dataStr);
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao converter data:', error, 'Data:', dataStr);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Função para ordenar tabelas
|
||||||
|
function configurarOrdenacaoTabela(tabelaId) {
|
||||||
|
console.log('Configurando ordenação para tabela:', tabelaId);
|
||||||
|
|
||||||
|
const table = document.getElementById(tabelaId);
|
||||||
|
if (!table) {
|
||||||
|
console.warn('Tabela não encontrada:', tabelaId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = table.querySelectorAll('th[data-sort]');
|
||||||
|
headers.forEach(header => {
|
||||||
|
if (header.dataset.sort) {
|
||||||
|
header.addEventListener('click', () => {
|
||||||
|
const column = header.dataset.sort;
|
||||||
|
const tbody = table.getElementsByTagName('tbody')[0];
|
||||||
|
const rows = Array.from(tbody.getElementsByTagName('tr'));
|
||||||
|
|
||||||
|
console.log('Ordenando coluna:', column);
|
||||||
|
|
||||||
|
rows.sort((a, b) => {
|
||||||
|
const aValue = a.querySelector(`td[data-${column}]`).dataset[column];
|
||||||
|
const bValue = b.querySelector(`td[data-${column}]`).dataset[column];
|
||||||
|
|
||||||
|
// Ordenação por data
|
||||||
|
if (column === 'data' ||
|
||||||
|
column === 'data_vencimento' ||
|
||||||
|
column === 'data_alteracao' ||
|
||||||
|
column === 'data_comprovante' ||
|
||||||
|
column === 'data_venda' ||
|
||||||
|
column === 'data_relatorio') {
|
||||||
|
const aDate = converterDataParaComparacao(aValue);
|
||||||
|
const bDate = converterDataParaComparacao(bValue);
|
||||||
|
|
||||||
|
// Se alguma data for inválida
|
||||||
|
if (!aDate && !bDate) return 0;
|
||||||
|
if (!aDate) return 1;
|
||||||
|
if (!bDate) return -1;
|
||||||
|
|
||||||
|
return aDate - bDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ordenação por valor monetário
|
||||||
|
if (column === 'valor' ||
|
||||||
|
column === 'valor_total' ||
|
||||||
|
column === 'valor_antigo' ||
|
||||||
|
column === 'valor_novo') {
|
||||||
|
const aNum = parseFloat(aValue.replace(/[^\d,-]/g, '').replace(',', '.'));
|
||||||
|
const bNum = parseFloat(bValue.replace(/[^\d,-]/g, '').replace(',', '.'));
|
||||||
|
return aNum - bNum;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ordenação padrão para texto
|
||||||
|
return aValue.localeCompare(bValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Alternar direção da ordenação
|
||||||
|
if (header.classList.contains('asc')) {
|
||||||
|
rows.reverse();
|
||||||
|
header.classList.remove('asc');
|
||||||
|
header.classList.add('desc');
|
||||||
|
console.log('Ordenação descendente');
|
||||||
|
} else {
|
||||||
|
header.classList.remove('desc');
|
||||||
|
header.classList.add('asc');
|
||||||
|
console.log('Ordenação ascendente');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Atualizar tabela
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
rows.forEach(row => tbody.appendChild(row));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configurar ordenação para todas as tabelas que precisam
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
console.log('Configurando ordenação para todas as tabelas...');
|
||||||
|
|
||||||
|
const tabelas = [
|
||||||
|
'materiaisTable',
|
||||||
|
'vendasTable',
|
||||||
|
'cotasTable',
|
||||||
|
'comprovantesTable'
|
||||||
|
];
|
||||||
|
|
||||||
|
tabelas.forEach(tabelaId => {
|
||||||
|
configurarOrdenacaoTabela(tabelaId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
console.log('Carregando script table_sort.js...');
|
||||||
|
|
||||||
|
// Função para comparar datas no formato DD/MM/YYYY
|
||||||
|
function compararDatas(a, b) {
|
||||||
|
if (!a || !b) return 0;
|
||||||
|
|
||||||
|
const [diaA, mesA, anoA] = a.split('/').map(Number);
|
||||||
|
const [diaB, mesB, anoB] = b.split('/').map(Number);
|
||||||
|
|
||||||
|
const dataA = new Date(anoA, mesA - 1, diaA);
|
||||||
|
const dataB = new Date(anoB, mesB - 1, diaB);
|
||||||
|
|
||||||
|
return dataA - dataB;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Função para comparar valores monetários
|
||||||
|
function compararValores(a, b) {
|
||||||
|
const valorA = parseFloat(a.replace('R$ ', '').replace('.', '').replace(',', '.'));
|
||||||
|
const valorB = parseFloat(b.replace('R$ ', '').replace('.', '').replace(',', '.'));
|
||||||
|
|
||||||
|
if (isNaN(valorA)) return -1;
|
||||||
|
if (isNaN(valorB)) return 1;
|
||||||
|
|
||||||
|
return valorA - valorB;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configurar ordenação para todas as tabelas com classe 'table-sort'
|
||||||
|
document.querySelectorAll('table.table-sort').forEach(tabela => {
|
||||||
|
const tbody = tabela.querySelector('tbody');
|
||||||
|
const headers = tabela.querySelectorAll('th[data-sort]');
|
||||||
|
|
||||||
|
headers.forEach(header => {
|
||||||
|
const tipoOrdenacao = header.dataset.sort;
|
||||||
|
|
||||||
|
header.addEventListener('click', () => {
|
||||||
|
const rows = Array.from(tbody.querySelectorAll('tr'));
|
||||||
|
const colIndex = Array.from(header.parentNode.children).indexOf(header);
|
||||||
|
|
||||||
|
rows.sort((rowA, rowB) => {
|
||||||
|
const cellA = rowA.children[colIndex].dataset[tipoOrdenacao] || rowA.children[colIndex].textContent.trim();
|
||||||
|
const cellB = rowB.children[colIndex].dataset[tipoOrdenacao] || rowB.children[colIndex].textContent.trim();
|
||||||
|
|
||||||
|
switch (tipoOrdenacao) {
|
||||||
|
case 'data':
|
||||||
|
return compararDatas(cellA, cellB);
|
||||||
|
case 'valor':
|
||||||
|
return compararValores(cellA, cellB);
|
||||||
|
case 'numero':
|
||||||
|
return parseFloat(cellA) - parseFloat(cellB);
|
||||||
|
default:
|
||||||
|
return cellA.localeCompare(cellB);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (header.classList.contains('asc')) {
|
||||||
|
rows.reverse();
|
||||||
|
header.classList.remove('asc');
|
||||||
|
header.classList.add('desc');
|
||||||
|
} else {
|
||||||
|
header.classList.remove('desc');
|
||||||
|
header.classList.add('asc');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remover classes de ordenação de outros headers
|
||||||
|
headers.forEach(h => {
|
||||||
|
if (h !== header) {
|
||||||
|
h.classList.remove('asc', 'desc');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Atualizar tabela
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
rows.forEach(row => tbody.appendChild(row));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function sortTable(table, column, type = 'text') {
|
||||||
|
// ... existing code ...
|
||||||
|
if (column === 'data_comprovante') {
|
||||||
|
// ... existing code ...
|
||||||
|
}
|
||||||
|
// ... existing code ...
|
||||||
|
}
|
||||||
284
static/js/testes.js
Normal file
284
static/js/testes.js
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
// Testes para o formulário de edição de militantes
|
||||||
|
console.log('Iniciando testes do formulário de edição...');
|
||||||
|
|
||||||
|
// Lista de campos que devem existir no formulário
|
||||||
|
const camposEsperados = {
|
||||||
|
'edit_militante_id': { tipo: 'hidden', obrigatorio: true },
|
||||||
|
'edit_nome': { tipo: 'text', obrigatorio: true },
|
||||||
|
'edit_cpf': { tipo: 'text', obrigatorio: true },
|
||||||
|
'edit_titulo_eleitoral': { tipo: 'text', obrigatorio: false },
|
||||||
|
'edit_data_nascimento': { tipo: 'text', obrigatorio: false },
|
||||||
|
'edit_data_entrada_oci': { tipo: 'text', obrigatorio: false },
|
||||||
|
'edit_data_efetivacao_oci': { tipo: 'text', obrigatorio: false },
|
||||||
|
'edit_email': { tipo: 'email', obrigatorio: true },
|
||||||
|
'edit_telefone1': { tipo: 'text', obrigatorio: false },
|
||||||
|
'edit_telefone2': { tipo: 'text', obrigatorio: false },
|
||||||
|
'edit_cep': { tipo: 'text', obrigatorio: false },
|
||||||
|
'edit_estado': { tipo: 'select', obrigatorio: false },
|
||||||
|
'edit_cidade': { tipo: 'text', obrigatorio: false },
|
||||||
|
'edit_bairro': { tipo: 'text', obrigatorio: false },
|
||||||
|
'edit_rua': { tipo: 'text', obrigatorio: false },
|
||||||
|
'edit_numero': { tipo: 'text', obrigatorio: false },
|
||||||
|
'edit_complemento': { tipo: 'text', obrigatorio: false },
|
||||||
|
'edit_empresa': { tipo: 'text', obrigatorio: false },
|
||||||
|
'edit_contratante': { tipo: 'text', obrigatorio: false },
|
||||||
|
'edit_instituicao_ensino': { tipo: 'text', obrigatorio: false },
|
||||||
|
'edit_tipo_instituicao': { tipo: 'select', obrigatorio: false },
|
||||||
|
'edit_sindicato': { tipo: 'text', obrigatorio: false },
|
||||||
|
'edit_cargo_sindical': { tipo: 'text', obrigatorio: false },
|
||||||
|
'edit_central_sindical': { tipo: 'text', obrigatorio: false },
|
||||||
|
'edit_celula': { tipo: 'select', obrigatorio: false },
|
||||||
|
'responsabilidades_values': { tipo: 'hidden', obrigatorio: false }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Função para testar a existência e configuração dos campos
|
||||||
|
function testarCamposFormulario() {
|
||||||
|
console.log('Testando campos do formulário...');
|
||||||
|
const form = document.getElementById('formEditarMilitante');
|
||||||
|
const erros = [];
|
||||||
|
|
||||||
|
if (!form) {
|
||||||
|
console.error('Formulário não encontrado!');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Testar cada campo esperado
|
||||||
|
for (const [id, config] of Object.entries(camposEsperados)) {
|
||||||
|
const campo = document.getElementById(id);
|
||||||
|
if (!campo) {
|
||||||
|
erros.push(`Campo ${id} não encontrado`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar tipo
|
||||||
|
if (campo.type !== config.tipo && config.tipo !== 'select') {
|
||||||
|
erros.push(`Campo ${id} tem tipo ${campo.type}, esperado ${config.tipo}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar obrigatoriedade
|
||||||
|
if (config.obrigatorio && !campo.hasAttribute('required')) {
|
||||||
|
erros.push(`Campo ${id} deveria ser obrigatório`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar se o campo tem name attribute
|
||||||
|
if (!campo.hasAttribute('name')) {
|
||||||
|
erros.push(`Campo ${id} não tem atributo name`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reportar erros encontrados
|
||||||
|
if (erros.length > 0) {
|
||||||
|
console.error('Erros encontrados nos campos:', erros);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Todos os campos estão configurados corretamente');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Função para testar o carregamento de dados
|
||||||
|
async function testarCarregamentoDados(militanteId) {
|
||||||
|
console.log('Testando carregamento de dados...');
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/militantes/dados/${militanteId}`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Erro HTTP: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log('Dados recebidos:', data);
|
||||||
|
|
||||||
|
// Verificar se os dados foram carregados corretamente
|
||||||
|
const erros = [];
|
||||||
|
|
||||||
|
// Verificar campos básicos
|
||||||
|
if (!data.nome) erros.push('Nome não carregado');
|
||||||
|
if (!data.cpf) erros.push('CPF não carregado');
|
||||||
|
|
||||||
|
// Verificar se os campos foram preenchidos
|
||||||
|
for (const [id, config] of Object.entries(camposEsperados)) {
|
||||||
|
const campo = document.getElementById(id);
|
||||||
|
if (!campo) continue;
|
||||||
|
|
||||||
|
// Mapear campos do servidor para campos do formulário
|
||||||
|
let valorEsperado = '';
|
||||||
|
switch(id) {
|
||||||
|
case 'edit_nome': valorEsperado = data.nome; break;
|
||||||
|
case 'edit_cpf': valorEsperado = data.cpf; break;
|
||||||
|
case 'edit_email': valorEsperado = data.emails?.[0]; break;
|
||||||
|
case 'edit_telefone1': valorEsperado = data.telefone1; break;
|
||||||
|
case 'edit_celula': valorEsperado = data.celula_id?.toString(); break;
|
||||||
|
case 'edit_cargo_sindical': valorEsperado = data.cargo_sindical; break;
|
||||||
|
case 'edit_central_sindical': valorEsperado = data.central_sindical; break;
|
||||||
|
case 'edit_sindicato': valorEsperado = data.sindicato; break;
|
||||||
|
// Adicione mais campos conforme necessário
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.obrigatorio && !valorEsperado) {
|
||||||
|
erros.push(`Campo obrigatório ${id} não tem valor no servidor`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valorEsperado && campo.value !== valorEsperado) {
|
||||||
|
erros.push(`Campo ${id} tem valor diferente do servidor. Esperado: ${valorEsperado}, Atual: ${campo.value}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (erros.length > 0) {
|
||||||
|
console.error('Erros no carregamento:', erros);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Dados carregados corretamente');
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao carregar dados:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Função para testar o salvamento de dados
|
||||||
|
async function testarSalvamentoDados(militanteId) {
|
||||||
|
console.log('Testando salvamento de dados...');
|
||||||
|
try {
|
||||||
|
const form = document.getElementById('formEditarMilitante');
|
||||||
|
const formData = new FormData(form);
|
||||||
|
|
||||||
|
// Guardar valores originais para comparação
|
||||||
|
const valoresOriginais = {
|
||||||
|
nome: formData.get('nome'),
|
||||||
|
cpf: formData.get('cpf'),
|
||||||
|
email: formData.get('email'),
|
||||||
|
celula: formData.get('celula'),
|
||||||
|
cargo_sindical: formData.get('cargo_sindical'),
|
||||||
|
central_sindical: formData.get('central_sindical'),
|
||||||
|
sindicato: formData.get('sindicato'),
|
||||||
|
responsabilidades: formData.get('responsabilidades_values')
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(`/militantes/editar/${militanteId}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
|
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')
|
||||||
|
},
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Erro HTTP: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log('Resposta do servidor:', data);
|
||||||
|
|
||||||
|
// Verificar se os dados foram salvos corretamente
|
||||||
|
const row = document.querySelector(`tr[data-militante="${militanteId}"]`);
|
||||||
|
if (!row) {
|
||||||
|
console.error('Linha da tabela não encontrada após salvamento');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const erros = [];
|
||||||
|
|
||||||
|
// Verificar dados básicos na tabela
|
||||||
|
const nome = row.querySelector('td[data-nome]')?.textContent;
|
||||||
|
const cpf = row.querySelector('td[data-cpf]')?.textContent;
|
||||||
|
const email = row.querySelector('td[data-email]')?.textContent;
|
||||||
|
|
||||||
|
if (nome !== valoresOriginais.nome) erros.push(`Nome não atualizado na tabela. Esperado: ${valoresOriginais.nome}, Atual: ${nome}`);
|
||||||
|
if (cpf !== valoresOriginais.cpf) erros.push(`CPF não atualizado na tabela. Esperado: ${valoresOriginais.cpf}, Atual: ${cpf}`);
|
||||||
|
if (email !== valoresOriginais.email) erros.push(`Email não atualizado na tabela. Esperado: ${valoresOriginais.email}, Atual: ${email}`);
|
||||||
|
|
||||||
|
// Verificar atributos para filtros
|
||||||
|
const celulaId = row.getAttribute('data-celula-id');
|
||||||
|
const responsabilidades = row.getAttribute('data-responsabilidades');
|
||||||
|
|
||||||
|
if (celulaId !== valoresOriginais.celula) erros.push(`Célula não atualizada na tabela. Esperado: ${valoresOriginais.celula}, Atual: ${celulaId}`);
|
||||||
|
if (responsabilidades !== valoresOriginais.responsabilidades) erros.push(`Responsabilidades não atualizadas na tabela. Esperado: ${valoresOriginais.responsabilidades}, Atual: ${responsabilidades}`);
|
||||||
|
|
||||||
|
// Verificar botão de edição
|
||||||
|
const btnEditar = row.querySelector('button[data-bs-target="#modalEditarMilitante"]');
|
||||||
|
if (btnEditar) {
|
||||||
|
if (btnEditar.getAttribute('data-militante-nome') !== valoresOriginais.nome) {
|
||||||
|
erros.push('Nome não atualizado no botão de edição');
|
||||||
|
}
|
||||||
|
if (btnEditar.getAttribute('data-celula-id') !== valoresOriginais.celula) {
|
||||||
|
erros.push('Célula não atualizada no botão de edição');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (erros.length > 0) {
|
||||||
|
console.error('Erros no salvamento:', erros);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Dados salvos e atualizados corretamente');
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao salvar dados:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Função principal de teste
|
||||||
|
async function testarFormularioEdicao(militanteId) {
|
||||||
|
console.log('Iniciando teste completo do formulário...');
|
||||||
|
|
||||||
|
// Testar campos do formulário
|
||||||
|
if (!testarCamposFormulario()) {
|
||||||
|
console.error('Teste dos campos falhou');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Testar carregamento de dados
|
||||||
|
if (!await testarCarregamentoDados(militanteId)) {
|
||||||
|
console.error('Teste de carregamento falhou');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Testar salvamento de dados
|
||||||
|
if (!await testarSalvamentoDados(militanteId)) {
|
||||||
|
console.error('Teste de salvamento falhou');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Todos os testes passaram com sucesso!');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Executar testes quando o documento estiver carregado
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Adicionar botão de teste na interface
|
||||||
|
const btnTeste = document.createElement('button');
|
||||||
|
btnTeste.className = 'btn btn-info me-2';
|
||||||
|
btnTeste.innerHTML = '<i class="fas fa-vial me-2"></i>Testar Formulário';
|
||||||
|
btnTeste.onclick = function() {
|
||||||
|
// Pegar ID do primeiro militante da lista
|
||||||
|
const primeiraLinha = document.querySelector('#militantesTable tbody tr');
|
||||||
|
if (!primeiraLinha) {
|
||||||
|
mostrarAlerta('danger', 'Nenhum militante encontrado para teste');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const militanteId = primeiraLinha.getAttribute('data-militante');
|
||||||
|
if (!militanteId) {
|
||||||
|
mostrarAlerta('danger', 'ID do militante não encontrado');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Executar testes
|
||||||
|
testarFormularioEdicao(militanteId).then(sucesso => {
|
||||||
|
if (sucesso) {
|
||||||
|
mostrarAlerta('success', 'Testes concluídos com sucesso!');
|
||||||
|
} else {
|
||||||
|
mostrarAlerta('danger', 'Alguns testes falharam. Verifique o console para mais detalhes.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Adicionar botão ao lado do botão de exportar
|
||||||
|
const btnExportar = document.querySelector('.btn-exportar');
|
||||||
|
if (btnExportar && btnExportar.parentNode) {
|
||||||
|
btnExportar.parentNode.insertBefore(btnTeste, btnExportar);
|
||||||
|
}
|
||||||
|
});
|
||||||
119
static/js/vendas.js
Normal file
119
static/js/vendas.js
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
console.log('Carregando script vendas.js...');
|
||||||
|
|
||||||
|
// Funções de validação e formatação de datas
|
||||||
|
function validarData(data) {
|
||||||
|
if (!data) return false;
|
||||||
|
|
||||||
|
const dataObj = new Date(data);
|
||||||
|
if (isNaN(dataObj.getTime())) return false;
|
||||||
|
|
||||||
|
const hoje = new Date();
|
||||||
|
hoje.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
return dataObj <= hoje;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatarData(data) {
|
||||||
|
if (!data) return '';
|
||||||
|
|
||||||
|
const dataObj = new Date(data);
|
||||||
|
if (isNaN(dataObj.getTime())) return '';
|
||||||
|
|
||||||
|
return dataObj.toLocaleDateString('pt-BR');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configurar campos de data
|
||||||
|
const camposData = document.querySelectorAll('input[type="date"]');
|
||||||
|
camposData.forEach(campo => {
|
||||||
|
// Definir data máxima como hoje
|
||||||
|
const hoje = new Date().toISOString().split('T')[0];
|
||||||
|
campo.setAttribute('max', hoje);
|
||||||
|
|
||||||
|
campo.addEventListener('change', function() {
|
||||||
|
if (!validarData(this.value)) {
|
||||||
|
this.setCustomValidity('Data inválida ou futura');
|
||||||
|
this.classList.add('is-invalid');
|
||||||
|
} else {
|
||||||
|
this.setCustomValidity('');
|
||||||
|
this.classList.remove('is-invalid');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configurar tabela de vendas
|
||||||
|
const tabelaVendas = $('#vendasTable').DataTable({
|
||||||
|
language: {
|
||||||
|
url: '//cdn.datatables.net/plug-ins/1.13.7/i18n/pt-BR.json'
|
||||||
|
},
|
||||||
|
columnDefs: [
|
||||||
|
{
|
||||||
|
targets: 3, // Coluna de data
|
||||||
|
type: 'date-br',
|
||||||
|
render: function(data, type, row) {
|
||||||
|
if (type === 'sort') {
|
||||||
|
return data.split('/').reverse().join('');
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
targets: 2, // Coluna de valor
|
||||||
|
type: 'numeric',
|
||||||
|
render: function(data, type, row) {
|
||||||
|
if (type === 'sort') {
|
||||||
|
return parseFloat(data.replace('R$ ', '').replace(',', '.'));
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ targets: -1, orderable: false } // Coluna de ações
|
||||||
|
],
|
||||||
|
order: [[3, 'desc']] // Ordenar por data decrescente por padrão
|
||||||
|
});
|
||||||
|
|
||||||
|
// Atualizar valor total ao mudar quantidade ou material
|
||||||
|
const campoQuantidade = document.getElementById('quantidade');
|
||||||
|
const campoMaterial = document.getElementById('material_id');
|
||||||
|
const campoValorTotal = document.getElementById('valor_total');
|
||||||
|
|
||||||
|
function atualizarValorTotal() {
|
||||||
|
if (!campoQuantidade || !campoMaterial || !campoValorTotal) return;
|
||||||
|
|
||||||
|
const quantidade = parseInt(campoQuantidade.value) || 0;
|
||||||
|
const materialSelecionado = campoMaterial.options[campoMaterial.selectedIndex];
|
||||||
|
const preco = materialSelecionado ? parseFloat(materialSelecionado.dataset.preco) || 0 : 0;
|
||||||
|
|
||||||
|
campoValorTotal.value = (quantidade * preco).toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (campoQuantidade) {
|
||||||
|
campoQuantidade.addEventListener('change', atualizarValorTotal);
|
||||||
|
}
|
||||||
|
if (campoMaterial) {
|
||||||
|
campoMaterial.addEventListener('change', atualizarValorTotal);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configurar modal de edição
|
||||||
|
const modalEditarVenda = document.getElementById('modalEditarVenda');
|
||||||
|
if (modalEditarVenda) {
|
||||||
|
modalEditarVenda.addEventListener('show.bs.modal', function(event) {
|
||||||
|
const button = event.relatedTarget;
|
||||||
|
if (!button) return;
|
||||||
|
|
||||||
|
const vendaId = button.getAttribute('data-venda-id');
|
||||||
|
const militanteId = button.getAttribute('data-militante-id');
|
||||||
|
const materialId = button.getAttribute('data-material-id');
|
||||||
|
const quantidade = button.getAttribute('data-quantidade');
|
||||||
|
const valorTotal = button.getAttribute('data-valor-total');
|
||||||
|
const dataVenda = button.getAttribute('data-data-venda');
|
||||||
|
|
||||||
|
document.getElementById('editVendaId').value = vendaId;
|
||||||
|
document.getElementById('editMilitanteId').value = militanteId;
|
||||||
|
document.getElementById('editMaterialId').value = materialId;
|
||||||
|
document.getElementById('editQuantidade').value = quantidade;
|
||||||
|
document.getElementById('editValorTotal').value = valorTotal;
|
||||||
|
document.getElementById('editDataVenda').value = dataVenda;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,156 +1,628 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="pt-BR">
|
<html lang="pt-br">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>{% block title %}{% endblock %} - Sistema de Controle OCI</title>
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='img/favicon.ico') }}">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" rel="stylesheet">
|
<title>{% block title %}{% endblock %} - Controles OCI</title>
|
||||||
|
|
||||||
|
<!-- Bootstrap 5 CSS -->
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css?v=1" rel="stylesheet">
|
||||||
|
<!-- Font Awesome 6 -->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css?v=1">
|
||||||
|
<!-- Componentes CSS -->
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/components.css') }}">
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
:root {
|
||||||
|
--primary-color: #dc3545;
|
||||||
|
--primary-light: #e35d6a;
|
||||||
|
--secondary-color: #6c757d;
|
||||||
|
--secondary-light: #868e96;
|
||||||
|
--success-color: #198754;
|
||||||
|
--danger-color: #dc3545;
|
||||||
|
--warning-color: #ffc107;
|
||||||
|
--info-color: #0dcaf0;
|
||||||
|
--background-gradient: linear-gradient(135deg, var(--primary-color) 40%, white 100%);
|
||||||
|
--navbar-stripe: 4px solid var(--primary-color);
|
||||||
|
|
||||||
|
/* Adicionando variáveis para os botões */
|
||||||
|
--bs-success: #198754;
|
||||||
|
--bs-success-dark: #157347;
|
||||||
|
--bs-secondary: #6c757d;
|
||||||
|
--bs-secondary-dark: #565e64;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
padding-top: 56px;
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
background: #343a40 !important;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
border-bottom: var(--navbar-stripe);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar > .container-fluid {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1320px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.navbar-brand {
|
.navbar-brand {
|
||||||
font-weight: bold;
|
flex: 0 0 auto;
|
||||||
}
|
margin-right: 2rem;
|
||||||
.nav-link {
|
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
color: #fff !important;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 1.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.navbar-brand img {
|
||||||
|
height: 35px;
|
||||||
|
margin-right: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#navbarNav {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-nav.mx-auto {
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-nav:last-child {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
margin-left: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
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: #fff !important;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link i {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu {
|
||||||
|
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 {
|
||||||
|
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: var(--primary-color);
|
||||||
|
color: #fff !important;
|
||||||
|
transform: translateX(3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item i {
|
||||||
|
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 {
|
||||||
|
max-width: 1320px !important;
|
||||||
|
margin: 0 auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1400px) {
|
||||||
|
.container {
|
||||||
|
max-width: 1140px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.container {
|
||||||
|
max-width: 960px !important;
|
||||||
|
}
|
||||||
|
.page-wrapper {
|
||||||
|
padding: 1.5rem 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
.container {
|
||||||
|
max-width: 720px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.container {
|
||||||
|
max-width: 540px !important;
|
||||||
|
}
|
||||||
|
.page-wrapper {
|
||||||
|
padding: 1rem 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
.page-wrapper {
|
||||||
|
padding: 0.75rem 0.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cards da Dashboard */
|
||||||
.card {
|
.card {
|
||||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
border: none;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-header {
|
.card-header {
|
||||||
background-color: #f8f9fa;
|
background-color: #f8f9fa;
|
||||||
border-bottom: 1px solid #dee2e6;
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
.btn-primary {
|
|
||||||
background-color: #0d6efd;
|
.card-header .card-title {
|
||||||
border-color: #0d6efd;
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
.btn-success {
|
|
||||||
background-color: #198754;
|
.card-header h5 {
|
||||||
border-color: #198754;
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 1.1rem;
|
||||||
}
|
}
|
||||||
.btn-secondary {
|
|
||||||
background-color: #6c757d;
|
.card-header h5 i {
|
||||||
border-color: #6c757d;
|
margin-right: 0.75rem;
|
||||||
|
color: var(--primary-color);
|
||||||
}
|
}
|
||||||
.btn-outline-primary {
|
|
||||||
color: #0d6efd;
|
.card-body {
|
||||||
border-color: #0d6efd;
|
padding: 1.5rem;
|
||||||
}
|
}
|
||||||
.btn-outline-primary:hover {
|
|
||||||
background-color: #0d6efd;
|
.card-footer {
|
||||||
color: #fff;
|
background: none;
|
||||||
|
border-top: 1px solid rgba(0,0,0,0.05);
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Estatísticas da Dashboard */
|
||||||
|
.stats-card {
|
||||||
|
position: relative;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 0.25rem 0.5rem rgba(0,0,0,0.1);
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 140px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 0.5rem 1rem rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card.blue {
|
||||||
|
background: linear-gradient(45deg, var(--primary-color), var(--primary-light));
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card.green {
|
||||||
|
background: linear-gradient(45deg, #1cc88a, #13855c);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card.cyan {
|
||||||
|
background: linear-gradient(45deg, #36b9cc, #258391);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card.yellow {
|
||||||
|
background: linear-gradient(45deg, #f6c23e, #dda20a);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card .title {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: rgba(255,255,255,0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card .value {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card .link {
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card .link:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card .icon {
|
||||||
|
position: absolute;
|
||||||
|
right: 1rem;
|
||||||
|
bottom: 1rem;
|
||||||
|
font-size: 4rem;
|
||||||
|
opacity: 0.2;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tabelas e Listas */
|
||||||
|
.table {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th {
|
||||||
|
border-top: none;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table td {
|
||||||
|
padding: 1rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-hover tbody tr:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item {
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid rgba(0,0,0,0.05);
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.militante-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.militante-info h6 {
|
||||||
|
margin: 0;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.militante-info small {
|
||||||
|
color: var(--secondary-color);
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Botões e Alertas */
|
||||||
.alert {
|
.alert {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,0.075);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
.form-control:focus {
|
|
||||||
border-color: #0d6efd;
|
.btn-primary {
|
||||||
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
|
background-color: var(--primary-color);
|
||||||
|
border-color: var(--primary-color);
|
||||||
}
|
}
|
||||||
.form-select:focus {
|
|
||||||
border-color: #0d6efd;
|
.btn-success {
|
||||||
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
|
background-color: var(--success-color);
|
||||||
|
border-color: var(--success-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background-color: var(--danger-color);
|
||||||
|
border-color: var(--danger-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
border-color: var(--secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Badges e Labels */
|
||||||
|
.badge {
|
||||||
|
padding: 0.5em 0.75em;
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-muted {
|
||||||
|
color: var(--secondary-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
border-top: 1px solid #e9ecef;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Login page specific */
|
||||||
|
.login-page {
|
||||||
|
background: var(--background-gradient);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-container {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border-radius: 1rem;
|
||||||
|
padding: 2rem;
|
||||||
|
box-shadow: 0 0.5rem 1rem rgba(0,0,0,0.15);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-logo {
|
||||||
|
height: 60px;
|
||||||
|
width: auto;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-title {
|
||||||
|
color: var(--primary-color);
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-subtitle {
|
||||||
|
color: var(--secondary-color);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-header {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.stats-card {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-wrapper {
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
min-height: calc(100vh - 70px);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
{% block extra_css %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
|
{% block navbar %}
|
||||||
<div class="container">
|
<nav class="navbar navbar-expand-lg navbar-dark">
|
||||||
<a class="navbar-brand" href="{{ url_for('home') }}">Sistema de Controle OCI</a>
|
<div class="container-fluid">
|
||||||
|
<a class="navbar-brand" href="{{ url_for('home') }}">
|
||||||
|
<img src="{{ url_for('static', filename='img/logo002-alpha.png') }}" alt="Logo OCI">
|
||||||
|
Controles OCI
|
||||||
|
</a>
|
||||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
|
{% if session.get('user_id') %}
|
||||||
<div class="collapse navbar-collapse" id="navbarNav">
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
<ul class="navbar-nav me-auto">
|
<ul class="navbar-nav mx-auto">
|
||||||
{% if current_user is defined and current_user.is_authenticated %}
|
<li class="nav-item dropdown">
|
||||||
{% if current_user.is_admin %}
|
<a class="nav-link" href="#" data-bs-toggle="dropdown">
|
||||||
<li class="nav-item">
|
<i class="fas fa-users me-1"></i>Militantes
|
||||||
<a class="nav-link" href="{{ url_for('dashboard_admin') }}">Dashboard Admin</a>
|
</a>
|
||||||
</li>
|
<ul class="dropdown-menu">
|
||||||
{% else %}
|
<li>
|
||||||
<li class="nav-item">
|
<a class="dropdown-item" href="{{ url_for('listar_militantes') }}">
|
||||||
<a class="nav-link" href="{{ url_for('home') }}">Início</a>
|
<i class="fas fa-list"></i>Listar Militantes
|
||||||
</li>
|
</a>
|
||||||
|
</li>
|
||||||
{% if current_user.has_permission('view_cell_data') %}
|
</ul>
|
||||||
<li class="nav-item">
|
</li>
|
||||||
<a class="nav-link" href="{{ url_for('listar_militantes') }}">Militantes</a>
|
<li class="nav-item dropdown">
|
||||||
</li>
|
<a class="nav-link" href="#" data-bs-toggle="dropdown">
|
||||||
{% endif %}
|
<i class="fas fa-dollar-sign me-1"></i>Financeiro
|
||||||
|
</a>
|
||||||
{% if current_user.has_permission('view_cell_reports') %}
|
<ul class="dropdown-menu">
|
||||||
<li class="nav-item">
|
<li>
|
||||||
<a class="nav-link" href="{{ url_for('listar_pagamentos') }}">Pagamentos</a>
|
<a class="dropdown-item" href="{{ url_for('listar_cotas') }}">
|
||||||
</li>
|
<i class="fas fa-money-bill-wave"></i>Cotas
|
||||||
<li class="nav-item">
|
</a>
|
||||||
<a class="nav-link" href="{{ url_for('listar_materiais') }}">Materiais</a>
|
</li>
|
||||||
</li>
|
<li>
|
||||||
<li class="nav-item">
|
<a class="dropdown-item" href="{{ url_for('listar_comprovantes') }}">
|
||||||
<a class="nav-link" href="{{ url_for('listar_relatorios_vendas') }}">Vendas</a>
|
<i class="fas fa-receipt"></i>Comprovantes
|
||||||
</li>
|
</a>
|
||||||
{% endif %}
|
</li>
|
||||||
|
</ul>
|
||||||
{% if current_user.has_permission('view_cell_reports') or current_user.has_permission('view_sector_reports') or current_user.has_permission('view_cr_reports') %}
|
</li>
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown">
|
<a class="nav-link" href="#" data-bs-toggle="dropdown">
|
||||||
Relatórios
|
<i class="fas fa-box me-1"></i>Materiais
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
{% if current_user.has_permission('view_cell_reports') %}
|
<li>
|
||||||
<li><a class="dropdown-item" href="{{ url_for('listar_relatorios_cotas') }}">Relatórios de Cotas</a></li>
|
<a class="dropdown-item" href="{{ url_for('listar_materiais') }}">
|
||||||
<li><a class="dropdown-item" href="{{ url_for('listar_relatorios_vendas') }}">Relatórios de Vendas</a></li>
|
<i class="fas fa-box"></i>Listar Materiais
|
||||||
{% endif %}
|
</a>
|
||||||
</ul>
|
</li>
|
||||||
</li>
|
<li>
|
||||||
{% endif %}
|
<a class="dropdown-item" href="{{ url_for('listar_vendas_jornal') }}">
|
||||||
{% endif %}
|
<i class="fas fa-newspaper"></i>Vendas de Jornais
|
||||||
{% endif %}
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="{{ url_for('listar_vendas_jornal') }}">
|
||||||
|
<i class="fas fa-file-signature"></i>Assinaturas de Jornal
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link" href="#" data-bs-toggle="dropdown">
|
||||||
|
<i class="fas fa-chart-bar me-1"></i>Relatórios
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="{{ url_for('listar_relatorios_cotas') }}">
|
||||||
|
<i class="fas fa-file-invoice-dollar"></i>Relatórios de Cotas
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="{{ url_for('listar_relatorios_vendas') }}">
|
||||||
|
<i class="fas fa-file-alt"></i>Relatórios de Vendas
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="navbar-nav">
|
<ul class="navbar-nav">
|
||||||
{% if current_user is defined and current_user.is_authenticated %}
|
<li class="nav-item dropdown">
|
||||||
<li class="nav-item">
|
<a class="nav-link" href="#" data-bs-toggle="dropdown">
|
||||||
<a class="nav-link" href="{{ url_for('logout') }}">Sair</a>
|
<i class="fas fa-user me-1"></i>{{ session.get('username', 'Usuário') }}
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
|
{% if session.get('is_admin') %}
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="{{ url_for('novo_usuario') }}">
|
||||||
|
<i class="fas fa-user-plus"></i>Novo Usuário
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
{% endif %}
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="{{ url_for('logout') }}">
|
||||||
|
<i class="fas fa-sign-out-alt"></i>Sair
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
{% else %}
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="{{ url_for('login') }}">Login</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
{% endblock %}
|
||||||
<div class="container mt-4">
|
|
||||||
{% block content %}{% endblock %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
|
||||||
<script>
|
|
||||||
// Verificar status da sessão a cada 5 minutos
|
|
||||||
function checkSession() {
|
|
||||||
fetch('/check_session')
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.expired) {
|
|
||||||
window.location.href = '/login';
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => console.error('Erro ao verificar sessão:', error));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verificar a cada 5 minutos
|
<div class="page-wrapper">
|
||||||
setInterval(checkSession, 5 * 60 * 1000);
|
<div class="container py-4">
|
||||||
|
{% block content %}{% endblock %}
|
||||||
// Verificar também quando a página ganha foco
|
</div>
|
||||||
document.addEventListener('visibilitychange', function() {
|
</div>
|
||||||
if (document.visibilityState === 'visible') {
|
|
||||||
checkSession();
|
<!-- Bootstrap 5 JS Bundle with Popper -->
|
||||||
}
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
});
|
{% block scripts %}{% endblock %}
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
<form method="POST" class="needs-validation" novalidate>
|
<form method="POST" class="needs-validation" novalidate>
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
<label for="nome" class="form-label">Nome</label>
|
<label for="nome" class="form-label">Nome</label>
|
||||||
|
|||||||
43
templates/editar_comprovante.html
Normal file
43
templates/editar_comprovante.html
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Editar Comprovante{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8 offset-md-2">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header bg-light">
|
||||||
|
<h4 class="card-title mb-0">
|
||||||
|
<i class="fas fa-money-bill-wave me-2"></i>Editar Comprovante
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="tipo_comprovante_id">Tipo de Comprovante</label>
|
||||||
|
<select class="form-control" id="tipo_comprovante_id" name="tipo_comprovante_id" required>
|
||||||
|
<option value="1" {% if comprovante.tipo_comprovante_id == 1 %}selected{% endif %}>1 - Comprovante Padrão</option>
|
||||||
|
{% if current_user.has_permission('gerenciar_tipos_comprovante') %}
|
||||||
|
<option value="2" {% if comprovante.tipo_comprovante_id == 2 %}selected{% endif %}>2 - Comprovante Especial</option>
|
||||||
|
<option value="3" {% if comprovante.tipo_comprovante_id == 3 %}selected{% endif %}>3 - Comprovante Extraordinário</option>
|
||||||
|
<option value="4" {% if comprovante.tipo_comprovante_id == 4 %}selected{% endif %}>4 - Jornal Avulso</option>
|
||||||
|
<option value="5" {% if comprovante.tipo_comprovante_id == 5 %}selected{% endif %}>5 - Assinatura de Jornal</option>
|
||||||
|
<option value="6" {% if comprovante.tipo_comprovante_id == 6 %}selected{% endif %}>6 - Campanha Financeira</option>
|
||||||
|
{% endif %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="data_comprovante" class="form-label">Data do Comprovante:</label>
|
||||||
|
<input type="date" class="form-control" id="data_comprovante" name="data_comprovante"
|
||||||
|
required max="{{ hoje }}">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Salvar</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
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 %}
|
||||||
@@ -16,7 +16,9 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
<form method="POST" class="needs-validation" novalidate>
|
<form id="formEditarMilitante" method="POST" class="needs-validation" novalidate>
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<input type="hidden" name="militante_id" value="{{ militante.id }}">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
<label for="nome" class="form-label">Nome</label>
|
<label for="nome" class="form-label">Nome</label>
|
||||||
@@ -28,7 +30,7 @@
|
|||||||
|
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
<label for="email" class="form-label">Email</label>
|
<label for="email" class="form-label">Email</label>
|
||||||
<input type="email" class="form-control" id="email" name="email" value="{{ militante.email }}" required>
|
<input type="email" class="form-control" id="email" name="email" value="{{ militante.emails[0].endereco_email if militante.emails else '' }}" required>
|
||||||
<div class="invalid-feedback">
|
<div class="invalid-feedback">
|
||||||
Por favor, insira um email válido.
|
Por favor, insira um email válido.
|
||||||
</div>
|
</div>
|
||||||
@@ -209,21 +211,43 @@
|
|||||||
<script>
|
<script>
|
||||||
// Validação do formulário
|
// Validação do formulário
|
||||||
(function () {
|
(function () {
|
||||||
'use strict'
|
'use strict';
|
||||||
|
|
||||||
var forms = document.querySelectorAll('.needs-validation')
|
var forms = document.querySelectorAll('.needs-validation');
|
||||||
|
|
||||||
Array.prototype.slice.call(forms)
|
Array.prototype.slice.call(forms)
|
||||||
.forEach(function (form) {
|
.forEach(function (form) {
|
||||||
form.addEventListener('submit', function (event) {
|
form.addEventListener('submit', function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
if (!form.checkValidity()) {
|
if (!form.checkValidity()) {
|
||||||
event.preventDefault()
|
event.stopPropagation();
|
||||||
event.stopPropagation()
|
} else {
|
||||||
|
salvarAlteracoesMilitante({{ militante.id }});
|
||||||
}
|
}
|
||||||
|
|
||||||
form.classList.add('was-validated')
|
form.classList.add('was-validated');
|
||||||
}, false)
|
}, false);
|
||||||
})
|
});
|
||||||
})()
|
})();
|
||||||
|
|
||||||
|
// Função para mostrar alertas
|
||||||
|
function mostrarAlerta(mensagem, tipo) {
|
||||||
|
const alertDiv = document.createElement('div');
|
||||||
|
alertDiv.className = `alert alert-${tipo} alert-dismissible fade show`;
|
||||||
|
alertDiv.role = 'alert';
|
||||||
|
alertDiv.innerHTML = `
|
||||||
|
${mensagem}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const container = document.querySelector('.container');
|
||||||
|
container.insertBefore(alertDiv, container.firstChild);
|
||||||
|
|
||||||
|
// Remover o alerta após 5 segundos
|
||||||
|
setTimeout(() => {
|
||||||
|
alertDiv.remove();
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
{% block title %}Editar Relatório de Pagamentos{% endblock %}
|
{% block title %}Editar Relatório de Comprovantes{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<h1 class="mb-4">Editar Relatório de Pagamentos</h1>
|
<h1 class="mb-4">Editar Relatório de Comprovantes</h1>
|
||||||
|
|
||||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
@@ -44,10 +44,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="total_pagamentos" class="form-label">Total de Pagamentos</label>
|
<label for="total_comprovantes" class="form-label">Total de Comprovantes</label>
|
||||||
<input type="number" class="form-control" id="total_pagamentos" name="total_pagamentos" step="0.01" value="{{ relatorio.total_pagamentos }}" required>
|
<input type="number" class="form-control" id="total_comprovantes" name="total_comprovantes" step="0.01" value="{{ relatorio.total_comprovantes }}" required>
|
||||||
<div class="invalid-feedback">
|
<div class="invalid-feedback">
|
||||||
Por favor, insira o total de pagamentos.
|
Por favor, insira o total de comprovantes.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
|
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<button type="submit" class="btn btn-success">Salvar</button>
|
<button type="submit" class="btn btn-success">Salvar</button>
|
||||||
<a href="{{ url_for('listar_relatorios_pagamentos') }}" class="btn btn-outline-secondary">Voltar</a>
|
<a href="{{ url_for('listar_relatorios_comprovantes') }}" class="btn btn-outline-secondary">Voltar</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,34 +1,612 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Home{% endblock %}
|
{% block title %}Início{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container">
|
<div class="row g-4">
|
||||||
<div class="row">
|
<div class="col-12">
|
||||||
<div class="col-md-12">
|
<div class="welcome-header">
|
||||||
<h1 class="mb-4">Bem-vindo, {{ current_user.username }}!</h1>
|
<h2 class="mb-2">Olá, {{ nome_usuario }}!</h2>
|
||||||
|
<h4 class="text-muted">
|
||||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
{{ data_atual }}
|
||||||
{% if messages %}
|
</h4>
|
||||||
{% for category, message in messages %}
|
</div>
|
||||||
<div class="alert alert-{{ category }}">{{ message }}</div>
|
</div>
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
<!-- Cards de Estatísticas -->
|
||||||
{% endwith %}
|
<div class="col-md-6 col-lg-3">
|
||||||
|
<div class="stats-card blue">
|
||||||
<div class="row">
|
<div class="title">Total de Militantes</div>
|
||||||
{% for link in links %}
|
<div class="value">{{ total_militantes }}</div>
|
||||||
<div class="col-md-4">
|
<a href="{{ url_for('listar_militantes') }}" class="link">
|
||||||
<div class="card mb-4">
|
Ver detalhes <i class="fas fa-arrow-right"></i>
|
||||||
<div class="card-body">
|
</a>
|
||||||
<h5 class="card-title">{{ link.text }}</h5>
|
<div class="icon">
|
||||||
<a href="{{ link.url }}" class="btn btn-primary">Acessar</a>
|
<i class="fas fa-users"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
|
||||||
|
<div class="col-md-6 col-lg-3">
|
||||||
|
<div class="stats-card green">
|
||||||
|
<div class="title">Total de Cotas</div>
|
||||||
|
<div class="value">R$ {{ total_cotas }}</div>
|
||||||
|
<a href="{{ url_for('listar_cotas') }}" class="link">
|
||||||
|
Ver detalhes <i class="fas fa-arrow-right"></i>
|
||||||
|
</a>
|
||||||
|
<div class="icon">
|
||||||
|
<i class="fas fa-dollar-sign"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 col-lg-3">
|
||||||
|
<div class="stats-card cyan">
|
||||||
|
<div class="title">Materiais Vendidos</div>
|
||||||
|
<div class="value">{{ total_materiais }}</div>
|
||||||
|
<a href="{{ url_for('listar_materiais') }}" class="link">
|
||||||
|
Ver detalhes <i class="fas fa-arrow-right"></i>
|
||||||
|
</a>
|
||||||
|
<div class="icon">
|
||||||
|
<i class="fas fa-book"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 col-lg-3">
|
||||||
|
<div class="stats-card yellow">
|
||||||
|
<div class="title">Assinaturas Ativas</div>
|
||||||
|
<div class="value">{{ total_assinaturas }}</div>
|
||||||
|
<a href="{{ url_for('listar_vendas_jornal') }}" class="link">
|
||||||
|
Ver detalhes <i class="fas fa-arrow-right"></i>
|
||||||
|
</a>
|
||||||
|
<div class="icon">
|
||||||
|
<i class="fas fa-newspaper"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-4">
|
||||||
|
<!-- Últimos Militantes -->
|
||||||
|
<div class="col-md-6 mb-4">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title">
|
||||||
|
<i class="fas fa-user-plus"></i>Últimos Militantes Cadastrados
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
{% if ultimos_militantes %}
|
||||||
|
<div class="list-group list-group-flush">
|
||||||
|
{% for militante in ultimos_militantes %}
|
||||||
|
<div class="list-group-item" style="cursor: pointer"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#modalEditarMilitante"
|
||||||
|
data-militante-id="{{ militante.id }}"
|
||||||
|
data-militante-nome="{{ militante.nome }}">
|
||||||
|
<div class="militante-info">
|
||||||
|
<h6 class="mb-1">{{ militante.nome }}</h6>
|
||||||
|
<small>{{ militante.emails[0].endereco_email if militante.emails else '' }}</small>
|
||||||
|
</div>
|
||||||
|
<i class="fas fa-chevron-right text-muted"></i>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-muted m-3">Nenhum militante cadastrado recentemente.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Últimos Pagamentos -->
|
||||||
|
<div class="col-md-6 mb-4">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title">
|
||||||
|
<i class="fas fa-money-bill-wave"></i>Últimos Pagamentos
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
{% if ultimos_pagamentos %}
|
||||||
|
<div class="list-group list-group-flush">
|
||||||
|
{% for pagamento in ultimos_pagamentos %}
|
||||||
|
<div class="list-group-item" style="cursor: pointer" onclick="carregarDadosPagamento({{ pagamento.id }})">
|
||||||
|
<div class="militante-info">
|
||||||
|
<h6 class="mb-1">{{ pagamento.militante.nome }}</h6>
|
||||||
|
<small>{{ pagamento.data_pagamento.strftime('%d/%m/%Y') }}</small>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<span class="badge bg-success">R$ {{ "%.2f"|format(pagamento.valor) }}</span>
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-link text-secondary p-0" type="button" data-bs-toggle="dropdown">
|
||||||
|
<i class="fas fa-ellipsis-v"></i>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="#" onclick="event.stopPropagation(); carregarDadosPagamento({{ pagamento.id }})">
|
||||||
|
<i class="fas fa-edit me-2"></i>Editar
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item text-danger" href="#" onclick="event.stopPropagation(); confirmarExclusao({{ pagamento.id }}, 'pagamentos')">
|
||||||
|
<i class="fas fa-trash me-2"></i>Excluir
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-muted m-3">Nenhum pagamento registrado recentemente.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal de Edição de Pagamento -->
|
||||||
|
<div class="modal fade" id="modalEditarPagamento" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">
|
||||||
|
<i class="fas fa-money-bill-wave me-2"></i>Editar Pagamento
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="formEditarPagamento" method="post">
|
||||||
|
<input type="hidden" id="editPagamentoId" name="id">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editValor" class="form-label">Valor</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text">R$</span>
|
||||||
|
<input type="number" step="0.01" class="form-control" id="editValor" name="valor" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editDataPagamento" class="form-label">Data do Pagamento</label>
|
||||||
|
<input type="date" class="form-control" id="editDataPagamento" name="data_pagamento" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editTipoPagamento" class="form-label">Tipo de Pagamento</label>
|
||||||
|
<select class="form-select" id="editTipoPagamento" name="tipo_pagamento" required>
|
||||||
|
{% for tipo in tipos_pagamento %}
|
||||||
|
<option value="{{ tipo.id }}">{{ tipo.nome }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editObservacao" class="form-label">Observação</label>
|
||||||
|
<textarea class="form-control" id="editObservacao" name="observacao" rows="3"></textarea>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||||
|
<i class="fas fa-times me-2"></i>Cancelar
|
||||||
|
</button>
|
||||||
|
<button type="submit" form="formEditarPagamento" class="btn btn-primary">
|
||||||
|
<i class="fas fa-save me-2"></i>Salvar Alterações
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal de Confirmação de Exclusão -->
|
||||||
|
<div class="modal fade" id="deleteModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">
|
||||||
|
<i class="fas fa-exclamation-triangle me-2"></i>Confirmar Exclusão
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>Tem certeza que deseja excluir este item?</p>
|
||||||
|
<p class="text-danger"><small>Esta ação não pode ser desfeita.</small></p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||||
|
<i class="fas fa-times me-2"></i>Cancelar
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-danger" id="btnConfirmarExclusao">
|
||||||
|
<i class="fas fa-trash me-2"></i>Excluir
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Incluir os modais globais de militantes -->
|
||||||
|
{% include 'modals/militante_editar.html' %}
|
||||||
|
{% include 'modals/militante_excluir.html' %}
|
||||||
|
|
||||||
|
<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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilo para o backdrop com blur em todos os modais */
|
||||||
|
.modal-backdrop.show {
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilo para o botão de fechar dos modais */
|
||||||
|
.btn-close {
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 0.5rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
filter: invert(1) grayscale(100%) brightness(200%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-close:hover {
|
||||||
|
opacity: 1;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilo para modais */
|
||||||
|
.modal-content {
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
background: linear-gradient(to right, var(--bs-gray-dark), var(--bs-gray));
|
||||||
|
color: white;
|
||||||
|
border-radius: 12px 12px 0 0;
|
||||||
|
border-bottom: none;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Garantir que o botão de editar fique azul */
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--bs-primary) !important;
|
||||||
|
border-color: var(--bs-primary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover,
|
||||||
|
.btn-primary:focus,
|
||||||
|
.btn-primary:active {
|
||||||
|
background-color: var(--bs-primary-dark) !important;
|
||||||
|
border-color: var(--bs-primary-dark) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilo para os itens da lista de militantes */
|
||||||
|
.list-group-item {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border-left: 3px solid transparent;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item:hover {
|
||||||
|
background-color: rgba(232, 0, 12, 0.05);
|
||||||
|
border-left-color: var(--primary-color);
|
||||||
|
transform: translateX(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item .militante-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item .fa-chevron-right {
|
||||||
|
color: var(--primary-color);
|
||||||
|
opacity: 0.5;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item:hover .fa-chevron-right {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Garantir que o botão de salvar mantenha a cor correta */
|
||||||
|
.btn-success,
|
||||||
|
.modal-footer .btn-success {
|
||||||
|
background-color: var(--bs-success) !important;
|
||||||
|
border-color: var(--bs-success) !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success:hover,
|
||||||
|
.btn-success:focus,
|
||||||
|
.btn-success:active,
|
||||||
|
.modal-footer .btn-success:hover,
|
||||||
|
.modal-footer .btn-success:focus,
|
||||||
|
.modal-footer .btn-success:active {
|
||||||
|
background-color: var(--bs-success-dark) !important;
|
||||||
|
border-color: var(--bs-success-dark) !important;
|
||||||
|
color: white !important;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Garantir que o botão de cancelar mantenha a cor correta */
|
||||||
|
.btn-secondary,
|
||||||
|
.modal-footer .btn-secondary {
|
||||||
|
background-color: var(--bs-secondary) !important;
|
||||||
|
border-color: var(--bs-secondary) !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover,
|
||||||
|
.btn-secondary:focus,
|
||||||
|
.btn-secondary:active,
|
||||||
|
.modal-footer .btn-secondary:hover,
|
||||||
|
.modal-footer .btn-secondary:focus,
|
||||||
|
.modal-footer .btn-secondary:active {
|
||||||
|
background-color: var(--bs-secondary-dark) !important;
|
||||||
|
border-color: var(--bs-secondary-dark) !important;
|
||||||
|
color: white !important;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Modal de Detalhes
|
||||||
|
const militanteModal = document.getElementById('militanteModal');
|
||||||
|
militanteModal.addEventListener('show.bs.modal', function(event) {
|
||||||
|
const button = event.relatedTarget;
|
||||||
|
const militanteId = button.getAttribute('data-militante-id');
|
||||||
|
|
||||||
|
// Preencher os dados do militante
|
||||||
|
document.getElementById('militanteNome').textContent = button.getAttribute('data-militante-nome');
|
||||||
|
document.getElementById('militanteCPF').textContent = button.getAttribute('data-militante-cpf');
|
||||||
|
document.getElementById('militanteEmail').textContent = button.getAttribute('data-militante-email');
|
||||||
|
document.getElementById('militanteTelefone').textContent = button.getAttribute('data-militante-telefone');
|
||||||
|
document.getElementById('militanteEndereco').textContent = button.getAttribute('data-militante-endereco');
|
||||||
|
document.getElementById('militanteFiliado').textContent = button.getAttribute('data-militante-filiado') === 'True' ? 'Filiado' : 'Não Filiado';
|
||||||
|
|
||||||
|
// Configurar dados para o modal de edição
|
||||||
|
const btnEditar = this.querySelector('.btn-primary');
|
||||||
|
btnEditar.addEventListener('click', function() {
|
||||||
|
const modalEditar = document.getElementById('modalEditarMilitante');
|
||||||
|
|
||||||
|
// Preencher o formulário de edição
|
||||||
|
document.getElementById('editNome').value = button.getAttribute('data-militante-nome');
|
||||||
|
document.getElementById('editCpf').value = button.getAttribute('data-militante-cpf');
|
||||||
|
document.getElementById('editEmail').value = button.getAttribute('data-militante-email');
|
||||||
|
document.getElementById('editTelefone').value = button.getAttribute('data-militante-telefone');
|
||||||
|
document.getElementById('editEndereco').value = button.getAttribute('data-militante-endereco');
|
||||||
|
document.getElementById('editFiliado').checked = button.getAttribute('data-militante-filiado') === 'True';
|
||||||
|
|
||||||
|
// Configurar action do formulário
|
||||||
|
document.getElementById('formEditarMilitante').action = `/militantes/editar/${militanteId}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configurar dados para o modal de exclusão
|
||||||
|
const btnExcluir = this.querySelector('.btn-danger');
|
||||||
|
btnExcluir.addEventListener('click', function() {
|
||||||
|
const deleteModal = document.getElementById('deleteModal');
|
||||||
|
const btnConfirmarExclusao = deleteModal.querySelector('#btnConfirmarExclusao');
|
||||||
|
|
||||||
|
// Atualizar texto do modal
|
||||||
|
deleteModal.querySelector('.modal-body p').textContent = `Tem certeza que deseja excluir o militante ${button.getAttribute('data-militante-nome')}?`;
|
||||||
|
|
||||||
|
// Configurar ação de exclusão
|
||||||
|
btnConfirmarExclusao.onclick = function() {
|
||||||
|
fetch(`/militantes/excluir/${militanteId}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'success') {
|
||||||
|
// Fechar os modais
|
||||||
|
bootstrap.Modal.getInstance(deleteModal).hide();
|
||||||
|
bootstrap.Modal.getInstance(militanteModal).hide();
|
||||||
|
|
||||||
|
// Atualizar a página
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert(data.message || 'Erro ao excluir militante');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Erro:', error);
|
||||||
|
alert('Erro ao excluir militante');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Limpar event listeners quando o modal for fechado
|
||||||
|
militanteModal.addEventListener('hidden.bs.modal', function () {
|
||||||
|
const btnEditar = this.querySelector('.btn-primary');
|
||||||
|
const btnExcluir = this.querySelector('.btn-danger');
|
||||||
|
|
||||||
|
btnEditar.replaceWith(btnEditar.cloneNode(true));
|
||||||
|
btnExcluir.replaceWith(btnExcluir.cloneNode(true));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Envio do formulário de edição via AJAX
|
||||||
|
const formEditarMilitante = document.getElementById('formEditarMilitante');
|
||||||
|
formEditarMilitante.addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const formData = new FormData(this);
|
||||||
|
|
||||||
|
fetch(this.action, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
headers: {
|
||||||
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'success') {
|
||||||
|
// Fechar os modais
|
||||||
|
bootstrap.Modal.getInstance(document.getElementById('modalEditarMilitante')).hide();
|
||||||
|
bootstrap.Modal.getInstance(document.getElementById('militanteModal')).hide();
|
||||||
|
|
||||||
|
// Atualizar a página
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
// Mostrar erro
|
||||||
|
const alertDiv = document.createElement('div');
|
||||||
|
alertDiv.className = 'alert alert-danger alert-dismissible fade show';
|
||||||
|
alertDiv.innerHTML = `
|
||||||
|
${data.message}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
`;
|
||||||
|
document.querySelector('.modal-body').insertBefore(alertDiv, formEditarMilitante);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Erro:', error);
|
||||||
|
const alertDiv = document.createElement('div');
|
||||||
|
alertDiv.className = 'alert alert-danger alert-dismissible fade show';
|
||||||
|
alertDiv.innerHTML = `
|
||||||
|
Erro ao atualizar militante. Tente novamente.
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
`;
|
||||||
|
document.querySelector('.modal-body').insertBefore(alertDiv, formEditarMilitante);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Função para carregar dados do pagamento no modal
|
||||||
|
function carregarDadosPagamento(id) {
|
||||||
|
fetch(`/api/pagamentos/${id}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
document.getElementById('editPagamentoId').value = data.id;
|
||||||
|
document.getElementById('editValor').value = data.valor;
|
||||||
|
document.getElementById('editDataPagamento').value = data.data_pagamento;
|
||||||
|
document.getElementById('editTipoPagamento').value = data.tipo_pagamento_id;
|
||||||
|
document.getElementById('editObservacao').value = data.observacao || '';
|
||||||
|
|
||||||
|
// Abre o modal
|
||||||
|
new bootstrap.Modal(document.getElementById('modalEditarPagamento')).show();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Erro ao carregar dados:', error);
|
||||||
|
alert('Erro ao carregar dados do pagamento');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Função para salvar alterações do pagamento
|
||||||
|
document.getElementById('formEditarPagamento').addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const id = document.getElementById('editPagamentoId').value;
|
||||||
|
const formData = new FormData(this);
|
||||||
|
|
||||||
|
fetch(`/api/pagamentos/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
// Fecha o modal
|
||||||
|
bootstrap.Modal.getInstance(document.getElementById('modalEditarPagamento')).hide();
|
||||||
|
// Recarrega a página
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Erro ao salvar alterações: ' + data.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Erro ao salvar:', error);
|
||||||
|
alert('Erro ao salvar alterações');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configuração do modal de exclusão
|
||||||
|
let itemParaExcluir = null;
|
||||||
|
let tipoItem = null;
|
||||||
|
|
||||||
|
function confirmarExclusao(id, tipo) {
|
||||||
|
itemParaExcluir = id;
|
||||||
|
tipoItem = tipo;
|
||||||
|
new bootstrap.Modal(document.getElementById('deleteModal')).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('btnConfirmarExclusao').addEventListener('click', function() {
|
||||||
|
if (!itemParaExcluir || !tipoItem) return;
|
||||||
|
|
||||||
|
fetch(`/api/${tipoItem}/${itemParaExcluir}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Erro ao excluir: ' + data.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Erro ao excluir:', error);
|
||||||
|
alert('Erro ao excluir item');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script src="{{ url_for('static', filename='js/militantes.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/home.js') }}"></script>
|
||||||
|
{% endblock %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
49
templates/lista_comprovantes.html
Normal file
49
templates/lista_comprovantes.html
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Lista de Comprovantes{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header bg-light">
|
||||||
|
<h4 class="card-title mb-0">
|
||||||
|
<i class="fas fa-list me-2"></i>Lista de Comprovantes
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Data</th>
|
||||||
|
<th>Valor</th>
|
||||||
|
<th>Tipo</th>
|
||||||
|
<th>Ações</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for comprovante in comprovantes %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ comprovante.id }}</td>
|
||||||
|
<td>{{ comprovante.data.strftime('%d/%m/%Y') }}</td>
|
||||||
|
<td>R$ {{ "%.2f"|format(comprovante.valor) }}</td>
|
||||||
|
<td>
|
||||||
|
{% if comprovante.tipo_comprovante_id == 1 %}
|
||||||
|
1 - Comprovante Padrão
|
||||||
|
{% elif current_user.has_permission('gerenciar_tipos_comprovante') %}
|
||||||
|
{% if comprovante.tipo_comprovante_id == 2 %}
|
||||||
|
2 - Comprovante Especial
|
||||||
|
<td>{{ comprovante.tipo }}</td>
|
||||||
|
<td>{{ comprovante.data }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -1,35 +1,284 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
{% block title %}Início{% endblock %}
|
{% block title %}Assinaturas{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>Assinaturas Anuais</h1>
|
<div class="container">
|
||||||
<a href="{{ url_for('nova_assinatura') }}">Adicionar Nova Assinatura</a>
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<table border="1">
|
<h2><i class="fas fa-newspaper me-2"></i>Assinaturas</h2>
|
||||||
<thead>
|
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalNovaAssinatura">
|
||||||
<tr>
|
<i class="fas fa-plus me-2"></i>Nova Assinatura
|
||||||
<th>ID</th>
|
</button>
|
||||||
<th>Militante ID</th>
|
</div>
|
||||||
<th>Tipo Material</th>
|
|
||||||
<th>Quantidade</th>
|
<div class="card">
|
||||||
<th>Valor Total</th>
|
<div class="card-body p-0">
|
||||||
<th>Data Início</th>
|
{% if assinaturas %}
|
||||||
<th>Data Fim</th>
|
<div class="table-responsive">
|
||||||
</tr>
|
<table class="table table-hover">
|
||||||
</thead>
|
<thead>
|
||||||
<tbody>
|
<tr>
|
||||||
{% for assinatura in assinaturas %}
|
<th>Militante</th>
|
||||||
<tr>
|
<th>Data Início</th>
|
||||||
<td>{{ assinatura.id }}</td>
|
<th>Data Fim</th>
|
||||||
<td>{{ assinatura.militante_id }}</td>
|
<th>Status</th>
|
||||||
<td>{{ assinatura.tipo_material_id }}</td>
|
<th>Valor</th>
|
||||||
<td>{{ assinatura.quantidade }}</td>
|
<th>Ações</th>
|
||||||
<td>R$ {{ assinatura.valor_total }}</td>
|
</tr>
|
||||||
<td>{{ assinatura.data_inicio }}</td>
|
</thead>
|
||||||
<td>{{ assinatura.data_fim }}</td>
|
<tbody>
|
||||||
</tr>
|
{% for assinatura in assinaturas %}
|
||||||
{% endfor %}
|
<tr>
|
||||||
</tbody>
|
<td>{{ assinatura.militante.nome }}</td>
|
||||||
</table>
|
<td>{{ assinatura.data_inicio.strftime('%d/%m/%Y') }}</td>
|
||||||
<a href="{{ url_for('home') }}">Home</a>
|
<td>{{ assinatura.data_fim.strftime('%d/%m/%Y') }}</td>
|
||||||
{% endblock %}
|
<td>
|
||||||
|
{% if assinatura.ativa %}
|
||||||
|
<span class="badge bg-success">Ativa</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-danger">Inativa</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>R$ {{ "%.2f"|format(assinatura.valor) }}</td>
|
||||||
|
<td>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary"
|
||||||
|
onclick="editarAssinatura({{ assinatura.id }})">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-danger"
|
||||||
|
onclick="confirmarExclusao({{ assinatura.id }})">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-4">
|
||||||
|
<p class="text-muted mb-0">Nenhuma assinatura encontrada.</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Nova Assinatura -->
|
||||||
|
<div class="modal fade" id="modalNovaAssinatura" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Nova Assinatura</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="formNovaAssinatura">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="militante" class="form-label">Militante</label>
|
||||||
|
<select class="form-select" id="militante" name="militante_id" required>
|
||||||
|
<option value="">Selecione um militante</option>
|
||||||
|
{% for militante in militantes %}
|
||||||
|
<option value="{{ militante.id }}">{{ militante.nome }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="dataInicio" class="form-label">Data de Início</label>
|
||||||
|
<input type="date" class="form-control" id="dataInicio" name="data_inicio" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="dataFim" class="form-label">Data de Fim</label>
|
||||||
|
<input type="date" class="form-control" id="dataFim" name="data_fim" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="valor" class="form-label">Valor</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text">R$</span>
|
||||||
|
<input type="number" step="0.01" class="form-control" id="valor" name="valor" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||||
|
<button type="submit" form="formNovaAssinatura" class="btn btn-primary">Salvar</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal de Confirmação de Exclusão -->
|
||||||
|
<div class="modal fade" id="modalConfirmarExclusao" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Confirmar Exclusão</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>Tem certeza que deseja excluir esta assinatura?</p>
|
||||||
|
<p class="text-danger mb-0"><small>Esta ação não pode ser desfeita.</small></p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||||
|
<button type="button" class="btn btn-danger" onclick="excluirAssinatura()">Excluir</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let assinaturaIdParaExcluir = null;
|
||||||
|
|
||||||
|
function editarAssinatura(id) {
|
||||||
|
// Implementar edição
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmarExclusao(id) {
|
||||||
|
assinaturaIdParaExcluir = id;
|
||||||
|
new bootstrap.Modal(document.getElementById('modalConfirmarExclusao')).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function excluirAssinatura() {
|
||||||
|
if (!assinaturaIdParaExcluir) return;
|
||||||
|
|
||||||
|
fetch(`/assinaturas/excluir/${assinaturaIdParaExcluir}`, {
|
||||||
|
method: 'POST'
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Erro ao excluir assinatura: ' + data.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Erro:', error);
|
||||||
|
alert('Erro ao excluir assinatura');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('formNovaAssinatura').addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const formData = new FormData(this);
|
||||||
|
|
||||||
|
fetch('/assinaturas/novo', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Erro ao criar assinatura: ' + data.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Erro:', error);
|
||||||
|
alert('Erro ao criar assinatura');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
/* Estilo para colunas ordenáveis */
|
||||||
|
th[data-sort] {
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
th[data-sort] i {
|
||||||
|
margin-left: 5px;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
th[data-sort].sort-asc i,
|
||||||
|
th[data-sort].sort-desc i {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animação para linhas da tabela */
|
||||||
|
#assinaturasTable tbody tr {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#assinaturasTable tbody tr:hover {
|
||||||
|
background-color: rgba(0,0,0,0.02);
|
||||||
|
transform: translateX(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilo para botões de ação */
|
||||||
|
.btn-group .btn {
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group .btn i {
|
||||||
|
width: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsividade */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.btn-group {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group .btn {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilo para o backdrop com blur em todos os modais */
|
||||||
|
.modal-backdrop.show {
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilo para o botão de fechar dos modais */
|
||||||
|
.btn-close {
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 0.5rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
filter: invert(1) grayscale(100%) brightness(200%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-close:hover {
|
||||||
|
opacity: 1;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilo para modais */
|
||||||
|
.modal-content {
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
background: linear-gradient(to right, var(--bs-gray-dark), var(--bs-gray));
|
||||||
|
color: white;
|
||||||
|
border-radius: 12px 12px 0 0;
|
||||||
|
border-bottom: none;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
309
templates/listar_comprovantes.html
Normal file
309
templates/listar_comprovantes.html
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Comprovantes{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid mt-3">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h2><i class="fas fa-money-bill-wave"></i> Comprovantes</h2>
|
||||||
|
<div>
|
||||||
|
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#novoComprovanteModal">
|
||||||
|
<i class="fas fa-plus"></i> Novo Comprovante
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-primary" id="btnExportar">
|
||||||
|
<i class="fas fa-file-export"></i> Exportar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-hover" id="tabelaComprovantes">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Militante</th>
|
||||||
|
<th>Data</th>
|
||||||
|
<th>Forma de Pagamento</th>
|
||||||
|
<th>Campanha</th>
|
||||||
|
<th>Centralizações</th>
|
||||||
|
<th>Ações</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for comprovante in comprovantes %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ comprovante.id }}</td>
|
||||||
|
<td>{{ comprovante.militante.nome }}</td>
|
||||||
|
<td>{{ comprovante.data_comprovante.strftime('%d/%m/%Y') }}</td>
|
||||||
|
<td>{{ comprovante.forma_pagamento }}</td>
|
||||||
|
<td>{{ comprovante.campanha.nome if comprovante.campanha else '-' }}</td>
|
||||||
|
<td>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
{% for centralizacao in comprovante.centralizacoes %}
|
||||||
|
<li>{{ centralizacao.tipo_comprovante }}: R$ {{ "%.2f"|format(centralizacao.valor) }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-sm btn-outline-primary"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#modalEditarComprovante"
|
||||||
|
data-comprovante-id="{{ comprovante.id }}"
|
||||||
|
data-militante-id="{{ comprovante.militante_id }}"
|
||||||
|
data-militante-nome="{{ comprovante.militante.nome }}"
|
||||||
|
data-data-comprovante="{{ comprovante.data_comprovante.strftime('%Y-%m-%d') }}"
|
||||||
|
title="Editar">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-sm btn-outline-danger"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#modalExcluirComprovante"
|
||||||
|
data-comprovante-id="{{ comprovante.id }}"
|
||||||
|
data-comprovante-info="Comprovante de {{ comprovante.militante.nome }} - Total: R$ {{ "%.2f"|format(comprovante.centralizacoes|sum(attribute='valor')) }}"
|
||||||
|
title="Excluir">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Novo Comprovante -->
|
||||||
|
<div class="modal fade" id="novoComprovanteModal" tabindex="-1" aria-labelledby="novoComprovanteModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="novoComprovanteModalLabel">Novo Comprovante</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="novoComprovanteForm">
|
||||||
|
<!-- Dados únicos do comprovante -->
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="militante_id" class="form-label">Militante</label>
|
||||||
|
<select class="form-select" id="militante_id" name="militante_id" required>
|
||||||
|
<option value="">Selecione o militante</option>
|
||||||
|
{% for militante in militantes %}
|
||||||
|
<option value="{{ militante.id }}">{{ militante.nome }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="data_comprovante" class="form-label">Data do Comprovante</label>
|
||||||
|
<input type="date" class="form-control" id="data_comprovante" name="data_comprovante" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="forma_pagamento" class="form-label">Forma de Pagamento</label>
|
||||||
|
<select class="form-select" id="forma_pagamento" name="forma_pagamento" required>
|
||||||
|
<option value="">Selecione a forma de pagamento</option>
|
||||||
|
<option value="PIX">PIX</option>
|
||||||
|
<option value="TRANSFERENCIA">Transferência/DOC</option>
|
||||||
|
<option value="DEPOSITO">Depósito</option>
|
||||||
|
<option value="MAQUININHA">Maquininha</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="campanha_id" class="form-label">Campanha</label>
|
||||||
|
<select class="form-select" id="campanha_id" name="campanha_id">
|
||||||
|
<option value="">Selecione a campanha</option>
|
||||||
|
{% for campanha in campanhas %}
|
||||||
|
<option value="{{ campanha.id }}">{{ campanha.nome }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Centralizações -->
|
||||||
|
<div class="centralizacoes-container">
|
||||||
|
<h6 class="mb-3">Centralizações</h6>
|
||||||
|
<div class="centralizacao-item mb-3">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Tipo de Comprovante</label>
|
||||||
|
<select class="form-select tipo-comprovante" name="tipo_comprovante[]" required>
|
||||||
|
<option value="">Selecione o tipo</option>
|
||||||
|
<option value="COTA">Cota</option>
|
||||||
|
<option value="JORNAL">Jornal</option>
|
||||||
|
<option value="ASSINATURA">Assinatura</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-5">
|
||||||
|
<label class="form-label">Valor</label>
|
||||||
|
<input type="number" class="form-control valor" name="valor[]" step="0.01" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-1 d-flex align-items-end">
|
||||||
|
<button type="button" class="btn btn-danger btn-sm remover-centralizacao">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-secondary btn-sm mb-3" id="adicionar-centralizacao">
|
||||||
|
<i class="bi bi-plus"></i> Adicionar Centralização
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||||
|
<button type="button" class="btn btn-primary" id="salvarComprovante">Salvar</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.centralizacao-item {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Adicionar nova centralização
|
||||||
|
document.getElementById('adicionar-centralizacao').addEventListener('click', function() {
|
||||||
|
const container = document.querySelector('.centralizacoes-container');
|
||||||
|
const newItem = document.querySelector('.centralizacao-item').cloneNode(true);
|
||||||
|
newItem.querySelector('.valor').value = '';
|
||||||
|
newItem.querySelector('.tipo-comprovante').value = '';
|
||||||
|
container.appendChild(newItem);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remover centralização
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
if (e.target.closest('.remover-centralizacao')) {
|
||||||
|
const centralizacoes = document.querySelectorAll('.centralizacao-item');
|
||||||
|
if (centralizacoes.length > 1) {
|
||||||
|
e.target.closest('.centralizacao-item').remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Salvar comprovante
|
||||||
|
document.getElementById('salvarComprovante').addEventListener('click', function() {
|
||||||
|
const form = document.getElementById('novoComprovanteForm');
|
||||||
|
const formData = new FormData(form);
|
||||||
|
|
||||||
|
// Coletar dados das centralizações
|
||||||
|
const centralizacoes = [];
|
||||||
|
document.querySelectorAll('.centralizacao-item').forEach(item => {
|
||||||
|
centralizacoes.push({
|
||||||
|
tipo_comprovante: item.querySelector('.tipo-comprovante').value,
|
||||||
|
valor: item.querySelector('.valor').value
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Adicionar centralizações ao formData
|
||||||
|
formData.append('centralizacoes', JSON.stringify(centralizacoes));
|
||||||
|
|
||||||
|
fetch('/comprovantes/novo', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert(data.message || 'Erro ao salvar comprovante');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('Erro ao salvar comprovante');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Modal Editar Comprovante -->
|
||||||
|
<div class="modal fade" id="modalEditarComprovante" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title"><i class="fas fa-edit"></i> Editar Comprovante</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="formEditarComprovante" method="post">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editMilitante" class="form-label">Militante:</label>
|
||||||
|
<input type="text" class="form-control bg-light" id="editMilitanteNome" readonly>
|
||||||
|
<input type="hidden" id="editMilitante" name="militante_id">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editTipoComprovante" class="form-label">Tipo de Comprovante:</label>
|
||||||
|
<select class="form-select" id="editTipoComprovante" name="tipo_comprovante" required>
|
||||||
|
<option value="">Selecione o tipo</option>
|
||||||
|
<option value="1">Cota</option>
|
||||||
|
{% if current_user.has_permission('gerenciar_tipos_comprovante') %}
|
||||||
|
<option value="2">Contribuição Extra</option>
|
||||||
|
<option value="3">Doação</option>
|
||||||
|
<option value="4">Taxa de Evento</option>
|
||||||
|
<option value="5">Jornal Avulso</option>
|
||||||
|
<option value="6">Assinatura de Jornal</option>
|
||||||
|
<option value="7">Campanha Financeira</option>
|
||||||
|
<option value="8">Outros</option>
|
||||||
|
{% endif %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editValor" class="form-label">Valor:</label>
|
||||||
|
<input type="number" step="0.01" class="form-control" id="editValor" name="valor" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editDataComprovante" class="form-label">Data do Comprovante:</label>
|
||||||
|
<input type="date" class="form-control" id="editDataComprovante" name="data_comprovante" required>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||||
|
<button type="submit" class="btn btn-primary">Salvar</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Excluir Comprovante -->
|
||||||
|
<div class="modal fade" id="modalExcluirComprovante" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title"><i class="fas fa-trash"></i> Excluir Comprovante</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>Tem certeza que deseja excluir este comprovante?</p>
|
||||||
|
<p id="comprovanteInfo" class="text-muted"></p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<form id="formExcluirComprovante" method="post">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||||
|
<button type="submit" class="btn btn-danger">Excluir</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script src="{{ url_for('static', filename='js/comprovantes.js') }}"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
@@ -1,31 +1,323 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Listar Militantes{% endblock %}
|
{% block title %}Cotas{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>Cotas Mensais</h1>
|
<div class="row mb-4">
|
||||||
<a href="{{ url_for('nova_cota') }}">Adicionar Nova Cota</a>
|
<div class="col-12">
|
||||||
<table border="1">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<thead>
|
<h1 class="mb-0">
|
||||||
<tr>
|
<i class="fas fa-money-bill me-2"></i>Cotas
|
||||||
<th>ID</th>
|
</h1>
|
||||||
<th>Militante ID</th>
|
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalNovaCota">
|
||||||
<th>Valor Antigo</th>
|
<i class="fas fa-plus me-2"></i>Nova Cota
|
||||||
<th>Valor Novo</th>
|
</button>
|
||||||
<th>Data de Alteração</th>
|
</div>
|
||||||
</tr>
|
</div>
|
||||||
</thead>
|
</div>
|
||||||
<tbody>
|
|
||||||
{% for cota in cotas %}
|
<div class="card shadow-sm">
|
||||||
<tr>
|
<div class="card-body">
|
||||||
<td>{{ cota.id }}</td>
|
<div class="row mb-4">
|
||||||
<td>{{ cota.militante_id }}</td>
|
<div class="col-md-6">
|
||||||
<td>R$ {{ cota.valor_antigo }}</td>
|
<div class="input-group">
|
||||||
<td>R$ {{ cota.valor_novo }}</td>
|
<span class="input-group-text">
|
||||||
<td>{{ cota.data_alteracao }}</td>
|
<i class="fas fa-search"></i>
|
||||||
</tr>
|
</span>
|
||||||
{% endfor %}
|
<input type="text" class="form-control" id="searchInput" placeholder="Pesquisar cotas...">
|
||||||
</tbody>
|
</div>
|
||||||
</table>
|
</div>
|
||||||
<a href="{{ url_for('home') }}">Home</a>
|
<div class="col-md-6 text-end">
|
||||||
|
<button id="btnExportar" class="btn btn-outline-primary">
|
||||||
|
<i class="fas fa-download me-2"></i>Exportar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover" id="cotasTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th data-sort="militante">Militante <i class="fas fa-sort"></i></th>
|
||||||
|
<th data-sort="valor_antigo">Valor Antigo <i class="fas fa-sort"></i></th>
|
||||||
|
<th data-sort="valor_novo">Valor Novo <i class="fas fa-sort"></i></th>
|
||||||
|
<th data-sort="data_alteracao">Data de Alteração <i class="fas fa-sort"></i></th>
|
||||||
|
<th data-sort="data_vencimento">Data de Vencimento <i class="fas fa-sort"></i></th>
|
||||||
|
<th data-sort="status">Status <i class="fas fa-sort"></i></th>
|
||||||
|
<th class="text-end">Ações</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for cota in cotas %}
|
||||||
|
<tr>
|
||||||
|
<td data-militante="{{ cota.militante.nome }}">{{ cota.militante.nome }}</td>
|
||||||
|
<td data-valor_antigo="{{ cota.valor_antigo }}">R$ {{ "%.2f"|format(cota.valor_antigo) }}</td>
|
||||||
|
<td data-valor_novo="{{ cota.valor_novo }}">R$ {{ "%.2f"|format(cota.valor_novo) }}</td>
|
||||||
|
<td data-data_alteracao="{{ cota.data_alteracao }}">{{ cota.data_alteracao.strftime('%d/%m/%Y') }}</td>
|
||||||
|
<td data-data_vencimento="{{ cota.data_vencimento }}">{{ cota.data_vencimento.strftime('%d/%m/%Y') }}</td>
|
||||||
|
<td data-status="{{ cota.status }}">
|
||||||
|
{% if cota.status == 'paga' %}
|
||||||
|
<span class="badge bg-success">Paga</span>
|
||||||
|
{% elif cota.status == 'atrasada' %}
|
||||||
|
<span class="badge bg-danger">Atrasada</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-warning text-dark">Pendente</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-sm btn-outline-primary"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#modalEditarCota"
|
||||||
|
data-cota-id="{{ cota.id }}"
|
||||||
|
data-cota-militante="{{ cota.militante_id }}"
|
||||||
|
data-cota-valor-antigo="{{ cota.valor_antigo }}"
|
||||||
|
data-cota-valor-novo="{{ cota.valor_novo }}"
|
||||||
|
data-cota-data-alteracao="{{ cota.data_alteracao.strftime('%Y-%m-%d') }}"
|
||||||
|
data-cota-data-vencimento="{{ cota.data_vencimento.strftime('%Y-%m-%d') }}"
|
||||||
|
data-cota-pago="{{ 'true' if cota.pago else 'false' }}"
|
||||||
|
title="Editar">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-sm btn-outline-danger"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#deleteModal"
|
||||||
|
data-cota-id="{{ cota.id }}"
|
||||||
|
data-cota-info="{{ cota.militante.nome }} - R$ {{ "%.2f"|format(cota.valor_novo) }}"
|
||||||
|
title="Excluir">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Nova Cota -->
|
||||||
|
<div class="modal fade" id="modalNovaCota" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">
|
||||||
|
<i class="fas fa-plus me-2"></i>Nova Cota
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="formNovaCota" method="post" action="{{ url_for('nova_cota') }}">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="militante_id" class="form-label">Militante:</label>
|
||||||
|
<select class="form-select" id="militante_id" name="militante_id" required>
|
||||||
|
<option value="">Selecione um militante</option>
|
||||||
|
{% for militante in militantes %}
|
||||||
|
<option value="{{ militante.id }}">{{ militante.nome }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="valor_antigo" class="form-label">Valor Antigo:</label>
|
||||||
|
<input type="number" step="0.01" class="form-control" id="valor_antigo" name="valor_antigo" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="valor_novo" class="form-label">Valor Novo:</label>
|
||||||
|
<input type="number" step="0.01" class="form-control" id="valor_novo" name="valor_novo" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="data_alteracao" class="form-label">Data de Alteração:</label>
|
||||||
|
<input type="date" class="form-control" id="data_alteracao" name="data_alteracao" required>
|
||||||
|
</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" required>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||||
|
<button type="submit" form="formNovaCota" class="btn btn-success">
|
||||||
|
<i class="fas fa-save me-2"></i>Salvar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal de Edição -->
|
||||||
|
<div class="modal fade" id="modalEditarCota" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">
|
||||||
|
<i class="fas fa-edit me-2"></i>Editar Cota
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="formEditarCota" method="post">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editMilitanteNome" class="form-label">Militante:</label>
|
||||||
|
<input type="text" class="form-control bg-light" id="editMilitanteNome" readonly>
|
||||||
|
<input type="hidden" id="editMilitante" name="militante_id">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editValorAntigo" class="form-label">Valor Antigo:</label>
|
||||||
|
<input type="number" step="0.01" class="form-control" id="editValorAntigo" name="valor_antigo" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editValorNovo" class="form-label">Valor Novo:</label>
|
||||||
|
<input type="number" step="0.01" class="form-control" id="editValorNovo" name="valor_novo" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editDataAlteracao" class="form-label">Data de Alteração:</label>
|
||||||
|
<input type="date" class="form-control" id="editDataAlteracao" name="data_alteracao" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editDataVencimento" class="form-label">Data de Vencimento:</label>
|
||||||
|
<input type="date" class="form-control" id="editDataVencimento" name="data_vencimento" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3 form-check">
|
||||||
|
<input type="checkbox" class="form-check-input" id="editPago" name="pago">
|
||||||
|
<label class="form-check-label" for="editPago">Pago</label>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||||
|
<button type="submit" form="formEditarCota" class="btn btn-success">
|
||||||
|
<i class="fas fa-save me-2"></i>Salvar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal de Exclusão -->
|
||||||
|
<div class="modal fade" id="deleteModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Confirmar Exclusão</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>Tem certeza que deseja excluir a cota de <strong id="cotaInfo"></strong>?</p>
|
||||||
|
<p class="text-danger mb-0">Esta ação não pode ser desfeita.</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||||
|
<form action="" method="POST" id="deleteForm" class="d-inline">
|
||||||
|
<button type="submit" class="btn btn-danger">
|
||||||
|
<i class="fas fa-trash me-2"></i>Excluir
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script src="{{ url_for('static', filename='js/cotas.js') }}"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
/* Estilo para colunas ordenáveis */
|
||||||
|
th[data-sort] {
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
th[data-sort] i {
|
||||||
|
margin-left: 5px;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
th[data-sort].sort-asc i,
|
||||||
|
th[data-sort].sort-desc i {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animação para linhas da tabela */
|
||||||
|
#cotasTable tbody tr {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#cotasTable tbody tr:hover {
|
||||||
|
background-color: rgba(0,0,0,0.02);
|
||||||
|
transform: translateX(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilo para botões de ação */
|
||||||
|
.btn-group .btn {
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group .btn i {
|
||||||
|
width: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsividade */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.btn-group {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group .btn {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilo para o backdrop com blur em todos os modais */
|
||||||
|
.modal-backdrop.show {
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilo para o botão de fechar dos modais */
|
||||||
|
.btn-close {
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 0.5rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
filter: invert(1) grayscale(100%) brightness(200%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-close:hover {
|
||||||
|
opacity: 1;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilo para modais */
|
||||||
|
.modal-content {
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
background: linear-gradient(to right, var(--bs-gray-dark), var(--bs-gray));
|
||||||
|
color: white;
|
||||||
|
border-radius: 12px 12px 0 0;
|
||||||
|
border-bottom: none;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,58 +1,330 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Listar Materiais{% endblock %}
|
{% block title %}Materiais{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container">
|
<div class="row mb-4">
|
||||||
<div class="row">
|
<div class="col-12">
|
||||||
<div class="col-md-12">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<h1 class="mb-4">Lista de Materiais</h1>
|
<h1 class="mb-0">
|
||||||
|
<i class="fas fa-box me-2"></i>Materiais
|
||||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
</h1>
|
||||||
{% if messages %}
|
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalNovoMaterial">
|
||||||
{% for category, message in messages %}
|
<i class="fas fa-plus me-2"></i>Novo Material
|
||||||
<div class="alert alert-{{ category }}">{{ message }}</div>
|
</button>
|
||||||
{% endfor %}
|
</div>
|
||||||
{% endif %}
|
</div>
|
||||||
{% endwith %}
|
</div>
|
||||||
|
|
||||||
<div class="d-flex justify-content-between mb-4">
|
<div class="card shadow-sm">
|
||||||
<a href="{{ url_for('novo_material') }}" class="btn btn-success">Novo Material</a>
|
<div class="card-body">
|
||||||
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text">
|
||||||
|
<i class="fas fa-search"></i>
|
||||||
|
</span>
|
||||||
|
<input type="text" class="form-control" id="searchInput" placeholder="Pesquisar materiais...">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-6 text-end">
|
||||||
<div class="table-responsive">
|
<button id="btnExportar" class="btn btn-outline-primary">
|
||||||
<table class="table table-striped table-hover">
|
<i class="fas fa-download me-2"></i>Exportar
|
||||||
<thead>
|
</button>
|
||||||
<tr>
|
</div>
|
||||||
<th>ID</th>
|
</div>
|
||||||
<th>Nome</th>
|
|
||||||
<th>Descrição</th>
|
<div class="table-responsive">
|
||||||
<th>Preço</th>
|
<table class="table table-hover" id="materiaisTable">
|
||||||
<th>Quantidade</th>
|
<thead>
|
||||||
<th>Tipo</th>
|
<tr>
|
||||||
<th>Ações</th>
|
<th data-sort="militante">Militante <i class="fas fa-sort"></i></th>
|
||||||
</tr>
|
<th data-sort="tipo">Tipo <i class="fas fa-sort"></i></th>
|
||||||
</thead>
|
<th data-sort="descricao">Descrição <i class="fas fa-sort"></i></th>
|
||||||
<tbody>
|
<th data-sort="valor">Valor <i class="fas fa-sort"></i></th>
|
||||||
{% for material in materiais %}
|
<th data-sort="data">Data <i class="fas fa-sort"></i></th>
|
||||||
<tr>
|
<th class="text-end">Ações</th>
|
||||||
<td>{{ material.id }}</td>
|
</tr>
|
||||||
<td>{{ material.nome }}</td>
|
</thead>
|
||||||
<td>{{ material.descricao }}</td>
|
<tbody>
|
||||||
<td>R$ {{ "%.2f"|format(material.preco) }}</td>
|
{% for material in materiais %}
|
||||||
<td>{{ material.quantidade }}</td>
|
<tr>
|
||||||
<td>{{ material.tipo.nome }}</td>
|
<td data-militante="{{ material.militante.nome }}">{{ material.militante.nome }}</td>
|
||||||
<td>
|
<td data-tipo="{{ material.tipo_material.nome }}">{{ material.tipo_material.nome }}</td>
|
||||||
<a href="{{ url_for('editar_material', id=material.id) }}" class="btn btn-primary btn-sm">Editar</a>
|
<td data-descricao="{{ material.descricao }}">{{ material.descricao }}</td>
|
||||||
<a href="{{ url_for('deletar_material', id=material.id) }}" class="btn btn-danger btn-sm" onclick="return confirm('Tem certeza que deseja excluir este material?')">Excluir</a>
|
<td data-valor="{{ material.valor }}">R$ {{ "%.2f"|format(material.valor) }}</td>
|
||||||
</td>
|
<td data-data="{{ material.data_venda }}">{{ material.data_venda.strftime('%d/%m/%Y') }}</td>
|
||||||
</tr>
|
<td class="text-end">
|
||||||
{% endfor %}
|
<div class="btn-group">
|
||||||
</tbody>
|
<button type="button"
|
||||||
</table>
|
class="btn btn-sm btn-outline-primary"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#modalEditarMaterial"
|
||||||
|
data-material-id="{{ material.id }}"
|
||||||
|
data-material-militante="{{ material.militante_id }}"
|
||||||
|
data-material-tipo="{{ material.tipo_material_id }}"
|
||||||
|
data-material-descricao="{{ material.descricao }}"
|
||||||
|
data-material-valor="{{ material.valor }}"
|
||||||
|
data-material-data="{{ material.data_venda.strftime('%Y-%m-%d') }}"
|
||||||
|
title="Editar">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-sm btn-outline-danger"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#deleteModal"
|
||||||
|
data-material-id="{{ material.id }}"
|
||||||
|
data-material-info="{{ material.militante.nome }} - {{ material.tipo_material.nome }}"
|
||||||
|
title="Excluir">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Novo Material -->
|
||||||
|
<div class="modal fade" id="modalNovoMaterial" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">
|
||||||
|
<i class="fas fa-plus me-2"></i>Novo Material
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="formNovoMaterial" method="post" action="{{ url_for('novo_material') }}">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="militante_id" class="form-label">Militante:</label>
|
||||||
|
<select class="form-select" id="militante_id" name="militante_id" required>
|
||||||
|
<option value="">Selecione um militante</option>
|
||||||
|
{% for militante in militantes %}
|
||||||
|
<option value="{{ militante.id }}">{{ militante.nome }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="tipo_material_id" class="form-label">Tipo de Material:</label>
|
||||||
|
<select class="form-select" id="tipo_material_id" name="tipo_material_id" required>
|
||||||
|
<option value="">Selecione um tipo</option>
|
||||||
|
{% for tipo in tipos_material %}
|
||||||
|
<option value="{{ tipo.id }}">{{ tipo.nome }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="descricao" class="form-label">Descrição:</label>
|
||||||
|
<input type="text" class="form-control" id="descricao" name="descricao" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="valor" class="form-label">Valor:</label>
|
||||||
|
<input type="number" step="0.01" class="form-control" id="valor" name="valor" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="data_venda" class="form-label">Data da Venda:</label>
|
||||||
|
<input type="date" class="form-control" id="data_venda" name="data_venda" required>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||||
|
<button type="submit" form="formNovoMaterial" class="btn btn-success">
|
||||||
|
<i class="fas fa-save me-2"></i>Salvar
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal de Edição -->
|
||||||
|
<div class="modal fade" id="modalEditarMaterial" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">
|
||||||
|
<i class="fas fa-edit me-2"></i>Editar Material
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="formEditarMaterial" method="post">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editMilitante" class="form-label">Militante:</label>
|
||||||
|
<select class="form-select" id="editMilitante" name="militante_id" required>
|
||||||
|
<option value="">Selecione um militante</option>
|
||||||
|
{% for militante in militantes %}
|
||||||
|
<option value="{{ militante.id }}">{{ militante.nome }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editTipo" class="form-label">Tipo de Material:</label>
|
||||||
|
<select class="form-select" id="editTipo" name="tipo_material_id" required>
|
||||||
|
<option value="">Selecione um tipo</option>
|
||||||
|
{% for tipo in tipos_material %}
|
||||||
|
<option value="{{ tipo.id }}">{{ tipo.nome }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editDescricao" class="form-label">Descrição:</label>
|
||||||
|
<input type="text" class="form-control" id="editDescricao" name="descricao" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editValor" class="form-label">Valor:</label>
|
||||||
|
<input type="number" step="0.01" class="form-control" id="editValor" name="valor" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editData" class="form-label">Data da Venda:</label>
|
||||||
|
<input type="date" class="form-control" id="editData" name="data_venda" required>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||||
|
<button type="submit" form="formEditarMaterial" class="btn btn-success">
|
||||||
|
<i class="fas fa-save me-2"></i>Salvar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal de Confirmação de Exclusão -->
|
||||||
|
<div class="modal fade" id="deleteModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">
|
||||||
|
<i class="fas fa-exclamation-triangle me-2 text-danger"></i>Confirmar Exclusão
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>Tem certeza que deseja excluir o material <strong id="materialInfo"></strong>?</p>
|
||||||
|
<p class="text-danger mb-0">Esta ação não pode ser desfeita.</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||||
|
<form id="formDeleteMaterial" method="post" style="display: inline;">
|
||||||
|
<button type="submit" class="btn btn-danger">
|
||||||
|
<i class="fas fa-trash me-2"></i>Excluir
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Configuração da tabela
|
||||||
|
const table = document.getElementById('materiaisTable');
|
||||||
|
const searchInput = document.getElementById('searchInput');
|
||||||
|
const exportBtn = document.getElementById('btnExportar');
|
||||||
|
|
||||||
|
// Função de pesquisa
|
||||||
|
searchInput.addEventListener('input', function() {
|
||||||
|
const searchTerm = this.value.toLowerCase();
|
||||||
|
const rows = table.getElementsByTagName('tbody')[0].getElementsByTagName('tr');
|
||||||
|
|
||||||
|
Array.from(rows).forEach(row => {
|
||||||
|
const text = row.textContent.toLowerCase();
|
||||||
|
row.style.display = text.includes(searchTerm) ? '' : 'none';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Função de ordenação
|
||||||
|
const headers = table.getElementsByTagName('th');
|
||||||
|
Array.from(headers).forEach(header => {
|
||||||
|
if (header.dataset.sort) {
|
||||||
|
header.addEventListener('click', () => {
|
||||||
|
const column = header.dataset.sort;
|
||||||
|
const tbody = table.getElementsByTagName('tbody')[0];
|
||||||
|
const rows = Array.from(tbody.getElementsByTagName('tr'));
|
||||||
|
|
||||||
|
rows.sort((a, b) => {
|
||||||
|
const aValue = a.querySelector(`td[data-${column}]`).dataset[column];
|
||||||
|
const bValue = b.querySelector(`td[data-${column}]`).dataset[column];
|
||||||
|
|
||||||
|
if (column === 'valor') {
|
||||||
|
return parseFloat(aValue) - parseFloat(bValue);
|
||||||
|
} else if (column === 'data') {
|
||||||
|
return new Date(aValue) - new Date(bValue);
|
||||||
|
}
|
||||||
|
return aValue.localeCompare(bValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (header.classList.contains('asc')) {
|
||||||
|
rows.reverse();
|
||||||
|
header.classList.remove('asc');
|
||||||
|
header.classList.add('desc');
|
||||||
|
} else {
|
||||||
|
header.classList.remove('desc');
|
||||||
|
header.classList.add('asc');
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
rows.forEach(row => tbody.appendChild(row));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configuração do modal de edição
|
||||||
|
const editModal = document.getElementById('modalEditarMaterial');
|
||||||
|
editModal.addEventListener('show.bs.modal', function(event) {
|
||||||
|
const button = event.relatedTarget;
|
||||||
|
const materialId = button.dataset.materialId;
|
||||||
|
const form = this.querySelector('form');
|
||||||
|
|
||||||
|
form.action = `/editar_material/${materialId}`;
|
||||||
|
|
||||||
|
document.getElementById('editMilitante').value = button.dataset.materialMilitante;
|
||||||
|
document.getElementById('editTipo').value = button.dataset.materialTipo;
|
||||||
|
document.getElementById('editDescricao').value = button.dataset.materialDescricao;
|
||||||
|
document.getElementById('editValor').value = button.dataset.materialValor;
|
||||||
|
document.getElementById('editData').value = button.dataset.materialData;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configuração do modal de exclusão
|
||||||
|
const deleteModal = document.getElementById('deleteModal');
|
||||||
|
deleteModal.addEventListener('show.bs.modal', function(event) {
|
||||||
|
const button = event.relatedTarget;
|
||||||
|
const materialId = button.dataset.materialId;
|
||||||
|
const materialInfo = button.dataset.materialInfo;
|
||||||
|
|
||||||
|
document.getElementById('materialInfo').textContent = materialInfo;
|
||||||
|
document.getElementById('formDeleteMaterial').action = `/deletar_material/${materialId}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configuração do botão de exportação
|
||||||
|
exportBtn.addEventListener('click', function() {
|
||||||
|
const rows = Array.from(table.getElementsByTagName('tbody')[0].getElementsByTagName('tr'));
|
||||||
|
const csv = [
|
||||||
|
['Militante', 'Tipo', 'Descrição', 'Valor', 'Data'],
|
||||||
|
...rows.map(row => [
|
||||||
|
row.cells[0].textContent,
|
||||||
|
row.cells[1].textContent,
|
||||||
|
row.cells[2].textContent,
|
||||||
|
row.cells[3].textContent,
|
||||||
|
row.cells[4].textContent
|
||||||
|
])
|
||||||
|
].map(row => row.join(',')).join('\n');
|
||||||
|
|
||||||
|
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = URL.createObjectURL(blob);
|
||||||
|
link.download = 'materiais.csv';
|
||||||
|
link.click();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,71 +1,511 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Lista de Militantes{% endblock %}
|
{% block head %}
|
||||||
|
<!-- Bootstrap Datepicker CSS -->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datepicker/1.9.0/css/bootstrap-datepicker.min.css">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block title %}Militantes{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row mb-4">
|
||||||
<div class="col-md-12">
|
<div class="col-12">
|
||||||
<h1 class="mb-4">Lista de Militantes</h1>
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<h1 class="h3 mb-0">
|
||||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
<i class="fas fa-users me-2"></i>Militantes
|
||||||
{% if messages %}
|
</h1>
|
||||||
{% for category, message in messages %}
|
<div>
|
||||||
<div class="alert alert-{{ category }}">{{ message }}</div>
|
<button type="button" class="btn btn-outline-primary me-2" id="btnExportar">
|
||||||
{% endfor %}
|
<i class="fas fa-file-export me-2"></i>Exportar
|
||||||
{% endif %}
|
</button>
|
||||||
{% endwith %}
|
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalNovoMilitante">
|
||||||
|
<i class="fas fa-user-plus me-2"></i>Novo Militante
|
||||||
<div class="d-flex justify-content-between mb-4">
|
</button>
|
||||||
<a href="{{ url_for('criar_militante') }}" class="btn btn-primary">Novo Militante</a>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="table-responsive">
|
</div>
|
||||||
<table class="table table-striped">
|
|
||||||
<thead>
|
<div class="row">
|
||||||
<tr>
|
<div class="col-12">
|
||||||
<th>Nome</th>
|
<div class="card">
|
||||||
<th>Email</th>
|
<div class="card-body">
|
||||||
<th>Célula</th>
|
<div class="row mb-4">
|
||||||
<th>Responsabilidades</th>
|
<div class="col-md-6">
|
||||||
<th>Ações</th>
|
<div class="input-group">
|
||||||
</tr>
|
<span class="input-group-text">
|
||||||
</thead>
|
<i class="fas fa-search"></i>
|
||||||
<tbody>
|
</span>
|
||||||
{% for militante in militantes %}
|
<input type="text" class="form-control" id="searchInput" placeholder="Pesquisar militantes...">
|
||||||
<tr>
|
</div>
|
||||||
<td>{{ militante.nome }}</td>
|
</div>
|
||||||
<td>{{ militante.email }}</td>
|
<div class="col-md-6 text-end">
|
||||||
<td>{{ militante.celula.nome }}</td>
|
<div class="btn-group me-2">
|
||||||
<td>
|
<button type="button" class="btn btn-outline-secondary btn-fixed-width dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
{% if militante.responsabilidades & Militante.RESPONSAVEL_FINANCAS %}
|
<i class="fas fa-filter me-2"></i>Filtrar
|
||||||
<span class="badge bg-primary">Finanças</span>
|
</button>
|
||||||
{% endif %}
|
<ul class="dropdown-menu">
|
||||||
{% if militante.responsabilidades & Militante.RESPONSAVEL_IMPRENSA %}
|
<li><h6 class="dropdown-header">Status</h6></li>
|
||||||
<span class="badge bg-info">Imprensa</span>
|
<li><a class="dropdown-item" href="#" data-filter="todos">Todos</a></li>
|
||||||
{% endif %}
|
<li><hr class="dropdown-divider"></li>
|
||||||
{% if militante.responsabilidades & Militante.QUADRO_ORIENTADOR %}
|
<li><h6 class="dropdown-header">Responsabilidades</h6></li>
|
||||||
<span class="badge bg-success">Quadro-Orientador</span>
|
<li><a class="dropdown-item" href="#" data-filter="responsavel-financas">Responsável de Finanças</a></li>
|
||||||
{% endif %}
|
<li><a class="dropdown-item" href="#" data-filter="responsavel-imprensa">Responsável de Imprensa</a></li>
|
||||||
</td>
|
<li><a class="dropdown-item" href="#" data-filter="quadro-orientador">Quadro-Orientador</a></li>
|
||||||
<td>
|
<li><a class="dropdown-item" href="#" data-filter="secretario">Secretário</a></li>
|
||||||
<a href="{{ url_for('editar_militante', id=militante.id) }}" class="btn btn-sm btn-warning">Editar</a>
|
<li><a class="dropdown-item" href="#" data-filter="tesoureiro">Tesoureiro</a></li>
|
||||||
<button type="button" class="btn btn-sm btn-danger" onclick="confirmarExclusao({{ militante.id }})">Excluir</button>
|
<li><a class="dropdown-item" href="#" data-filter="imprensa">Imprensa</a></li>
|
||||||
</td>
|
<li><a class="dropdown-item" href="#" data-filter="mns">MNS</a></li>
|
||||||
</tr>
|
<li><a class="dropdown-item" href="#" data-filter="mps">MPS</a></li>
|
||||||
{% endfor %}
|
<li><a class="dropdown-item" href="#" data-filter="juventude">Juventude</a></li>
|
||||||
</tbody>
|
<li><a class="dropdown-item" href="#" data-filter="aspirante">Aspirante</a></li>
|
||||||
</table>
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
<li><h6 class="dropdown-header">Célula</h6></li>
|
||||||
|
{% for celula in celulas %}
|
||||||
|
<li><a class="dropdown-item" href="#" data-filter="celula" data-celula="{{ celula.id }}">{{ celula.nome }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover" id="militantesTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th data-sort="nome">Nome <i class="fas fa-sort"></i></th>
|
||||||
|
<th data-sort="cpf">CPF <i class="fas fa-sort"></i></th>
|
||||||
|
<th data-sort="email">Email <i class="fas fa-sort"></i></th>
|
||||||
|
<th data-sort="telefone">Telefone <i class="fas fa-sort"></i></th>
|
||||||
|
<th>Responsabilidades</th>
|
||||||
|
<th class="text-end">Ações</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for militante in militantes %}
|
||||||
|
<tr data-militante="{{ militante.id }}"
|
||||||
|
data-celula-id="{{ militante.celula_id }}"
|
||||||
|
data-responsabilidades="{{ militante.responsabilidades }}">
|
||||||
|
<td data-nome="{{ militante.nome }}">{{ militante.nome }}</td>
|
||||||
|
<td data-cpf="{{ militante.cpf }}">{{ militante.cpf }}</td>
|
||||||
|
<td data-email="{{ militante.emails[0].endereco_email if militante.emails else '' }}">{{ militante.emails[0].endereco_email if militante.emails else '' }}</td>
|
||||||
|
<td data-telefone="{{ militante.telefone1 }}">{{ militante.telefone1 }}</td>
|
||||||
|
<td>
|
||||||
|
{% if militante.responsabilidades is defined and militante.responsabilidades %}
|
||||||
|
{% if militante.responsabilidades|bitwise_and(Militante.RESPONSAVEL_FINANCAS) %}
|
||||||
|
<span class="badge bg-primary" title="Responsável de Finanças">RFI</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if militante.responsabilidades|bitwise_and(Militante.RESPONSAVEL_IMPRENSA) %}
|
||||||
|
<span class="badge bg-info" title="Responsável de Imprensa">RIM</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if militante.responsabilidades|bitwise_and(Militante.QUADRO_ORIENTADOR) %}
|
||||||
|
<span class="badge bg-success" title="Quadro-Orientador">QOR</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if militante.responsabilidades|bitwise_and(Militante.SECRETARIO) %}
|
||||||
|
<span class="badge bg-secondary" title="Secretário">SEC</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if militante.responsabilidades|bitwise_and(Militante.TESOUREIRO) %}
|
||||||
|
<span class="badge bg-warning" title="Tesoureiro">TES</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if militante.responsabilidades|bitwise_and(Militante.IMPRENSA) %}
|
||||||
|
<span class="badge bg-danger" title="Imprensa">IMP</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if militante.responsabilidades|bitwise_and(Militante.MNS) %}
|
||||||
|
<span class="badge bg-purple" title="MNS">MNS</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if militante.responsabilidades|bitwise_and(Militante.MPS) %}
|
||||||
|
<span class="badge bg-teal" title="MPS">MPS</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if militante.responsabilidades|bitwise_and(Militante.JUVENTUDE) %}
|
||||||
|
<span class="badge bg-orange" title="Juventude">JUV</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if militante.responsabilidades|bitwise_and(Militante.ASPIRANTE) %}
|
||||||
|
<span class="badge bg-dark" title="Aspirante">ASP</span>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-sm btn-outline-primary"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#modalEditarMilitante"
|
||||||
|
data-militante-id="{{ militante.id }}"
|
||||||
|
data-militante-nome="{{ militante.nome }}"
|
||||||
|
title="Editar">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-sm btn-outline-danger"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#deleteModal"
|
||||||
|
data-militante-id="{{ militante.id }}"
|
||||||
|
data-militante-nome="{{ militante.nome }}"
|
||||||
|
title="Excluir">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pagination-container d-flex justify-content-between align-items-center">
|
||||||
|
<div class="text-muted">
|
||||||
|
Mostrando <span id="countMilitantes">{{ militantes|length }}</span> militantes
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center gap-3">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<span class="me-2">Mostrar</span>
|
||||||
|
<select class="form-select form-select-sm me-2" id="rowsPerPage" style="width: auto;">
|
||||||
|
<option value="10">10</option>
|
||||||
|
<option value="20" selected>20</option>
|
||||||
|
<option value="50">50</option>
|
||||||
|
<option value="100">100</option>
|
||||||
|
</select>
|
||||||
|
<span>linhas</span>
|
||||||
|
</div>
|
||||||
|
<nav aria-label="Navegação de páginas">
|
||||||
|
<ul class="pagination mb-0">
|
||||||
|
<li class="page-item disabled" id="prevPage">
|
||||||
|
<a class="page-link" href="#"><i class="fas fa-chevron-left"></i></a>
|
||||||
|
</li>
|
||||||
|
<li class="page-item active"><a class="page-link" href="#">1</a></li>
|
||||||
|
<li class="page-item"><a class="page-link" href="#">2</a></li>
|
||||||
|
<li class="page-item"><a class="page-link" href="#">3</a></li>
|
||||||
|
<li class="page-item" id="nextPage">
|
||||||
|
<a class="page-link" href="#"><i class="fas fa-chevron-right"></i></a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<!-- Modais -->
|
||||||
function confirmarExclusao(id) {
|
{% include 'modals/militante_novo.html' %}
|
||||||
if (confirm('Tem certeza que deseja excluir este militante?')) {
|
{% include 'modals/militante_editar.html' %}
|
||||||
window.location.href = "{{ url_for('excluir_militante', id=0) }}".replace('0', id);
|
{% include 'modals/militante_excluir.html' %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<!-- jQuery -->
|
||||||
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
|
|
||||||
|
<!-- jQuery Mask Plugin -->
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.mask/1.14.16/jquery.mask.min.js"></script>
|
||||||
|
|
||||||
|
<!-- Nosso script -->
|
||||||
|
<script src="{{ url_for('static', filename='js/militantes.js') }}"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
/* Estilo para o botão Novo Militante */
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--bs-danger);
|
||||||
|
border-color: var(--bs-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover,
|
||||||
|
.btn-primary:focus,
|
||||||
|
.btn-primary:active {
|
||||||
|
background-color: var(--bs-danger-dark, #b02a37) !important;
|
||||||
|
border-color: var(--bs-danger-dark, #b02a37) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilo para os switches */
|
||||||
|
.form-check-input {
|
||||||
|
background-color: #fff;
|
||||||
|
border-color: rgba(220, 53, 69, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-check-input:checked {
|
||||||
|
background-color: var(--bs-danger);
|
||||||
|
border-color: var(--bs-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-check-input:focus {
|
||||||
|
border-color: var(--bs-danger);
|
||||||
|
box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-switch .form-check-input {
|
||||||
|
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28220, 53, 69, 0.85%29'/%3e%3c/svg%3e");
|
||||||
|
background-position: left center;
|
||||||
|
border-radius: 2em;
|
||||||
|
transition: background-position .15s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-switch .form-check-input:checked {
|
||||||
|
background-position: right center;
|
||||||
|
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e");
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-check {
|
||||||
|
min-height: 1.5rem;
|
||||||
|
padding-left: 2.8em;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-check-label {
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilo para o backdrop com blur em todos os modais */
|
||||||
|
.modal-backdrop.show {
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilo para botões com largura fixa */
|
||||||
|
.btn-fixed-width {
|
||||||
|
min-width: 120px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
text-align: center;
|
||||||
|
height: 38px; /* Altura padrão do Bootstrap para btn */
|
||||||
|
line-height: 1.5;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-fixed-width i {
|
||||||
|
margin-right: 8px;
|
||||||
|
font-size: 0.875rem; /* 14px - tamanho padrão de ícone */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-toggle::after {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilo para o botão de fechar dos modais */
|
||||||
|
.btn-close {
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 0.5rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
filter: invert(1) grayscale(100%) brightness(200%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-close:hover {
|
||||||
|
opacity: 1;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilo para colunas ordenáveis */
|
||||||
|
th[data-sort] {
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
th[data-sort] i {
|
||||||
|
margin-left: 5px;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
th[data-sort].sort-asc i,
|
||||||
|
th[data-sort].sort-desc i {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animação para linhas da tabela */
|
||||||
|
#militantesTable tbody tr {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#militantesTable tbody tr:hover {
|
||||||
|
background-color: rgba(0,0,0,0.02);
|
||||||
|
transform: translateX(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilo para badges */
|
||||||
|
.badge {
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 0.4em 0.6em;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
margin-right: 0.3rem;
|
||||||
|
min-width: 2em;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cores personalizadas para badges */
|
||||||
|
.bg-purple { background-color: #6f42c1 !important; color: white !important; }
|
||||||
|
.bg-teal { background-color: #20c997 !important; color: white !important; }
|
||||||
|
.bg-orange { background-color: #fd7e14 !important; color: white !important; }
|
||||||
|
.bg-indigo { background-color: #6610f2 !important; color: white !important; }
|
||||||
|
.bg-pink { background-color: #d63384 !important; color: white !important; }
|
||||||
|
|
||||||
|
/* Cores do Bootstrap que vamos usar */
|
||||||
|
.badge.bg-primary { background-color: #0d6efd !important; }
|
||||||
|
.badge.bg-info { background-color: #0dcaf0 !important; }
|
||||||
|
.badge.bg-success { background-color: #198754 !important; }
|
||||||
|
.badge.bg-danger { background-color: #dc3545 !important; }
|
||||||
|
.badge.bg-dark { background-color: #212529 !important; }
|
||||||
|
|
||||||
|
/* Tooltip personalizado */
|
||||||
|
.tooltip {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip .tooltip-inner {
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilo para botões de ação */
|
||||||
|
.btn-group .btn {
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group .btn i {
|
||||||
|
width: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilo para modais */
|
||||||
|
.modal-content {
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
background: linear-gradient(to right, var(--bs-gray-dark), var(--bs-gray));
|
||||||
|
color: white;
|
||||||
|
border-radius: 12px 12px 0 0;
|
||||||
|
border-bottom: none;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsividade */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.btn-group {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group .btn {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer .btn {
|
||||||
|
min-width: 120px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
|
||||||
|
/* Estilos personalizados para o Bootstrap Datepicker */
|
||||||
|
.datepicker {
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
background-color: white !important;
|
||||||
|
color: #212529 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.datepicker table {
|
||||||
|
background-color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.datepicker table tr td,
|
||||||
|
.datepicker table tr th {
|
||||||
|
text-align: center;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 2px;
|
||||||
|
color: #212529 !important;
|
||||||
|
background-color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.datepicker table tr td.day:hover,
|
||||||
|
.datepicker table tr td.focused {
|
||||||
|
background: #f8f9fa !important;
|
||||||
|
color: #212529 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.datepicker table tr td.active,
|
||||||
|
.datepicker table tr td.active:hover {
|
||||||
|
background-color: var(--bs-primary) !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.datepicker table tr td.today {
|
||||||
|
background-color: #e9ecef !important;
|
||||||
|
color: #212529 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.datepicker .datepicker-switch,
|
||||||
|
.datepicker .prev,
|
||||||
|
.datepicker .next {
|
||||||
|
font-weight: normal;
|
||||||
|
padding: 4px;
|
||||||
|
color: #212529 !important;
|
||||||
|
background-color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.datepicker .dow {
|
||||||
|
font-weight: normal;
|
||||||
|
padding: 4px;
|
||||||
|
color: #212529 !important;
|
||||||
|
background-color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.datepicker-dropdown:after {
|
||||||
|
border-bottom-color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.datepicker-dropdown.datepicker-orient-top:after {
|
||||||
|
border-top-color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilo para os campos de data */
|
||||||
|
.datepicker-input {
|
||||||
|
background-color: white !important;
|
||||||
|
color: #212529 !important;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.datepicker-clear-btn {
|
||||||
|
color: #6c757d !important;
|
||||||
|
background-color: white !important;
|
||||||
|
padding: 5px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.datepicker-clear-btn:hover {
|
||||||
|
background-color: #f8f9fa !important;
|
||||||
|
color: #495057 !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
{% extends 'base.html' %}
|
|
||||||
|
|
||||||
{% block title %}Listar Militantes{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<h1>Pagamentos</h1>
|
|
||||||
<a href="{{ url_for('novo_pagamento') }}">Adicionar Novo Pagamento</a>
|
|
||||||
<table border="1">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>ID</th>
|
|
||||||
<th>Militante ID</th>
|
|
||||||
<th>Tipo de Pagamento</th>
|
|
||||||
<th>Valor</th>
|
|
||||||
<th>Data do Pagamento</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for pagamento in pagamentos %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ pagamento.id }}</td>
|
|
||||||
<td>{{ pagamento.militante_id }}</td>
|
|
||||||
<td>{{ pagamento.tipo_pagamento_id }}</td>
|
|
||||||
<td>R$ {{ pagamento.valor }}</td>
|
|
||||||
<td>{{ pagamento.data_pagamento }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<a href="{{ url_for('home') }}">Home</a>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
{% block title %}Listar Relatórios de Pagamentos{% endblock %}
|
{% block title %}Listar Relatórios de Comprovantes{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<h1 class="mb-4">Lista de Relatórios de Pagamentos</h1>
|
<h1 class="mb-4">Lista de Relatórios de Comprovantes</h1>
|
||||||
|
|
||||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
<div class="d-flex justify-content-between mb-4">
|
<div class="d-flex justify-content-between mb-4">
|
||||||
<a href="{{ url_for('novo_relatorio_pagamentos') }}" class="btn btn-success">Novo Relatório</a>
|
<a href="{{ url_for('novo_relatorio_comprovantes') }}" class="btn btn-success">Novo Relatório</a>
|
||||||
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
|
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
<th>ID</th>
|
<th>ID</th>
|
||||||
<th>Setor</th>
|
<th>Setor</th>
|
||||||
<th>Comitê Central</th>
|
<th>Comitê Central</th>
|
||||||
<th>Total de Pagamentos</th>
|
<th>Total de Comprovantes</th>
|
||||||
<th>Data do Relatório</th>
|
<th>Data do Relatório</th>
|
||||||
<th>Ações</th>
|
<th>Ações</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -39,11 +39,11 @@
|
|||||||
<td>{{ relatorio.id }}</td>
|
<td>{{ relatorio.id }}</td>
|
||||||
<td>{{ relatorio.setor.nome }}</td>
|
<td>{{ relatorio.setor.nome }}</td>
|
||||||
<td>{{ relatorio.comite.nome }}</td>
|
<td>{{ relatorio.comite.nome }}</td>
|
||||||
<td>R$ {{ "%.2f"|format(relatorio.total_pagamentos) }}</td>
|
<td>R$ {{ "%.2f"|format(relatorio.total_comprovantes) }}</td>
|
||||||
<td>{{ relatorio.data_relatorio.strftime('%d/%m/%Y') }}</td>
|
<td>{{ relatorio.data_relatorio.strftime('%d/%m/%Y') }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="{{ url_for('editar_relatorio_pagamentos', id=relatorio.id) }}" class="btn btn-primary btn-sm">Editar</a>
|
<a href="{{ url_for('editar_relatorio_comprovantes', id=relatorio.id) }}" class="btn btn-primary btn-sm">Editar</a>
|
||||||
<a href="{{ url_for('deletar_relatorio_pagamentos', id=relatorio.id) }}" class="btn btn-danger btn-sm" onclick="return confirm('Tem certeza que deseja excluir este relatório?')">Excluir</a>
|
<a href="{{ url_for('deletar_relatorio_comprovantes', id=relatorio.id) }}" class="btn btn-danger btn-sm" onclick="return confirm('Tem certeza que deseja excluir este relatório?')">Excluir</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -3,47 +3,47 @@
|
|||||||
{% block title %}Listar Relatórios de Cotas{% endblock %}
|
{% block title %}Listar Relatórios de Cotas{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container">
|
<div class="container mt-4">
|
||||||
<div class="row">
|
<div class="card">
|
||||||
<div class="col-md-12">
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
<h1 class="mb-4">Lista de Relatórios de Cotas</h1>
|
<h5 class="mb-0"><i class="fas fa-file-invoice-dollar me-2"></i>Relatórios de Cotas</h5>
|
||||||
|
<a href="{{ url_for('novo_relatorio_cotas') }}" class="btn btn-success">
|
||||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
<i class="fas fa-plus me-2"></i>Novo Relatório
|
||||||
{% if messages %}
|
</a>
|
||||||
{% for category, message in messages %}
|
</div>
|
||||||
<div class="alert alert-{{ category }}">{{ message }}</div>
|
<div class="card-body">
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
{% endwith %}
|
|
||||||
|
|
||||||
<div class="d-flex justify-content-between mb-4">
|
|
||||||
<a href="{{ url_for('novo_relatorio_cotas') }}" class="btn btn-success">Novo Relatório</a>
|
|
||||||
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-striped table-hover">
|
<table class="table table-hover" id="relatoriosTable">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
<th data-sort="id">ID <i class="fas fa-sort"></i></th>
|
||||||
<th>Setor</th>
|
<th data-sort="setor">Setor <i class="fas fa-sort"></i></th>
|
||||||
<th>Comitê Central</th>
|
<th data-sort="comite">Comitê Central <i class="fas fa-sort"></i></th>
|
||||||
<th>Total de Cotas</th>
|
<th data-sort="total">Total de Cotas <i class="fas fa-sort"></i></th>
|
||||||
<th>Data do Relatório</th>
|
<th data-sort="data">Data do Relatório <i class="fas fa-sort"></i></th>
|
||||||
<th>Ações</th>
|
<th class="text-end">Ações</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for relatorio in relatorios %}
|
{% for relatorio in relatorios %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ relatorio.id }}</td>
|
<td data-id="{{ relatorio.id }}">{{ relatorio.id }}</td>
|
||||||
<td>{{ relatorio.setor.nome }}</td>
|
<td data-setor="{{ relatorio.setor.nome }}">{{ relatorio.setor.nome }}</td>
|
||||||
<td>{{ relatorio.comite.nome }}</td>
|
<td data-comite="{{ relatorio.comite.nome }}">{{ relatorio.comite.nome }}</td>
|
||||||
<td>R$ {{ "%.2f"|format(relatorio.total_cotas) }}</td>
|
<td data-total="{{ relatorio.total_cotas }}">R$ {{ "%.2f"|format(relatorio.total_cotas) }}</td>
|
||||||
<td>{{ relatorio.data_relatorio.strftime('%d/%m/%Y') }}</td>
|
<td data-data="{{ relatorio.data_relatorio }}">{{ relatorio.data_relatorio.strftime('%d/%m/%Y') }}</td>
|
||||||
<td>
|
<td class="text-end">
|
||||||
<a href="{{ url_for('editar_relatorio_cotas', id=relatorio.id) }}" class="btn btn-primary btn-sm">Editar</a>
|
<a href="{{ url_for('editar_relatorio_cotas', id=relatorio.id) }}"
|
||||||
<a href="{{ url_for('deletar_relatorio_cotas', id=relatorio.id) }}" class="btn btn-danger btn-sm" onclick="return confirm('Tem certeza que deseja excluir este relatório?')">Excluir</a>
|
class="btn btn-primary btn-sm"
|
||||||
|
title="Editar">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('deletar_relatorio_cotas', id=relatorio.id) }}"
|
||||||
|
class="btn btn-danger btn-sm"
|
||||||
|
onclick="return confirm('Tem certeza que deseja excluir este relatório?')"
|
||||||
|
title="Excluir">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -53,5 +53,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="{{ url_for('static', filename='js/table_sort.js') }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@@ -3,47 +3,47 @@
|
|||||||
{% block title %}Listar Relatórios de Vendas{% endblock %}
|
{% block title %}Listar Relatórios de Vendas{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container">
|
<div class="container mt-4">
|
||||||
<div class="row">
|
<div class="card">
|
||||||
<div class="col-md-12">
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
<h1 class="mb-4">Lista de Relatórios de Vendas</h1>
|
<h5 class="mb-0"><i class="fas fa-file-invoice me-2"></i>Relatórios de Vendas</h5>
|
||||||
|
<a href="{{ url_for('novo_relatorio_vendas') }}" class="btn btn-success">
|
||||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
<i class="fas fa-plus me-2"></i>Novo Relatório
|
||||||
{% if messages %}
|
</a>
|
||||||
{% for category, message in messages %}
|
</div>
|
||||||
<div class="alert alert-{{ category }}">{{ message }}</div>
|
<div class="card-body">
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
{% endwith %}
|
|
||||||
|
|
||||||
<div class="d-flex justify-content-between mb-4">
|
|
||||||
<a href="{{ url_for('novo_relatorio_vendas') }}" class="btn btn-success">Novo Relatório</a>
|
|
||||||
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-striped table-hover">
|
<table class="table table-hover" id="relatoriosTable">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
<th data-sort="id">ID <i class="fas fa-sort"></i></th>
|
||||||
<th>Setor</th>
|
<th data-sort="setor">Setor <i class="fas fa-sort"></i></th>
|
||||||
<th>Comitê Central</th>
|
<th data-sort="comite">Comitê Central <i class="fas fa-sort"></i></th>
|
||||||
<th>Total de Vendas</th>
|
<th data-sort="total">Total de Vendas <i class="fas fa-sort"></i></th>
|
||||||
<th>Data do Relatório</th>
|
<th data-sort="data">Data do Relatório <i class="fas fa-sort"></i></th>
|
||||||
<th>Ações</th>
|
<th class="text-end">Ações</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for relatorio in relatorios %}
|
{% for relatorio in relatorios %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ relatorio.id }}</td>
|
<td data-id="{{ relatorio.id }}">{{ relatorio.id }}</td>
|
||||||
<td>{{ relatorio.setor.nome }}</td>
|
<td data-setor="{{ relatorio.setor.nome }}">{{ relatorio.setor.nome }}</td>
|
||||||
<td>{{ relatorio.comite.nome }}</td>
|
<td data-comite="{{ relatorio.comite.nome }}">{{ relatorio.comite.nome }}</td>
|
||||||
<td>R$ {{ "%.2f"|format(relatorio.total_vendas) }}</td>
|
<td data-total="{{ relatorio.total_vendas }}">R$ {{ "%.2f"|format(relatorio.total_vendas) }}</td>
|
||||||
<td>{{ relatorio.data_relatorio.strftime('%d/%m/%Y') }}</td>
|
<td data-data="{{ relatorio.data_relatorio }}">{{ relatorio.data_relatorio.strftime('%d/%m/%Y') }}</td>
|
||||||
<td>
|
<td class="text-end">
|
||||||
<a href="{{ url_for('editar_relatorio_vendas', id=relatorio.id) }}" class="btn btn-primary btn-sm">Editar</a>
|
<a href="{{ url_for('editar_relatorio_vendas', id=relatorio.id) }}"
|
||||||
<a href="{{ url_for('deletar_relatorio_vendas', id=relatorio.id) }}" class="btn btn-danger btn-sm" onclick="return confirm('Tem certeza que deseja excluir este relatório?')">Excluir</a>
|
class="btn btn-primary btn-sm"
|
||||||
|
title="Editar">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('deletar_relatorio_vendas', id=relatorio.id) }}"
|
||||||
|
class="btn btn-danger btn-sm"
|
||||||
|
onclick="return confirm('Tem certeza que deseja excluir este relatório?')"
|
||||||
|
title="Excluir">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -53,4 +53,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="{{ url_for('static', filename='js/table_sort.js') }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,32 +1,515 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Listar Militantes{% endblock %}
|
{% block title %}Vendas de Jornais{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>Vendas de Jornais Avulsos</h1>
|
<div class="row mb-4">
|
||||||
<a href="{{ url_for('nova_venda_jornal') }}">Adicionar Nova Venda</a>
|
<div class="col-12">
|
||||||
<table border="1">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<thead>
|
<h1 class="mb-0">
|
||||||
<tr>
|
<i class="fas fa-newspaper me-2"></i>Vendas de Jornais
|
||||||
<th>ID</th>
|
</h1>
|
||||||
<th>Militante ID</th>
|
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalNovaVenda">
|
||||||
<th>Quantidade</th>
|
<i class="fas fa-plus me-2"></i>Nova Venda
|
||||||
<th>Valor Total</th>
|
</button>
|
||||||
<th>Data da Venda</th>
|
</div>
|
||||||
</tr>
|
</div>
|
||||||
</thead>
|
</div>
|
||||||
<tbody>
|
|
||||||
{% for venda in vendas %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ venda.id }}</td>
|
|
||||||
<td>{{ venda.militante_id }}</td>
|
|
||||||
<td>{{ venda.quantidade }}</td>
|
|
||||||
<td>R$ {{ venda.valor_total }}</td>
|
|
||||||
<td>{{ venda.data_venda }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<a href="{{ url_for('home') }}">Home</a>
|
|
||||||
|
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text">
|
||||||
|
<i class="fas fa-search"></i>
|
||||||
|
</span>
|
||||||
|
<input type="text" class="form-control" id="searchInput" placeholder="Pesquisar vendas...">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 text-end">
|
||||||
|
<button id="btnExportar" class="btn btn-outline-primary">
|
||||||
|
<i class="fas fa-download me-2"></i>Exportar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover" id="vendasTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th data-sort="militante">Militante <i class="fas fa-sort"></i></th>
|
||||||
|
<th data-sort="quantidade">Quantidade <i class="fas fa-sort"></i></th>
|
||||||
|
<th data-sort="valor_total">Valor Total <i class="fas fa-sort"></i></th>
|
||||||
|
<th data-sort="data">Data <i class="fas fa-sort"></i></th>
|
||||||
|
<th class="text-end">Ações</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for venda in vendas %}
|
||||||
|
<tr>
|
||||||
|
<td data-militante="{{ venda.militante.nome }}">{{ venda.militante.nome }}</td>
|
||||||
|
<td data-quantidade="{{ venda.quantidade }}">{{ venda.quantidade }}</td>
|
||||||
|
<td data-valor_total="{{ venda.valor_total }}">R$ {{ "%.2f"|format(venda.valor_total) }}</td>
|
||||||
|
<td data-data="{{ venda.data_venda }}">{{ venda.data_venda.strftime('%d/%m/%Y') }}</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-sm btn-outline-primary"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#modalEditarVenda"
|
||||||
|
data-venda-id="{{ venda.id }}"
|
||||||
|
data-venda-militante="{{ venda.militante_id }}"
|
||||||
|
data-venda-quantidade="{{ venda.quantidade }}"
|
||||||
|
data-venda-valor-total="{{ venda.valor_total }}"
|
||||||
|
data-venda-data="{{ venda.data_venda.strftime('%Y-%m-%d') }}"
|
||||||
|
title="Editar">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-sm btn-outline-danger"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#deleteModal"
|
||||||
|
data-venda-id="{{ venda.id }}"
|
||||||
|
data-venda-info="{{ venda.militante.nome }} - {{ venda.quantidade }} jornais"
|
||||||
|
title="Excluir">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Nova Venda -->
|
||||||
|
<div class="modal fade" id="modalNovaVenda" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">
|
||||||
|
<i class="fas fa-plus me-2"></i>Nova Venda de Jornal
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="formNovaVenda" method="post" action="{{ url_for('nova_venda_jornal') }}">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="militante_id" class="form-label">Militante:</label>
|
||||||
|
<select class="form-select" id="militante_id" name="militante_id" required>
|
||||||
|
<option value="">Selecione um militante</option>
|
||||||
|
{% for militante in militantes %}
|
||||||
|
<option value="{{ militante.id }}">{{ militante.nome }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="quantidade" class="form-label">Quantidade:</label>
|
||||||
|
<input type="number" class="form-control" id="quantidade" name="quantidade" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="valor_total" class="form-label">Valor Total:</label>
|
||||||
|
<input type="number" step="0.01" class="form-control" id="valor_total" name="valor_total" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="data_venda" class="form-label">Data da Venda:</label>
|
||||||
|
<input type="date" class="form-control" id="data_venda" name="data_venda" required>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||||
|
<button type="submit" form="formNovaVenda" class="btn btn-success">
|
||||||
|
<i class="fas fa-save me-2"></i>Salvar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal de Edição -->
|
||||||
|
<div class="modal fade" id="modalEditarVenda" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">
|
||||||
|
<i class="fas fa-edit me-2"></i>Editar Venda de Jornal
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="formEditarVenda" method="post">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editMilitante" class="form-label">Militante:</label>
|
||||||
|
<select class="form-select" id="editMilitante" name="militante_id" required>
|
||||||
|
<option value="">Selecione um militante</option>
|
||||||
|
{% for militante in militantes %}
|
||||||
|
<option value="{{ militante.id }}">{{ militante.nome }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editQuantidade" class="form-label">Quantidade:</label>
|
||||||
|
<input type="number" class="form-control" id="editQuantidade" name="quantidade" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editValorTotal" class="form-label">Valor Total:</label>
|
||||||
|
<input type="number" step="0.01" class="form-control" id="editValorTotal" name="valor_total" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editData" class="form-label">Data da Venda:</label>
|
||||||
|
<input type="date" class="form-control" id="editData" name="data_venda" required>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||||
|
<button type="submit" form="formEditarVenda" class="btn btn-success">
|
||||||
|
<i class="fas fa-save me-2"></i>Salvar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal de Exclusão -->
|
||||||
|
<div class="modal fade" id="deleteModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Confirmar Exclusão</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>Tem certeza que deseja excluir a venda de <strong id="vendaInfo"></strong>?</p>
|
||||||
|
<p class="text-danger mb-0">Esta ação não pode ser desfeita.</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||||
|
<form action="" method="POST" id="deleteForm" class="d-inline">
|
||||||
|
<button type="submit" class="btn btn-danger">
|
||||||
|
<i class="fas fa-trash me-2"></i>Excluir
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Configuração do modal de exclusão
|
||||||
|
const deleteModal = document.getElementById('deleteModal');
|
||||||
|
deleteModal.addEventListener('show.bs.modal', function(event) {
|
||||||
|
const button = event.relatedTarget;
|
||||||
|
const vendaId = button.getAttribute('data-venda-id');
|
||||||
|
const vendaInfo = button.getAttribute('data-venda-info');
|
||||||
|
|
||||||
|
document.getElementById('vendaInfo').textContent = vendaInfo;
|
||||||
|
document.getElementById('deleteForm').action = `/jornais/excluir/${vendaId}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Envio do formulário de nova venda via AJAX
|
||||||
|
const formNovaVenda = document.getElementById('formNovaVenda');
|
||||||
|
formNovaVenda.addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const formData = new FormData(this);
|
||||||
|
|
||||||
|
fetch(this.action, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
headers: {
|
||||||
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'success') {
|
||||||
|
// Fechar o modal
|
||||||
|
bootstrap.Modal.getInstance(document.getElementById('modalNovaVenda')).hide();
|
||||||
|
|
||||||
|
// Atualizar a lista
|
||||||
|
location.reload();
|
||||||
|
|
||||||
|
// Mostrar mensagem de sucesso
|
||||||
|
const alertDiv = document.createElement('div');
|
||||||
|
alertDiv.className = 'alert alert-success alert-dismissible fade show';
|
||||||
|
alertDiv.innerHTML = `
|
||||||
|
${data.message}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
`;
|
||||||
|
document.querySelector('.container').insertBefore(alertDiv, document.querySelector('.container').firstChild);
|
||||||
|
} else {
|
||||||
|
// Mostrar erro
|
||||||
|
const alertDiv = document.createElement('div');
|
||||||
|
alertDiv.className = 'alert alert-danger alert-dismissible fade show';
|
||||||
|
alertDiv.innerHTML = `
|
||||||
|
${data.message}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
`;
|
||||||
|
document.querySelector('.modal-body').insertBefore(alertDiv, formNovaVenda);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Erro:', error);
|
||||||
|
const alertDiv = document.createElement('div');
|
||||||
|
alertDiv.className = 'alert alert-danger alert-dismissible fade show';
|
||||||
|
alertDiv.innerHTML = `
|
||||||
|
Erro ao cadastrar venda. Tente novamente.
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
`;
|
||||||
|
document.querySelector('.modal-body').insertBefore(alertDiv, formNovaVenda);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configuração do modal de edição
|
||||||
|
const modalEditarVenda = document.getElementById('modalEditarVenda');
|
||||||
|
modalEditarVenda.addEventListener('show.bs.modal', function(event) {
|
||||||
|
const button = event.relatedTarget;
|
||||||
|
const vendaId = button.getAttribute('data-venda-id');
|
||||||
|
|
||||||
|
// Preencher o formulário com os dados da venda
|
||||||
|
document.getElementById('editMilitante').value = button.getAttribute('data-venda-militante');
|
||||||
|
document.getElementById('editQuantidade').value = button.getAttribute('data-venda-quantidade');
|
||||||
|
document.getElementById('editValorTotal').value = button.getAttribute('data-venda-valor-total');
|
||||||
|
document.getElementById('editData').value = button.getAttribute('data-venda-data');
|
||||||
|
|
||||||
|
// Configurar a action do formulário
|
||||||
|
document.getElementById('formEditarVenda').action = `/jornais/editar/${vendaId}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Envio do formulário de edição via AJAX
|
||||||
|
const formEditarVenda = document.getElementById('formEditarVenda');
|
||||||
|
formEditarVenda.addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const formData = new FormData(this);
|
||||||
|
|
||||||
|
fetch(this.action, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
headers: {
|
||||||
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'success') {
|
||||||
|
// Fechar o modal
|
||||||
|
bootstrap.Modal.getInstance(modalEditarVenda).hide();
|
||||||
|
|
||||||
|
// Atualizar a lista
|
||||||
|
location.reload();
|
||||||
|
|
||||||
|
// Mostrar mensagem de sucesso
|
||||||
|
const alertDiv = document.createElement('div');
|
||||||
|
alertDiv.className = 'alert alert-success alert-dismissible fade show';
|
||||||
|
alertDiv.innerHTML = `
|
||||||
|
${data.message}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
`;
|
||||||
|
document.querySelector('.container').insertBefore(alertDiv, document.querySelector('.container').firstChild);
|
||||||
|
} else {
|
||||||
|
// Mostrar erro
|
||||||
|
const alertDiv = document.createElement('div');
|
||||||
|
alertDiv.className = 'alert alert-danger alert-dismissible fade show';
|
||||||
|
alertDiv.innerHTML = `
|
||||||
|
${data.message}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
`;
|
||||||
|
document.querySelector('.modal-body').insertBefore(alertDiv, formEditarVenda);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Erro:', error);
|
||||||
|
const alertDiv = document.createElement('div');
|
||||||
|
alertDiv.className = 'alert alert-danger alert-dismissible fade show';
|
||||||
|
alertDiv.innerHTML = `
|
||||||
|
Erro ao atualizar venda. Tente novamente.
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
`;
|
||||||
|
document.querySelector('.modal-body').insertBefore(alertDiv, formEditarVenda);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Limpar alertas quando os modais forem fechados
|
||||||
|
[modalEditarVenda, document.getElementById('modalNovaVenda')].forEach(modal => {
|
||||||
|
modal.addEventListener('hidden.bs.modal', function () {
|
||||||
|
const alerts = this.querySelectorAll('.alert');
|
||||||
|
alerts.forEach(alert => alert.remove());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pesquisa em tempo real
|
||||||
|
const searchInput = document.getElementById('searchInput');
|
||||||
|
searchInput.addEventListener('input', function() {
|
||||||
|
const searchTerm = this.value.toLowerCase();
|
||||||
|
const rows = document.querySelectorAll('#vendasTable tbody tr');
|
||||||
|
|
||||||
|
rows.forEach(row => {
|
||||||
|
const text = row.textContent.toLowerCase();
|
||||||
|
row.style.display = text.includes(searchTerm) ? '' : 'none';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ordenação
|
||||||
|
const headers = document.querySelectorAll('#vendasTable th[data-sort]');
|
||||||
|
headers.forEach(header => {
|
||||||
|
header.addEventListener('click', function() {
|
||||||
|
const column = this.getAttribute('data-sort');
|
||||||
|
const tbody = document.querySelector('#vendasTable tbody');
|
||||||
|
const rows = Array.from(tbody.querySelectorAll('tr'));
|
||||||
|
const isAsc = !this.classList.contains('sort-asc');
|
||||||
|
|
||||||
|
// Remover classes de ordenação de todos os headers
|
||||||
|
headers.forEach(h => {
|
||||||
|
h.classList.remove('sort-asc', 'sort-desc');
|
||||||
|
h.querySelector('i').className = 'fas fa-sort';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Adicionar classe de ordenação ao header clicado
|
||||||
|
this.classList.add(isAsc ? 'sort-asc' : 'sort-desc');
|
||||||
|
this.querySelector('i').className = `fas fa-sort-${isAsc ? 'up' : 'down'}`;
|
||||||
|
|
||||||
|
// Ordenar linhas
|
||||||
|
rows.sort((a, b) => {
|
||||||
|
const aVal = a.querySelector(`td[data-${column}]`).getAttribute(`data-${column}`);
|
||||||
|
const bVal = b.querySelector(`td[data-${column}]`).getAttribute(`data-${column}`);
|
||||||
|
return isAsc ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reposicionar linhas
|
||||||
|
rows.forEach(row => tbody.appendChild(row));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Exportar para CSV
|
||||||
|
document.getElementById('btnExportar').addEventListener('click', function() {
|
||||||
|
const rows = document.querySelectorAll('#vendasTable tbody tr:not([style*="display: none"])');
|
||||||
|
const headers = ['Militante', 'Quantidade', 'Valor Total', 'Data'];
|
||||||
|
let csv = headers.join(',') + '\n';
|
||||||
|
|
||||||
|
rows.forEach(row => {
|
||||||
|
const cols = row.querySelectorAll('td');
|
||||||
|
const values = [
|
||||||
|
cols[0].textContent,
|
||||||
|
cols[1].textContent,
|
||||||
|
cols[2].textContent,
|
||||||
|
cols[3].textContent
|
||||||
|
].map(val => `"${val}"`);
|
||||||
|
csv += values.join(',') + '\n';
|
||||||
|
});
|
||||||
|
|
||||||
|
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = URL.createObjectURL(blob);
|
||||||
|
link.setAttribute('download', 'vendas_jornal.csv');
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
/* Estilo para colunas ordenáveis */
|
||||||
|
th[data-sort] {
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
th[data-sort] i {
|
||||||
|
margin-left: 5px;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
th[data-sort].sort-asc i,
|
||||||
|
th[data-sort].sort-desc i {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animação para linhas da tabela */
|
||||||
|
#vendasTable tbody tr {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#vendasTable tbody tr:hover {
|
||||||
|
background-color: rgba(0,0,0,0.02);
|
||||||
|
transform: translateX(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilo para botões de ação */
|
||||||
|
.btn-group .btn {
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group .btn i {
|
||||||
|
width: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsividade */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.btn-group {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group .btn {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilo para o backdrop com blur em todos os modais */
|
||||||
|
.modal-backdrop.show {
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilo para o botão de fechar dos modais */
|
||||||
|
.btn-close {
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 0.5rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
filter: invert(1) grayscale(100%) brightness(200%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-close:hover {
|
||||||
|
opacity: 1;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilo para modais */
|
||||||
|
.modal-content {
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
background: linear-gradient(to right, var(--bs-gray-dark), var(--bs-gray));
|
||||||
|
color: white;
|
||||||
|
border-radius: 12px 12px 0 0;
|
||||||
|
border-bottom: none;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,48 +1,215 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Login{% endblock %}
|
{% block title %}Login{% endblock %}
|
||||||
|
|
||||||
|
{% block navbar %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container">
|
<div class="alert-container">
|
||||||
<div class="row justify-content-center">
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
<div class="col-md-6">
|
{% if messages %}
|
||||||
<div class="card">
|
{% for category, message in messages %}
|
||||||
<div class="card-header">
|
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
|
||||||
<h3 class="card-title">Login</h3>
|
{{ message }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
{% endfor %}
|
||||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
{% endif %}
|
||||||
{% if messages %}
|
{% endwith %}
|
||||||
{% for category, message in messages %}
|
</div>
|
||||||
<div class="alert alert-{{ category }}">{{ message }}</div>
|
|
||||||
{% endfor %}
|
<div class="login-container">
|
||||||
{% endif %}
|
<div class="login-content">
|
||||||
{% endwith %}
|
<div class="login-header">
|
||||||
|
<img src="{{ url_for('static', filename='img/logo001-alpha.png') }}" alt="Logo OCI" class="login-logo">
|
||||||
<form method="POST" action="{{ url_for('login') }}">
|
<h4 class="login-title">Controles OCI</h4>
|
||||||
<div class="mb-3">
|
</div>
|
||||||
<label for="username" class="form-label">Usuário</label>
|
|
||||||
<input type="text" class="form-control" id="username" name="username" required>
|
<form method="POST" action="{{ url_for('login') }}" class="needs-validation" novalidate>
|
||||||
</div>
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<div class="form-floating mb-3">
|
||||||
<div class="mb-3">
|
<input type="text" class="form-control" id="email" name="email" placeholder="Email ou Usuário" required>
|
||||||
<label for="password" class="form-label">Senha</label>
|
<label for="email">Email ou Usuário</label>
|
||||||
<input type="password" class="form-control" id="password" name="password" required>
|
<div class="invalid-feedback">
|
||||||
</div>
|
Por favor, informe seu email ou nome de usuário.
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="otp_code" class="form-label">Código OTP</label>
|
|
||||||
<input type="text" class="form-control" id="otp_code" name="otp_code" required>
|
|
||||||
<small class="text-muted">Digite o código gerado pelo seu aplicativo autenticador</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-grid">
|
|
||||||
<button type="submit" class="btn btn-primary">Entrar</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
<div class="form-floating mb-3 position-relative">
|
||||||
|
<input type="password" class="form-control" id="password" name="password" placeholder="Senha" required>
|
||||||
|
<label for="password">Senha</label>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
Por favor, informe sua senha.
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-link text-secondary position-absolute end-0 top-50 translate-middle-y me-2" type="button" id="togglePassword">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-floating mb-4">
|
||||||
|
<input type="text" class="form-control" id="otp" name="otp" placeholder="Código OTP">
|
||||||
|
<label for="otp">Código OTP</label>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
Por favor, informe o código OTP.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid">
|
||||||
|
<button type="submit" class="btn btn-lg login-button">
|
||||||
|
<i class="fas fa-sign-in-alt me-2"></i>Entrar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Form validation
|
||||||
|
const form = document.querySelector('form');
|
||||||
|
form.addEventListener('submit', function(event) {
|
||||||
|
if (!form.checkValidity()) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
form.classList.add('was-validated');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Toggle password visibility
|
||||||
|
const togglePassword = document.getElementById('togglePassword');
|
||||||
|
const password = document.getElementById('password');
|
||||||
|
|
||||||
|
togglePassword.addEventListener('click', function() {
|
||||||
|
const type = password.getAttribute('type') === 'password' ? 'text' : 'password';
|
||||||
|
password.setAttribute('type', type);
|
||||||
|
this.querySelector('i').classList.toggle('fa-eye');
|
||||||
|
this.querySelector('i').classList.toggle('fa-eye-slash');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-hide alerts after 5 seconds
|
||||||
|
const alerts = document.querySelectorAll('.alert');
|
||||||
|
alerts.forEach(alert => {
|
||||||
|
setTimeout(() => {
|
||||||
|
const bsAlert = new bootstrap.Alert(alert);
|
||||||
|
bsAlert.close();
|
||||||
|
}, 5000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background: var(--primary-color);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 1rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: auto;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 1rem;
|
||||||
|
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-content {
|
||||||
|
padding: 2rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-logo {
|
||||||
|
height: 50px;
|
||||||
|
width: auto;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-title {
|
||||||
|
color: #343a40;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-floating > .form-control {
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-floating > .form-control:hover {
|
||||||
|
border-color: rgba(220, 53, 69, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-floating > .form-control:focus {
|
||||||
|
border-color: rgba(220, 53, 69, 0.5);
|
||||||
|
box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-floating > label {
|
||||||
|
padding: 1rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button {
|
||||||
|
background-color: #0d6efd;
|
||||||
|
border-color: #0d6efd;
|
||||||
|
color: white;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button:hover,
|
||||||
|
.login-button:focus,
|
||||||
|
.login-button:active {
|
||||||
|
background-color: #0b5ed7;
|
||||||
|
border-color: #0b5ed7;
|
||||||
|
color: white;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.login-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-content {
|
||||||
|
padding: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-logo {
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.login-container {
|
||||||
|
margin: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-content {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/components.css') }}">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
|
||||||
|
{% endblock %}
|
||||||
|
|||||||
35
templates/militantes.html
Normal file
35
templates/militantes.html
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<!-- Botões de ação -->
|
||||||
|
<td class="text-end">
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-sm btn-outline-primary"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#modalEditarMilitante"
|
||||||
|
data-militante-id="{{ militante.id }}"
|
||||||
|
data-militante-nome="{{ militante.nome }}"
|
||||||
|
title="Editar">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-sm btn-outline-danger"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#deleteModal"
|
||||||
|
data-militante-id="{{ militante.id }}"
|
||||||
|
data-militante-nome="{{ militante.nome }}"
|
||||||
|
title="Excluir">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{% include 'modals/militante_editar.html' %}
|
||||||
|
{% include 'modals/militante_excluir.html' %}
|
||||||
|
|
||||||
|
<!-- Scripts -->
|
||||||
|
<script src="{{ url_for('static', filename='js/militantes.js') }}"></script>
|
||||||
|
{% if config.DEBUG %}
|
||||||
|
<script src="{{ url_for('static', filename='js/tests/militantes.test.js') }}"></script>
|
||||||
|
<script>ativarTestesMilitantes();</script>
|
||||||
|
{% endif %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
434
templates/modals/militante_editar.html
Normal file
434
templates/modals/militante_editar.html
Normal file
@@ -0,0 +1,434 @@
|
|||||||
|
<!-- Modal de Editar Militante -->
|
||||||
|
<div class="modal fade" id="modalEditarMilitante" tabindex="-1" aria-labelledby="modalEditarMilitanteLabel" aria-hidden="true" data-bs-backdrop="true" data-bs-keyboard="true">
|
||||||
|
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="modalEditarMilitanteLabel">
|
||||||
|
<i class="fas fa-user-edit me-2"></i>Editar Militante
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Fechar"></button>
|
||||||
|
</div>
|
||||||
|
<form id="formEditarMilitante" method="POST" action="/militantes/editar/" novalidate>
|
||||||
|
<input type="hidden" id="edit_militante_id" name="militante_id" value="">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<input type="hidden" id="responsabilidades_values" name="responsabilidades_valor" value="0">
|
||||||
|
|
||||||
|
<!-- Tabs de navegação -->
|
||||||
|
<ul class="nav nav-tabs nav-fill" role="tablist">
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#edit-dados-basicos" type="button" role="tab">
|
||||||
|
<i class="fas fa-user me-2"></i>Dados Básicos
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#edit-contato" type="button" role="tab">
|
||||||
|
<i class="fas fa-address-book me-2"></i>Contato
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#edit-profissional" type="button" role="tab">
|
||||||
|
<i class="fas fa-briefcase me-2"></i>Profissional
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#edit-organizacao" type="button" role="tab">
|
||||||
|
<i class="fas fa-sitemap me-2"></i>Organização
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- Conteúdo das tabs -->
|
||||||
|
<div class="tab-content p-3">
|
||||||
|
<!-- Dados Básicos -->
|
||||||
|
<div class="tab-pane fade show active" id="edit-dados-basicos">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="edit_nome" class="form-label">Nome</label>
|
||||||
|
<input type="text" class="form-control" id="edit_nome" name="nome" required>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
Por favor, insira o nome do militante.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="edit_cpf" class="form-label">CPF</label>
|
||||||
|
<input type="text" class="form-control" id="edit_cpf" name="cpf" required>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
Por favor, insira um CPF válido.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="edit_titulo_eleitoral" class="form-label">Título Eleitoral</label>
|
||||||
|
<input type="text" class="form-control" id="edit_titulo_eleitoral" name="titulo_eleitoral">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="edit_data_nascimento" class="form-label">Data de Nascimento</label>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control date-mask"
|
||||||
|
id="edit_data_nascimento"
|
||||||
|
name="data_nascimento"
|
||||||
|
placeholder="DD/MM/AAAA"
|
||||||
|
maxlength="10"
|
||||||
|
pattern="\d{2}/\d{2}/\d{4}"
|
||||||
|
title="Data no formato DD/MM/AAAA">
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
Por favor, insira uma data válida no formato DD/MM/AAAA.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="edit_data_entrada_oci" class="form-label">Data de Entrada na OCI</label>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control date-mask"
|
||||||
|
id="edit_data_entrada_oci"
|
||||||
|
name="data_entrada_oci"
|
||||||
|
placeholder="DD/MM/AAAA"
|
||||||
|
maxlength="10"
|
||||||
|
pattern="\d{2}/\d{2}/\d{4}"
|
||||||
|
title="Data no formato DD/MM/AAAA">
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
Por favor, insira uma data válida no formato DD/MM/AAAA.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="edit_data_efetivacao_oci" class="form-label">Data de Efetivação na OCI</label>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control date-mask"
|
||||||
|
id="edit_data_efetivacao_oci"
|
||||||
|
name="data_efetivacao_oci"
|
||||||
|
placeholder="DD/MM/AAAA"
|
||||||
|
maxlength="10"
|
||||||
|
pattern="\d{2}/\d{2}/\d{4}"
|
||||||
|
title="Data no formato DD/MM/AAAA">
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
Por favor, insira uma data válida no formato DD/MM/AAAA.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Contato -->
|
||||||
|
<div class="tab-pane fade" id="edit-contato">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="edit_telefone1" class="form-label">Telefone Principal</label>
|
||||||
|
<input type="text" class="form-control" id="edit_telefone1" name="telefone1">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="edit_telefone2" class="form-label">Telefone Alternativo</label>
|
||||||
|
<input type="text" class="form-control" id="edit_telefone2" name="telefone2">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Email Principal -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="edit_email" class="form-label">Email Principal</label>
|
||||||
|
<input type="email"
|
||||||
|
class="form-control"
|
||||||
|
id="edit_email"
|
||||||
|
name="email"
|
||||||
|
required>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
Por favor, insira um email válido.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Endereço -->
|
||||||
|
<div class="endereco-container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<label for="edit_cep" class="form-label">CEP</label>
|
||||||
|
<input type="text" class="form-control" id="edit_cep" name="cep">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<label for="edit_estado" class="form-label">Estado</label>
|
||||||
|
<select class="form-select" id="edit_estado" name="estado">
|
||||||
|
<option value="">Selecione...</option>
|
||||||
|
<!-- Estados serão carregados via JavaScript -->
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<label for="edit_cidade" class="form-label">Cidade</label>
|
||||||
|
<input type="text" class="form-control" id="edit_cidade" name="cidade">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<label for="edit_bairro" class="form-label">Bairro</label>
|
||||||
|
<input type="text" class="form-control" id="edit_bairro" name="bairro">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="edit_rua" class="form-label">Rua</label>
|
||||||
|
<input type="text" class="form-control" id="edit_rua" name="rua">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 mb-3">
|
||||||
|
<label for="edit_numero" class="form-label">Número</label>
|
||||||
|
<input type="text" class="form-control" id="edit_numero" name="numero">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="edit_complemento" class="form-label">Complemento</label>
|
||||||
|
<input type="text" class="form-control" id="edit_complemento" name="complemento">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Profissional -->
|
||||||
|
<div class="tab-pane fade" id="edit-profissional">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="edit_empresa" class="form-label">Empresa</label>
|
||||||
|
<input type="text" class="form-control" id="edit_empresa" name="empresa">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="edit_contratante" class="form-label">Contratante</label>
|
||||||
|
<input type="text" class="form-control" id="edit_contratante" name="contratante">
|
||||||
|
<small class="text-muted">Para terceirizados</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<!-- Dados Acadêmicos -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8 mb-3">
|
||||||
|
<label for="edit_instituicao_ensino" class="form-label">Instituição de Ensino</label>
|
||||||
|
<input type="text" class="form-control" id="edit_instituicao_ensino" name="instituicao_ensino">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<label for="edit_tipo_instituicao" class="form-label">Tipo</label>
|
||||||
|
<select class="form-select" id="edit_tipo_instituicao" name="tipo_instituicao">
|
||||||
|
<option value="">Selecione...</option>
|
||||||
|
<option value="Federal">Federal</option>
|
||||||
|
<option value="Estadual">Estadual</option>
|
||||||
|
<option value="Municipal">Municipal</option>
|
||||||
|
<option value="Privada">Privada</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Organização -->
|
||||||
|
<div class="tab-pane fade" id="edit-organizacao">
|
||||||
|
<!-- Dados Sindicais -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="edit_sindicato" class="form-label">Sindicato</label>
|
||||||
|
<input type="text" class="form-control" id="edit_sindicato" name="sindicato">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="edit_cargo_sindical" class="form-label">Cargo Sindical</label>
|
||||||
|
<input type="text" class="form-control" id="edit_cargo_sindical" name="cargo_sindical">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="edit_central_sindical" class="form-label">Central Sindical</label>
|
||||||
|
<input type="text" class="form-control" id="edit_central_sindical" name="central_sindical">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3 d-flex align-items-center">
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="checkbox" class="form-check-input" id="edit_dirigente_sindical" name="dirigente_sindical">
|
||||||
|
<label class="form-check-label" for="edit_dirigente_sindical">Dirigente Sindical</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<!-- Estado na Organização -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="edit_estado_militante" class="form-label">Estado</label>
|
||||||
|
<select class="form-select" id="edit_estado_militante" name="estado">
|
||||||
|
<option value="ATIVO">Ativo</option>
|
||||||
|
<option value="LICENCIADO">Licenciado</option>
|
||||||
|
<option value="SUSPENSO">Suspenso</option>
|
||||||
|
<option value="DESLIGADO">Desligado</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="edit_celula" class="form-label">Célula</label>
|
||||||
|
<select class="form-select" id="edit_celula" name="celula_id">
|
||||||
|
<option value="">Selecione...</option>
|
||||||
|
{% for celula in celulas %}
|
||||||
|
<option value="{{ celula.id }}">{{ celula.nome }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Responsabilidades -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label">Responsabilidades</label>
|
||||||
|
<div class="d-flex flex-wrap gap-2">
|
||||||
|
<span class="badge badge-clickable bg-secondary" data-value="{{ Militante.SECRETARIO }}" data-original-class="bg-secondary" title="Secretário">SEC</span>
|
||||||
|
<span class="badge badge-clickable bg-warning" data-value="{{ Militante.TESOUREIRO }}" data-original-class="bg-warning" title="Tesoureiro">TES</span>
|
||||||
|
<span class="badge badge-clickable bg-danger" data-value="{{ Militante.IMPRENSA }}" data-original-class="bg-danger" title="Imprensa">IMP</span>
|
||||||
|
<span class="badge badge-clickable bg-purple" data-value="{{ Militante.MNS }}" data-original-class="bg-purple" title="MNS">MNS</span>
|
||||||
|
<span class="badge badge-clickable bg-teal" data-value="{{ Militante.MPS }}" data-original-class="bg-teal" title="MPS">MPS</span>
|
||||||
|
<span class="badge badge-clickable bg-orange" data-value="{{ Militante.JUVENTUDE }}" data-original-class="bg-orange" title="Juventude">JUV</span>
|
||||||
|
<span class="badge badge-clickable bg-success" data-value="{{ Militante.QUADRO_ORIENTADOR }}" data-original-class="bg-success" title="Quadro-Orientador">QOR</span>
|
||||||
|
<span class="badge badge-clickable bg-primary" data-value="{{ Militante.RESPONSAVEL_FINANCAS }}" data-original-class="bg-primary" title="Responsável de Finanças">RFI</span>
|
||||||
|
<span class="badge badge-clickable bg-info" data-value="{{ Militante.RESPONSAVEL_IMPRENSA }}" data-original-class="bg-info" title="Responsável de Imprensa">RIM</span>
|
||||||
|
<span class="badge badge-clickable bg-dark" data-value="{{ Militante.ASPIRANTE }}" data-original-class="bg-dark" title="Aspirante">ASP</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-save me-2"></i>Salvar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Estilo para badges clicáveis */
|
||||||
|
.badge-clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
opacity: 0.7;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
min-width: 50px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-clickable:hover {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-clickable.active {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cores personalizadas para badges */
|
||||||
|
.bg-purple {
|
||||||
|
background-color: #6f42c1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-teal {
|
||||||
|
background-color: #20c997;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-orange {
|
||||||
|
background-color: #fd7e14;
|
||||||
|
}
|
||||||
|
|
||||||
|
.responsabilidades-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cores personalizadas para badges */
|
||||||
|
.bg-purple { background-color: #6f42c1 !important; color: white !important; }
|
||||||
|
.bg-teal { background-color: #20c997 !important; color: white !important; }
|
||||||
|
.bg-orange { background-color: #fd7e14 !important; color: white !important; }
|
||||||
|
.bg-indigo { background-color: #6610f2 !important; color: white !important; }
|
||||||
|
.bg-pink { background-color: #d63384 !important; color: white !important; }
|
||||||
|
|
||||||
|
/* Cores do Bootstrap que vamos usar */
|
||||||
|
.active.bg-primary { background-color: #0d6efd !important; color: white !important; }
|
||||||
|
.active.bg-success { background-color: #198754 !important; color: white !important; }
|
||||||
|
.active.bg-info { background-color: #0dcaf0 !important; color: white !important; }
|
||||||
|
.active.bg-danger { background-color: #dc3545 !important; color: white !important; }
|
||||||
|
.active.bg-dark { background-color: #212529 !important; color: white !important; }
|
||||||
|
|
||||||
|
/* Estilos para as tabs */
|
||||||
|
.nav-tabs {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs .nav-link {
|
||||||
|
border: none;
|
||||||
|
color: var(--bs-danger);
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs .nav-link:hover {
|
||||||
|
border: none;
|
||||||
|
color: var(--bs-danger);
|
||||||
|
background-color: rgba(var(--bs-danger-rgb), 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs .nav-link.active {
|
||||||
|
color: var(--bs-danger);
|
||||||
|
background-color: rgba(var(--bs-danger-rgb), 0.1);
|
||||||
|
border-bottom: 2px solid var(--bs-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Adicionar nav-fill para distribuir as abas igualmente */
|
||||||
|
.nav-tabs {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs .nav-item {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilos para o conteúdo das tabs */
|
||||||
|
.tab-content {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 0 0 0.25rem 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-pane {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const modalEditarMilitante = document.getElementById('modalEditarMilitante');
|
||||||
|
if (modalEditarMilitante) {
|
||||||
|
modalEditarMilitante.addEventListener('hidden.bs.modal', function() {
|
||||||
|
// Limpar formulário
|
||||||
|
const form = this.querySelector('form');
|
||||||
|
if (form) {
|
||||||
|
form.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limpar campos hidden
|
||||||
|
document.getElementById('edit_militante_id').value = '';
|
||||||
|
document.getElementById('responsabilidades_values').value = '0';
|
||||||
|
|
||||||
|
// Resetar badges
|
||||||
|
this.querySelectorAll('.badge-clickable').forEach(badge => {
|
||||||
|
badge.classList.remove('active');
|
||||||
|
const originalClass = badge.getAttribute('data-original-class');
|
||||||
|
if (originalClass) {
|
||||||
|
badge.className = `badge badge-clickable ${originalClass}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Limpar mensagens de erro
|
||||||
|
this.querySelectorAll('.is-invalid').forEach(field => {
|
||||||
|
field.classList.remove('is-invalid');
|
||||||
|
});
|
||||||
|
this.querySelectorAll('.invalid-feedback').forEach(feedback => {
|
||||||
|
feedback.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Voltar para a primeira aba
|
||||||
|
const firstTab = this.querySelector('button[data-bs-target="#edit-dados-basicos"]');
|
||||||
|
if (firstTab) {
|
||||||
|
firstTab.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
23
templates/modals/militante_excluir.html
Normal file
23
templates/modals/militante_excluir.html
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<!-- Modal de Confirmação de Exclusão -->
|
||||||
|
<div class="modal fade" id="deleteModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Confirmar Exclusão</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>Tem certeza que deseja excluir o militante <strong id="militanteNome"></strong>?</p>
|
||||||
|
<p class="text-danger mb-0">Esta ação não pode ser desfeita.</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||||
|
<form action="" method="POST" id="deleteForm" class="d-inline">
|
||||||
|
<button type="submit" class="btn btn-danger">
|
||||||
|
<i class="fas fa-trash me-2"></i>Excluir
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
303
templates/modals/militante_novo.html
Normal file
303
templates/modals/militante_novo.html
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
<!-- Modal de Novo Militante -->
|
||||||
|
<div class="modal fade" id="modalNovoMilitante" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">
|
||||||
|
<i class="fas fa-user-plus me-2"></i>Novo Militante
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="formNovoMilitante" method="post" action="{{ url_for('criar_militante') }}">
|
||||||
|
<!-- Nav tabs -->
|
||||||
|
<ul class="nav nav-tabs nav-fill mb-3" role="tablist">
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#tab-dados-basicos" type="button">
|
||||||
|
<i class="fas fa-user me-2"></i>Dados Básicos
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tab-contato" type="button">
|
||||||
|
<i class="fas fa-address-book me-2"></i>Contato
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tab-profissional" type="button">
|
||||||
|
<i class="fas fa-briefcase me-2"></i>Profissional
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tab-organizacao" type="button">
|
||||||
|
<i class="fas fa-users me-2"></i>Organização
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- Tab content -->
|
||||||
|
<div class="tab-content">
|
||||||
|
<!-- Dados Básicos -->
|
||||||
|
<div class="tab-pane fade show active" id="tab-dados-basicos">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="nome" class="form-label">Nome</label>
|
||||||
|
<input type="text" class="form-control" id="nome" name="nome" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="cpf" class="form-label">CPF</label>
|
||||||
|
<input type="text" class="form-control" id="cpf" name="cpf" required
|
||||||
|
pattern="\d{3}\.?\d{3}\.?\d{3}-?\d{2}"
|
||||||
|
title="Digite um CPF no formato: xxx.xxx.xxx-xx">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="titulo_eleitoral" class="form-label">Título Eleitoral</label>
|
||||||
|
<input type="text" class="form-control" id="titulo_eleitoral" name="titulo_eleitoral">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="data_nascimento" class="form-label">Data de Nascimento</label>
|
||||||
|
<input type="text" class="form-control date-mask" id="data_nascimento" name="data_nascimento"
|
||||||
|
placeholder="DD/MM/AAAA" maxlength="10">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="data_entrada" class="form-label">Data de Entrada OCI</label>
|
||||||
|
<input type="text" class="form-control date-mask" id="data_entrada" name="data_entrada_oci"
|
||||||
|
placeholder="DD/MM/AAAA" maxlength="10">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="data_efetivacao" class="form-label">Data de Efetivação</label>
|
||||||
|
<input type="text" class="form-control date-mask" id="data_efetivacao" name="data_efetivacao_oci"
|
||||||
|
placeholder="DD/MM/AAAA" maxlength="10">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Contato -->
|
||||||
|
<div class="tab-pane fade" id="tab-contato">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="telefone1" class="form-label">Telefone Principal</label>
|
||||||
|
<input type="text" class="form-control" id="telefone1" name="telefone1">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="telefone2" class="form-label">Telefone Alternativo</label>
|
||||||
|
<input type="text" class="form-control" id="telefone2" name="telefone2">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Email Principal -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="email" class="form-label">Email Principal</label>
|
||||||
|
<input type="email" class="form-control" id="email" name="email" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Endereço -->
|
||||||
|
<div class="endereco-container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<label for="cep" class="form-label">CEP</label>
|
||||||
|
<input type="text" class="form-control" id="cep" name="cep">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<label for="estado" class="form-label">Estado</label>
|
||||||
|
<select class="form-select" id="estado" name="estado">
|
||||||
|
<option value="">Selecione...</option>
|
||||||
|
<!-- Estados serão carregados via JavaScript -->
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<label for="cidade" class="form-label">Cidade</label>
|
||||||
|
<input type="text" class="form-control" id="cidade" name="cidade">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<label for="bairro" class="form-label">Bairro</label>
|
||||||
|
<input type="text" class="form-control" id="bairro" name="bairro">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="logradouro" class="form-label">Logradouro</label>
|
||||||
|
<input type="text" class="form-control" id="logradouro" name="logradouro">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 mb-3">
|
||||||
|
<label for="numero" class="form-label">Número</label>
|
||||||
|
<input type="text" class="form-control" id="numero" name="numero">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="complemento" class="form-label">Complemento</label>
|
||||||
|
<input type="text" class="form-control" id="complemento" name="complemento">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Profissional -->
|
||||||
|
<div class="tab-pane fade" id="tab-profissional">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="profissao" class="form-label">Profissão</label>
|
||||||
|
<input type="text" class="form-control" id="profissao" name="profissao">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="regime_trabalho" class="form-label">Regime de Trabalho</label>
|
||||||
|
<select class="form-select" id="regime_trabalho" name="regime_trabalho">
|
||||||
|
<option value="">Selecione...</option>
|
||||||
|
<option value="CLT">CLT</option>
|
||||||
|
<option value="Estatutário">Estatutário</option>
|
||||||
|
<option value="Terceirizado">Terceirizado</option>
|
||||||
|
<option value="Autônomo">Autônomo</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="empresa" class="form-label">Empresa</label>
|
||||||
|
<input type="text" class="form-control" id="empresa" name="empresa">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="contratante" class="form-label">Contratante</label>
|
||||||
|
<input type="text" class="form-control" id="contratante" name="contratante">
|
||||||
|
<small class="text-muted">Para terceirizados</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<!-- Dados Acadêmicos -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8 mb-3">
|
||||||
|
<label for="instituicao_ensino" class="form-label">Instituição de Ensino</label>
|
||||||
|
<input type="text" class="form-control" id="instituicao_ensino" name="instituicao_ensino">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<label for="tipo_instituicao" class="form-label">Tipo</label>
|
||||||
|
<select class="form-select" id="tipo_instituicao" name="tipo_instituicao">
|
||||||
|
<option value="">Selecione...</option>
|
||||||
|
<option value="Federal">Federal</option>
|
||||||
|
<option value="Estadual">Estadual</option>
|
||||||
|
<option value="Municipal">Municipal</option>
|
||||||
|
<option value="Privada">Privada</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Organização -->
|
||||||
|
<div class="tab-pane fade" id="tab-organizacao">
|
||||||
|
<!-- Dados Sindicais -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="sindicato" class="form-label">Sindicato</label>
|
||||||
|
<input type="text" class="form-control" id="sindicato" name="sindicato">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="cargo_sindical" class="form-label">Cargo Sindical</label>
|
||||||
|
<input type="text" class="form-control" id="cargo_sindical" name="cargo_sindical">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="central_sindical" class="form-label">Central Sindical</label>
|
||||||
|
<input type="text" class="form-control" id="central_sindical" name="central_sindical">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3 d-flex align-items-center">
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="checkbox" class="form-check-input" id="dirigente_sindical" name="dirigente_sindical">
|
||||||
|
<label class="form-check-label" for="dirigente_sindical">Dirigente Sindical</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<!-- Estado na Organização -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="estado_militante" class="form-label">Estado</label>
|
||||||
|
<select class="form-select" id="estado_militante" name="estado">
|
||||||
|
<option value="ATIVO">Ativo</option>
|
||||||
|
<option value="DESLIGADO">Desligado</option>
|
||||||
|
<option value="SUSPENSO">Suspenso</option>
|
||||||
|
<option value="AFASTADO">Afastado</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="celula" class="form-label">Célula</label>
|
||||||
|
<select class="form-select" id="celula" name="celula_id" required>
|
||||||
|
<option value="">Selecione...</option>
|
||||||
|
{% for celula in celulas %}
|
||||||
|
<option value="{{ celula.id }}">{{ celula.nome }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label">Responsabilidades</label>
|
||||||
|
<div class="responsabilidades-container">
|
||||||
|
<input type="hidden" name="responsabilidades" id="novo_responsabilidades_values" value="0">
|
||||||
|
|
||||||
|
<span class="badge badge-clickable bg-secondary" data-value="{{ Militante.SECRETARIO }}" data-bs-toggle="tooltip" title="Clique para alternar">Secretário</span>
|
||||||
|
|
||||||
|
<span class="badge badge-clickable bg-info" data-value="{{ Militante.RESPONSAVEL_IMPRENSA }}" data-bs-toggle="tooltip" title="Clique para alternar">Responsável de Imprensa</span>
|
||||||
|
|
||||||
|
<span class="badge badge-clickable bg-warning text-dark" data-value="{{ Militante.IMPRENSA }}" data-bs-toggle="tooltip" title="Clique para alternar">Imprensa</span>
|
||||||
|
|
||||||
|
<span class="badge badge-clickable bg-warning text-dark" data-value="{{ Militante.MPS }}" data-bs-toggle="tooltip" title="Clique para alternar">MPS</span>
|
||||||
|
|
||||||
|
<span class="badge badge-clickable bg-success" data-value="{{ Militante.QUADRO_ORIENTADOR }}" data-bs-toggle="tooltip" title="Clique para alternar">Quadro-Orientador</span>
|
||||||
|
|
||||||
|
<span class="badge badge-clickable bg-primary" data-value="{{ Militante.RESPONSAVEL_FINANCAS }}" data-bs-toggle="tooltip" title="Clique para alternar">Responsável de Finanças</span>
|
||||||
|
|
||||||
|
<span class="badge badge-clickable bg-dark" data-value="{{ Militante.TESOUREIRO }}" data-bs-toggle="tooltip" title="Clique para alternar">Tesoureiro</span>
|
||||||
|
|
||||||
|
<span class="badge badge-clickable bg-info" data-value="{{ Militante.MNS }}" data-bs-toggle="tooltip" title="Clique para alternar">MNS</span>
|
||||||
|
|
||||||
|
<span class="badge badge-clickable bg-danger" data-value="{{ Militante.JUVENTUDE }}" data-bs-toggle="tooltip" title="Clique para alternar">Juventude</span>
|
||||||
|
|
||||||
|
<span class="badge badge-clickable bg-light text-dark border" data-value="{{ Militante.ASPIRANTE }}" data-bs-toggle="tooltip" title="Clique para alternar">Aspirante</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||||
|
<button type="submit" form="formNovoMilitante" class="btn btn-primary">
|
||||||
|
<i class="fas fa-save me-2"></i>Salvar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.badge-clickable {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
margin: 0.3rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
opacity: 0.5;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-clickable:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-clickable.active {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.responsabilidades-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<button type="submit" class="btn btn-primary">Registrar</button>
|
<button type="submit" class="btn btn-primary">Registrar</button>
|
||||||
<a href="{{ url_for('listar_assinaturas') }}" class="btn btn-secondary">Voltar</a>
|
<a href="{{ url_for('listar_vendas_jornal') }}" class="btn btn-secondary">Voltar</a>
|
||||||
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
|
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
95
templates/novo_comprovante.html
Normal file
95
templates/novo_comprovante.html
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Novo Comprovante{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8 offset-md-2">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header bg-light">
|
||||||
|
<h4 class="card-title mb-0">
|
||||||
|
<i class="fas fa-money-bill-wave me-2"></i>Registrar Novo Comprovante
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post" class="needs-validation" novalidate>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="militante_id" class="form-label">Militante:</label>
|
||||||
|
<select class="form-select" id="militante_id" name="militante_id" required>
|
||||||
|
<option value="">Selecione um militante</option>
|
||||||
|
{% for militante in militantes %}
|
||||||
|
<option value="{{ militante.id }}">{{ militante.nome }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
Por favor, selecione um militante.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="tipo_comprovante_id">Tipo de Comprovante</label>
|
||||||
|
<select class="form-control" id="tipo_comprovante_id" name="tipo_comprovante_id" required>
|
||||||
|
<option value="1">1 - Comprovante Padrão</option>
|
||||||
|
{% if current_user.has_permission('gerenciar_tipos_comprovante') %}
|
||||||
|
<option value="2">2 - Comprovante Especial</option>
|
||||||
|
<option value="3">3 - Comprovante Extraordinário</option>
|
||||||
|
<option value="4">4 - Jornal Avulso</option>
|
||||||
|
<option value="5">5 - Assinatura de Jornal</option>
|
||||||
|
<option value="6">6 - Campanha Financeira</option>
|
||||||
|
{% endif %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="valor" class="form-label">Valor:</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text">R$</span>
|
||||||
|
<input type="text" class="form-control money" id="valor" name="valor" required>
|
||||||
|
</div>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
Por favor, informe um valor válido.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="data_comprovante" class="form-label">Data do Comprovante:</label>
|
||||||
|
<input type="date" class="form-control" id="data_comprovante" name="data_comprovante"
|
||||||
|
required max="{{ hoje }}">
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
Por favor, informe uma data válida.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-save me-1"></i>Registrar
|
||||||
|
</button>
|
||||||
|
<a href="{{ url_for('listar_comprovantes') }}" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-arrow-left me-1"></i>Voltar
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.mask/1.14.16/jquery.mask.min.js"></script>
|
||||||
|
<script>
|
||||||
|
$(document).ready(function(){
|
||||||
|
$('.money').mask('000.000.000.000.000,00', {reverse: true});
|
||||||
|
|
||||||
|
// Converter valor para formato aceito pelo backend
|
||||||
|
$('form').on('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const valor = $('#valor').val().replace(/\./g, '').replace(',', '.');
|
||||||
|
$('#valor').val(valor);
|
||||||
|
this.submit();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
{% extends 'base.html' %}
|
|
||||||
|
|
||||||
{% block title %}Novo Pagamento{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="container">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-8 offset-md-2">
|
|
||||||
<h1 class="mb-4">Novo Pagamento</h1>
|
|
||||||
|
|
||||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
|
||||||
{% if messages %}
|
|
||||||
{% for category, message in messages %}
|
|
||||||
<div class="alert alert-{{ category }}">{{ message }}</div>
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
{% endwith %}
|
|
||||||
|
|
||||||
<form method="post" class="mb-4">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="militante_id" class="form-label">Militante:</label>
|
|
||||||
<select class="form-select" id="militante_id" name="militante_id" required>
|
|
||||||
<option value="">Selecione o militante</option>
|
|
||||||
{% for militante in militantes %}
|
|
||||||
<option value="{{ militante.id }}">{{ militante.nome }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="tipo_pagamento" class="form-label">Tipo de Pagamento:</label>
|
|
||||||
<select class="form-select" id="tipo_pagamento" name="tipo_pagamento" required>
|
|
||||||
<option value="">Selecione o tipo</option>
|
|
||||||
<option value="cota">Cota</option>
|
|
||||||
<option value="jornal">Jornal</option>
|
|
||||||
<option value="assinatura">Assinatura</option>
|
|
||||||
<option value="campanha">Campanha Financeira</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="mes_referencia" class="form-label">Mês de Referência:</label>
|
|
||||||
<input type="month" class="form-control" id="mes_referencia" name="mes_referencia" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="numero_jornal" class="form-label">Número do Jornal:</label>
|
|
||||||
<input type="number" class="form-control" id="numero_jornal" name="numero_jornal">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="numero_inicial_assinatura" class="form-label">Número Inicial da Assinatura:</label>
|
|
||||||
<input type="number" class="form-control" id="numero_inicial_assinatura" name="numero_inicial_assinatura">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="numero_final_assinatura" class="form-label">Número Final da Assinatura:</label>
|
|
||||||
<input type="number" class="form-control" id="numero_final_assinatura" name="numero_final_assinatura">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="campanha_financeira" class="form-label">Campanha Financeira:</label>
|
|
||||||
<input type="text" class="form-control" id="campanha_financeira" name="campanha_financeira">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="valor" class="form-label">Valor:</label>
|
|
||||||
<input type="number" class="form-control" id="valor" name="valor" step="0.01" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="data_pagamento" class="form-label">Data do Pagamento:</label>
|
|
||||||
<input type="date" class="form-control" id="data_pagamento" name="data_pagamento" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-flex gap-2">
|
|
||||||
<button type="submit" class="btn btn-primary">Registrar</button>
|
|
||||||
<a href="{{ url_for('listar_pagamentos') }}" class="btn btn-secondary">Voltar</a>
|
|
||||||
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
{% block title %}Novo Relatório de Pagamentos{% endblock %}
|
{% block title %}Novo Relatório de Comprovantes{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<h1 class="mb-4">Novo Relatório de Pagamentos</h1>
|
<h1 class="mb-4">Novo Relatório de Comprovantes</h1>
|
||||||
|
|
||||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
@@ -44,10 +44,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="total_pagamentos" class="form-label">Total de Pagamentos</label>
|
<label for="total_comprovantes" class="form-label">Total de Comprovantes</label>
|
||||||
<input type="number" class="form-control" id="total_pagamentos" name="total_pagamentos" step="0.01" required>
|
<input type="number" class="form-control" id="total_comprovantes" name="total_comprovantes" step="0.01" required>
|
||||||
<div class="invalid-feedback">
|
<div class="invalid-feedback">
|
||||||
Por favor, insira o total de pagamentos.
|
Por favor, insira o total de comprovantes.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
|
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<button type="submit" class="btn btn-success">Registrar</button>
|
<button type="submit" class="btn btn-success">Registrar</button>
|
||||||
<a href="{{ url_for('listar_relatorios_pagamentos') }}" class="btn btn-outline-secondary">Voltar</a>
|
<a href="{{ url_for('listar_relatorios_comprovantes') }}" class="btn btn-outline-secondary">Voltar</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -3,11 +3,12 @@
|
|||||||
{% block title %}Novo Relatório de Cotas{% endblock %}
|
{% block title %}Novo Relatório de Cotas{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container">
|
<div class="container mt-4">
|
||||||
<div class="row">
|
<div class="card">
|
||||||
<div class="col-md-12">
|
<div class="card-header">
|
||||||
<h1 class="mb-4">Novo Relatório de Cotas</h1>
|
<h5 class="mb-0"><i class="fas fa-file-invoice-dollar me-2"></i>Novo Relatório de Cotas</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
{% for category, message in messages %}
|
{% for category, message in messages %}
|
||||||
@@ -20,7 +21,7 @@
|
|||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="setor_id" class="form-label">Setor</label>
|
<label for="setor_id" class="form-label">Setor</label>
|
||||||
<select class="form-select" id="setor_id" name="setor_id" required>
|
<select class="form-select" id="setor_id" name="setor_id" required>
|
||||||
<option value="">Selecione um setor</option>
|
<option value="">Selecione o setor</option>
|
||||||
{% for setor in setores %}
|
{% for setor in setores %}
|
||||||
<option value="{{ setor.id }}">{{ setor.nome }}</option>
|
<option value="{{ setor.id }}">{{ setor.nome }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -33,35 +34,53 @@
|
|||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="comite_id" class="form-label">Comitê Central</label>
|
<label for="comite_id" class="form-label">Comitê Central</label>
|
||||||
<select class="form-select" id="comite_id" name="comite_id" required>
|
<select class="form-select" id="comite_id" name="comite_id" required>
|
||||||
<option value="">Selecione um comitê</option>
|
<option value="">Selecione o comitê</option>
|
||||||
{% for comite in comites %}
|
{% for comite in comites %}
|
||||||
<option value="{{ comite.id }}">{{ comite.nome }}</option>
|
<option value="{{ comite.id }}">{{ comite.nome }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
<div class="invalid-feedback">
|
<div class="invalid-feedback">
|
||||||
Por favor, selecione o comitê central.
|
Por favor, selecione o comitê.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="total_cotas" class="form-label">Total de Cotas</label>
|
<label for="total_cotas" class="form-label">Total de Cotas</label>
|
||||||
<input type="number" class="form-control" id="total_cotas" name="total_cotas" step="0.01" required>
|
<div class="input-group">
|
||||||
|
<span class="input-group-text">R$</span>
|
||||||
|
<input type="number"
|
||||||
|
class="form-control"
|
||||||
|
id="total_cotas"
|
||||||
|
name="total_cotas"
|
||||||
|
step="0.01"
|
||||||
|
min="0.01"
|
||||||
|
required>
|
||||||
|
</div>
|
||||||
<div class="invalid-feedback">
|
<div class="invalid-feedback">
|
||||||
Por favor, insira o total de cotas.
|
Por favor, insira um valor válido para o total de cotas.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="data_relatorio" class="form-label">Data do Relatório</label>
|
<label for="data_relatorio" class="form-label">Data do Relatório</label>
|
||||||
<input type="date" class="form-control" id="data_relatorio" name="data_relatorio" required>
|
<input type="date"
|
||||||
|
class="form-control"
|
||||||
|
id="data_relatorio"
|
||||||
|
name="data_relatorio"
|
||||||
|
max="{{ hoje }}"
|
||||||
|
required>
|
||||||
<div class="invalid-feedback">
|
<div class="invalid-feedback">
|
||||||
Por favor, insira a data do relatório.
|
Por favor, insira uma data válida não futura.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<button type="submit" class="btn btn-success">Registrar</button>
|
<button type="submit" class="btn btn-success">
|
||||||
<a href="{{ url_for('listar_relatorios_cotas') }}" class="btn btn-outline-secondary">Voltar</a>
|
<i class="fas fa-save me-2"></i>Registrar
|
||||||
|
</button>
|
||||||
|
<a href="{{ url_for('listar_relatorios_cotas') }}" class="btn btn-outline-secondary">
|
||||||
|
<i class="fas fa-arrow-left me-2"></i>Voltar
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -73,20 +92,51 @@
|
|||||||
(function () {
|
(function () {
|
||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
var forms = document.querySelectorAll('.needs-validation')
|
const forms = document.querySelectorAll('.needs-validation');
|
||||||
|
|
||||||
Array.prototype.slice.call(forms)
|
forms.forEach(form => {
|
||||||
.forEach(function (form) {
|
form.addEventListener('submit', event => {
|
||||||
form.addEventListener('submit', function (event) {
|
if (!form.checkValidity()) {
|
||||||
if (!form.checkValidity()) {
|
event.preventDefault();
|
||||||
event.preventDefault()
|
event.stopPropagation();
|
||||||
event.stopPropagation()
|
}
|
||||||
}
|
|
||||||
|
// Validar valor mínimo
|
||||||
form.classList.add('was-validated')
|
const totalCotas = form.querySelector('#total_cotas');
|
||||||
}, false)
|
if (totalCotas.value <= 0) {
|
||||||
})
|
totalCotas.setCustomValidity('O valor deve ser maior que zero');
|
||||||
})()
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
} else {
|
||||||
|
totalCotas.setCustomValidity('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validar data não futura
|
||||||
|
const dataRelatorio = form.querySelector('#data_relatorio');
|
||||||
|
const hoje = new Date();
|
||||||
|
hoje.setHours(0, 0, 0, 0);
|
||||||
|
const dataSelecionada = new Date(dataRelatorio.value);
|
||||||
|
|
||||||
|
if (dataSelecionada > hoje) {
|
||||||
|
dataRelatorio.setCustomValidity('A data não pode ser futura');
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
} else {
|
||||||
|
dataRelatorio.setCustomValidity('');
|
||||||
|
}
|
||||||
|
|
||||||
|
form.classList.add('was-validated');
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
// Limpar validação ao mudar valor
|
||||||
|
const inputs = form.querySelectorAll('input, select');
|
||||||
|
inputs.forEach(input => {
|
||||||
|
input.addEventListener('input', () => {
|
||||||
|
input.setCustomValidity('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,12 @@
|
|||||||
{% block title %}Novo Relatório de Vendas{% endblock %}
|
{% block title %}Novo Relatório de Vendas{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container">
|
<div class="container mt-4">
|
||||||
<div class="row">
|
<div class="card">
|
||||||
<div class="col-md-12">
|
<div class="card-header">
|
||||||
<h1 class="mb-4">Novo Relatório de Vendas</h1>
|
<h5 class="mb-0"><i class="fas fa-file-invoice me-2"></i>Novo Relatório de Vendas</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
{% for category, message in messages %}
|
{% for category, message in messages %}
|
||||||
@@ -20,7 +21,7 @@
|
|||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="setor_id" class="form-label">Setor</label>
|
<label for="setor_id" class="form-label">Setor</label>
|
||||||
<select class="form-select" id="setor_id" name="setor_id" required>
|
<select class="form-select" id="setor_id" name="setor_id" required>
|
||||||
<option value="">Selecione um setor</option>
|
<option value="">Selecione o setor</option>
|
||||||
{% for setor in setores %}
|
{% for setor in setores %}
|
||||||
<option value="{{ setor.id }}">{{ setor.nome }}</option>
|
<option value="{{ setor.id }}">{{ setor.nome }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -33,35 +34,53 @@
|
|||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="comite_id" class="form-label">Comitê Central</label>
|
<label for="comite_id" class="form-label">Comitê Central</label>
|
||||||
<select class="form-select" id="comite_id" name="comite_id" required>
|
<select class="form-select" id="comite_id" name="comite_id" required>
|
||||||
<option value="">Selecione um comitê</option>
|
<option value="">Selecione o comitê</option>
|
||||||
{% for comite in comites %}
|
{% for comite in comites %}
|
||||||
<option value="{{ comite.id }}">{{ comite.nome }}</option>
|
<option value="{{ comite.id }}">{{ comite.nome }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
<div class="invalid-feedback">
|
<div class="invalid-feedback">
|
||||||
Por favor, selecione o comitê central.
|
Por favor, selecione o comitê.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="total_vendas" class="form-label">Total de Vendas</label>
|
<label for="total_vendas" class="form-label">Total de Vendas</label>
|
||||||
<input type="number" class="form-control" id="total_vendas" name="total_vendas" step="0.01" required>
|
<div class="input-group">
|
||||||
|
<span class="input-group-text">R$</span>
|
||||||
|
<input type="number"
|
||||||
|
class="form-control"
|
||||||
|
id="total_vendas"
|
||||||
|
name="total_vendas"
|
||||||
|
step="0.01"
|
||||||
|
min="0.01"
|
||||||
|
required>
|
||||||
|
</div>
|
||||||
<div class="invalid-feedback">
|
<div class="invalid-feedback">
|
||||||
Por favor, insira o total de vendas.
|
Por favor, insira um valor válido para o total de vendas.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="data_relatorio" class="form-label">Data do Relatório</label>
|
<label for="data_relatorio" class="form-label">Data do Relatório</label>
|
||||||
<input type="date" class="form-control" id="data_relatorio" name="data_relatorio" required>
|
<input type="date"
|
||||||
|
class="form-control"
|
||||||
|
id="data_relatorio"
|
||||||
|
name="data_relatorio"
|
||||||
|
max="{{ hoje }}"
|
||||||
|
required>
|
||||||
<div class="invalid-feedback">
|
<div class="invalid-feedback">
|
||||||
Por favor, insira a data do relatório.
|
Por favor, insira uma data válida não futura.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<button type="submit" class="btn btn-success">Registrar</button>
|
<button type="submit" class="btn btn-success">
|
||||||
<a href="{{ url_for('listar_relatorios_vendas') }}" class="btn btn-outline-secondary">Voltar</a>
|
<i class="fas fa-save me-2"></i>Registrar
|
||||||
|
</button>
|
||||||
|
<a href="{{ url_for('listar_relatorios_vendas') }}" class="btn btn-outline-secondary">
|
||||||
|
<i class="fas fa-arrow-left me-2"></i>Voltar
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -73,19 +92,50 @@
|
|||||||
(function () {
|
(function () {
|
||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
var forms = document.querySelectorAll('.needs-validation')
|
const forms = document.querySelectorAll('.needs-validation');
|
||||||
|
|
||||||
Array.prototype.slice.call(forms)
|
forms.forEach(form => {
|
||||||
.forEach(function (form) {
|
form.addEventListener('submit', event => {
|
||||||
form.addEventListener('submit', function (event) {
|
if (!form.checkValidity()) {
|
||||||
if (!form.checkValidity()) {
|
event.preventDefault();
|
||||||
event.preventDefault()
|
event.stopPropagation();
|
||||||
event.stopPropagation()
|
}
|
||||||
}
|
|
||||||
|
// Validar valor mínimo
|
||||||
form.classList.add('was-validated')
|
const totalVendas = form.querySelector('#total_vendas');
|
||||||
}, false)
|
if (totalVendas.value <= 0) {
|
||||||
})
|
totalVendas.setCustomValidity('O valor deve ser maior que zero');
|
||||||
})()
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
} else {
|
||||||
|
totalVendas.setCustomValidity('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validar data não futura
|
||||||
|
const dataRelatorio = form.querySelector('#data_relatorio');
|
||||||
|
const hoje = new Date();
|
||||||
|
hoje.setHours(0, 0, 0, 0);
|
||||||
|
const dataSelecionada = new Date(dataRelatorio.value);
|
||||||
|
|
||||||
|
if (dataSelecionada > hoje) {
|
||||||
|
dataRelatorio.setCustomValidity('A data não pode ser futura');
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
} else {
|
||||||
|
dataRelatorio.setCustomValidity('');
|
||||||
|
}
|
||||||
|
|
||||||
|
form.classList.add('was-validated');
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
// Limpar validação ao mudar valor
|
||||||
|
const inputs = form.querySelectorAll('input, select');
|
||||||
|
inputs.forEach(input => {
|
||||||
|
input.addEventListener('input', () => {
|
||||||
|
input.setCustomValidity('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
89
tests/test_routes.py
Normal file
89
tests/test_routes.py
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import pytest
|
||||||
|
from flask import url_for
|
||||||
|
from app import create_app
|
||||||
|
from functions.database import get_db_connection, init_database
|
||||||
|
import os
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def app():
|
||||||
|
app = create_app()
|
||||||
|
app.config['TESTING'] = True
|
||||||
|
app.config['WTF_CSRF_ENABLED'] = False
|
||||||
|
|
||||||
|
# Criar banco de dados temporário para testes
|
||||||
|
with app.app_context():
|
||||||
|
init_database()
|
||||||
|
|
||||||
|
yield app
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client(app):
|
||||||
|
return app.test_client()
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def runner(app):
|
||||||
|
return app.test_cli_runner()
|
||||||
|
|
||||||
|
def test_home_page(client):
|
||||||
|
response = client.get('/')
|
||||||
|
assert response.status_code == 302 # Redireciona para login
|
||||||
|
|
||||||
|
def test_login_page(client):
|
||||||
|
response = client.get('/login')
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert b'Login' in response.data
|
||||||
|
|
||||||
|
def test_listar_assinaturas_jornal(client):
|
||||||
|
# Primeiro fazer login
|
||||||
|
client.post('/login', data={
|
||||||
|
'username': 'admin',
|
||||||
|
'password': 'admin123'
|
||||||
|
})
|
||||||
|
|
||||||
|
response = client.get('/assinaturas/jornal')
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert b'Assinaturas de Jornal' in response.data
|
||||||
|
|
||||||
|
def test_nova_assinatura_jornal(client):
|
||||||
|
# Primeiro fazer login
|
||||||
|
client.post('/login', data={
|
||||||
|
'username': 'admin',
|
||||||
|
'password': 'admin123'
|
||||||
|
})
|
||||||
|
|
||||||
|
response = client.get('/assinaturas/jornal/novo')
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert b'Registrar Nova Assinatura Anual' in response.data
|
||||||
|
|
||||||
|
def test_listar_militantes(client):
|
||||||
|
# Primeiro fazer login
|
||||||
|
client.post('/login', data={
|
||||||
|
'username': 'admin',
|
||||||
|
'password': 'admin123'
|
||||||
|
})
|
||||||
|
|
||||||
|
response = client.get('/militantes')
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert b'Militantes' in response.data
|
||||||
|
|
||||||
|
def test_listar_cotas(client):
|
||||||
|
# Primeiro fazer login
|
||||||
|
client.post('/login', data={
|
||||||
|
'username': 'admin',
|
||||||
|
'password': 'admin123'
|
||||||
|
})
|
||||||
|
|
||||||
|
response = client.get('/cotas')
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert b'Cotas' in response.data
|
||||||
|
|
||||||
|
def test_listar_materiais(client):
|
||||||
|
# Primeiro fazer login
|
||||||
|
client.post('/login', data={
|
||||||
|
'username': 'admin',
|
||||||
|
'password': 'admin123'
|
||||||
|
})
|
||||||
|
|
||||||
|
response = client.get('/materiais')
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert b'Materiais' in response.data
|
||||||
171
utils/date_utils.py
Normal file
171
utils/date_utils.py
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
from datetime import datetime, date
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def validar_data(data_str: str, formato: str = '%Y-%m-%d') -> bool:
|
||||||
|
"""
|
||||||
|
Valida se uma string representa uma data válida no formato especificado.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data_str: String contendo a data
|
||||||
|
formato: Formato esperado da data (default: YYYY-MM-DD)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True se a data é válida, False caso contrário
|
||||||
|
"""
|
||||||
|
if not data_str:
|
||||||
|
return True
|
||||||
|
|
||||||
|
try:
|
||||||
|
datetime.strptime(data_str, formato)
|
||||||
|
return True
|
||||||
|
except ValueError as e:
|
||||||
|
logger.warning(f"Data inválida: {data_str} (formato esperado: {formato}). Erro: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def converter_data(data_str: str, formato_entrada: str = '%Y-%m-%d', formato_saida: str = None) -> date:
|
||||||
|
"""
|
||||||
|
Converte uma string de data para um objeto date.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data_str: String contendo a data
|
||||||
|
formato_entrada: Formato da data de entrada (default: YYYY-MM-DD)
|
||||||
|
formato_saida: Se especificado, retorna a data como string neste formato
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
date: Objeto date se formato_saida=None, string formatada caso contrário
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: Se a data for inválida
|
||||||
|
"""
|
||||||
|
if not data_str:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = datetime.strptime(data_str, formato_entrada).date()
|
||||||
|
|
||||||
|
if formato_saida:
|
||||||
|
return data.strftime(formato_saida)
|
||||||
|
return data
|
||||||
|
except ValueError as e:
|
||||||
|
logger.error(f"Erro ao converter data '{data_str}': {e}")
|
||||||
|
raise ValueError(f"Data inválida: {data_str}. Use o formato {formato_entrada}")
|
||||||
|
|
||||||
|
def validar_sequencia_datas(data_nascimento: date = None,
|
||||||
|
data_entrada: date = None,
|
||||||
|
data_efetivacao: date = None) -> None:
|
||||||
|
"""
|
||||||
|
Valida a sequência lógica entre datas.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data_nascimento: Data de nascimento
|
||||||
|
data_entrada: Data de entrada na OCI
|
||||||
|
data_efetivacao: Data de efetivação na OCI
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: Se houver inconsistência entre as datas
|
||||||
|
"""
|
||||||
|
hoje = date.today()
|
||||||
|
|
||||||
|
# Validar datas futuras
|
||||||
|
for nome, data in [
|
||||||
|
("Data de nascimento", data_nascimento),
|
||||||
|
("Data de entrada", data_entrada),
|
||||||
|
("Data de efetivação", data_efetivacao)
|
||||||
|
]:
|
||||||
|
if data and data > hoje:
|
||||||
|
logger.warning(f"{nome} no futuro: {data}")
|
||||||
|
raise ValueError(f"{nome} não pode ser no futuro")
|
||||||
|
|
||||||
|
# Validar sequência
|
||||||
|
if data_nascimento and data_entrada and data_nascimento > data_entrada:
|
||||||
|
logger.warning(f"Data de entrada ({data_entrada}) anterior à data de nascimento ({data_nascimento})")
|
||||||
|
raise ValueError("Data de entrada na OCI não pode ser anterior à data de nascimento")
|
||||||
|
|
||||||
|
if data_entrada and data_efetivacao and data_entrada > data_efetivacao:
|
||||||
|
logger.warning(f"Data de efetivação ({data_efetivacao}) anterior à data de entrada ({data_entrada})")
|
||||||
|
raise ValueError("Data de efetivação não pode ser anterior à data de entrada")
|
||||||
|
|
||||||
|
def calcular_idade(data_nascimento: date) -> int:
|
||||||
|
"""
|
||||||
|
Calcula a idade com base na data de nascimento.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data_nascimento: Data de nascimento
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: Idade em anos
|
||||||
|
"""
|
||||||
|
if not data_nascimento:
|
||||||
|
return None
|
||||||
|
|
||||||
|
hoje = date.today()
|
||||||
|
idade = hoje.year - data_nascimento.year
|
||||||
|
|
||||||
|
# Ajustar se ainda não fez aniversário este ano
|
||||||
|
if hoje.month < data_nascimento.month or \
|
||||||
|
(hoje.month == data_nascimento.month and hoje.day < data_nascimento.day):
|
||||||
|
idade -= 1
|
||||||
|
|
||||||
|
return idade
|
||||||
|
|
||||||
|
def converter_data_br(data_str):
|
||||||
|
"""Converte string de data no formato DD/MM/YYYY para objeto date"""
|
||||||
|
if not data_str:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
dia, mes, ano = map(int, data_str.split('/'))
|
||||||
|
return date(ano, mes, dia)
|
||||||
|
except (ValueError, TypeError) as e:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def converter_data_iso(data_str):
|
||||||
|
"""Converte string de data no formato YYYY-MM-DD para objeto date"""
|
||||||
|
if not data_str:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return datetime.strptime(data_str, '%Y-%m-%d').date()
|
||||||
|
except (ValueError, TypeError) as e:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def formatar_data_br(data):
|
||||||
|
"""Formata objeto date para string no formato DD/MM/YYYY"""
|
||||||
|
if not data:
|
||||||
|
return ''
|
||||||
|
if isinstance(data, str):
|
||||||
|
data = converter_data_iso(data) or converter_data_br(data)
|
||||||
|
if not data:
|
||||||
|
return ''
|
||||||
|
return data.strftime('%d/%m/%Y')
|
||||||
|
|
||||||
|
def formatar_data_iso(data):
|
||||||
|
"""Formata objeto date para string no formato YYYY-MM-DD"""
|
||||||
|
if not data:
|
||||||
|
return ''
|
||||||
|
if isinstance(data, str):
|
||||||
|
data = converter_data_br(data) or converter_data_iso(data)
|
||||||
|
if not data:
|
||||||
|
return ''
|
||||||
|
return data.strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
def validar_data(data, data_maxima=None, data_minima=None):
|
||||||
|
"""Valida se a data está dentro do intervalo permitido"""
|
||||||
|
if not data:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if isinstance(data, str):
|
||||||
|
data = converter_data_br(data) or converter_data_iso(data)
|
||||||
|
if not data:
|
||||||
|
return False
|
||||||
|
|
||||||
|
hoje = date.today()
|
||||||
|
|
||||||
|
if data_maxima and data > data_maxima:
|
||||||
|
return False
|
||||||
|
if data_minima and data < data_minima:
|
||||||
|
return False
|
||||||
|
if data > hoje:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
Reference in New Issue
Block a user