feat: implementa sistema de responsabilidades e instâncias - Adiciona responsabilidades de Finanças e Imprensa para todas as instâncias - Cria templates genéricos para gerenciamento de instâncias - Implementa sistema de permissões baseado em RBAC - Adiciona status de Aspirante com avaliação obrigatória - Atualiza documentação com novas regras e responsabilidades - Cria testes para validação das permissões - Adiciona migração para novos campos no banco de dados
This commit is contained in:
51
templates/alterar_senha.html
Normal file
51
templates/alterar_senha.html
Normal file
@@ -0,0 +1,51 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Alterar Senha{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Alterar Senha</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="POST" action="{{ url_for('alterar_senha') }}">
|
||||
<div class="mb-3">
|
||||
<label for="senha_atual" class="form-label">Senha Atual</label>
|
||||
<input type="password" class="form-control" id="senha_atual" name="senha_atual" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="nova_senha" class="form-label">Nova Senha</label>
|
||||
<input type="password" class="form-control" id="nova_senha" name="nova_senha" required>
|
||||
<small class="text-muted">
|
||||
A senha deve ter no mínimo 8 caracteres e conter letras e números.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="confirmar_senha" class="form-label">Confirmar Nova Senha</label>
|
||||
<input type="password" class="form-control" id="confirmar_senha" name="confirmar_senha" required>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-primary">Alterar Senha</button>
|
||||
<a href="{{ url_for('home') }}" class="btn btn-secondary">Cancelar</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -65,42 +65,60 @@
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('home') }}">Início</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('listar_militantes') }}">Militantes</a>
|
||||
</li>
|
||||
<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>
|
||||
<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">
|
||||
<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>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown">
|
||||
Configurações
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
</ul>
|
||||
</li>
|
||||
{% 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>
|
||||
<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>
|
||||
{% else %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('login') }}">Login</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -111,5 +129,28 @@
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
// Verificar status da sessão a cada 5 minutos
|
||||
function checkSession() {
|
||||
fetch('/check_session')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.expired) {
|
||||
window.location.href = '/login';
|
||||
}
|
||||
})
|
||||
.catch(error => console.error('Erro ao verificar sessão:', error));
|
||||
}
|
||||
|
||||
// Verificar a cada 5 minutos
|
||||
setInterval(checkSession, 5 * 60 * 1000);
|
||||
|
||||
// Verificar também quando a página ganha foco
|
||||
document.addEventListener('visibilitychange', function() {
|
||||
if (document.visibilityState === 'visible') {
|
||||
checkSession();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
111
templates/criar_instancia.html
Normal file
111
templates/criar_instancia.html
Normal file
@@ -0,0 +1,111 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Criar {{ tipo_instancia }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h1 class="mb-4">Criar {{ tipo_instancia }}</h1>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="POST" class="needs-validation" novalidate>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="nome" class="form-label">Nome</label>
|
||||
<input type="text" class="form-control" id="nome" name="nome" required>
|
||||
<div class="invalid-feedback">
|
||||
Por favor, insira o nome da {{ tipo_instancia }}.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if tipo_instancia != 'Célula' %}
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="instancia_superior_id" class="form-label">{{ instancia_superior }}</label>
|
||||
<select class="form-select" id="instancia_superior_id" name="instancia_superior_id" required>
|
||||
<option value="">Selecione uma {{ instancia_superior }}</option>
|
||||
{% for superior in instancias_superiores %}
|
||||
<option value="{{ superior.id }}">{{ superior.nome }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div class="invalid-feedback">
|
||||
Por favor, selecione uma {{ instancia_superior }}.
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="responsavel_geral_id" class="form-label">Responsável Geral</label>
|
||||
<select class="form-select" id="responsavel_geral_id" name="responsavel_geral_id" required>
|
||||
<option value="">Selecione o responsável geral</option>
|
||||
{% for militante in militantes %}
|
||||
<option value="{{ militante.id }}">{{ militante.nome }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div class="invalid-feedback">
|
||||
Por favor, selecione o responsável geral.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="responsavel_financas_id" class="form-label">Responsável de Finanças</label>
|
||||
<select class="form-select" id="responsavel_financas_id" name="responsavel_financas_id">
|
||||
<option value="">Selecione o responsável de finanças</option>
|
||||
{% for militante in militantes %}
|
||||
<option value="{{ militante.id }}">{{ militante.nome }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="responsavel_imprensa_id" class="form-label">Responsável de Imprensa</label>
|
||||
<select class="form-select" id="responsavel_imprensa_id" name="responsavel_imprensa_id">
|
||||
<option value="">Selecione o responsável de imprensa</option>
|
||||
{% for militante in militantes %}
|
||||
<option value="{{ militante.id }}">{{ militante.nome }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between mt-4">
|
||||
<button type="submit" class="btn btn-primary">Criar</button>
|
||||
<a href="{{ url_for('listar_' + tipo_instancia.lower() + 's') }}" class="btn btn-secondary">Cancelar</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Validação do formulário
|
||||
(function () {
|
||||
'use strict'
|
||||
|
||||
var forms = document.querySelectorAll('.needs-validation')
|
||||
|
||||
Array.prototype.slice.call(forms)
|
||||
.forEach(function (form) {
|
||||
form.addEventListener('submit', function (event) {
|
||||
if (!form.checkValidity()) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
form.classList.add('was-validated')
|
||||
}, false)
|
||||
})
|
||||
})()
|
||||
</script>
|
||||
{% endblock %}
|
||||
106
templates/criar_militante.html
Normal file
106
templates/criar_militante.html
Normal file
@@ -0,0 +1,106 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Criar Militante{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h1 class="mb-4">Criar Militante</h1>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="POST" class="needs-validation" novalidate>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="nome" class="form-label">Nome</label>
|
||||
<input type="text" class="form-control" id="nome" name="nome" required>
|
||||
<div class="invalid-feedback">
|
||||
Por favor, insira o nome do militante.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="email" class="form-label">Email</label>
|
||||
<input type="email" class="form-control" id="email" name="email" required>
|
||||
<div class="invalid-feedback">
|
||||
Por favor, insira um email válido.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="celula_id" class="form-label">Célula</label>
|
||||
<select class="form-select" id="celula_id" name="celula_id" required>
|
||||
<option value="">Selecione uma célula</option>
|
||||
{% for celula in celulas %}
|
||||
<option value="{{ celula.id }}">{{ celula.nome }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div class="invalid-feedback">
|
||||
Por favor, selecione uma célula.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 mb-3">
|
||||
<label class="form-label">Responsabilidades</label>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="responsavel_financas" name="responsabilidades" value="{{ Militante.RESPONSAVEL_FINANCAS }}">
|
||||
<label class="form-check-label" for="responsavel_financas">
|
||||
Responsável de Finanças
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="responsavel_imprensa" name="responsabilidades" value="{{ Militante.RESPONSAVEL_IMPRENSA }}">
|
||||
<label class="form-check-label" for="responsavel_imprensa">
|
||||
Responsável de Imprensa
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="quadro_orientador" name="responsabilidades" value="{{ Militante.QUADRO_ORIENTADOR }}">
|
||||
<label class="form-check-label" for="quadro_orientador">
|
||||
Quadro-Orientador
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between mt-4">
|
||||
<button type="submit" class="btn btn-primary">Criar</button>
|
||||
<a href="{{ url_for('listar_militantes') }}" class="btn btn-secondary">Cancelar</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Validação do formulário
|
||||
(function () {
|
||||
'use strict'
|
||||
|
||||
var forms = document.querySelectorAll('.needs-validation')
|
||||
|
||||
Array.prototype.slice.call(forms)
|
||||
.forEach(function (form) {
|
||||
form.addEventListener('submit', function (event) {
|
||||
if (!form.checkValidity()) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
form.classList.add('was-validated')
|
||||
}, false)
|
||||
})
|
||||
})()
|
||||
</script>
|
||||
{% endblock %}
|
||||
284
templates/dashboard.html
Normal file
284
templates/dashboard.html
Normal file
@@ -0,0 +1,284 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Dashboard Administrativo{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<h1 class="mb-4">Dashboard Administrativo</h1>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Gerenciamento de Acessos</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Usuário</th>
|
||||
<th>Email</th>
|
||||
<th>Status</th>
|
||||
<th>Último Login</th>
|
||||
<th>Nível</th>
|
||||
<th>Responsabilidades</th>
|
||||
<th>Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in users %}
|
||||
{% if current_user.has_permission('system_config') or
|
||||
(current_user.has_permission('manage_cr_sectors') and user.cr_id == current_user.cr_id) or
|
||||
(current_user.has_permission('manage_sector_cells') and user.setor_id == current_user.setor_id) or
|
||||
(current_user.has_permission('manage_cell_members') and user.celula_id == current_user.celula_id) %}
|
||||
<tr>
|
||||
<td>{{ user.id }}</td>
|
||||
<td>{{ user.username }}</td>
|
||||
<td>{{ user.email }}</td>
|
||||
<td>
|
||||
{% if user.ativo %}
|
||||
<span class="badge bg-success">Ativo</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger">Inativo</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ user.ultimo_login.strftime('%d/%m/%Y %H:%M') if user.ultimo_login else 'Nunca' }}</td>
|
||||
<td>
|
||||
<span class="badge bg-info">{{ user.role }}</span>
|
||||
{% if current_user.has_permission('system_config') or
|
||||
(current_user.has_permission('manage_cr_sectors') and user.cr_id == current_user.cr_id) or
|
||||
(current_user.has_permission('manage_sector_cells') and user.setor_id == current_user.setor_id) %}
|
||||
<select class="form-select form-select-sm d-inline-block w-auto" onchange="alterarNivel({{ user.id }}, this.value)">
|
||||
<option value="">Alterar Nível</option>
|
||||
{% if current_user.has_permission('system_config') %}
|
||||
<option value="militante_basico">Militante Básico</option>
|
||||
<option value="secretario_celula">Secretário de Célula</option>
|
||||
<option value="membro_setor">Membro de Setor</option>
|
||||
<option value="secretario_setor">Secretário de Setor</option>
|
||||
<option value="membro_cr">Membro de CR</option>
|
||||
<option value="secretario_cr">Secretário de CR</option>
|
||||
<option value="membro_cc">Membro do CC</option>
|
||||
<option value="secretario_geral">Secretário Geral</option>
|
||||
{% elif current_user.has_permission('manage_cr_sectors') %}
|
||||
<option value="membro_cr">Membro de CR</option>
|
||||
<option value="secretario_cr">Secretário de CR</option>
|
||||
{% elif current_user.has_permission('manage_sector_cells') %}
|
||||
<option value="membro_setor">Membro de Setor</option>
|
||||
<option value="secretario_setor">Secretário de Setor</option>
|
||||
{% endif %}
|
||||
</select>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if user.militante %}
|
||||
{% if user.militante.quadro_orientador %}
|
||||
<span class="badge bg-primary">Quadro-Orientador</span>
|
||||
{% endif %}
|
||||
{% if user.militante.aspirante %}
|
||||
<span class="badge bg-warning">Aspirante</span>
|
||||
<small class="text-muted">
|
||||
(desde {{ user.militante.data_inicio_aspirante.strftime('%d/%m/%Y') }})
|
||||
</small>
|
||||
{% if user.militante.avaliacao_aspirante %}
|
||||
<button type="button" class="btn btn-sm btn-info"
|
||||
onclick="verAvaliacaoAspirante({{ user.id }})">
|
||||
Ver Avaliação
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if current_user.has_permission('system_config') or
|
||||
(current_user.has_permission('manage_cr_sectors') and user.cr_id == current_user.cr_id) or
|
||||
(current_user.has_permission('manage_sector_cells') and user.setor_id == current_user.setor_id) %}
|
||||
{% if user.militante.quadro_orientador %}
|
||||
<button type="button" class="btn btn-sm btn-danger"
|
||||
onclick="toggleQuadroOrientador({{ user.id }}, {{ user.militante.quadro_orientador|lower }})">
|
||||
Remover QO
|
||||
</button>
|
||||
{% else %}
|
||||
<button type="button" class="btn btn-sm btn-success"
|
||||
onclick="toggleQuadroOrientador({{ user.id }}, {{ user.militante.quadro_orientador|lower }})">
|
||||
Tornar QO
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if user.militante.aspirante %}
|
||||
{% if datetime.utcnow() - user.militante.data_inicio_aspirante >= timedelta(days=90) %}
|
||||
{% if not user.militante.avaliacao_aspirante %}
|
||||
<button type="button" class="btn btn-sm btn-primary"
|
||||
onclick="avaliarAspirante({{ user.id }})">
|
||||
Avaliar Aspirante
|
||||
</button>
|
||||
{% endif %}
|
||||
<button type="button" class="btn btn-sm btn-danger"
|
||||
onclick="toggleAspirante({{ user.id }}, {{ user.militante.aspirante|lower }})">
|
||||
Remover Aspirante
|
||||
</button>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<button type="button" class="btn btn-sm btn-warning"
|
||||
onclick="toggleAspirante({{ user.id }}, {{ user.militante.aspirante|lower }})">
|
||||
Tornar Aspirante
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group" role="group">
|
||||
{% if current_user.has_permission('system_config') or
|
||||
(current_user.has_permission('manage_cr_sectors') and user.cr_id == current_user.cr_id) or
|
||||
(current_user.has_permission('manage_sector_cells') and user.setor_id == current_user.setor_id) or
|
||||
(current_user.has_permission('manage_cell_members') and user.celula_id == current_user.celula_id) %}
|
||||
<button type="button" class="btn btn-sm btn-primary"
|
||||
onclick="resetOTP({{ user.id }})">
|
||||
Gerar Novo OTP
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-warning"
|
||||
onclick="resetPassword({{ user.id }})">
|
||||
Resetar Senha
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm {% if user.ativo %}btn-danger{% else %}btn-success{% endif %}"
|
||||
onclick="toggleUserStatus({{ user.id }}, {{ user.ativo|lower }})">
|
||||
{% if user.ativo %}Desativar{% else %}Ativar{% endif %} Login
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function resetOTP(userId) {
|
||||
if (confirm('Tem certeza que deseja gerar um novo OTP para este usuário? O OTP atual será invalidado.')) {
|
||||
fetch(`/usuarios/${userId}/otp/reset`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert('Novo OTP gerado com sucesso!');
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Erro ao gerar novo OTP: ' + data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert('Erro ao gerar novo OTP: ' + error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function resetPassword(userId) {
|
||||
if (confirm('Tem certeza que deseja resetar a senha deste usuário? Uma nova senha será gerada e enviada por email.')) {
|
||||
fetch(`/usuarios/${userId}/password/reset`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert('Senha resetada com sucesso! A nova senha foi enviada por email.');
|
||||
} else {
|
||||
alert('Erro ao resetar senha: ' + data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert('Erro ao resetar senha: ' + error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function toggleUserStatus(userId, currentStatus) {
|
||||
const action = currentStatus ? 'desativar' : 'ativar';
|
||||
if (confirm(`Tem certeza que deseja ${action} o login deste usuário?`)) {
|
||||
fetch(`/usuarios/${userId}/toggle_status`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert(`Login ${action}do com sucesso!`);
|
||||
location.reload();
|
||||
} else {
|
||||
alert(`Erro ao ${action} login: ` + data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert(`Erro ao ${action} login: ` + error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function alterarNivel(userId, novoNivel) {
|
||||
if (!novoNivel) return;
|
||||
|
||||
if (confirm('Tem certeza que deseja alterar o nível deste usuário?')) {
|
||||
fetch(`/usuarios/${userId}/alterar_nivel`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ nivel: novoNivel })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert('Nível do usuário alterado com sucesso!');
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Erro ao alterar nível: ' + data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert('Erro ao alterar nível: ' + error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function toggleQuadroOrientador(userId, isQuadroOrientador) {
|
||||
const action = isQuadroOrientador ? 'remover' : 'adicionar';
|
||||
if (confirm(`Tem certeza que deseja ${action} a responsabilidade de Quadro-Orientador deste militante?`)) {
|
||||
fetch(`/usuarios/${userId}/toggle_quadro_orientador`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert(`Responsabilidade de Quadro-Orientador ${action}da com sucesso!`);
|
||||
location.reload();
|
||||
} else {
|
||||
alert(`Erro ao ${action} responsabilidade: ` + data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert(`Erro ao ${action} responsabilidade: ` + error);
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
83
templates/dashboard_admin.html
Normal file
83
templates/dashboard_admin.html
Normal file
@@ -0,0 +1,83 @@
|
||||
{% 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>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Usuário</th>
|
||||
<th>Email</th>
|
||||
<th>Admin</th>
|
||||
<th>OTP Configurado</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>
|
||||
{% if usuario.is_admin %}
|
||||
<span class="badge bg-success">Sim</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">Não</span>
|
||||
{% 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
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
111
templates/editar_instancia.html
Normal file
111
templates/editar_instancia.html
Normal file
@@ -0,0 +1,111 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Editar {{ tipo_instancia }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h1 class="mb-4">Editar {{ tipo_instancia }}</h1>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="POST" class="needs-validation" novalidate>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="nome" class="form-label">Nome</label>
|
||||
<input type="text" class="form-control" id="nome" name="nome" value="{{ instancia.nome }}" required>
|
||||
<div class="invalid-feedback">
|
||||
Por favor, insira o nome da {{ tipo_instancia }}.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if tipo_instancia != 'Célula' %}
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="instancia_superior_id" class="form-label">{{ instancia_superior }}</label>
|
||||
<select class="form-select" id="instancia_superior_id" name="instancia_superior_id" required>
|
||||
<option value="">Selecione uma {{ instancia_superior }}</option>
|
||||
{% for superior in instancias_superiores %}
|
||||
<option value="{{ superior.id }}" {% if superior.id == instancia.instancia_superior_id %}selected{% endif %}>{{ superior.nome }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div class="invalid-feedback">
|
||||
Por favor, selecione uma {{ instancia_superior }}.
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="responsavel_geral_id" class="form-label">Responsável Geral</label>
|
||||
<select class="form-select" id="responsavel_geral_id" name="responsavel_geral_id" required>
|
||||
<option value="">Selecione o responsável geral</option>
|
||||
{% for militante in militantes %}
|
||||
<option value="{{ militante.id }}" {% if militante.id == instancia.responsavel_geral_id %}selected{% endif %}>{{ militante.nome }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div class="invalid-feedback">
|
||||
Por favor, selecione o responsável geral.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="responsavel_financas_id" class="form-label">Responsável de Finanças</label>
|
||||
<select class="form-select" id="responsavel_financas_id" name="responsavel_financas_id">
|
||||
<option value="">Selecione o responsável de finanças</option>
|
||||
{% for militante in militantes %}
|
||||
<option value="{{ militante.id }}" {% if militante.id == instancia.responsavel_financas_id %}selected{% endif %}>{{ militante.nome }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="responsavel_imprensa_id" class="form-label">Responsável de Imprensa</label>
|
||||
<select class="form-select" id="responsavel_imprensa_id" name="responsavel_imprensa_id">
|
||||
<option value="">Selecione o responsável de imprensa</option>
|
||||
{% for militante in militantes %}
|
||||
<option value="{{ militante.id }}" {% if militante.id == instancia.responsavel_imprensa_id %}selected{% endif %}>{{ militante.nome }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between mt-4">
|
||||
<button type="submit" class="btn btn-primary">Salvar</button>
|
||||
<a href="{{ url_for('listar_' + tipo_instancia.lower() + 's') }}" class="btn btn-secondary">Cancelar</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Validação do formulário
|
||||
(function () {
|
||||
'use strict'
|
||||
|
||||
var forms = document.querySelectorAll('.needs-validation')
|
||||
|
||||
Array.prototype.slice.call(forms)
|
||||
.forEach(function (form) {
|
||||
form.addEventListener('submit', function (event) {
|
||||
if (!form.checkValidity()) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
form.classList.add('was-validated')
|
||||
}, false)
|
||||
})
|
||||
})()
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -27,99 +27,107 @@
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="cpf" class="form-label">CPF</label>
|
||||
<input type="text" class="form-control" id="cpf" name="cpf" value="{{ militante.cpf }}" required>
|
||||
<label for="email" class="form-label">Email</label>
|
||||
<input type="email" class="form-control" id="email" name="email" value="{{ militante.email }}" required>
|
||||
<div class="invalid-feedback">
|
||||
Por favor, insira o CPF do militante.
|
||||
Por favor, insira um email válido.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="cpf" class="form-label">CPF</label>
|
||||
<input type="text" class="form-control" id="cpf" name="cpf" value="{{ militante.cpf }}" required>
|
||||
<div class="invalid-feedback">
|
||||
Por favor, insira o CPF do militante.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="titulo_eleitoral" class="form-label">Título Eleitoral</label>
|
||||
<input type="text" class="form-control" id="titulo_eleitoral" name="titulo_eleitoral" value="{{ militante.titulo_eleitoral }}">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="data_nascimento" class="form-label">Data de Nascimento</label>
|
||||
<input type="date" class="form-control" id="data_nascimento" name="data_nascimento" value="{{ militante.data_nascimento.strftime('%Y-%m-%d') }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="data_entrada_oci" class="form-label">Data de Entrada na OCI</label>
|
||||
<input type="date" class="form-control" id="data_entrada_oci" name="data_entrada_oci" value="{{ militante.data_entrada_oci.strftime('%Y-%m-%d') }}">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="data_efetivacao_oci" class="form-label">Data de Efetivação na OCI</label>
|
||||
<input type="date" class="form-control" id="data_efetivacao_oci" name="data_efetivacao_oci" value="{{ militante.data_efetivacao_oci.strftime('%Y-%m-%d') }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="telefone1" class="form-label">Telefone 1</label>
|
||||
<input type="text" class="form-control" id="telefone1" name="telefone1" value="{{ militante.telefone1 }}">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="telefone2" class="form-label">Telefone 2</label>
|
||||
<input type="text" class="form-control" id="telefone2" name="telefone2" value="{{ militante.telefone2 }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="profissao" class="form-label">Profissão</label>
|
||||
<input type="text" class="form-control" id="profissao" name="profissao" value="{{ militante.profissao }}">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="regime_trabalho" class="form-label">Regime de Trabalho</label>
|
||||
<input type="text" class="form-control" id="regime_trabalho" name="regime_trabalho" value="{{ militante.regime_trabalho }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="empresa" class="form-label">Empresa</label>
|
||||
<input type="text" class="form-control" id="empresa" name="empresa" value="{{ militante.empresa }}">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="contratante" class="form-label">Contratante</label>
|
||||
<input type="text" class="form-control" id="contratante" name="contratante" value="{{ militante.contratante }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="instituicao_ensino" class="form-label">Instituição de Ensino</label>
|
||||
<input type="text" class="form-control" id="instituicao_ensino" name="instituicao_ensino" value="{{ militante.instituicao_ensino }}">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="tipo_instituicao" class="form-label">Tipo de Instituição</label>
|
||||
<input type="text" class="form-control" id="tipo_instituicao" name="tipo_instituicao" value="{{ militante.tipo_instituicao }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="sindicato" class="form-label">Sindicato</label>
|
||||
<input type="text" class="form-control" id="sindicato" name="sindicato" value="{{ militante.sindicato }}">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="cargo_sindical" class="form-label">Cargo Sindical</label>
|
||||
<input type="text" class="form-control" id="cargo_sindical" name="cargo_sindical" value="{{ militante.cargo_sindical }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="dirigente_sindical" name="dirigente_sindical" {% if militante.dirigente_sindical %}checked{% endif %}>
|
||||
@@ -128,14 +136,14 @@
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="central_sindical" class="form-label">Central Sindical</label>
|
||||
<input type="text" class="form-control" id="central_sindical" name="central_sindical" value="{{ militante.central_sindical }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="setor_id" class="form-label">Setor</label>
|
||||
<select class="form-select" id="setor_id" name="setor_id" required>
|
||||
@@ -148,7 +156,9 @@
|
||||
Por favor, selecione um setor.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="celula_id" class="form-label">Célula</label>
|
||||
<select class="form-select" id="celula_id" name="celula_id" required>
|
||||
@@ -163,6 +173,30 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 mb-3">
|
||||
<label class="form-label">Responsabilidades</label>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="responsavel_financas" name="responsabilidades" value="{{ Militante.RESPONSAVEL_FINANCAS }}" {% if militante.responsabilidades & Militante.RESPONSAVEL_FINANCAS %}checked{% endif %}>
|
||||
<label class="form-check-label" for="responsavel_financas">
|
||||
Responsável de Finanças
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="responsavel_imprensa" name="responsabilidades" value="{{ Militante.RESPONSAVEL_IMPRENSA }}" {% if militante.responsabilidades & Militante.RESPONSAVEL_IMPRENSA %}checked{% endif %}>
|
||||
<label class="form-check-label" for="responsavel_imprensa">
|
||||
Responsável de Imprensa
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="quadro_orientador" name="responsabilidades" value="{{ Militante.QUADRO_ORIENTADOR }}" {% if militante.responsabilidades & Militante.QUADRO_ORIENTADOR %}checked{% endif %}>
|
||||
<label class="form-check-label" for="quadro_orientador">
|
||||
Quadro-Orientador
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between mt-4">
|
||||
<button type="submit" class="btn btn-primary">Salvar</button>
|
||||
<a href="{{ url_for('listar_militantes') }}" class="btn btn-secondary">Cancelar</a>
|
||||
|
||||
@@ -17,82 +17,16 @@
|
||||
{% endwith %}
|
||||
|
||||
<div class="row">
|
||||
{% for link in links %}
|
||||
<div class="col-md-4">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title">Militantes</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="card-text">Gerencie os militantes da organização.</p>
|
||||
<a href="{{ url_for('listar_militantes') }}" class="btn btn-primary">Listar Militantes</a>
|
||||
<a href="{{ url_for('novo_militante') }}" class="btn btn-success">Novo Militante</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title">Pagamentos</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="card-text">Gerencie os pagamentos dos militantes.</p>
|
||||
<a href="{{ url_for('listar_pagamentos') }}" class="btn btn-primary">Listar Pagamentos</a>
|
||||
<a href="{{ url_for('novo_pagamento') }}" class="btn btn-success">Novo Pagamento</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title">Materiais</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="card-text">Gerencie os materiais da organização.</p>
|
||||
<a href="{{ url_for('listar_materiais') }}" class="btn btn-primary">Listar Materiais</a>
|
||||
<a href="{{ url_for('novo_material') }}" class="btn btn-success">Novo Material</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title">Vendas</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="card-text">Gerencie as vendas de materiais.</p>
|
||||
<a href="{{ url_for('listar_relatorios_vendas') }}" class="btn btn-primary">Listar Vendas</a>
|
||||
<a href="{{ url_for('novo_relatorio_vendas') }}" class="btn btn-success">Nova Venda</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title">Relatórios</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="card-text">Gerencie os relatórios da organização.</p>
|
||||
<a href="{{ url_for('listar_relatorios_cotas') }}" class="btn btn-primary">Relatórios de Cotas</a>
|
||||
<a href="{{ url_for('listar_relatorios_vendas') }}" class="btn btn-primary">Relatórios de Vendas</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title">Configurações</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="card-text">Gerencie as configurações da organização.</p>
|
||||
<a href="{{ url_for('novo_usuario') }}" class="btn btn-primary">Novo Usuário</a>
|
||||
<h5 class="card-title">{{ link.text }}</h5>
|
||||
<a href="{{ link.url }}" class="btn btn-primary">Acessar</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
79
templates/listar_instancias.html
Normal file
79
templates/listar_instancias.html
Normal file
@@ -0,0 +1,79 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Lista de {{ tipo_instancia }}s{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h1 class="mb-4">Lista de {{ tipo_instancia }}s</h1>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<div class="d-flex justify-content-between mb-4">
|
||||
<a href="{{ url_for('criar_' + tipo_instancia.lower()) }}" class="btn btn-primary">Nova {{ tipo_instancia }}</a>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nome</th>
|
||||
{% if tipo_instancia != 'Célula' %}
|
||||
<th>{{ instancia_superior }}</th>
|
||||
{% endif %}
|
||||
<th>Responsável Geral</th>
|
||||
<th>Responsável de Finanças</th>
|
||||
<th>Responsável de Imprensa</th>
|
||||
<th>Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for instancia in instancias %}
|
||||
<tr>
|
||||
<td>{{ instancia.nome }}</td>
|
||||
{% if tipo_instancia != 'Célula' %}
|
||||
<td>{{ instancia.instancia_superior.nome }}</td>
|
||||
{% endif %}
|
||||
<td>{{ instancia.responsavel_geral.nome }}</td>
|
||||
<td>
|
||||
{% if instancia.responsavel_financas %}
|
||||
{{ instancia.responsavel_financas.nome }}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if instancia.responsavel_imprensa %}
|
||||
{{ instancia.responsavel_imprensa.nome }}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ url_for('editar_' + tipo_instancia.lower(), id=instancia.id) }}" class="btn btn-sm btn-warning">Editar</a>
|
||||
<button type="button" class="btn btn-sm btn-danger" onclick="confirmarExclusao({{ instancia.id }})">Excluir</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function confirmarExclusao(id) {
|
||||
if (confirm('Tem certeza que deseja excluir esta {{ tipo_instancia }}?')) {
|
||||
window.location.href = "{{ url_for('excluir_' + tipo_instancia.lower(), id=0) }}".replace('0', id);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Listar Militantes{% endblock %}
|
||||
{% block title %}Lista de Militantes{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
@@ -17,67 +17,42 @@
|
||||
{% endwith %}
|
||||
|
||||
<div class="d-flex justify-content-between mb-4">
|
||||
<a href="{{ url_for('novo_militante') }}" class="btn btn-success">Novo Militante</a>
|
||||
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
|
||||
<a href="{{ url_for('criar_militante') }}" class="btn btn-primary">Novo Militante</a>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Nome</th>
|
||||
<th>CPF</th>
|
||||
<th>Título Eleitoral</th>
|
||||
<th>Data de Nascimento</th>
|
||||
<th>Data de Entrada</th>
|
||||
<th>Data de Efetivação</th>
|
||||
<th>Telefone 1</th>
|
||||
<th>Telefone 2</th>
|
||||
<th>Profissão</th>
|
||||
<th>Regime de Trabalho</th>
|
||||
<th>Empresa</th>
|
||||
<th>Contratante</th>
|
||||
<th>Instituição de Ensino</th>
|
||||
<th>Tipo de Instituição</th>
|
||||
<th>Sindicato</th>
|
||||
<th>Cargo Sindical</th>
|
||||
<th>Dirigente Sindical</th>
|
||||
<th>Central Sindical</th>
|
||||
<th>Setor</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.id }}</td>
|
||||
<td>{{ militante.nome }}</td>
|
||||
<td>{{ militante.cpf }}</td>
|
||||
<td>{{ militante.titulo_eleitoral }}</td>
|
||||
<td>{{ militante.data_nascimento.strftime('%d/%m/%Y') }}</td>
|
||||
<td>{{ militante.data_entrada_oci.strftime('%d/%m/%Y') }}</td>
|
||||
<td>{{ militante.data_efetivacao_oci.strftime('%d/%m/%Y') }}</td>
|
||||
<td>{{ militante.telefone1 }}</td>
|
||||
<td>{{ militante.telefone2 }}</td>
|
||||
<td>{{ militante.profissao }}</td>
|
||||
<td>{{ militante.regime_trabalho }}</td>
|
||||
<td>{{ militante.empresa }}</td>
|
||||
<td>{{ militante.contratante }}</td>
|
||||
<td>{{ militante.instituicao_ensino }}</td>
|
||||
<td>{{ militante.tipo_instituicao }}</td>
|
||||
<td>{{ militante.sindicato }}</td>
|
||||
<td>{{ militante.cargo_sindical }}</td>
|
||||
<td>{{ 'Sim' if militante.dirigente_sindical else 'Não' }}</td>
|
||||
<td>{{ militante.central_sindical }}</td>
|
||||
<td>{{ militante.setor.nome }}</td>
|
||||
<td>{{ militante.celula.nome }}</td>
|
||||
<td>
|
||||
<a href="{{ url_for('editar_militante', id=militante.id) }}" class="btn btn-primary btn-sm">Editar</a>
|
||||
<a href="{{ url_for('deletar_militante', id=militante.id) }}" class="btn btn-danger btn-sm" onclick="return confirm('Tem certeza que deseja excluir este militante?')">Excluir</a>
|
||||
</td>
|
||||
</tr>
|
||||
<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>
|
||||
@@ -85,4 +60,12 @@
|
||||
</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);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -4,43 +4,38 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-6 offset-md-3">
|
||||
<div class="card mt-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="text-center">Login</h3>
|
||||
<h3 class="card-title">Login</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }}">
|
||||
{{ message }}
|
||||
{% if category == 'danger' %}
|
||||
<br>
|
||||
<small>Se o problema persistir, contate o administrador.</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="POST">
|
||||
|
||||
<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
|
||||
value="{{ request.form.username }}">
|
||||
<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" class="form-label">Código OTP</label>
|
||||
<input type="text" class="form-control" id="otp" name="otp" required
|
||||
pattern="[0-9]{6}" title="Digite o código de 6 dígitos">
|
||||
<small class="form-text text-muted">Digite o código de 6 dígitos do seu aplicativo autenticador</small>
|
||||
<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>
|
||||
|
||||
@@ -22,6 +22,12 @@
|
||||
<input type="text" class="form-control" id="nome" name="nome" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">Email:</label>
|
||||
<input type="email" class="form-control" id="email" name="email" required>
|
||||
<small class="form-text text-muted">Este email será usado para login e comunicação do sistema</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="cpf" class="form-label">CPF:</label>
|
||||
<input type="text" class="form-control" id="cpf" name="cpf" required>
|
||||
@@ -115,25 +121,67 @@
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="setor_id" class="form-label">Setor:</label>
|
||||
<select class="form-select" id="setor_id" name="setor_id" required>
|
||||
<option value="">Selecione o setor</option>
|
||||
{% for setor in setores %}
|
||||
<option value="{{ setor.id }}">{{ setor.nome }}</option>
|
||||
<label for="cr_id" class="form-label">Comitê Regional:</label>
|
||||
<select class="form-select" id="cr_id" name="cr_id" required>
|
||||
<option value="">Selecione o CR</option>
|
||||
{% for cr in crs %}
|
||||
<option value="{{ cr.id }}">{{ cr.nome }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="celula_id" class="form-label">Célula:</label>
|
||||
<select class="form-select" id="celula_id" name="celula_id" required>
|
||||
<option value="">Selecione a célula</option>
|
||||
{% for celula in celulas %}
|
||||
<option value="{{ celula.id }}">{{ celula.nome }}</option>
|
||||
<label for="setor_id" class="form-label">Setor:</label>
|
||||
<select class="form-select" id="setor_id" name="setor_id" required>
|
||||
<option value="">Selecione o setor</option>
|
||||
{% for setor in setores %}
|
||||
<option value="{{ setor.id }}" data-cr="{{ setor.cr_id }}">{{ setor.nome }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Célula:</label>
|
||||
{% if pode_criar_celula %}
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="celula_opcao" id="celula_existente" value="existente" checked>
|
||||
<label class="form-check-label" for="celula_existente">
|
||||
Usar célula existente
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="celula_opcao" id="celula_nova" value="nova">
|
||||
<label class="form-check-label" for="celula_nova">
|
||||
Criar nova célula
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div id="celula_existente_container">
|
||||
<select class="form-select" id="celula_id" name="celula_id" required>
|
||||
<option value="">Selecione a célula</option>
|
||||
{% for celula in celulas %}
|
||||
<option value="{{ celula.id }}" data-setor="{{ celula.setor_id }}">{{ celula.nome }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{% if pode_criar_celula %}
|
||||
<div id="celula_nova_container" style="display: none;">
|
||||
<div class="mb-3">
|
||||
<label for="nova_celula_nome" class="form-label">Nome da Nova Célula:</label>
|
||||
<input type="text" class="form-control" id="nova_celula_nome" name="nova_celula_nome">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="nova_celula_quadro_orientador" class="form-label">Quadro Orientador:</label>
|
||||
<input type="text" class="form-control" id="nova_celula_quadro_orientador" name="nova_celula_quadro_orientador">
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">Registrar</button>
|
||||
<a href="{{ url_for('listar_militantes') }}" class="btn btn-secondary">Voltar</a>
|
||||
@@ -143,5 +191,66 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Função para atualizar as células baseado no setor selecionado
|
||||
function atualizarCelulas() {
|
||||
const setorId = document.getElementById('setor_id').value;
|
||||
const celulaSelect = document.getElementById('celula_id');
|
||||
|
||||
// Limpar opções existentes
|
||||
celulaSelect.innerHTML = '<option value="">Selecione a célula</option>';
|
||||
|
||||
// Adicionar apenas células do setor selecionado
|
||||
document.querySelectorAll('#celula_id option').forEach(option => {
|
||||
if (option.dataset.setor === setorId) {
|
||||
celulaSelect.appendChild(option.cloneNode(true));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Função para atualizar os setores baseado no CR selecionado
|
||||
function atualizarSetores() {
|
||||
const crId = document.getElementById('cr_id').value;
|
||||
const setorSelect = document.getElementById('setor_id');
|
||||
|
||||
// Limpar opções existentes
|
||||
setorSelect.innerHTML = '<option value="">Selecione o setor</option>';
|
||||
|
||||
// Adicionar apenas setores do CR selecionado
|
||||
document.querySelectorAll('#setor_id option').forEach(option => {
|
||||
if (option.dataset.cr === crId) {
|
||||
setorSelect.appendChild(option.cloneNode(true));
|
||||
}
|
||||
});
|
||||
|
||||
// Atualizar células após mudar o setor
|
||||
atualizarCelulas();
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
document.getElementById('cr_id').addEventListener('change', atualizarSetores);
|
||||
document.getElementById('setor_id').addEventListener('change', atualizarCelulas);
|
||||
|
||||
// Toggle entre célula existente e nova
|
||||
const celulaExistente = document.getElementById('celula_existente');
|
||||
const celulaNova = document.getElementById('celula_nova');
|
||||
const celulaExistenteContainer = document.getElementById('celula_existente_container');
|
||||
const celulaNovaContainer = document.getElementById('celula_nova_container');
|
||||
|
||||
if (celulaExistente && celulaNova) {
|
||||
celulaExistente.addEventListener('change', function() {
|
||||
celulaExistenteContainer.style.display = 'block';
|
||||
celulaNovaContainer.style.display = 'none';
|
||||
});
|
||||
|
||||
celulaNova.addEventListener('change', function() {
|
||||
celulaExistenteContainer.style.display = 'none';
|
||||
celulaNovaContainer.style.display = 'block';
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Novo Usuário{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-8 offset-md-2">
|
||||
<h1 class="mb-4">Novo Usuário</h1>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="post" class="mb-4">
|
||||
<div class="mb-3">
|
||||
<label for="militante_id" class="form-label">Militante:</label>
|
||||
<select class="form-select" id="militante_id" name="militante_id" required>
|
||||
<option value="">Selecione o militante</option>
|
||||
{% for militante in militantes %}
|
||||
<option value="{{ militante.id }}">{{ militante.nome }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="setor_id" class="form-label">Setor:</label>
|
||||
<select class="form-select" id="setor_id" name="setor_id" required>
|
||||
<option value="">Selecione o setor</option>
|
||||
{% for setor in setores %}
|
||||
<option value="{{ setor.id }}">{{ setor.nome }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="comite_id" class="form-label">Comitê Central:</label>
|
||||
<select class="form-select" id="comite_id" name="comite_id" required>
|
||||
<option value="">Selecione o comitê</option>
|
||||
{% for comite in comites %}
|
||||
<option value="{{ comite.id }}">{{ comite.nome }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Nome de 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="confirm_password" class="form-label">Confirmar Senha:</label>
|
||||
<input type="password" class="form-control" id="confirm_password" name="confirm_password" required>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">Registrar</button>
|
||||
<a href="{{ url_for('listar_usuarios') }}" class="btn btn-secondary">Voltar</a>
|
||||
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user