Browse Source

Feat(Materia/Assinatura): tela "Assinar Despachos em Lote" para Presidente da Mesa

Adiciona pagina onde o Presidente da Mesa Diretora pode visualizar e
assinar em lote, com um unico certificado A1, TODOS os documentos
acessorios do tipo "Despacho" ainda pendentes de assinatura digital
(filtro frouxo: tipo__descricao__icontains "despacho").

Reuso integral do backend ja existente:
- docacessorio_assinar_lote() em views_assinatura.py ja aceita PKs de
  documentos de multiplas materias num unico POST.
- Modal de 4 passos (selecao -> certificado -> previa -> progresso ->
  resumo) extraido de documentoacessorio_list.html para partial
  reusavel e incluido nos dois templates (refactor mecanico, HTML/JS
  byte-a-byte identico).

Acesso: restrito ao grupo SGVP_GROUP_PRESIDENTE_MESA (ou superuser
para depuracao); demais usuarios recebem 403. Atalhos pra tela
aparecem so quando usuario esta no grupo:
- "Pesquisar Materia Legislativa" (botao na linha de filtro de
  assinatura)
- "Materias Pendentes de Assinatura" (botao no bloco de acoes)

Arquivos:
- sapl/materia/views.py: ListView DespachosPendentesLoteView nova +
  flag is_presidente_mesa nos contextos das views existentes.
- sapl/materia/urls.py: rota /materia/despachos-pendentes-lote.
- sapl/templates/materia/assinatura_doc_lote_modal.html (novo): partial
  com modal completo.
- sapl/templates/materia/despachos_pendentes_lote_list.html (novo):
  tela do presidente.
- sapl/templates/materia/documentoacessorio_list.html: substitui modal
  embedded por include do partial.
- sapl/templates/materia/materialegislativa_filter.html: atalho.
- sapl/templates/materia/materias_pendentes_assinatura_list.html: atalho.
pull/3858/head
Gustavo 3 months ago
parent
commit
74996add63
  1. 4
      sapl/materia/urls.py
  2. 90
      sapl/materia/views.py
  3. 420
      sapl/templates/materia/assinatura_doc_lote_modal.html
  4. 136
      sapl/templates/materia/despachos_pendentes_lote_list.html
  5. 405
      sapl/templates/materia/documentoacessorio_list.html
  6. 10
      sapl/templates/materia/materialegislativa_filter.html
  7. 7
      sapl/templates/materia/materias_pendentes_assinatura_list.html

4
sapl/materia/urls.py

@ -18,6 +18,7 @@ from sapl.materia.views import (AcompanhamentoConfirmarView,
MateriaLegislativaCrud,
MateriaLegislativaPesquisaView, MateriaTaView,
MateriasPendentesAssinaturaView,
DespachosPendentesLoteView,
NumeracaoCrud, OrgaoCrud, OrigemCrud,
PrimeiraTramitacaoEmLoteView, ProposicaoCrud,
ProposicaoDevolvida, ProposicaoPendente,
@ -131,6 +132,9 @@ urlpatterns_materia = [
url(r'^materia/pendentes-assinatura$',
MateriasPendentesAssinaturaView.as_view(), name='materias_pendentes_assinatura'),
url(r'^materia/despachos-pendentes-lote$',
DespachosPendentesLoteView.as_view(), name='despachos_pendentes_lote'),
url(r'^materia/(?P<pk>\d+)/acompanhar-materia/$',
AcompanhamentoMateriaView.as_view(), name='acompanhar_materia'),
url(r'^materia/(?P<pk>\d+)/acompanhar-confirmar$',

90
sapl/materia/views.py

@ -685,6 +685,88 @@ class MateriasPendentesAssinaturaView(LoginRequiredMixin, ListView):
reverse('sapl.materia:pesquisar_materia')
+ '?status_assinatura=pendente'
)
# Flag pra mostrar atalho "Assinar Despachos em Lote" só pra
# Presidente da Mesa Diretora (feature dedicada — assina em lote
# todos os DocumentoAcessorio do tipo Despacho ainda pendentes)
from sapl.rules import SGVP_GROUP_PRESIDENTE_MESA
u = self.request.user
context['is_presidente_mesa'] = u.is_authenticated and (
u.is_superuser or
u.groups.filter(name=SGVP_GROUP_PRESIDENTE_MESA).exists()
)
return context
class DespachosPendentesLoteView(LoginRequiredMixin, ListView):
"""
Lista todos os Documentos Acessórios do tipo "Despacho" que ainda
não têm assinatura digital (pdf_assinado vazio), para que o
Presidente da Mesa Diretora possa assiná-los em lote.
Reusa o backend `docacessorio_assinar_lote` (views_assinatura.py),
que aceita PKs de documentos de múltiplas matérias. Reusa também
o modal de assinatura em lote existente em
`documentoacessorio_list.html` (copiado no template desta view).
Acesso restrito ao grupo `Presidente da Mesa Diretora` (ou
superuser, para depuração). Demais usuários recebem 403.
"""
template_name = 'materia/despachos_pendentes_lote_list.html'
model = DocumentoAcessorio
paginate_by = 50
login_url = '/login/'
def _is_presidente(self):
from sapl.rules import SGVP_GROUP_PRESIDENTE_MESA
u = self.request.user
return u.is_superuser or u.groups.filter(
name=SGVP_GROUP_PRESIDENTE_MESA
).exists()
def dispatch(self, request, *args, **kwargs):
# LoginRequiredMixin já trata anônimo
if request.user.is_authenticated and not self._is_presidente():
from django.http import HttpResponseForbidden
return HttpResponseForbidden(
'Acesso restrito ao grupo "Presidente da Mesa Diretora".'
)
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
# Filtro frouxo por nome do tipo — pega "Despacho", "Despacho do
# Presidente", "Despacho Inicial", etc. Decisão de produto.
return DocumentoAcessorio.objects.filter(
tipo__descricao__icontains='despacho'
).filter(
Q(pdf_assinado__isnull=True) | Q(pdf_assinado='')
).select_related('materia', 'materia__tipo', 'tipo').order_by(
'-data', '-id'
)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# `docs_pendentes_lote` no MESMO formato que o modal de
# documentoacessorio_list.html espera (id + descricao). Aqui a
# descrição inclui referência à matéria pra o presidente
# conseguir identificar de qual matéria é o despacho.
docs = list(context['object_list'])
context['docs_pendentes_lote'] = [
{
'id': d.pk,
'descricao': (
f'{d.nome}{d.materia.tipo.sigla} '
f'{d.materia.numero}/{d.materia.ano} '
f'({d.tipo}) — {d.data}'
),
}
for d in docs
]
paginator = context['paginator']
page_obj = context['page_obj']
context['page_range'] = make_pagination(
page_obj.number, paginator.num_pages
)
context['total'] = paginator.count
return context
@ -2846,6 +2928,14 @@ class MateriaLegislativaPesquisaView(MultiFormatOutputMixin, FilterView):
else:
context['materias_pendentes_lote'] = []
# Flag pra mostrar atalho "Assinar Despachos em Lote" só pra
# Presidente da Mesa Diretora (vê DespachosPendentesLoteView)
from sapl.rules import SGVP_GROUP_PRESIDENTE_MESA
u = self.request.user
context['is_presidente_mesa'] = u.is_authenticated and (
u.is_superuser or
u.groups.filter(name=SGVP_GROUP_PRESIDENTE_MESA).exists()
)
return context

420
sapl/templates/materia/assinatura_doc_lote_modal.html

@ -0,0 +1,420 @@
{% comment %}
Partial reusavel: Modal de Assinatura Digital em Lote de Documentos
Acessorios. Espera no contexto a variavel `docs_pendentes_lote`
(lista de dicts com chaves `id` e `descricao`). O caller deve
envolver com if docs_pendentes_lote include endif e renderizar em
algum lugar um botao com id="btn-assinar-doc-lote" para disparar o
modal.
Backend: POST para sapl.materia:docacessorio_assinar_lote
(views_assinatura.py::docacessorio_assinar_lote).
Usado por:
- sapl/templates/materia/documentoacessorio_list.html
(assinar docs acessorios pendentes de UMA materia)
- sapl/templates/materia/despachos_pendentes_lote_list.html
(assinar despachos pendentes de TODAS as materias - Presidente da Mesa)
{% endcomment %}
<div class="modal fade" id="assinaturaDocLoteModal" tabindex="-1" role="dialog" aria-labelledby="assinaturaDocLoteModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header bg-warning">
<h5 class="modal-title" id="assinaturaDocLoteModalLabel">
<i class="fa fa-certificate"></i>
Assinar Documentos Acessórios em Lote &mdash;
<strong id="doc-lote-titulo-total">{{ docs_pendentes_lote|length }}</strong> pendente(s)
</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Fechar">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
{# Passo 0: seleção #}
<div id="doc-lote-selecao-step">
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="font-weight-bold">
<i class="fa fa-list-ul"></i> Selecione os documentos a assinar
</span>
<div>
<button type="button" class="btn btn-sm btn-outline-secondary mr-1" id="doc-lote-btn-todas">
<i class="fa fa-check-square-o"></i> Todos
</button>
<button type="button" class="btn btn-sm btn-outline-secondary" id="doc-lote-btn-nenhuma">
<i class="fa fa-square-o"></i> Nenhum
</button>
</div>
</div>
<div id="doc-lote-lista-checkboxes" style="max-height:320px;overflow-y:auto;border:1px solid #dee2e6;border-radius:4px;padding:8px;">
{% for d in docs_pendentes_lote %}
<div class="form-check py-1" style="border-bottom:1px solid #f0f0f0;">
<input class="form-check-input doc-lote-chk" type="checkbox"
id="doc-lote-chk-{{ d.id }}" value="{{ d.id }}" checked>
<label class="form-check-label w-100" for="doc-lote-chk-{{ d.id }}" style="cursor:pointer;">
{{ d.descricao }}
</label>
</div>
{% endfor %}
</div>
<p class="text-muted small mt-2 mb-0">
<span id="doc-lote-selecao-contagem">{{ docs_pendentes_lote|length }}</span> documento(s) selecionado(s)
</p>
</div>
{# Passo 1: certificado #}
<div id="doc-lote-form-step" style="display:none;">
<div class="alert alert-warning">
<i class="fa fa-exclamation-triangle"></i>
<strong>Atenção:</strong> Esta operação assinará digitalmente
<strong><span id="doc-lote-alerta-total">{{ docs_pendentes_lote|length }}</span> documento(s)</strong>
com validade jurídica nos termos da MP 2.200-2/2001.
</div>
<form id="form-assinatura-doc-lote" enctype="multipart/form-data">
{% csrf_token %}
<div class="form-group">
<label for="doc-lote-certificado">
<i class="fa fa-file"></i> Certificado Digital (.pfx / .p12)
</label>
<div class="custom-file">
<input type="file" class="custom-file-input" id="doc-lote-certificado"
name="certificado" accept=".pfx,.p12" required>
<label class="custom-file-label" for="doc-lote-certificado">
Selecione o arquivo do certificado...
</label>
</div>
</div>
<div class="form-group">
<label for="doc-lote-senha">
<i class="fa fa-lock"></i> Senha do Certificado
</label>
<input type="password" class="form-control" id="doc-lote-senha"
name="senha" placeholder="Digite a senha do certificado" required>
</div>
</form>
</div>
{# Passo 1.5: prévia #}
<div id="doc-lote-previa-step" style="display:none;">
<div class="alert alert-info mb-3">
<i class="fa fa-eye"></i>
<strong>Revise os documentos antes de assinar.</strong>
Clique em cada item para visualizar o PDF em nova aba.
</div>
<div id="doc-lote-previa-lista" style="max-height:300px;overflow-y:auto;border:1px solid #dee2e6;border-radius:4px;padding:8px;" class="mb-3"></div>
<div class="form-check mt-3">
<input class="form-check-input" type="checkbox" id="doc-lote-previa-confirmacao">
<label class="form-check-label font-weight-bold text-danger" for="doc-lote-previa-confirmacao">
<i class="fa fa-check-square-o"></i>
Confirmo que revisei os documentos e que estão corretos para assinatura.
</label>
</div>
</div>
{# Passo 2: progresso #}
<div id="doc-lote-progresso-step" style="display:none;">
<h6 class="mb-3">
<i class="fa fa-spinner fa-spin text-primary"></i>
Processando assinaturas&hellip;
</h6>
<div class="progress mb-3" style="height:22px;">
<div id="doc-lote-progress-bar"
class="progress-bar progress-bar-striped progress-bar-animated bg-warning"
role="progressbar" style="width:0%">0%</div>
</div>
<p class="text-muted small" id="doc-lote-status-texto">Iniciando&hellip;</p>
</div>
{# Passo 3: resumo #}
<div id="doc-lote-resumo-step" style="display:none;">
<div id="doc-lote-resumo-alerta"></div>
<div id="doc-lote-resumo-lista" style="max-height:300px;overflow-y:auto;"></div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal" id="doc-lote-btn-fechar">
<i class="fa fa-times"></i> Fechar
</button>
<button type="button" class="btn btn-warning" id="doc-lote-btn-proximo">
<i class="fa fa-arrow-right"></i>
Continuar &mdash; <span id="doc-lote-btn-proximo-contagem">{{ docs_pendentes_lote|length }}</span> Doc(s)
</button>
<button type="button" class="btn btn-outline-secondary" id="doc-lote-btn-voltar" style="display:none;">
<i class="fa fa-arrow-left"></i> Voltar
</button>
<button type="button" class="btn btn-info" id="doc-lote-btn-previa" style="display:none;">
<i class="fa fa-eye"></i> Visualizar Documentos
</button>
<button type="button" class="btn btn-outline-secondary" id="doc-lote-btn-voltar-form" style="display:none;">
<i class="fa fa-arrow-left"></i> Voltar
</button>
<button type="button" class="btn btn-warning" id="doc-lote-btn-assinar" style="display:none;" disabled>
<i class="fa fa-certificate"></i>
Assinar <span id="doc-lote-btn-assinar-contagem">{{ docs_pendentes_lote|length }}</span> Doc(s)
</button>
<button type="button" class="btn btn-success" id="doc-lote-btn-recarregar" style="display:none;"
onclick="location.reload()">
<i class="fa fa-refresh"></i> Atualizar página
</button>
</div>
</div>
</div>
</div>
<script>
(function () {
var DOCS_LOTE = {{ docs_pendentes_lote|safe }};
var URL_LOTE = "{% url 'sapl.materia:docacessorio_assinar_lote' %}";
function getCSRF() {
var el = document.querySelector('#form-assinatura-doc-lote [name=csrfmiddlewaretoken]');
if (el) return el.value;
var match = document.cookie.match(/csrftoken=([^;]+)/);
return match ? match[1] : '';
}
function getIdsSelecionados() {
return Array.from(document.querySelectorAll('.doc-lote-chk:checked'))
.map(function(c){ return parseInt(c.value, 10); });
}
function atualizarContagem() {
var n = getIdsSelecionados().length;
document.getElementById('doc-lote-selecao-contagem').textContent = n;
document.getElementById('doc-lote-btn-proximo-contagem').textContent = n;
document.getElementById('doc-lote-btn-assinar-contagem').textContent = n;
document.getElementById('doc-lote-alerta-total').textContent = n;
document.getElementById('doc-lote-titulo-total').textContent = n;
document.getElementById('doc-lote-btn-proximo').disabled = (n === 0);
}
document.getElementById('doc-lote-lista-checkboxes').addEventListener('change', function(e){
if (e.target && e.target.classList.contains('doc-lote-chk')) atualizarContagem();
});
document.getElementById('doc-lote-btn-todas').addEventListener('click', function(){
document.querySelectorAll('.doc-lote-chk').forEach(function(c){ c.checked = true; });
atualizarContagem();
});
document.getElementById('doc-lote-btn-nenhuma').addEventListener('click', function(){
document.querySelectorAll('.doc-lote-chk').forEach(function(c){ c.checked = false; });
atualizarContagem();
});
document.getElementById('doc-lote-certificado').addEventListener('change', function(){
var label = this.nextElementSibling;
label.textContent = this.files[0] ? this.files[0].name : 'Selecione o arquivo do certificado...';
});
function irParaSelecao() {
document.getElementById('doc-lote-selecao-step').style.display = 'block';
document.getElementById('doc-lote-form-step').style.display = 'none';
document.getElementById('doc-lote-previa-step').style.display = 'none';
document.getElementById('doc-lote-progresso-step').style.display = 'none';
document.getElementById('doc-lote-resumo-step').style.display = 'none';
document.getElementById('doc-lote-btn-proximo').style.display = 'inline-block';
document.getElementById('doc-lote-btn-voltar').style.display = 'none';
document.getElementById('doc-lote-btn-previa').style.display = 'none';
document.getElementById('doc-lote-btn-voltar-form').style.display= 'none';
document.getElementById('doc-lote-btn-assinar').style.display = 'none';
document.getElementById('doc-lote-btn-recarregar').style.display = 'none';
document.getElementById('doc-lote-btn-fechar').disabled = false;
}
function irParaForm() {
document.getElementById('doc-lote-selecao-step').style.display = 'none';
document.getElementById('doc-lote-form-step').style.display = 'block';
document.getElementById('doc-lote-previa-step').style.display = 'none';
document.getElementById('doc-lote-progresso-step').style.display = 'none';
document.getElementById('doc-lote-resumo-step').style.display = 'none';
document.getElementById('doc-lote-btn-proximo').style.display = 'none';
document.getElementById('doc-lote-btn-voltar').style.display = 'inline-block';
document.getElementById('doc-lote-btn-previa').style.display = 'inline-block';
document.getElementById('doc-lote-btn-voltar-form').style.display= 'none';
document.getElementById('doc-lote-btn-assinar').style.display = 'none';
document.getElementById('doc-lote-btn-recarregar').style.display = 'none';
}
function irParaPrevia() {
var certFile = document.getElementById('doc-lote-certificado').files[0];
var senha = document.getElementById('doc-lote-senha').value;
if (!certFile) { alert('Selecione o arquivo do certificado.'); return; }
if (!senha) { alert('Digite a senha do certificado.'); return; }
var idsSel = getIdsSelecionados();
var html = '';
DOCS_LOTE.filter(function(d){ return idsSel.indexOf(d.id) !== -1; })
.forEach(function(d) {
html += '<div class="d-flex align-items-center justify-content-between py-2" ' +
'style="border-bottom:1px solid #f0f0f0;">' +
'<span><i class="fa fa-file-pdf-o text-danger mr-1"></i>' +
'<strong>' + escHtml(d.descricao) + '</strong></span>' +
'<a href="/materia/documentoacessorio/' + d.id + '/pdf-previa" target="_blank" ' +
'class="btn btn-sm btn-outline-primary ml-2" title="Visualizar PDF">' +
'<i class="fa fa-eye"></i> Visualizar</a>' +
'</div>';
});
document.getElementById('doc-lote-previa-lista').innerHTML = html || '<p class="text-muted">Nenhum documento selecionado.</p>';
var chk = document.getElementById('doc-lote-previa-confirmacao');
chk.checked = false;
document.getElementById('doc-lote-btn-assinar').disabled = true;
document.getElementById('doc-lote-selecao-step').style.display = 'none';
document.getElementById('doc-lote-form-step').style.display = 'none';
document.getElementById('doc-lote-previa-step').style.display = 'block';
document.getElementById('doc-lote-progresso-step').style.display = 'none';
document.getElementById('doc-lote-resumo-step').style.display = 'none';
document.getElementById('doc-lote-btn-proximo').style.display = 'none';
document.getElementById('doc-lote-btn-voltar').style.display = 'none';
document.getElementById('doc-lote-btn-previa').style.display = 'none';
document.getElementById('doc-lote-btn-voltar-form').style.display= 'inline-block';
document.getElementById('doc-lote-btn-assinar').style.display = 'inline-block';
document.getElementById('doc-lote-btn-recarregar').style.display = 'none';
}
document.getElementById('btn-assinar-doc-lote').addEventListener('click', function(){
document.querySelectorAll('.doc-lote-chk').forEach(function(c){ c.checked = true; });
document.getElementById('form-assinatura-doc-lote').reset();
document.querySelector('#form-assinatura-doc-lote .custom-file-label').textContent =
'Selecione o arquivo do certificado...';
atualizarContagem();
irParaSelecao();
$('#assinaturaDocLoteModal').modal('show');
});
document.getElementById('doc-lote-btn-proximo').addEventListener('click', function(){
if (getIdsSelecionados().length === 0) { alert('Selecione ao menos um documento.'); return; }
irParaForm();
});
document.getElementById('doc-lote-btn-voltar').addEventListener('click', irParaSelecao);
document.getElementById('doc-lote-btn-previa').addEventListener('click', irParaPrevia);
document.getElementById('doc-lote-btn-voltar-form').addEventListener('click', irParaForm);
document.getElementById('doc-lote-previa-confirmacao').addEventListener('change', function(){
document.getElementById('doc-lote-btn-assinar').disabled = !this.checked;
});
document.getElementById('doc-lote-btn-assinar').addEventListener('click', function(){
var certFile = document.getElementById('doc-lote-certificado').files[0];
var senha = document.getElementById('doc-lote-senha').value;
var idsSel = getIdsSelecionados();
if (!certFile) { alert('Selecione o arquivo do certificado.'); return; }
if (!senha) { alert('Digite a senha do certificado.'); return; }
if (!idsSel.length) { alert('Nenhum documento selecionado.'); return; }
var fd = new FormData();
fd.append('certificado', certFile);
fd.append('senha', senha);
fd.append('ids', JSON.stringify(idsSel));
document.getElementById('doc-lote-previa-step').style.display = 'none';
document.getElementById('doc-lote-progresso-step').style.display = 'block';
document.getElementById('doc-lote-btn-voltar-form').style.display= 'none';
document.getElementById('doc-lote-btn-assinar').style.display = 'none';
document.getElementById('doc-lote-btn-fechar').disabled = true;
// Animação de progresso proporcional ao número de documentos
var nDocs = idsSel.length;
var fases = [
{ ate: 10, label: 'Enviando certificado e iniciando assinatura…', ms: 700 },
{ ate: 25, label: 'Validando certificado e gerando PDFs…', ms: Math.min(600, 200 + nDocs * 30) },
{ ate: 50, label: 'Preparando ' + nDocs + ' documento(s) para assinatura…', ms: Math.min(800, 200 + nDocs * 50) },
{ ate: 70, label: 'Enviando ao microserviço de assinatura…', ms: Math.min(1000, 300 + nDocs * 60) },
{ ate: 85, label: 'Aguardando resposta do servidor…', ms: Math.min(800, 300 + nDocs * 40) },
{ ate: 93, label: 'Salvando documentos assinados…', ms: 500 },
{ ate: 97, label: 'Finalizando…', ms: 300 },
];
var faseIdx = 0, progrAtual = 0, progrTimer = null;
function avancarProgresso() {
if (faseIdx >= fases.length) return;
var fase = fases[faseIdx];
if (progrAtual < fase.ate) {
progrAtual = Math.min(progrAtual + 1, fase.ate);
setProgresso(progrAtual, fase.label);
} else { faseIdx++; }
progrTimer = setTimeout(avancarProgresso, fases[Math.min(faseIdx, fases.length-1)].ms / (fase.ate - (faseIdx > 0 ? fases[faseIdx-1].ate : 0)));
}
progrTimer = setTimeout(avancarProgresso, 400);
fetch(URL_LOTE, {
method: 'POST',
body: fd,
headers: { 'X-CSRFToken': getCSRF() }
})
.then(function(r){
clearTimeout(progrTimer);
setProgresso(98, 'Processando resposta…');
if (!r.ok) return r.json().then(function(d){ throw new Error(d.error || ('HTTP ' + r.status)); });
return r.json();
})
.then(function(data){
clearTimeout(progrTimer);
setProgresso(100, 'Concluído.');
document.getElementById('doc-lote-btn-fechar').disabled = false;
mostrarResumo(data);
})
.catch(function(err){
clearTimeout(progrTimer);
document.getElementById('doc-lote-btn-fechar').disabled = false;
setProgresso(100, 'Erro.');
mostrarErroFatal(err.message || String(err));
});
});
function setProgresso(pct, texto) {
var bar = document.getElementById('doc-lote-progress-bar');
bar.style.width = pct + '%';
bar.textContent = pct + '%';
document.getElementById('doc-lote-status-texto').textContent = texto;
}
function mostrarResumo(data) {
document.getElementById('doc-lote-progresso-step').style.display = 'none';
document.getElementById('doc-lote-resumo-step').style.display = 'block';
document.getElementById('doc-lote-btn-recarregar').style.display = 'inline-block';
var temErro = data.erros > 0;
var alertaCls = data.sucesso > 0 ? (temErro ? 'alert-warning' : 'alert-success') : 'alert-danger';
var icone = data.sucesso > 0 ? (temErro ? 'exclamation-triangle' : 'check-circle') : 'times-circle';
document.getElementById('doc-lote-resumo-alerta').innerHTML =
'<div class="alert ' + alertaCls + '">' +
'<i class="fa fa-' + icone + '"></i> ' +
'<strong>' + data.sucesso + ' assinado(s)</strong> com sucesso' +
(temErro ? ', <strong>' + data.erros + '</strong> com erro(s).' : '.') +
' Total: ' + data.total + ' documento(s).</div>';
var html = '<ul class="list-group">';
(data.resultados || []).forEach(function(r){
var cls = r.success ? 'list-group-item-success' : 'list-group-item-danger';
var icon = r.success ? 'check text-success' : 'times text-danger';
html += '<li class="list-group-item list-group-item-sm ' + cls + '">' +
'<i class="fa fa-' + icon + ' mr-1"></i>' +
'<strong>' + escHtml(r.descricao) + '</strong>' +
(r.error ? ' — <small class="text-muted">' + escHtml(r.error) + '</small>' : '') +
'</li>';
});
html += '</ul>';
document.getElementById('doc-lote-resumo-lista').innerHTML = html;
}
function mostrarErroFatal(msg) {
document.getElementById('doc-lote-progresso-step').style.display = 'none';
document.getElementById('doc-lote-resumo-step').style.display = 'block';
document.getElementById('doc-lote-resumo-alerta').innerHTML =
'<div class="alert alert-danger"><i class="fa fa-times-circle"></i> ' +
'<strong>Erro:</strong> ' + escHtml(msg) + '</div>';
document.getElementById('doc-lote-resumo-lista').innerHTML = '';
}
function escHtml(s) {
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
})();
</script>

136
sapl/templates/materia/despachos_pendentes_lote_list.html

@ -0,0 +1,136 @@
{% extends "crud/detail.html" %}
{% load i18n %}
{% load static %}
{% block actions %}
<div class="actions btn-group float-right pb-4" role="group">
<a href="{% url 'sapl.materia:materias_pendentes_assinatura' %}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left"></i> {% trans "Voltar para Minhas Matérias" %}
</a>
</div>
{% endblock %}
{% block detail_content %}
<div class="d-flex align-items-center justify-content-between mb-3 flex-wrap" style="gap:.75rem;">
<div>
<h4 class="mb-0">
<span class="badge badge-warning text-dark px-3 py-2" style="font-size:1rem;">
<i class="fas fa-stamp mr-1"></i> {% trans "Despachos Pendentes de Assinatura" %}
</span>
</h4>
<small class="text-muted mt-1 d-block">
<i class="fas fa-user-tie mr-1"></i>
{% trans "Acesso exclusivo do" %} <strong>{% trans "Presidente da Mesa Diretora" %}</strong>
</small>
</div>
<div>
{% if total == 0 %}
<span class="badge badge-success px-3 py-2" style="font-size:.95rem;">
<i class="fas fa-check-circle mr-1"></i> {% trans "Nenhum despacho pendente" %}
</span>
{% elif total == 1 %}
<span class="badge badge-warning text-dark px-3 py-2" style="font-size:.95rem;">
1 {% trans "despacho pendente" %}
</span>
{% else %}
<span class="badge badge-warning text-dark px-3 py-2" style="font-size:.95rem;">
{{ total }} {% trans "despachos pendentes" %}
</span>
{% endif %}
</div>
</div>
{% if not object_list %}
<div class="alert alert-success d-flex align-items-center" role="alert">
<i class="fas fa-check-circle fa-2x mr-3"></i>
<div>
<strong>{% trans "Tudo em dia!" %}</strong><br>
{% trans "Não há despachos pendentes de assinatura digital no sistema." %}
</div>
</div>
{% else %}
<div class="alert alert-warning d-flex justify-content-between align-items-center mb-3 flex-wrap"
role="alert" style="border-left: 5px solid #f0ad4e; gap:1rem;">
<div>
<i class="fas fa-exclamation-triangle fa-lg mr-2"></i>
{% trans "Os despachos abaixo aguardam assinatura digital. Você pode assiná-los todos em lote com seu certificado digital." %}
</div>
<button type="button" id="btn-assinar-doc-lote"
class="btn btn-warning"
title="{% trans 'Assinar digitalmente todos os despachos selecionados em lote' %}">
<i class="fa fa-certificate"></i>
{% trans "Assinar Despachos em Lote" %}
<span class="badge badge-light text-dark ml-1">{{ docs_pendentes_lote|length }}</span>
</button>
</div>
<table class="table table-hover table-bordered">
<thead class="thead-light">
<tr>
<th style="width:170px;">{% trans "Matéria" %}</th>
<th>{% trans "Nome do Despacho" %}</th>
<th style="width:140px;">{% trans "Tipo" %}</th>
<th style="width:140px;">{% trans "Autor" %}</th>
<th style="width:110px;" class="text-center">{% trans "Data" %}</th>
<th style="width:90px;" class="text-center">{% trans "Ações" %}</th>
</tr>
</thead>
<tbody>
{% for d in object_list %}
<tr>
<td>
<a href="{% url 'sapl.materia:materialegislativa_detail' d.materia.pk %}"
class="font-weight-bold"
title="{% trans 'Abrir matéria' %}">
{{ d.materia.tipo.sigla }} {{ d.materia.numero }}/{{ d.materia.ano }}
</a>
</td>
<td>
<a href="{% url 'sapl.materia:documentoacessorio_detail' d.pk %}"
title="{% trans 'Abrir documento' %}">
{{ d.nome }}
</a>
</td>
<td>
<small class="text-muted">{{ d.tipo }}</small>
</td>
<td>
{% if d.autor %}
{{ d.autor }}
{% else %}
<span class="text-muted"></span>
{% endif %}
</td>
<td class="text-center">
{% if d.data %}
{{ d.data|date:"d/m/Y" }}
{% else %}
<span class="text-muted"></span>
{% endif %}
</td>
<td class="text-center">
{% if d.arquivo %}
<a href="{{ d.arquivo.url }}" target="_blank"
class="btn btn-sm btn-outline-primary"
title="{% trans 'Baixar arquivo original' %}">
<i class="fas fa-download"></i>
</a>
{% else %}
<span class="text-muted"></span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% include "paginacao.html" %}
{# Modal de assinatura em lote (partial reusado de documentoacessorio_list.html) #}
{% include "materia/assinatura_doc_lote_modal.html" %}
{% endif %}
{% endblock detail_content %}

405
sapl/templates/materia/documentoacessorio_list.html

@ -182,409 +182,6 @@
{% endif %}
{% if docs_pendentes_lote %}
{# ── Modal de Assinatura em Lote – Documentos Acessórios ─────────────── #}
<div class="modal fade" id="assinaturaDocLoteModal" tabindex="-1" role="dialog" aria-labelledby="assinaturaDocLoteModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header bg-warning">
<h5 class="modal-title" id="assinaturaDocLoteModalLabel">
<i class="fa fa-certificate"></i>
Assinar Documentos Acessórios em Lote —
<strong id="doc-lote-titulo-total">{{ docs_pendentes_lote|length }}</strong> pendente(s)
</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Fechar">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
{# Passo 0: seleção #}
<div id="doc-lote-selecao-step">
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="font-weight-bold">
<i class="fa fa-list-ul"></i> Selecione os documentos a assinar
</span>
<div>
<button type="button" class="btn btn-sm btn-outline-secondary mr-1" id="doc-lote-btn-todas">
<i class="fa fa-check-square-o"></i> Todos
</button>
<button type="button" class="btn btn-sm btn-outline-secondary" id="doc-lote-btn-nenhuma">
<i class="fa fa-square-o"></i> Nenhum
</button>
</div>
</div>
<div id="doc-lote-lista-checkboxes" style="max-height:320px;overflow-y:auto;border:1px solid #dee2e6;border-radius:4px;padding:8px;">
{% for d in docs_pendentes_lote %}
<div class="form-check py-1" style="border-bottom:1px solid #f0f0f0;">
<input class="form-check-input doc-lote-chk" type="checkbox"
id="doc-lote-chk-{{ d.id }}" value="{{ d.id }}" checked>
<label class="form-check-label w-100" for="doc-lote-chk-{{ d.id }}" style="cursor:pointer;">
{{ d.descricao }}
</label>
</div>
{% endfor %}
</div>
<p class="text-muted small mt-2 mb-0">
<span id="doc-lote-selecao-contagem">{{ docs_pendentes_lote|length }}</span> documento(s) selecionado(s)
</p>
</div>
{# Passo 1: certificado #}
<div id="doc-lote-form-step" style="display:none;">
<div class="alert alert-warning">
<i class="fa fa-exclamation-triangle"></i>
<strong>Atenção:</strong> Esta operação assinará digitalmente
<strong><span id="doc-lote-alerta-total">{{ docs_pendentes_lote|length }}</span> documento(s)</strong>
com validade jurídica nos termos da MP 2.200-2/2001.
</div>
<form id="form-assinatura-doc-lote" enctype="multipart/form-data">
{% csrf_token %}
<div class="form-group">
<label for="doc-lote-certificado">
<i class="fa fa-file"></i> Certificado Digital (.pfx / .p12)
</label>
<div class="custom-file">
<input type="file" class="custom-file-input" id="doc-lote-certificado"
name="certificado" accept=".pfx,.p12" required>
<label class="custom-file-label" for="doc-lote-certificado">
Selecione o arquivo do certificado...
</label>
</div>
</div>
<div class="form-group">
<label for="doc-lote-senha">
<i class="fa fa-lock"></i> Senha do Certificado
</label>
<input type="password" class="form-control" id="doc-lote-senha"
name="senha" placeholder="Digite a senha do certificado" required>
</div>
</form>
</div>
{# Passo 1.5: prévia #}
<div id="doc-lote-previa-step" style="display:none;">
<div class="alert alert-info mb-3">
<i class="fa fa-eye"></i>
<strong>Revise os documentos antes de assinar.</strong>
Clique em cada item para visualizar o PDF em nova aba.
</div>
<div id="doc-lote-previa-lista" style="max-height:300px;overflow-y:auto;border:1px solid #dee2e6;border-radius:4px;padding:8px;" class="mb-3"></div>
<div class="form-check mt-3">
<input class="form-check-input" type="checkbox" id="doc-lote-previa-confirmacao">
<label class="form-check-label font-weight-bold text-danger" for="doc-lote-previa-confirmacao">
<i class="fa fa-check-square-o"></i>
Confirmo que revisei os documentos e que estão corretos para assinatura.
</label>
</div>
</div>
{# Passo 2: progresso #}
<div id="doc-lote-progresso-step" style="display:none;">
<h6 class="mb-3">
<i class="fa fa-spinner fa-spin text-primary"></i>
Processando assinaturas…
</h6>
<div class="progress mb-3" style="height:22px;">
<div id="doc-lote-progress-bar"
class="progress-bar progress-bar-striped progress-bar-animated bg-warning"
role="progressbar" style="width:0%">0%</div>
</div>
<p class="text-muted small" id="doc-lote-status-texto">Iniciando…</p>
</div>
{# Passo 3: resumo #}
<div id="doc-lote-resumo-step" style="display:none;">
<div id="doc-lote-resumo-alerta"></div>
<div id="doc-lote-resumo-lista" style="max-height:300px;overflow-y:auto;"></div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal" id="doc-lote-btn-fechar">
<i class="fa fa-times"></i> Fechar
</button>
<button type="button" class="btn btn-warning" id="doc-lote-btn-proximo">
<i class="fa fa-arrow-right"></i>
Continuar — <span id="doc-lote-btn-proximo-contagem">{{ docs_pendentes_lote|length }}</span> Doc(s)
</button>
<button type="button" class="btn btn-outline-secondary" id="doc-lote-btn-voltar" style="display:none;">
<i class="fa fa-arrow-left"></i> Voltar
</button>
<button type="button" class="btn btn-info" id="doc-lote-btn-previa" style="display:none;">
<i class="fa fa-eye"></i> Visualizar Documentos
</button>
<button type="button" class="btn btn-outline-secondary" id="doc-lote-btn-voltar-form" style="display:none;">
<i class="fa fa-arrow-left"></i> Voltar
</button>
<button type="button" class="btn btn-warning" id="doc-lote-btn-assinar" style="display:none;" disabled>
<i class="fa fa-certificate"></i>
Assinar <span id="doc-lote-btn-assinar-contagem">{{ docs_pendentes_lote|length }}</span> Doc(s)
</button>
<button type="button" class="btn btn-success" id="doc-lote-btn-recarregar" style="display:none;"
onclick="location.reload()">
<i class="fa fa-refresh"></i> Atualizar página
</button>
</div>
</div>
</div>
</div>
<script>
(function () {
var DOCS_LOTE = {{ docs_pendentes_lote|safe }};
var URL_LOTE = "{% url 'sapl.materia:docacessorio_assinar_lote' %}";
function getCSRF() {
var el = document.querySelector('#form-assinatura-doc-lote [name=csrfmiddlewaretoken]');
if (el) return el.value;
var match = document.cookie.match(/csrftoken=([^;]+)/);
return match ? match[1] : '';
}
function getIdsSelecionados() {
return Array.from(document.querySelectorAll('.doc-lote-chk:checked'))
.map(function(c){ return parseInt(c.value, 10); });
}
function atualizarContagem() {
var n = getIdsSelecionados().length;
document.getElementById('doc-lote-selecao-contagem').textContent = n;
document.getElementById('doc-lote-btn-proximo-contagem').textContent = n;
document.getElementById('doc-lote-btn-assinar-contagem').textContent = n;
document.getElementById('doc-lote-alerta-total').textContent = n;
document.getElementById('doc-lote-titulo-total').textContent = n;
document.getElementById('doc-lote-btn-proximo').disabled = (n === 0);
}
document.getElementById('doc-lote-lista-checkboxes').addEventListener('change', function(e){
if (e.target && e.target.classList.contains('doc-lote-chk')) atualizarContagem();
});
document.getElementById('doc-lote-btn-todas').addEventListener('click', function(){
document.querySelectorAll('.doc-lote-chk').forEach(function(c){ c.checked = true; });
atualizarContagem();
});
document.getElementById('doc-lote-btn-nenhuma').addEventListener('click', function(){
document.querySelectorAll('.doc-lote-chk').forEach(function(c){ c.checked = false; });
atualizarContagem();
});
document.getElementById('doc-lote-certificado').addEventListener('change', function(){
var label = this.nextElementSibling;
label.textContent = this.files[0] ? this.files[0].name : 'Selecione o arquivo do certificado...';
});
function irParaSelecao() {
document.getElementById('doc-lote-selecao-step').style.display = 'block';
document.getElementById('doc-lote-form-step').style.display = 'none';
document.getElementById('doc-lote-previa-step').style.display = 'none';
document.getElementById('doc-lote-progresso-step').style.display = 'none';
document.getElementById('doc-lote-resumo-step').style.display = 'none';
document.getElementById('doc-lote-btn-proximo').style.display = 'inline-block';
document.getElementById('doc-lote-btn-voltar').style.display = 'none';
document.getElementById('doc-lote-btn-previa').style.display = 'none';
document.getElementById('doc-lote-btn-voltar-form').style.display= 'none';
document.getElementById('doc-lote-btn-assinar').style.display = 'none';
document.getElementById('doc-lote-btn-recarregar').style.display = 'none';
document.getElementById('doc-lote-btn-fechar').disabled = false;
}
function irParaForm() {
document.getElementById('doc-lote-selecao-step').style.display = 'none';
document.getElementById('doc-lote-form-step').style.display = 'block';
document.getElementById('doc-lote-previa-step').style.display = 'none';
document.getElementById('doc-lote-progresso-step').style.display = 'none';
document.getElementById('doc-lote-resumo-step').style.display = 'none';
document.getElementById('doc-lote-btn-proximo').style.display = 'none';
document.getElementById('doc-lote-btn-voltar').style.display = 'inline-block';
document.getElementById('doc-lote-btn-previa').style.display = 'inline-block';
document.getElementById('doc-lote-btn-voltar-form').style.display= 'none';
document.getElementById('doc-lote-btn-assinar').style.display = 'none';
document.getElementById('doc-lote-btn-recarregar').style.display = 'none';
}
function irParaPrevia() {
var certFile = document.getElementById('doc-lote-certificado').files[0];
var senha = document.getElementById('doc-lote-senha').value;
if (!certFile) { alert('Selecione o arquivo do certificado.'); return; }
if (!senha) { alert('Digite a senha do certificado.'); return; }
var idsSel = getIdsSelecionados();
var html = '';
DOCS_LOTE.filter(function(d){ return idsSel.indexOf(d.id) !== -1; })
.forEach(function(d) {
html += '<div class="d-flex align-items-center justify-content-between py-2" ' +
'style="border-bottom:1px solid #f0f0f0;">' +
'<span><i class="fa fa-file-pdf-o text-danger mr-1"></i>' +
'<strong>' + escHtml(d.descricao) + '</strong></span>' +
'<a href="/materia/documentoacessorio/' + d.id + '/pdf-previa" target="_blank" ' +
'class="btn btn-sm btn-outline-primary ml-2" title="Visualizar PDF">' +
'<i class="fa fa-eye"></i> Visualizar</a>' +
'</div>';
});
document.getElementById('doc-lote-previa-lista').innerHTML = html || '<p class="text-muted">Nenhum documento selecionado.</p>';
var chk = document.getElementById('doc-lote-previa-confirmacao');
chk.checked = false;
document.getElementById('doc-lote-btn-assinar').disabled = true;
document.getElementById('doc-lote-selecao-step').style.display = 'none';
document.getElementById('doc-lote-form-step').style.display = 'none';
document.getElementById('doc-lote-previa-step').style.display = 'block';
document.getElementById('doc-lote-progresso-step').style.display = 'none';
document.getElementById('doc-lote-resumo-step').style.display = 'none';
document.getElementById('doc-lote-btn-proximo').style.display = 'none';
document.getElementById('doc-lote-btn-voltar').style.display = 'none';
document.getElementById('doc-lote-btn-previa').style.display = 'none';
document.getElementById('doc-lote-btn-voltar-form').style.display= 'inline-block';
document.getElementById('doc-lote-btn-assinar').style.display = 'inline-block';
document.getElementById('doc-lote-btn-recarregar').style.display = 'none';
}
document.getElementById('btn-assinar-doc-lote').addEventListener('click', function(){
document.querySelectorAll('.doc-lote-chk').forEach(function(c){ c.checked = true; });
document.getElementById('form-assinatura-doc-lote').reset();
document.querySelector('#form-assinatura-doc-lote .custom-file-label').textContent =
'Selecione o arquivo do certificado...';
atualizarContagem();
irParaSelecao();
$('#assinaturaDocLoteModal').modal('show');
});
document.getElementById('doc-lote-btn-proximo').addEventListener('click', function(){
if (getIdsSelecionados().length === 0) { alert('Selecione ao menos um documento.'); return; }
irParaForm();
});
document.getElementById('doc-lote-btn-voltar').addEventListener('click', irParaSelecao);
document.getElementById('doc-lote-btn-previa').addEventListener('click', irParaPrevia);
document.getElementById('doc-lote-btn-voltar-form').addEventListener('click', irParaForm);
document.getElementById('doc-lote-previa-confirmacao').addEventListener('change', function(){
document.getElementById('doc-lote-btn-assinar').disabled = !this.checked;
});
document.getElementById('doc-lote-btn-assinar').addEventListener('click', function(){
var certFile = document.getElementById('doc-lote-certificado').files[0];
var senha = document.getElementById('doc-lote-senha').value;
var idsSel = getIdsSelecionados();
if (!certFile) { alert('Selecione o arquivo do certificado.'); return; }
if (!senha) { alert('Digite a senha do certificado.'); return; }
if (!idsSel.length) { alert('Nenhum documento selecionado.'); return; }
var fd = new FormData();
fd.append('certificado', certFile);
fd.append('senha', senha);
fd.append('ids', JSON.stringify(idsSel));
document.getElementById('doc-lote-previa-step').style.display = 'none';
document.getElementById('doc-lote-progresso-step').style.display = 'block';
document.getElementById('doc-lote-btn-voltar-form').style.display= 'none';
document.getElementById('doc-lote-btn-assinar').style.display = 'none';
document.getElementById('doc-lote-btn-fechar').disabled = true;
// Animação de progresso proporcional ao número de documentos
var nDocs = idsSel.length;
var fases = [
{ ate: 10, label: 'Enviando certificado e iniciando assinatura…', ms: 700 },
{ ate: 25, label: 'Validando certificado e gerando PDFs…', ms: Math.min(600, 200 + nDocs * 30) },
{ ate: 50, label: 'Preparando ' + nDocs + ' documento(s) para assinatura…', ms: Math.min(800, 200 + nDocs * 50) },
{ ate: 70, label: 'Enviando ao microserviço de assinatura…', ms: Math.min(1000, 300 + nDocs * 60) },
{ ate: 85, label: 'Aguardando resposta do servidor…', ms: Math.min(800, 300 + nDocs * 40) },
{ ate: 93, label: 'Salvando documentos assinados…', ms: 500 },
{ ate: 97, label: 'Finalizando…', ms: 300 },
];
var faseIdx = 0, progrAtual = 0, progrTimer = null;
function avancarProgresso() {
if (faseIdx >= fases.length) return;
var fase = fases[faseIdx];
if (progrAtual < fase.ate) {
progrAtual = Math.min(progrAtual + 1, fase.ate);
setProgresso(progrAtual, fase.label);
} else { faseIdx++; }
progrTimer = setTimeout(avancarProgresso, fases[Math.min(faseIdx, fases.length-1)].ms / (fase.ate - (faseIdx > 0 ? fases[faseIdx-1].ate : 0)));
}
progrTimer = setTimeout(avancarProgresso, 400);
fetch(URL_LOTE, {
method: 'POST',
body: fd,
headers: { 'X-CSRFToken': getCSRF() }
})
.then(function(r){
clearTimeout(progrTimer);
setProgresso(98, 'Processando resposta…');
if (!r.ok) return r.json().then(function(d){ throw new Error(d.error || ('HTTP ' + r.status)); });
return r.json();
})
.then(function(data){
clearTimeout(progrTimer);
setProgresso(100, 'Concluído.');
document.getElementById('doc-lote-btn-fechar').disabled = false;
mostrarResumo(data);
})
.catch(function(err){
clearTimeout(progrTimer);
document.getElementById('doc-lote-btn-fechar').disabled = false;
setProgresso(100, 'Erro.');
mostrarErroFatal(err.message || String(err));
});
});
function setProgresso(pct, texto) {
var bar = document.getElementById('doc-lote-progress-bar');
bar.style.width = pct + '%';
bar.textContent = pct + '%';
document.getElementById('doc-lote-status-texto').textContent = texto;
}
function mostrarResumo(data) {
document.getElementById('doc-lote-progresso-step').style.display = 'none';
document.getElementById('doc-lote-resumo-step').style.display = 'block';
document.getElementById('doc-lote-btn-recarregar').style.display = 'inline-block';
var temErro = data.erros > 0;
var alertaCls = data.sucesso > 0 ? (temErro ? 'alert-warning' : 'alert-success') : 'alert-danger';
var icone = data.sucesso > 0 ? (temErro ? 'exclamation-triangle' : 'check-circle') : 'times-circle';
document.getElementById('doc-lote-resumo-alerta').innerHTML =
'<div class="alert ' + alertaCls + '">' +
'<i class="fa fa-' + icone + '"></i> ' +
'<strong>' + data.sucesso + ' assinado(s)</strong> com sucesso' +
(temErro ? ', <strong>' + data.erros + '</strong> com erro(s).' : '.') +
' Total: ' + data.total + ' documento(s).</div>';
var html = '<ul class="list-group">';
(data.resultados || []).forEach(function(r){
var cls = r.success ? 'list-group-item-success' : 'list-group-item-danger';
var icon = r.success ? 'check text-success' : 'times text-danger';
html += '<li class="list-group-item list-group-item-sm ' + cls + '">' +
'<i class="fa fa-' + icon + ' mr-1"></i>' +
'<strong>' + escHtml(r.descricao) + '</strong>' +
(r.error ? ' — <small class="text-muted">' + escHtml(r.error) + '</small>' : '') +
'</li>';
});
html += '</ul>';
document.getElementById('doc-lote-resumo-lista').innerHTML = html;
}
function mostrarErroFatal(msg) {
document.getElementById('doc-lote-progresso-step').style.display = 'none';
document.getElementById('doc-lote-resumo-step').style.display = 'block';
document.getElementById('doc-lote-resumo-alerta').innerHTML =
'<div class="alert alert-danger"><i class="fa fa-times-circle"></i> ' +
'<strong>Erro:</strong> ' + escHtml(msg) + '</div>';
document.getElementById('doc-lote-resumo-lista').innerHTML = '';
}
function escHtml(s) {
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
})();
</script>
{% include "materia/assinatura_doc_lote_modal.html" %}
{% endif %}
{% endblock %}

10
sapl/templates/materia/materialegislativa_filter.html

@ -144,6 +144,16 @@
<span class="badge badge-warning text-dark ml-1" id="badge-lote-total">{{ materias_pendentes_lote|length }}</span>
</button>
{% endif %}
{# Assinar Despachos em Lote — atalho exclusivo do Presidente da Mesa Diretora #}
{% if is_presidente_mesa %}
<a href="{% url 'sapl.materia:despachos_pendentes_lote' %}"
class="btn btn-warning btn-sm"
title="Assinar todos os despachos pendentes do sistema em lote (acesso exclusivo do Presidente da Mesa Diretora)">
<i class="fas fa-stamp"></i>
Assinar Despachos em Lote
</a>
{% endif %}
</div>
{# ── /Linha 2 ────────────────────────────────────────────────────── #}

7
sapl/templates/materia/materias_pendentes_assinatura_list.html

@ -4,6 +4,13 @@
{% block actions %}
<div class="actions btn-group float-right pb-4" role="group">
{% if is_presidente_mesa %}
<a href="{% url 'sapl.materia:despachos_pendentes_lote' %}"
class="btn btn-warning"
title="{% trans 'Assinar todos os despachos pendentes em lote (acesso exclusivo do Presidente da Mesa Diretora)' %}">
<i class="fas fa-stamp"></i> {% trans "Assinar Despachos em Lote" %}
</a>
{% endif %}
<a href="{{ url_pesquisa_completa }}" class="btn btn-outline-secondary">
<i class="fas fa-search"></i> {% trans "Ver na Pesquisa Completa" %}
</a>

Loading…
Cancel
Save