88 Commits

Author SHA1 Message Date
LS
911ead7835 fix: Corrige relacionamentos entre modelos na estrutura MVC
- Adiciona os modelos que faltavam na arquitetura MVC
- Corrige relacionamentos de referência cruzada entre modelos
- Atualiza script de preparação para criar arquivos __init__.py adequados
- Torna o script de preparação executável

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-04-23 08:16:53 -03:00
LS
91d9bef6c6 rebase usando pagamentos 2025-04-22 18:14:32 -03:00
andersonid
4742a888b7 tela de administração em ajustes 2025-04-22 18:13:33 -03:00
LS
6a3675b735 rebase usando pagamentos 2025-04-22 18:13:24 -03:00
LS
bb6e5c887b rebase usando pagamentos 2025-04-22 18:11:32 -03:00
LS
63ebf09fb6 refactor: renomeia pagamentos para comprovantes e adiciona novos tipos 2025-04-22 18:10:25 -03:00
andersonid
f87e03640d Correções nas mensagens de notificação 2025-04-22 18:10:25 -03:00
andersonid
debcbe6663 Corrige erro de sintaxe no template de login 2025-04-22 18:10:25 -03:00
andersonid
d45fefd72c mensagem de notificação movida para fora do card de formulario de login 2025-04-22 18:10:25 -03:00
LS
62aaec3fbe refactor: Implementa arquitetura MVC limpa
- Separa modelos em entidades individuais
- Cria camada de serviços para acesso a dados
- Implementa controladores para lógica de negócio
- Organiza rotas em blueprints por funcionalidade
- Adiciona documentação de arquitetura no README
- Cria script para preparação da estrutura

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-04-22 16:35:08 -03:00
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
110 changed files with 15208 additions and 2427 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

184
README.md
View File

@@ -1,134 +1,116 @@
# Sistema de Controle de Militantes
# Sistema de Controles
Sistema para gerenciamento de militantes, células, setores e comitês regionais.
Sistema de gestão para controle de militantes, pagamentos, cotas e relatórios.
## Estrutura de Permissões (RBAC)
## Arquitetura MVC
O sistema utiliza um sistema de controle de acesso baseado em papéis (RBAC) com a seguinte hierarquia:
O projeto segue a arquitetura Model-View-Controller (MVC) para separação de responsabilidades:
### Níveis de Papéis
### Models
1. **Militante Básico** (Nível 1)
- Visualizar próprios dados
- Editar próprios dados
- Visualizar dados da célula
Os modelos representam as entidades do sistema e estão organizados em:
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
- **models/entities/**: Classes de entidades do banco de dados (SQLAlchemy)
- `base.py`: Configuração do SQLAlchemy e classe Base
- `usuario.py`: Modelo de usuário
- `militante.py`: Modelo de militante
- `cota_mensal.py`: Modelo de cota mensal
- etc.
3. **Membro de Setor** (Nível 3)
- Todas as permissões do Secretário de Célula
- Visualizar relatórios do setor
### Controllers
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
Os controladores contêm a lógica de negócio e manipulam os dados dos modelos:
5. **Membro de CR** (Nível 5)
- Todas as permissões do Secretário de Setor
- Visualizar relatórios do CR
- **controllers/**: Implementação dos controladores
- `auth_controller.py`: Controle de autenticação
- `usuario_controller.py`: Operações com usuários
- `militante_controller.py`: Operações com militantes
- `home_controller.py`: Controlador da página inicial
- etc.
6. **Secretário de CR** (Nível 6)
- Todas as permissões do Membro de CR
- Gerenciar setores do CR
- Criar setores no CR
### Views
7. **Membro do CC** (Nível 7)
- Todas as permissões do Secretário de CR
- Visualizar relatórios nacionais
As views são os templates que exibem os dados para o usuário:
8. **Secretário Geral** (Nível 8)
- Todas as permissões do Membro do CC
- Gerenciar CRs
- Criar CRs
- Configurar sistema
- **templates/**: Templates Jinja2
- Organizados por funcionalidade (admin, militantes, cotas, etc.)
### Services
Camada adicional para encapsular a lógica de acesso a dados:
- **services/**: Serviços para acesso a dados
- `database_service.py`: Gerenciamento de conexões com o banco
- `usuario_service.py`: Acesso a dados de usuários
- `militante_service.py`: Acesso a dados de militantes
- etc.
### Routes
Rotas da aplicação organizadas em blueprints:
- **routes/**: Módulos de rotas (Flask Blueprints)
- `main.py`: Rotas principais
- `auth.py`: Rotas de autenticação
- `admin.py`: Rotas administrativas
- `militante.py`: Rotas para gerenciamento de militantes
- etc.
## 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
1. Clone o repositório:
```
git clone [URL_DO_REPOSITORIO]
```
2. Crie e ative um ambiente virtual:
```
python -m venv myenv
source myenv/bin/activate # Linux/Mac
myenv\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
4. Inicialize o banco de dados:
```
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
python app.py --init
```
## Uso
5. Execute a aplicação:
```
python app.py
```
### Decoradores de Permissão
## Credenciais padrão
O sistema fornece três decoradores para controle de acesso:
- **Administrador**:
- Usuário: admin
- Senha: admin123
1. `@require_permission(permission_name)`
- Verifica se o usuário tem uma permissão específica
- Exemplo: `@require_permission('create_cell_member')`
## Desenvolvimento
2. `@require_role(role_name)`
- Verifica se o usuário tem um papel específico
- Exemplo: `@require_role('Secretário de Célula')`
Para adicionar novos recursos, siga a arquitetura MVC:
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)`
1. Crie modelos necessários em `models/entities/`
2. Implemente serviços para acesso a dados em `services/`
3. Crie controladores com lógica de negócio em `controllers/`
4. Adicione rotas em módulos existentes ou crie novos em `routes/`
5. Desenvolva templates em `templates/`
### Verificando Permissões no Código
## Testes
```python
# Verificar se um usuário tem uma permissão
if user.has_permission('create_cell_member'):
# Faça algo
Execute os testes usando pytest:
# 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
```
python -m pytest
```
## Estrutura do Banco de Dados
Ou use o script de teste:
O sistema utiliza as seguintes tabelas para o RBAC:
- `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
## Segurança
- 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
```
./run_tests.sh
```

View File

@@ -1 +0,0 @@
otpauth://totp/Sistema%20de%20Controles:admin?secret=27NESPSPWKWIXVIDBUJPTK7MPAKGF4WG&issuer=Sistema%20de%20Controles

3456
app.py

File diff suppressed because it is too large Load Diff

123
app.py.new Normal file
View File

@@ -0,0 +1,123 @@
from flask import Flask
from flask_bootstrap import Bootstrap5
from flask_mail import Mail
from flask_login import LoginManager
from flask_wtf.csrf import CSRFProtect
from dotenv import load_dotenv
import os
import secrets
import logging
# Importações de configurações
from models.entities.base import Base, engine
from routes.main import main_bp
from routes.admin import admin_bp
from routes.auth import auth_bp
from routes.militante import militante_bp
from routes.pagamento import pagamento_bp
from routes.relatorio import relatorio_bp
from routes.cota import cota_bp
# Configuração do logger
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Carregar variáveis de ambiente
load_dotenv()
def create_app():
"""Factory para criação da aplicação Flask"""
app = Flask(__name__)
# Configuração secreta
app.secret_key = os.getenv('SECRET_KEY', secrets.token_hex(16))
# Configuração de Bootstrap
bootstrap = Bootstrap5(app)
# Registrar blueprints
app.register_blueprint(main_bp)
app.register_blueprint(admin_bp)
app.register_blueprint(auth_bp)
app.register_blueprint(militante_bp)
app.register_blueprint(pagamento_bp)
app.register_blueprint(relatorio_bp)
app.register_blueprint(cota_bp)
# Configurar proteção CSRF
csrf = CSRFProtect()
csrf.init_app(app)
app.config['WTF_CSRF_CHECK_DEFAULT'] = False
app.config['WTF_CSRF_HEADERS'] = ['X-CSRFToken']
# Configurar Flask-Login
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'auth.login'
# Função para carregar usuário no login_manager
from models.entities.usuario import Usuario
from services.database_service import DatabaseService
from sqlalchemy.orm import joinedload
@login_manager.user_loader
def load_user(user_id):
"""Carrega o usuário pelo ID"""
db = DatabaseService.get_db_connection()
try:
user = db.query(Usuario).options(
joinedload(Usuario.roles)
).get(user_id)
return user
finally:
db.close()
# Adicionar filtros Jinja2
@app.template_filter('bitwise_and')
def bitwise_and(value1, value2):
"""Filtro para operação bit a bit AND"""
return value1 & value2
# Configurar Flask-Mail
app.config['MAIL_SERVER'] = os.getenv('MAIL_SERVER', 'smtp.gmail.com')
app.config['MAIL_PORT'] = int(os.getenv('MAIL_PORT', 587))
app.config['MAIL_USE_TLS'] = os.getenv('MAIL_USE_TLS', 'True').lower() == 'true'
app.config['MAIL_USERNAME'] = os.getenv('MAIL_USERNAME')
app.config['MAIL_PASSWORD'] = os.getenv('MAIL_PASSWORD')
app.config['MAIL_DEFAULT_SENDER'] = os.getenv('MAIL_DEFAULT_SENDER')
# Inicializar Mail
mail = Mail(app)
return app
def init_system():
"""Inicializa o sistema com banco de dados e usuários padrão"""
from functions.database import init_database
# Inicializar banco de dados
logger.info("Inicializando banco de dados...")
init_database()
# Outros procedimentos de inicialização podem ser adicionados aqui
def main():
"""Inicializa e retorna a aplicação Flask"""
return create_app()
# Criar a aplicação
app = main()
if __name__ == '__main__':
import sys
# Verificar se é para inicializar o sistema
if '--init' in sys.argv:
init_system()
else:
# Executar a aplicação
app.run(
host='0.0.0.0',
port=5000,
debug=os.getenv('FLASK_ENV') == 'development'
)

View File

@@ -0,0 +1,124 @@
from flask import session, flash, redirect, url_for, request
from flask_login import login_user, logout_user, current_user
from datetime import datetime
import pyotp
import qrcode
from io import BytesIO
import base64
from models.entities.usuario import Usuario
from services.database_service import DatabaseService
class AuthController:
"""Controlador para funções de autenticação"""
@staticmethod
def login():
"""Processa o login de usuário"""
if request.method != "POST":
return False
email_or_username = request.form.get("email")
password = request.form.get("password")
otp = request.form.get("otp")
if not all([email_or_username, password]):
flash("Email/usuário e senha são obrigatórios.", "danger")
return False
db = DatabaseService.get_db_connection()
try:
# Tenta encontrar o usuário por email ou username
user = db.query(Usuario).filter(
(Usuario.email == email_or_username) |
(Usuario.username == email_or_username)
).first()
if not user or not user.check_password(password):
flash("Email/usuário ou senha incorretos.", "danger")
return False
# Verificar OTP se o usuário tiver configurado
if user.otp_secret and not otp:
flash("Código OTP é obrigatório para sua conta.", "danger")
return False
if user.otp_secret and not user.verify_otp(otp):
flash("Código OTP inválido.", "danger")
return False
# Atualizar último login
user.ultimo_login = datetime.utcnow()
db.commit()
# Fazer login e setar sessão
login_user(user)
session['user_id'] = user.id
session['username'] = user.username
session['is_admin'] = user.is_admin
return True
finally:
db.close()
@staticmethod
def logout():
"""Processa o logout de usuário"""
db = DatabaseService.get_db_connection()
try:
user = current_user
if user.is_authenticated:
user.logout()
db.commit()
logout_user()
flash('Logout realizado com sucesso!', 'success')
return True
finally:
db.close()
@staticmethod
def alterar_senha(user_id, senha_atual, nova_senha, confirmar_senha):
"""Altera a senha do usuário"""
if not all([senha_atual, nova_senha, confirmar_senha]):
flash("Todos os campos são obrigatórios.", "error")
return False
if nova_senha != confirmar_senha:
flash("As senhas não coincidem.", "error")
return False
db = DatabaseService.get_db_connection()
try:
user = db.query(Usuario).get(user_id)
if not user:
flash("Usuário não encontrado.", "error")
return False
if not user.check_password(senha_atual):
flash("Senha atual incorreta.", "error")
return False
user.set_password(nova_senha)
db.commit()
flash("Senha alterada com sucesso!", "success")
return True
finally:
db.close()
@staticmethod
def generate_qr_code(user):
"""Gera um QR code para o usuário"""
if not user.otp_secret:
user.otp_secret = pyotp.random_base32()
totp = pyotp.TOTP(user.otp_secret)
qr = qrcode.QRCode(version=1, box_size=10, border=5)
qr.add_data(totp.provisioning_uri(user.email, issuer_name="Sistema de Controles"))
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
buffer = BytesIO()
img.save(buffer, format="PNG")
qr_code = base64.b64encode(buffer.getvalue()).decode('utf-8')
return qr_code

View File

@@ -0,0 +1,80 @@
from flask import session, render_template
from datetime import datetime
from sqlalchemy.sql import func
from models.entities.militante import Militante
from models.entities.cota_mensal import CotaMensal
from models.entities.material_vendido import MaterialVendido
from models.entities.assinatura_anual import AssinaturaAnual
from models.entities.pagamento import Pagamento
from models.entities.tipo_pagamento import TipoPagamento
from models.entities.usuario import Usuario
from services.database_service import DatabaseService
class HomeController:
"""Controlador para página inicial e dashboard"""
@staticmethod
def dashboard():
"""Gera dados para o dashboard principal"""
db = DatabaseService.get_db_connection()
try:
# Buscar nome do usuário
usuario = db.query(Usuario).get(session.get('user_id'))
nome_usuario = usuario.username if usuario else "Usuário"
# Formatar data atual em português
data_atual = datetime.now().strftime("%d de %B de %Y")
# Buscar dados para o dashboard
total_militantes = db.query(Militante).count()
total_cotas = db.query(func.sum(CotaMensal.valor_novo)).scalar() or 0
total_materiais = db.query(MaterialVendido).count()
total_assinaturas = db.query(AssinaturaAnual).count()
# Buscar últimos militantes cadastrados
ultimos_militantes = db.query(Militante)\
.order_by(Militante.id.desc())\
.limit(5)\
.all()
# Buscar últimos pagamentos
ultimos_pagamentos = db.query(Pagamento)\
.join(Militante)\
.order_by(Pagamento.data_pagamento.desc())\
.limit(5)\
.all()
# Buscar tipos de pagamento
tipos_pagamento = db.query(TipoPagamento).all()
return {
'nome_usuario': nome_usuario,
'data_atual': data_atual,
'total_militantes': total_militantes,
'total_cotas': "{:.2f}".format(total_cotas),
'total_materiais': total_materiais,
'total_assinaturas': total_assinaturas,
'ultimos_militantes': ultimos_militantes,
'ultimos_pagamentos': ultimos_pagamentos,
'tipos_pagamento': tipos_pagamento
}
except Exception as e:
print(f"Erro ao carregar dashboard: {e}")
import traceback
traceback.print_exc()
return {
'nome_usuario': "Usuário",
'data_atual': datetime.now().strftime("%d/%m/%Y"),
'total_militantes': 0,
'total_cotas': "0.00",
'total_materiais': 0,
'total_assinaturas': 0,
'ultimos_militantes': [],
'ultimos_pagamentos': [],
'tipos_pagamento': []
}
finally:
db.close()

View File

@@ -0,0 +1,270 @@
from flask import request, jsonify, flash
from datetime import datetime
from werkzeug.exceptions import NotFound
from services.militante_service import MilitanteService
from models.entities.militante import Militante, EstadoMilitante
from models.entities.endereco import Endereco
from models.entities.email_militante import EmailMilitante
from utils.date_utils import validar_data, converter_data, validar_sequencia_datas, calcular_idade
class MilitanteController:
"""Controlador para operações com militantes"""
@staticmethod
def listar_militantes():
"""Lista todos os militantes"""
return MilitanteService.listar_militantes()
@staticmethod
def buscar_militante(militante_id):
"""Busca um militante pelo ID"""
militante = MilitanteService.buscar_militante(militante_id)
if not militante:
raise NotFound(f"Militante com ID {militante_id} não encontrado")
return militante
@staticmethod
def criar_militante(form_data):
"""Cria um novo militante"""
# Validar CPF
from functions.validations import validar_cpf
cpf = form_data.get('cpf')
if not validar_cpf(cpf):
return jsonify({
'status': 'error',
'message': 'CPF inválido'
}), 400
# Verificar se já existe militante com este CPF
if MilitanteService.buscar_por_cpf(cpf):
return jsonify({
'status': 'error',
'message': 'CPF já cadastrado'
}), 400
try:
# Criar endereço
endereco = Endereco(
cep=form_data.get('cep'),
estado=form_data.get('estado'),
cidade=form_data.get('cidade'),
bairro=form_data.get('bairro'),
rua=form_data.get('logradouro'),
numero=form_data.get('numero'),
complemento=form_data.get('complemento')
)
# Salvar endereço para obter ID
endereco_id = MilitanteService.salvar_endereco(endereco)
# Processar datas
data_nascimento = datetime.strptime(form_data.get('data_nascimento'), '%Y-%m-%d') if form_data.get('data_nascimento') else None
data_entrada_oci = datetime.strptime(form_data.get('data_entrada_oci'), '%Y-%m-%d') if form_data.get('data_entrada_oci') else None
data_efetivacao_oci = datetime.strptime(form_data.get('data_efetivacao_oci'), '%Y-%m-%d') if form_data.get('data_efetivacao_oci') else None
# Criar militante
militante = Militante(
# Dados Básicos
nome=form_data.get('nome'),
cpf=cpf,
titulo_eleitoral=form_data.get('titulo_eleitoral'),
data_nascimento=data_nascimento,
data_entrada_oci=data_entrada_oci,
data_efetivacao_oci=data_efetivacao_oci,
# Contato
telefone1=form_data.get('telefone1'),
telefone2=form_data.get('telefone2'),
endereco_id=endereco_id,
# Profissional
profissao=form_data.get('profissao'),
regime_trabalho=form_data.get('regime_trabalho'),
empresa=form_data.get('empresa'),
contratante=form_data.get('contratante'),
# Acadêmico
instituicao_ensino=form_data.get('instituicao_ensino'),
tipo_instituicao=form_data.get('tipo_instituicao'),
# Sindical
sindicato=form_data.get('sindicato'),
cargo_sindical=form_data.get('cargo_sindical'),
central_sindical=form_data.get('central_sindical'),
dirigente_sindical=form_data.get('dirigente_sindical') == 'on',
# Organização
estado=EstadoMilitante(form_data.get('estado', 'ATIVO')),
celula_id=form_data.get('celula_id', type=int),
responsabilidades=form_data.get('responsabilidades', type=int, default=0),
# Por padrão, todo novo militante é aspirante
aspirante=True,
data_inicio_aspirante=datetime.now()
)
# Salvar militante para obter ID
militante_id = MilitanteService.salvar_militante(militante)
# Adicionar email principal se fornecido
email = form_data.get('email')
if email:
email_militante = EmailMilitante(
endereco_email=email,
militante_id=militante_id
)
MilitanteService.salvar_email_militante(email_militante)
return jsonify({
'status': 'success',
'message': 'Militante criado com sucesso!',
'id': militante_id
})
except Exception as e:
return jsonify({
'status': 'error',
'message': f'Erro ao criar militante: {str(e)}'
}), 500
@staticmethod
def atualizar_militante(militante_id, form_data):
"""Atualiza um militante existente"""
try:
militante = MilitanteService.buscar_militante(militante_id)
if not militante:
return jsonify({
'status': 'error',
'message': 'Militante não encontrado'
}), 404
# Obter dados do formulário
nome = form_data.get('nome')
cpf = form_data.get('cpf')
titulo_eleitoral = form_data.get('titulo_eleitoral')
data_nascimento = form_data.get('data_nascimento')
data_entrada_oci = form_data.get('data_entrada_oci')
data_efetivacao_oci = form_data.get('data_efetivacao_oci')
telefone1 = form_data.get('telefone1')
telefone2 = form_data.get('telefone2')
email = form_data.get('email')
# Validar e converter datas
try:
data_nascimento = converter_data(data_nascimento) if data_nascimento else None
data_entrada_oci = converter_data(data_entrada_oci) if data_entrada_oci else None
data_efetivacao_oci = converter_data(data_efetivacao_oci) if data_efetivacao_oci else None
# Validar sequência lógica das datas
validar_sequencia_datas(
data_nascimento=data_nascimento,
data_entrada=data_entrada_oci,
data_efetivacao=data_efetivacao_oci
)
except ValueError as e:
return jsonify({
'status': 'error',
'message': str(e)
}), 400
# Atualizar dados básicos
if nome: militante.nome = nome
if cpf: militante.cpf = cpf
if titulo_eleitoral: militante.titulo_eleitoral = titulo_eleitoral
militante.data_nascimento = data_nascimento
militante.data_entrada_oci = data_entrada_oci
militante.data_efetivacao_oci = data_efetivacao_oci
militante.telefone1 = telefone1
militante.telefone2 = telefone2
# Calcular idade
if data_nascimento:
militante.idade = calcular_idade(data_nascimento)
# Atualizar ou criar email
if email:
MilitanteService.atualizar_email_militante(militante_id, email)
# Salvar alterações
MilitanteService.salvar_militante(militante)
return jsonify({
'status': 'success',
'message': 'Militante atualizado com sucesso',
'data': {
'nome': militante.nome,
'cpf': militante.cpf,
'idade': militante.idade if hasattr(militante, 'idade') else None,
'emails': [e.endereco_email for e in militante.emails],
'telefone1': militante.telefone1,
'celula_id': str(militante.celula_id) if militante.celula_id else None,
'responsabilidades_valor': militante.responsabilidades
}
})
except Exception as e:
return jsonify({
'status': 'error',
'message': f'Erro ao atualizar militante: {str(e)}'
}), 500
@staticmethod
def excluir_militante(militante_id):
"""Exclui um militante"""
try:
if MilitanteService.excluir_militante(militante_id):
flash('Militante excluído com sucesso!', 'success')
return True
else:
flash('Militante não encontrado', 'danger')
return False
except Exception as e:
flash(f'Erro ao excluir militante: {str(e)}', 'danger')
return False
@staticmethod
def buscar_dados_militante(militante_id):
"""Busca os dados de um militante específico"""
militante = MilitanteService.buscar_militante(militante_id)
if not militante:
return jsonify({
'status': 'error',
'message': 'Militante não encontrado'
}), 404
# Função auxiliar para formatar data com validação
def formatar_data_segura(data):
try:
if not data:
return None
return data.strftime('%Y-%m-%d')
except Exception as e:
print(f"Erro ao formatar data: {str(e)}, valor: {data}")
return None
# Formatar datas com validação
data_nascimento = formatar_data_segura(militante.data_nascimento)
data_entrada_oci = formatar_data_segura(militante.data_entrada_oci)
data_efetivacao_oci = formatar_data_segura(militante.data_efetivacao_oci)
return jsonify({
'status': 'success',
'id': militante.id,
'nome': militante.nome,
'cpf': militante.cpf,
'titulo_eleitoral': militante.titulo_eleitoral,
'data_nascimento': data_nascimento,
'data_entrada_oci': data_entrada_oci,
'data_efetivacao_oci': data_efetivacao_oci,
'emails': [email.endereco_email for email in militante.emails] if militante.emails else [],
'telefone1': militante.telefone1,
'telefone2': militante.telefone2,
'celula_id': militante.celula_id,
'responsabilidades_valor': militante.responsabilidades,
'sindicato': militante.sindicato,
'cargo_sindical': militante.cargo_sindical,
'central_sindical': militante.central_sindical,
'dirigente_sindical': militante.dirigente_sindical
})

View File

@@ -0,0 +1,202 @@
from flask import request, jsonify, flash, session
from flask_login import current_user
from datetime import datetime
import secrets
import pyotp
from models.entities.usuario import Usuario
from services.usuario_service import UsuarioService
from services.database_service import DatabaseService
class UsuarioController:
"""Controlador para operações com usuários"""
@staticmethod
def listar_usuarios():
"""Lista todos os usuários do sistema"""
return UsuarioService.listar_usuarios()
@staticmethod
def buscar_usuario(user_id):
"""Busca um usuário pelo ID"""
return UsuarioService.buscar_usuario(user_id)
@staticmethod
def criar_usuario(data):
"""Cria um novo usuário"""
# Verificar campos obrigatórios
required_fields = ['username', 'password', 'email']
for field in required_fields:
if field not in data:
flash(f'Campo {field} é obrigatório.', 'danger')
return False
# Verificar se usuário já existe
if UsuarioService.buscar_por_username(data['username']):
flash('Nome de usuário já existe.', 'danger')
return False
try:
# Criar usuário
usuario = Usuario(
username=data['username'],
email=data['email'],
nome=data.get('nome'),
is_admin=data.get('is_admin', False)
)
# Definir senha
usuario.set_password(data['password'])
# Gerar OTP secret
usuario.otp_secret = pyotp.random_base32()
# Definir outros campos
if 'role_id' in data:
usuario.role_id = data['role_id']
if 'setor_id' in data:
usuario.setor_id = data['setor_id']
# Salvar no banco
result = UsuarioService.salvar_usuario(usuario)
if result:
flash('Usuário cadastrado com sucesso!', 'success')
return True
else:
flash('Erro ao cadastrar usuário.', 'danger')
return False
except Exception as e:
flash(f'Erro ao cadastrar usuário: {str(e)}', 'danger')
return False
@staticmethod
def atualizar_usuario(user_id, data):
"""Atualiza um usuário existente"""
usuario = UsuarioService.buscar_usuario(user_id)
if not usuario:
flash('Usuário não encontrado.', 'danger')
return False
# Atualizar campos
if 'email' in data:
usuario.email = data['email']
if 'nome' in data:
usuario.nome = data['nome']
if 'role_id' in data:
usuario.role_id = data['role_id']
if 'setor_id' in data:
usuario.setor_id = data['setor_id']
if 'is_admin' in data:
usuario.is_admin = data['is_admin']
if 'password' in data and data['password']:
usuario.set_password(data['password'])
# Salvar alterações
result = UsuarioService.salvar_usuario(usuario)
if result:
flash('Usuário atualizado com sucesso!', 'success')
return True
else:
flash('Erro ao atualizar usuário.', 'danger')
return False
@staticmethod
def toggle_status(user_id):
"""Ativa/desativa um usuário"""
if not current_user.is_admin:
return jsonify({
'success': False,
'error': 'Você não tem permissão para alterar o status de usuários.'
}), 403
usuario = UsuarioService.buscar_usuario(user_id)
if not usuario:
return jsonify({
'success': False,
'error': 'Usuário não encontrado.'
}), 404
usuario.ativo = not usuario.ativo
if UsuarioService.salvar_usuario(usuario):
return jsonify({
'success': True,
'message': f'Usuário {\'ativado\' if usuario.ativo else \'desativado\'}'
})
else:
return jsonify({
'success': False,
'error': 'Erro ao salvar alterações.'
}), 500
@staticmethod
def reset_password(user_id):
"""Reseta a senha de um usuário"""
usuario = UsuarioService.buscar_usuario(user_id)
if not usuario:
flash('Usuário não encontrado.', 'danger')
return False, None
# Gerar nova senha
new_password = secrets.token_urlsafe(8)
usuario.set_password(new_password)
# Salvar alterações
if UsuarioService.salvar_usuario(usuario):
return True, new_password
else:
flash('Erro ao resetar senha.', 'danger')
return False, None
@staticmethod
def reset_otp(user_id):
"""Reseta o OTP de um usuário"""
usuario = UsuarioService.buscar_usuario(user_id)
if not usuario:
flash('Usuário não encontrado.', 'danger')
return False
# Gerar novo OTP secret
usuario.otp_secret = pyotp.random_base32()
# Salvar alterações
if UsuarioService.salvar_usuario(usuario):
flash(f'OTP resetado com sucesso para {usuario.email}.', 'success')
return True
else:
flash('Erro ao resetar OTP.', 'danger')
return False
@staticmethod
def check_session():
"""Verifica status da sessão"""
if 'user_id' not in session:
return {'status': 'expired'}
if 'last_activity' in session:
last_activity = datetime.fromtimestamp(session['last_activity'])
now = datetime.now()
if now - last_activity > timedelta(hours=2):
# Registrar o logout por timeout
try:
user = UsuarioService.buscar_usuario(session.get('user_id'))
if user:
user.ultimo_logout = datetime.now()
user.motivo_logout = "Timeout de sessão"
UsuarioService.salvar_usuario(user)
except Exception as e:
print(f"Erro ao registrar logout por timeout: {e}")
session.clear()
return {'status': 'expired'}
return {'status': 'active'}

View File

@@ -1,91 +1,151 @@
import os
from functions.database import get_db_connection, Usuario
from functions.rbac import Role
import pyotp
from functions.database import init_database, Usuario, Role, get_db_connection
import qrcode
import base64
from io import BytesIO
import os
from pathlib import Path
import pyotp
def create_admin():
"""Cria o usuário admin se não existir"""
db = get_db_connection()
def generate_qr_code(user):
"""
Gera o QR code para um usuário específico
Args:
user: Instância do modelo Usuario
Returns:
Path: Caminho do arquivo QR code gerado
"""
# Gerar QR Code apenas na raiz do projeto
qr_path = Path('admin_qr.png')
# Remover arquivo antigo se existir
if qr_path.exists():
os.remove(str(qr_path))
# Gerar e salvar QR Code
qr = qrcode.QRCode(version=1, box_size=10, border=5)
# Gerar URI do OTP
totp = pyotp.TOTP(user.otp_secret)
otp_uri = totp.provisioning_uri(
name=user.username,
issuer_name="Sistema de Controles"
)
qr.add_data(otp_uri)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
img.save(str(qr_path))
print(f"\nQR Code gerado em: {os.path.abspath(qr_path)}")
return qr_path, otp_uri
def create_admin_user():
"""Cria ou atualiza o usuário admin"""
try:
# Verificar se o admin já existe
admin = db.query(Usuario).filter_by(username='admin').first()
# Inicializar banco de dados
init_database()
if admin:
print("Usuário admin já existe")
# Criar sessão
db = get_db_connection()
try:
# Verificar se já existe um usuário admin
admin = db.query(Usuario).filter_by(username="admin").first()
# Verificar se o arquivo admin_qr.png existe
if os.path.exists('admin_qr.png'):
print("Usando OTP existente do arquivo admin_qr.png")
# Extrair o OTP secret do QR code existente
with open('admin_qr.png', 'rb') as f:
qr_data = f.read()
# Aqui você precisaria implementar a lógica para extrair o OTP secret do QR code
# Por enquanto, vamos apenas manter o OTP existente
return
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("Gerando novo OTP para o admin...")
# Gerar novo OTP
otp_secret = pyotp.random_base32()
admin.otp_secret = otp_secret
db.commit()
else:
print("Criando usuário admin...")
# Criar usuário admin
admin = Usuario(
username='admin',
password='admin123',
is_admin=True
)
admin.email = 'admin@controles.com'
db.add(admin)
db.commit()
# Gerar OTP
otp_secret = pyotp.random_base32()
admin.otp_secret = otp_secret
db.commit()
# Atribuir role de Secretário Geral
admin_role = db.query(Role).filter_by(nivel=Role.SECRETARIO_GERAL).first()
if admin_role:
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
totp = pyotp.TOTP(otp_secret)
provisioning_uri = totp.provisioning_uri(admin.username, issuer_name="Sistema de Controles")
qr = qrcode.QRCode(version=1, box_size=10, border=5)
qr.add_data(provisioning_uri)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
# Salvar QR code como base64
buffered = BytesIO()
img.save(buffered, format="PNG")
qr_base64 = base64.b64encode(buffered.getvalue()).decode()
# Salvar QR code como arquivo
img.save('admin_qr.png')
print("\nConfiguração do OTP para o admin:")
print(f"OTP Secret: {otp_secret}")
print("\nInstruções:")
print("1. Use um aplicativo autenticador (como Google Authenticator ou Authy)")
print("2. Escaneie o QR code ou insira o OTP Secret manualmente")
print("3. Use o código gerado para fazer login")
print("\nQR code salvo em 'admin_qr.png'")
# 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()
except Exception as e:
print(f"Erro ao criar admin: {str(e)}")
db.rollback()
raise
finally:
db.close()
print(f"\nErro durante a execução: {e}")
import traceback
traceback.print_exc()
if __name__ == '__main__':
create_admin()
if __name__ == "__main__":
create_admin_user()

View File

@@ -1,104 +1,56 @@
from functions.database import get_db_connection, Usuario
from functions.rbac import Role
import pyotp
import qrcode
import os
import base64
from io import BytesIO
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 se não existirem"""
"""Cria usuários de teste"""
db = get_db_connection()
try:
# Usuários de teste
# Lista de usuários de teste
test_users = [
{
'username': 'teste',
'password': 'admin123', # Mesma senha do admin
'email': 'teste@controles.com',
'is_admin': True
},
{
'username': 'aligner',
'email': 'aligner@test.com',
'password': 'Test123!@#',
'email': 'aligner@controles.com',
'is_admin': False
},
{
'username': 'tester',
'email': 'tester@test.com',
'password': 'Test123!@#',
'email': 'tester@controles.com',
'is_admin': False
},
{
'username': 'deployer',
'email': 'deployer@test.com',
'password': 'Test123!@#',
'email': 'deployer@controles.com',
'is_admin': False
}
]
# Obter o OTP secret do admin se existir
admin = db.query(Usuario).filter_by(username='admin').first()
admin_otp_secret = admin.otp_secret if admin else None
# Criar cada usuário
for user_data in test_users:
# Verificar se o usuário já existe
user = db.query(Usuario).filter_by(username=user_data['username']).first()
if not user:
print(f"Criando usuário {user_data['username']}...")
# Criar usuário
user = Usuario(
username=user_data['username'],
password=user_data['password'],
email=user_data['email'],
is_admin=user_data['is_admin']
)
user.email = user_data['email']
user.set_password(user_data['password'])
db.add(user)
db.commit()
# Se for o usuário teste, usar o mesmo OTP do admin
if user_data['username'] == 'teste' and admin_otp_secret:
user.otp_secret = admin_otp_secret
db.commit()
else:
# Gerar novo OTP para outros usuários
otp_secret = pyotp.random_base32()
user.otp_secret = otp_secret
db.commit()
# Atribuir role de Secretário Geral para o usuário teste
if user_data['username'] == 'teste':
admin_role = db.query(Role).filter_by(nivel=Role.SECRETARIO_GERAL).first()
if admin_role:
user.roles.append(admin_role)
db.commit()
print(f"Usuário {user_data['username']} criado com sucesso!")
print(f"Usuário {user_data['username']} criado")
else:
print(f"Usuário {user_data['username']} já existe")
# Se for o usuário teste e não tiver o OTP do admin, atualizar
if user_data['username'] == 'teste' and admin_otp_secret and user.otp_secret != admin_otp_secret:
user.otp_secret = admin_otp_secret
db.commit()
print(f"OTP do usuário teste atualizado para o mesmo do admin")
# Verificar se o usuário teste tem a role de Secretário Geral
if user_data['username'] == 'teste':
admin_role = db.query(Role).filter_by(nivel=Role.SECRETARIO_GERAL).first()
if admin_role and admin_role not in user.roles:
user.roles.append(admin_role)
db.commit()
print(f"Role de Secretário Geral atribuída ao usuário teste")
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()
raise
finally:
db.close()
if __name__ == '__main__':
if __name__ == "__main__":
create_test_users()

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

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

View File

@@ -109,22 +109,26 @@ CREATE TABLE user_roles (
- `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
- `REGISTER_CELL_RECEIPT`: Registrar comprovantes 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
- `REGISTER_SECTOR_RECEIPT`: Registrar comprovantes 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
- `REGISTER_CR_RECEIPT`: Registrar comprovantes 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
- `REGISTER_CC_RECEIPT`: Registrar comprovantes do CC
## Uso no Código
@@ -166,12 +170,12 @@ O sistema possui uma estrutura hierárquica com os seguintes níveis:
- `MANAGE_CELL_MEMBERS`: Gerenciar membros da célula
- `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
- `REGISTER_CELL_RECEIPT`: Registrar comprovantes 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
- `REGISTER_CELL_RECEIPT`: Registrar comprovantes da célula
- **Militante**:
- `VIEW_OWN_DATA`: Visualizar apenas seus próprios dados
@@ -180,32 +184,32 @@ O sistema possui uma estrutura hierárquica com os seguintes níveis:
- **Secretário(a)**:
- `MANAGE_SECTOR_CELLS`: Gerenciar células do setor
- `VIEW_SECTOR_REPORTS`: Visualizar relatórios do setor
- `REGISTER_SECTOR_PAYMENT`: Registrar pagamentos do setor
- `REGISTER_SECTOR_RECEIPT`: Registrar comprovantes do setor
- **Tesoureiro(a)**:
- `VIEW_SECTOR_REPORTS`: Visualizar relatórios do setor
- `REGISTER_SECTOR_PAYMENT`: Registrar pagamentos do setor
- `REGISTER_SECTOR_RECEIPT`: Registrar comprovantes 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
- `REGISTER_CR_RECEIPT`: Registrar comprovantes do CR
- **Tesoureiro(a)**:
- `VIEW_CR_REPORTS`: Visualizar relatórios do CR
- `REGISTER_CR_PAYMENT`: Registrar pagamentos do CR
- `REGISTER_CR_RECEIPT`: Registrar comprovantes 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
- `REGISTER_CC_RECEIPT`: Registrar comprovantes do CC
- `SYSTEM_CONFIG`: Configurar o sistema
- **Tesoureiro(a)**:
- `VIEW_CC_REPORTS`: Visualizar relatórios do CC
- `REGISTER_CC_PAYMENT`: Registrar pagamentos do CC
- `REGISTER_CC_RECEIPT`: Registrar comprovantes do CC
## Regras de Acesso a Dados
@@ -214,10 +218,10 @@ O sistema possui uma estrutura hierárquica com os seguintes níveis:
- Secretários e tesoureiros podem ver dados de sua instância
- O CC tem acesso a todos os dados
2. **Registro de Pagamentos**:
- Apenas tesoureiros e secretários podem registrar pagamentos
2. **Registro de Comprovantes**:
- Apenas tesoureiros e secretários podem registrar comprovantes
- O registro é restrito à instância do usuário
- O CC pode registrar pagamentos em qualquer nível
- O CC pode registrar comprovantes em qualquer nível
## Implementação Técnica

View File

@@ -1,14 +1,26 @@
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from pathlib import Path
import os
# Configuração do banco de dados
DATABASE_URL = os.getenv('DATABASE_URL', 'sqlite:///database.db')
engine = create_engine(DATABASE_URL)
Session = sessionmaker(bind=engine)
# 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'
# Base declarativa do SQLAlchemy
# Configurar SQLite com opções para melhor concorrência
engine = create_engine(
f'sqlite:///{db_path}',
connect_args={
'timeout': 30, # Tempo de espera em segundos
'check_same_thread': False # Permite acesso de múltiplas threads
},
pool_pre_ping=True, # Verifica conexão antes de usar
pool_recycle=3600 # Recicla conexões após 1 hora
)
Session = sessionmaker(bind=engine)
Base = declarative_base()
def get_db_connection():

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,6 +1,6 @@
from datetime import datetime, timedelta
from werkzeug.security import generate_password_hash, check_password_hash
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text, Numeric, Date, Enum
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text, Numeric, Date, Enum, create_engine, text, Float
from sqlalchemy.orm import sessionmaker, relationship, backref
import os
import pyotp
@@ -13,35 +13,30 @@ 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'
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 SQLite e verifica timeout
"""
session = SessionLocal()
"""Retorna uma nova sessão do banco de dados"""
Session = sessionmaker(bind=engine)
db = Session()
try:
# Verificar timeout para usuários logados
usuario_atual = session.query(Usuario).filter(
Usuario.ultimo_login.isnot(None),
Usuario.ultimo_logout.is_(None)
).first()
if usuario_atual and usuario_atual.check_session_timeout():
usuario_atual.logout()
session.commit()
raise Exception("Sessão expirada. Por favor, faça login novamente.")
return session
except Exception as e:
session.close()
raise e
# 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):
"""
@@ -58,15 +53,21 @@ 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'))
cr_id = Column(Integer, ForeignKey('comites_regionais.id'))
secretario = Column(Integer, ForeignKey('militantes.id'))
responsavel_financas = Column(Integer, ForeignKey('militantes.id'))
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
@@ -83,10 +84,10 @@ class ComiteRegional(Base):
id = Column(Integer, primary_key=True, autoincrement=True)
nome = Column(String(100), nullable=False)
responsavel_financas = Column(Integer, ForeignKey('militantes.id'))
responsavel_formacao = Column(Integer, ForeignKey('militantes.id'))
secretario_organizacao = Column(Integer, ForeignKey('militantes.id'))
correspondente_jornal = Column(Integer, ForeignKey('militantes.id'))
responsavel_financas = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_cr_responsavel_financas'))
responsavel_formacao = Column(Integer, ForeignKey('militantes.id', 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])
@@ -144,7 +145,7 @@ class Militante(Base):
# Relacionamento para múltiplos emails
emails = relationship("EmailMilitante", back_populates="militante")
# Endereço
endereco_id = Column(Integer, ForeignKey('enderecos.id'))
endereco_id = Column(Integer, ForeignKey('enderecos.id', use_alter=True, name='fk_militante_endereco'))
endereco = relationship("Endereco", back_populates="militantes")
# Redes sociais
redes_sociais = relationship("RedeSocial", back_populates="militante")
@@ -162,9 +163,9 @@ class Militante(Base):
dirigente_sindical = Column(Boolean)
central_sindical = Column(String(100))
# Responsável pelo cadastro
registrado_por = Column(Integer, ForeignKey('militantes.id'))
registrado_por = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_militante_registrado_por'))
# Campos existentes
celula_id = Column(Integer, ForeignKey('celulas.id'))
celula_id = Column(Integer, ForeignKey('celulas.id', use_alter=True, name='fk_militante_celula'))
responsabilidades = Column(Integer, default=0)
otp_secret = Column(String(32))
temp_token = Column(String(64))
@@ -176,14 +177,21 @@ class Militante(Base):
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")
vendas_jornais = relationship("VendaJornal", back_populates="militante")
assinaturas = relationship("AssinaturaJornal", back_populates="militante")
celula = relationship("Celula", back_populates="militantes", foreign_keys=[celula_id])
comprovantes = relationship("Comprovante", back_populates="militante")
vendas_jornais_avulsos = relationship("VendaJornalAvulso", back_populates="militante")
# Constantes para responsabilidades
SECRETARIO = 1
@@ -296,6 +304,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")
@@ -353,7 +363,7 @@ class VendaJornalAvulso(Base):
valor_total = Column(Numeric(10, 2), nullable=False)
data_venda = Column(Date, nullable=False)
militante = relationship("Militante", back_populates="vendas_jornais")
militante = relationship("Militante", back_populates="vendas_jornais_avulsos")
class AssinaturaAnual(Base):
__tablename__ = 'assinaturas_anuais'
@@ -374,9 +384,9 @@ class Setor(Base):
id = Column(Integer, primary_key=True)
nome = Column(String(100), nullable=False)
cr_id = Column(Integer, ForeignKey('comites_regionais.id'))
responsavel = Column(Integer, ForeignKey('militantes.id'))
responsavel_financas = Column(Integer, ForeignKey('militantes.id'))
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")
@@ -433,6 +443,7 @@ class Usuario(Base, UserMixin):
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'))
@@ -456,31 +467,19 @@ class Usuario(Base, UserMixin):
cr = relationship('ComiteRegional', back_populates='usuarios')
celula = relationship('Celula', back_populates='usuarios')
def get_id(self):
return str(self.id)
@property
def is_authenticated(self):
return True
@property
def is_active(self):
return self.ativo
@property
def is_anonymous(self):
return False
def __init__(self, username, password, is_admin=False, email=None, tipo="USUARIO"):
def __init__(self, username, email=None, is_admin=False, nome=None):
self.username = username
self.password_hash = generate_password_hash(password)
self.is_admin = is_admin
self.email = email
self.is_admin = is_admin
self.nome = nome
self.ativo = True
self.session_timeout = 30
self.tipo = tipo
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)
@@ -501,7 +500,11 @@ class Usuario(Base, UserMixin):
return time_diff.total_seconds() > (self.session_timeout * 60)
def has_permission(self, permission_name):
"""Verifica se o usuário tem uma determinada permissão"""
"""Verifica se o usuário tem uma permissão específica"""
if self.is_admin: # Se for admin, tem todas as permissões
return True
# Verifica se o usuário tem a permissão através de suas roles
for role in self.roles:
for permission in role.permissions:
if permission.nome == permission_name:
@@ -549,6 +552,10 @@ class Usuario(Base, UserMixin):
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'
@@ -618,149 +625,135 @@ class TransacaoPIX(Base):
status = Column(String(20)) # Pendente, Pago, Expirado
qr_code = Column(Text)
pagamento_id = Column(Integer, ForeignKey('pagamentos.id'))
comprovante_id = Column(Integer, ForeignKey('comprovantes.id'))
pagamento = relationship("Pagamento", back_populates="transacoes_pix")
comprovante = relationship("Comprovante", back_populates="transacoes_pix")
# Remover o banco de dados existente (se existir)
if os.path.exists(db_path):
os.remove(db_path)
class TipoComprovante(Base):
__tablename__ = 'tipos_comprovante'
id = Column(Integer, primary_key=True)
descricao = Column(String(50), nullable=False)
valor = Column(Float, nullable=False)
def init_rbac():
"""Inicializa o sistema RBAC"""
print("Inicializando sistema RBAC...")
class Comprovante(Base):
__tablename__ = 'comprovantes'
id = Column(Integer, primary_key=True)
militante_id = Column(Integer, ForeignKey('militantes.id'), nullable=False)
tipo_comprovante = Column(String(50)) # Cota, Jornal, Assinatura, etc.
data_comprovante = Column(Date, nullable=False)
session = SessionLocal()
try:
# Verificar se já existe um admin
admin = session.query(Usuario).filter_by(username="admin").first()
if not admin:
print("Criando role de administrador...")
# Criar role de admin
admin_role = session.query(Role).filter_by(nome="Administrador").first()
if not admin_role:
admin_role = Role(nome="Administrador", nivel=Role.SECRETARIO_GERAL)
session.add(admin_role)
session.commit()
print("Criando usuário admin...")
# Criar usuário admin
admin = Usuario(
username="admin",
password="admin123",
is_admin=True
)
admin.email = "admin@example.com"
admin.role_id = admin_role.id
# Adicionar apenas a permissão de system_config ao admin
permission = session.query(Permission).filter_by(nome='system_config').first()
if permission and permission not in admin_role.permissions:
admin_role.permissions.append(permission)
session.add(admin)
session.commit()
print("=== Usuário Admin Criado ===")
print(f"Username: admin")
print(f"Senha: admin123")
print(f"Email: {admin.email}")
print(f"OTP Secret: {admin.otp_secret}")
else:
print("Usuário admin já existe")
# Garantir que o admin tenha apenas a permissão de system_config
admin_role = session.query(Role).filter_by(nome="Administrador").first()
if admin_role:
# Remover todas as permissões atuais
admin_role.permissions = []
# Adicionar apenas a permissão de system_config
permission = session.query(Permission).filter_by(nome='system_config').first()
if permission:
admin_role.permissions.append(permission)
session.commit()
except Exception as e:
print(f"Erro na inicialização do sistema RBAC: {e}")
session.rollback()
raise
finally:
session.close()
militante = relationship("Militante", back_populates="comprovantes")
transacoes_pix = relationship("TransacaoPIX", back_populates="comprovante")
class VendaJornal(Base):
__tablename__ = 'vendas_jornais'
id = Column(Integer, primary_key=True, autoincrement=True)
militante_id = Column(Integer, ForeignKey('militantes.id'))
quantidade = Column(Integer, nullable=False)
valor_total = Column(Numeric(10, 2), nullable=False)
data_venda = Column(Date, nullable=False)
militante = relationship("Militante", back_populates="vendas_jornais")
class AssinaturaJornal(Base):
__tablename__ = 'assinaturas_jornais'
id = Column(Integer, primary_key=True, autoincrement=True)
militante_id = Column(Integer, ForeignKey('militantes.id'))
tipo_material_id = Column(Integer, ForeignKey('tipos_materiais.id'))
quantidade = Column(Integer, nullable=False)
valor_total = Column(Numeric(10, 2), nullable=False)
data_inicio = Column(Date, nullable=False)
data_fim = Column(Date, nullable=False)
militante = relationship("Militante", back_populates="assinaturas")
tipo_material = relationship("TipoMaterial", back_populates="assinaturas")
def init_database():
"""Inicializa o banco de dados com dados básicos"""
print("Inicializando banco de dados...")
# Criar todas as tabelas
Base.metadata.drop_all(engine) # Remover todas as tabelas existentes
Base.metadata.create_all(engine)
session = SessionLocal()
session = get_db_connection()
try:
# Criar role de administrador
admin_role = Role(nome="Administrador", nivel=Role.SECRETARIO_GERAL)
session.add(admin_role)
# 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()
# Verificar se existe um QR code salvo
qr_path = Path('admin_qr.png')
admin_otp_secret = None
# 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()
if qr_path.exists():
# Extrair o segredo OTP do nome do arquivo temporário dentro do QR
try:
import re
with open('admin_qr.txt', 'r') as f:
qr_content = f.read()
# O segredo OTP está no formato otpauth://totp/admin?secret=XXXXX&issuer=Sistema%20de%20Controles
match = re.search(r'secret=([A-Z0-9]+)&', qr_content)
if match:
admin_otp_secret = match.group(1)
print(f"Usando OTP existente: {admin_otp_secret}")
except Exception as e:
print(f"Erro ao ler OTP existente: {e}")
# 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()
if not admin_otp_secret:
admin_otp_secret = pyotp.random_base32()
print(f"Novo OTP gerado: {admin_otp_secret}")
# 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",
password="admin123",
is_admin=True,
email="admin@example.com",
tipo="ADMIN"
is_admin=True
)
admin.role_id = admin_role.id
admin.set_password("admin123")
admin.tipo = "ADMIN"
admin.otp_secret = admin_otp_secret
admin.roles.append(admin_role)
admin.setor = setor
session.add(admin)
session.commit()
# Gerar novo QR code se não existir
if not qr_path.exists():
totp = pyotp.totp.TOTP(admin_otp_secret)
provisioning_uri = totp.provisioning_uri("admin", issuer_name="Sistema de Controles")
# Salvar a URI em um arquivo texto para referência futura
with open('admin_qr.txt', 'w') as f:
f.write(provisioning_uri)
# Gerar QR code
import qrcode
qr = qrcode.QRCode(version=1, box_size=10, border=5)
qr.add_data(provisioning_uri)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
img.save('admin_qr.png')
# 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: {qr_path}")
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!")
except Exception as e:
print(f"Erro na inicialização do banco: {e}")
@@ -769,12 +762,5 @@ def init_database():
finally:
session.close()
# Inicializar o sistema RBAC
init_rbac()
# Inicializar o banco de dados automaticamente quando o módulo for importado
init_database()
# Executar a criação dos dados iniciais
if __name__ == "__main__":
init_database()

View File

@@ -2,7 +2,7 @@ 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
from .database import get_db_connection, Usuario, Role
from .rbac import Permission
def require_login(f):
@@ -10,25 +10,37 @@ def require_login(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')
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
# Carregar o usuário com suas roles e permissões
user = db.query(Usuario).options(
joinedload(Usuario.roles)
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')
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
@@ -39,14 +51,38 @@ def require_permission(permission_name):
@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')
flash('Você precisa estar logado para acessar esta página.', 'error')
return redirect(url_for('login'))
if not current_user.has_permission(permission_name):
flash('Você não tem permissão para acessar esta página.', 'error')
return redirect(url_for('home'))
return f(*args, **kwargs)
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

1
functions/notificacao.py Normal file
View File

@@ -0,0 +1 @@

View File

@@ -73,6 +73,7 @@ class Permission(Base):
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
@@ -107,6 +108,7 @@ class Permission(Base):
(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
@@ -131,18 +133,26 @@ class Permission(Base):
def init_rbac():
"""Inicializa o sistema RBAC com roles e permissões básicas"""
from .database import get_db_connection
from .database import Usuario, get_db_connection
session = get_db_connection()
try:
# Criar roles se não existirem
for nivel, nome in Role.get_roles_list():
role = session.query(Role).filter_by(nivel=nivel).first()
if not role:
role = Role(nome=nome, nivel=nivel)
session.add(role)
# 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 permissões se não existirem
# Criar outras roles
for nivel, nome in Role.get_roles_list():
if nome != "Administrador": # Pular Administrador pois já foi criado
role = session.query(Role).filter_by(nivel=nivel).first()
if not role:
role = Role(nome=nome, nivel=nivel)
session.add(role)
# Criar permissões
for nome, descricao in Permission.get_permissions_list():
permission = session.query(Permission).filter_by(nome=nome).first()
if not permission:
@@ -151,8 +161,20 @@ def init_rbac():
session.commit()
# Mapear permissões para roles
for role in session.query(Role).all():
# 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 = [
@@ -170,6 +192,7 @@ def init_rbac():
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()
]
@@ -182,6 +205,7 @@ def init_rbac():
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()
]
@@ -195,6 +219,7 @@ def init_rbac():
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(),
@@ -210,6 +235,7 @@ def init_rbac():
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(),
@@ -226,6 +252,7 @@ def init_rbac():
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(),
@@ -244,6 +271,7 @@ def init_rbac():
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(),
@@ -263,6 +291,7 @@ def init_rbac():
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(),
@@ -275,12 +304,6 @@ def init_rbac():
session.query(Permission).filter_by(nome=Permission.REGISTER_CC_PAYMENT).first(),
session.query(Permission).filter_by(nome=Permission.SYSTEM_CONFIG).first()
]
# Administrador
elif role.nome == "Administrador":
role.permissions = [
session.query(Permission).filter_by(nome=Permission.SYSTEM_CONFIG).first()
]
session.commit()

1
functions/relatorio.py Normal file
View File

@@ -0,0 +1 @@

23
functions/usuario.py Normal file
View File

@@ -0,0 +1,23 @@
def get_permissoes_por_cargo(cargo_id):
permissoes = {
1: [ # Secretário Geral
'gerenciar_relatorios_celula',
'visualizar_relatorios_celula',
'gerenciar_militantes',
'gerenciar_tipos_comprovante'
],
2: [ # Admin
'gerenciar_relatorios_celula',
'visualizar_relatorios_celula',
'gerenciar_militantes',
'gerenciar_tipos_comprovante'
],
3: [ # Secretário Financeiro do Comitê Central
'gerenciar_relatorios_celula',
'visualizar_relatorios_celula',
'gerenciar_militantes',
'gerenciar_tipos_comprovante'
],
# ... existing code ...
}
return permissoes.get(cargo_id, [])

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

View File

@@ -0,0 +1,18 @@
from sqlalchemy import Column, Integer, String, ForeignKey, Numeric, Date
from sqlalchemy.orm import relationship
from models.entities.base import Base
class AssinaturaJornal(Base):
__tablename__ = 'assinaturas_jornais'
id = Column(Integer, primary_key=True, autoincrement=True)
militante_id = Column(Integer, ForeignKey('militantes.id'))
tipo_material_id = Column(Integer, ForeignKey('tipos_materiais.id'))
quantidade = Column(Integer, nullable=False)
valor_total = Column(Numeric(10, 2), nullable=False)
data_inicio = Column(Date, nullable=False)
data_fim = Column(Date, nullable=False)
militante = relationship("Militante", back_populates="assinaturas", foreign_keys=[militante_id])
tipo_material = relationship("TipoMaterial", back_populates="assinaturas")

17
models/entities/base.py Normal file
View File

@@ -0,0 +1,17 @@
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text, Numeric, Date, Enum, create_engine, text
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, relationship, backref
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'
DATABASE_URL = f"sqlite:///{db_path}"
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# Base SQLAlchemy
Base = declarative_base()

View File

@@ -0,0 +1,15 @@
from sqlalchemy import Column, Integer, String, ForeignKey, Numeric, Date
from sqlalchemy.orm import relationship
from models.entities.base import Base
class Comprovante(Base):
__tablename__ = 'comprovantes'
id = Column(Integer, primary_key=True, autoincrement=True)
militante_id = Column(Integer, ForeignKey('militantes.id'), nullable=False)
tipo_comprovante = Column(String(50)) # Cota, Jornal, Assinatura, etc.
data_comprovante = Column(Date, nullable=False)
militante = relationship("Militante", back_populates="comprovantes")
transacoes_pix = relationship("TransacaoPIX", back_populates="comprovante")

View File

@@ -0,0 +1,17 @@
from sqlalchemy import Column, Integer, String, ForeignKey, Numeric, Date, Boolean
from sqlalchemy.orm import relationship
from models.entities.base import Base
class CotaMensal(Base):
__tablename__ = 'cotas_mensais'
id = Column(Integer, primary_key=True, autoincrement=True)
militante_id = Column(Integer, ForeignKey('militantes.id'))
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")

View File

@@ -0,0 +1,14 @@
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
from models.entities.base import Base
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))
# Relacionamentos
militante = relationship("Militante", back_populates="emails")

View File

@@ -0,0 +1,19 @@
from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import relationship
from models.entities.base import Base
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))
# Relacionamentos
militantes = relationship("Militante", back_populates="endereco")

View File

@@ -0,0 +1,17 @@
from sqlalchemy import Column, Integer, String, ForeignKey, Numeric, Date
from sqlalchemy.orm import relationship
from models.entities.base import Base
class MaterialVendido(Base):
__tablename__ = 'materiais_vendidos'
id = Column(Integer, primary_key=True, autoincrement=True)
militante_id = Column(Integer, ForeignKey('militantes.id'))
tipo_material_id = Column(Integer, ForeignKey('tipos_materiais.id'))
descricao = Column(String(255), nullable=False)
valor = Column(Numeric(10, 2), nullable=False)
data_venda = Column(Date, nullable=False)
militante = relationship("Militante", back_populates="materiais_vendidos")
tipo_material = relationship("TipoMaterial", back_populates="materiais_vendidos")

View File

@@ -0,0 +1,155 @@
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Date, Text, Enum
from sqlalchemy.orm import relationship
from datetime import datetime
import enum
import secrets
from models.entities.base import Base
class EstadoMilitante(enum.Enum):
ATIVO = 'ativo'
DESLIGADO = 'desligado'
SUSPENSO = 'suspenso'
AFASTADO = 'afastado'
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)
# 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_avulsos = relationship("VendaJornalAvulso", back_populates="militante")
vendas_jornais = relationship("VendaJornal", back_populates="militante", foreign_keys="[VendaJornal.militante_id]")
assinaturas = relationship("AssinaturaJornal", back_populates="militante", foreign_keys="[AssinaturaJornal.militante_id]")
celula = relationship("Celula", back_populates="militantes", foreign_keys=[celula_id])
comprovantes = relationship("Comprovante", back_populates="militante")
# 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 generate_username(self):
"""Gera um nome de usuário único baseado no primeiro nome e um código"""
from sqlalchemy import func
from services.database_service import DatabaseService
db = DatabaseService.get_db_connection()
try:
# Pega o primeiro nome
primeiro_nome = self.nome.split()[0].lower()
# Importação local para evitar dependência circular
from models.entities.usuario import Usuario
# 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()

View File

@@ -0,0 +1,21 @@
from sqlalchemy import Column, Integer, String, ForeignKey, Numeric, Date
from sqlalchemy.orm import relationship
from models.entities.base import Base
class Pagamento(Base):
__tablename__ = 'pagamentos'
id = Column(Integer, primary_key=True, autoincrement=True)
militante_id = Column(Integer, ForeignKey('militantes.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")
transacoes_pix = relationship("TransacaoPIX", back_populates="pagamento")

View File

@@ -0,0 +1,15 @@
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
from models.entities.base import Base
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))
# Relacionamentos
militante = relationship("Militante", back_populates="redes_sociais")

View File

@@ -0,0 +1,13 @@
from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import relationship
from models.entities.base import Base
class TipoMaterial(Base):
__tablename__ = 'tipos_materiais'
id = Column(Integer, primary_key=True, autoincrement=True)
descricao = Column(String(100), nullable=False)
materiais_vendidos = relationship("MaterialVendido", back_populates="tipo_material")
assinaturas = relationship("AssinaturaJornal", back_populates="tipo_material")

126
models/entities/usuario.py Normal file
View File

@@ -0,0 +1,126 @@
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey
from sqlalchemy.orm import relationship, backref
from werkzeug.security import generate_password_hash, check_password_hash
from datetime import datetime, timedelta
from flask_login import UserMixin
import pyotp
from models.entities.base import Base
from models.entities.militante import Militante
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)
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'))
ativo = Column(Boolean, default=True)
is_admin = Column(Boolean, default=False)
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'))
# 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')
militante = relationship("Militante", backref=backref("usuario", uselist=False))
def __init__(self, username, email=None, is_admin=False, nome=None):
self.username = username
self.email = email
self.is_admin = is_admin
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 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
# 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 autenticação em duas etapas"""
if not self.otp_secret:
self.otp_secret = pyotp.random_base32()
return pyotp.totp.TOTP(self.otp_secret).provisioning_uri(
self.username,
issuer_name="Sistema de Controles"
)
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
totp = pyotp.totp.TOTP(self.otp_secret)
is_valid = totp.verify(code)
return is_valid
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)

View File

@@ -0,0 +1,15 @@
from sqlalchemy import Column, Integer, String, ForeignKey, Numeric, Date
from sqlalchemy.orm import relationship
from models.entities.base import Base
class VendaJornal(Base):
__tablename__ = 'vendas_jornais'
id = Column(Integer, primary_key=True, autoincrement=True)
militante_id = Column(Integer, ForeignKey('militantes.id'))
quantidade = Column(Integer, nullable=False)
valor_total = Column(Numeric(10, 2), nullable=False)
data_venda = Column(Date, nullable=False)
militante = relationship("Militante", back_populates="vendas_jornais", foreign_keys=[militante_id])

View File

@@ -0,0 +1,15 @@
from sqlalchemy import Column, Integer, String, ForeignKey, Numeric, Date
from sqlalchemy.orm import relationship
from models.entities.base import Base
class VendaJornalAvulso(Base):
__tablename__ = 'vendas_jornais_avulsos'
id = Column(Integer, primary_key=True, autoincrement=True)
militante_id = Column(Integer, ForeignKey('militantes.id'))
quantidade = Column(Integer, nullable=False)
valor_total = Column(Numeric(10, 2), nullable=False)
data_venda = Column(Date, nullable=False)
militante = relationship("Militante", back_populates="vendas_jornais_avulsos")

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

@@ -8,9 +8,12 @@ Werkzeug==3.0.1
python-dotenv==1.0.1
pyotp==2.9.0
qrcode==7.4.2
Pillow==10.2.0
Pillow==9.5.0
email-validator==2.1.0.post1
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

135
routes/admin.py Normal file
View File

@@ -0,0 +1,135 @@
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
<<<<<<< HEAD
from datetime import datetime
=======
>>>>>>> a22b0e4 (refactor(#11): Integra listagem de usuários no dashboard)
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,
<<<<<<< HEAD
users=users,
now=now
=======
users=users
>>>>>>> a22b0e4 (refactor(#11): Integra listagem de usuários no dashboard)
)
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()

61
routes/auth.py Normal file
View File

@@ -0,0 +1,61 @@
from flask import Blueprint, render_template, redirect, url_for, request, jsonify
from flask_login import login_required, current_user
from controllers.auth_controller import AuthController
from services.database_service import DatabaseService
from models.entities.usuario import Usuario
auth_bp = Blueprint('auth', __name__)
@auth_bp.route("/login", methods=["GET", "POST"])
def login():
"""Rota de login"""
if request.method == "POST":
# Processar o login através do controlador
if AuthController.login():
# Redirecionar para home em caso de sucesso
return redirect(url_for("home"))
# GET ou falha no login, renderizar template
return render_template("login.html")
@auth_bp.route("/logout")
@login_required
def logout():
"""Rota de logout"""
AuthController.logout()
return redirect(url_for('auth.login'))
@auth_bp.route("/alterar_senha", methods=["GET", "POST"])
@login_required
def alterar_senha():
"""Rota para alterar a senha do usuário"""
if request.method == "POST":
senha_atual = request.form.get("senha_atual")
nova_senha = request.form.get("nova_senha")
confirmar_senha = request.form.get("confirmar_senha")
if AuthController.alterar_senha(current_user.id, senha_atual, nova_senha, confirmar_senha):
return redirect(url_for("home"))
return render_template("alterar_senha.html")
@auth_bp.route("/qr/<token>")
def get_qr_code(token):
"""Rota para exibir QR code para configuração 2FA"""
db = DatabaseService.get_db_connection()
try:
user = db.query(Usuario).filter_by(username='admin').first()
if not user:
flash('Usuário não encontrado', 'error')
return redirect(url_for('auth.login'))
qr_uri = user.get_otp_uri()
return render_template('mostrar_qr_code.html', qr_uri=qr_uri)
finally:
db.close()
@auth_bp.route('/check_session')
def check_session():
"""Rota para verificar status da sessão via AJAX"""
return jsonify(AuthController.check_session())

View File

@@ -1,30 +1,123 @@
from flask import Blueprint, request, jsonify
from models.integracao import calcular_cota
from flask import Blueprint, render_template, redirect, url_for, request, jsonify, flash
from flask_login import login_required
cota_bp = Blueprint('cota', __name__)
from models.entities.cota_mensal import CotaMensal
from services.cota_service import CotaService
from services.militante_service import MilitanteService
from functions.decorators import require_permission
from utils.date_utils import validar_data, converter_data
@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})
cota_bp = Blueprint('cota', __name__, url_prefix='/cotas')
@cota_bp.route("/")
@login_required
@require_permission('gerenciar_cotas')
def listar():
"""Lista todas as cotas mensais"""
cotas = CotaService.listar_cotas()
# Calcular status de cada cota
for cota in cotas:
if cota.pago:
cota.status = "paga"
elif cota.data_vencimento < datetime.now().date():
cota.status = "atrasada"
else:
cota.status = "pendente"
except Exception as e:
return jsonify({'error': str(e)}), 400
militantes = MilitanteService.listar_militantes()
return render_template("listar_cotas.html", cotas=cotas, militantes=militantes)
@cota_bp.route("/novo", methods=["GET", "POST"])
@login_required
@require_permission('gerenciar_cotas')
def nova():
"""Cria uma nova cota"""
if request.method == "POST":
militante_id = request.form.get("militante_id")
valor_antigo = float(request.form.get("valor_antigo"))
valor_novo = float(request.form.get("valor_novo"))
data_alteracao = converter_data(request.form.get("data_alteracao"))
data_vencimento = converter_data(request.form.get("data_vencimento"))
# Validar datas
if not validar_data(data_alteracao) or not validar_data(data_vencimento):
flash('Datas inválidas', 'danger')
return redirect(url_for('cota.nova'))
result = CotaService.criar_cota(militante_id, valor_antigo, valor_novo,
data_alteracao, data_vencimento)
if result:
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({
'status': 'success',
'message': 'Cota cadastrada com sucesso!'
})
flash('Cota cadastrada com sucesso!', 'success')
return redirect(url_for('cota.listar'))
else:
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({
'status': 'error',
'message': 'Erro ao cadastrar cota. Verifique os dados e tente novamente.'
}), 400
flash('Erro ao cadastrar cota', 'danger')
return redirect(url_for('cota.nova'))
# GET
militantes = MilitanteService.listar_militantes()
return render_template("nova_cota.html", militantes=militantes)
@cota_bp.route('/editar/<int:id>', methods=['GET', 'POST'])
@login_required
@require_permission('gerenciar_cotas')
def editar(id):
"""Edita uma cota existente"""
cota = CotaService.buscar_cota(id)
if not cota:
flash('Cota não encontrada', 'danger')
return redirect(url_for('cota.listar'))
if request.method == 'POST':
militante_id = int(request.form['militante_id'])
valor_antigo = float(request.form['valor_antigo'])
valor_novo = float(request.form['valor_novo'])
data_alteracao = converter_data(request.form['data_alteracao'])
data_vencimento = converter_data(request.form['data_vencimento'])
pago = request.form.get('pago', '').lower() == 'true'
if CotaService.atualizar_cota(id, militante_id, valor_antigo, valor_novo,
data_alteracao, data_vencimento, pago):
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({
'status': 'success',
'message': 'Cota atualizada com sucesso!'
})
flash('Cota atualizada com sucesso!', 'success')
return redirect(url_for('cota.listar'))
else:
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({
'status': 'error',
'message': 'Erro ao atualizar cota'
}), 400
flash('Erro ao atualizar cota', 'danger')
return redirect(url_for('cota.editar', id=id))
return render_template('editar_cota.html', cota=cota)
@cota_bp.route('/excluir/<int:id>', methods=['POST'])
@login_required
@require_permission('gerenciar_cotas')
def excluir(id):
"""Exclui uma cota existente"""
if CotaService.excluir_cota(id):
flash('Cota excluída com sucesso!', 'success')
else:
flash('Erro ao excluir cota', 'danger')
return redirect(url_for('cota.listar'))

41
routes/main.py Normal file
View File

@@ -0,0 +1,41 @@
from flask import Blueprint, render_template, redirect, url_for
from flask_login import login_required
from functions.decorators import require_login
from controllers.home_controller import HomeController
main_bp = Blueprint('main', __name__)
@main_bp.route("/")
@require_login
def index():
"""Rota principal - redireciona para home se autenticado"""
return redirect(url_for('main.home'))
@main_bp.route("/home")
@require_login
def home():
"""Página inicial do sistema com dashboard"""
dashboard_data = HomeController.dashboard()
return render_template('home.html',
nome_usuario=dashboard_data['nome_usuario'],
data_atual=dashboard_data['data_atual'],
total_militantes=dashboard_data['total_militantes'],
total_cotas=dashboard_data['total_cotas'],
total_materiais=dashboard_data['total_materiais'],
total_assinaturas=dashboard_data['total_assinaturas'],
ultimos_militantes=dashboard_data['ultimos_militantes'],
ultimos_pagamentos=dashboard_data['ultimos_pagamentos'],
tipos_pagamento=dashboard_data['tipos_pagamento'],
Militante=None) # Militante class for constants
@main_bp.route("/api/setores/<int:cr_id>")
@require_login
def get_setores(cr_id):
"""API para listar setores por comitê regional"""
from services.setor_service import SetorService
setores = SetorService.listar_setores_por_cr(cr_id)
return jsonify({
'setores': [{'id': s.id, 'nome': s.nome} for s in setores]
})

50
routes/militante.py Normal file
View File

@@ -0,0 +1,50 @@
from flask import Blueprint, render_template, redirect, url_for, request, jsonify
from flask_login import login_required
from controllers.militante_controller import MilitanteController
from services.celula_service import CelulaService
from functions.decorators import require_permission, require_role
militante_bp = Blueprint('militante', __name__, url_prefix='/militantes')
@militante_bp.route("/")
@login_required
@require_permission('gerenciar_militantes')
def listar():
"""Lista todos os militantes"""
militantes = MilitanteController.listar_militantes()
celulas = CelulaService.listar_celulas()
return render_template('listar_militantes.html',
militantes=militantes,
celulas=celulas,
Militante=None) # Militante class for constants
@militante_bp.route("/criar", methods=["POST"])
@login_required
@require_permission('gerenciar_militantes')
def criar():
"""Cria um novo militante"""
return MilitanteController.criar_militante(request.form)
@militante_bp.route("/editar/<int:militante_id>", methods=["POST"])
@login_required
@require_permission('gerenciar_militantes')
def editar(militante_id):
"""Edita um militante existente"""
return MilitanteController.atualizar_militante(militante_id, request.form)
@militante_bp.route("/excluir/<int:militante_id>", methods=["POST"])
@login_required
@require_permission('gerenciar_militantes')
def excluir(militante_id):
"""Exclui um militante"""
if MilitanteController.excluir_militante(militante_id):
return redirect(url_for('militante.listar'))
return redirect(url_for('militante.listar'))
@militante_bp.route("/dados/<int:militante_id>")
@login_required
@require_permission('gerenciar_militantes')
def dados(militante_id):
"""Busca os dados de um militante específico"""
return MilitanteController.buscar_dados_militante(militante_id)

116
routes/pagamento.py Normal file
View File

@@ -0,0 +1,116 @@
from flask import Blueprint, render_template, redirect, url_for, request, jsonify, flash
from flask_login import login_required
from services.pagamento_service import PagamentoService
from services.militante_service import MilitanteService
from services.tipo_pagamento_service import TipoPagamentoService
from functions.decorators import require_permission
from utils.date_utils import validar_data, converter_data
pagamento_bp = Blueprint('pagamento', __name__, url_prefix='/pagamentos')
@pagamento_bp.route("/")
@login_required
@require_permission('gerenciar_pagamentos')
def listar():
"""Lista todos os pagamentos"""
pagamentos = PagamentoService.listar_pagamentos()
militantes = MilitanteService.listar_militantes()
tipos_pagamento = TipoPagamentoService.listar_tipos_pagamento()
return render_template("listar_pagamentos.html",
pagamentos=pagamentos,
militantes=militantes,
tipos_pagamento=tipos_pagamento)
@pagamento_bp.route("/novo", methods=["GET", "POST"])
@login_required
@require_permission('gerenciar_pagamentos')
def novo():
"""Cria um novo pagamento"""
if request.method == "POST":
militante_id = request.form.get("militante_id")
tipo_pagamento_id = request.form.get("tipo_pagamento_id")
valor = float(request.form.get("valor"))
data_pagamento = converter_data(request.form.get("data_pagamento"))
if not validar_data(data_pagamento):
flash('Data de pagamento inválida ou futura', 'danger')
return redirect(url_for('pagamento.novo'))
if PagamentoService.criar_pagamento(militante_id, tipo_pagamento_id, valor, data_pagamento):
flash('Pagamento cadastrado com sucesso!', 'success')
return redirect(url_for('pagamento.listar'))
else:
flash('Erro ao cadastrar pagamento', 'danger')
return redirect(url_for('pagamento.novo'))
# GET
militantes = MilitanteService.listar_militantes()
tipos_pagamento = TipoPagamentoService.listar_tipos_pagamento()
return render_template("novo_pagamento.html",
militantes=militantes,
tipos_pagamento=tipos_pagamento)
@pagamento_bp.route("/adicionar", methods=["POST"])
@login_required
@require_permission('gerenciar_pagamentos')
def adicionar():
"""Adiciona um novo pagamento (via AJAX)"""
militante_id = request.form.get("militante_id")
tipo_pagamento = request.form.get("tipo_pagamento")
valor = float(request.form.get("valor"))
data_pagamento = converter_data(request.form.get("data_pagamento"))
if PagamentoService.criar_pagamento_simples(militante_id, tipo_pagamento, valor, data_pagamento):
return jsonify({
'status': 'success',
'message': 'Pagamento adicionado com sucesso!'
})
else:
return jsonify({
'status': 'error',
'message': 'Erro ao adicionar pagamento'
}), 400
@pagamento_bp.route('/editar/<int:id>', methods=['GET', 'POST'])
@login_required
@require_permission('gerenciar_pagamentos')
def editar(id):
"""Edita um pagamento existente"""
pagamento = PagamentoService.buscar_pagamento(id)
if not pagamento:
flash('Pagamento não encontrado', 'danger')
return redirect(url_for('pagamento.listar'))
if request.method == 'POST':
militante_id = int(request.form['militante_id'])
tipo_pagamento_id = int(request.form['tipo_pagamento_id'])
valor = float(request.form['valor'])
data_pagamento = converter_data(request.form['data_pagamento'])
if PagamentoService.atualizar_pagamento(id, militante_id, tipo_pagamento_id, valor, data_pagamento):
flash('Pagamento atualizado com sucesso!', 'success')
return redirect(url_for('pagamento.listar'))
else:
flash('Erro ao atualizar pagamento', 'danger')
return redirect(url_for('pagamento.editar', id=id))
militantes = MilitanteService.listar_militantes()
tipos_pagamento = TipoPagamentoService.listar_tipos_pagamento()
return render_template('editar_pagamento.html',
pagamento=pagamento,
militantes=militantes,
tipos_pagamento=tipos_pagamento)
@pagamento_bp.route('/excluir/<int:id>', methods=['POST'])
@login_required
@require_permission('gerenciar_pagamentos')
def excluir(id):
"""Exclui um pagamento existente"""
if PagamentoService.excluir_pagamento(id):
flash('Pagamento excluído com sucesso!', 'success')
else:
flash('Erro ao excluir pagamento', 'danger')
return redirect(url_for('pagamento.listar'))

149
routes/relatorio.py Normal file
View File

@@ -0,0 +1,149 @@
from flask import Blueprint, render_template, redirect, url_for, request, jsonify, flash
from flask_login import login_required
from datetime import date
from services.relatorio_service import RelatorioService
from services.setor_service import SetorService
from services.comite_service import ComiteService
from functions.decorators import require_permission
from utils.date_utils import validar_data, converter_data
relatorio_bp = Blueprint('relatorio', __name__, url_prefix='/relatorios')
# Rotas para relatórios de cotas
@relatorio_bp.route("/cotas")
@login_required
@require_permission('visualizar_relatorios')
def listar_cotas():
"""Lista todos os relatórios de cotas"""
relatorios = RelatorioService.listar_relatorios_cotas()
return render_template("listar_relatorios_cotas.html", relatorios=relatorios)
@relatorio_bp.route("/cotas/novo", methods=["GET", "POST"])
@login_required
@require_permission('gerar_relatorios')
def novo_relatorio_cotas():
"""Cria um novo relatório de cotas"""
if request.method == "POST":
setor_id = request.form.get("setor_id")
comite_id = request.form.get("comite_id")
total_cotas = float(request.form.get("total_cotas"))
data_relatorio = request.form.get("data_relatorio")
# Validar data
if not validar_data(data_relatorio):
flash('Data do relatório inválida', 'danger')
return render_template("novo_relatorio_cotas.html")
# Converter data
data_relatorio = converter_data(data_relatorio)
# Validar data futura
if data_relatorio > date.today():
flash('A data do relatório não pode ser futura', 'danger')
return render_template("novo_relatorio_cotas.html")
if RelatorioService.criar_relatorio_cotas(setor_id, comite_id, total_cotas, data_relatorio):
flash('Relatório de cotas cadastrado com sucesso!', 'success')
return redirect(url_for('relatorio.listar_cotas'))
else:
flash('Erro ao cadastrar relatório de cotas', 'danger')
return render_template("novo_relatorio_cotas.html")
# GET
setores = SetorService.listar_setores()
comites = ComiteService.listar_comites()
return render_template("novo_relatorio_cotas.html",
setores=setores,
comites=comites,
hoje=date.today().strftime('%Y-%m-%d'))
@relatorio_bp.route('/cotas/editar/<int:id>', methods=['GET', 'POST'])
@login_required
@require_permission('gerar_relatorios')
def editar_relatorio_cotas(id):
"""Edita um relatório de cotas existente"""
relatorio = RelatorioService.buscar_relatorio_cotas(id)
if not relatorio:
flash('Relatório não encontrado', 'danger')
return redirect(url_for('relatorio.listar_cotas'))
if request.method == 'POST':
setor_id = int(request.form['setor_id']) if request.form['setor_id'] else None
comite_id = int(request.form['comite_id']) if request.form['comite_id'] else None
total_cotas = float(request.form['total_cotas'])
data_relatorio = converter_data(request.form['data_relatorio'])
if RelatorioService.atualizar_relatorio_cotas(id, setor_id, comite_id, total_cotas, data_relatorio):
flash('Relatório atualizado com sucesso!', 'success')
return redirect(url_for('relatorio.listar_cotas'))
else:
flash('Erro ao atualizar relatório', 'danger')
return redirect(url_for('relatorio.editar_relatorio_cotas', id=id))
setores = SetorService.listar_setores()
comites = ComiteService.listar_comites()
return render_template('editar_relatorio_cotas.html',
relatorio=relatorio,
setores=setores,
comites=comites)
@relatorio_bp.route('/cotas/excluir/<int:id>', methods=['POST'])
@login_required
@require_permission('gerar_relatorios')
def excluir_relatorio_cotas(id):
"""Exclui um relatório de cotas existente"""
if RelatorioService.excluir_relatorio_cotas(id):
flash('Relatório excluído com sucesso!', 'success')
else:
flash('Erro ao excluir relatório', 'danger')
return redirect(url_for('relatorio.listar_cotas'))
# Rotas para relatórios de vendas
@relatorio_bp.route("/vendas")
@login_required
@require_permission('visualizar_relatorios')
def listar_vendas():
"""Lista todos os relatórios de vendas"""
relatorios = RelatorioService.listar_relatorios_vendas()
return render_template("listar_relatorios_vendas.html", relatorios=relatorios)
@relatorio_bp.route("/vendas/novo", methods=["GET", "POST"])
@login_required
@require_permission('gerar_relatorios')
def novo_relatorio_vendas():
"""Cria um novo relatório de vendas"""
if request.method == "POST":
setor_id = request.form.get("setor_id")
comite_id = request.form.get("comite_id")
total_vendas = float(request.form.get("total_vendas"))
data_relatorio = request.form.get("data_relatorio")
# Validar data
if not validar_data(data_relatorio):
flash('Data do relatório inválida', 'danger')
return render_template("novo_relatorio_vendas.html")
# Converter data
data_relatorio = converter_data(data_relatorio)
# Validar data futura
if data_relatorio > date.today():
flash('A data do relatório não pode ser futura', 'danger')
return render_template("novo_relatorio_vendas.html")
if RelatorioService.criar_relatorio_vendas(setor_id, comite_id, total_vendas, data_relatorio):
flash('Relatório de vendas cadastrado com sucesso!', 'success')
return redirect(url_for('relatorio.listar_vendas'))
else:
flash('Erro ao cadastrar relatório de vendas', 'danger')
return render_template("novo_relatorio_vendas.html")
# GET
setores = SetorService.listar_setores()
comites = ComiteService.listar_comites()
return render_template("novo_relatorio_vendas.html",
setores=setores,
comites=comites,
hoje=date.today().strftime('%Y-%m-%d'))

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

44
scripts/prepare_mvc.sh Executable file
View File

@@ -0,0 +1,44 @@
#!/bin/bash
# Script para preparar a estrutura MVC
echo "Preparando a estrutura MVC para o Sistema de Controles..."
# Criar estrutura de diretórios
echo "Criando estrutura de diretórios..."
mkdir -p models/entities controllers services
# Mover arquivos refatorados
echo "Movendo arquivos refatorados..."
cp app.py.new app.py
# Criar arquivo __init__.py nos diretórios Python
echo "Criando arquivos de inicialização..."
touch models/__init__.py
touch models/entities/__init__.py
touch controllers/__init__.py
touch services/__init__.py
# Criar arquivo __init__.py com importações para models/entities
cat > models/entities/__init__.py << EOF
from models.entities.base import Base
from models.entities.usuario import Usuario, TipoUsuario
from models.entities.militante import Militante, EstadoMilitante
from models.entities.endereco import Endereco
from models.entities.email_militante import EmailMilitante
from models.entities.rede_social import RedeSocial
from models.entities.cota_mensal import CotaMensal
from models.entities.pagamento import Pagamento
from models.entities.tipo_material import TipoMaterial
from models.entities.material_vendido import MaterialVendido
from models.entities.venda_jornal import VendaJornal
from models.entities.venda_jornal_avulso import VendaJornalAvulso
from models.entities.assinatura_jornal import AssinaturaJornal
from models.entities.comprovante import Comprovante
EOF
echo "Todos os arquivos criados com sucesso!"
echo "Para usar a nova estrutura MVC, execute:"
echo "1. chmod +x scripts/prepare_mvc.sh"
echo "2. ./scripts/prepare_mvc.sh"
echo "3. python app.py"

344
seed_data.py Normal file
View File

@@ -0,0 +1,344 @@
from datetime import datetime, timedelta
from functions.database import (
Base, Militante, CotaMensal, TipoComprovante, Comprovante,
MaterialVendido, TipoMaterial, VendaJornalAvulso, AssinaturaAnual,
RelatorioCotasMensais, RelatorioVendasMateriais, engine, SessionLocal,
Setor, ComiteCentral, Usuario, Role, EmailMilitante, Endereco,
ComiteRegional, Celula, EstadoMilitante, get_db_connection,
init_database
)
import random
from faker import Faker
import time
from werkzeug.security import generate_password_hash
fake = Faker('pt_BR')
def criar_estrutura_organizacional(session):
"""Cria a estrutura organizacional básica"""
print("\nCriando estrutura organizacional...")
# Criar Comitê Central
cc = ComiteCentral(nome="Comitê Central SP")
session.add(cc)
session.flush()
# Criar Comitês Regionais
crs = []
for nome in ["CR São Paulo", "CR ABC", "CR Campinas"]:
cr = ComiteRegional(nome=nome)
session.add(cr)
session.flush()
crs.append(cr)
# Criar Setores para cada CR
setores = []
for cr in crs:
for i in range(2): # 2 setores por CR
setor = Setor(
nome=f"Setor {i+1} - {cr.nome}",
cr_id=cr.id
)
session.add(setor)
session.flush()
setores.append(setor)
# Criar Células para cada Setor
for setor in setores:
for i in range(2): # 2 células por setor
celula = Celula(
nome=f"Célula {i+1} - {setor.nome}",
setor_id=setor.id
)
session.add(celula)
session.commit()
return crs, setores
def criar_tipos_comprovante(session):
"""Cria tipos de comprovante padrão"""
print("\nCriando tipos de comprovante...")
tipos = [
"Comprovante Padrão",
"Comprovante Especial",
"Comprovante Extraordinário",
"Jornal Avulso",
"Assinatura de Jornal",
"Campanha Financeira"
]
for tipo in tipos:
if not session.query(TipoComprovante).filter_by(descricao=tipo).first():
session.add(TipoComprovante(descricao=tipo))
try:
session.commit()
print("Tipos de comprovante criados com sucesso!")
except Exception as e:
session.rollback()
print(f"Erro ao criar tipos de comprovante: {e}")
def criar_tipos_material(session):
"""Cria tipos de material padrão"""
print("\nCriando tipos de material...")
tipos = [
"Jornal",
"Revista",
"Livro",
"Panfleto",
"Cartilha"
]
for tipo in tipos:
if not session.query(TipoMaterial).filter_by(descricao=tipo).first():
session.add(TipoMaterial(descricao=tipo))
session.commit()
def criar_militantes(session, num_militantes, setores):
"""Cria militantes com todos os dados necessários"""
print(f"\nCriando {num_militantes} militantes...")
militantes = []
emails_usados = set()
for i in range(num_militantes):
try:
# Dados básicos
nome = fake.name()
cpf = fake.cpf()
# Email único
while True:
email = fake.email()
if email not in emails_usados:
emails_usados.add(email)
break
# Criar endereço
endereco = Endereco(
cep=fake.postcode(),
estado=fake.estado_sigla(),
cidade=fake.city(),
bairro=fake.bairro(),
rua=fake.street_name(),
numero=str(random.randint(1, 999)),
complemento=f"Bloco {random.randint(1, 10)}, Apto {random.randint(1, 999)}" if random.random() < 0.3 else None
)
session.add(endereco)
session.flush()
# Selecionar setor e célula aleatórios
setor = random.choice(setores)
celula = random.choice(session.query(Celula).filter_by(setor_id=setor.id).all())
# Definir responsabilidades
responsabilidades = 0
if random.random() < 0.2: # 20% chance de ser Responsável de Finanças
responsabilidades |= Militante.RESPONSAVEL_FINANCAS
if random.random() < 0.2: # 20% chance de ser Responsável de Imprensa
responsabilidades |= Militante.RESPONSAVEL_IMPRENSA
if random.random() < 0.2: # 20% chance de ser Quadro-Orientador
responsabilidades |= Militante.QUADRO_ORIENTADOR
if random.random() < 0.2: # 20% chance de ser Secretário
responsabilidades |= Militante.SECRETARIO
if random.random() < 0.2: # 20% chance de ser MPS
responsabilidades |= Militante.MPS
if random.random() < 0.2: # 20% chance de ser Tesoureiro
responsabilidades |= Militante.TESOUREIRO
if random.random() < 0.2: # 20% chance de ser MNS
responsabilidades |= Militante.MNS
if random.random() < 0.2: # 20% chance de ser da Juventude
responsabilidades |= Militante.JUVENTUDE
if random.random() < 0.3: # 30% chance de ser Aspirante
responsabilidades |= Militante.ASPIRANTE
print(f"Criando militante {i+1}: {nome}")
# Criar militante com todos os dados
militante = Militante(
nome=nome,
cpf=cpf,
titulo_eleitoral=str(random.randint(100000000000, 999999999999)),
data_nascimento=fake.date_of_birth(minimum_age=18, maximum_age=65),
data_entrada_oci=fake.date_between(start_date='-5y', end_date='today'),
data_efetivacao_oci=fake.date_between(start_date='-4y', end_date='today'),
telefone1=fake.phone_number(),
telefone2=fake.phone_number() if random.random() < 0.3 else None,
profissao=fake.job(),
regime_trabalho=random.choice(['CLT', 'PJ', 'Estatutário', 'Autônomo']),
empresa=fake.company(),
contratante=fake.company() if random.random() < 0.2 else None,
instituicao_ensino=fake.company() if random.random() < 0.4 else None,
tipo_instituicao=random.choice(['Federal', 'Estadual', 'Municipal', 'Privada']) if random.random() < 0.4 else None,
sindicato=fake.company() if random.random() < 0.6 else None,
cargo_sindical=random.choice(['Diretor', 'Delegado', 'Conselheiro']) if random.random() < 0.3 else None,
dirigente_sindical=random.random() < 0.2,
central_sindical=random.choice(['CUT', 'CSP-Conlutas', 'CTB', 'Força Sindical']) if random.random() < 0.4 else None,
endereco_id=endereco.id,
celula_id=celula.id,
responsabilidades=responsabilidades,
estado=random.choice(list(EstadoMilitante))
)
session.add(militante)
session.flush()
# Criar email do militante
email_militante = EmailMilitante(
militante_id=militante.id,
endereco_email=email
)
session.add(email_militante)
militantes.append(militante)
session.commit()
except Exception as e:
print(f"Erro ao criar militante {i+1}: {e}")
session.rollback()
continue
return militantes
def criar_cotas(session, militantes):
"""Cria cotas mensais para os militantes"""
print("\nCriando cotas mensais...")
for militante in militantes:
try:
# Criar 12 cotas (1 ano) para cada militante
for i in range(12):
data_base = datetime.now() - timedelta(days=30 * i)
valor = random.uniform(50, 200)
cota = CotaMensal(
militante_id=militante.id,
valor_antigo=valor,
valor_novo=valor * 1.1,
data_alteracao=data_base,
data_vencimento=data_base + timedelta(days=30),
pago=random.choice([True, False])
)
session.add(cota)
session.commit()
except Exception as e:
print(f"Erro ao criar cotas para militante {militante.nome}: {e}")
session.rollback()
def criar_comprovantes(session, militantes):
"""Cria comprovantes para os militantes"""
print("\nCriando comprovantes...")
tipos_comprovante = session.query(TipoComprovante).all()
for militante in militantes:
try:
# Criar entre 3 e 8 comprovantes por militante
for _ in range(random.randint(3, 8)):
tipo = random.choice(tipos_comprovante)
comprovante = Comprovante(
militante_id=militante.id,
tipo_comprovante=tipo.descricao, # Usando a descrição do tipo
valor=random.uniform(10, 1000),
data_comprovante=fake.date_between(start_date='-1y', end_date='today')
)
session.add(comprovante)
session.commit()
except Exception as e:
session.rollback()
print(f"Erro ao criar comprovantes para militante {militante.nome}: {e}")
def criar_materiais_vendidos(session, militantes):
"""Cria registros de materiais vendidos"""
print("\nCriando materiais vendidos...")
tipos_material = session.query(TipoMaterial).all()
for militante in militantes:
try:
# Criar entre 2 e 5 materiais vendidos por militante
for _ in range(random.randint(2, 5)):
material = MaterialVendido(
militante_id=militante.id,
tipo_material_id=random.choice(tipos_material).id,
descricao=fake.sentence(),
valor=random.uniform(20, 100),
data_venda=fake.date_time_between(start_date='-1y', end_date='now')
)
session.add(material)
session.commit()
except Exception as e:
print(f"Erro ao criar materiais vendidos para militante {militante.nome}: {e}")
session.rollback()
def criar_vendas_jornal(session, militantes):
"""Cria vendas de jornal avulso"""
print("\nCriando vendas de jornal...")
for militante in militantes:
try:
# Criar entre 2 e 6 vendas de jornal por militante
for _ in range(random.randint(2, 6)):
quantidade = random.randint(1, 10)
valor_unitario = random.uniform(5, 15)
venda = VendaJornalAvulso(
militante_id=militante.id,
quantidade=quantidade,
valor_total=quantidade * valor_unitario,
data_venda=fake.date_time_between(start_date='-1y', end_date='now')
)
session.add(venda)
session.commit()
except Exception as e:
print(f"Erro ao criar vendas de jornal para militante {militante.nome}: {e}")
session.rollback()
def criar_assinaturas(session, militantes):
"""Cria assinaturas anuais"""
print("\nCriando assinaturas anuais...")
tipos_material = session.query(TipoMaterial).all()
for militante in militantes:
try:
# 30% de chance de ter assinatura
if random.random() < 0.3:
data_inicio = fake.date_time_between(start_date='-1y', end_date='now')
assinatura = AssinaturaAnual(
militante_id=militante.id,
tipo_material_id=random.choice(tipos_material).id,
quantidade=random.randint(1, 3),
valor_total=random.uniform(100, 500),
data_inicio=data_inicio,
data_fim=data_inicio + timedelta(days=365)
)
session.add(assinatura)
session.commit()
except Exception as e:
print(f"Erro ao criar assinatura para militante {militante.nome}: {e}")
session.rollback()
def seed_database():
"""Função principal para popular o banco de dados"""
session = get_db_connection()
try:
print("Iniciando população do banco de dados...")
# Criar estrutura organizacional
crs, setores = criar_estrutura_organizacional(session)
# Criar tipos básicos
criar_tipos_comprovante(session)
criar_tipos_material(session)
# Criar militantes (30 militantes para teste)
militantes = criar_militantes(session, 30, setores)
# Criar dados financeiros e materiais
criar_cotas(session, militantes)
criar_comprovantes(session, militantes)
criar_materiais_vendidos(session, militantes)
criar_vendas_jornal(session, militantes)
criar_assinaturas(session, militantes)
print("\nBanco de dados populado com sucesso!")
except Exception as e:
print(f"Erro durante a população do banco: {e}")
session.rollback()
finally:
session.close()
if __name__ == "__main__":
seed_database()

View File

@@ -0,0 +1,35 @@
from sqlalchemy import text
from models.entities.base import engine, SessionLocal
class DatabaseService:
"""Serviço para gerenciar conexões com o banco de dados"""
@staticmethod
def get_db_connection():
"""Retorna uma nova sessão do banco de dados"""
db = SessionLocal()
try:
# 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
@staticmethod
def execute_query(query, params=None):
"""
Executa uma query usando SQLAlchemy
"""
session = DatabaseService.get_db_connection()
try:
result = session.execute(query, params)
session.commit()
return result
except Exception as e:
session.rollback()
raise e
finally:
session.close()

View File

@@ -0,0 +1,161 @@
from sqlalchemy.orm import joinedload
from datetime import datetime
from models.entities.militante import Militante
from models.entities.email_militante import EmailMilitante
from models.entities.endereco import Endereco
from services.database_service import DatabaseService
class MilitanteService:
"""Serviço para operações com militantes"""
@staticmethod
def listar_militantes():
"""Lista todos os militantes"""
db = DatabaseService.get_db_connection()
try:
militantes = db.query(Militante)\
.options(
joinedload(Militante.celula),
joinedload(Militante.emails)
)\
.order_by(Militante.nome)\
.all()
return militantes
finally:
db.close()
@staticmethod
def buscar_militante(militante_id):
"""Busca um militante pelo ID"""
db = DatabaseService.get_db_connection()
try:
militante = db.query(Militante)\
.options(
joinedload(Militante.celula),
joinedload(Militante.emails),
joinedload(Militante.endereco),
joinedload(Militante.redes_sociais)
)\
.get(militante_id)
return militante
finally:
db.close()
@staticmethod
def buscar_por_cpf(cpf):
"""Busca um militante pelo CPF"""
db = DatabaseService.get_db_connection()
try:
militante = db.query(Militante).filter(Militante.cpf == cpf).first()
return militante
finally:
db.close()
@staticmethod
def salvar_militante(militante):
"""Salva um militante no banco de dados"""
db = DatabaseService.get_db_connection()
try:
if militante.id is None: # Novo militante
db.add(militante)
db.flush() # Para obter o ID gerado
militante_id = militante.id
else: # Militante existente
db.merge(militante)
militante_id = militante.id
db.commit()
return militante_id
except Exception as e:
db.rollback()
print(f"Erro ao salvar militante: {e}")
raise
finally:
db.close()
@staticmethod
def salvar_endereco(endereco):
"""Salva um endereço no banco de dados"""
db = DatabaseService.get_db_connection()
try:
db.add(endereco)
db.flush() # Para obter o ID gerado
endereco_id = endereco.id
db.commit()
return endereco_id
except Exception as e:
db.rollback()
print(f"Erro ao salvar endereço: {e}")
raise
finally:
db.close()
@staticmethod
def salvar_email_militante(email_militante):
"""Salva um email de militante no banco de dados"""
db = DatabaseService.get_db_connection()
try:
db.add(email_militante)
db.commit()
return email_militante.id
except Exception as e:
db.rollback()
print(f"Erro ao salvar email: {e}")
raise
finally:
db.close()
@staticmethod
def atualizar_email_militante(militante_id, email):
"""Atualiza ou cria o email principal de um militante"""
db = DatabaseService.get_db_connection()
try:
# Verificar se já existe email
email_existente = db.query(EmailMilitante)\
.filter_by(militante_id=militante_id)\
.first()
if email_existente:
email_existente.endereco_email = email
db.commit()
else:
novo_email = EmailMilitante(
endereco_email=email,
militante_id=militante_id
)
db.add(novo_email)
db.commit()
return True
except Exception as e:
db.rollback()
print(f"Erro ao atualizar email: {e}")
raise
finally:
db.close()
@staticmethod
def excluir_militante(militante_id):
"""Exclui um militante pelo ID"""
db = DatabaseService.get_db_connection()
try:
militante = db.query(Militante).get(militante_id)
if not militante:
return False
# Excluir emails associados
db.query(EmailMilitante)\
.filter_by(militante_id=militante_id)\
.delete()
# Excluir o militante
db.delete(militante)
db.commit()
return True
except Exception as e:
db.rollback()
print(f"Erro ao excluir militante: {e}")
raise
finally:
db.close()

View File

@@ -0,0 +1,95 @@
from sqlalchemy.orm import joinedload
from models.entities.usuario import Usuario
from services.database_service import DatabaseService
class UsuarioService:
"""Serviço para operações com usuários"""
@staticmethod
def listar_usuarios():
"""Lista todos os usuários"""
db = DatabaseService.get_db_connection()
try:
usuarios = db.query(Usuario).options(
joinedload(Usuario.roles),
joinedload(Usuario.militante),
joinedload(Usuario.setor),
joinedload(Usuario.cr),
joinedload(Usuario.celula)
).all()
return usuarios
finally:
db.close()
@staticmethod
def buscar_usuario(user_id):
"""Busca um usuário pelo ID"""
db = DatabaseService.get_db_connection()
try:
usuario = db.query(Usuario).options(
joinedload(Usuario.roles),
joinedload(Usuario.militante),
joinedload(Usuario.setor),
joinedload(Usuario.cr),
joinedload(Usuario.celula)
).get(user_id)
return usuario
finally:
db.close()
@staticmethod
def buscar_por_username(username):
"""Busca um usuário pelo nome de usuário"""
db = DatabaseService.get_db_connection()
try:
usuario = db.query(Usuario).filter(Usuario.username == username).first()
return usuario
finally:
db.close()
@staticmethod
def buscar_por_email(email):
"""Busca um usuário pelo email"""
db = DatabaseService.get_db_connection()
try:
usuario = db.query(Usuario).filter(Usuario.email == email).first()
return usuario
finally:
db.close()
@staticmethod
def salvar_usuario(usuario):
"""Salva um usuário no banco de dados"""
db = DatabaseService.get_db_connection()
try:
if usuario.id is None: # Novo usuário
db.add(usuario)
else: # Usuário existente
db.merge(usuario)
db.commit()
return True
except Exception as e:
db.rollback()
print(f"Erro ao salvar usuário: {e}")
return False
finally:
db.close()
@staticmethod
def excluir_usuario(user_id):
"""Exclui um usuário pelo ID"""
db = DatabaseService.get_db_connection()
try:
usuario = db.query(Usuario).get(user_id)
if usuario:
db.delete(usuario)
db.commit()
return True
return False
except Exception as e:
db.rollback()
print(f"Erro ao excluir usuário: {e}")
return False
finally:
db.close()

View File

@@ -2,17 +2,17 @@ from setuptools import setup, find_packages
setup(
name="controles",
version="0.1.0",
version="0.1",
packages=find_packages(),
include_package_data=True,
install_requires=[
"fastapi",
"uvicorn",
"sqlalchemy",
"python-jose[cryptography]",
"passlib[bcrypt]",
"python-multipart",
"qrcode",
"pillow",
"python-dotenv"
'flask',
'flask-login',
'flask-sqlalchemy',
'flask-wtf',
'flask-mail',
'python-dotenv',
'pyotp',
'qrcode',
],
)

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

51
static/js/comprovantes.js Normal file
View File

@@ -0,0 +1,51 @@
$(document).ready(function() {
// Inicialização da tabela
$('#tabelaComprovantes').DataTable({
language: {
url: '//cdn.datatables.net/plug-ins/1.13.7/i18n/pt-BR.json'
}
});
// Modal de edição
$('#modalEditarComprovante').on('show.bs.modal', function(event) {
var button = $(event.relatedTarget);
var comprovanteId = button.data('comprovante-id');
var militanteId = button.data('militante-id');
var militanteNome = button.data('militante-nome');
var tipoComprovante = button.data('tipo-comprovante');
var valor = button.data('valor');
var dataComprovante = button.data('data-comprovante');
var modal = $(this);
modal.find('#editMilitante').val(militanteId);
modal.find('#editMilitanteNome').val(militanteNome);
modal.find('#editTipoComprovante').val(tipoComprovante);
modal.find('#editValor').val(valor);
modal.find('#editDataComprovante').val(dataComprovante);
modal.find('form').attr('action', '/comprovantes/editar/' + comprovanteId);
});
// Modal de exclusão
$('#modalExcluirComprovante').on('show.bs.modal', function(event) {
var button = $(event.relatedTarget);
var comprovanteId = button.data('comprovante-id');
var comprovanteInfo = button.data('comprovante-info');
var modal = $(this);
modal.find('#comprovanteInfo').text(comprovanteInfo);
modal.find('form').attr('action', '/comprovantes/excluir/' + comprovanteId);
});
// Formatação de valores monetários
$('.money').mask('000.000.000.000.000,00', {reverse: true});
// Validação de formulários
$('form').on('submit', function(e) {
if (!this.checkValidity()) {
e.preventDefault();
e.stopPropagation();
}
$(this).addClass('was-validated');
});
});

127
static/js/cotas.js Normal file
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 comprovantes
document.querySelectorAll('.list-group-item[onclick*="carregarDadosComprovante"]').forEach(item => {
item.addEventListener('click', function(e) {
const comprovanteId = this.getAttribute('data-comprovante-id');
if (comprovanteId) {
carregarDadosComprovante(comprovanteId);
}
});
});
});

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

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

@@ -0,0 +1,208 @@
// Função para converter data DD/MM/YYYY para objeto Date
function converterDataParaComparacao(dataStr) {
console.log('Convertendo data para comparação:', dataStr);
if (!dataStr) return null;
try {
// Se já estiver no formato ISO
if (/^\d{4}-\d{2}-\d{2}/.test(dataStr)) {
const data = new Date(dataStr);
console.log('Data ISO convertida:', data);
return data;
}
// Se estiver no formato DD/MM/YYYY
if (/^\d{2}\/\d{2}\/\d{4}/.test(dataStr)) {
const [dia, mes, ano] = dataStr.split('/').map(Number);
const data = new Date(ano, mes - 1, dia);
console.log('Data DD/MM/YYYY convertida:', data);
return data;
}
console.warn('Formato de data não reconhecido:', dataStr);
return null;
} catch (error) {
console.error('Erro ao converter data:', error, 'Data:', dataStr);
return null;
}
}
// Função para ordenar tabelas
function configurarOrdenacaoTabela(tabelaId) {
console.log('Configurando ordenação para tabela:', tabelaId);
const table = document.getElementById(tabelaId);
if (!table) {
console.warn('Tabela não encontrada:', tabelaId);
return;
}
const headers = table.querySelectorAll('th[data-sort]');
headers.forEach(header => {
if (header.dataset.sort) {
header.addEventListener('click', () => {
const column = header.dataset.sort;
const tbody = table.getElementsByTagName('tbody')[0];
const rows = Array.from(tbody.getElementsByTagName('tr'));
console.log('Ordenando coluna:', column);
rows.sort((a, b) => {
const aValue = a.querySelector(`td[data-${column}]`).dataset[column];
const bValue = b.querySelector(`td[data-${column}]`).dataset[column];
// Ordenação por data
if (column === 'data' ||
column === 'data_vencimento' ||
column === 'data_alteracao' ||
column === 'data_comprovante' ||
column === 'data_venda' ||
column === 'data_relatorio') {
const aDate = converterDataParaComparacao(aValue);
const bDate = converterDataParaComparacao(bValue);
// Se alguma data for inválida
if (!aDate && !bDate) return 0;
if (!aDate) return 1;
if (!bDate) return -1;
return aDate - bDate;
}
// Ordenação por valor monetário
if (column === 'valor' ||
column === 'valor_total' ||
column === 'valor_antigo' ||
column === 'valor_novo') {
const aNum = parseFloat(aValue.replace(/[^\d,-]/g, '').replace(',', '.'));
const bNum = parseFloat(bValue.replace(/[^\d,-]/g, '').replace(',', '.'));
return aNum - bNum;
}
// Ordenação padrão para texto
return aValue.localeCompare(bValue);
});
// Alternar direção da ordenação
if (header.classList.contains('asc')) {
rows.reverse();
header.classList.remove('asc');
header.classList.add('desc');
console.log('Ordenação descendente');
} else {
header.classList.remove('desc');
header.classList.add('asc');
console.log('Ordenação ascendente');
}
// Atualizar tabela
tbody.innerHTML = '';
rows.forEach(row => tbody.appendChild(row));
});
}
});
}
// Configurar ordenação para todas as tabelas que precisam
document.addEventListener('DOMContentLoaded', function() {
console.log('Configurando ordenação para todas as tabelas...');
const tabelas = [
'materiaisTable',
'vendasTable',
'cotasTable',
'comprovantesTable'
];
tabelas.forEach(tabelaId => {
configurarOrdenacaoTabela(tabelaId);
});
});
document.addEventListener('DOMContentLoaded', function() {
console.log('Carregando script table_sort.js...');
// Função para comparar datas no formato DD/MM/YYYY
function compararDatas(a, b) {
if (!a || !b) return 0;
const [diaA, mesA, anoA] = a.split('/').map(Number);
const [diaB, mesB, anoB] = b.split('/').map(Number);
const dataA = new Date(anoA, mesA - 1, diaA);
const dataB = new Date(anoB, mesB - 1, diaB);
return dataA - dataB;
}
// Função para comparar valores monetários
function compararValores(a, b) {
const valorA = parseFloat(a.replace('R$ ', '').replace('.', '').replace(',', '.'));
const valorB = parseFloat(b.replace('R$ ', '').replace('.', '').replace(',', '.'));
if (isNaN(valorA)) return -1;
if (isNaN(valorB)) return 1;
return valorA - valorB;
}
// Configurar ordenação para todas as tabelas com classe 'table-sort'
document.querySelectorAll('table.table-sort').forEach(tabela => {
const tbody = tabela.querySelector('tbody');
const headers = tabela.querySelectorAll('th[data-sort]');
headers.forEach(header => {
const tipoOrdenacao = header.dataset.sort;
header.addEventListener('click', () => {
const rows = Array.from(tbody.querySelectorAll('tr'));
const colIndex = Array.from(header.parentNode.children).indexOf(header);
rows.sort((rowA, rowB) => {
const cellA = rowA.children[colIndex].dataset[tipoOrdenacao] || rowA.children[colIndex].textContent.trim();
const cellB = rowB.children[colIndex].dataset[tipoOrdenacao] || rowB.children[colIndex].textContent.trim();
switch (tipoOrdenacao) {
case 'data':
return compararDatas(cellA, cellB);
case 'valor':
return compararValores(cellA, cellB);
case 'numero':
return parseFloat(cellA) - parseFloat(cellB);
default:
return cellA.localeCompare(cellB);
}
});
if (header.classList.contains('asc')) {
rows.reverse();
header.classList.remove('asc');
header.classList.add('desc');
} else {
header.classList.remove('desc');
header.classList.add('asc');
}
// Remover classes de ordenação de outros headers
headers.forEach(h => {
if (h !== header) {
h.classList.remove('asc', 'desc');
}
});
// Atualizar tabela
tbody.innerHTML = '';
rows.forEach(row => tbody.appendChild(row));
});
});
});
});
function sortTable(table, column, type = 'text') {
// ... existing code ...
if (column === 'data_comprovante') {
// ... existing code ...
}
// ... existing code ...
}

284
static/js/testes.js Normal file
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,329 @@
{% extends "admin/base.html" %}
{% block title %}Dashboard Administrativo{% endblock %}
<<<<<<< HEAD
{% 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>
=======
{% block content %}
<div class="row mb-4">
<div class="col-md-4">
<div class="card">
<div class="card-body">
<h5 class="card-title">Total de Usuários</h5>
<p class="card-text display-4">{{ total_users }}</p>
<i class="fas fa-users fa-2x text-primary"></i>
>>>>>>> a22b0e4 (refactor(#11): Integra listagem de usuários no dashboard)
</div>
</div>
</div>
<div class="col-md-4">
<<<<<<< HEAD
<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 class="card">
<div class="card-body">
<h5 class="card-title">Usuários Ativos</h5>
<p class="card-text display-4">{{ active_users }}</p>
<i class="fas fa-user-check fa-2x text-success"></i>
>>>>>>> a22b0e4 (refactor(#11): Integra listagem de usuários no dashboard)
</div>
</div>
</div>
<div class="col-md-4">
<<<<<<< HEAD
<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 class="card">
<div class="card-body">
<h5 class="card-title">Usuários Inativos</h5>
<p class="card-text display-4">{{ inactive_users }}</p>
<i class="fas fa-user-times fa-2x text-danger"></i>
>>>>>>> a22b0e4 (refactor(#11): Integra listagem de usuários no dashboard)
</div>
</div>
</div>
</div>
<<<<<<< HEAD
<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>
=======
<div class="card">
<div class="card-header">
<h5 class="mb-0">Gerenciamento de Usuários</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table id="users-table" class="table table-striped">
<thead>
<tr>
<th>Email</th>
<th>Nome</th>
<th>Status</th>
<th>Último Login</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{ user.email }}</td>
<td>{{ user.name }}</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>
<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" onclick="return confirm('Confirma o reset do OTP deste usuário?')">
<i class="fas fa-key"></i> Reset OTP
>>>>>>> a22b0e4 (refactor(#11): Integra listagem de usuários no dashboard)
</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() }}">
<<<<<<< HEAD
<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>
=======
<button type="submit" class="btn btn-info btn-sm" onclick="return confirm('Confirma o reset da senha deste usuário?')">
<i class="fas fa-lock"></i> Reset Senha
</button>
</form>
<button onclick="toggleUserStatus({{ user.id }})" class="btn btn-{% if user.is_active %}danger{% else %}success{% endif %} btn-sm">
<i class="fas fa-{% if user.is_active %}user-times{% else %}user-check{% endif %}"></i>
{{ "Desativar" if user.is_active else "Ativar" }}
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
>>>>>>> a22b0e4 (refactor(#11): Integra listagem de usuários no dashboard)
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
<<<<<<< HEAD
=======
function toggleUserStatus(userId) {
if (confirm('Deseja alterar o status deste usuário?')) {
fetch(`/admin/users/${userId}/toggle-status`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}'
}
}).then(response => {
if (response.ok) {
window.location.reload();
}
});
}
}
>>>>>>> a22b0e4 (refactor(#11): Integra listagem de usuários no dashboard)
$(document).ready(function() {
$('#users-table').DataTable({
language: {
url: '//cdn.datatables.net/plug-ins/1.13.7/i18n/pt-BR.json'
},
<<<<<<< HEAD
order: [[0, 'asc']],
pageLength: 25
=======
order: [[1, 'asc']]
>>>>>>> a22b0e4 (refactor(#11): Integra listagem de usuários no dashboard)
});
});
</script>
{% endblock %}

View File

@@ -1,156 +1,633 @@
<!DOCTYPE html>
<html lang="pt-BR">
<html lang="pt-br">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{% endblock %} - Sistema de Controle OCI</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" rel="stylesheet">
<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 {
padding-top: 56px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f8f9fa;
margin: 0;
padding: 0;
min-height: 100vh;
}
.navbar {
background: #343a40 !important;
padding: 0.5rem 1rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
border-bottom: var(--navbar-stripe);
}
.navbar > .container-fluid {
width: 100%;
max-width: 1320px;
margin: 0 auto;
padding: 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.navbar-brand {
font-weight: bold;
}
.nav-link {
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 {
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
border: none;
border-radius: 0.5rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
height: 100%;
}
.card-header {
background-color: #f8f9fa;
border-bottom: 1px solid #dee2e6;
border-bottom: 1px solid #e9ecef;
padding: 1rem;
}
.btn-primary {
background-color: #0d6efd;
border-color: #0d6efd;
.card-header .card-title {
margin: 0;
display: flex;
align-items: center;
gap: 0.5rem;
}
.btn-success {
background-color: #198754;
border-color: #198754;
.card-header h5 {
margin: 0;
display: flex;
align-items: center;
font-size: 1.1rem;
}
.btn-secondary {
background-color: #6c757d;
border-color: #6c757d;
.card-header h5 i {
margin-right: 0.75rem;
color: var(--primary-color);
}
.btn-outline-primary {
color: #0d6efd;
border-color: #0d6efd;
.card-body {
padding: 1.5rem;
}
.btn-outline-primary:hover {
background-color: #0d6efd;
color: #fff;
.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;
}
.form-control:focus {
border-color: #0d6efd;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
.btn-primary {
background-color: var(--primary-color);
border-color: var(--primary-color);
}
.form-select:focus {
border-color: #0d6efd;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
.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>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
<div class="container">
<a class="navbar-brand" href="{{ url_for('home') }}">Sistema de Controle OCI</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">
{% if current_user is defined and current_user.is_authenticated %}
{% if current_user.is_admin %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('dashboard_admin') }}">Dashboard Admin</a>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('home') }}">Início</a>
</li>
{% if current_user.has_permission('view_cell_data') %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('listar_militantes') }}">Militantes</a>
</li>
{% endif %}
{% if current_user.has_permission('view_cell_reports') %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('listar_pagamentos') }}">Pagamentos</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('listar_materiais') }}">Materiais</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('listar_relatorios_vendas') }}">Vendas</a>
</li>
{% endif %}
{% if current_user.has_permission('view_cell_reports') or current_user.has_permission('view_sector_reports') or current_user.has_permission('view_cr_reports') %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown">
Relatórios
</a>
<ul class="dropdown-menu">
{% if current_user.has_permission('view_cell_reports') %}
<li><a class="dropdown-item" href="{{ url_for('listar_relatorios_cotas') }}">Relatórios de Cotas</a></li>
<li><a class="dropdown-item" href="{{ url_for('listar_relatorios_vendas') }}">Relatórios de Vendas</a></li>
{% endif %}
</ul>
</li>
{% endif %}
{% endif %}
{% endif %}
<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 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 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 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">
{% if current_user is defined and current_user.is_authenticated %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('logout') }}">Sair</a>
<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>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('login') }}">Login</a>
</li>
{% endif %}
</ul>
</div>
{% endif %}
</div>
</nav>
<div class="container mt-4">
{% block content %}{% endblock %}
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Verificar status da sessão a cada 5 minutos
function checkSession() {
fetch('/check_session')
.then(response => response.json())
.then(data => {
if (data.expired) {
window.location.href = '/login';
}
})
.catch(error => console.error('Erro ao verificar sessão:', error));
}
{% endblock %}
// Verificar a cada 5 minutos
setInterval(checkSession, 5 * 60 * 1000);
// Verificar também quando a página ganha foco
document.addEventListener('visibilitychange', function() {
if (document.visibilityState === 'visible') {
checkSession();
}
});
</script>
<div class="page-wrapper">
<div class="container py-4">
{% block content %}{% endblock %}
</div>
</div>
<!-- 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>
</html>

View File

@@ -17,6 +17,7 @@
{% 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>

View File

@@ -1,63 +1,77 @@
{% extends 'base.html' %}
{% 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 mb-4">
<div class="card-header">
<h5 class="card-title mb-0">Gerenciamento de Usuários</h5>
<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-header bg-dark text-white">
<h3 class="mb-0"><i class="fas fa-users-cog"></i> Administração de Usuários</h3>
</div>
<div class="card-body">
<div class="alert alert-info" role="alert">
<i class="fas fa-info-circle"></i> Aqui você pode gerenciar todos os usuários do sistema. Use os controles abaixo para ativar/desativar contas ou alterar níveis de acesso.
</div>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<table class="table table-hover">
<thead class="thead-light">
<tr>
<th>ID</th>
<th>Usuário</th>
<th>Email</th>
<th>Admin</th>
<th>OTP Configurado</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.id }}</td>
<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 %}">
<span class="badge {% if usuario.ativo %}badge-success{% else %}badge-danger{% endif %}">
{{ "Ativo" if usuario.ativo else "Inativo" }}
</span>
</td>
<td>
{% if usuario.is_admin %}
<span class="badge bg-success">Sim</span>
Administrador
{% else %}
<span class="badge bg-secondary">Não</span>
{{ usuario.nivel }}
{% endif %}
</td>
<td>
{% if usuario.otp_secret %}
<span class="badge bg-success">Sim</span>
{% else %}
<span class="badge bg-danger">Não</span>
{% endif %}
</td>
<td>
<form action="{{ url_for('reset_otp', user_id=usuario.id) }}" method="POST" class="d-inline">
<button type="submit" class="btn btn-warning btn-sm"
onclick="return confirm('Tem certeza que deseja resetar o OTP deste usuário?')">
Resetar OTP
<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>
</form>
<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 %}
@@ -66,18 +80,127 @@
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Ações Rápidas</h5>
</div>
<div class="card-body">
<div class="d-grid gap-2">
<a href="{{ url_for('novo_usuario') }}" class="btn btn-primary">
Criar Novo Usuário
</a>
</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 %}
{% 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,43 @@
{% extends 'base.html' %}
{% block title %}Editar Comprovante{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-8 offset-md-2">
<div class="card shadow-sm">
<div class="card-header bg-light">
<h4 class="card-title mb-0">
<i class="fas fa-money-bill-wave me-2"></i>Editar Comprovante
</h4>
</div>
<div class="card-body">
<form method="post">
{% csrf_token %}
<div class="form-group">
<label for="tipo_comprovante_id">Tipo de Comprovante</label>
<select class="form-control" id="tipo_comprovante_id" name="tipo_comprovante_id" required>
<option value="1" {% if comprovante.tipo_comprovante_id == 1 %}selected{% endif %}>1 - Comprovante Padrão</option>
{% if current_user.has_permission('gerenciar_tipos_comprovante') %}
<option value="2" {% if comprovante.tipo_comprovante_id == 2 %}selected{% endif %}>2 - Comprovante Especial</option>
<option value="3" {% if comprovante.tipo_comprovante_id == 3 %}selected{% endif %}>3 - Comprovante Extraordinário</option>
<option value="4" {% if comprovante.tipo_comprovante_id == 4 %}selected{% endif %}>4 - Jornal Avulso</option>
<option value="5" {% if comprovante.tipo_comprovante_id == 5 %}selected{% endif %}>5 - Assinatura de Jornal</option>
<option value="6" {% if comprovante.tipo_comprovante_id == 6 %}selected{% endif %}>6 - Campanha Financeira</option>
{% endif %}
</select>
</div>
<div class="mb-3">
<label for="data_comprovante" class="form-label">Data do Comprovante:</label>
<input type="date" class="form-control" id="data_comprovante" name="data_comprovante"
required max="{{ hoje }}">
</div>
<button type="submit" class="btn btn-primary">Salvar</button>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

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

@@ -16,7 +16,9 @@
{% endif %}
{% endwith %}
<form method="POST" class="needs-validation" novalidate>
<form id="formEditarMilitante" method="POST" class="needs-validation" novalidate>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="militante_id" value="{{ militante.id }}">
<div class="row">
<div class="col-md-6 mb-3">
<label for="nome" class="form-label">Nome</label>
@@ -28,7 +30,7 @@
<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.email }}" required>
<input type="email" class="form-control" id="email" name="email" value="{{ militante.emails[0].endereco_email if militante.emails else '' }}" required>
<div class="invalid-feedback">
Por favor, insira um email válido.
</div>
@@ -209,21 +211,43 @@
<script>
// Validação do formulário
(function () {
'use strict'
'use strict';
var forms = document.querySelectorAll('.needs-validation')
var forms = document.querySelectorAll('.needs-validation');
Array.prototype.slice.call(forms)
.forEach(function (form) {
form.addEventListener('submit', function (event) {
event.preventDefault();
if (!form.checkValidity()) {
event.preventDefault()
event.stopPropagation()
event.stopPropagation();
} else {
salvarAlteracoesMilitante({{ militante.id }});
}
form.classList.add('was-validated')
}, false)
})
})()
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

@@ -1,12 +1,12 @@
{% extends 'base.html' %}
{% block title %}Editar Relatório de Pagamentos{% endblock %}
{% block title %}Editar Relatório de Comprovantes{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Editar Relatório de Pagamentos</h1>
<h1 class="mb-4">Editar Relatório de Comprovantes</h1>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
@@ -44,10 +44,10 @@
</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>
<label for="total_comprovantes" class="form-label">Total de Comprovantes</label>
<input type="number" class="form-control" id="total_comprovantes" name="total_comprovantes" step="0.01" value="{{ relatorio.total_comprovantes }}" required>
<div class="invalid-feedback">
Por favor, insira o total de pagamentos.
Por favor, insira o total de comprovantes.
</div>
</div>
@@ -61,7 +61,7 @@
<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>
<a href="{{ url_for('listar_relatorios_comprovantes') }}" class="btn btn-outline-secondary">Voltar</a>
</div>
</form>
</div>

View File

@@ -1,34 +1,612 @@
{% extends 'base.html' %}
{% extends "base.html" %}
{% block title %}Home{% endblock %}
{% block title %}Início{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Bem-vindo, {{ current_user.username }}!</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="row">
{% for link in links %}
<div class="col-md-4">
<div class="card mb-4">
<div class="card-body">
<h5 class="card-title">{{ link.text }}</h5>
<a href="{{ link.url }}" class="btn btn-primary">Acessar</a>
</div>
</div>
</div>
{% 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

@@ -0,0 +1,49 @@
{% extends 'base.html' %}
{% block title %}Lista de Comprovantes{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<div class="card shadow-sm">
<div class="card-header bg-light">
<h4 class="card-title mb-0">
<i class="fas fa-list me-2"></i>Lista de Comprovantes
</h4>
</div>
<div class="card-body">
<table class="table table-striped">
<thead>
<tr>
<th>ID</th>
<th>Data</th>
<th>Valor</th>
<th>Tipo</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{% for comprovante in comprovantes %}
<tr>
<td>{{ comprovante.id }}</td>
<td>{{ comprovante.data.strftime('%d/%m/%Y') }}</td>
<td>R$ {{ "%.2f"|format(comprovante.valor) }}</td>
<td>
{% if comprovante.tipo_comprovante_id == 1 %}
1 - Comprovante Padrão
{% elif current_user.has_permission('gerenciar_tipos_comprovante') %}
{% if comprovante.tipo_comprovante_id == 2 %}
2 - Comprovante Especial
<td>{{ comprovante.tipo }}</td>
<td>{{ comprovante.data }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

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,219 @@
{% extends "base.html" %}
{% block title %}Comprovantes{% endblock %}
{% block content %}
<div class="container-fluid mt-3">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2><i class="fas fa-money-bill-wave"></i> Comprovantes</h2>
<div>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalNovoComprovante">
<i class="fas fa-plus"></i> Novo Comprovante
</button>
<button type="button" class="btn btn-outline-primary" id="btnExportar">
<i class="fas fa-file-export"></i> Exportar
</button>
</div>
</div>
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover" id="tabelaComprovantes">
<thead>
<tr>
<th>Militante</th>
<th>Tipo de Comprovante</th>
<th>Valor</th>
<th>Data do Comprovante</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{% for comprovante in comprovantes %}
<tr>
<td data-militante="{{ comprovante.militante_id }}">{{ comprovante.militante.nome if comprovante.militante else 'N/A' }}</td>
<td data-tipo="{{ comprovante.tipo_comprovante }}">
{% if comprovante.tipo_comprovante == 1 %}
Cota
{% elif comprovante.tipo_comprovante == 2 %}
Contribuição Extra
{% elif comprovante.tipo_comprovante == 3 %}
Doação
{% elif comprovante.tipo_comprovante == 4 %}
Taxa de Evento
{% elif comprovante.tipo_comprovante == 5 %}
Jornal Avulso
{% elif comprovante.tipo_comprovante == 6 %}
Assinatura de Jornal
{% elif comprovante.tipo_comprovante == 7 %}
Campanha Financeira
{% elif comprovante.tipo_comprovante == 8 %}
Outros
{% else %}
Não Definido
{% endif %}
</td>
<td data-valor="{{ comprovante.valor }}">R$ {{ "%.2f"|format(comprovante.valor) }}</td>
<td data-data="{{ comprovante.data_comprovante }}">{{ comprovante.data_comprovante.strftime('%d/%m/%Y') }}</td>
<td>
<button type="button"
class="btn btn-sm btn-outline-primary"
data-bs-toggle="modal"
data-bs-target="#modalEditarComprovante"
data-comprovante-id="{{ comprovante.id }}"
data-militante-id="{{ comprovante.militante_id }}"
data-tipo-comprovante="{{ comprovante.tipo_comprovante }}"
data-valor="{{ comprovante.valor }}"
data-data-comprovante="{{ comprovante.data_comprovante.strftime('%Y-%m-%d') }}"
title="Editar">
<i class="fas fa-edit"></i>
</button>
<button type="button"
class="btn btn-sm btn-outline-danger"
data-bs-toggle="modal"
data-bs-target="#modalExcluirComprovante"
data-comprovante-id="{{ comprovante.id }}"
data-comprovante-info="Comprovante de {{ comprovante.militante.nome if comprovante.militante else 'N/A' }} - R$ {{ "%.2f"|format(comprovante.valor) }}"
title="Excluir">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Modal Novo Comprovante -->
<div class="modal fade" id="modalNovoComprovante" 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 Comprovante</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="formNovoComprovante" method="post" action="{{ url_for('adicionar_comprovante') }}">
<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="tipoComprovante" class="form-label">Tipo de Comprovante:</label>
<select class="form-select" id="tipoComprovante" name="tipo_comprovante" required>
<option value="">Selecione o tipo</option>
<option value="1">Cota</option>
{% if current_user.has_permission('gerenciar_tipos_comprovante') %}
<option value="2">Contribuição Extra</option>
<option value="3">Doação</option>
<option value="4">Taxa de Evento</option>
<option value="5">Jornal Avulso</option>
<option value="6">Assinatura de Jornal</option>
<option value="7">Campanha Financeira</option>
<option value="8">Outros</option>
{% endif %}
</select>
</div>
<div class="mb-3">
<label for="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="dataComprovante" class="form-label">Data do Comprovante:</label>
<input type="date" class="form-control" id="dataComprovante" name="data_comprovante" required>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" class="btn btn-primary">Salvar</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Modal Editar Comprovante -->
<div class="modal fade" id="modalEditarComprovante" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="fas fa-edit"></i> Editar Comprovante</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="formEditarComprovante" method="post">
<div class="mb-3">
<label for="editMilitante" class="form-label">Militante:</label>
<input type="text" class="form-control bg-light" id="editMilitanteNome" readonly>
<input type="hidden" id="editMilitante" name="militante_id">
</div>
<div class="mb-3">
<label for="editTipoComprovante" class="form-label">Tipo de Comprovante:</label>
<select class="form-select" id="editTipoComprovante" name="tipo_comprovante" required>
<option value="">Selecione o tipo</option>
<option value="1">Cota</option>
{% if current_user.has_permission('gerenciar_tipos_comprovante') %}
<option value="2">Contribuição Extra</option>
<option value="3">Doação</option>
<option value="4">Taxa de Evento</option>
<option value="5">Jornal Avulso</option>
<option value="6">Assinatura de Jornal</option>
<option value="7">Campanha Financeira</option>
<option value="8">Outros</option>
{% endif %}
</select>
</div>
<div class="mb-3">
<label for="editValor" class="form-label">Valor:</label>
<input type="number" step="0.01" class="form-control" id="editValor" name="valor" required>
</div>
<div class="mb-3">
<label for="editDataComprovante" class="form-label">Data do Comprovante:</label>
<input type="date" class="form-control" id="editDataComprovante" name="data_comprovante" required>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" class="btn btn-primary">Salvar</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Modal Excluir Comprovante -->
<div class="modal fade" id="modalExcluirComprovante" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="fas fa-trash"></i> Excluir Comprovante</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>Tem certeza que deseja excluir este comprovante?</p>
<p id="comprovanteInfo" class="text-muted"></p>
</div>
<div class="modal-footer">
<form id="formExcluirComprovante" method="post">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" class="btn btn-danger">Excluir</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/comprovantes.js') }}"></script>
{% endblock %}

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

@@ -1,58 +1,330 @@
{% extends 'base.html' %}
{% extends "base.html" %}
{% block title %}Listar Materiais{% endblock %}
{% block title %}Materiais{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Lista 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_material') }}" class="btn btn-success">Novo Material</a>
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</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="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>Quantidade</th>
<th>Tipo</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{% for material in materiais %}
<tr>
<td>{{ material.id }}</td>
<td>{{ material.nome }}</td>
<td>{{ material.descricao }}</td>
<td>R$ {{ "%.2f"|format(material.preco) }}</td>
<td>{{ material.quantidade }}</td>
<td>{{ material.tipo.nome }}</td>
<td>
<a href="{{ url_for('editar_material', id=material.id) }}" class="btn btn-primary btn-sm">Editar</a>
<a href="{{ url_for('deletar_material', id=material.id) }}" class="btn btn-danger btn-sm" onclick="return confirm('Tem certeza que deseja excluir este material?')">Excluir</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<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,71 +1,511 @@
{% extends 'base.html' %}
{% extends "base.html" %}
{% block title %}Lista de Militantes{% endblock %}
{% block head %}
<!-- Bootstrap Datepicker CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datepicker/1.9.0/css/bootstrap-datepicker.min.css">
{% endblock %}
{% block title %}Militantes{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Lista de Militantes</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_militante') }}" class="btn btn-primary">Novo Militante</a>
<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 class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Nome</th>
<th>Email</th>
<th>Célula</th>
<th>Responsabilidades</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{% for militante in militantes %}
<tr>
<td>{{ militante.nome }}</td>
<td>{{ militante.email }}</td>
<td>{{ militante.celula.nome }}</td>
<td>
{% if militante.responsabilidades & Militante.RESPONSAVEL_FINANCAS %}
<span class="badge bg-primary">Finanças</span>
{% endif %}
{% if militante.responsabilidades & Militante.RESPONSAVEL_IMPRENSA %}
<span class="badge bg-info">Imprensa</span>
{% endif %}
{% if militante.responsabilidades & Militante.QUADRO_ORIENTADOR %}
<span class="badge bg-success">Quadro-Orientador</span>
{% endif %}
</td>
<td>
<a href="{{ url_for('editar_militante', id=militante.id) }}" class="btn btn-sm btn-warning">Editar</a>
<button type="button" class="btn btn-sm btn-danger" onclick="confirmarExclusao({{ militante.id }})">Excluir</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</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>
<script>
function confirmarExclusao(id) {
if (confirm('Tem certeza que deseja excluir este militante?')) {
window.location.href = "{{ url_for('excluir_militante', id=0) }}".replace('0', id);
<!-- 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>
/* Estilo para o botão Novo Militante */
.btn-primary {
background-color: var(--bs-danger);
border-color: var(--bs-danger);
}
.btn-primary:hover,
.btn-primary:focus,
.btn-primary:active {
background-color: var(--bs-danger-dark, #b02a37) !important;
border-color: var(--bs-danger-dark, #b02a37) !important;
}
/* Estilo para os switches */
.form-check-input {
background-color: #fff;
border-color: rgba(220, 53, 69, 0.5);
}
.form-check-input:checked {
background-color: var(--bs-danger);
border-color: var(--bs-danger);
}
.form-check-input:focus {
border-color: var(--bs-danger);
box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.25);
}
.form-switch .form-check-input {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28220, 53, 69, 0.85%29'/%3e%3c/svg%3e");
background-position: left center;
border-radius: 2em;
transition: background-position .15s ease-in-out;
}
.form-switch .form-check-input:checked {
background-position: right center;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e");
}
.form-check {
min-height: 1.5rem;
padding-left: 2.8em;
margin-bottom: 0.5rem;
}
.form-check-label {
cursor: pointer;
user-select: none;
}
/* Estilo para o backdrop com blur em todos os modais */
.modal-backdrop.show {
backdrop-filter: blur(8px);
background-color: rgba(0, 0, 0, 0.7);
}
/* Estilo para botões com largura fixa */
.btn-fixed-width {
min-width: 120px;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.375rem 0.75rem;
text-align: center;
height: 38px; /* Altura padrão do Bootstrap para btn */
line-height: 1.5;
vertical-align: middle;
}
.btn-fixed-width i {
margin-right: 8px;
font-size: 0.875rem; /* 14px - tamanho padrão de ícone */
}
.dropdown-toggle::after {
margin-left: 8px;
}
/* Estilo para o botão de fechar dos modais */
.btn-close {
background-color: transparent;
padding: 0.5rem;
opacity: 0.8;
transition: opacity 0.2s;
filter: invert(1) grayscale(100%) brightness(200%);
}
.btn-close:hover {
opacity: 1;
background-color: transparent;
}
/* Estilo para colunas ordenáveis */
th[data-sort] {
cursor: pointer;
user-select: none;
}
th[data-sort] i {
margin-left: 5px;
color: #ccc;
}
th[data-sort].sort-asc i,
th[data-sort].sort-desc i {
color: var(--primary-color);
}
/* Animação para linhas da tabela */
#militantesTable tbody tr {
transition: all 0.3s ease;
}
#militantesTable tbody tr:hover {
background-color: rgba(0,0,0,0.02);
transform: translateX(5px);
}
/* Estilo para badges */
.badge {
font-weight: 500;
padding: 0.4em 0.6em;
font-size: 0.75rem;
margin-right: 0.3rem;
min-width: 2em;
text-align: center;
border-radius: 4px;
cursor: help;
}
.badge:last-child {
margin-right: 0;
}
/* Cores personalizadas para badges */
.bg-purple { background-color: #6f42c1 !important; color: white !important; }
.bg-teal { background-color: #20c997 !important; color: white !important; }
.bg-orange { background-color: #fd7e14 !important; color: white !important; }
.bg-indigo { background-color: #6610f2 !important; color: white !important; }
.bg-pink { background-color: #d63384 !important; color: white !important; }
/* Cores do Bootstrap que vamos usar */
.badge.bg-primary { background-color: #0d6efd !important; }
.badge.bg-info { background-color: #0dcaf0 !important; }
.badge.bg-success { background-color: #198754 !important; }
.badge.bg-danger { background-color: #dc3545 !important; }
.badge.bg-dark { background-color: #212529 !important; }
/* Tooltip personalizado */
.tooltip {
font-size: 0.875rem;
}
.tooltip .tooltip-inner {
padding: 0.5rem 0.75rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
/* Estilo para botões de ação */
.btn-group .btn {
padding: 0.25rem 0.5rem;
}
.btn-group .btn i {
width: 16px;
text-align: center;
}
/* Estilo para modais */
.modal-content {
border: none;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
}
.modal-header {
background: linear-gradient(to right, var(--bs-gray-dark), var(--bs-gray));
color: white;
border-radius: 12px 12px 0 0;
border-bottom: none;
padding: 1rem;
}
.modal-body {
padding: 1.5rem;
}
.modal-footer {
border-top: 1px solid #eee;
padding: 1rem;
}
/* Responsividade */
@media (max-width: 768px) {
.btn-group {
display: flex;
margin-top: 1rem;
}
.btn-group .btn {
flex: 1;
}
.modal-footer {
justify-content: center;
}
.modal-footer .btn {
min-width: 120px;
}
}
</script>
/* Estilos personalizados para o Bootstrap Datepicker */
.datepicker {
padding: 4px;
border-radius: 4px;
border: 1px solid #dee2e6;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
font-size: 0.875rem;
background-color: white !important;
color: #212529 !important;
}
.datepicker table {
background-color: white !important;
}
.datepicker table tr td,
.datepicker table tr th {
text-align: center;
width: 30px;
height: 30px;
border-radius: 2px;
color: #212529 !important;
background-color: white !important;
}
.datepicker table tr td.day:hover,
.datepicker table tr td.focused {
background: #f8f9fa !important;
color: #212529 !important;
}
.datepicker table tr td.active,
.datepicker table tr td.active:hover {
background-color: var(--bs-primary) !important;
color: white !important;
}
.datepicker table tr td.today {
background-color: #e9ecef !important;
color: #212529 !important;
}
.datepicker .datepicker-switch,
.datepicker .prev,
.datepicker .next {
font-weight: normal;
padding: 4px;
color: #212529 !important;
background-color: white !important;
}
.datepicker .dow {
font-weight: normal;
padding: 4px;
color: #212529 !important;
background-color: white !important;
}
.datepicker-dropdown:after {
border-bottom-color: white !important;
}
.datepicker-dropdown.datepicker-orient-top:after {
border-top-color: white !important;
}
/* Estilo para os campos de data */
.datepicker-input {
background-color: white !important;
color: #212529 !important;
cursor: pointer;
}
.datepicker-clear-btn {
color: #6c757d !important;
background-color: white !important;
padding: 5px 10px;
font-size: 12px;
border-radius: 4px;
}
.datepicker-clear-btn:hover {
background-color: #f8f9fa !important;
color: #495057 !important;
}
</style>
{% endblock %}

View File

@@ -1,32 +0,0 @@
{% extends 'base.html' %}
{% block title %}Listar Militantes{% endblock %}
{% block content %}
<h1>Pagamentos</h1>
<a href="{{ url_for('novo_pagamento') }}">Adicionar Novo Pagamento</a>
<table border="1">
<thead>
<tr>
<th>ID</th>
<th>Militante ID</th>
<th>Tipo de Pagamento</th>
<th>Valor</th>
<th>Data do Pagamento</th>
</tr>
</thead>
<tbody>
{% for pagamento in pagamentos %}
<tr>
<td>{{ pagamento.id }}</td>
<td>{{ pagamento.militante_id }}</td>
<td>{{ pagamento.tipo_pagamento_id }}</td>
<td>R$ {{ pagamento.valor }}</td>
<td>{{ pagamento.data_pagamento }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<a href="{{ url_for('home') }}">Home</a>
{% endblock %}

View File

@@ -1,12 +1,12 @@
{% extends 'base.html' %}
{% block title %}Listar Relatórios de Pagamentos{% endblock %}
{% block title %}Listar Relatórios de Comprovantes{% 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>
<h1 class="mb-4">Lista de Relatórios de Comprovantes</h1>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
@@ -17,7 +17,7 @@
{% 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('novo_relatorio_comprovantes') }}" class="btn btn-success">Novo Relatório</a>
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
</div>
@@ -28,7 +28,7 @@
<th>ID</th>
<th>Setor</th>
<th>Comitê Central</th>
<th>Total de Pagamentos</th>
<th>Total de Comprovantes</th>
<th>Data do Relatório</th>
<th>Ações</th>
</tr>
@@ -39,11 +39,11 @@
<td>{{ relatorio.id }}</td>
<td>{{ relatorio.setor.nome }}</td>
<td>{{ relatorio.comite.nome }}</td>
<td>R$ {{ "%.2f"|format(relatorio.total_pagamentos) }}</td>
<td>R$ {{ "%.2f"|format(relatorio.total_comprovantes) }}</td>
<td>{{ relatorio.data_relatorio.strftime('%d/%m/%Y') }}</td>
<td>
<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>
<a href="{{ url_for('editar_relatorio_comprovantes', id=relatorio.id) }}" class="btn btn-primary btn-sm">Editar</a>
<a href="{{ url_for('deletar_relatorio_comprovantes', id=relatorio.id) }}" class="btn btn-danger btn-sm" onclick="return confirm('Tem certeza que deseja excluir este relatório?')">Excluir</a>
</td>
</tr>
{% endfor %}

View File

@@ -3,47 +3,47 @@
{% block title %}Listar Relatórios de Cotas{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Lista de Relatórios 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 %}
<div class="d-flex justify-content-between mb-4">
<a href="{{ url_for('novo_relatorio_cotas') }}" class="btn btn-success">Novo Relatório</a>
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
</div>
<div class="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-striped table-hover">
<table class="table table-hover" id="relatoriosTable">
<thead>
<tr>
<th>ID</th>
<th>Setor</th>
<th>Comitê Central</th>
<th>Total de Cotas</th>
<th>Data do Relatório</th>
<th>Ações</th>
<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>{{ relatorio.id }}</td>
<td>{{ relatorio.setor.nome }}</td>
<td>{{ relatorio.comite.nome }}</td>
<td>R$ {{ "%.2f"|format(relatorio.total_cotas) }}</td>
<td>{{ relatorio.data_relatorio.strftime('%d/%m/%Y') }}</td>
<td>
<a href="{{ url_for('editar_relatorio_cotas', id=relatorio.id) }}" class="btn btn-primary btn-sm">Editar</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?')">Excluir</a>
<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 %}
@@ -53,5 +53,7 @@
</div>
</div>
</div>
<script src="{{ url_for('static', filename='js/table_sort.js') }}"></script>
{% endblock %}

View File

@@ -3,47 +3,47 @@
{% block title %}Listar Relatórios de Vendas{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">Lista de Relatórios 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('novo_relatorio_vendas') }}" class="btn btn-success">Novo Relatório</a>
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
</div>
<div class="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-striped table-hover">
<table class="table table-hover" id="relatoriosTable">
<thead>
<tr>
<th>ID</th>
<th>Setor</th>
<th>Comitê Central</th>
<th>Total de Vendas</th>
<th>Data do Relatório</th>
<th>Ações</th>
<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>{{ relatorio.id }}</td>
<td>{{ relatorio.setor.nome }}</td>
<td>{{ relatorio.comite.nome }}</td>
<td>R$ {{ "%.2f"|format(relatorio.total_vendas) }}</td>
<td>{{ relatorio.data_relatorio.strftime('%d/%m/%Y') }}</td>
<td>
<a href="{{ url_for('editar_relatorio_vendas', id=relatorio.id) }}" class="btn btn-primary btn-sm">Editar</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?')">Excluir</a>
<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 %}
@@ -53,4 +53,6 @@
</div>
</div>
</div>
<script src="{{ url_for('static', filename='js/table_sort.js') }}"></script>
{% 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,48 +1,215 @@
{% extends 'base.html' %}
{% extends "base.html" %}
{% block title %}Login{% endblock %}
{% block navbar %}{% 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">Login</h3>
<div class="alert-container">
{% 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>
<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('login') }}">
<div class="mb-3">
<label for="username" class="form-label">Usuário</label>
<input type="text" class="form-control" id="username" name="username" required>
</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_code" class="form-label">Código OTP</label>
<input type="text" class="form-control" id="otp_code" name="otp_code" required>
<small class="text-muted">Digite o código gerado pelo seu aplicativo autenticador</small>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">Entrar</button>
</div>
</form>
{% endfor %}
{% endif %}
{% endwith %}
</div>
<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>
<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>
{% endblock %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Form validation
const form = document.querySelector('form');
form.addEventListener('submit', function(event) {
if (!form.checkValidity()) {
event.preventDefault();
event.stopPropagation();
}
form.classList.add('was-validated');
});
// Toggle password visibility
const togglePassword = document.getElementById('togglePassword');
const password = document.getElementById('password');
togglePassword.addEventListener('click', function() {
const type = password.getAttribute('type') === 'password' ? 'text' : 'password';
password.setAttribute('type', type);
this.querySelector('i').classList.toggle('fa-eye');
this.querySelector('i').classList.toggle('fa-eye-slash');
});
// Auto-hide alerts after 5 seconds
const alerts = document.querySelectorAll('.alert');
alerts.forEach(alert => {
setTimeout(() => {
const bsAlert = new bootstrap.Alert(alert);
bsAlert.close();
}, 5000);
});
});
</script>
<style>
body {
background: var(--primary-color);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
margin: 0;
}
.login-container {
width: 100%;
max-width: 800px;
margin: auto;
background: #ffffff;
border-radius: 1rem;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
overflow: hidden;
}
.login-content {
padding: 2rem;
display: flex;
flex-direction: column;
align-items: center;
}
.login-header {
text-align: center;
margin-bottom: 2rem;
}
.login-logo {
height: 50px;
width: auto;
margin-bottom: 1rem;
}
.login-title {
color: #343a40;
font-weight: 600;
margin: 0;
}
form {
width: 100%;
max-width: 400px;
}
.form-floating > .form-control {
border: 1px solid #dee2e6;
border-radius: 0.5rem;
}
.form-floating > .form-control:hover {
border-color: rgba(220, 53, 69, 0.3);
}
.form-floating > .form-control:focus {
border-color: rgba(220, 53, 69, 0.5);
box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.15);
}
.form-floating > label {
padding: 1rem 0.75rem;
}
.login-button {
background-color: #0d6efd;
border-color: #0d6efd;
color: white;
border-radius: 0.5rem;
padding: 0.75rem 1.5rem;
font-weight: 500;
transition: all 0.2s ease-in-out;
}
.login-button:hover,
.login-button:focus,
.login-button:active {
background-color: #0b5ed7;
border-color: #0b5ed7;
color: white;
transform: translateY(-1px);
}
@media (min-width: 768px) {
.login-container {
display: flex;
align-items: stretch;
}
.login-content {
padding: 3rem;
}
.login-logo {
height: 60px;
}
}
@media (max-width: 767px) {
.login-container {
margin: 1rem;
}
.login-content {
padding: 1.5rem;
}
}
</style>
<link rel="stylesheet" href="{{ url_for('static', filename='css/components.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
{% endblock %}

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

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