84 Commits

Author SHA1 Message Date
andersonid
e6057cd566 fix(#11): Corrige inicialização do sistema para não recriar usuários a cada execução 2025-04-15 15:10:21 -03:00
andersonid
8255f1d933 feat(#11): Adiciona módulo create_test_users 2025-04-15 15:09:03 -03:00
andersonid
0f32eae5cf refactor(#11): Integra listagem de usuários no dashboard 2025-04-15 14:26:02 -03:00
andersonid
47f13e7c18 feat(#11): Implementa estrutura inicial da área administrativa - Cria blueprint administrativo com rotas básicas - Implementa templates base para área administrativa - Adiciona dashboard administrativo - Implementa gerenciamento de usuários - Organiza rotas em pacote separado 2025-04-15 10:49:15 -03:00
andersonid
53769cf080 Adicionado parâmetro de versão aleatório no CSS para evitar cache 2025-04-15 10:21:41 -03:00
andersonid
92bc21dbd8 Tela inicial de administração desenvolvida. 2025-04-15 10:19:59 -03:00
andersonid
5057802220 tela de administração em ajustes 2025-04-13 22:48:27 -03:00
andersonid
e43b089155 fix: Correções na página de administração e suas dependências 2025-04-13 22:30:05 -03:00
LS
295a433d59 fix: remove debug duplicado na chamada de app.run() 2025-04-09 11:23:48 -03:00
LS
203751deeb feat: adiciona classes Controle, Notificacao e Relatorio para gerenciamento do sistema 2025-04-09 11:21:45 -03:00
LS
71f926e6be fix: corrige erro de sintaxe na chamada de app.run() 2025-04-09 10:12:05 -03:00
LS
8cef19576e fix: adiciona use_alter=True e nomes específicos para chaves estrangeiras circulares 2025-04-09 10:09:31 -03:00
andersonid
abc46704c3 Corrigir atualização de dados na tabela de militantes 2025-04-09 09:59:41 -03:00
andersonid
c640a756df chore: remove arquivos não utilizados do projeto - Remoção de scripts obsoletos (seed.py, create_test_users.py) e arquivos de configuração não utilizados (setup.py, models.py) após análise completa de dependências 2025-04-09 09:59:41 -03:00
andersonid
3f2e6e3022 fix: corrige validação e salvamento do formulário de edição de militante - Corrige validação do email, ajusta conversão de datas, corrige CSRF token e melhora feedback visual 2025-04-09 09:59:41 -03:00
LS
179ea3cad0 resolvido merge com nova ui 2025-04-09 09:59:12 -03:00
andersonid
b47c9efc21 Melhorias na lógica de ativação de badges e atualização de responsabilidades 2025-04-09 09:54:59 -03:00
andersonid
97711d30c7 fix: corrige campos de data no modal de novo militante 2025-04-09 09:54:59 -03:00
andersonid
50ef370c2b fix: corrige comportamento dos campos de data para manter calendário e formato brasileiro 2025-04-09 09:54:59 -03:00
andersonid
53594517c0 fix: ajusta formato de data para padrão brasileiro (DD/MM/AAAA) 2025-04-09 09:54:59 -03:00
andersonid
874df1d340 feat: melhora visualização do formato de data nos formulários 2025-04-09 09:54:59 -03:00
LS
b170f94058 fix: adiciona Faker como dependência para geração de dados de teste 2025-04-04 18:11:24 -03:00
LS
786040162b fix: configura Flask para produção com gunicorn e ajusta Dockerfile para Coolify 2025-04-04 18:07:04 -03:00
LS
daaa7fd462 feat: atualiza Dockerfile para incluir dependências necessárias 2025-04-04 18:04:23 -03:00
LS
ad0ea2f259 refactor: atualiza Dockerfile para usar Alpine Linux e corrige instalação do Python 2025-04-04 17:55:58 -03:00
Levy Sant'Anna
74e5a1f7e3 Update Dockerfile
changed fedora version to latest
2025-04-04 17:50:17 -03:00
LS
d07a227e80 docker compose 2025-04-04 17:43:34 -03:00
Levy Sant'Anna
0635003485 Update Dockerfile 2025-04-04 17:30:43 -03:00
LS
d931fb4b5e Dockerfile 2025-04-04 15:21:39 -03:00
andersonid
a302a259a6 feat: implementa paginação e contagem correta na lista de militantes 2025-04-04 13:31:52 -03:00
andersonid
75ba696355 feat: adiciona modal global para edição de militantes na home 2025-04-04 13:02:23 -03:00
andersonid
7f4fe77711 fix: corrige estilos dos botões nos modais para manter cores e visibilidade no hover 2025-04-04 13:01:22 -03:00
andersonid
c29eed0c69 refactor: Remove filtro de Filiados/Não Filiados da listagem de militantes 2025-04-04 12:22:05 -03:00
andersonid
52a6bf9eb0 fix: Atualiza valores dos checkboxes de responsabilidades no template do modal 2025-04-04 12:16:41 -03:00
andersonid
d468f8ff39 fix: Corrige processamento das responsabilidades no modal de edição dos militantes 2025-04-04 12:15:19 -03:00
andersonid
5527db8729 chore: Ignora arquivos de QR code gerados dinamicamente 2025-04-04 11:39:15 -03:00
andersonid
56b8e7aa54 feat: Melhorias de segurança e interface - Segurança: Implementação de CSRF token em formulários, validação no backend, proteção AJAX - QR Code: Preservação do otp_secret, evita geração desnecessária - Interface: Correções visuais, padronização de cores, melhorias em formulários 2025-04-04 11:37:48 -03:00
andersonid
9ffc562357 feat: implementa filtros e pesquisa AJAX na interface de militantes 2025-04-04 09:49:06 -03:00
andersonid
3ed3002410 style: padroniza altura dos botões na interface de militantes 2025-04-04 09:48:14 -03:00
andersonid
f58c340235 chore: remove arquivo .pyc do controle de versão 2025-04-04 09:26:22 -03:00
andersonid
9158a86655 chore: atualiza QR code do admin 2025-04-04 09:25:08 -03:00
andersonid
6b23adcb34 chore: atualiza Makefile para melhor limpeza e inicialização do ambiente 2025-04-04 09:24:56 -03:00
andersonid
c7c3b95f0b refactor: melhora processo de seed de dados com melhor tratamento de erros e concorrência 2025-04-04 09:24:45 -03:00
andersonid
9bb62c81a7 fix: ajusta configurações do SQLite para melhor concorrência e tratamento de locks 2025-04-04 09:24:34 -03:00
andersonid
c17a3eaa0f fix: corrige exibição das responsabilidades dos militantes na interface /militantes 2025-04-04 09:23:45 -03:00
andersonid
07605797d1 . 2025-04-04 02:35:54 -03:00
andersonid
745803fef3 fix: corrige problemas de permissões e rotas 2025-04-04 02:34:51 -03:00
andersonid
241543ea63 feat: melhorias na interface da home e navbar - Ajustes no layout da navbar e menu - Correção do logo e nome do sistema - Melhorias no estilo dos cards da dashboard - Ajustes nas permissões e autenticação - Correção de bugs na exibição de mensagens 2025-04-03 20:58:02 -03:00
andersonid
50516664e4 feat: melhoria no layout da tela de login - ajustes no container, responsividade e estilização dos campos 2025-04-03 20:29:07 -03:00
andersonid
0447524a91 feat: padroniza tela de pagamentos - Adiciona DataTables, modais e funcionalidades CRUD 2025-04-03 18:28:03 -03:00
andersonid
77cf5ad99c fix: corrige edição de cotas - Ajusta exibição do militante e valores no modal de edição 2025-04-03 18:28:03 -03:00
andersonid
9cc3f408f8 feat: padroniza componentes e estilos globais - Cria components.css, ajusta cores e organiza modais 2025-04-03 18:27:37 -03:00
andersonid
758dbdb26d fix: corrige funcionamento do modal de edição de militantes - Corrige bloco de scripts e melhora tratamento de dados 2025-04-03 18:26:41 -03:00
andersonid
83ae798033 fix: corrige fluxo de modais na home - Ajusta modal de militantes para abrir detalhes primeiro - Corrige preenchimento de dados nos modais - Mantém fluxo: detalhes -> editar/excluir 2025-04-03 18:24:57 -03:00
andersonid
742f820bc2 style: melhora layout e espaçamento das interfaces - Ajusta margens e padding do container principal - Adiciona page-wrapper com espaçamento consistente - Corrige cores do menu para melhor legibilidade - Implementa responsividade para diferentes tamanhos de tela - Mantém estrutura consistente em todas as páginas 2025-04-03 18:24:07 -03:00
andersonid
a28f543478 refactor: melhorias na interface e funcionalidades - Atualização do layout do dashboard com Bootstrap 5 - Remoção do template editar_pagamento.html (integrado ao modal) - Melhorias no template home.html com cards estatísticos - Ajustes nos estilos e responsividade - Correções nas rotas e conexões do banco de dados - Implementação do modal de edição de pagamentos - Adição de efeitos hover e melhorias visuais 2025-04-03 18:23:08 -03:00
andersonid
417b5c3f96 fix: restaura layout da dashboard e corrige exibição das listas 2025-04-03 18:17:46 -03:00
andersonid
10ff9cab3b feat: Melhorias na interface do modal de detalhes do militante - Adicionado efeito de blur consistente em todos os modais - Corrigido estilo do botão de editar para manter a cor azul padrão - Adicionado backdrop-filter no modal de confirmação de exclusão - Melhorias na responsividade dos modais para dispositivos móveis - Ajustado comportamento de fechamento dos modais 2025-04-03 17:59:52 -03:00
andersonid
8803c971e4 feat: Melhorias no Dashboard
- Interface:
  - Adicionada saudação personalizada com nome do usuário
  - Melhorado formato da data em português
  - Ajustado layout do header com gradiente e sombra
  - Corrigida categoria de mensagens flash de 'error' para 'danger'

- Card de Cotas:
  - Reorganizado layout para melhor exibição de valores grandes
  - Ajustado tamanho da fonte usando calc(1.2rem + 0.8vw)
  - Adicionado container específico para valor com min-width: 0
  - Redimensionado e reposicionado ícone
  - Melhorado espaçamento e alinhamento

- Lista de Militantes:
  - Ajustada query para ordenar por ID
  - Removida dependência da coluna created_at
  - Adicionado ID do militante na listagem

- Estilos:
  - Adicionadas classes valor-container e icon-container
  - Melhorado responsividade dos valores monetários
  - Ajustado gradiente no header de boas-vindas
  - Refinado espaçamento e margens dos componentes
2025-04-03 17:59:41 -03:00
andersonid
d4869dcfaa feat: adiciona script de dados fictícios e padroniza tratamento de erros 2025-04-03 17:56:44 -03:00
andersonid
06e7c79488 refactor: padroniza tratamento de erro na listagem de militantes 2025-04-03 17:54:29 -03:00
andersonid
0a2d5c1d23 fix: corrige tratamento de erro na listagem de cotas 2025-04-03 17:54:29 -03:00
andersonid
855f97c72b feat: moderniza a página de listagem de cotas 2025-04-03 17:54:25 -03:00
andersonid
8e6ccb70e9 fix: corrige blocos não fechados no template de listagem de militantes 2025-04-03 17:53:24 -03:00
andersonid
65406276ae feat: adiciona hover vermelho nos menus e centraliza data no mobile 2025-04-03 17:53:24 -03:00
andersonid
b1acc2fdfc fix: corrige referências dos logos de acordo com o fundo 2025-04-03 17:53:24 -03:00
andersonid
c44ce94bef refactor: simplifica e moderniza a tela de login 2025-04-03 17:53:17 -03:00
andersonid
ce3b5a4231 refactor: melhorias na UI - formulário de pagamento e cards 2025-04-03 17:52:30 -03:00
andersonid
f0faf4270b refactor: melhorias na UI - navbar, logo e layout da data 2025-04-03 17:51:27 -03:00
andersonid
178a58bb00 fix: correções no logo, remoção de mensagem de login e auto-dismiss de alertas 2025-04-03 17:49:42 -03:00
andersonid
e9c1f3aedf fix: correções no logo, degradê e alertas 2025-04-03 17:48:47 -03:00
andersonid
1ff8e97bbc fix: ajustes visuais - degradê, card branco, logo e barra vermelha 2025-04-03 17:48:41 -03:00
andersonid
b815f77240 refactor: melhorias na interface de login e alertas, foco em mobile-first 2025-04-03 17:47:05 -03:00
andersonid
ba4f6d6de3 fix: ajustes no logo e nome do sistema em todas as interfaces 2025-04-03 17:44:44 -03:00
andersonid
ac461ce800 fix: removido Flask-Moment e adicionada data formatada em português 2025-04-03 17:41:29 -03:00
andersonid
4f781b2a0e refactor: ajustes na interface - logo, nome do sistema e layout da home 2025-04-03 17:41:29 -03:00
andersonid
32cd4b70c1 feat: atualização da identidade visual com cores e logo da OCI 2025-04-03 17:41:29 -03:00
andersonid
54261e455c feat: melhorias na interface e estrutura do frontend 2025-04-03 17:41:29 -03:00
LS
9d17c66c46 fix: corrige uso do decorator require_instance_permission - Modifica o decorator para aceitar o nome do parâmetro da instância - Atualiza as rotas de pagamentos para usar o decorator corretamente - Adiciona verificação do ID da instância nos argumentos da função - Melhora mensagens de erro para casos de permissão negada 2025-04-03 17:10:43 -03:00
LS
cbaf227e58 feat: implementa sistema de responsabilidades e instâncias - Adiciona responsabilidades de Finanças e Imprensa para todas as instâncias - Cria templates genéricos para gerenciamento de instâncias - Implementa sistema de permissões baseado em RBAC - Adiciona status de Aspirante com avaliação obrigatória - Atualiza documentação com novas regras e responsabilidades - Cria testes para validação das permissões - Adiciona migração para novos campos no banco de dados 2025-04-03 15:58:07 -03:00
LS
8dac8dc234 Limapdo o repo e corrigidos bugs 2025-04-03 11:24:47 -03:00
LS
bf93e84cec Adicionados os campos mínimos do Banco de Dados, precisa melhorar interface e controle de acesso que será posterior nessa branch mesmo 2025-04-03 10:30:48 -03:00
LS
449a203926 feat rbac com adicionados os novos campos para hierarquia 2025-04-01 15:27:16 -03:00
Levy Sant'Anna
01f5901eb2 Merge pull request #22 from ComunaTec/login
Login
2025-03-31 12:03:24 -03:00
123 changed files with 18109 additions and 2169 deletions

50
.dockerignore Normal file
View 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
View File

@@ -263,3 +263,10 @@ database.db
admin_qr.png
# 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
View 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"]

View File

@@ -2,11 +2,22 @@ install:
pip install -r requirements.txt
clean:
rm -rf ~/.local/share/controles/database.db
rm -rf ~/.local/share/controles/database.db*
rm -f admin_qr.png
run: clean
python app.py
init-db: clean
python init_db.py
seed: init-db
python seed.py
init:
python app.py --init
run:
python app.py
run-with-seed: seed init run
reset-admin: clean
python create_admin.py
python create_admin.py

161
README.md
View File

@@ -1,43 +1,134 @@
# Sistema de Controles
# Sistema de Controle de Militantes
## Para instalar
Sistema para gerenciamento de militantes, células, setores e comitês regionais.
```bash
make install
## Estrutura de Permissões (RBAC)
O sistema utiliza um sistema de controle de acesso baseado em papéis (RBAC) com a seguinte hierarquia:
### Níveis de Papéis
1. **Militante Básico** (Nível 1)
- Visualizar próprios dados
- Editar próprios dados
- Visualizar dados da célula
2. **Secretário de Célula** (Nível 2)
- Todas as permissões do Militante Básico
- Gerenciar membros da célula
- Criar membros na célula
- Visualizar relatórios da célula
3. **Membro de Setor** (Nível 3)
- Todas as permissões do Secretário de Célula
- Visualizar relatórios do setor
4. **Secretário de Setor** (Nível 4)
- Todas as permissões do Membro de Setor
- Gerenciar células do setor
- Criar células no setor
5. **Membro de CR** (Nível 5)
- Todas as permissões do Secretário de Setor
- Visualizar relatórios do CR
6. **Secretário de CR** (Nível 6)
- Todas as permissões do Membro de CR
- Gerenciar setores do CR
- Criar setores no CR
7. **Membro do CC** (Nível 7)
- Todas as permissões do Secretário de CR
- Visualizar relatórios nacionais
8. **Secretário Geral** (Nível 8)
- Todas as permissões do Membro do CC
- Gerenciar CRs
- Criar CRs
- Configurar sistema
## Instalação
1. Clone o repositório
2. Crie um ambiente virtual:
```bash
python -m venv venv
source venv/bin/activate # Linux/Mac
# ou
venv\Scripts\activate # Windows
```
3. Instale as dependências:
```bash
pip install -r requirements.txt
```
4. Execute as migrações do banco de dados:
```bash
python sql/migrate_db.py
```
5. Configure as variáveis de ambiente no arquivo `.env`:
```
FLASK_APP=app.py
FLASK_ENV=development
SECRET_KEY=sua_chave_secreta
MAIL_SERVER=seu_servidor_smtp
MAIL_PORT=587
MAIL_USE_TLS=True
MAIL_USERNAME=seu_email
MAIL_PASSWORD=sua_senha
```
6. Execute o aplicativo:
```bash
flask run
```
## Uso
### Decoradores de Permissão
O sistema fornece três decoradores para controle de acesso:
1. `@require_permission(permission_name)`
- Verifica se o usuário tem uma permissão específica
- Exemplo: `@require_permission('create_cell_member')`
2. `@require_role(role_name)`
- Verifica se o usuário tem um papel específico
- Exemplo: `@require_role('Secretário de Célula')`
3. `@require_minimum_role(min_level)`
- Verifica se o usuário tem um papel com nível mínimo
- Exemplo: `@require_minimum_role(Role.SECRETARIO_CR)`
### Verificando Permissões no Código
```python
# Verificar se um usuário tem uma permissão
if user.has_permission('create_cell_member'):
# Faça algo
# Verificar se um usuário tem um papel
if user.has_role('Secretário de Célula'):
# Faça algo
# Obter o papel mais alto do usuário
highest_role = user.get_highest_role()
if highest_role and highest_role.nivel >= Role.SECRETARIO_CR:
# Faça algo
```
## Sobre o QR Code de Autenticação (admin_qr.png)
## Estrutura do Banco de Dados
O arquivo `admin_qr.png` é um QR code gerado automaticamente para configuração da autenticação de dois fatores (2FA) do usuário administrador. Este arquivo é:
O sistema utiliza as seguintes tabelas para o RBAC:
- Gerado na raiz do projeto quando:
- O comando `make reset-admin` é executado
- O servidor é iniciado com `make run` e existe um usuário admin
- Um novo usuário é criado através da interface web
- `roles`: Armazena os papéis disponíveis
- `permissions`: Armazena as permissões disponíveis
- `role_permissions`: Mapeia papéis para permissões
- `user_roles`: Mapeia usuários para papéis
- Usado para:
- Configurar a autenticação 2FA no aplicativo autenticador (Google Authenticator, Microsoft Authenticator, etc.)
- Gerar os códigos OTP necessários para fazer login no sistema
## Segurança
### Importante:
- O QR code é atualizado sempre que um novo usuário é criado ou quando o sistema é reiniciado
- Cada QR code é único e corresponde ao segredo OTP atual do usuário
- Se você recriar o banco de dados ou resetar o admin, será necessário reconfigurar o aplicativo autenticador com o novo QR code
- Mantenha este arquivo seguro, pois ele contém informações sensíveis de autenticação
### Como usar:
1. Instale um aplicativo autenticador no seu celular (Google Authenticator, Microsoft Authenticator, etc.)
2. Escaneie o QR code (`admin_qr.png`) com o aplicativo
3. O aplicativo irá gerar códigos de 6 dígitos a cada 30 segundos
4. Use estes códigos junto com seu usuário e senha para fazer login no sistema
### Comandos relacionados:
```bash
# Limpar banco e criar novo admin com novo QR code
make reset-admin
# Iniciar o servidor (também gera novo QR code se necessário)
make run
```
Acesse por: http://127.0.0.1:5000
- Todas as senhas são armazenadas com hash bcrypt
- Sessões expiram após período de inatividade
- Controle de acesso granular baseado em papéis
- Proteção contra CSRF
- Validação de entrada de dados

2182
app.py

File diff suppressed because it is too large Load Diff

View File

@@ -1 +0,0 @@
SECRET_KEY = 'sua_chave_secreta_aqui' # Use uma chave segura em produção

View File

@@ -1,4 +1,4 @@
from functions.database import init_database, Usuario, get_db_connection
from functions.database import init_database, Usuario, Role, get_db_connection
import qrcode
import os
from pathlib import Path
@@ -14,8 +14,6 @@ def generate_qr_code(user):
Returns:
Path: Caminho do arquivo QR code gerado
"""
import qrcode
# Gerar QR Code apenas na raiz do projeto
qr_path = Path('admin_qr.png')
@@ -25,112 +23,129 @@ def generate_qr_code(user):
# Gerar e salvar QR Code
qr = qrcode.QRCode(version=1, box_size=10, border=5)
qr.add_data(user.get_otp_uri())
# Gerar URI do OTP
totp = pyotp.TOTP(user.otp_secret)
otp_uri = totp.provisioning_uri(
name=user.username,
issuer_name="Sistema de Controles"
)
qr.add_data(otp_uri)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
img.save(str(qr_path))
print(f"\nQR Code gerado em: {os.path.abspath(qr_path)}")
return qr_path
return qr_path, otp_uri
def create_admin_user():
"""Cria ou atualiza o usuário admin"""
try:
# Inicializar o banco de dados
# Inicializar banco de dados
init_database()
# Obter a sessão
db_session = get_db_connection()
# Criar sessão
db = get_db_connection()
# Verificar se o admin foi criado
admin = db_session.query(Usuario).filter_by(username="admin").first()
if admin:
print("\n=== Usuário Admin Encontrado ===")
# Atualizar QR code mesmo se o usuário já existir
qr_path = generate_qr_code(admin)
else:
print("\n=== Criando Novo Usuário Admin ===")
# Criar usuário admin com novo segredo OTP
admin = Usuario(
username="admin",
password="admin123",
is_admin=True
)
admin.email = "admin@example.com"
try:
# Verificar se já existe um usuário admin
admin = db.query(Usuario).filter_by(username="admin").first()
# Adicionar e fazer commit para obter o ID
db_session.add(admin)
db_session.commit()
if admin:
print("\n=== Usuário Admin Encontrado ===")
if not admin.otp_secret:
print("Gerando novo segredo OTP...")
admin.generate_otp_secret()
db.commit()
else:
print("\n=== Criando Novo Usuário Admin ===")
# Criar novo usuário admin
admin = Usuario(
username="admin",
email="admin@example.com",
is_admin=True
)
admin.set_password("admin123")
admin.generate_otp_secret()
# Buscar ou criar role de admin
admin_role = db.query(Role).filter_by(nome="admin").first()
if not admin_role:
admin_role = Role(nome="admin", nivel=0) # Nível 0 é o mais alto
db.add(admin_role)
# Adicionar role ao usuário
admin.roles.append(admin_role)
# Adicionar e fazer commit
db.add(admin)
db.commit()
# Gerar QR code apenas se solicitado ou se for novo usuário
if not os.path.exists('admin_qr.png'):
qr_path, otp_uri = generate_qr_code(admin)
print("\n=== QR Code Gerado ===")
print(f"QR Code salvo em: {qr_path}")
print(f"URI do OTP: {otp_uri}")
else:
print("\n=== QR Code Existente ===")
print("Usando QR Code existente em: admin_qr.png")
qr_path = 'admin_qr.png'
# Mostrar informações
print("\n=== Informações do Admin ===")
print(f"Username: {admin.username}")
print(f"Email: {admin.email}")
print(f"Senha: admin123")
print(f"Segredo OTP: {admin.otp_secret}")
# Gerar código atual para verificação
totp = pyotp.TOTP(admin.otp_secret)
current_code = totp.now()
print("\n=== Verificação do OTP ===")
print(f"Código OTP atual: {current_code}")
print(f"Verificação do código: {totp.verify(current_code)}")
print("\n=== Instruções para Configuração ===")
print("1. Instale um aplicativo autenticador no seu celular")
print(" (Google Authenticator, Microsoft Authenticator, etc)")
print("2. Abra o aplicativo")
print("3. Selecione a opção para adicionar uma nova conta")
print("4. Escaneie o QR Code salvo em:", qr_path)
print("\nOU configure manualmente:")
print(f"- Nome da conta: {admin.username}")
print(f"- Segredo: {admin.otp_secret}")
print("- Tipo: Baseado em tempo (TOTP)")
print("- Algoritmo: SHA1")
print("- Dígitos: 6")
print("- Intervalo: 30 segundos")
# Verificação final
print("\n=== Teste de Verificação ===")
test_code = totp.now()
print(f"Código de teste: {test_code}")
is_valid = admin.verify_otp(test_code)
print(f"Verificação do código: {'Sucesso' if is_valid else 'Falha'}")
if not is_valid:
print("\nALERTA: Verificação do OTP falhou!")
print("Por favor, verifique se o segredo OTP está correto.")
# Fazer commit final para garantir que tudo foi salvo
db.commit()
except Exception as e:
db.rollback()
raise e
finally:
db.close()
# Recarregar o usuário do banco para garantir dados atualizados
admin = db_session.query(Usuario).filter_by(username="admin").first()
# Verificar e mostrar informações do OTP
print("\n=== Informações do Usuário Admin ===")
print(f"Username: admin")
print(f"Senha: admin123")
print(f"Email: {admin.email}")
print(f"Segredo OTP atual: {admin.otp_secret}")
# Gerar URI do OTP usando o segredo armazenado
totp = pyotp.TOTP(admin.otp_secret)
otp_uri = totp.provisioning_uri(
name=admin.username,
issuer_name="Sistema de Controles"
)
# Usar a função extraída para gerar o QR code
qr_path = generate_qr_code(admin)
print("\n=== QR Code Gerado ===")
print(f"QR Code salvo em: {qr_path}")
print(f"URI do OTP: {otp_uri}")
# Gerar código atual para verificação
current_code = totp.now()
print("\n=== Verificação do OTP ===")
print(f"Código OTP atual: {current_code}")
print(f"Verificação do código: {totp.verify(current_code)}")
print("\n=== Instruções para Configuração ===")
print("1. Instale um aplicativo autenticador no seu celular")
print(" (Google Authenticator, Microsoft Authenticator, etc)")
print("2. Abra o aplicativo")
print("3. Selecione a opção para adicionar uma nova conta")
print("4. Escaneie o QR Code salvo em:", qr_path)
print("\nOU configure manualmente:")
print(f"- Nome da conta: {admin.username}")
print(f"- Segredo: {admin.otp_secret}")
print("- Tipo: Baseado em tempo (TOTP)")
print("- Algoritmo: SHA1")
print("- Dígitos: 6")
print("- Intervalo: 30 segundos")
# Verificação final
print("\n=== Teste de Verificação ===")
test_code = totp.now()
print(f"Código de teste: {test_code}")
is_valid = admin.verify_otp(test_code)
print(f"Verificação do código: {'Sucesso' if is_valid else 'Falha'}")
if not is_valid:
print("\nALERTA: Verificação do OTP falhou!")
print("Por favor, verifique se o segredo OTP está correto.")
except Exception as e:
print(f"\nErro durante a execução: {e}")
import traceback
traceback.print_exc()
finally:
if 'db_session' in locals():
db_session.close()
if __name__ == "__main__":
# Remover banco de dados existente para começar limpo
db_path = Path.home() / '.local' / 'share' / 'controles' / 'database.db'
if db_path.exists():
os.remove(db_path)
print("Banco de dados antigo removido.")
create_admin_user()
create_admin_user()

56
create_test_users.py Normal file
View File

@@ -0,0 +1,56 @@
from functions.database import get_db_connection, Usuario, Role
from werkzeug.security import generate_password_hash
def create_test_users():
"""Cria usuários de teste"""
db = get_db_connection()
try:
# Lista de usuários de teste
test_users = [
{
'username': 'aligner',
'email': 'aligner@test.com',
'password': 'Test123!@#',
'is_admin': False
},
{
'username': 'tester',
'email': 'tester@test.com',
'password': 'Test123!@#',
'is_admin': False
},
{
'username': 'deployer',
'email': 'deployer@test.com',
'password': 'Test123!@#',
'is_admin': False
}
]
# Criar cada usuário
for user_data in test_users:
user = db.query(Usuario).filter_by(username=user_data['username']).first()
if not user:
user = Usuario(
username=user_data['username'],
email=user_data['email'],
is_admin=user_data['is_admin']
)
user.set_password(user_data['password'])
db.add(user)
print(f"Usuário {user_data['username']} criado")
else:
print(f"Usuário {user_data['username']} já existe")
db.commit()
print("Usuários de teste criados com sucesso")
except Exception as e:
print(f"Erro ao criar usuários de teste: {str(e)}")
db.rollback()
finally:
db.close()
if __name__ == "__main__":
create_test_users()

8
dao.py
View File

@@ -1,8 +0,0 @@
from functions.database import execute_query
def get_user_by_email(email):
query = "SELECT * FROM users WHERE email = %s"
cursor = execute_query(query, (email,))
if cursor:
return cursor.fetchone()
return None

View File

@@ -1,99 +0,0 @@
-- Tabela de Militantes
CREATE TABLE militantes (
id INT PRIMARY KEY AUTO_INCREMENT,
nome VARCHAR(100) NOT NULL,
cpf VARCHAR(14) UNIQUE,
email VARCHAR(100) UNIQUE,
telefone VARCHAR(15),
endereco VARCHAR(255),
filiado BOOLEAN DEFAULT false
);
-- Tabela de Cotas Mensais
CREATE TABLE cotas_mensais (
id INT PRIMARY KEY AUTO_INCREMENT,
militante_id INT,
valor_antigo DECIMAL(10, 2) NOT NULL,
valor_novo DECIMAL(10, 2) NOT NULL,
data_alteracao DATE NOT NULL,
FOREIGN KEY (militante_id) REFERENCES militantes(id)
);
-- Tabela de Pagamentos
CREATE TABLE tipos_pagamento (
id INT PRIMARY KEY AUTO_INCREMENT,
descricao VARCHAR(100) NOT NULL
);
CREATE TABLE pagamentos (
id INT PRIMARY KEY AUTO_INCREMENT,
militante_id INT,
tipo_pagamento_id INT,
valor DECIMAL(10, 2) NOT NULL,
data_pagamento DATE NOT NULL,
FOREIGN KEY (militante_id) REFERENCES militantes(id),
FOREIGN KEY (tipo_pagamento_id) REFERENCES tipos_pagamento(id)
);
-- Tabela de Tipos de Materiais
CREATE TABLE tipos_materiais (
id INT PRIMARY KEY AUTO_INCREMENT,
descricao VARCHAR(100) NOT NULL
);
-- Tabela de Materiais Vendidos
CREATE TABLE materiais_vendidos (
id INT PRIMARY KEY AUTO_INCREMENT,
militante_id INT,
tipo_material_id INT,
descricao VARCHAR(255) NOT NULL,
valor DECIMAL(10, 2) NOT NULL,
data_venda DATE NOT NULL,
FOREIGN KEY (militante_id) REFERENCES militantes(id),
FOREIGN KEY (tipo_material_id) REFERENCES tipos_materiais(id)
);
-- Tabela de Vendas de Jornais Avulsos
CREATE TABLE vendas_jornais_avulsos (
id INT PRIMARY KEY AUTO_INCREMENT,
militante_id INT,
quantidade INT NOT NULL,
valor_total DECIMAL(10, 2) NOT NULL,
data_venda DATE NOT NULL,
FOREIGN KEY (militante_id) REFERENCES militantes(id)
);
-- Tabela de Assinaturas Anuais
CREATE TABLE assinaturas_anuais (
id INT PRIMARY KEY AUTO_INCREMENT,
militante_id INT,
tipo_material_id INT,
quantidade INT NOT NULL,
valor_total DECIMAL(10, 2) NOT NULL,
data_inicio DATE NOT NULL,
data_fim DATE NOT NULL,
FOREIGN KEY (militante_id) REFERENCES militantes(id),
FOREIGN KEY (tipo_material_id) REFERENCES tipos_materiais(id)
);
-- Tabela de Relatório de Cotas Mensais
CREATE TABLE relatorio_cotas_mensais (
id INT PRIMARY KEY AUTO_INCREMENT,
setor_id INT,
comite_id INT,
total_cotas DECIMAL(10, 2) NOT NULL,
data_relatorio DATE NOT NULL,
FOREIGN KEY (setor_id) REFERENCES setores(id),
FOREIGN KEY (comite_id) REFERENCES comites_centrais(id)
);
-- Tabela de Relatório de Vendas de Materiais
CREATE TABLE relatorio_vendas_materiais (
id INT PRIMARY KEY AUTO_INCREMENT,
setor_id INT,
comite_id INT,
total_vendas DECIMAL(10, 2) NOT NULL,
data_relatorio DATE NOT NULL,
FOREIGN KEY (setor_id) REFERENCES setores(id),
FOREIGN KEY (comite_id) REFERENCES comites_centrais(id)
);

14
docker-compose.yml Normal file
View 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

165
docs/README.md Normal file
View File

@@ -0,0 +1,165 @@
# Sistema de Controle OCI
## Hierarquia e Permissões
### Níveis de Acesso
1. **Militante Básico**
- Pode ver apenas os membros da sua própria célula
- Não pode alterar níveis de outros usuários
2. **Secretário de Célula**
- Pode ver e gerenciar apenas os membros da sua célula
- Não pode alterar níveis de outros usuários
3. **Membro de Setor**
- Pode ver apenas os dados do setor ao qual pertence
- Não pode alterar níveis de outros usuários
4. **Secretário de Setor**
- Pode ver e gerenciar todos os dados do seu setor
- Pode alterar níveis de militantes do setor, transformando-os em secretários
- Não pode alterar níveis de membros de outros setores
5. **Membro de CR**
- Pode ver apenas os dados do CR ao qual pertence
- Não pode alterar níveis de outros usuários
6. **Secretário de CR**
- Pode ver e gerenciar todos os dados do seu CR
- Pode alterar níveis de membros do CR
- Não pode alterar níveis de membros de outros CRs
7. **Membro do CC**
- Pode ver todos os dados do sistema
- Não pode alterar níveis de outros usuários
8. **Secretário Geral e Secretário de Organização**
- Pode ver todos os dados do sistema
- Pode alterar níveis de qualquer usuário em qualquer instância
### Regras de Visualização
- Cada militante só pode ver os membros da sua própria célula
- Membros de setor só veem dados do setor ao qual pertencem
- Membros de CR só veem informações do CR ao qual pertencem
- Membros do CC podem ver todas as informações do sistema
### Regras de Edição
- Apenas o Secretário Geral e o Secretário de Organização podem alterar níveis em todas as instâncias
- Secretários de CR podem alterar níveis apenas dentro do seu CR
- Secretários de Setor podem alterar níveis apenas dentro do seu setor, transformando militantes em secretários
- Outros níveis não podem alterar níveis de outros usuários
## Responsabilidades
O sistema suporta as seguintes responsabilidades para militantes:
- Militante Básico (1)
- Secretário de Célula (2)
- Secretário de Setor (4)
- Secretário de CR (8)
- Secretário de CC (16)
- Secretário Geral (32)
- Quadro-Orientador (64)
- Responsável de Finanças (256)
- Responsável de Imprensa (512)
### Status de Aspirante
Todo novo militante começa como Aspirante. Este status tem as seguintes características:
1. **Duração Mínima**: O status de Aspirante deve ser mantido por pelo menos 3 meses após a integração do militante.
2. **Avaliação Obrigatória**: Para remover o status de Aspirante, é necessário:
- Ter passado o período mínimo de 3 meses
- Registrar uma avaliação detalhada da atuação do militante durante este período
3. **Quem pode Avaliar**: A avaliação e remoção do status de Aspirante pode ser feita por:
- Secretário Geral
- Secretário de Organização
- Secretários de CR (para militantes de seu CR)
- Secretários de Setor (para militantes de seu setor)
4. **Registro da Avaliação**: A avaliação deve incluir:
- Análise da participação do militante nas atividades
- Desenvolvimento político e organizativo
- Pontos fortes e aspectos a melhorar
- Recomendações para o desenvolvimento futuro
5. **Histórico**: O sistema mantém registro de:
- Data de início do período como Aspirante
- Data da avaliação
- Texto completo da avaliação
O Quadro-Orientador é uma responsabilidade especial que pode ser atribuída a militantes em qualquer nível hierárquico, incluindo membros de CR e CC. Esta responsabilidade indica que o militante tem a função de orientar e apoiar outros militantes em sua formação política e organizativa.
A atribuição da responsabilidade de Quadro-Orientador pode ser feita por:
- Secretário Geral
- Secretário de Organização
- Secretários de CR (para militantes de seu CR)
- Secretários de Setor (para militantes de seu setor)
### Responsáveis de Finanças e Imprensa
Cada instância (Célula, Setor, CR e CC) possui três responsáveis:
1. **Responsável Geral**: Obrigatório para todas as instâncias. É o principal responsável pela instância.
2. **Responsável de Finanças**: Opcional. Responsável por:
- Controle financeiro da instância
- Arrecadação de contribuições
- Prestação de contas
- Planejamento financeiro
3. **Responsável de Imprensa**: Opcional. Responsável por:
- Comunicação externa da instância
- Produção de materiais de divulgação
- Gestão de redes sociais
- Relacionamento com a mídia
Os responsáveis de finanças e imprensa são designados pelo responsável geral da instância, com aprovação da instância superior.
## Hierarquia de Instâncias
1. **Comitê Central (CC)**
- Instância máxima da organização
- Possui responsável geral, de finanças e de imprensa
- Coordena todos os CRs
2. **Comitê Regional (CR)**
- Subordinado ao CC
- Possui responsável geral, de finanças e de imprensa
- Coordena os setores da sua região
3. **Setor**
- Subordinado ao CR
- Possui responsável geral, de finanças e de imprensa
- Coordena as células do seu setor
4. **Célula**
- Subordinada ao Setor
- Possui responsável geral, de finanças e de imprensa
- Unidade básica de organização
## Permissões
As permissões no sistema são baseadas nas responsabilidades do militante e na hierarquia das instâncias:
1. **Visualização**
- Militantes básicos veem apenas sua célula
- Secretários de célula veem sua célula
- Secretários de setor veem seu setor e células
- Secretários de CR veem seu CR, setores e células
- Secretários de CC veem todos os dados
2. **Edição**
- Cada nível pode gerenciar apenas os níveis abaixo
- Responsáveis de finanças e imprensa podem editar apenas suas áreas
- Quadros-Orientadores podem avaliar militantes
3. **Responsabilidades**
- Apenas o nível superior pode atribuir responsabilidades
- Responsáveis de finanças e imprensa são designados pelo responsável geral
- O status de Quadro-Orientador segue regras específicas

View 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

239
docs/rbac.md Normal file
View File

@@ -0,0 +1,239 @@
# Sistema de Permissões RBAC (Role-Based Access Control)
## Níveis de Permissão
O sistema de permissões é hierárquico, onde cada nível herda as permissões do nível anterior. A hierarquia é a seguinte (do menor para o maior nível):
### 1. Militante Básico
- Acesso apenas aos seus próprios dados
- Visualização de sua célula
- Sem permissões administrativas
### 2. Secretário de Célula
- Todas as permissões do Militante Básico
- Gerenciamento de militantes da sua célula
- Visualização de dados da célula
- Cadastro de novos militantes na célula
### 3. Membro de Setor
- Todas as permissões do Secretário de Célula
- Visualização de dados de todas as células do setor
- Acesso a relatórios do setor
### 4. Secretário de Setor
- Todas as permissões do Membro de Setor
- Gerenciamento de todas as células do setor
- Criação de novas células no setor
- Geração de relatórios do setor
- Gerenciamento de militantes do setor
### 5. Membro de CR (Comitê Regional)
- Todas as permissões do Secretário de Setor
- Visualização de dados de todos os setores do CR
- Acesso a relatórios do CR
### 6. Secretário de CR
- Todas as permissões do Membro de CR
- Gerenciamento de todos os setores do CR
- Criação de novos setores no CR
- Geração de relatórios do CR
- Gerenciamento de militantes do CR
### 7. Membro do CC (Comitê Central)
- Todas as permissões do Secretário de CR
- Visualização de dados de todos os CRs
- Acesso a relatórios nacionais
### 8. Secretário Geral / Secretário de Organização do CC
- Todas as permissões do Membro do CC
- Gerenciamento de todos os CRs
- Criação de novos CRs
- Geração de relatórios nacionais
- Gerenciamento de todos os militantes
- Configurações do sistema
## Implementação Técnica
O sistema RBAC é implementado através de:
1. **Roles**: Definem os níveis de acesso
2. **Permissions**: Definem as ações permitidas
3. **Role-Permission Mapping**: Mapeia quais permissões cada role possui
4. **User-Role Assignment**: Atribui roles aos usuários
### Estrutura do Banco de Dados
```sql
-- Roles
CREATE TABLE roles (
id INTEGER PRIMARY KEY,
nome VARCHAR(50) UNIQUE NOT NULL,
nivel INTEGER NOT NULL,
descricao TEXT
);
-- Permissions
CREATE TABLE permissions (
id INTEGER PRIMARY KEY,
nome VARCHAR(50) UNIQUE NOT NULL,
descricao TEXT
);
-- Role-Permission Mapping
CREATE TABLE role_permissions (
role_id INTEGER,
permission_id INTEGER,
PRIMARY KEY (role_id, permission_id),
FOREIGN KEY (role_id) REFERENCES roles(id),
FOREIGN KEY (permission_id) REFERENCES permissions(id)
);
-- User-Role Assignment
CREATE TABLE user_roles (
user_id INTEGER,
role_id INTEGER,
PRIMARY KEY (user_id, role_id),
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (role_id) REFERENCES roles(id)
);
```
## Exemplos de Permissões
### Permissões Básicas
- `view_own_data`: Visualizar seus próprios dados
- `edit_own_data`: Editar seus próprios dados
- `view_cell_data`: Visualizar dados da célula
### Permissões de Célula
- `manage_cell_members`: Gerenciar membros da célula
- `create_cell_member`: Criar novos membros na célula
- `view_cell_reports`: Visualizar relatórios da célula
### Permissões de Setor
- `manage_sector_cells`: Gerenciar células do setor
- `create_sector_cell`: Criar novas células no setor
- `view_sector_reports`: Visualizar relatórios do setor
### Permissões de CR
- `manage_cr_sectors`: Gerenciar setores do CR
- `create_cr_sector`: Criar novos setores no CR
- `view_cr_reports`: Visualizar relatórios do CR
### Permissões de CC
- `manage_cc_crs`: Gerenciar CRs
- `create_cc_cr`: Criar novos CRs
- `view_cc_reports`: Visualizar relatórios nacionais
- `system_config`: Configurar o sistema
## Uso no Código
```python
# Verificar permissão
if user.has_permission('manage_cell_members'):
# Permitir ação
# Verificar nível
if user.has_role_level(3): # Membro de Setor
# Permitir ação
# Verificar hierarquia
if user.is_higher_or_equal_than(other_user):
# Permitir ação
```
# Controle de Acesso Baseado em Funções (RBAC)
## Estrutura Hierárquica
O sistema possui uma estrutura hierárquica com os seguintes níveis:
- Célula (base)
- Setor (agrupa células)
- Comitê Regional - CR (agrupa setores)
- Comitê Central - CC (único, agrupa CRs)
## Regras de Associação
- Cada militante pertence a apenas uma célula
- Cada célula pertence a apenas um setor
- Cada setor pertence a apenas um CR
- Existe apenas um Comitê Central (CC)
## Permissões por Instância
### Célula
- **Secretário(a)**:
- `MANAGE_CELL_MEMBERS`: Gerenciar membros da célula
- `VIEW_CELL_DATA`: Visualizar dados da célula
- `VIEW_CELL_REPORTS`: Visualizar relatórios da célula
- `REGISTER_CELL_PAYMENT`: Registrar pagamentos da célula
- **Tesoureiro(a)**:
- `VIEW_CELL_DATA`: Visualizar dados da célula
- `VIEW_CELL_REPORTS`: Visualizar relatórios da célula
- `REGISTER_CELL_PAYMENT`: Registrar pagamentos da célula
- **Militante**:
- `VIEW_OWN_DATA`: Visualizar apenas seus próprios dados
### Setor
- **Secretário(a)**:
- `MANAGE_SECTOR_CELLS`: Gerenciar células do setor
- `VIEW_SECTOR_REPORTS`: Visualizar relatórios do setor
- `REGISTER_SECTOR_PAYMENT`: Registrar pagamentos do setor
- **Tesoureiro(a)**:
- `VIEW_SECTOR_REPORTS`: Visualizar relatórios do setor
- `REGISTER_SECTOR_PAYMENT`: Registrar pagamentos do setor
### CR
- **Secretário(a)**:
- `MANAGE_CR_SECTORS`: Gerenciar setores do CR
- `VIEW_CR_REPORTS`: Visualizar relatórios do CR
- `REGISTER_CR_PAYMENT`: Registrar pagamentos do CR
- **Tesoureiro(a)**:
- `VIEW_CR_REPORTS`: Visualizar relatórios do CR
- `REGISTER_CR_PAYMENT`: Registrar pagamentos do CR
### CC
- **Secretário(a)**:
- `MANAGE_CC_CRS`: Gerenciar CRs
- `VIEW_CC_REPORTS`: Visualizar relatórios do CC
- `REGISTER_CC_PAYMENT`: Registrar pagamentos do CC
- `SYSTEM_CONFIG`: Configurar o sistema
- **Tesoureiro(a)**:
- `VIEW_CC_REPORTS`: Visualizar relatórios do CC
- `REGISTER_CC_PAYMENT`: Registrar pagamentos do CC
## Regras de Acesso a Dados
1. **Visualização de Dados**:
- Militantes podem ver apenas seus próprios dados
- Secretários e tesoureiros podem ver dados de sua instância
- O CC tem acesso a todos os dados
2. **Registro de Pagamentos**:
- Apenas tesoureiros e secretários podem registrar pagamentos
- O registro é restrito à instância do usuário
- O CC pode registrar pagamentos em qualquer nível
## Implementação Técnica
O controle de acesso é implementado através de:
1. **Decorators**:
- `@require_login`: Verifica se o usuário está logado
- `@require_permission`: Verifica se o usuário tem uma permissão específica
- `@require_instance_permission`: Verifica permissão em uma instância específica
- `@require_instance_access`: Verifica acesso a uma instância específica
2. **Verificações de Acesso**:
- Cada rota verifica as permissões necessárias
- O acesso é negado se o usuário não tiver as permissões requeridas
- Mensagens de erro são exibidas para o usuário
3. **Filtragem de Dados**:
- As consultas ao banco de dados são filtradas baseadas nas permissões
- Cada nível hierárquico tem suas próprias regras de acesso

33
functions/base.py Normal file
View File

@@ -0,0 +1,33 @@
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from pathlib import Path
import os
# Configurar caminho do banco de dados
db_dir = Path.home() / '.local' / 'share' / 'controles'
db_dir.mkdir(parents=True, exist_ok=True)
db_path = db_dir / 'database.db'
# 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()
def get_db_connection():
"""Retorna uma nova sessão do banco de dados"""
session = Session()
try:
return session
except Exception as e:
session.rollback()
raise e

View File

@@ -1,692 +0,0 @@
// TODO: extract all CONTANTS TO EASILY CHANGE CELLS
const planilhaID= "13sLipAAD5LkzZK19iuzgscbCmODiS11hJDRgaNsnYvw";
// LOCAIS DE LIMPEZA \/\/\/\/
const cotas = 'B5:E40' ;
const contribuintes = 'B43:E57' ;
const brochuras = 'B60:D65';
const campanha = 'B68:D84' ;
const outras = 'B87:D94';
const assinantes = 'B97:D109';
const jornal = 'B112:D126';
const despesaCE = 'D129';
const depositos = 'B134:F251' ;
const carimbo = 'Q287' ;
// ACABOU :LOCAIS DE LIMPEZA /\/\/\/\
const contagemRF='E2';
const celulaPrincipal = 'A1' ;
const enddepositos = 'D252';
const endvendas = 'D130' ;
const celulaValorTotalCotas = 'E41';
const timeZone = Session.getScriptTimeZone();
const CRSP = "crsptesouraria@gmail.com";
const areaAProteger = 'A1:Y999' ;
function getUser(){ return Session.getEffectiveUser();}
function voltaAoTopo(){
SpreadsheetApp.getActiveSheet().setCurrentCell(SpreadsheetApp.getActiveSheet().getRange(celulaPrincipal)) ;
}
function onOpen() {
var ui = SpreadsheetApp.getUi();
ui.createMenu('CR')
.addItem('Enviar RF', 'menuItem1')
.addItem('Totalizar Cotas', 'menuItem2')
.addItem('Teste - Não usar', 'menuItem3')
.addToUi();
}
/// MENU ITEMS
function menuItem1() {
SpreadsheetApp.getUi()
{
Logger.log(getUser());
resultado = enviaCR();
Logger.log("Resultado: " + resultado + ".");
}
}
function menuItem2() {
SpreadsheetApp.getUi()
{
Logger.log(getUser());
resultado = totalizar(curName)
Logger.log("Resultado: " + resultado + ".");
}
}
function menuItem3() {
SpreadsheetApp.getUi()
{
Logger.log(getUser());
carimboValue = pegarCarimbo(SpreadsheetApp.getActiveSpreadsheet().getActiveSheet()) ;
if(!isNaN(parseFloat(carimboValue)) ) {
var mesAtual = Utilities.formatDate(carimboValue,timeZone, "MM");
Logger.log("Carimbo lido: " + mesAtual + ".");
}
else {
mesAtual = Utilities.formatDate(new Date(),timeZone, "MM") ;
Logger.log("Carimbo vazio, mês atual: " + mesAtual + ".");
}
voltaAoTopo();
}
}
/// FUNCTIONS BELOW
/// SEND RF
function enviaCR() {
var ss = SpreadsheetApp.getActiveSpreadsheet(); // cria o objeto do arquivo da planilha
var sheet = ss.getActiveSheet(); // cria objeto da Sheet ativa agora
var curName = ss.getActiveSheet().getName() ; // pega nome da Sheet
// validar contas
if (validar(sheet))
{
// subir dados na planilha de controle
var resultadoEnvio = enviando(curName,sheet,ss);
if (resultadoEnvio == "Enviado" )
{SpreadsheetApp.getUi().alert('Relatório Enviado!');}
else
{SpreadsheetApp.getUi().alert('ERRO: ' + resultadoEnvio );}
} return resultadoEnvio;
}
// VALIDAR VALORES TODO: ADIOCIONAR NOVAS
function validar(sheet){
// trocar vendas por centralizado
var celulaDepositos = sheet.getRange(enddepositos);
var depositos = sheet.setCurrentCell(celulaDepositos).getValue();
var celulaVendas = sheet.getRange(endvendas);
var vendas = sheet.setCurrentCell(celulaVendas).getValue();
if ( vendas === depositos )
{ return true;}
else
{ SpreadsheetApp.getUi().alert('Centralizado ' + vendas + ' não bate com Depósitos ' + depositos ); return false ;}
}
function enviando(curName,sheet,ss) {
valorCotas = pegarTotalCota(curName, ss); // TOTAL das cotas
marcaCarimbos(curName, valorCotas, sheet); // SALVA TOTAL DAS COTAS ETC
novaAba = renomearAba(curName, ss); // Renomeia Aba e coloca nomero da nova aba no numero do relatorio
limpaEntradas(novaAba) ; // limpa carimbo e entradas
if (travar(curName, ss) === "Travada"){
ss.setActiveSheet(novaAba); // coloca novo em evidencia
return "Enviado";
}
}
function pegarTotalCota(curName, ss){
var sheet = ss.getSheetByName(curName);
var valorNovaAvulso = Number(sheet.setCurrentCell(sheet.getRange(celulaValorTotalCotas)).getValue());
Logger.log(" valorNovaAvulso: " + valorNovaAvulso + ".");
return valorNovaAvulso;
}
function marcaCarimbos(curName,totalCota,sheet){
var gravarTempo = Utilities.formatDate(new Date(),timeZone, "yyyyMMddHHmmssSSS");
var celulaTempo = 'D900';
var celulaTotalCotas = 'D901';
var celulaResponsavel = 'D902';
var celulaNomeContagem = 'D902';
var celulaResponsavelCel = 'H2';
var username = getUser();
sheet.setCurrentCell(sheet.getRange(celulaTempo)).setValue(gravarTempo);
sheet.setCurrentCell(sheet.getRange(celulaResponsavel)).setValue(username);
sheet.setCurrentCell(sheet.getRange(celulaNomeContagem)).setValue(curName);
sheet.setCurrentCell(sheet.getRange(celulaResponsavelCel)).setValue(username);
sheet.setCurrentCell(sheet.getRange(celulaTotalCotas)).setValue(totalCota);
}
function pegarCarimbo(sheet)
{
new Date(sheet.setCurrentCell(sheet.getRange(carimbo)).getValue())
}
function renomearAba(curName,ss){
var newName = Number(curName) + 1 ; // cria nome da nova
ss.moveActiveSheet(ss.getNumSheets() - 1); // move a atual para a ultima posicao antes da Validacao que é escondida
ss.duplicateActiveSheet(); // duplica ativa
ss.renameActiveSheet(newName); // renomeia nova
ss.moveActiveSheet(1); // move para a primeira posicao
var sheet = ss.getSheetByName(newName); // torna a nova ativa usando nome
sheet.getRange(contagemRF).setValue(newName); //altera contagem do relatorio usando numero da aba
return sheet;
}
function limpaEntradas(sheet)
{
function limpaTudo(value){
sheet.getRange(value).clearContent();
}
var limpeza = [ cotas, contribuintes, brochuras, campanha , outras, assinantes, jornal , despesaCE, depositos, carimbo ];
limpeza.forEach(limpaTudo) ;
let range = sheet.getRange("I:Y");
sheet.hideColumn(range);
range = sheet.getRange("A258:A999");
sheet.hideRow(range);
SpreadsheetApp.getActiveSheet().setCurrentCell(SpreadsheetApp.getActiveSheet().getRange(celulaPrincipal)) ;
}
function travar(curName, ss){
var sheet = ss.getSheetByName(curName);
var areaProtegida = false ;
var abaProtegida = false ;
var userName = getUser();
var protections = sheet.getProtections(SpreadsheetApp.ProtectionType.SHEET);
for (var i = 0; i < protections.length; i++) {
var desc = protections[i].getDescription();
Logger.log("protection desc: " + desc);
if ( desc === 'Area protegida' ){ areaProtegida = true ; }
if ( desc === 'Aba protegida') { abaProtegida = true ; }
}
// Protege area, e remove todos da lista de editores.
var range = sheet.getRange(areaAProteger);
if (areaProtegida === false && userName != CRSP ) {
proteRange = range.protect().setDescription('Area protegida') ;
areaProtegida = true ;
proteRange.removeEditor(userName);
if (proteRange.canDomainEdit()) {
proteRange.setDomainEdit(false);
}
}
Logger.log(userName);
if (abaProtegida === false && userName != CRSP ) {
var proteSheet = sheet.protect().setDescription('Aba protegida');
abaProtegida = true ;
Logger.log("Removendo: " + userName);
proteSheet.removeEditor(userName);
if (proteSheet.canDomainEdit()) {
proteSheet.setDomainEdit(false);
}
}
if (abaProtegida === true && areaProtegida === true ) { return "Travada" ;}
}
function efetuarRotinaMadrugada(){
travaNoturna();
totalizar();
}
function travaNoturna(){
var ss = SpreadsheetApp.openById(planilhaID);
var trava = 0 ;
console.log( getUser());
// Protects the sheet.
const sampleProtectedSheet = sheet.protect();
// Logs whether domain users have permission to edit the protected sheet to the console.
console.log(sampleProtectedSheet.canDomainEdit());
var sheets = SpreadsheetApp.getActiveSpreadsheet().getSheets();
for (var cadaSheet = 0 ; cadaSheet < sheets.length ; cadaSheet++){
var nomeSheet = sheets[cadaSheet].getName();
Logger.log(" TravaNoturna nomeSheet: " + nomeSheet);
if (!isNaN(parseFloat(nomeSheet)) && isFinite(nomeSheet) && nomeSheet === anterior) {
SpreadsheetApp.setActiveSheet(sheets[cadaSheet]);
var protections = sheets[cadaSheet].getProtections(SpreadsheetApp.ProtectionType.SHEET);
for (var i = 0; i < protections.length; i++) {
var desc = protections[i].getDescription();
Logger.log("trava desc: " + desc);
if ( desc === 'Area protegida' || desc === 'Aba protegida' ) {
trava = trava + 1 ;
}
}
if ( trava == 2 ){
const protection = sheets[cadaSheet].protect();
// Logs whether domain users have permission to edit the protected sheet to the console.
console.log(protection.canDomainEdit());
protection.removeEditors(protection.getEditors());
if (protection.canDomainEdit()) {
protection.setDomainEdit(false);
}
console.log(protection.canDomainEdit());
protection.setDescription('Trava Noturna');
}
}
var sheet = ss.getSheetByName(anterior);
sheet.setCurrentCell(sheet.getRange(celulaPrincipal)) ;
}
}
function totalizar(curName){
var anterior = curName ;
Logger.log("anterior: " + anterior + ".");
var ss = SpreadsheetApp.getActiveSpreadsheet() ;
var sheets = SpreadsheetApp.getActiveSpreadsheet().getSheets();
var gravou = 0;
function enviarTotal() {
var sheet = ss.getSheetByName(anterior);
var brochuras = sheet.setCurrentCell(sheet.getRange('D66')).getValue() ;
var campanha = sheet.setCurrentCell(sheet.getRange('D71')).getValue();
var campanhaCCCE = sheet.setCurrentCell(sheet.getRange('D85')).getValue();
var outras = sheet.setCurrentCell(sheet.getRange('D95')).getValue();
var assinantes = sheet.setCurrentCell(sheet.getRange('D115')).getValue();
var jornal = sheet.setCurrentCell(sheet.getRange('D127')).getValue();
var carimboValue = pegarCarimbo() ;
var sheet = ss.getSheetByName("TOTAL");
// ABA TOTAL COLUNAS DE VALORES TOTALIZADOS
var cotascol = 'C'; // 0
var contribuintescol = 'E'; // 1
var brochurascol = 'H' ; // 2
var cfcol = 'J'; // 3
var outrascol = 'L'; // 4
var asscol = 'P'; // 5
var jornalcol ='R'; // 6
var varJaneiro = 3 ;
var varFevereiro = 4 ;
var varMarço = 5 ;
var varAbril = 6 ;
var varMaio = 7 ;
var varJunho = 8 ;
var varJulho = 9 ;
var varAgosto = 10 ;
var varSetembro = 11 ;
var varOutubro = 12 ;
var varNovembro = 13 ;
var varDezembro = 14 ;
var decimoTerceiro = 15 ;
var decimoQuarto = 16 ;
var decimoQuinto = 17 ;
var colunas = [ cotascol, contribuintescol , brochurascol , cfcol , outrascol , asscol , jornalcol ] ;
var linhas = [varJaneiro , varFevereiro ,varMarço ,varAbril ,varMaio ,varJunho ,varJulho ,varAgosto ,varSetembro ,varOutubro ,varNovembro ,varDezembro, decimoTerceiro, decimoQuarto, decimoQuinto] ;
// TERMINOU TABELA TOTAL /\
// PEGAR MES ATUAL
if(!isNaN(parseFloat(carimboValue)) ) {var mesAtual = Utilities.formatDate(carimboValue,timeZone, "MM");}
else { mesAtual = Utilities.formatDate(new Date(),timeZone, "MM") }
// Para cada Coluna de TOTAL executar totalização:
colunas.forEach(function(letra,coluna,tudo) {
Logger.log("letra: " + letra );
// começa com cota, checa se é o mes e coloca no switch.
switch (letra){
case cotascol:
// mes igual mes da primeira linha
for (var cadaMesdeCota = 5 ; cadaMesdeCota <=40 ; cadaMesdeCota++ ){
var celulaMilitante = 'B' + cadaMesdeCota ;
var celulaAno = 'C' + cadaMesdeCota ;
var celulaMes = 'D' + cadaMesdeCota ;
var celulaValor = 'E' + cadaMesdeCota ;
// Vai pra Anterior pra pegar cota de cadaMesdeCota ++++++++++++++++++++++++++++++++++++++++++++++
var sheet = ss.getSheetByName(anterior);
valorCelula = sheet.setCurrentCell(sheet.getRange(celulaValor)).getValue() ;
// if (!isNaN(parseFloat(mesCelula)) && !isNaN(parseFloat(valorCelula)))
if (!isNaN(parseFloat(valorCelula)))
{
var mesCelula = sheet.setCurrentCell(sheet.getRange(celulaMes)).getValue();
var retornoMes = checkMonth(mesCelula);
militanteCota = sheet.setCurrentCell(sheet.getRange(celulaMilitante)).getValue();
anoCota = sheet.setCurrentCell(sheet.getRange(celulaAno)).getValue();
Logger.log( " COTA valorCelula: " + valorCelula + " militanteCota " + militanteCota + "retornoMes" + retornoMes);
if ( !isNaN(parseFloat(retornoMes)) ) {
var mesNovaCota = new Date(retornoMes) ;
var mesNCemN = Number(Utilities.formatDate(mesNovaCota,timeZone, "MM")) - 1;
var valorNovaCota = valorCelula ;
if (!isNaN(parseFloat(valorNovaCota))){
// ENVIA PARA TOTAL:
Logger.log( " COTA valorNovaCota: " + valorNovaCota + ".");
var sheet = ss.getSheetByName("TOTAL");
var celulaObjetivo = letra + linhas[mesNCemN] ;
Logger.log( " COTA celulaObjetivo: " + celulaObjetivo + ".");
var valorAntigoCota = sheet.setCurrentCell(sheet.getRange(celulaObjetivo)).getValue() ;
if (!isNaN(parseFloat(valorAntigoCota))){
Logger.log( " COTA valorAntigoCota: " + valorAntigoCota + ".");
var gravar = valorAntigoCota + valorNovaCota ;
}
else { var gravar = valorNovaCota ; }
sheet.setCurrentCell(sheet.getRange(celulaObjetivo)).setValue(gravar);
Logger.log( " COTA Gravou: " + gravar + ".");
// ENVIOU PARA TOTAL /\
}
}
}
}
break;
// segunda iteração contribuintes, checa se é o mes e coloca no switch.
case contribuintescol:
// mes igual mes da primeira linha
for (var cadaMesContrib = 43 ; cadaMesContrib <=57 ; cadaMesContrib++ ){
var celulaContribuinte = 'B' + cadaMesContrib ;
var celulaAno = 'C' + cadaMesContrib ;
var celulaMes = 'D' + cadaMesContrib ;
var celulaValor = 'E' + cadaMesContrib ;
var celulaResponsavel = 'F' + cadaMesContrib ;
// Vai pra Anterior pra pegar Contribuição de cadaMesdeCota
var sheet = ss.getSheetByName(anterior);
var retornoMes = sheet.setCurrentCell(sheet.getRange(celulaMes)).getValue();
if ( !isNaN(parseFloat(retornoMes)) ) {
var mesNovaContrib = new Date(checkMonth(retornoMes)) ;
var mesNCemN = Number(Utilities.formatDate(mesNovaContrib,timeZone, "MM")) - 1;
var valorNovaContr = Number(sheet.setCurrentCell(sheet.getRange(celulaValor)).getValue());
if (!isNaN(parseFloat(valorNovaContr))){
Logger.log( " CONTRIB valorNovaContr: " + valorNovaContr + ".");
var celulaObjetivo = letra + linhas[mesNCemN] ;
Logger.log( " CONTRIB celulaObjetivo: " + celulaObjetivo + ".");
var sheet = ss.getSheetByName("TOTAL");
var valorAntigoContr = sheet.setCurrentCell(sheet.getRange(celulaObjetivo)).getValue() ;
Logger.log( " CONTRIB valorAntigoContr: " + valorAntigoContr + ".");
if (!isNaN(parseFloat(valorAntigoContr)) ){
var gravar = valorNovaContr + valorAntigoContr ; }
else {
gravar = valorNovaContr ;
}
Logger.log( " CONTRIB celulaMes: " + celulaMes + " celulaValor: " + celulaValor + ".");
var sheet = ss.getSheetByName("TOTAL");
sheet.setCurrentCell(sheet.getRange(celulaObjetivo)).setValue(gravar);
Logger.log( " CONTRIB Gravou: " + gravar + ".");
}
}
}
break;
// FALTA TERMINAR BROCHURAS
case brochurascol:
for (var linhaBro = 60 ; linhaBro <=65 ; linhaBro++ ){
// var celulaNome = 'B' +linhaBro ;
var celulaQuantidade = 'C' + linhaBro ;
var celulaValor = 'D' + linhaBro ;
var celulaCodigo = 'E' + linhaBro ;
// Vai pra Anterior pra pegar dados acima
var sheet = ss.getSheetByName(anterior);
var difLinBro = 2 ;
var quantidadeBro = sheet.setCurrentCell(sheet.getRange(celulaQuantidade)).getValue();
if ( !isNaN(parseFloat(quantidadeBro)) ) {
var valorNovaBro = Number(sheet.setCurrentCell(sheet.getRange(celulaValor)).getValue());
var codigoNovaBro = Number(sheet.setCurrentCell(sheet.getRange(celulaCodigo)).getValue()) + difLinBro ;
Logger.log(" BROCHURAS valorNovaBro: " + valorNovaBro + " codigoNovaBro: " + codigoNovaBro);
if (!isNaN(parseFloat(valorNovaBro))){
var celulaObjetivo = letra + codigoNovaBro ;
var qtdObjetivo = 'G' + codigoNovaBro ;
var qtdAntigoBro = sheet.setCurrentCell(sheet.getRange(qtdObjetivo)).getValue() ;
var sheet = ss.getSheetByName("TOTAL");
var valorAntigoBro = sheet.setCurrentCell(sheet.getRange(celulaObjetivo)).getValue() ;
if (!isNaN(parseFloat(valorAntigoBro)) ){
var gravar = valorNovaBro + valorAntigoBro ; }
else {
gravar = valorNovaBro ;
}
Logger.log(" BROCHURAS celulaQuantidade: " + celulaQuantidade + " celulaValor: " + celulaValor + " gravar: " + gravar );
if (!isNaN(parseFloat(qtdAntigoBro)) ){
var gravarQtd = quantidadeBro + qtdAntigoBro ; }
else {
var gravarQtd = quantidadeBro ;
}
var sheet = ss.getSheetByName("TOTAL");
// Grava Valor
Logger.log(" BROCHURAS celulaValorObjetivo: " + celulaObjetivo + " gravar: " + gravar );
sheet.setCurrentCell(sheet.getRange(celulaObjetivo)).setValue(gravar);
Logger.log( " BROCHURAS Gravou Valor: " + gravar + ".");
// Grava quantidade
sheet.setCurrentCell(sheet.getRange(qtdObjetivo)).setValue(gravarQtd);
Logger.log( " BROCHURAS Gravou Qtd: " + gravarQtd + ".")
}
}
}
break;
case cfcol:
for (var linhaCF = 68 ; linhaCF <=84 ; linhaCF++ ){
var celulaNome = 'B' + linhaCF ;
var celulaQuantidade = 'C' + linhaCF ;
var celulaValor = 'D' + linhaCF ;
var celulaCodigo = 'F' + linhaCF ;
var sheet = ss.getSheetByName(anterior);
var valorCF = sheet.setCurrentCell(sheet.getRange(celulaValor)).getValue();
if ( !isNaN(parseFloat(valorCF)) ) {
var militanteNovaCF = sheet.setCurrentCell(sheet.getRange(celulaNome)).getValue();
// VAI PRA TOTAL
var sheet = ss.getSheetByName("TOTAL");
var linhaSalvar = sheet.getRange('I3:J22').createTextFinder(militanteNovaCF).findNext();
if (linhaSalvar){
var valorAntigoCF = linhaSalvar.offset(0,1).getValue();
Logger.log(" CF linhaSalvar.getA1Notation(): " + linhaSalvar.getA1Notation() + " militanteNovaCF: " + militanteNovaCF + " valorCF: " + valorCF + " valorAntigoCF: " + valorAntigoCF );
if (!isNaN(parseFloat(valorAntigoCF)) ){ var gravar = valorCF + valorAntigoCF ; }
else { gravar = valorCF ; }
// Grava Valor
linhaSalvar.offset(0,1).setValue(gravar);
Logger.log( " CF Gravou Valor: " + gravar + ".");
}
}
}
break;
case outrascol:
for (var linhaOutros = 87 ; linhaOutros <=94 ; linhaOutros++ ){
var celulaQuantidade = 'C' + linhaOutros ;
var celulaValor = 'D' + linhaOutros ;
var celulaCodigo = 'E' + linhaOutros ;
// Vai pra Anterior pra pegar dados acima
var sheet = ss.getSheetByName(anterior);
var quantidadeOutro = sheet.setCurrentCell(sheet.getRange(celulaQuantidade)).getValue();
if ( !isNaN(parseFloat(quantidadeOutro)) ) {
var valorNovaOutro = Number(sheet.setCurrentCell(sheet.getRange(celulaValor)).getValue());
var difLinOut = 2 ;
var codigoNovaOutro = Number(sheet.setCurrentCell(sheet.getRange(celulaCodigo)).getValue()) + difLinOut ;
if (!isNaN(parseFloat(valorNovaOutro))){
var celulaObjetivo = letra + codigoNovaOutro ;
var qtdObjetivo = 'K' + codigoNovaOutro ;
var qtdAntigoOutro = sheet.setCurrentCell(sheet.getRange(qtdObjetivo)).getValue() ;
var sheet = ss.getSheetByName("TOTAL");
var valorAntigoOutro = sheet.setCurrentCell(sheet.getRange(celulaObjetivo)).getValue() ;
if (!isNaN(parseFloat(valorAntigoOutro)) ){
var gravar = valorNovaOutro + valorAntigoOutro ; }
else {
gravar = valorNovaOutro ;
}
Logger.log( " OUTRAS celulaQuantidade: " + celulaQuantidade + " celulaValor: " + celulaValor + " gravar: " + gravar );
if (!isNaN(parseFloat(qtdAntigoOutro)) ){
var gravarQtd = quantidadeOutro + qtdAntigoOutro ; }
else {
var gravarQtd = quantidadeOutro ;
}
var sheet = ss.getSheetByName("TOTAL");
// Grava Valor
Logger.log( " OUTRAS celulaValorObjetivo: " + celulaObjetivo + " gravar: " + gravar );
sheet.setCurrentCell(sheet.getRange(celulaObjetivo)).setValue(gravar);
Logger.log( " OUTRAS Gravou Valor: " + gravar + ".");
// Grava quantidade
sheet.setCurrentCell(sheet.getRange(qtdObjetivo)).setValue(gravarQtd);
Logger.log( " OUTRAS Gravou Qtd: " + gravarQtd + ".")
}
}
}
break;
case asscol:
// ARRUMAR ASSINATURAS
var linha = mesAtual - 1 ;
Logger.log(" ASSINATURA linhas[linha]: " + linhas[linha] + " linha: " + linha + " mesAtual: " + mesAtual + ".");
var celula = letra + linhas[linha] ;
// PEGAR TOTAL ATUAL
var sheet = ss.getSheetByName(anterior);
var totalatual = sheet.setCurrentCell(sheet.getRange(celula)).getValue() ;
if ( !isNaN(parseFloat(outras)) && assinantes > 0 ){
var gravar = totalatual + assinantes ;
// GRAVAR
var sheet = ss.getSheetByName("TOTAL");
sheet.setCurrentCell(sheet.getRange(celula)).setValue(gravar);
}
break ;
case jornalcol:
for (var linhaAvulso = 112 ; linhaAvulso <=126 ; linhaAvulso++ ){
var celulaQuantidade = 'C' + linhaAvulso ;
var celulaValor = 'D' + linhaAvulso ;
var celulaEdicao = 'B' + linhaAvulso ;
// Vai pra Anterior pra pegar dados acima
var sheet = ss.getSheetByName(anterior);
var difLinEdicao = 11
var quantidadeAvulso = sheet.setCurrentCell(sheet.getRange(celulaQuantidade)).getValue();
if ( !isNaN(parseFloat(quantidadeAvulso)) ) {
var valorNovaAvulso = Number(sheet.setCurrentCell(sheet.getRange(celulaValor)).getValue());
var edicaoNovaAvulso = Number(sheet.setCurrentCell(sheet.getRange(celulaEdicao)).getValue()) - difLinEdicao ;
Logger.log( " JORNALA. edicaoNovaAvulso: " + edicaoNovaAvulso + "valorNovaAvulso: " + valorNovaAvulso + "Quantidade: " + quantidadeAvulso );
if (!isNaN(parseFloat(valorNovaAvulso))){
var celulaValorObjetivo = letra + edicaoNovaAvulso ;
var qtdVendido = 'T' + edicaoNovaAvulso ;
var sheet = ss.getSheetByName("TOTAL");
var valorAntigoAvulso = sheet.setCurrentCell(sheet.getRange(celulaValorObjetivo)).getValue() ;
var qtdAntigoAvulso = sheet.setCurrentCell(sheet.getRange(qtdVendido)).getValue() ;
if (!isNaN(parseFloat(valorAntigoAvulso)) ){
var gravar = valorNovaAvulso + valorAntigoAvulso ; }
else {
var gravar = valorNovaAvulso ;
}
if (!isNaN(parseFloat(qtdAntigoAvulso)) ){
var gravarQtd = quantidadeAvulso + qtdAntigoAvulso ; }
else {
var gravarQtd = quantidadeAvulso ;
}
var sheet = ss.getSheetByName("TOTAL");
// Grava Valor
sheet.setCurrentCell(sheet.getRange(celulaValorObjetivo)).setValue(gravar);
Logger.log( " JORNALA. Gravou Valor: " + gravar );
// Grava quantidade
sheet.setCurrentCell(sheet.getRange(qtdVendido)).setValue(gravarQtd);
Logger.log( " JORNALA. Gravou Qtd: " + gravarQtd );
}
}
}
break;
}
} )
}
if (sheets.length > 1) {
for (var cadaSheet = 0 ; cadaSheet < sheets.length ; cadaSheet++)
{
var nomeSheet = sheets[cadaSheet].getName();
Logger.log("nomeSheet: " + nomeSheet);
if (!isNaN(parseFloat(nomeSheet)) && isFinite(nomeSheet) && nomeSheet === anterior) {
SpreadsheetApp.setActiveSheet(sheets[cadaSheet]);
var protections = sheets[cadaSheet].getProtections(SpreadsheetApp.ProtectionType.SHEET);
for (var i = 0; i < protections.length; i++) {
var desc = protections[i].getDescription();
Logger.log("protection desc: " + desc);
if ( desc === 'Aba protegida') {
enviarTotal();
gravou = 1;
}
}
}
var sheet = ss.getSheetByName(anterior);
sheet.setCurrentCell(sheet.getRange(celulaPrincipal)) ;
}
}
if (gravou === 1 ) { return "Totalizado";}
Logger.log("gravou: " + gravou + ".");
}
function checkMonth(nomedoMes)
{
Logger.log(" checkMonth nomedoMes: " + nomedoMes + ".");
switch (nomedoMes){
case 1:
case "Janeiro" :
return "1";
break;
case 2:
case "Fevereiro":
return "2";
break;
case 3:
case "Março":
return "3";
break;
case 4:
case "Abril":
return "4";
break;
case 5:
case "Maio":
return "5";
break;
case 6:
case "Junho":
return "6";
break;
case 7:
case "Julho":
return "7";
break;
case 8:
case "Agosto":
return "8";
break;
case 9:
case "Setembro":
return "9";
break;
case 10:
case "Outubro":
return "10";
break;
case 11:
case "Novembro":
return "11";
break;
case 12:
case "Dezembro":
return "12";
break;
default:
return nomedoMes;
break;
}
}

View File

@@ -1,19 +0,0 @@
from PIL import Image, ImageDraw, ImageFont
import qrcode
def gerar_carteirinha(militante_id, nome):
# Criar imagem base
img = Image.new('RGB', (300, 200), color=(255, 255, 255))
d = ImageDraw.Draw(img)
# Adicionar texto
font = ImageFont.load_default()
d.text((10, 10), f"Nome: {nome}", font=font, fill=(0, 0, 0))
d.text((10, 30), f"ID: {militante_id}", font=font, fill=(0, 0, 0))
# Gerar QR code
qr = qrcode.make(f"ID: {militante_id}")
img.paste(qr, (200, 50))
# Salvar imagem
img.save(f"carteirinha_{militante_id}.png")

84
functions/controle.py Normal file
View 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()

View File

@@ -1,35 +1,42 @@
from sqlalchemy import create_engine, Column, Integer, String, Boolean, Numeric, Date, ForeignKey, DateTime
from sqlalchemy.orm import relationship, sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from datetime import datetime, timedelta
from werkzeug.security import generate_password_hash, check_password_hash
import pyotp
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text, Numeric, Date, Enum, create_engine, text
from sqlalchemy.orm import sessionmaker, relationship, backref
import os
import pyotp
from pathlib import Path
from sqlalchemy.pool import NullPool
import secrets
from flask_mail import Message
from flask import url_for
import enum
from flask_login import UserMixin
from .rbac import Role, Permission, role_permissions, user_roles
from .base import Base, engine, Session
import logging
# Configurar caminho do banco de dados
db_dir = Path.home() / '.local' / 'share' / 'controles'
db_dir.mkdir(parents=True, exist_ok=True)
db_path = db_dir / 'database.db'
# Configurar engine com NullPool
engine = create_engine(
f'sqlite:///{db_path}',
echo=True,
poolclass=NullPool # Usar NullPool ao invés do pool padrão
)
Base = declarative_base()
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():
"""
Retorna uma nova sessão do banco de dados
"""
"""Retorna uma nova sessão do banco de dados"""
Session = sessionmaker(bind=engine)
db = Session()
try:
return SessionLocal()
finally:
engine.dispose()
# Configurar SQLite para melhor tratamento de concorrência
db.execute(text("PRAGMA journal_mode=WAL"))
db.execute(text("PRAGMA busy_timeout=5000"))
return db
except:
db.close()
raise
def execute_query(query, params=None):
"""
@@ -46,22 +53,246 @@ def execute_query(query, params=None):
finally:
session.close()
class EstadoMilitante(enum.Enum):
ATIVO = 'ativo'
DESLIGADO = 'desligado'
SUSPENSO = 'suspenso'
AFASTADO = 'afastado'
class Celula(Base):
__tablename__ = 'celulas'
id = Column(Integer, primary_key=True, autoincrement=True)
nome = Column(String(100), nullable=False)
setor_id = Column(Integer, ForeignKey('setores.id', use_alter=True, name='fk_celula_setor'))
cr_id = Column(Integer, ForeignKey('comites_regionais.id', use_alter=True, name='fk_celula_cr'))
secretario = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_celula_secretario'))
responsavel_financas = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_celula_responsavel_financas'))
quadro_orientador = Column(String(255))
# Relacionamentos
setor = relationship("Setor", back_populates="celulas")
cr = relationship("ComiteRegional", back_populates="celulas")
militantes = relationship("Militante", back_populates="celula", foreign_keys="[Militante.celula_id]")
secretario_rel = relationship("Militante", foreign_keys=[secretario])
responsavel_financas_rel = relationship("Militante", foreign_keys=[responsavel_financas])
pagamentos = relationship("PagamentoCelula", back_populates="celula")
usuarios = relationship("Usuario", back_populates="celula")
class ComiteRegional(Base):
__tablename__ = 'comites_regionais'
id = Column(Integer, primary_key=True, autoincrement=True)
nome = Column(String(100), nullable=False)
responsavel_financas = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_cr_responsavel_financas'))
responsavel_formacao = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_cr_responsavel_formacao'))
secretario_organizacao = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_cr_secretario_organizacao'))
correspondente_jornal = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_cr_correspondente_jornal'))
# Relacionamentos
responsavel_financas_rel = relationship("Militante", foreign_keys=[responsavel_financas])
responsavel_formacao_rel = relationship("Militante", foreign_keys=[responsavel_formacao])
secretario_organizacao_rel = relationship("Militante", foreign_keys=[secretario_organizacao])
correspondente_jornal_rel = relationship("Militante", foreign_keys=[correspondente_jornal])
setores = relationship("Setor", back_populates="cr")
celulas = relationship("Celula", back_populates="cr")
usuarios = relationship("Usuario", back_populates="cr")
class EmailMilitante(Base):
__tablename__ = 'emails_militantes'
id = Column(Integer, primary_key=True, autoincrement=True)
militante_id = Column(Integer, ForeignKey('militantes.id'))
endereco_email = Column(String(100))
militante = relationship("Militante", back_populates="emails")
class Endereco(Base):
__tablename__ = 'enderecos'
id = Column(Integer, primary_key=True, autoincrement=True)
estado = Column(String(2))
cidade = Column(String(50))
bairro = Column(String(50))
rua = Column(String(100))
numero = Column(String(10))
complemento = Column(String(50))
cep = Column(String(9))
militantes = relationship("Militante", back_populates="endereco")
class RedeSocial(Base):
__tablename__ = 'redes_sociais'
id = Column(Integer, primary_key=True, autoincrement=True)
militante_id = Column(Integer, ForeignKey('militantes.id'))
tipo = Column(String(20)) # Instagram, TikTok, Discord, etc.
identificador = Column(String(100))
militante = relationship("Militante", back_populates="redes_sociais")
class Militante(Base):
__tablename__ = 'militantes'
id = Column(Integer, primary_key=True, autoincrement=True)
nome = Column(String(100), nullable=False)
cpf = Column(String(14), unique=True)
email = Column(String(100), unique=True)
telefone = Column(String(15))
endereco = Column(String(255))
filiado = Column(Boolean, default=False)
# Novos campos básicos
titulo_eleitoral = Column(String(20))
data_nascimento = Column(Date)
data_entrada_oci = Column(Date)
data_efetivacao_oci = Column(Date)
# Campos de contato
telefone1 = Column(String(15))
telefone2 = Column(String(15))
# Relacionamento para múltiplos emails
emails = relationship("EmailMilitante", back_populates="militante")
# Endereço
endereco_id = Column(Integer, ForeignKey('enderecos.id', use_alter=True, name='fk_militante_endereco'))
endereco = relationship("Endereco", back_populates="militantes")
# Redes sociais
redes_sociais = relationship("RedeSocial", back_populates="militante")
# Campos profissionais
profissao = Column(String(100))
regime_trabalho = Column(String(50)) # CLT, Estatutário, etc.
empresa = Column(String(100))
contratante = Column(String(100)) # Para terceirizados
# Campos acadêmicos
instituicao_ensino = Column(String(100))
tipo_instituicao = Column(String(20)) # Federal, Estadual, etc.
# Campos sindicais
sindicato = Column(String(100))
cargo_sindical = Column(String(50))
dirigente_sindical = Column(Boolean)
central_sindical = Column(String(100))
# Responsável pelo cadastro
registrado_por = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_militante_registrado_por'))
# Campos existentes
celula_id = Column(Integer, ForeignKey('celulas.id', use_alter=True, name='fk_militante_celula'))
responsabilidades = Column(Integer, default=0)
otp_secret = Column(String(32))
temp_token = Column(String(64))
temp_token_expiry = Column(DateTime)
# Novo campo para Quadro-Orientador
quadro_orientador = Column(Boolean, default=False)
# Campos para Aspirante
aspirante = Column(Boolean, default=True) # Por padrão, todo novo militante é aspirante
data_inicio_aspirante = Column(DateTime, default=datetime.utcnow)
avaliacao_aspirante = Column(Text)
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
cotas_mensais = relationship("CotaMensal", back_populates="militante")
pagamentos = relationship("Pagamento", back_populates="militante")
materiais_vendidos = relationship("MaterialVendido", back_populates="militante")
vendas_jornais = relationship("VendaJornalAvulso", back_populates="militante")
assinaturas = relationship("AssinaturaAnual", back_populates="militante")
celula = relationship("Celula", back_populates="militantes", foreign_keys=[celula_id])
# Constantes para responsabilidades
SECRETARIO = 1
TESOUREIRO = 2
IMPRENSA = 4
MNS = 8
MPS = 16
JUVENTUDE = 32
QUADRO_ORIENTADOR = 64
ASPIRANTE = 128
RESPONSAVEL_FINANCAS = 256
RESPONSAVEL_IMPRENSA = 512
@staticmethod
def get_responsabilidades_list():
return [
(Militante.SECRETARIO, "Secretário"),
(Militante.TESOUREIRO, "Tesoureiro"),
(Militante.IMPRENSA, "Imprensa"),
(Militante.MNS, "MNS"),
(Militante.MPS, "MPS"),
(Militante.JUVENTUDE, "Juventude"),
(Militante.QUADRO_ORIENTADOR, "Quadro-Orientador"),
(Militante.ASPIRANTE, "Aspirante"),
(Militante.RESPONSAVEL_FINANCAS, "Responsável de Finanças"),
(Militante.RESPONSAVEL_IMPRENSA, "Responsável de Imprensa")
]
def set_responsabilidades(self, resp_list):
"""
Define as responsabilidades do militante
resp_list: lista de inteiros representando as responsabilidades
"""
self.responsabilidades = sum(resp_list)
def get_responsabilidades(self):
"""
Retorna lista de responsabilidades ativas
"""
resp = []
for valor, nome in self.get_responsabilidades_list():
if self.responsabilidades & valor:
resp.append(nome)
return resp
def generate_temp_token(self):
"""
Gera um token temporário para acesso ao QR code
"""
self.temp_token = secrets.token_urlsafe(32)
self.temp_token_expiry = datetime.now() + timedelta(hours=48)
return self.temp_token
def send_otp_email(self, mail):
"""
Envia email com link para QR code
"""
token = self.generate_temp_token()
qr_url = url_for('get_qr_code', token=token, _external=True)
msg = Message(
'Configuração de Autenticação em Duas Etapas',
recipients=[self.email]
)
msg.body = f"""
Olá {self.nome},
Para configurar sua autenticação em duas etapas, acesse o link abaixo:
{qr_url}
Este link expirará em 48 horas.
Instruções:
1. Instale um aplicativo autenticador (Google Authenticator, Microsoft Authenticator)
2. Acesse o link acima
3. Escaneie o QR code com o aplicativo
4. Use o código gerado para fazer login no sistema
Atenciosamente,
Sistema de Controles
"""
mail.send(msg)
def generate_username(self):
"""Gera um nome de usuário único baseado no primeiro nome e um código"""
from sqlalchemy import func
db = get_db_connection()
try:
# Pega o primeiro nome
primeiro_nome = self.nome.split()[0].lower()
# Conta quantos usuários já existem com esse prefixo
count = db.query(func.count(Usuario.id)).filter(
Usuario.username.like(f"{primeiro_nome}%")
).scalar()
# Gera o código (número sequencial)
codigo = str(count + 1).zfill(3)
return f"{primeiro_nome}{codigo}"
finally:
db.close()
class CotaMensal(Base):
__tablename__ = 'cotas_mensais'
@@ -71,6 +302,8 @@ class CotaMensal(Base):
valor_antigo = Column(Numeric(10, 2), nullable=False)
valor_novo = Column(Numeric(10, 2), 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")
@@ -80,19 +313,22 @@ class TipoPagamento(Base):
id = Column(Integer, primary_key=True, autoincrement=True)
descricao = Column(String(100), nullable=False)
pagamentos = relationship("Pagamento", back_populates="tipo_pagamento")
class Pagamento(Base):
__tablename__ = 'pagamentos'
id = Column(Integer, primary_key=True, autoincrement=True)
militante_id = Column(Integer, ForeignKey('militantes.id'))
tipo_pagamento_id = Column(Integer, ForeignKey('tipos_pagamento.id'))
tipo_pagamento = Column(String(50)) # Cota, Jornal, Assinatura, etc.
mes_referencia = Column(Date)
numero_jornal = Column(String(20))
numero_inicial_assinatura = Column(String(20))
numero_final_assinatura = Column(String(20))
campanha_financeira = Column(String(50))
valor = Column(Numeric(10, 2), nullable=False)
data_pagamento = Column(Date, nullable=False)
militante = relationship("Militante", back_populates="pagamentos")
tipo_pagamento = relationship("TipoPagamento", back_populates="pagamentos")
transacoes_pix = relationship("TransacaoPIX", back_populates="pagamento")
class TipoMaterial(Base):
__tablename__ = 'tipos_materiais'
@@ -144,12 +380,20 @@ class AssinaturaAnual(Base):
class Setor(Base):
__tablename__ = 'setores'
id = Column(Integer, primary_key=True, autoincrement=True)
id = Column(Integer, primary_key=True)
nome = Column(String(100), nullable=False)
cr_id = Column(Integer, ForeignKey('comites_regionais.id', use_alter=True, name='fk_setor_cr'))
responsavel = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_setor_responsavel'))
responsavel_financas = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_setor_responsavel_financas'))
# Relacionamentos
cr = relationship("ComiteRegional", back_populates="setores")
responsavel_rel = relationship("Militante", foreign_keys=[responsavel])
responsavel_financas_rel = relationship("Militante", foreign_keys=[responsavel_financas])
usuarios = relationship("Usuario", back_populates="setor")
celulas = relationship("Celula", back_populates="setor")
relatorios_cotas = relationship("RelatorioCotasMensais", back_populates="setor")
relatorios_vendas = relationship("RelatorioVendasMateriais", back_populates="setor")
usuarios = relationship("Usuario", back_populates="setor")
class ComiteCentral(Base):
__tablename__ = 'comites_centrais'
@@ -184,13 +428,20 @@ class RelatorioVendasMateriais(Base):
setor = relationship("Setor", back_populates="relatorios_vendas")
comite = relationship("ComiteCentral", back_populates="relatorios_vendas")
class Usuario(Base):
class TipoUsuario(enum.Enum):
ADMIN = "admin"
CR_RESPONSAVEL = "cr_responsavel"
SETOR_RESPONSAVEL = "setor_responsavel"
USUARIO = "usuario"
class Usuario(Base, UserMixin):
__tablename__ = 'usuarios'
id = Column(Integer, primary_key=True, autoincrement=True)
id = Column(Integer, primary_key=True)
username = Column(String(50), unique=True, nullable=False)
password_hash = Column(String(255), nullable=False)
email = Column(String(100), unique=True, nullable=False)
nome = Column(String(100)) # Nome completo do usuário
otp_secret = Column(String(32))
role_id = Column(Integer, ForeignKey('roles.id'))
setor_id = Column(Integer, ForeignKey('setores.id'))
@@ -199,119 +450,266 @@ class Usuario(Base):
ultimo_login = Column(DateTime)
ultimo_logout = Column(DateTime)
motivo_logout = Column(String(100))
cr_id = Column(Integer, ForeignKey('comites_regionais.id'))
celula_id = Column(Integer, ForeignKey('celulas.id'))
session_timeout = Column(Integer, default=30)
tipo = Column(String(17), nullable=False)
ultima_atividade = Column(DateTime, default=datetime.utcnow)
# Relacionamento com militante
militante_id = Column(Integer, ForeignKey('militantes.id'))
militante = relationship("Militante", backref=backref("usuario", uselist=False))
role = relationship("Role", back_populates="usuarios")
setor = relationship("Setor", back_populates="usuarios")
# Relacionamentos
roles = relationship("Role", secondary="user_roles", back_populates="users")
setor = relationship('Setor', back_populates='usuarios')
cr = relationship('ComiteRegional', back_populates='usuarios')
celula = relationship('Celula', back_populates='usuarios')
def __init__(self, username, password, is_admin=False):
def __init__(self, username, email=None, is_admin=False, nome=None):
self.username = username
self.password_hash = generate_password_hash(password)
self.email = email
self.is_admin = is_admin
self.otp_secret = pyotp.random_base32() # Gerar segredo OTP na criação
self.nome = nome
self.ativo = True
self.session_timeout = 30
self.tipo = "USUARIO"
self.ultima_atividade = datetime.utcnow()
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
def verify_otp(self, otp_code):
"""Verifica se o código OTP fornecido é válido"""
if not self.otp_secret:
print(f"Erro: Usuário {self.username} não tem segredo OTP configurado")
return False
def update_last_activity(self):
self.ultima_atividade = datetime.utcnow()
def is_session_expired(self):
if not self.ultima_atividade:
return True
time_diff = datetime.utcnow() - self.ultima_atividade
return time_diff.total_seconds() > (self.session_timeout * 60)
def check_session_timeout(self):
"""Verifica se a sessão do usuário expirou"""
if not self.ultima_atividade:
return True
time_diff = datetime.utcnow() - self.ultima_atividade
return time_diff.total_seconds() > (self.session_timeout * 60)
def has_permission(self, permission_name):
"""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
totp = pyotp.TOTP(self.otp_secret)
is_valid = totp.verify(otp_code)
print(f"Verificando OTP para {self.username}")
print(f"Segredo: {self.otp_secret}")
print(f"Código fornecido: {otp_code}")
print(f"Resultado: {'válido' if is_valid else 'inválido'}")
return is_valid
# Verifica se o usuário tem a permissão através de suas roles
for role in self.roles:
for permission in role.permissions:
if permission.nome == permission_name:
return True
return False
def has_role(self, role_nivel):
"""Verifica se o usuário tem um determinado nível de role"""
for role in self.roles:
if role.nivel == role_nivel:
return True
return False
def get_otp_uri(self):
"""Gera a URI para o QR code do OTP"""
"""Gera a URI para autenticação em duas etapas"""
if not self.otp_secret:
self.otp_secret = pyotp.random_base32()
totp = pyotp.TOTP(self.otp_secret)
return totp.provisioning_uri(
name=self.username,
return pyotp.totp.TOTP(self.otp_secret).provisioning_uri(
self.username,
issuer_name="Sistema de Controles"
)
class Role(Base):
__tablename__ = 'roles'
id = Column(Integer, primary_key=True, autoincrement=True)
nome = Column(String(50), unique=True, nullable=False)
nivel = Column(Integer, nullable=False) # Nível hierárquico (1: admin, 2: coordenador, 3: militante)
usuarios = relationship("Usuario", back_populates="role")
permissoes = relationship("RolePermissao", back_populates="role")
def verify_otp(self, code):
"""Verifica se um código OTP é válido"""
if not self.otp_secret:
print(f"Erro: OTP secret não configurado para o usuário {self.username}")
return False
print(f"Verificando OTP para usuário {self.username}")
print(f"OTP Secret: {self.otp_secret}")
print(f"Código fornecido: {code}")
totp = pyotp.totp.TOTP(self.otp_secret)
is_valid = totp.verify(code)
print(f"Resultado da verificação: {'Válido' if is_valid else 'Inválido'}")
print(f"Tempo atual: {datetime.utcnow()}")
print(f"Período atual: {totp.timecode(datetime.utcnow())}")
return is_valid
class Permissao(Base):
__tablename__ = 'permissoes'
def logout(self):
"""Registra o logout do usuário"""
self.ultimo_logout = datetime.utcnow()
self.motivo_logout = "Logout manual"
self.ultima_atividade = None
def is_admin_user(self):
"""Verifica se o usuário é admin"""
return self.is_admin or any(role.nome == "admin" for role in self.roles)
class PagamentoCelula(Base):
__tablename__ = 'pagamentos_celula'
id = Column(Integer, primary_key=True, autoincrement=True)
nome = Column(String(50), unique=True, nullable=False)
celula_id = Column(Integer, ForeignKey('celulas.id'))
data = Column(Date)
valor = Column(Numeric(10, 2))
metodo_pagamento = Column(String(20)) # PIX, Dinheiro, etc.
codigo_pix = Column(String(100))
descricao = Column(String(255))
registrado_por = Column(Integer, ForeignKey('militantes.id'))
roles = relationship("RolePermissao", back_populates="permissao")
celula = relationship("Celula", back_populates="pagamentos")
registrado_por_rel = relationship("Militante", foreign_keys=[registrado_por])
class RolePermissao(Base):
__tablename__ = 'roles_permissoes'
class Atividade(Base):
__tablename__ = 'atividades'
role_id = Column(Integer, ForeignKey('roles.id'), primary_key=True)
permissao_id = Column(Integer, ForeignKey('permissoes.id'), primary_key=True)
id = Column(Integer, primary_key=True, autoincrement=True)
descricao = Column(String(255))
data = Column(Date)
responsavel1 = Column(Integer, ForeignKey('militantes.id'))
responsavel2 = Column(Integer, ForeignKey('militantes.id'))
role = relationship("Role", back_populates="permissoes")
permissao = relationship("Permissao", back_populates="roles")
responsavel1_rel = relationship("Militante", foreign_keys=[responsavel1])
responsavel2_rel = relationship("Militante", foreign_keys=[responsavel2])
materiais = relationship("MaterialAtividade", back_populates="atividade")
# Remover o banco de dados existente (se existir)
if os.path.exists(db_path):
os.remove(db_path)
class MaterialAtividade(Base):
__tablename__ = 'materiais_atividades'
id = Column(Integer, primary_key=True, autoincrement=True)
atividade_id = Column(Integer, ForeignKey('atividades.id'))
tipo = Column(String(20)) # Jornal, Revista, etc.
quantidade = Column(Integer)
detalhes = Column(String(255))
atividade = relationship("Atividade", back_populates="materiais")
class Relatorio(Base):
__tablename__ = 'relatorios'
id = Column(Integer, primary_key=True, autoincrement=True)
tipo = Column(String(50)) # Semanal, Quinzenal, Mensal
periodo_inicio = Column(Date)
periodo_fim = Column(Date)
gerado_por = Column(Integer, ForeignKey('militantes.id'))
conteudo = Column(Text)
# Relacionamento hierárquico
celula_id = Column(Integer, ForeignKey('celulas.id'))
setor_id = Column(Integer, ForeignKey('setores.id'))
cr_id = Column(Integer, ForeignKey('comites_regionais.id'))
gerado_por_rel = relationship("Militante", foreign_keys=[gerado_por])
celula = relationship("Celula", foreign_keys=[celula_id])
setor = relationship("Setor", foreign_keys=[setor_id])
cr = relationship("ComiteRegional", foreign_keys=[cr_id])
class TransacaoPIX(Base):
__tablename__ = 'transacoes_pix'
id = Column(Integer, primary_key=True, autoincrement=True)
chave_pix = Column(String(100))
valor = Column(Numeric(10, 2))
data_geracao = Column(DateTime)
data_pagamento = Column(DateTime)
status = Column(String(20)) # Pendente, Pago, Expirado
qr_code = Column(Text)
pagamento_id = Column(Integer, ForeignKey('pagamentos.id'))
pagamento = relationship("Pagamento", back_populates="transacoes_pix")
def init_database():
"""Inicializa o banco de dados com dados básicos"""
print("Inicializando banco de dados...")
# Criar todas as tabelas
Base.metadata.create_all(engine)
session = SessionLocal()
session = get_db_connection()
try:
# Verificar se já existe um admin
admin = session.query(Usuario).filter_by(username="admin").first()
# Criar todas as tabelas
Base.metadata.drop_all(engine) # Remover todas as tabelas existentes
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()
# Criar setores padrão
setores = ["Setor 1", "Setor 2", "Setor 3"]
for nome in setores:
if not session.query(Setor).filter_by(nome=nome).first():
setor = Setor(nome=nome)
session.add(setor)
session.commit()
# Criar comitês padrão
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()
# Gerar OTP para admin
admin_otp_secret = pyotp.random_base32()
print(f"Novo OTP gerado: {admin_otp_secret}")
# Criar usuário admin
admin_role = session.query(Role).filter_by(nome="Administrador").first()
setor = session.query(Setor).first()
admin = Usuario(
username="admin",
email="admin@example.com",
is_admin=True
)
admin.set_password("admin123")
admin.tipo = "ADMIN"
admin.otp_secret = admin_otp_secret
admin.roles.append(admin_role)
admin.setor = setor
session.add(admin)
session.commit()
# Gerar QR code
totp = pyotp.totp.TOTP(admin_otp_secret)
provisioning_uri = totp.provisioning_uri("admin", issuer_name="Sistema de Controles")
import qrcode
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")
img.save('admin_qr.png')
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}")
print(f"QR Code: admin_qr.png")
# 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!")
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=1)
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
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")
except Exception as e:
print(f"Erro na inicialização do banco: {e}")
session.rollback()
@@ -319,9 +717,5 @@ def init_database():
finally:
session.close()
# Inicializar o banco de dados automaticamente quando o módulo for importado
init_database()
# Executar a criação dos dados iniciais
if __name__ == "__main__":
init_database()

204
functions/decorators.py Normal file
View File

@@ -0,0 +1,204 @@
from functools import wraps
from flask import session, redirect, url_for, flash
from flask_login import current_user, login_required
from sqlalchemy.orm import joinedload
from .database import get_db_connection, Usuario, Role
from .rbac import Permission
def require_login(f):
"""Decorador para verificar se o usuário está logado"""
@wraps(f)
def decorated_function(*args, **kwargs):
if not current_user.is_authenticated:
flash('Por favor, faça login para acessar esta página.', 'danger')
return redirect(url_for('login'))
db = get_db_connection()
try:
# Carregar o usuário com suas roles e permissões
user = db.query(Usuario).options(
joinedload(Usuario.roles).joinedload(Role.permissions),
joinedload(Usuario.militante),
joinedload(Usuario.cr),
joinedload(Usuario.setor),
joinedload(Usuario.celula)
).get(current_user.id)
if not user:
flash('Usuário não encontrado.', 'danger')
return redirect(url_for('login'))
# Atualiza timestamp da última atividade
user.update_last_activity()
db.commit()
# Substituir o current_user pelo usuário carregado
setattr(current_user, '_get_current_object', lambda: user)
# Executar a função com o usuário carregado
return f(*args, **kwargs)
except Exception as e:
db.rollback()
flash('Erro ao carregar dados do usuário.', 'danger')
return redirect(url_for('login'))
finally:
db.close()
return decorated_function
def require_permission(permission_name):
"""Decorador para verificar se o usuário tem uma permissão específica"""
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not current_user.is_authenticated:
flash('Você precisa estar logado para acessar esta página.', 'error')
return redirect(url_for('login'))
db = get_db_connection()
try:
# Carregar o usuário com suas roles e permissões
user = db.query(Usuario).options(
joinedload(Usuario.roles).joinedload(Role.permissions),
joinedload(Usuario.militante),
joinedload(Usuario.cr),
joinedload(Usuario.setor),
joinedload(Usuario.celula)
).get(current_user.id)
if not user:
flash('Usuário não encontrado.', 'error')
return redirect(url_for('login'))
if not user.has_permission(permission_name):
flash('Você não tem permissão para acessar esta página.', 'error')
return redirect(url_for('index'))
# Atualiza timestamp da última atividade
user.update_last_activity()
db.commit()
# Substituir o current_user pelo usuário carregado
setattr(current_user, '_get_current_object', lambda: user)
return f(*args, **kwargs)
finally:
db.close()
return decorated_function
return decorator
def require_role(role_name):
"""Decorador para verificar se o usuário tem um papel específico"""
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not current_user.is_authenticated:
flash('Você precisa estar logado para acessar esta página.', 'error')
return redirect(url_for('login'))
db = get_db_connection()
try:
user = db.query(Usuario).get(current_user.id)
if not user or not user.has_role(role_name):
flash('Você não tem permissão para acessar esta página.', 'error')
return redirect(url_for('index'))
# Atualiza timestamp da última atividade
user.update_last_activity()
db.commit()
return f(*args, **kwargs)
finally:
db.close()
return decorated_function
return decorator
def require_minimum_role(min_level):
"""Decorador para verificar se o usuário tem um papel com nível mínimo"""
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not current_user.is_authenticated:
flash('Você precisa estar logado para acessar esta página.', 'error')
return redirect(url_for('login'))
db = get_db_connection()
try:
user = db.query(Usuario).get(current_user.id)
if not user:
flash('Usuário não encontrado.', 'error')
return redirect(url_for('login'))
highest_role = user.get_highest_role()
if not highest_role or highest_role.nivel < min_level:
flash('Você não tem permissão para acessar esta página.', 'error')
return redirect(url_for('index'))
# Atualiza timestamp da última atividade
user.update_last_activity()
db.commit()
return f(*args, **kwargs)
finally:
db.close()
return decorated_function
return decorator
def require_instance_permission(permission_name, instance_param):
"""Decorator para verificar se o usuário tem permissão em uma instância específica"""
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not current_user.is_authenticated:
flash('Por favor, faça login para acessar esta página.', 'error')
return redirect(url_for('login'))
# Obtém o ID da instância dos argumentos da função
instance_id = kwargs.get(instance_param)
if instance_id is None:
flash('ID da instância não encontrado.', 'error')
return redirect(url_for('home'))
if not current_user.has_instance_permission(permission_name, instance_id):
flash('Você não tem permissão para acessar esta instância.', 'error')
return redirect(url_for('home'))
return f(*args, **kwargs)
return decorated_function
return decorator
def require_instance_access(instance_type, instance_id):
"""Decorator para verificar se o usuário tem acesso a uma instância específica"""
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not current_user.is_authenticated:
flash('Por favor, faça login para acessar esta página.', 'error')
return redirect(url_for('login'))
# Verificar acesso baseado na instância do usuário
if instance_type == 'celula':
if not (current_user.celula_id == instance_id or
current_user.has_permission(Permission.VIEW_SECTOR_REPORTS) or
current_user.has_permission(Permission.VIEW_CR_REPORTS) or
current_user.has_permission(Permission.VIEW_CC_REPORTS)):
flash('Você não tem acesso a esta célula.', 'error')
return redirect(url_for('index'))
elif instance_type == 'setor':
if not (current_user.setor_id == instance_id or
current_user.has_permission(Permission.VIEW_CR_REPORTS) or
current_user.has_permission(Permission.VIEW_CC_REPORTS)):
flash('Você não tem acesso a este setor.', 'error')
return redirect(url_for('index'))
elif instance_type == 'cr':
if not (current_user.cr_id == instance_id or
current_user.has_permission(Permission.VIEW_CC_REPORTS)):
flash('Você não tem acesso a este CR.', 'error')
return redirect(url_for('index'))
# Atualiza timestamp da última atividade
current_user.update_last_activity()
db_session.commit()
return f(*args, **kwargs)
return decorated_function
return decorator

1
functions/notificacao.py Normal file
View File

@@ -0,0 +1 @@

222
functions/permissions.py Normal file
View File

@@ -0,0 +1,222 @@
from functools import wraps
from flask import abort, g
from .database import Militante, Celula, Setor, CR, CC
def check_permission(permission_func):
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not permission_func(*args, **kwargs):
abort(403)
return f(*args, **kwargs)
return decorated_function
return decorator
def can_manage_militante(militante_id):
"""Verifica se o usuário atual pode gerenciar um militante específico."""
if not g.user or not g.user.militante:
return False
militante = Militante.query.get(militante_id)
if not militante:
return False
# Secretário Geral e Secretário de Organização podem gerenciar qualquer militante
if g.user.militante.responsabilidades & (Militante.SECRETARIO_GERAL | Militante.SECRETARIO_ORGANIZACAO):
return True
# Secretário de CC pode gerenciar militantes do seu CC
if g.user.militante.responsabilidades & Militante.SECRETARIO_CC:
if militante.celula.setor.cr.cc_id == g.user.militante.celula.setor.cr.cc_id:
return True
# Secretário de CR pode gerenciar militantes do seu CR
if g.user.militante.responsabilidades & Militante.SECRETARIO_CR:
if militante.celula.setor.cr_id == g.user.militante.celula.setor.cr_id:
return True
# Secretário de Setor pode gerenciar militantes do seu setor
if g.user.militante.responsabilidades & Militante.SECRETARIO_SETOR:
if militante.celula.setor_id == g.user.militante.celula.setor_id:
return True
# Secretário de Célula pode gerenciar militantes da sua célula
if g.user.militante.responsabilidades & Militante.SECRETARIO_CELULA:
if militante.celula_id == g.user.militante.celula_id:
return True
return False
def can_manage_celula(celula_id):
"""Verifica se o usuário atual pode gerenciar uma célula específica."""
if not g.user or not g.user.militante:
return False
celula = Celula.query.get(celula_id)
if not celula:
return False
# Secretário Geral e Secretário de Organização podem gerenciar qualquer célula
if g.user.militante.responsabilidades & (Militante.SECRETARIO_GERAL | Militante.SECRETARIO_ORGANIZACAO):
return True
# Secretário de CC pode gerenciar células do seu CC
if g.user.militante.responsabilidades & Militante.SECRETARIO_CC:
if celula.setor.cr.cc_id == g.user.militante.celula.setor.cr.cc_id:
return True
# Secretário de CR pode gerenciar células do seu CR
if g.user.militante.responsabilidades & Militante.SECRETARIO_CR:
if celula.setor.cr_id == g.user.militante.celula.setor.cr_id:
return True
# Secretário de Setor pode gerenciar células do seu setor
if g.user.militante.responsabilidades & Militante.SECRETARIO_SETOR:
if celula.setor_id == g.user.militante.celula.setor_id:
return True
return False
def can_manage_setor(setor_id):
"""Verifica se o usuário atual pode gerenciar um setor específico."""
if not g.user or not g.user.militante:
return False
setor = Setor.query.get(setor_id)
if not setor:
return False
# Secretário Geral e Secretário de Organização podem gerenciar qualquer setor
if g.user.militante.responsabilidades & (Militante.SECRETARIO_GERAL | Militante.SECRETARIO_ORGANIZACAO):
return True
# Secretário de CC pode gerenciar setores do seu CC
if g.user.militante.responsabilidades & Militante.SECRETARIO_CC:
if setor.cr.cc_id == g.user.militante.celula.setor.cr.cc_id:
return True
# Secretário de CR pode gerenciar setores do seu CR
if g.user.militante.responsabilidades & Militante.SECRETARIO_CR:
if setor.cr_id == g.user.militante.celula.setor.cr_id:
return True
return False
def can_manage_cr(cr_id):
"""Verifica se o usuário atual pode gerenciar um CR específico."""
if not g.user or not g.user.militante:
return False
cr = CR.query.get(cr_id)
if not cr:
return False
# Secretário Geral e Secretário de Organização podem gerenciar qualquer CR
if g.user.militante.responsabilidades & (Militante.SECRETARIO_GERAL | Militante.SECRETARIO_ORGANIZACAO):
return True
# Secretário de CC pode gerenciar CRs do seu CC
if g.user.militante.responsabilidades & Militante.SECRETARIO_CC:
if cr.cc_id == g.user.militante.celula.setor.cr.cc_id:
return True
return False
def can_manage_cc(cc_id):
"""Verifica se o usuário atual pode gerenciar um CC específico."""
if not g.user or not g.user.militante:
return False
# Apenas Secretário Geral e Secretário de Organização podem gerenciar CCs
if g.user.militante.responsabilidades & (Militante.SECRETARIO_GERAL | Militante.SECRETARIO_ORGANIZACAO):
return True
return False
def can_manage_financas(instancia_id, tipo_instancia):
"""Verifica se o usuário atual pode gerenciar finanças de uma instância específica."""
if not g.user or not g.user.militante:
return False
# Secretário Geral e Secretário de Organização podem gerenciar finanças de qualquer instância
if g.user.militante.responsabilidades & (Militante.SECRETARIO_GERAL | Militante.SECRETARIO_ORGANIZACAO):
return True
# Responsável de Finanças da instância pode gerenciar suas finanças
if tipo_instancia == 'celula':
celula = Celula.query.get(instancia_id)
if celula and celula.responsavel_financas_id == g.user.militante.id:
return True
elif tipo_instancia == 'setor':
setor = Setor.query.get(instancia_id)
if setor and setor.responsavel_financas_id == g.user.militante.id:
return True
elif tipo_instancia == 'cr':
cr = CR.query.get(instancia_id)
if cr and cr.responsavel_financas_id == g.user.militante.id:
return True
elif tipo_instancia == 'cc':
cc = CC.query.get(instancia_id)
if cc and cc.responsavel_financas_id == g.user.militante.id:
return True
return False
def can_manage_imprensa(instancia_id, tipo_instancia):
"""Verifica se o usuário atual pode gerenciar imprensa de uma instância específica."""
if not g.user or not g.user.militante:
return False
# Secretário Geral e Secretário de Organização podem gerenciar imprensa de qualquer instância
if g.user.militante.responsabilidades & (Militante.SECRETARIO_GERAL | Militante.SECRETARIO_ORGANIZACAO):
return True
# Responsável de Imprensa da instância pode gerenciar sua imprensa
if tipo_instancia == 'celula':
celula = Celula.query.get(instancia_id)
if celula and celula.responsavel_imprensa_id == g.user.militante.id:
return True
elif tipo_instancia == 'setor':
setor = Setor.query.get(instancia_id)
if setor and setor.responsavel_imprensa_id == g.user.militante.id:
return True
elif tipo_instancia == 'cr':
cr = CR.query.get(instancia_id)
if cr and cr.responsavel_imprensa_id == g.user.militante.id:
return True
elif tipo_instancia == 'cc':
cc = CC.query.get(instancia_id)
if cc and cc.responsavel_imprensa_id == g.user.militante.id:
return True
return False
def can_manage_responsabilidades(militante_id):
"""Verifica se o usuário atual pode gerenciar responsabilidades de um militante específico."""
if not g.user or not g.user.militante:
return False
militante = Militante.query.get(militante_id)
if not militante:
return False
# Secretário Geral e Secretário de Organização podem gerenciar responsabilidades de qualquer militante
if g.user.militante.responsabilidades & (Militante.SECRETARIO_GERAL | Militante.SECRETARIO_ORGANIZACAO):
return True
# Secretário de CC pode gerenciar responsabilidades de militantes do seu CC
if g.user.militante.responsabilidades & Militante.SECRETARIO_CC:
if militante.celula.setor.cr.cc_id == g.user.militante.celula.setor.cr.cc_id:
return True
# Secretário de CR pode gerenciar responsabilidades de militantes do seu CR
if g.user.militante.responsabilidades & Militante.SECRETARIO_CR:
if militante.celula.setor.cr_id == g.user.militante.celula.setor.cr_id:
return True
# Secretário de Setor pode gerenciar responsabilidades de militantes do seu setor
if g.user.militante.responsabilidades & Militante.SECRETARIO_SETOR:
if militante.celula.setor_id == g.user.militante.celula.setor_id:
return True
return False

315
functions/rbac.py Normal file
View File

@@ -0,0 +1,315 @@
from sqlalchemy import Column, Integer, String, Text, ForeignKey, Table
from sqlalchemy.orm import relationship
from .base import Base
# Tabela de mapeamento Role-Permission
role_permissions = Table(
'role_permissions',
Base.metadata,
Column('role_id', Integer, ForeignKey('roles.id'), primary_key=True),
Column('permission_id', Integer, ForeignKey('permissions.id'), primary_key=True)
)
# Tabela de mapeamento User-Role
user_roles = Table(
'user_roles',
Base.metadata,
Column('user_id', Integer, ForeignKey('usuarios.id'), primary_key=True),
Column('role_id', Integer, ForeignKey('roles.id'), primary_key=True)
)
class Role(Base):
__tablename__ = 'roles'
id = Column(Integer, primary_key=True, autoincrement=True)
nome = Column(String(50), unique=True, nullable=False)
nivel = Column(Integer, nullable=False) # Nível hierárquico
descricao = Column(Text)
# Relacionamentos
permissions = relationship("Permission", secondary=role_permissions, back_populates="roles")
users = relationship("Usuario", secondary=user_roles, back_populates="roles")
# Níveis de role
MILITANTE_BASICO = 1
SECRETARIO_CELULA = 2
MEMBRO_SETOR = 3
SECRETARIO_SETOR = 4
MEMBRO_CR = 5
SECRETARIO_CR = 6
MEMBRO_CC = 7
SECRETARIO_GERAL = 8
@staticmethod
def get_roles_list():
return [
(Role.MILITANTE_BASICO, "Militante Básico"),
(Role.SECRETARIO_CELULA, "Secretário de Célula"),
(Role.MEMBRO_SETOR, "Membro de Setor"),
(Role.SECRETARIO_SETOR, "Secretário de Setor"),
(Role.MEMBRO_CR, "Membro de CR"),
(Role.SECRETARIO_CR, "Secretário de CR"),
(Role.MEMBRO_CC, "Membro do CC"),
(Role.SECRETARIO_GERAL, "Secretário Geral")
]
class Permission(Base):
__tablename__ = 'permissions'
id = Column(Integer, primary_key=True, autoincrement=True)
nome = Column(String(50), unique=True, nullable=False)
descricao = Column(Text)
# Relacionamentos
roles = relationship("Role", secondary=role_permissions, back_populates="permissions")
# Permissões básicas
VIEW_OWN_DATA = "view_own_data"
EDIT_OWN_DATA = "edit_own_data"
VIEW_CELL_DATA = "view_cell_data"
CREATE_MILITANT = "create_militant" # Nova permissão para criar militantes
# Permissões de célula
MANAGE_CELL_MEMBERS = "manage_cell_members"
CREATE_CELL_MEMBER = "create_cell_member"
VIEW_CELL_REPORTS = "view_cell_reports"
MANAGE_CELL_REPORTS = "manage_cell_reports" # Nova permissão
REGISTER_CELL_PAYMENT = "register_cell_payment"
# Permissões de setor
MANAGE_SECTOR_CELLS = "manage_sector_cells"
CREATE_SECTOR_CELL = "create_sector_cell"
VIEW_SECTOR_REPORTS = "view_sector_reports"
REGISTER_SECTOR_PAYMENT = "register_sector_payment"
# Permissões de CR
MANAGE_CR_SECTORS = "manage_cr_sectors"
CREATE_CR_SECTOR = "create_cr_sector"
VIEW_CR_REPORTS = "view_cr_reports"
REGISTER_CR_PAYMENT = "register_cr_payment"
# Permissões de CC
MANAGE_CC_CRS = "manage_cc_crs"
CREATE_CC_CR = "create_cc_cr"
VIEW_CC_REPORTS = "view_cc_reports"
REGISTER_CC_PAYMENT = "register_cc_payment"
SYSTEM_CONFIG = "system_config"
@staticmethod
def get_permissions_list():
return [
# Permissões básicas
(Permission.VIEW_OWN_DATA, "Visualizar próprios dados"),
(Permission.EDIT_OWN_DATA, "Editar próprios dados"),
(Permission.VIEW_CELL_DATA, "Visualizar dados da célula"),
(Permission.CREATE_MILITANT, "Criar novos militantes"), # Nova permissão
# Permissões de célula
(Permission.MANAGE_CELL_MEMBERS, "Gerenciar membros da célula"),
(Permission.CREATE_CELL_MEMBER, "Criar membros na célula"),
(Permission.VIEW_CELL_REPORTS, "Visualizar relatórios da célula"),
(Permission.MANAGE_CELL_REPORTS, "Gerenciar relatórios da célula"), # Nova permissão
(Permission.REGISTER_CELL_PAYMENT, "Registrar pagamentos da célula"),
# Permissões de setor
(Permission.MANAGE_SECTOR_CELLS, "Gerenciar células do setor"),
(Permission.CREATE_SECTOR_CELL, "Criar células no setor"),
(Permission.VIEW_SECTOR_REPORTS, "Visualizar relatórios do setor"),
(Permission.REGISTER_SECTOR_PAYMENT, "Registrar pagamentos do setor"),
# Permissões de CR
(Permission.MANAGE_CR_SECTORS, "Gerenciar setores do CR"),
(Permission.CREATE_CR_SECTOR, "Criar setores no CR"),
(Permission.VIEW_CR_REPORTS, "Visualizar relatórios do CR"),
(Permission.REGISTER_CR_PAYMENT, "Registrar pagamentos do CR"),
# Permissões de CC
(Permission.MANAGE_CC_CRS, "Gerenciar CRs"),
(Permission.CREATE_CC_CR, "Criar CRs"),
(Permission.VIEW_CC_REPORTS, "Visualizar relatórios nacionais"),
(Permission.REGISTER_CC_PAYMENT, "Registrar pagamentos nacionais"),
(Permission.SYSTEM_CONFIG, "Configurar sistema")
]
def init_rbac():
"""Inicializa o sistema RBAC com roles e permissões básicas"""
from .database import Usuario, get_db_connection
session = get_db_connection()
try:
# Criar role de administrador primeiro
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()
# 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():
permission = session.query(Permission).filter_by(nome=nome).first()
if not permission:
permission = Permission(nome=nome, descricao=descricao)
session.add(permission)
session.commit()
# Dar todas as permissões para o admin
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
if role.nivel == Role.MILITANTE_BASICO:
role.permissions = [
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first()
]
# Secretário de Célula
elif role.nivel == Role.SECRETARIO_CELULA:
role.permissions = [
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).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.VIEW_CELL_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
session.query(Permission).filter_by(nome=Permission.REGISTER_CELL_PAYMENT).first()
]
# Membro de Setor
elif role.nivel == Role.MEMBRO_SETOR:
role.permissions = [
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).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.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.REGISTER_SECTOR_PAYMENT).first()
]
# Secretário de Setor
elif role.nivel == Role.SECRETARIO_SETOR:
role.permissions = [
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).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.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.MANAGE_SECTOR_CELLS).first(),
session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(),
session.query(Permission).filter_by(nome=Permission.REGISTER_SECTOR_PAYMENT).first()
]
# Membro de CR
elif role.nivel == Role.MEMBRO_CR:
role.permissions = [
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).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.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.MANAGE_SECTOR_CELLS).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.REGISTER_CR_PAYMENT).first()
]
# Secretário de CR
elif role.nivel == Role.SECRETARIO_CR:
role.permissions = [
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).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.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.MANAGE_SECTOR_CELLS).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.MANAGE_CR_SECTORS).first(),
session.query(Permission).filter_by(nome=Permission.CREATE_CR_SECTOR).first(),
session.query(Permission).filter_by(nome=Permission.REGISTER_CR_PAYMENT).first()
]
# Membro do CC
elif role.nivel == Role.MEMBRO_CC:
role.permissions = [
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).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.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.MANAGE_SECTOR_CELLS).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.MANAGE_CR_SECTORS).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.REGISTER_CC_PAYMENT).first()
]
# Secretário Geral
elif role.nivel == Role.SECRETARIO_GERAL:
role.permissions = [
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).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.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.MANAGE_SECTOR_CELLS).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.MANAGE_CR_SECTORS).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.MANAGE_CC_CRS).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.SYSTEM_CONFIG).first()
]
session.commit()
except Exception as e:
print(f"Erro ao inicializar RBAC: {e}")
session.rollback()
raise
finally:
session.close()

1
functions/relatorio.py Normal file
View File

@@ -0,0 +1 @@

19
init_db.py Normal file
View 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!")

58
init_system.py Normal file
View File

@@ -0,0 +1,58 @@
from create_admin import create_admin
from create_test_users import create_test_users
from functions.database import get_db_connection, Usuario
from functions.rbac import Role
def init_system():
print("=== Inicializando Sistema ===")
# Criar admin
print("\nCriando usuário admin...")
create_admin()
# Criar usuários de teste
print("\nCriando usuários de teste...")
create_test_users()
# Verificar configuração
print("\n=== Verificando Configuração ===")
session = get_db_connection()
try:
# Verificar admin
admin = session.query(Usuario).filter_by(username='admin').first()
if admin:
print("Admin: OK")
print(f"OTP configurado: {'Sim' if admin.otp_secret else 'Não'}")
else:
print("Admin: FALHOU")
# Verificar usuários de teste
test_users = ['aligner', 'tester', 'deployer']
for username in test_users:
user = session.query(Usuario).filter_by(username=username).first()
if user:
print(f"{username}: OK")
print(f"OTP configurado: {'Sim' if user.otp_secret else 'Não'}")
else:
print(f"{username}: FALHOU")
print("\n=== Instruções ===")
print("1. Use o aplicativo autenticador para configurar o OTP de cada usuário")
print("2. Faça login com cada usuário para testar")
print("3. Altere a senha no primeiro login")
print("\nCredenciais:")
print("Admin:")
print(" Usuário: admin")
print(" Senha: admin123")
print("\nUsuários de teste:")
print(" Usuário: aligner, tester, deployer")
print(" Senha: Test123!@#")
except Exception as e:
print(f"Erro ao verificar configuração: {str(e)}")
session.rollback()
finally:
session.close()
if __name__ == "__main__":
init_system()

View File

@@ -0,0 +1,64 @@
"""add_responsaveis_financas_imprensa
Revision ID: add_responsaveis_financas_imprensa
Revises: add_aspirante_fields
Create Date: 2024-03-19 10:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'add_responsaveis_financas_imprensa'
down_revision = 'add_aspirante_fields'
branch_labels = None
depends_on = None
def upgrade():
# Adicionar colunas na tabela celulas
op.add_column('celulas', sa.Column('responsavel_financas_id', sa.Integer(), nullable=True))
op.add_column('celulas', sa.Column('responsavel_imprensa_id', sa.Integer(), nullable=True))
op.create_foreign_key('fk_celulas_responsavel_financas', 'celulas', 'militantes', ['responsavel_financas_id'], ['id'])
op.create_foreign_key('fk_celulas_responsavel_imprensa', 'celulas', 'militantes', ['responsavel_imprensa_id'], ['id'])
# Adicionar colunas na tabela setores
op.add_column('setores', sa.Column('responsavel_financas_id', sa.Integer(), nullable=True))
op.add_column('setores', sa.Column('responsavel_imprensa_id', sa.Integer(), nullable=True))
op.create_foreign_key('fk_setores_responsavel_financas', 'setores', 'militantes', ['responsavel_financas_id'], ['id'])
op.create_foreign_key('fk_setores_responsavel_imprensa', 'setores', 'militantes', ['responsavel_imprensa_id'], ['id'])
# Adicionar colunas na tabela crs
op.add_column('crs', sa.Column('responsavel_financas_id', sa.Integer(), nullable=True))
op.add_column('crs', sa.Column('responsavel_imprensa_id', sa.Integer(), nullable=True))
op.create_foreign_key('fk_crs_responsavel_financas', 'crs', 'militantes', ['responsavel_financas_id'], ['id'])
op.create_foreign_key('fk_crs_responsavel_imprensa', 'crs', 'militantes', ['responsavel_imprensa_id'], ['id'])
# Adicionar colunas na tabela ccs
op.add_column('ccs', sa.Column('responsavel_financas_id', sa.Integer(), nullable=True))
op.add_column('ccs', sa.Column('responsavel_imprensa_id', sa.Integer(), nullable=True))
op.create_foreign_key('fk_ccs_responsavel_financas', 'ccs', 'militantes', ['responsavel_financas_id'], ['id'])
op.create_foreign_key('fk_ccs_responsavel_imprensa', 'ccs', 'militantes', ['responsavel_imprensa_id'], ['id'])
def downgrade():
# Remover foreign keys
op.drop_constraint('fk_celulas_responsavel_financas', 'celulas', type_='foreignkey')
op.drop_constraint('fk_celulas_responsavel_imprensa', 'celulas', type_='foreignkey')
op.drop_constraint('fk_setores_responsavel_financas', 'setores', type_='foreignkey')
op.drop_constraint('fk_setores_responsavel_imprensa', 'setores', type_='foreignkey')
op.drop_constraint('fk_crs_responsavel_financas', 'crs', type_='foreignkey')
op.drop_constraint('fk_crs_responsavel_imprensa', 'crs', type_='foreignkey')
op.drop_constraint('fk_ccs_responsavel_financas', 'ccs', type_='foreignkey')
op.drop_constraint('fk_ccs_responsavel_imprensa', 'ccs', type_='foreignkey')
# Remover colunas
op.drop_column('celulas', 'responsavel_financas_id')
op.drop_column('celulas', 'responsavel_imprensa_id')
op.drop_column('setores', 'responsavel_financas_id')
op.drop_column('setores', 'responsavel_imprensa_id')
op.drop_column('crs', 'responsavel_financas_id')
op.drop_column('crs', 'responsavel_imprensa_id')
op.drop_column('ccs', 'responsavel_financas_id')
op.drop_column('ccs', 'responsavel_imprensa_id')

5
pytest.ini Normal file
View File

@@ -0,0 +1,5 @@
[pytest]
pythonpath = .
testpaths = tests
python_files = test_*.py
addopts = -v --cov=. --cov-report=term-missing

View File

@@ -1,14 +1,19 @@
Flask==3.0.2
Flask-SQLAlchemy==3.1.1
SQLAlchemy==2.0.39
Flask-Login==0.6.3
Flask-WTF==1.2.1
Flask-Mail==0.9.1
SQLAlchemy==2.0.27
Werkzeug==3.0.1
python-dotenv==1.0.1
pyotp==2.9.0
qrcode==7.4.2
pillow==11.0.0
python-dotenv==1.0.1
flask-login==0.6.3
flask-wtf==1.2.1
Pillow==10.2.0
email-validator==2.1.0.post1
Bootstrap-Flask==2.4.1
cryptography==42.0.2
bcrypt==4.1.2
Bootstrap-Flask==2.3.3
flask-bootstrap5==0.1.dev1
PyJWT==2.8.0
gunicorn==21.2.0
Faker==19.13.0

2
routes/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
# Este arquivo está intencionalmente vazio
# Ele é usado para marcar o diretório como um pacote Python

128
routes/admin.py Normal file
View File

@@ -0,0 +1,128 @@
from flask import Blueprint, render_template, flash, redirect, url_for, request, jsonify
from functions.database import Usuario, get_db_connection
from functions.decorators import require_permission, require_role, require_minimum_role
from flask_login import login_required, current_user
from sqlalchemy.orm import joinedload
import pyotp
from werkzeug.security import generate_password_hash
import secrets
from functools import wraps
from sqlalchemy.exc import SQLAlchemyError
import logging
from datetime import datetime
logger = logging.getLogger(__name__)
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
def admin_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not current_user.is_admin:
flash('Acesso não autorizado.', 'danger')
return redirect(url_for('main.index'))
return f(*args, **kwargs)
return decorated_function
@admin_bp.route('/')
@login_required
@admin_required
def dashboard():
"""Dashboard principal da área administrativa com lista de usuários"""
db = get_db_connection()
try:
now = datetime.now()
# Carregar estatísticas relevantes
total_users = db.query(Usuario).count()
active_users = db.query(Usuario).filter(Usuario.is_active == True).count()
inactive_users = total_users - active_users
# Carregar lista de usuários
users = db.query(Usuario).options(
joinedload(Usuario.roles),
joinedload(Usuario.militante)
).all()
return render_template(
'admin/dashboard.html',
total_users=total_users,
active_users=active_users,
inactive_users=inactive_users,
users=users,
now=now
)
except SQLAlchemyError as e:
logger.error(f"Erro ao buscar dados do dashboard: {str(e)}")
flash('Erro ao carregar dados. Por favor, tente novamente.', 'danger')
return render_template('admin/dashboard.html',
total_users=0,
active_users=0,
inactive_users=0,
users=[])
finally:
db.close()
@admin_bp.route('/users/<int:user_id>/reset-otp', methods=['POST'])
@login_required
@require_role('ADMIN')
def reset_user_otp(user_id):
"""Reseta o OTP de um usuário"""
db = get_db_connection()
try:
user = db.query(Usuario).get(user_id)
if not user:
flash('Usuário não encontrado.', 'danger')
return redirect(url_for('admin.dashboard'))
# Gerar novo segredo OTP
user.otp_secret = pyotp.random_base32()
db.commit()
flash(f'OTP resetado com sucesso para {user.email}.', 'success')
return redirect(url_for('admin.dashboard'))
finally:
db.close()
@admin_bp.route('/users/<int:user_id>/reset-password', methods=['POST'])
@login_required
@require_role('ADMIN')
def reset_user_password(user_id):
"""Reseta a senha de um usuário"""
db = get_db_connection()
try:
user = db.query(Usuario).get(user_id)
if not user:
flash('Usuário não encontrado.', 'danger')
return redirect(url_for('admin.dashboard'))
# Gerar nova senha aleatória
new_password = secrets.token_urlsafe(8)
user.password = generate_password_hash(new_password)
db.commit()
flash(f'Senha resetada com sucesso. Nova senha: {new_password}', 'success')
return redirect(url_for('admin.dashboard'))
finally:
db.close()
@admin_bp.route('/users/<int:user_id>/toggle-status', methods=['POST'])
@login_required
@require_role('ADMIN')
def toggle_user_status(user_id):
"""Ativa/desativa um usuário"""
db = get_db_connection()
try:
user = db.query(Usuario).get(user_id)
if not user:
flash('Usuário não encontrado.', 'danger')
return redirect(url_for('admin.dashboard'))
user.is_active = not user.is_active
db.commit()
status = 'ativado' if user.is_active else 'desativado'
flash(f'Usuário {status} com sucesso.', 'success')
return redirect(url_for('admin.dashboard'))
finally:
db.close()

View File

@@ -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

17
run_tests.sh Executable file
View File

@@ -0,0 +1,17 @@
#!/bin/bash
# Criar e ativar ambiente virtual
python -m venv venv
source venv/bin/activate
# Instalar dependências de teste
pip install -r tests/requirements-test.txt
# Instalar o projeto em modo de desenvolvimento
pip install -e .
# Executar testes
python -m pytest
# Desativar ambiente virtual
deactivate

335
seed_data.py Normal file
View File

@@ -0,0 +1,335 @@
from datetime import datetime, timedelta
from functions.database import (
Base, Militante, CotaMensal, TipoPagamento, Pagamento,
MaterialVendido, TipoMaterial, VendaJornalAvulso, AssinaturaAnual,
RelatorioCotasMensais, RelatorioVendasMateriais, engine, SessionLocal,
Setor, ComiteCentral, Usuario, Role, EmailMilitante, Endereco,
ComiteRegional, Celula, EstadoMilitante
)
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_pagamento(session):
"""Cria tipos de pagamento padrão"""
print("\nCriando tipos de pagamento...")
tipos = [
"Dinheiro",
"PIX",
"Cartão de Crédito",
"Cartão de Débito",
"Transferência Bancária"
]
for tipo in tipos:
if not session.query(TipoPagamento).filter_by(descricao=tipo).first():
session.add(TipoPagamento(descricao=tipo))
session.commit()
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_pagamentos(session, militantes):
"""Cria pagamentos para os militantes"""
print("\nCriando pagamentos...")
tipos_pagamento = session.query(TipoPagamento).all()
for militante in militantes:
try:
# Criar entre 3 e 8 pagamentos por militante
for _ in range(random.randint(3, 8)):
tipo = random.choice(tipos_pagamento)
pagamento = Pagamento(
militante_id=militante.id,
tipo_pagamento=tipo.descricao, # Usando a descrição do tipo
valor=random.uniform(50, 500),
data_pagamento=fake.date_between(start_date='-1y', end_date='today')
)
session.add(pagamento)
session.commit()
except Exception as e:
print(f"Erro ao criar pagamentos para militante {militante.nome}: {e}")
session.rollback()
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 = SessionLocal()
try:
print("Iniciando população do banco de dados...")
# Criar estrutura organizacional
crs, setores = criar_estrutura_organizacional(session)
# Criar tipos básicos
criar_tipos_pagamento(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_pagamentos(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 Normal file
View File

@@ -0,0 +1,18 @@
from setuptools import setup, find_packages
setup(
name="controles",
version="0.1",
packages=find_packages(),
include_package_data=True,
install_requires=[
'flask',
'flask-login',
'flask-sqlalchemy',
'flask-wtf',
'flask-mail',
'python-dotenv',
'pyotp',
'qrcode',
],
)

66
sql/migrate_db.py Normal file
View File

@@ -0,0 +1,66 @@
import os
import sqlite3
import sys
from pathlib import Path
# Adiciona o diretório raiz ao PYTHONPATH
root_dir = str(Path(__file__).parent.parent)
sys.path.append(root_dir)
from functions.base import Base, engine
from functions.database import init_database
from functions.rbac import init_rbac
def execute_sql_file(file_path):
"""Executa um arquivo SQL"""
print(f"Executando arquivo {file_path}...")
try:
with open(file_path, 'r') as sql_file:
sql_commands = sql_file.read().split(';')
conn = sqlite3.connect('database.db')
cursor = conn.cursor()
for command in sql_commands:
command = command.strip()
if command:
try:
cursor.execute(command)
except sqlite3.OperationalError as e:
if "already exists" in str(e):
print(f"Aviso: {str(e)}")
else:
raise e
conn.commit()
conn.close()
print(f"Arquivo {file_path} executado com sucesso!")
except Exception as e:
print(f"Erro ao executar {file_path}: {str(e)}")
raise e
def migrate_database():
"""Executa a migração do banco de dados"""
print("Inicializando banco de dados...")
# Criar todas as tabelas
Base.metadata.create_all(engine)
# Executar scripts SQL
sql_dir = Path(__file__).parent
rbac_tables_sql = sql_dir / 'rbac_tables.sql'
if rbac_tables_sql.exists():
execute_sql_file(rbac_tables_sql)
# Inicializar RBAC
init_rbac()
# Inicializar banco de dados
init_database()
print("Migração concluída com sucesso!")
if __name__ == '__main__':
migrate_database()

47
sql/migrate_rbac.py Normal file
View File

@@ -0,0 +1,47 @@
from functions.database import get_db_connection, Usuario
from functions.rbac import Role, Permission
def migrate_existing_users():
"""Migra os usuários existentes para o novo sistema RBAC"""
session = get_db_connection()
try:
# Buscar todos os usuários
usuarios = session.query(Usuario).all()
# Buscar ou criar role de administrador
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)
# Buscar ou criar role de militante básico
militante_role = session.query(Role).filter_by(nome="Militante Básico").first()
if not militante_role:
militante_role = Role(nome="Militante Básico", nivel=Role.MILITANTE_BASICO)
session.add(militante_role)
# Atualizar usuários
for usuario in usuarios:
# Se o usuário já tem roles, pular
if usuario.roles:
continue
# Atribuir role com base no is_admin
if usuario.is_admin:
usuario.roles.append(admin_role)
else:
usuario.roles.append(militante_role)
session.commit()
print("Migração de usuários concluída com sucesso!")
except Exception as e:
session.rollback()
print(f"Erro durante a migração de usuários: {str(e)}")
raise e
finally:
session.close()
if __name__ == '__main__':
migrate_existing_users()

152
sql/rbac_tables.sql Normal file
View File

@@ -0,0 +1,152 @@
-- Tabela de roles
CREATE TABLE IF NOT EXISTS roles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nome VARCHAR(50) NOT NULL UNIQUE,
nivel INTEGER NOT NULL,
descricao TEXT
);
-- Tabela de permissões
CREATE TABLE IF NOT EXISTS permissions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nome VARCHAR(50) NOT NULL UNIQUE,
descricao TEXT
);
-- Tabela de mapeamento Role-Permission
CREATE TABLE IF NOT EXISTS role_permissions (
role_id INTEGER NOT NULL,
permission_id INTEGER NOT NULL,
PRIMARY KEY (role_id, permission_id),
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE
);
-- Tabela de mapeamento User-Role
CREATE TABLE IF NOT EXISTS user_roles (
user_id INTEGER NOT NULL,
role_id INTEGER NOT NULL,
PRIMARY KEY (user_id, role_id),
FOREIGN KEY (user_id) REFERENCES usuarios(id) ON DELETE CASCADE,
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE
);
-- Inserir roles básicas
INSERT OR IGNORE INTO roles (nome, nivel, descricao) VALUES
('Militante Básico', 1, 'Militante com permissões básicas'),
('Secretário de Célula', 2, 'Responsável por uma célula'),
('Membro de Setor', 3, 'Membro de um setor'),
('Secretário de Setor', 4, 'Responsável por um setor'),
('Membro de CR', 5, 'Membro de um Comitê Regional'),
('Secretário de CR', 6, 'Responsável por um Comitê Regional'),
('Membro do CC', 7, 'Membro do Comitê Central'),
('Secretário Geral', 8, 'Secretário Geral ou de Organização do CC');
-- Inserir permissões básicas
INSERT OR IGNORE INTO permissions (nome, descricao) VALUES
-- Permissões básicas
('view_own_data', 'Visualizar próprios dados'),
('edit_own_data', 'Editar próprios dados'),
('view_cell_data', 'Visualizar dados da célula'),
('create_militant', 'Criar novos militantes'),
-- Permissões de célula
('manage_cell_members', 'Gerenciar membros da célula'),
('create_cell_member', 'Criar membros na célula'),
('view_cell_reports', 'Visualizar relatórios da célula'),
-- Permissões de setor
('manage_sector_cells', 'Gerenciar células do setor'),
('create_sector_cell', 'Criar células no setor'),
('view_sector_reports', 'Visualizar relatórios do setor'),
-- Permissões de CR
('manage_cr_sectors', 'Gerenciar setores do CR'),
('create_cr_sector', 'Criar setores no CR'),
('view_cr_reports', 'Visualizar relatórios do CR'),
-- Permissões de CC
('manage_cc_crs', 'Gerenciar CRs'),
('create_cc_cr', 'Criar CRs'),
('view_cc_reports', 'Visualizar relatórios nacionais'),
('system_config', 'Configurar sistema');
-- Mapear permissões para roles
-- Militante Básico
INSERT OR IGNORE INTO role_permissions (role_id, permission_id)
SELECT r.id, p.id
FROM roles r, permissions p
WHERE r.nome = 'Militante Básico'
AND p.nome IN ('view_own_data', 'edit_own_data', 'view_cell_data');
-- Secretário de Célula
INSERT OR IGNORE INTO role_permissions (role_id, permission_id)
SELECT r.id, p.id
FROM roles r, permissions p
WHERE r.nome = 'Secretário de Célula'
AND p.nome IN ('view_own_data', 'edit_own_data', 'view_cell_data',
'manage_cell_members', 'create_cell_member', 'view_cell_reports',
'create_militant');
-- Membro de Setor
INSERT OR IGNORE INTO role_permissions (role_id, permission_id)
SELECT r.id, p.id
FROM roles r, permissions p
WHERE r.nome = 'Membro de Setor'
AND p.nome IN ('view_own_data', 'edit_own_data', 'view_cell_data',
'manage_cell_members', 'create_cell_member', 'view_cell_reports',
'view_sector_reports', 'create_militant');
-- Secretário de Setor
INSERT OR IGNORE INTO role_permissions (role_id, permission_id)
SELECT r.id, p.id
FROM roles r, permissions p
WHERE r.nome = 'Secretário de Setor'
AND p.nome IN ('view_own_data', 'edit_own_data', 'view_cell_data',
'manage_cell_members', 'create_cell_member', 'view_cell_reports',
'view_sector_reports', 'manage_sector_cells', 'create_sector_cell',
'create_militant');
-- Membro de CR
INSERT OR IGNORE INTO role_permissions (role_id, permission_id)
SELECT r.id, p.id
FROM roles r, permissions p
WHERE r.nome = 'Membro de CR'
AND p.nome IN ('view_own_data', 'edit_own_data', 'view_cell_data',
'manage_cell_members', 'create_cell_member', 'view_cell_reports',
'view_sector_reports', 'manage_sector_cells', 'create_sector_cell',
'view_cr_reports', 'create_militant');
-- Secretário de CR
INSERT OR IGNORE INTO role_permissions (role_id, permission_id)
SELECT r.id, p.id
FROM roles r, permissions p
WHERE r.nome = 'Secretário de CR'
AND p.nome IN ('view_own_data', 'edit_own_data', 'view_cell_data',
'manage_cell_members', 'create_cell_member', 'view_cell_reports',
'view_sector_reports', 'manage_sector_cells', 'create_sector_cell',
'view_cr_reports', 'manage_cr_sectors', 'create_cr_sector',
'create_militant');
-- Membro do CC
INSERT OR IGNORE INTO role_permissions (role_id, permission_id)
SELECT r.id, p.id
FROM roles r, permissions p
WHERE r.nome = 'Membro do CC'
AND p.nome IN ('view_own_data', 'edit_own_data', 'view_cell_data',
'manage_cell_members', 'create_cell_member', 'view_cell_reports',
'view_sector_reports', 'manage_sector_cells', 'create_sector_cell',
'view_cr_reports', 'manage_cr_sectors', 'create_cr_sector',
'view_cc_reports', 'create_militant');
-- Secretário Geral
INSERT OR IGNORE INTO role_permissions (role_id, permission_id)
SELECT r.id, p.id
FROM roles r, permissions p
WHERE r.nome = 'Secretário Geral'
AND p.nome IN ('view_own_data', 'edit_own_data', 'view_cell_data',
'manage_cell_members', 'create_cell_member', 'view_cell_reports',
'view_sector_reports', 'manage_sector_cells', 'create_sector_cell',
'view_cr_reports', 'manage_cr_sectors', 'create_cr_sector',
'view_cc_reports', 'manage_cc_crs', 'create_cc_cr',
'system_config', 'create_militant');

626
static/css/components.css Normal file
View File

@@ -0,0 +1,626 @@
/* 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;
/* Variáveis para status */
--status-active: #28a745;
--status-inactive: #dc3545;
}
/* 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;
}
/* Status styles */
.status-active {
color: var(--status-active);
font-weight: 500;
}
.status-inactive {
color: var(--status-inactive);
font-weight: 500;
}

450
static/css/style.css Normal file
View 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
View 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
View File

@@ -0,0 +1 @@

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
static/img/logo001.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

127
static/js/cotas.js Normal file
View 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
View 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
View File

@@ -0,0 +1,11 @@
document.addEventListener('DOMContentLoaded', function() {
// Configurar clique nos itens da lista de pagamentos
document.querySelectorAll('.list-group-item[onclick*="carregarDadosPagamento"]').forEach(item => {
item.addEventListener('click', function(e) {
const pagamentoId = this.getAttribute('data-pagamento-id');
if (pagamentoId) {
carregarDadosPagamento(pagamentoId);
}
});
});
});

145
static/js/main.js Normal file
View File

@@ -0,0 +1,145 @@
// Máscaras para campos de formulário
document.addEventListener('DOMContentLoaded', function() {
// Máscara para CPF
const cpfInputs = document.querySelectorAll('input[name="cpf"]');
cpfInputs.forEach(input => {
input.addEventListener('input', function(e) {
let value = e.target.value.replace(/\D/g, '');
if (value.length <= 11) {
value = value.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, "$1.$2.$3-$4");
e.target.value = value;
}
});
});
// Máscara para telefone
const phoneInputs = document.querySelectorAll('input[name="telefone"]');
phoneInputs.forEach(input => {
input.addEventListener('input', function(e) {
let value = e.target.value.replace(/\D/g, '');
if (value.length <= 11) {
if (value.length === 11) {
value = value.replace(/(\d{2})(\d{5})(\d{4})/, "($1) $2-$3");
} else {
value = value.replace(/(\d{2})(\d{4})(\d{4})/, "($1) $2-$3");
}
e.target.value = value;
}
});
});
// Formatação de valores monetários
const moneyInputs = document.querySelectorAll('input[type="number"][step="0.01"]');
moneyInputs.forEach(input => {
input.addEventListener('blur', function(e) {
const value = parseFloat(e.target.value);
if (!isNaN(value)) {
e.target.value = value.toFixed(2);
}
});
});
});
// Funções para tabelas
document.addEventListener('DOMContentLoaded', function() {
const tables = document.querySelectorAll('.table');
tables.forEach(table => {
// Ordenação
const headers = table.querySelectorAll('th[data-sort]');
headers.forEach(header => {
header.addEventListener('click', function() {
const column = this.dataset.sort;
const asc = this.classList.toggle('sort-asc');
const tbody = table.querySelector('tbody');
const rows = Array.from(tbody.querySelectorAll('tr'));
rows.sort((a, b) => {
const aVal = a.querySelector(`td[data-${column}]`).dataset[column];
const bVal = b.querySelector(`td[data-${column}]`).dataset[column];
return asc ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal);
});
rows.forEach(row => tbody.appendChild(row));
});
});
// Filtro
const filterInput = document.querySelector(`#filter-${table.id}`);
if (filterInput) {
filterInput.addEventListener('input', function() {
const searchTerm = this.value.toLowerCase();
const rows = table.querySelectorAll('tbody tr');
rows.forEach(row => {
const text = row.textContent.toLowerCase();
row.style.display = text.includes(searchTerm) ? '' : 'none';
});
});
}
});
});
// Validação de formulários
document.addEventListener('DOMContentLoaded', function() {
const forms = document.querySelectorAll('form');
forms.forEach(form => {
form.addEventListener('submit', function(e) {
if (!form.checkValidity()) {
e.preventDefault();
e.stopPropagation();
// Destacar campos inválidos
const invalidInputs = form.querySelectorAll(':invalid');
invalidInputs.forEach(input => {
input.classList.add('is-invalid');
// Adicionar mensagem de erro
const feedback = document.createElement('div');
feedback.className = 'invalid-feedback';
feedback.textContent = input.validationMessage;
input.parentNode.appendChild(feedback);
});
}
form.classList.add('was-validated');
});
});
});
// Animações e feedback visual
document.addEventListener('DOMContentLoaded', function() {
// Animar cards ao carregar
const cards = document.querySelectorAll('.card');
cards.forEach((card, index) => {
card.style.opacity = '0';
card.style.transform = 'translateY(20px)';
setTimeout(() => {
card.style.transition = 'all 0.3s ease';
card.style.opacity = '1';
card.style.transform = 'translateY(0)';
}, index * 100);
});
// Feedback visual para ações
const actionButtons = document.querySelectorAll('[data-action]');
actionButtons.forEach(button => {
button.addEventListener('click', function() {
button.classList.add('animate__animated', 'animate__pulse');
setTimeout(() => {
button.classList.remove('animate__animated', 'animate__pulse');
}, 1000);
});
});
});
// Confirmações de ações
document.addEventListener('DOMContentLoaded', function() {
const deleteButtons = document.querySelectorAll('[data-confirm]');
deleteButtons.forEach(button => {
button.addEventListener('click', function(e) {
if (!confirm(this.dataset.confirm)) {
e.preventDefault();
}
});
});
});

1461
static/js/militantes.js Normal file

File diff suppressed because it is too large Load Diff

316
static/js/pagamentos.js Normal file
View File

@@ -0,0 +1,316 @@
document.addEventListener('DOMContentLoaded', function() {
console.log('Carregando script pagamentos.js...');
// Inicializar DataTable
const table = $('#tabelaPagamentos').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
});
// Configuração do modal de edição
const modalEditarPagamento = document.getElementById('modalEditarPagamento');
if (modalEditarPagamento) {
modalEditarPagamento.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 pagamentoId = button.getAttribute('data-pagamento-id');
console.log('ID do pagamento:', pagamentoId);
// Dados do pagamento
const dados = {
militanteId: button.getAttribute('data-militante-id'),
militanteNome: button.closest('tr').querySelector('td').textContent.trim(),
tipoPagamento: button.getAttribute('data-tipo-pagamento'),
valor: button.getAttribute('data-valor'),
dataPagamento: button.getAttribute('data-data-pagamento')
};
console.log('Dados do pagamento:', dados);
// Preencher campos
document.getElementById('editMilitante').value = dados.militanteId;
document.getElementById('editMilitanteNome').value = dados.militanteNome;
document.getElementById('editTipoPagamento').value = dados.tipoPagamento;
document.getElementById('editValor').value = dados.valor;
document.getElementById('editDataPagamento').value = dados.dataPagamento;
// Configurar formulário
const form = document.getElementById('formEditarPagamento');
if (form) {
form.action = `/pagamentos/editar/${pagamentoId}`;
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);
// 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(modalEditarPagamento);
modal.hide();
// Recarregar página
window.location.reload();
} else {
alert('Erro ao atualizar pagamento: ' + data.message);
}
})
.catch(error => {
console.error('Erro:', error);
alert('Erro ao atualizar pagamento. Por favor, tente novamente.');
});
});
}
});
}
// Configuração do modal de exclusão
const modalExcluirPagamento = document.getElementById('modalExcluirPagamento');
if (modalExcluirPagamento) {
modalExcluirPagamento.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 pagamentoId = button.getAttribute('data-pagamento-id');
const pagamentoInfo = button.getAttribute('data-pagamento-info');
console.log('ID do pagamento:', pagamentoId);
// Atualizar informações no modal
document.getElementById('pagamentoInfo').textContent = pagamentoInfo;
// Configurar formulário
const form = document.getElementById('formExcluirPagamento');
if (form) {
form.action = `/pagamentos/excluir/${pagamentoId}`;
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');
// Enviar requisição
fetch(this.action, {
method: 'POST'
})
.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(modalExcluirPagamento);
modal.hide();
// Recarregar página
window.location.reload();
} else {
alert('Erro ao excluir pagamento: ' + data.message);
}
})
.catch(error => {
console.error('Erro:', error);
alert('Erro ao excluir pagamento. Por favor, tente novamente.');
});
});
}
});
}
// Configuração do formulário de novo pagamento
const formNovoPagamento = document.getElementById('formNovoPagamento');
if (formNovoPagamento) {
formNovoPagamento.addEventListener('submit', function(e) {
e.preventDefault();
console.log('Formulário de novo pagamento submetido');
// Criar FormData com os dados do formulário
const formData = new FormData(this);
// 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(document.getElementById('modalNovoPagamento'));
modal.hide();
// Recarregar página
window.location.reload();
} else {
alert('Erro ao adicionar pagamento: ' + data.message);
}
})
.catch(error => {
console.error('Erro:', error);
alert('Erro ao adicionar pagamento. Por favor, tente novamente.');
});
});
}
// Configuração do botão de exportar
const btnExportar = document.getElementById('btnExportar');
if (btnExportar) {
btnExportar.addEventListener('click', function() {
console.log('Exportando dados...');
// Coletar dados da tabela
const dados = [];
table.rows().every(function() {
const row = this.data();
dados.push({
militante: row[0],
tipo_pagamento: row[1],
valor: row[2].replace('R$ ', ''),
data_pagamento: row[3]
});
});
// Converter para CSV
const csv = [
['Militante', 'Tipo de Pagamento', 'Valor', 'Data do Pagamento'],
...dados.map(row => [
row.militante,
row.tipo_pagamento,
row.valor,
row.data_pagamento
])
]
.map(row => row.join(','))
.join('\n');
// Criar blob e fazer download
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
if (link.download !== undefined) {
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', 'pagamentos.csv');
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
});
}
// 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');
}
});
});
});

200
static/js/table_sort.js Normal file
View File

@@ -0,0 +1,200 @@
// 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_pagamento' ||
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',
'pagamentosTable'
];
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));
});
});
});
});

284
static/js/testes.js Normal file
View 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
View 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;
});
}
});

102
templates/admin/base.html Normal file
View File

@@ -0,0 +1,102 @@
{% extends "base.html" %}
{% block title %}Área Administrativa{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row">
<!-- Sidebar -->
<nav id="sidebar" class="col-md-3 col-lg-2 d-md-block bg-light sidebar">
<div class="position-sticky pt-3">
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'admin.dashboard' %}active{% endif %}"
href="{{ url_for('admin.dashboard') }}">
<i class="fas fa-tachometer-alt me-2"></i>
Dashboard
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'admin.list_users' %}active{% endif %}"
href="{{ url_for('admin.list_users') }}">
<i class="fas fa-users me-2"></i>
Usuários
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('home') }}">
<i class="fas fa-arrow-left me-2"></i>
Voltar ao Sistema
</a>
</li>
</ul>
</div>
</nav>
<!-- Main content -->
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">{% block admin_title %}{% endblock %}</h1>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block admin_content %}{% endblock %}
</main>
</div>
</div>
{% endblock %}
{% block extra_css %}
<style>
.sidebar {
position: fixed;
top: 0;
bottom: 0;
left: 0;
z-index: 100;
padding: 48px 0 0;
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
}
.sidebar .nav-link {
font-weight: 500;
color: #333;
}
.sidebar .nav-link.active {
color: #2470dc;
}
.sidebar-heading {
font-size: .75rem;
text-transform: uppercase;
}
main {
padding-top: 48px;
}
@media (max-width: 767.98px) {
.sidebar {
position: static;
padding-top: 0;
}
main {
padding-top: 0;
}
}
</style>
{% endblock %}
{% block extra_scripts %}{% endblock %}

View File

@@ -0,0 +1,227 @@
{% extends "admin/base.html" %}
{% block title %}Dashboard Administrativo{% endblock %}
{% block extra_css %}
<style>
.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);
}
.bg-primary {
background: linear-gradient(135deg, #0d6efd, #0a58ca) !important;
}
.bg-success {
background: linear-gradient(135deg, #198754, #146c43) !important;
}
.bg-danger {
background: linear-gradient(135deg, #dc3545, #b02a37) !important;
}
.card .opacity-50 {
opacity: 0.2 !important;
transition: all 0.3s ease;
}
.card:hover .opacity-50 {
opacity: 0.3 !important;
transform: scale(1.1);
}
.card-title {
font-size: 0.9rem;
font-weight: 600;
margin-bottom: 1rem;
text-transform: uppercase;
color: rgba(255,255,255,0.8);
}
.display-4 {
font-size: 2.5rem;
font-weight: 600;
margin: 0.5rem 0;
}
.btn-group {
gap: 0.25rem;
}
/* Estilo da lista de usuários */
.card.lista-usuarios {
border-radius: 0;
box-shadow: none;
transition: none;
border: 1px solid #dee2e6;
}
.card.lista-usuarios:hover {
transform: none;
box-shadow: none;
}
.card.lista-usuarios .card-header {
background: linear-gradient(to right, var(--secondary-dark), var(--secondary-color));
color: white;
border: none;
padding: 1rem 1.5rem;
}
.card.lista-usuarios .card-header h5 {
margin: 0;
font-size: 1.1rem;
font-weight: 500;
}
.card.lista-usuarios .table {
margin-bottom: 0;
}
.card.lista-usuarios .table th {
border-top: none;
font-weight: 600;
padding: 1rem;
background-color: #f8f9fa;
}
.card.lista-usuarios .table td {
padding: 1rem;
vertical-align: middle;
}
.card.lista-usuarios .badge {
padding: 0.5em 0.8em;
font-weight: 500;
}
.btn-group .btn {
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
}
</style>
{% endblock %}
{% block content %}
<h2 class="mb-4">
<i class="fas fa-users-cog"></i>
Administração de Usuários
</h2>
<div class="row mb-4">
<div class="col-md-4">
<div class="card bg-primary text-white">
<div class="card-body">
<h5 class="card-title text-uppercase">Total de Usuários</h5>
<div class="d-flex justify-content-between align-items-center">
<h2 class="display-4 mb-0">{{ total_users }}</h2>
<i class="fas fa-users fa-3x opacity-50"></i>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card bg-success text-white">
<div class="card-body">
<h5 class="card-title text-uppercase">Usuários Ativos</h5>
<div class="d-flex justify-content-between align-items-center">
<h2 class="display-4 mb-0">{{ active_users }}</h2>
<i class="fas fa-user-check fa-3x opacity-50"></i>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card bg-danger text-white">
<div class="card-body">
<h5 class="card-title text-uppercase">Usuários Inativos</h5>
<div class="d-flex justify-content-between align-items-center">
<h2 class="display-4 mb-0">{{ inactive_users }}</h2>
<i class="fas fa-user-times fa-3x opacity-50"></i>
</div>
</div>
</div>
</div>
</div>
<div class="card lista-usuarios">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">
<i class="fas fa-users me-2"></i>
Lista de Usuários
</h5>
</div>
<div class="card-body p-0">
<table id="users-table" class="table table-striped table-hover">
<thead>
<tr>
<th>Nome</th>
<th>Email</th>
<th>Status</th>
<th>Último Login</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{ user.name }}</td>
<td>{{ user.email }}</td>
<td>
<span class="badge {% if user.is_active %}bg-success{% else %}bg-danger{% endif %}">
{{ "Ativo" if user.is_active else "Inativo" }}
</span>
</td>
<td>{{ user.last_login.strftime('%d/%m/%Y %H:%M') if user.last_login else 'Nunca' }}</td>
<td>
<div class="btn-group">
<form action="{{ url_for('admin.reset_user_otp', user_id=user.id) }}" method="post" class="d-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-warning btn-sm" title="Reset OTP" onclick="return confirm('Confirma o reset do OTP deste usuário?')">
<i class="fas fa-key"></i>
</button>
</form>
<form action="{{ url_for('admin.reset_user_password', user_id=user.id) }}" method="post" class="d-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-info btn-sm" title="Reset Senha" onclick="return confirm('Confirma o reset da senha deste usuário?')">
<i class="fas fa-lock"></i>
</button>
</form>
<form action="{{ url_for('admin.toggle_user_status', user_id=user.id) }}" method="post" class="d-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-{{ 'danger' if user.is_active else 'success' }} btn-sm" title="{{ 'Desativar' if user.is_active else 'Ativar' }} Usuário">
<i class="fas fa-{{ 'user-times' if user.is_active else 'user-check' }}"></i>
</button>
</form>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
$(document).ready(function() {
$('#users-table').DataTable({
language: {
url: '//cdn.datatables.net/plug-ins/1.13.7/i18n/pt-BR.json'
},
order: [[0, 'asc']],
pageLength: 25
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,51 @@
{% extends 'base.html' %}
{% block title %}Alterar Senha{% endblock %}
{% block content %}
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h3 class="card-title">Alterar Senha</h3>
</div>
<div class="card-body">
{% 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" action="{{ url_for('alterar_senha') }}">
<div class="mb-3">
<label for="senha_atual" class="form-label">Senha Atual</label>
<input type="password" class="form-control" id="senha_atual" name="senha_atual" required>
</div>
<div class="mb-3">
<label for="nova_senha" class="form-label">Nova Senha</label>
<input type="password" class="form-control" id="nova_senha" name="nova_senha" required>
<small class="text-muted">
A senha deve ter no mínimo 8 caracteres e conter letras e números.
</small>
</div>
<div class="mb-3">
<label for="confirmar_senha" class="form-label">Confirmar Nova Senha</label>
<input type="password" class="form-control" id="confirmar_senha" name="confirmar_senha" required>
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">Alterar Senha</button>
<a href="{{ url_for('home') }}" class="btn btn-secondary">Cancelar</a>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -3,65 +3,631 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{% endblock %} - Sistema de Gestão</title>
{{ bootstrap.load_css() }}
<meta name="csrf-token" content="{{ csrf_token() }}">
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='img/favicon.ico') }}">
<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" integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<!-- Componentes CSS -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/components.css') }}?v={{ range(1, 10000) | random }}">
<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 {
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 {
flex: 0 0 auto;
margin-right: 2rem;
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 {
border: none;
border-radius: 0.5rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
height: 100%;
}
.card-header {
background-color: #f8f9fa;
border-bottom: 1px solid #e9ecef;
padding: 1rem;
}
.card-header .card-title {
margin: 0;
display: flex;
align-items: center;
gap: 0.5rem;
}
.card-header h5 {
margin: 0;
display: flex;
align-items: center;
font-size: 1.1rem;
}
.card-header h5 i {
margin-right: 0.75rem;
color: var(--primary-color);
}
.card-body {
padding: 1.5rem;
}
.card-footer {
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 {
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;
font-weight: 500;
}
.btn-primary {
background-color: var(--primary-color);
border-color: var(--primary-color);
}
.btn-success {
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>
{% block extra_css %}{% endblock %}
</head>
<body>
{% if 'user_id' in session %}
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="{{ url_for('home') }}">Sistema de Gestão</a>
{% block navbar %}
<nav class="navbar navbar-expand-lg navbar-dark">
<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">
<span class="navbar-toggler-icon"></span>
</button>
{% if session.get('user_id') %}
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" href="{{ url_for('listar_militantes') }}">Militantes</a>
<ul class="navbar-nav mx-auto">
<li class="nav-item dropdown">
<a class="nav-link" href="#" data-bs-toggle="dropdown">
<i class="fas fa-users me-1"></i>Militantes
</a>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" href="{{ url_for('listar_militantes') }}">
<i class="fas fa-list"></i>Listar Militantes
</a>
</li>
</ul>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('listar_cotas') }}">Cotas</a>
<li class="nav-item dropdown">
<a class="nav-link" href="#" data-bs-toggle="dropdown">
<i class="fas fa-dollar-sign me-1"></i>Financeiro
</a>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" href="{{ url_for('listar_cotas') }}">
<i class="fas fa-money-bill-wave"></i>Cotas
</a>
</li>
<li>
<a class="dropdown-item" href="{{ url_for('listar_pagamentos') }}">
<i class="fas fa-receipt"></i>Pagamentos
</a>
</li>
</ul>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('listar_pagamentos') }}">Pagamentos</a>
<li class="nav-item dropdown">
<a class="nav-link" href="#" data-bs-toggle="dropdown">
<i class="fas fa-box me-1"></i>Materiais
</a>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" href="{{ url_for('listar_materiais') }}">
<i class="fas fa-box"></i>Listar Materiais
</a>
</li>
<li>
<a class="dropdown-item" href="{{ url_for('listar_vendas_jornal') }}">
<i class="fas fa-newspaper"></i>Vendas de Jornais
</a>
</li>
<li>
<a class="dropdown-item" href="{{ url_for('listar_assinaturas') }}">
<i class="fas fa-file-signature"></i>Assinaturas
</a>
</li>
</ul>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('listar_materiais') }}">Materiais</a>
<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 class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="{{ url_for('logout') }}">
<i class="fas fa-sign-out-alt"></i> Sair
<li class="nav-item dropdown">
<a class="nav-link" href="#" data-bs-toggle="dropdown">
<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>
<a class="dropdown-item" href="{{ url_for('admin.dashboard') }}">
<i class="fas fa-cog fa fa-cog fa-solid fa-cog" style="display: inline-block !important; visibility: visible !important;"></i>Administração
</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>
</ul>
</div>
{% endif %}
</div>
</nav>
{% endif %}
{% endblock %}
<div class="container mt-4">
{% block content %}{% endblock %}
<div class="page-wrapper">
<div class="container py-4">
{% block content %}{% endblock %}
</div>
</div>
{{ bootstrap.load_js() }}
<!-- Adicionar Font Awesome para o ícone de Sair -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
{% if 'user_id' in session %}
<script>
// Verificar a sessão a cada minuto
setInterval(function() {
fetch('/check_session')
.then(response => response.json())
.then(data => {
if (data.expired) {
window.location.href = '/login';
}
});
}, 60000);
</script>
{% endif %}
<!-- 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 %}
</body>
</html>

View File

@@ -0,0 +1,111 @@
{% extends 'base.html' %}
{% block title %}Criar {{ tipo_instancia }}{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Criar {{ tipo_instancia }}</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="needs-validation" novalidate>
<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 class="invalid-feedback">
Por favor, insira o nome da {{ tipo_instancia }}.
</div>
</div>
{% if tipo_instancia != 'Célula' %}
<div class="col-md-6 mb-3">
<label for="instancia_superior_id" class="form-label">{{ instancia_superior }}</label>
<select class="form-select" id="instancia_superior_id" name="instancia_superior_id" required>
<option value="">Selecione uma {{ instancia_superior }}</option>
{% for superior in instancias_superiores %}
<option value="{{ superior.id }}">{{ superior.nome }}</option>
{% endfor %}
</select>
<div class="invalid-feedback">
Por favor, selecione uma {{ instancia_superior }}.
</div>
</div>
{% endif %}
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="responsavel_geral_id" class="form-label">Responsável Geral</label>
<select class="form-select" id="responsavel_geral_id" name="responsavel_geral_id" required>
<option value="">Selecione o responsável geral</option>
{% for militante in militantes %}
<option value="{{ militante.id }}">{{ militante.nome }}</option>
{% endfor %}
</select>
<div class="invalid-feedback">
Por favor, selecione o responsável geral.
</div>
</div>
<div class="col-md-6 mb-3">
<label for="responsavel_financas_id" class="form-label">Responsável de Finanças</label>
<select class="form-select" id="responsavel_financas_id" name="responsavel_financas_id">
<option value="">Selecione o responsável de finanças</option>
{% for militante in militantes %}
<option value="{{ militante.id }}">{{ militante.nome }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="responsavel_imprensa_id" class="form-label">Responsável de Imprensa</label>
<select class="form-select" id="responsavel_imprensa_id" name="responsavel_imprensa_id">
<option value="">Selecione o responsável de imprensa</option>
{% for militante in militantes %}
<option value="{{ militante.id }}">{{ militante.nome }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="d-flex justify-content-between mt-4">
<button type="submit" class="btn btn-primary">Criar</button>
<a href="{{ url_for('listar_' + tipo_instancia.lower() + 's') }}" class="btn btn-secondary">Cancelar</a>
</div>
</form>
</div>
</div>
</div>
<script>
// Validação do formulário
(function () {
'use strict'
var forms = document.querySelectorAll('.needs-validation')
Array.prototype.slice.call(forms)
.forEach(function (form) {
form.addEventListener('submit', function (event) {
if (!form.checkValidity()) {
event.preventDefault()
event.stopPropagation()
}
form.classList.add('was-validated')
}, false)
})
})()
</script>
{% endblock %}

View File

@@ -0,0 +1,107 @@
{% extends 'base.html' %}
{% block title %}Criar Militante{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Criar Militante</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="needs-validation" novalidate>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<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 class="invalid-feedback">
Por favor, insira o nome do militante.
</div>
</div>
<div class="col-md-6 mb-3">
<label for="email" class="form-label">Email</label>
<input type="email" class="form-control" id="email" name="email" required>
<div class="invalid-feedback">
Por favor, insira um email válido.
</div>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="celula_id" class="form-label">Célula</label>
<select class="form-select" id="celula_id" name="celula_id" required>
<option value="">Selecione uma célula</option>
{% for celula in celulas %}
<option value="{{ celula.id }}">{{ celula.nome }}</option>
{% endfor %}
</select>
<div class="invalid-feedback">
Por favor, selecione uma célula.
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 mb-3">
<label class="form-label">Responsabilidades</label>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="responsavel_financas" name="responsabilidades" value="{{ Militante.RESPONSAVEL_FINANCAS }}">
<label class="form-check-label" for="responsavel_financas">
Responsável de Finanças
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="responsavel_imprensa" name="responsabilidades" value="{{ Militante.RESPONSAVEL_IMPRENSA }}">
<label class="form-check-label" for="responsavel_imprensa">
Responsável de Imprensa
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="quadro_orientador" name="responsabilidades" value="{{ Militante.QUADRO_ORIENTADOR }}">
<label class="form-check-label" for="quadro_orientador">
Quadro-Orientador
</label>
</div>
</div>
</div>
<div class="d-flex justify-content-between mt-4">
<button type="submit" class="btn btn-primary">Criar</button>
<a href="{{ url_for('listar_militantes') }}" class="btn btn-secondary">Cancelar</a>
</div>
</form>
</div>
</div>
</div>
<script>
// Validação do formulário
(function () {
'use strict'
var forms = document.querySelectorAll('.needs-validation')
Array.prototype.slice.call(forms)
.forEach(function (form) {
form.addEventListener('submit', function (event) {
if (!form.checkValidity()) {
event.preventDefault()
event.stopPropagation()
}
form.classList.add('was-validated')
}, false)
})
})()
</script>
{% endblock %}

284
templates/dashboard.html Normal file
View File

@@ -0,0 +1,284 @@
{% extends 'base.html' %}
{% block title %}Dashboard Administrativo{% endblock %}
{% block content %}
<div class="container">
<h1 class="mb-4">Dashboard Administrativo</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 %}
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Gerenciamento de Acessos</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>ID</th>
<th>Usuário</th>
<th>Email</th>
<th>Status</th>
<th>Último Login</th>
<th>Nível</th>
<th>Responsabilidades</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{% for user in users %}
{% if current_user.has_permission('system_config') or
(current_user.has_permission('manage_cr_sectors') and user.cr_id == current_user.cr_id) or
(current_user.has_permission('manage_sector_cells') and user.setor_id == current_user.setor_id) or
(current_user.has_permission('manage_cell_members') and user.celula_id == current_user.celula_id) %}
<tr>
<td>{{ user.id }}</td>
<td>{{ user.username }}</td>
<td>{{ user.email }}</td>
<td>
{% if user.ativo %}
<span class="badge bg-success">Ativo</span>
{% else %}
<span class="badge bg-danger">Inativo</span>
{% endif %}
</td>
<td>{{ user.ultimo_login.strftime('%d/%m/%Y %H:%M') if user.ultimo_login else 'Nunca' }}</td>
<td>
<span class="badge bg-info">{{ user.role }}</span>
{% if current_user.has_permission('system_config') or
(current_user.has_permission('manage_cr_sectors') and user.cr_id == current_user.cr_id) or
(current_user.has_permission('manage_sector_cells') and user.setor_id == current_user.setor_id) %}
<select class="form-select form-select-sm d-inline-block w-auto" onchange="alterarNivel({{ user.id }}, this.value)">
<option value="">Alterar Nível</option>
{% if current_user.has_permission('system_config') %}
<option value="militante_basico">Militante Básico</option>
<option value="secretario_celula">Secretário de Célula</option>
<option value="membro_setor">Membro de Setor</option>
<option value="secretario_setor">Secretário de Setor</option>
<option value="membro_cr">Membro de CR</option>
<option value="secretario_cr">Secretário de CR</option>
<option value="membro_cc">Membro do CC</option>
<option value="secretario_geral">Secretário Geral</option>
{% elif current_user.has_permission('manage_cr_sectors') %}
<option value="membro_cr">Membro de CR</option>
<option value="secretario_cr">Secretário de CR</option>
{% elif current_user.has_permission('manage_sector_cells') %}
<option value="membro_setor">Membro de Setor</option>
<option value="secretario_setor">Secretário de Setor</option>
{% endif %}
</select>
{% endif %}
</td>
<td>
{% if user.militante %}
{% if user.militante.quadro_orientador %}
<span class="badge bg-primary">Quadro-Orientador</span>
{% endif %}
{% if user.militante.aspirante %}
<span class="badge bg-warning">Aspirante</span>
<small class="text-muted">
(desde {{ user.militante.data_inicio_aspirante.strftime('%d/%m/%Y') }})
</small>
{% if user.militante.avaliacao_aspirante %}
<button type="button" class="btn btn-sm btn-info"
onclick="verAvaliacaoAspirante({{ user.id }})">
Ver Avaliação
</button>
{% endif %}
{% endif %}
{% if current_user.has_permission('system_config') or
(current_user.has_permission('manage_cr_sectors') and user.cr_id == current_user.cr_id) or
(current_user.has_permission('manage_sector_cells') and user.setor_id == current_user.setor_id) %}
{% if user.militante.quadro_orientador %}
<button type="button" class="btn btn-sm btn-danger"
onclick="toggleQuadroOrientador({{ user.id }}, {{ user.militante.quadro_orientador|lower }})">
Remover QO
</button>
{% else %}
<button type="button" class="btn btn-sm btn-success"
onclick="toggleQuadroOrientador({{ user.id }}, {{ user.militante.quadro_orientador|lower }})">
Tornar QO
</button>
{% endif %}
{% if user.militante.aspirante %}
{% if datetime.utcnow() - user.militante.data_inicio_aspirante >= timedelta(days=90) %}
{% if not user.militante.avaliacao_aspirante %}
<button type="button" class="btn btn-sm btn-primary"
onclick="avaliarAspirante({{ user.id }})">
Avaliar Aspirante
</button>
{% endif %}
<button type="button" class="btn btn-sm btn-danger"
onclick="toggleAspirante({{ user.id }}, {{ user.militante.aspirante|lower }})">
Remover Aspirante
</button>
{% endif %}
{% else %}
<button type="button" class="btn btn-sm btn-warning"
onclick="toggleAspirante({{ user.id }}, {{ user.militante.aspirante|lower }})">
Tornar Aspirante
</button>
{% endif %}
{% endif %}
{% endif %}
</td>
<td>
<div class="btn-group" role="group">
{% if current_user.has_permission('system_config') or
(current_user.has_permission('manage_cr_sectors') and user.cr_id == current_user.cr_id) or
(current_user.has_permission('manage_sector_cells') and user.setor_id == current_user.setor_id) or
(current_user.has_permission('manage_cell_members') and user.celula_id == current_user.celula_id) %}
<button type="button" class="btn btn-sm btn-primary"
onclick="resetOTP({{ user.id }})">
Gerar Novo OTP
</button>
<button type="button" class="btn btn-sm btn-warning"
onclick="resetPassword({{ user.id }})">
Resetar Senha
</button>
<button type="button" class="btn btn-sm {% if user.ativo %}btn-danger{% else %}btn-success{% endif %}"
onclick="toggleUserStatus({{ user.id }}, {{ user.ativo|lower }})">
{% if user.ativo %}Desativar{% else %}Ativar{% endif %} Login
</button>
{% endif %}
</div>
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<script>
function resetOTP(userId) {
if (confirm('Tem certeza que deseja gerar um novo OTP para este usuário? O OTP atual será invalidado.')) {
fetch(`/usuarios/${userId}/otp/reset`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Novo OTP gerado com sucesso!');
location.reload();
} else {
alert('Erro ao gerar novo OTP: ' + data.message);
}
})
.catch(error => {
alert('Erro ao gerar novo OTP: ' + error);
});
}
}
function resetPassword(userId) {
if (confirm('Tem certeza que deseja resetar a senha deste usuário? Uma nova senha será gerada e enviada por email.')) {
fetch(`/usuarios/${userId}/password/reset`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Senha resetada com sucesso! A nova senha foi enviada por email.');
} else {
alert('Erro ao resetar senha: ' + data.message);
}
})
.catch(error => {
alert('Erro ao resetar senha: ' + error);
});
}
}
function toggleUserStatus(userId, currentStatus) {
const action = currentStatus ? 'desativar' : 'ativar';
if (confirm(`Tem certeza que deseja ${action} o login deste usuário?`)) {
fetch(`/usuarios/${userId}/toggle_status`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert(`Login ${action}do com sucesso!`);
location.reload();
} else {
alert(`Erro ao ${action} login: ` + data.message);
}
})
.catch(error => {
alert(`Erro ao ${action} login: ` + error);
});
}
}
function alterarNivel(userId, novoNivel) {
if (!novoNivel) return;
if (confirm('Tem certeza que deseja alterar o nível deste usuário?')) {
fetch(`/usuarios/${userId}/alterar_nivel`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ nivel: novoNivel })
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Nível do usuário alterado com sucesso!');
location.reload();
} else {
alert('Erro ao alterar nível: ' + data.message);
}
})
.catch(error => {
alert('Erro ao alterar nível: ' + error);
});
}
}
function toggleQuadroOrientador(userId, isQuadroOrientador) {
const action = isQuadroOrientador ? 'remover' : 'adicionar';
if (confirm(`Tem certeza que deseja ${action} a responsabilidade de Quadro-Orientador deste militante?`)) {
fetch(`/usuarios/${userId}/toggle_quadro_orientador`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert(`Responsabilidade de Quadro-Orientador ${action}da com sucesso!`);
location.reload();
} else {
alert(`Erro ao ${action} responsabilidade: ` + data.message);
}
})
.catch(error => {
alert(`Erro ao ${action} responsabilidade: ` + error);
});
}
}
</script>
{% endblock %}

View File

@@ -0,0 +1,198 @@
{% extends "base.html" %}
{% block title %}Dashboard Administrativo{% endblock %}
{% block content %}
<div class="container mt-4">
<h2 class="mb-4"><i class="fas fa-users-cog"></i> Administração de Usuários</h2>
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead class="thead-light">
<tr>
<th>Usuário</th>
<th>Email</th>
<th>Nome</th>
<th>Último Acesso</th>
<th>Status</th>
<th>Nível</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{% for usuario in usuarios %}
<tr>
<td>{{ usuario.username }}</td>
<td>{{ usuario.email }}</td>
<td>{{ usuario.nome }}</td>
<td>{{ usuario.last_login }}</td>
<td>
<span class="badge {% if usuario.ativo %}bg-success{% else %}bg-danger{% endif %}">
{{ "Ativo" if usuario.ativo else "Inativo" }}
</span>
</td>
<td>
{% if usuario.is_admin %}
Administrador
{% else %}
{{ usuario.nivel }}
{% endif %}
</td>
<td>
<div class="btn-group" role="group">
<button class="btn btn-sm btn-outline-primary"
onclick="toggleStatus('{{ usuario.id }}')"
data-toggle="tooltip"
title="{{ 'Desativar' if usuario.ativo else 'Ativar' }} usuário">
<i class="fas {% if usuario.ativo %}fa-user-times{% else %}fa-user-check{% endif %}"></i>
</button>
<button class="btn btn-sm btn-outline-warning"
onclick="resetarSenha('{{ usuario.id }}')"
data-toggle="tooltip"
title="Resetar senha">
<i class="fas fa-key"></i>
</button>
{% if not usuario.is_admin %}
<button class="btn btn-sm btn-outline-info"
onclick="alterarNivel('{{ usuario.id }}')"
data-toggle="tooltip"
title="Alterar nível">
<i class="fas fa-level-up-alt"></i>
</button>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Modal de Feedback -->
<div class="modal fade" id="feedbackModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Aviso</h5>
<button type="button" class="close" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<div class="modal-body">
<p id="feedbackMessage"></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Fechar</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function showFeedback(message, type = 'info') {
const modal = document.getElementById('feedbackModal');
const messageElement = document.getElementById('feedbackMessage');
messageElement.textContent = message;
messageElement.className = `alert alert-${type}`;
$(modal).modal('show');
}
function handleResponse(response) {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}
function toggleStatus(userId) {
if (!confirm('Tem certeza que deseja alterar o status deste usuário?')) {
return;
}
fetch(`/usuarios/${userId}/toggle_status`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').content
}
})
.then(handleResponse)
.then(data => {
showFeedback(data.message || 'Status alterado com sucesso!', data.success ? 'success' : 'danger');
if (data.success) {
setTimeout(() => location.reload(), 1500);
}
})
.catch(error => {
console.error('Error:', error);
showFeedback('Erro ao alterar status do usuário. Por favor, tente novamente.', 'danger');
});
}
function resetarSenha(userId) {
if (!confirm('Tem certeza que deseja resetar a senha deste usuário?')) {
return;
}
fetch(`/reset_password/${userId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').content
}
})
.then(handleResponse)
.then(data => {
showFeedback(data.message || 'Senha resetada com sucesso!', data.success ? 'success' : 'danger');
})
.catch(error => {
console.error('Error:', error);
showFeedback('Erro ao resetar senha. Por favor, tente novamente.', 'danger');
});
}
function alterarNivel(userId) {
const novoNivel = prompt('Digite o novo nível do usuário (1-5):');
if (!novoNivel) return;
if (!/^[1-5]$/.test(novoNivel)) {
showFeedback('Por favor, insira um nível válido entre 1 e 5.', 'warning');
return;
}
fetch(`/usuarios/${userId}/alterar_nivel`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({ nivel: parseInt(novoNivel) })
})
.then(handleResponse)
.then(data => {
showFeedback(data.message || 'Nível alterado com sucesso!', data.success ? 'success' : 'danger');
if (data.success) {
setTimeout(() => location.reload(), 1500);
}
})
.catch(error => {
console.error('Error:', error);
showFeedback('Erro ao alterar nível. Por favor, tente novamente.', 'danger');
});
}
// Inicializa os tooltips do Bootstrap
$(function () {
$('[data-toggle="tooltip"]').tooltip();
});
</script>
{% endblock %}

View File

@@ -0,0 +1,94 @@
{% extends 'base.html' %}
{% block title %}Editar Célula{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Editar Célula</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="needs-validation" novalidate>
<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" value="{{ celula.nome }}" required>
<div class="invalid-feedback">
Por favor, insira o nome da célula.
</div>
</div>
<div class="col-md-6 mb-3">
<label for="setor_id" class="form-label">Setor</label>
<select class="form-select" id="setor_id" name="setor_id" required>
<option value="">Selecione um setor</option>
{% for setor in setores %}
<option value="{{ setor.id }}" {% if setor.id == celula.setor_id %}selected{% endif %}>{{ setor.nome }}</option>
{% endfor %}
</select>
<div class="invalid-feedback">
Por favor, selecione um setor.
</div>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="responsavel" class="form-label">Responsável</label>
<select class="form-select" id="responsavel" name="responsavel">
<option value="">Selecione um responsável</option>
{% for militante in militantes %}
<option value="{{ militante.id }}" {% if militante.id == celula.responsavel %}selected{% endif %}>{{ militante.nome }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-6 mb-3">
<label for="responsavel_financas" class="form-label">Responsável Finanças</label>
<select class="form-select" id="responsavel_financas" name="responsavel_financas">
<option value="">Selecione um responsável financeiro</option>
{% for militante in militantes %}
<option value="{{ militante.id }}" {% if militante.id == celula.responsavel_financas %}selected{% endif %}>{{ militante.nome }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="d-flex justify-content-between mt-4">
<button type="submit" class="btn btn-primary">Salvar</button>
<a href="{{ url_for('listar_celulas') }}" class="btn btn-secondary">Cancelar</a>
</div>
</form>
</div>
</div>
</div>
<script>
// Validação do formulário
(function () {
'use strict'
var forms = document.querySelectorAll('.needs-validation')
Array.prototype.slice.call(forms)
.forEach(function (form) {
form.addEventListener('submit', function (event) {
if (!form.checkValidity()) {
event.preventDefault()
event.stopPropagation()
}
form.classList.add('was-validated')
}, false)
})
})()
</script>
{% endblock %}

View File

@@ -0,0 +1,94 @@
{% extends 'base.html' %}
{% block title %}Editar Comitê Regional{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Editar Comitê Regional</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="needs-validation" novalidate>
<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" value="{{ comite.nome }}" required>
<div class="invalid-feedback">
Por favor, insira o nome do comitê regional.
</div>
</div>
<div class="col-md-6 mb-3">
<label for="comite_central_id" class="form-label">Comitê Central</label>
<select class="form-select" id="comite_central_id" name="comite_central_id" required>
<option value="">Selecione um comitê central</option>
{% for comite_central in comites_centrais %}
<option value="{{ comite_central.id }}" {% if comite_central.id == comite.comite_central_id %}selected{% endif %}>{{ comite_central.nome }}</option>
{% endfor %}
</select>
<div class="invalid-feedback">
Por favor, selecione um comitê central.
</div>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="responsavel" class="form-label">Responsável</label>
<select class="form-select" id="responsavel" name="responsavel">
<option value="">Selecione um responsável</option>
{% for militante in militantes %}
<option value="{{ militante.id }}" {% if militante.id == comite.responsavel %}selected{% endif %}>{{ militante.nome }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-6 mb-3">
<label for="responsavel_financas" class="form-label">Responsável Finanças</label>
<select class="form-select" id="responsavel_financas" name="responsavel_financas">
<option value="">Selecione um responsável financeiro</option>
{% for militante in militantes %}
<option value="{{ militante.id }}" {% if militante.id == comite.responsavel_financas %}selected{% endif %}>{{ militante.nome }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="d-flex justify-content-between mt-4">
<button type="submit" class="btn btn-primary">Salvar</button>
<a href="{{ url_for('listar_comites') }}" class="btn btn-secondary">Cancelar</a>
</div>
</form>
</div>
</div>
</div>
<script>
// Validação do formulário
(function () {
'use strict'
var forms = document.querySelectorAll('.needs-validation')
Array.prototype.slice.call(forms)
.forEach(function (form) {
form.addEventListener('submit', function (event) {
if (!form.checkValidity()) {
event.preventDefault()
event.stopPropagation()
}
form.classList.add('was-validated')
}, false)
})
})()
</script>
{% endblock %}

View File

@@ -0,0 +1,81 @@
{% extends 'base.html' %}
{% block title %}Editar Comitê Central{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Editar Comitê Central</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="needs-validation" novalidate>
<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" value="{{ comite.nome }}" required>
<div class="invalid-feedback">
Por favor, insira o nome do comitê central.
</div>
</div>
<div class="col-md-6 mb-3">
<label for="responsavel" class="form-label">Responsável</label>
<select class="form-select" id="responsavel" name="responsavel">
<option value="">Selecione um responsável</option>
{% for militante in militantes %}
<option value="{{ militante.id }}" {% if militante.id == comite.responsavel %}selected{% endif %}>{{ militante.nome }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="responsavel_financas" class="form-label">Responsável Finanças</label>
<select class="form-select" id="responsavel_financas" name="responsavel_financas">
<option value="">Selecione um responsável financeiro</option>
{% for militante in militantes %}
<option value="{{ militante.id }}" {% if militante.id == comite.responsavel_financas %}selected{% endif %}>{{ militante.nome }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="d-flex justify-content-between mt-4">
<button type="submit" class="btn btn-primary">Salvar</button>
<a href="{{ url_for('listar_comites_centrais') }}" class="btn btn-secondary">Cancelar</a>
</div>
</form>
</div>
</div>
</div>
<script>
// Validação do formulário
(function () {
'use strict'
var forms = document.querySelectorAll('.needs-validation')
Array.prototype.slice.call(forms)
.forEach(function (form) {
form.addEventListener('submit', function (event) {
if (!form.checkValidity()) {
event.preventDefault()
event.stopPropagation()
}
form.classList.add('was-validated')
}, false)
})
})()
</script>
{% endblock %}

View 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 %}

View File

@@ -0,0 +1,111 @@
{% extends 'base.html' %}
{% block title %}Editar {{ tipo_instancia }}{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Editar {{ tipo_instancia }}</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="needs-validation" novalidate>
<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" value="{{ instancia.nome }}" required>
<div class="invalid-feedback">
Por favor, insira o nome da {{ tipo_instancia }}.
</div>
</div>
{% if tipo_instancia != 'Célula' %}
<div class="col-md-6 mb-3">
<label for="instancia_superior_id" class="form-label">{{ instancia_superior }}</label>
<select class="form-select" id="instancia_superior_id" name="instancia_superior_id" required>
<option value="">Selecione uma {{ instancia_superior }}</option>
{% for superior in instancias_superiores %}
<option value="{{ superior.id }}" {% if superior.id == instancia.instancia_superior_id %}selected{% endif %}>{{ superior.nome }}</option>
{% endfor %}
</select>
<div class="invalid-feedback">
Por favor, selecione uma {{ instancia_superior }}.
</div>
</div>
{% endif %}
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="responsavel_geral_id" class="form-label">Responsável Geral</label>
<select class="form-select" id="responsavel_geral_id" name="responsavel_geral_id" required>
<option value="">Selecione o responsável geral</option>
{% for militante in militantes %}
<option value="{{ militante.id }}" {% if militante.id == instancia.responsavel_geral_id %}selected{% endif %}>{{ militante.nome }}</option>
{% endfor %}
</select>
<div class="invalid-feedback">
Por favor, selecione o responsável geral.
</div>
</div>
<div class="col-md-6 mb-3">
<label for="responsavel_financas_id" class="form-label">Responsável de Finanças</label>
<select class="form-select" id="responsavel_financas_id" name="responsavel_financas_id">
<option value="">Selecione o responsável de finanças</option>
{% for militante in militantes %}
<option value="{{ militante.id }}" {% if militante.id == instancia.responsavel_financas_id %}selected{% endif %}>{{ militante.nome }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="responsavel_imprensa_id" class="form-label">Responsável de Imprensa</label>
<select class="form-select" id="responsavel_imprensa_id" name="responsavel_imprensa_id">
<option value="">Selecione o responsável de imprensa</option>
{% for militante in militantes %}
<option value="{{ militante.id }}" {% if militante.id == instancia.responsavel_imprensa_id %}selected{% endif %}>{{ militante.nome }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="d-flex justify-content-between mt-4">
<button type="submit" class="btn btn-primary">Salvar</button>
<a href="{{ url_for('listar_' + tipo_instancia.lower() + 's') }}" class="btn btn-secondary">Cancelar</a>
</div>
</form>
</div>
</div>
</div>
<script>
// Validação do formulário
(function () {
'use strict'
var forms = document.querySelectorAll('.needs-validation')
Array.prototype.slice.call(forms)
.forEach(function (form) {
form.addEventListener('submit', function (event) {
if (!form.checkValidity()) {
event.preventDefault()
event.stopPropagation()
}
form.classList.add('was-validated')
}, false)
})
})()
</script>
{% endblock %}

View File

@@ -0,0 +1,94 @@
{% extends 'base.html' %}
{% block title %}Editar Material{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Editar Material</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="needs-validation" novalidate>
<div class="mb-3">
<label for="nome" class="form-label">Nome</label>
<input type="text" class="form-control" id="nome" name="nome" value="{{ material.nome }}" required>
<div class="invalid-feedback">
Por favor, insira o nome do material.
</div>
</div>
<div class="mb-3">
<label for="descricao" class="form-label">Descrição</label>
<textarea class="form-control" id="descricao" name="descricao" rows="3" required>{{ material.descricao }}</textarea>
<div class="invalid-feedback">
Por favor, insira a descrição do material.
</div>
</div>
<div class="mb-3">
<label for="preco" class="form-label">Preço</label>
<input type="number" class="form-control" id="preco" name="preco" step="0.01" value="{{ material.preco }}" required>
<div class="invalid-feedback">
Por favor, insira o preço do material.
</div>
</div>
<div class="mb-3">
<label for="quantidade" class="form-label">Quantidade</label>
<input type="number" class="form-control" id="quantidade" name="quantidade" value="{{ material.quantidade }}" required>
<div class="invalid-feedback">
Por favor, insira a quantidade do material.
</div>
</div>
<div class="mb-3">
<label for="tipo_id" class="form-label">Tipo de Material</label>
<select class="form-select" id="tipo_id" name="tipo_id" required>
<option value="">Selecione um tipo</option>
{% for tipo in tipos %}
<option value="{{ tipo.id }}" {% if tipo.id == material.tipo_id %}selected{% endif %}>{{ tipo.nome }}</option>
{% endfor %}
</select>
<div class="invalid-feedback">
Por favor, selecione o tipo do material.
</div>
</div>
<div class="d-flex justify-content-between">
<button type="submit" class="btn btn-success">Salvar</button>
<a href="{{ url_for('listar_materiais') }}" class="btn btn-outline-secondary">Voltar</a>
</div>
</form>
</div>
</div>
</div>
<script>
// Validação do formulário
(function () {
'use strict'
var forms = document.querySelectorAll('.needs-validation')
Array.prototype.slice.call(forms)
.forEach(function (form) {
form.addEventListener('submit', function (event) {
if (!form.checkValidity()) {
event.preventDefault()
event.stopPropagation()
}
form.classList.add('was-validated')
}, false)
})
})()
</script>
{% endblock %}

View File

@@ -3,32 +3,251 @@
{% block title %}Editar Militante{% endblock %}
{% block content %}
<h1>Editar Militante</h1>
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Editar Militante</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 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="col-md-6 mb-3">
<label for="nome" class="form-label">Nome</label>
<input type="text" class="form-control" id="nome" name="nome" value="{{ militante.nome }}" required>
<div class="invalid-feedback">
Por favor, insira o nome do militante.
</div>
</div>
<div class="col-md-6 mb-3">
<label for="email" class="form-label">Email</label>
<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">
Por favor, insira um email válido.
</div>
</div>
</div>
<div class="row">
<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" value="{{ militante.cpf }}" required>
<div class="invalid-feedback">
Por favor, insira o CPF do militante.
</div>
</div>
<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" value="{{ militante.titulo_eleitoral }}">
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="data_nascimento" class="form-label">Data de Nascimento</label>
<input type="date" class="form-control" id="data_nascimento" name="data_nascimento" value="{{ militante.data_nascimento.strftime('%Y-%m-%d') }}">
</div>
<div class="col-md-6 mb-3">
<label for="data_entrada_oci" class="form-label">Data de Entrada na OCI</label>
<input type="date" class="form-control" id="data_entrada_oci" name="data_entrada_oci" value="{{ militante.data_entrada_oci.strftime('%Y-%m-%d') }}">
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="data_efetivacao_oci" class="form-label">Data de Efetivação na OCI</label>
<input type="date" class="form-control" id="data_efetivacao_oci" name="data_efetivacao_oci" value="{{ militante.data_efetivacao_oci.strftime('%Y-%m-%d') }}">
</div>
<div class="col-md-6 mb-3">
<label for="telefone1" class="form-label">Telefone 1</label>
<input type="text" class="form-control" id="telefone1" name="telefone1" value="{{ militante.telefone1 }}">
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="telefone2" class="form-label">Telefone 2</label>
<input type="text" class="form-control" id="telefone2" name="telefone2" value="{{ militante.telefone2 }}">
</div>
<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" value="{{ militante.profissao }}">
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="regime_trabalho" class="form-label">Regime de Trabalho</label>
<input type="text" class="form-control" id="regime_trabalho" name="regime_trabalho" value="{{ militante.regime_trabalho }}">
</div>
<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" value="{{ militante.empresa }}">
</div>
</div>
<div class="row">
<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" value="{{ militante.contratante }}">
</div>
<div class="col-md-6 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" value="{{ militante.instituicao_ensino }}">
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="tipo_instituicao" class="form-label">Tipo de Instituição</label>
<input type="text" class="form-control" id="tipo_instituicao" name="tipo_instituicao" value="{{ militante.tipo_instituicao }}">
</div>
<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" value="{{ militante.sindicato }}">
</div>
</div>
<div class="row">
<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" value="{{ militante.cargo_sindical }}">
</div>
<div class="col-md-6 mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="dirigente_sindical" name="dirigente_sindical" {% if militante.dirigente_sindical %}checked{% endif %}>
<label class="form-check-label" for="dirigente_sindical">
Dirigente Sindical
</label>
</div>
</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" value="{{ militante.central_sindical }}">
</div>
<div class="col-md-6 mb-3">
<label for="setor_id" class="form-label">Setor</label>
<select class="form-select" id="setor_id" name="setor_id" required>
<option value="">Selecione um setor</option>
{% for setor in setores %}
<option value="{{ setor.id }}" {% if setor.id == militante.setor_id %}selected{% endif %}>{{ setor.nome }}</option>
{% endfor %}
</select>
<div class="invalid-feedback">
Por favor, selecione um setor.
</div>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="celula_id" class="form-label">Célula</label>
<select class="form-select" id="celula_id" name="celula_id" required>
<option value="">Selecione uma célula</option>
{% for celula in celulas %}
<option value="{{ celula.id }}" {% if celula.id == militante.celula_id %}selected{% endif %}>{{ celula.nome }}</option>
{% endfor %}
</select>
<div class="invalid-feedback">
Por favor, selecione uma célula.
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 mb-3">
<label class="form-label">Responsabilidades</label>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="responsavel_financas" name="responsabilidades" value="{{ Militante.RESPONSAVEL_FINANCAS }}" {% if militante.responsabilidades & Militante.RESPONSAVEL_FINANCAS %}checked{% endif %}>
<label class="form-check-label" for="responsavel_financas">
Responsável de Finanças
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="responsavel_imprensa" name="responsabilidades" value="{{ Militante.RESPONSAVEL_IMPRENSA }}" {% if militante.responsabilidades & Militante.RESPONSAVEL_IMPRENSA %}checked{% endif %}>
<label class="form-check-label" for="responsavel_imprensa">
Responsável de Imprensa
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="quadro_orientador" name="responsabilidades" value="{{ Militante.QUADRO_ORIENTADOR }}" {% if militante.responsabilidades & Militante.QUADRO_ORIENTADOR %}checked{% endif %}>
<label class="form-check-label" for="quadro_orientador">
Quadro-Orientador
</label>
</div>
</div>
</div>
<div class="d-flex justify-content-between mt-4">
<button type="submit" class="btn btn-primary">Salvar</button>
<a href="{{ url_for('listar_militantes') }}" class="btn btn-secondary">Cancelar</a>
</div>
</form>
</div>
</div>
</div>
<script>
// Validação do formulário
(function () {
'use strict';
{% 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 %}
var forms = document.querySelectorAll('.needs-validation');
<form method="post">
Nome: <input type="text" name="nome" required
value="{{ militante.nome }}"><br>
CPF: <input type="text" name="cpf" required
value="{{ militante.cpf }}"
pattern="\d{3}\.?\d{3}\.?\d{3}-?\d{2}"
title="Digite um CPF no formato: xxx.xxx.xxx-xx"><br>
Email: <input type="email" name="email" required
value="{{ militante.email }}"><br>
Telefone: <input type="text" name="telefone"
value="{{ militante.telefone }}"><br>
Endereço: <input type="text" name="endereco"
value="{{ militante.endereco }}"><br>
Filiado: <input type="checkbox" name="filiado"
{% if militante.filiado %}checked{% endif %}><br>
<input type="submit" value="Salvar" class="btn btn-primary">
<a href="{{ url_for('listar_militantes') }}" class="btn btn-secondary">Cancelar</a>
</form>
Array.prototype.slice.call(forms)
.forEach(function (form) {
form.addEventListener('submit', function (event) {
event.preventDefault();
if (!form.checkValidity()) {
event.stopPropagation();
} else {
salvarAlteracoesMilitante({{ militante.id }});
}
form.classList.add('was-validated');
}, 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>
{% endblock %}

View File

@@ -0,0 +1,91 @@
{% extends 'base.html' %}
{% block title %}Editar Relatório de Cotas{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Editar Relatório de Cotas</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="needs-validation" novalidate>
<div class="mb-3">
<label for="setor_id" class="form-label">Setor</label>
<select class="form-select" id="setor_id" name="setor_id" required>
<option value="">Selecione um setor</option>
{% for setor in setores %}
<option value="{{ setor.id }}" {% if setor.id == relatorio.setor_id %}selected{% endif %}>{{ setor.nome }}</option>
{% endfor %}
</select>
<div class="invalid-feedback">
Por favor, selecione o setor.
</div>
</div>
<div class="mb-3">
<label for="comite_id" class="form-label">Comitê Central</label>
<select class="form-select" id="comite_id" name="comite_id" required>
<option value="">Selecione um comitê</option>
{% for comite in comites %}
<option value="{{ comite.id }}" {% if comite.id == relatorio.comite_id %}selected{% endif %}>{{ comite.nome }}</option>
{% endfor %}
</select>
<div class="invalid-feedback">
Por favor, selecione o comitê central.
</div>
</div>
<div class="mb-3">
<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" value="{{ relatorio.total_cotas }}" required>
<div class="invalid-feedback">
Por favor, insira o total de cotas.
</div>
</div>
<div class="mb-3">
<label for="data_relatorio" class="form-label">Data do Relatório</label>
<input type="date" class="form-control" id="data_relatorio" name="data_relatorio" value="{{ relatorio.data_relatorio.strftime('%Y-%m-%d') }}" required>
<div class="invalid-feedback">
Por favor, insira a data do relatório.
</div>
</div>
<div class="d-flex justify-content-between">
<button type="submit" class="btn btn-success">Salvar</button>
<a href="{{ url_for('listar_relatorios_cotas') }}" class="btn btn-outline-secondary">Voltar</a>
</div>
</form>
</div>
</div>
</div>
<script>
// Validação do formulário
(function () {
'use strict'
var forms = document.querySelectorAll('.needs-validation')
Array.prototype.slice.call(forms)
.forEach(function (form) {
form.addEventListener('submit', function (event) {
if (!form.checkValidity()) {
event.preventDefault()
event.stopPropagation()
}
form.classList.add('was-validated')
}, false)
})
})()
</script>
{% endblock %}

View File

@@ -0,0 +1,91 @@
{% extends 'base.html' %}
{% block title %}Editar Relatório de Pagamentos{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Editar Relatório de Pagamentos</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="needs-validation" novalidate>
<div class="mb-3">
<label for="setor_id" class="form-label">Setor</label>
<select class="form-select" id="setor_id" name="setor_id" required>
<option value="">Selecione um setor</option>
{% for setor in setores %}
<option value="{{ setor.id }}" {% if setor.id == relatorio.setor_id %}selected{% endif %}>{{ setor.nome }}</option>
{% endfor %}
</select>
<div class="invalid-feedback">
Por favor, selecione o setor.
</div>
</div>
<div class="mb-3">
<label for="comite_id" class="form-label">Comitê Central</label>
<select class="form-select" id="comite_id" name="comite_id" required>
<option value="">Selecione um comitê</option>
{% for comite in comites %}
<option value="{{ comite.id }}" {% if comite.id == relatorio.comite_id %}selected{% endif %}>{{ comite.nome }}</option>
{% endfor %}
</select>
<div class="invalid-feedback">
Por favor, selecione o comitê central.
</div>
</div>
<div class="mb-3">
<label for="total_pagamentos" class="form-label">Total de Pagamentos</label>
<input type="number" class="form-control" id="total_pagamentos" name="total_pagamentos" step="0.01" value="{{ relatorio.total_pagamentos }}" required>
<div class="invalid-feedback">
Por favor, insira o total de pagamentos.
</div>
</div>
<div class="mb-3">
<label for="data_relatorio" class="form-label">Data do Relatório</label>
<input type="date" class="form-control" id="data_relatorio" name="data_relatorio" value="{{ relatorio.data_relatorio.strftime('%Y-%m-%d') }}" required>
<div class="invalid-feedback">
Por favor, insira a data do relatório.
</div>
</div>
<div class="d-flex justify-content-between">
<button type="submit" class="btn btn-success">Salvar</button>
<a href="{{ url_for('listar_relatorios_pagamentos') }}" class="btn btn-outline-secondary">Voltar</a>
</div>
</form>
</div>
</div>
</div>
<script>
// Validação do formulário
(function () {
'use strict'
var forms = document.querySelectorAll('.needs-validation')
Array.prototype.slice.call(forms)
.forEach(function (form) {
form.addEventListener('submit', function (event) {
if (!form.checkValidity()) {
event.preventDefault()
event.stopPropagation()
}
form.classList.add('was-validated')
}, false)
})
})()
</script>
{% endblock %}

View File

@@ -0,0 +1,91 @@
{% extends 'base.html' %}
{% block title %}Editar Relatório de Vendas{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Editar Relatório de Vendas</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="needs-validation" novalidate>
<div class="mb-3">
<label for="setor_id" class="form-label">Setor</label>
<select class="form-select" id="setor_id" name="setor_id" required>
<option value="">Selecione um setor</option>
{% for setor in setores %}
<option value="{{ setor.id }}" {% if setor.id == relatorio.setor_id %}selected{% endif %}>{{ setor.nome }}</option>
{% endfor %}
</select>
<div class="invalid-feedback">
Por favor, selecione o setor.
</div>
</div>
<div class="mb-3">
<label for="comite_id" class="form-label">Comitê Central</label>
<select class="form-select" id="comite_id" name="comite_id" required>
<option value="">Selecione um comitê</option>
{% for comite in comites %}
<option value="{{ comite.id }}" {% if comite.id == relatorio.comite_id %}selected{% endif %}>{{ comite.nome }}</option>
{% endfor %}
</select>
<div class="invalid-feedback">
Por favor, selecione o comitê central.
</div>
</div>
<div class="mb-3">
<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" value="{{ relatorio.total_vendas }}" required>
<div class="invalid-feedback">
Por favor, insira o total de vendas.
</div>
</div>
<div class="mb-3">
<label for="data_relatorio" class="form-label">Data do Relatório</label>
<input type="date" class="form-control" id="data_relatorio" name="data_relatorio" value="{{ relatorio.data_relatorio.strftime('%Y-%m-%d') }}" required>
<div class="invalid-feedback">
Por favor, insira a data do relatório.
</div>
</div>
<div class="d-flex justify-content-between">
<button type="submit" class="btn btn-success">Salvar</button>
<a href="{{ url_for('listar_relatorios_vendas') }}" class="btn btn-outline-secondary">Voltar</a>
</div>
</form>
</div>
</div>
</div>
<script>
// Validação do formulário
(function () {
'use strict'
var forms = document.querySelectorAll('.needs-validation')
Array.prototype.slice.call(forms)
.forEach(function (form) {
form.addEventListener('submit', function (event) {
if (!form.checkValidity()) {
event.preventDefault()
event.stopPropagation()
}
form.classList.add('was-validated')
}, false)
})
})()
</script>
{% endblock %}

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,74 @@
{% extends 'base.html' %}
{% block title %}Editar Tipo de Material{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Editar Tipo de Material</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="needs-validation" novalidate>
<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" value="{{ tipo.nome }}" required>
<div class="invalid-feedback">
Por favor, insira o nome do tipo de material.
</div>
</div>
<div class="col-md-6 mb-3">
<label for="preco" class="form-label">Preço</label>
<input type="number" class="form-control" id="preco" name="preco" step="0.01" min="0" value="{{ tipo.preco }}" required>
<div class="invalid-feedback">
Por favor, insira o preço do tipo de material.
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 mb-3">
<label for="descricao" class="form-label">Descrição</label>
<textarea class="form-control" id="descricao" name="descricao" rows="3">{{ tipo.descricao }}</textarea>
</div>
</div>
<div class="d-flex justify-content-between mt-4">
<button type="submit" class="btn btn-primary">Salvar</button>
<a href="{{ url_for('listar_tipos_materiais') }}" class="btn btn-secondary">Cancelar</a>
</div>
</form>
</div>
</div>
</div>
<script>
// Validação do formulário
(function () {
'use strict'
var forms = document.querySelectorAll('.needs-validation')
Array.prototype.slice.call(forms)
.forEach(function (form) {
form.addEventListener('submit', function (event) {
if (!form.checkValidity()) {
event.preventDefault()
event.stopPropagation()
}
form.classList.add('was-validated')
}, false)
})
})()
</script>
{% endblock %}

124
templates/editar_venda.html Normal file
View File

@@ -0,0 +1,124 @@
{% extends 'base.html' %}
{% block title %}Editar Venda{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Editar Venda</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="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 }}" {% if militante.id == venda.militante_id %}selected{% endif %}>{{ militante.nome }}</option>
{% endfor %}
</select>
<div class="invalid-feedback">
Por favor, selecione o militante.
</div>
</div>
<div class="mb-3">
<label for="material_id" class="form-label">Material</label>
<select class="form-select" id="material_id" name="material_id" required>
<option value="">Selecione um material</option>
{% for material in materiais %}
<option value="{{ material.id }}" data-preco="{{ material.preco }}" {% if material.id == venda.material_id %}selected{% endif %}>{{ material.nome }} - R$ {{ "%.2f"|format(material.preco) }}</option>
{% endfor %}
</select>
<div class="invalid-feedback">
Por favor, selecione o material.
</div>
</div>
<div class="mb-3">
<label for="quantidade" class="form-label">Quantidade</label>
<input type="number" class="form-control" id="quantidade" name="quantidade" min="1" value="{{ venda.quantidade }}" required>
<div class="invalid-feedback">
Por favor, insira a quantidade.
</div>
</div>
<div class="mb-3">
<label for="valor_total" class="form-label">Valor Total</label>
<input type="number" class="form-control" id="valor_total" name="valor_total" step="0.01" value="{{ venda.valor_total }}" readonly 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" value="{{ venda.data_venda.strftime('%Y-%m-%d') }}" required>
<div class="invalid-feedback">
Por favor, insira a data da venda.
</div>
</div>
<div class="d-flex justify-content-between">
<button type="submit" class="btn btn-success">Salvar</button>
<a href="{{ url_for('listar_vendas') }}" class="btn btn-outline-secondary">Voltar</a>
</div>
</form>
</div>
</div>
</div>
<script>
// Validação do formulário
(function () {
'use strict'
var forms = document.querySelectorAll('.needs-validation')
Array.prototype.slice.call(forms)
.forEach(function (form) {
form.addEventListener('submit', function (event) {
if (!form.checkValidity()) {
event.preventDefault()
event.stopPropagation()
}
form.classList.add('was-validated')
}, false)
})
})()
// Cálculo do valor total
document.getElementById('material_id').addEventListener('change', function() {
calcularValorTotal();
});
document.getElementById('quantidade').addEventListener('input', function() {
calcularValorTotal();
});
function calcularValorTotal() {
const materialSelect = document.getElementById('material_id');
const quantidadeInput = document.getElementById('quantidade');
const valorTotalInput = document.getElementById('valor_total');
if (materialSelect.value && quantidadeInput.value) {
const preco = parseFloat(materialSelect.options[materialSelect.selectedIndex].dataset.preco);
const quantidade = parseFloat(quantidadeInput.value);
const valorTotal = preco * quantidade;
valorTotalInput.value = valorTotal.toFixed(2);
} else {
valorTotalInput.value = '';
}
}
// Calcular valor total inicial
calcularValorTotal();
</script>
{% endblock %}

View File

@@ -1,29 +1,612 @@
{% extends 'base.html' %}
{% extends "base.html" %}
{% block title %}Início{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-8 offset-md-2">
<h2 class="mb-4">Menu do Sistema</h2>
{% 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 %}
<div class="list-group">
{% for url, nome in links %}
<a href="{{ url }}" class="list-group-item list-group-item-action">
{{ nome }}
</a>
{% endfor %}
<div class="row g-4">
<div class="col-12">
<div class="welcome-header">
<h2 class="mb-2">Olá, {{ nome_usuario }}!</h2>
<h4 class="text-muted">
{{ data_atual }}
</h4>
</div>
</div>
<!-- Cards de Estatísticas -->
<div class="col-md-6 col-lg-3">
<div class="stats-card blue">
<div class="title">Total de Militantes</div>
<div class="value">{{ total_militantes }}</div>
<a href="{{ url_for('listar_militantes') }}" class="link">
Ver detalhes <i class="fas fa-arrow-right"></i>
</a>
<div class="icon">
<i class="fas fa-users"></i>
</div>
</div>
</div>
<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_assinaturas') }}" 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 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 %}

View File

@@ -1,35 +1,284 @@
{% extends 'base.html' %}
{% block title %}Início{% endblock %}
{% block title %}Assinaturas{% endblock %}
{% block content %}
<h1>Assinaturas Anuais</h1>
<a href="{{ url_for('nova_assinatura') }}">Adicionar Nova Assinatura</a>
<table border="1">
<thead>
<tr>
<th>ID</th>
<th>Militante ID</th>
<th>Tipo Material</th>
<th>Quantidade</th>
<th>Valor Total</th>
<th>Data Início</th>
<th>Data Fim</th>
</tr>
</thead>
<tbody>
{% for assinatura in assinaturas %}
<tr>
<td>{{ assinatura.id }}</td>
<td>{{ assinatura.militante_id }}</td>
<td>{{ assinatura.tipo_material_id }}</td>
<td>{{ assinatura.quantidade }}</td>
<td>R$ {{ assinatura.valor_total }}</td>
<td>{{ assinatura.data_inicio }}</td>
<td>{{ assinatura.data_fim }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<a href="{{ url_for('home') }}">Home</a>
{% endblock %}
<div class="container">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="fas fa-newspaper me-2"></i>Assinaturas</h2>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalNovaAssinatura">
<i class="fas fa-plus me-2"></i>Nova Assinatura
</button>
</div>
<div class="card">
<div class="card-body p-0">
{% if assinaturas %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Militante</th>
<th>Data Início</th>
<th>Data Fim</th>
<th>Status</th>
<th>Valor</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{% for assinatura in assinaturas %}
<tr>
<td>{{ assinatura.militante.nome }}</td>
<td>{{ assinatura.data_inicio.strftime('%d/%m/%Y') }}</td>
<td>{{ assinatura.data_fim.strftime('%d/%m/%Y') }}</td>
<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 %}

View File

@@ -0,0 +1,56 @@
{% extends 'base.html' %}
{% block title %}Listar Células{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Lista de Células</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 %}
<div class="d-flex justify-content-between mb-4">
<a href="{{ url_for('nova_celula') }}" class="btn btn-success">Nova Célula</a>
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>ID</th>
<th>Nome</th>
<th>Responsável</th>
<th>Responsável Finanças</th>
<th>Setor</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{% for celula in celulas %}
<tr>
<td>{{ celula.id }}</td>
<td>{{ celula.nome }}</td>
<td>{{ celula.responsavel_rel.nome if celula.responsavel_rel else '-' }}</td>
<td>{{ celula.responsavel_financas_rel.nome if celula.responsavel_financas_rel else '-' }}</td>
<td>{{ celula.setor.nome }}</td>
<td>
<a href="{{ url_for('editar_celula', id=celula.id) }}" class="btn btn-primary btn-sm">Editar</a>
<a href="{{ url_for('deletar_celula', id=celula.id) }}" class="btn btn-danger btn-sm" onclick="return confirm('Tem certeza que deseja excluir esta célula?')">Excluir</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,56 @@
{% extends 'base.html' %}
{% block title %}Listar Comitês Regionais{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Lista de Comitês Regionais</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 %}
<div class="d-flex justify-content-between mb-4">
<a href="{{ url_for('novo_comite') }}" class="btn btn-success">Novo Comitê Regional</a>
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>ID</th>
<th>Nome</th>
<th>Responsável</th>
<th>Responsável Finanças</th>
<th>Comitê Central</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{% for comite in comites %}
<tr>
<td>{{ comite.id }}</td>
<td>{{ comite.nome }}</td>
<td>{{ comite.responsavel_rel.nome if comite.responsavel_rel else '-' }}</td>
<td>{{ comite.responsavel_financas_rel.nome if comite.responsavel_financas_rel else '-' }}</td>
<td>{{ comite.comite_central.nome }}</td>
<td>
<a href="{{ url_for('editar_comite', id=comite.id) }}" class="btn btn-primary btn-sm">Editar</a>
<a href="{{ url_for('deletar_comite', id=comite.id) }}" class="btn btn-danger btn-sm" onclick="return confirm('Tem certeza que deseja excluir este comitê regional?')">Excluir</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,54 @@
{% extends 'base.html' %}
{% block title %}Listar Comitês Centrais{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Lista de Comitês Centrais</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 %}
<div class="d-flex justify-content-between mb-4">
<a href="{{ url_for('novo_comite_central') }}" class="btn btn-success">Novo Comitê Central</a>
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>ID</th>
<th>Nome</th>
<th>Responsável</th>
<th>Responsável Finanças</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{% for comite in comites %}
<tr>
<td>{{ comite.id }}</td>
<td>{{ comite.nome }}</td>
<td>{{ comite.responsavel_rel.nome if comite.responsavel_rel else '-' }}</td>
<td>{{ comite.responsavel_financas_rel.nome if comite.responsavel_financas_rel else '-' }}</td>
<td>
<a href="{{ url_for('editar_comite_central', id=comite.id) }}" class="btn btn-primary btn-sm">Editar</a>
<a href="{{ url_for('deletar_comite_central', id=comite.id) }}" class="btn btn-danger btn-sm" onclick="return confirm('Tem certeza que deseja excluir este comitê central?')">Excluir</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,31 +1,323 @@
{% extends 'base.html' %}
{% extends "base.html" %}
{% block title %}Listar Militantes{% endblock %}
{% block title %}Cotas{% endblock %}
{% block content %}
<h1>Cotas Mensais</h1>
<a href="{{ url_for('nova_cota') }}">Adicionar Nova Cota</a>
<table border="1">
<thead>
<tr>
<th>ID</th>
<th>Militante ID</th>
<th>Valor Antigo</th>
<th>Valor Novo</th>
<th>Data de Alteração</th>
</tr>
</thead>
<tbody>
{% for cota in cotas %}
<tr>
<td>{{ cota.id }}</td>
<td>{{ cota.militante_id }}</td>
<td>R$ {{ cota.valor_antigo }}</td>
<td>R$ {{ cota.valor_novo }}</td>
<td>{{ cota.data_alteracao }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<a href="{{ url_for('home') }}">Home</a>
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<h1 class="mb-0">
<i class="fas fa-money-bill me-2"></i>Cotas
</h1>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalNovaCota">
<i class="fas fa-plus me-2"></i>Nova Cota
</button>
</div>
</div>
</div>
<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 cotas...">
</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="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 %}

View File

@@ -0,0 +1,79 @@
{% extends 'base.html' %}
{% block title %}Lista de {{ tipo_instancia }}s{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Lista de {{ tipo_instancia }}s</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 %}
<div class="d-flex justify-content-between mb-4">
<a href="{{ url_for('criar_' + tipo_instancia.lower()) }}" class="btn btn-primary">Nova {{ tipo_instancia }}</a>
</div>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Nome</th>
{% if tipo_instancia != 'Célula' %}
<th>{{ instancia_superior }}</th>
{% endif %}
<th>Responsável Geral</th>
<th>Responsável de Finanças</th>
<th>Responsável de Imprensa</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{% for instancia in instancias %}
<tr>
<td>{{ instancia.nome }}</td>
{% if tipo_instancia != 'Célula' %}
<td>{{ instancia.instancia_superior.nome }}</td>
{% endif %}
<td>{{ instancia.responsavel_geral.nome }}</td>
<td>
{% if instancia.responsavel_financas %}
{{ instancia.responsavel_financas.nome }}
{% else %}
-
{% endif %}
</td>
<td>
{% if instancia.responsavel_imprensa %}
{{ instancia.responsavel_imprensa.nome }}
{% else %}
-
{% endif %}
</td>
<td>
<a href="{{ url_for('editar_' + tipo_instancia.lower(), id=instancia.id) }}" class="btn btn-sm btn-warning">Editar</a>
<button type="button" class="btn btn-sm btn-danger" onclick="confirmarExclusao({{ instancia.id }})">Excluir</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<script>
function confirmarExclusao(id) {
if (confirm('Tem certeza que deseja excluir esta {{ tipo_instancia }}?')) {
window.location.href = "{{ url_for('excluir_' + tipo_instancia.lower(), id=0) }}".replace('0', id);
}
}
</script>
{% endblock %}

View File

@@ -1,33 +1,330 @@
{% extends 'base.html' %}
{% extends "base.html" %}
{% block title %}Listar Militantes{% endblock %}
{% block title %}Materiais{% endblock %}
{% block content %}
<h1>Materiais Vendidos</h1>
<a href="{{ url_for('novo_material') }}">Adicionar Novo Material</a>
<table border="1">
<thead>
<tr>
<th>ID</th>
<th>Militante ID</th>
<th>Tipo Material</th>
<th>Descrição</th>
<th>Valor</th>
<th>Data da Venda</th>
</tr>
</thead>
<tbody>
{% for material in materiais %}
<tr>
<td>{{ material.id }}</td>
<td>{{ material.militante_id }}</td>
<td>{{ material.tipo_material_id }}</td>
<td>{{ material.descricao }}</td>
<td>R$ {{ material.valor }}</td>
<td>{{ material.data_venda }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<a href="{{ url_for('home') }}">Home</a>
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<h1 class="mb-0">
<i class="fas fa-box me-2"></i>Materiais
</h1>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalNovoMaterial">
<i class="fas fa-plus me-2"></i>Novo Material
</button>
</div>
</div>
</div>
<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 materiais...">
</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="materiaisTable">
<thead>
<tr>
<th data-sort="militante">Militante <i class="fas fa-sort"></i></th>
<th data-sort="tipo">Tipo <i class="fas fa-sort"></i></th>
<th data-sort="descricao">Descrição <i class="fas fa-sort"></i></th>
<th data-sort="valor">Valor <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 material in materiais %}
<tr>
<td data-militante="{{ material.militante.nome }}">{{ material.militante.nome }}</td>
<td data-tipo="{{ material.tipo_material.nome }}">{{ material.tipo_material.nome }}</td>
<td data-descricao="{{ material.descricao }}">{{ material.descricao }}</td>
<td data-valor="{{ material.valor }}">R$ {{ "%.2f"|format(material.valor) }}</td>
<td data-data="{{ material.data_venda }}">{{ material.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="#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>
<!-- 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 %}

View File

@@ -1,57 +1,511 @@
{% extends 'base.html' %}
{% extends "base.html" %}
{% block title %}Listar 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 %}
<div class="row">
<div class="col-md-12">
<h2>Lista de Militantes</h2>
<a href="{{ url_for('novo_militante') }}" class="btn btn-primary mb-3">Novo Militante</a>
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Nome</th>
<th>CPF</th>
<th>Email</th>
<th>Telefone</th>
<th>Endereço</th>
<th>Filiado</th>
</tr>
</thead>
<tbody>
{% for militante in militantes %}
<tr class="clickable-row" data-href="{{ url_for('editar_militante', id=militante.id) }}">
<td>{{ militante.nome }}</td>
<td>{{ militante.cpf }}</td>
<td>{{ militante.email }}</td>
<td>{{ militante.telefone }}</td>
<td>{{ militante.endereco }}</td>
<td>{{ 'Sim' if militante.filiado else 'Não' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="container-fluid">
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<h1 class="h3 mb-0">
<i class="fas fa-users me-2"></i>Militantes
</h1>
<div>
<button type="button" class="btn btn-outline-primary me-2" id="btnExportar">
<i class="fas fa-file-export me-2"></i>Exportar
</button>
<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
</button>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card">
<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 militantes...">
</div>
</div>
<div class="col-md-6 text-end">
<div class="btn-group me-2">
<button type="button" class="btn btn-outline-secondary btn-fixed-width dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fas fa-filter me-2"></i>Filtrar
</button>
<ul class="dropdown-menu">
<li><h6 class="dropdown-header">Status</h6></li>
<li><a class="dropdown-item" href="#" data-filter="todos">Todos</a></li>
<li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">Responsabilidades</h6></li>
<li><a class="dropdown-item" href="#" data-filter="responsavel-financas">Responsável de Finanças</a></li>
<li><a class="dropdown-item" href="#" data-filter="responsavel-imprensa">Responsável de Imprensa</a></li>
<li><a class="dropdown-item" href="#" data-filter="quadro-orientador">Quadro-Orientador</a></li>
<li><a class="dropdown-item" href="#" data-filter="secretario">Secretário</a></li>
<li><a class="dropdown-item" href="#" data-filter="tesoureiro">Tesoureiro</a></li>
<li><a class="dropdown-item" href="#" data-filter="imprensa">Imprensa</a></li>
<li><a class="dropdown-item" href="#" data-filter="mns">MNS</a></li>
<li><a class="dropdown-item" href="#" data-filter="mps">MPS</a></li>
<li><a class="dropdown-item" href="#" data-filter="juventude">Juventude</a></li>
<li><a class="dropdown-item" href="#" data-filter="aspirante">Aspirante</a></li>
<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>
<!-- Modais -->
{% include 'modals/militante_novo.html' %}
{% include 'modals/militante_editar.html' %}
{% 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>
.clickable-row {
/* 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;
}
}
/* 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;
}
.clickable-row:hover {
background-color: #f5f5f5;
.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>
<script>
document.addEventListener('DOMContentLoaded', function() {
const rows = document.querySelectorAll('.clickable-row');
rows.forEach(row => {
row.addEventListener('click', function() {
window.location.href = this.dataset.href;
});
});
});
</script>
{% endblock %}

View File

@@ -1,32 +1,203 @@
{% extends 'base.html' %}
{% extends "base.html" %}
{% block title %}Listar Militantes{% endblock %}
{% block title %}Pagamentos{% 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>
<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> Pagamentos</h2>
<div>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalNovoPagamento">
<i class="fas fa-plus"></i> Novo Pagamento
</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="tabelaPagamentos">
<thead>
<tr>
<th>Militante</th>
<th>Tipo de Pagamento</th>
<th>Valor</th>
<th>Data do Pagamento</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{% for pagamento in pagamentos %}
<tr>
<td data-militante="{{ pagamento.militante_id }}">{{ pagamento.militante.nome if pagamento.militante else 'N/A' }}</td>
<td data-tipo="{{ pagamento.tipo_pagamento }}">
{% if pagamento.tipo_pagamento == 1 %}
Mensalidade
{% elif pagamento.tipo_pagamento == 2 %}
Contribuição Extra
{% elif pagamento.tipo_pagamento == 3 %}
Doação
{% elif pagamento.tipo_pagamento == 4 %}
Taxa de Evento
{% elif pagamento.tipo_pagamento == 5 %}
Outros
{% else %}
Não Definido
{% endif %}
</td>
<td data-valor="{{ pagamento.valor }}">R$ {{ "%.2f"|format(pagamento.valor) }}</td>
<td data-data="{{ pagamento.data_pagamento }}">{{ pagamento.data_pagamento.strftime('%d/%m/%Y') }}</td>
<td>
<button type="button"
class="btn btn-sm btn-outline-primary"
data-bs-toggle="modal"
data-bs-target="#modalEditarPagamento"
data-pagamento-id="{{ pagamento.id }}"
data-militante-id="{{ pagamento.militante_id }}"
data-tipo-pagamento="{{ pagamento.tipo_pagamento }}"
data-valor="{{ pagamento.valor }}"
data-data-pagamento="{{ pagamento.data_pagamento.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="#modalExcluirPagamento"
data-pagamento-id="{{ pagamento.id }}"
data-pagamento-info="Pagamento de {{ pagamento.militante.nome if pagamento.militante else 'N/A' }} - R$ {{ "%.2f"|format(pagamento.valor) }}"
title="Excluir">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Modal Novo Pagamento -->
<div class="modal fade" id="modalNovoPagamento" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="fas fa-plus"></i> Novo Pagamento</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="formNovoPagamento" method="post" action="{{ url_for('adicionar_pagamento') }}">
<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="tipoPagamento" class="form-label">Tipo de Pagamento:</label>
<select class="form-select" id="tipoPagamento" name="tipo_pagamento" required>
<option value="">Selecione o tipo</option>
<option value="1">Mensalidade</option>
<option value="2">Contribuição Extra</option>
<option value="3">Doação</option>
<option value="4">Taxa de Evento</option>
<option value="5">Outros</option>
</select>
</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="dataPagamento" class="form-label">Data do Pagamento:</label>
<input type="date" class="form-control" id="dataPagamento" name="data_pagamento" 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 Editar Pagamento -->
<div class="modal fade" id="modalEditarPagamento" 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 Pagamento</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="formEditarPagamento" 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="editTipoPagamento" class="form-label">Tipo de Pagamento:</label>
<select class="form-select" id="editTipoPagamento" name="tipo_pagamento" required>
<option value="">Selecione o tipo</option>
<option value="1">Mensalidade</option>
<option value="2">Contribuição Extra</option>
<option value="3">Doação</option>
<option value="4">Taxa de Evento</option>
<option value="5">Outros</option>
</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="editDataPagamento" class="form-label">Data do Pagamento:</label>
<input type="date" class="form-control" id="editDataPagamento" name="data_pagamento" 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 Pagamento -->
<div class="modal fade" id="modalExcluirPagamento" 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 Pagamento</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>Tem certeza que deseja excluir este pagamento?</p>
<p id="pagamentoInfo" class="text-muted"></p>
</div>
<div class="modal-footer">
<form id="formExcluirPagamento" 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/pagamentos.js') }}"></script>
{% endblock %}

View File

@@ -1,32 +1,59 @@
{% extends 'base.html' %}
{% block title %}Listar Militantes{% endblock %}
{% block title %}Listar Relatórios de Cotas{% endblock %}
{% block content %}
<h1>Relatórios de Cotas Mensais</h1>
<a href="{{ url_for('novo_relatorio_cotas') }}">Adicionar Novo Relatório</a>
<table border="1">
<thead>
<tr>
<th>ID</th>
<th>Setor ID</th>
<th>Comitê ID</th>
<th>Total de Cotas</th>
<th>Data do Relatório</th>
</tr>
</thead>
<tbody>
{% for relatorio in relatorios %}
<tr>
<td>{{ relatorio.id }}</td>
<td>{{ relatorio.setor_id }}</td>
<td>{{ relatorio.comite_id }}</td>
<td>R$ {{ relatorio.total_cotas }}</td>
<td>{{ relatorio.data_relatorio }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<a href="{{ url_for('home') }}">Home</a>
<div class="container mt-4">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<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">
<i class="fas fa-plus me-2"></i>Novo Relatório
</a>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover" id="relatoriosTable">
<thead>
<tr>
<th data-sort="id">ID <i class="fas fa-sort"></i></th>
<th data-sort="setor">Setor <i class="fas fa-sort"></i></th>
<th data-sort="comite">Comitê Central <i class="fas fa-sort"></i></th>
<th data-sort="total">Total de Cotas <i class="fas fa-sort"></i></th>
<th data-sort="data">Data do Relatório <i class="fas fa-sort"></i></th>
<th class="text-end">Ações</th>
</tr>
</thead>
<tbody>
{% for relatorio in relatorios %}
<tr>
<td data-id="{{ relatorio.id }}">{{ relatorio.id }}</td>
<td data-setor="{{ relatorio.setor.nome }}">{{ relatorio.setor.nome }}</td>
<td data-comite="{{ relatorio.comite.nome }}">{{ relatorio.comite.nome }}</td>
<td data-total="{{ relatorio.total_cotas }}">R$ {{ "%.2f"|format(relatorio.total_cotas) }}</td>
<td data-data="{{ relatorio.data_relatorio }}">{{ relatorio.data_relatorio.strftime('%d/%m/%Y') }}</td>
<td class="text-end">
<a href="{{ url_for('editar_relatorio_cotas', id=relatorio.id) }}"
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>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<script src="{{ url_for('static', filename='js/table_sort.js') }}"></script>
{% endblock %}

View File

@@ -0,0 +1,56 @@
{% extends 'base.html' %}
{% block title %}Listar Relatórios de Pagamentos{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Lista de Relatórios de Pagamentos</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 %}
<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('home') }}" class="btn btn-outline-primary">Início</a>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>ID</th>
<th>Setor</th>
<th>Comitê Central</th>
<th>Total de Pagamentos</th>
<th>Data do Relatório</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{% for relatorio in relatorios %}
<tr>
<td>{{ relatorio.id }}</td>
<td>{{ relatorio.setor.nome }}</td>
<td>{{ relatorio.comite.nome }}</td>
<td>R$ {{ "%.2f"|format(relatorio.total_pagamentos) }}</td>
<td>{{ relatorio.data_relatorio.strftime('%d/%m/%Y') }}</td>
<td>
<a href="{{ url_for('editar_relatorio_pagamentos', 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>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,32 +1,58 @@
{% extends 'base.html' %}
{% block title %}Listar Militantes{% endblock %}
{% block title %}Listar Relatórios de Vendas{% endblock %}
{% block content %}
<h1>Relatórios de Vendas de Materiais</h1>
<a href="{{ url_for('novo_relatorio_vendas') }}">Adicionar Novo Relatório</a>
<table border="1">
<thead>
<tr>
<th>ID</th>
<th>Setor ID</th>
<th>Comitê ID</th>
<th>Total de Vendas</th>
<th>Data do Relatório</th>
</tr>
</thead>
<tbody>
{% for relatorio in relatorios %}
<tr>
<td>{{ relatorio.id }}</td>
<td>{{ relatorio.setor_id }}</td>
<td>{{ relatorio.comite_id }}</td>
<td>R$ {{ relatorio.total_vendas }}</td>
<td>{{ relatorio.data_relatorio }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<a href="{{ url_for('home') }}">Home</a>
<div class="container mt-4">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<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">
<i class="fas fa-plus me-2"></i>Novo Relatório
</a>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover" id="relatoriosTable">
<thead>
<tr>
<th data-sort="id">ID <i class="fas fa-sort"></i></th>
<th data-sort="setor">Setor <i class="fas fa-sort"></i></th>
<th data-sort="comite">Comitê Central <i class="fas fa-sort"></i></th>
<th data-sort="total">Total de Vendas <i class="fas fa-sort"></i></th>
<th data-sort="data">Data do Relatório <i class="fas fa-sort"></i></th>
<th class="text-end">Ações</th>
</tr>
</thead>
<tbody>
{% for relatorio in relatorios %}
<tr>
<td data-id="{{ relatorio.id }}">{{ relatorio.id }}</td>
<td data-setor="{{ relatorio.setor.nome }}">{{ relatorio.setor.nome }}</td>
<td data-comite="{{ relatorio.comite.nome }}">{{ relatorio.comite.nome }}</td>
<td data-total="{{ relatorio.total_vendas }}">R$ {{ "%.2f"|format(relatorio.total_vendas) }}</td>
<td data-data="{{ relatorio.data_relatorio }}">{{ relatorio.data_relatorio.strftime('%d/%m/%Y') }}</td>
<td class="text-end">
<a href="{{ url_for('editar_relatorio_vendas', id=relatorio.id) }}"
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>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<script src="{{ url_for('static', filename='js/table_sort.js') }}"></script>
{% endblock %}

View File

@@ -0,0 +1,56 @@
{% extends 'base.html' %}
{% block title %}Listar Setores{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Lista de Setores</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 %}
<div class="d-flex justify-content-between mb-4">
<a href="{{ url_for('novo_setor') }}" class="btn btn-success">Novo Setor</a>
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>ID</th>
<th>Nome</th>
<th>Responsável</th>
<th>Responsável Finanças</th>
<th>Comitê Regional</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{% for setor in setores %}
<tr>
<td>{{ setor.id }}</td>
<td>{{ setor.nome }}</td>
<td>{{ setor.responsavel_rel.nome if setor.responsavel_rel else '-' }}</td>
<td>{{ setor.responsavel_financas_rel.nome if setor.responsavel_financas_rel else '-' }}</td>
<td>{{ setor.comite_regional.nome }}</td>
<td>
<a href="{{ url_for('editar_setor', id=setor.id) }}" class="btn btn-primary btn-sm">Editar</a>
<a href="{{ url_for('deletar_setor', id=setor.id) }}" class="btn btn-danger btn-sm" onclick="return confirm('Tem certeza que deseja excluir este setor?')">Excluir</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,54 @@
{% extends 'base.html' %}
{% block title %}Listar Tipos de Materiais{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Lista de Tipos de Materiais</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 %}
<div class="d-flex justify-content-between mb-4">
<a href="{{ url_for('novo_tipo_material') }}" class="btn btn-success">Novo Tipo de Material</a>
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>ID</th>
<th>Nome</th>
<th>Descrição</th>
<th>Preço</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{% for tipo in tipos %}
<tr>
<td>{{ tipo.id }}</td>
<td>{{ tipo.nome }}</td>
<td>{{ tipo.descricao }}</td>
<td>R$ {{ "%.2f"|format(tipo.preco) }}</td>
<td>
<a href="{{ url_for('editar_tipo_material', id=tipo.id) }}" class="btn btn-primary btn-sm">Editar</a>
<a href="{{ url_for('deletar_tipo_material', id=tipo.id) }}" class="btn btn-danger btn-sm" onclick="return confirm('Tem certeza que deseja excluir este tipo de material?')">Excluir</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,58 @@
{% extends 'base.html' %}
{% block title %}Listar Vendas{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Lista de Vendas</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 %}
<div class="d-flex justify-content-between mb-4">
<a href="{{ url_for('nova_venda') }}" class="btn btn-success">Nova Venda</a>
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>ID</th>
<th>Militante</th>
<th>Material</th>
<th>Quantidade</th>
<th>Valor Total</th>
<th>Data da Venda</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{% for venda in vendas %}
<tr>
<td>{{ venda.id }}</td>
<td>{{ venda.militante.nome }}</td>
<td>{{ venda.material.nome }}</td>
<td>{{ venda.quantidade }}</td>
<td>R$ {{ "%.2f"|format(venda.valor_total) }}</td>
<td>{{ venda.data_venda.strftime('%d/%m/%Y') }}</td>
<td>
<a href="{{ url_for('editar_venda', id=venda.id) }}" class="btn btn-primary btn-sm">Editar</a>
<a href="{{ url_for('deletar_venda', id=venda.id) }}" class="btn btn-danger btn-sm" onclick="return confirm('Tem certeza que deseja excluir esta venda?')">Excluir</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,32 +1,515 @@
{% extends 'base.html' %}
{% extends "base.html" %}
{% block title %}Listar Militantes{% endblock %}
{% block title %}Vendas de Jornais{% endblock %}
{% block content %}
<h1>Vendas de Jornais Avulsos</h1>
<a href="{{ url_for('nova_venda_jornal') }}">Adicionar Nova Venda</a>
<table border="1">
<thead>
<tr>
<th>ID</th>
<th>Militante ID</th>
<th>Quantidade</th>
<th>Valor Total</th>
<th>Data da Venda</th>
</tr>
</thead>
<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="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<h1 class="mb-0">
<i class="fas fa-newspaper me-2"></i>Vendas de Jornais
</h1>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalNovaVenda">
<i class="fas fa-plus me-2"></i>Nova Venda
</button>
</div>
</div>
</div>
<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 %}

View File

@@ -1,53 +1,211 @@
{% extends 'base.html' %}
{% extends "base.html" %}
{% block title %}Login{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-6 offset-md-3">
<div class="card mt-5">
<div class="card-header">
<h3 class="text-center">Login</h3>
</div>
<div class="card-body">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">
{{ message }}
{% if category == 'danger' %}
<br>
<small>Se o problema persistir, contate o administrador.</small>
{% endif %}
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block navbar %}{% endblock %}
<form method="POST">
<div class="mb-3">
<label for="username" class="form-label">Usuário</label>
<input type="text" class="form-control" id="username" name="username" required
value="{{ request.form.username }}">
</div>
<div class="mb-3">
<label for="password" class="form-label">Senha</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<div class="mb-3">
<label for="otp" class="form-label">Código OTP</label>
<input type="text" class="form-control" id="otp" name="otp" required
pattern="[0-9]{6}" title="Digite o código de 6 dígitos">
<small class="form-text text-muted">Digite o código de 6 dígitos do seu aplicativo autenticador</small>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">Entrar</button>
</div>
</form>
{% block content %}
<div class="login-container">
<div class="login-content">
<div class="login-header">
<img src="{{ url_for('static', filename='img/logo001-alpha.png') }}" alt="Logo OCI" class="login-logo">
<h4 class="login-title">Controles OCI</h4>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST" action="{{ url_for('login') }}" class="needs-validation" novalidate>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="form-floating mb-3">
<input type="text" class="form-control" id="email" name="email" placeholder="Email ou Usuário" required>
<label for="email">Email ou Usuário</label>
<div class="invalid-feedback">
Por favor, informe seu email ou nome de usuário.
</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" required>
<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>
<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>
{% endblock %}

35
templates/militantes.html Normal file
View 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>

View 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>

View 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>

View 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>

Some files were not shown because too many files have changed in this diff Show More