Browse Source

Feat (Materias): Como cliente, quero poder imprimir diversos documentos de uma única vez [AB#1325]

pull/3858/head
KemuelAlves 4 months ago
parent
commit
1f93ec5817
  1. 3
      sapl/materia/urls.py
  2. 91
      sapl/materia/views.py
  3. 293
      sapl/static/js/materia_impressao_em_massa.js
  4. 205
      sapl/templates/materia/materialegislativa_filter.html

3
sapl/materia/urls.py

@ -34,6 +34,7 @@ from sapl.materia.views import (AcompanhamentoConfirmarView,
MateriaPesquisaSimplesView, MateriaPesquisaSimplesView,
DespachoInicialMultiCreateView, DespachoInicialMultiCreateView,
get_zip_docacessorios, get_pdf_docacessorios, get_zip_completo, get_pdf_completo, get_zip_docacessorios, get_pdf_docacessorios, get_zip_completo, get_pdf_completo,
get_pdf_multiplos,
configEtiquetaMateriaLegislativaCrud, configEtiquetaMateriaLegislativaCrud,
PesquisarStatusTramitacaoView, HistoricoProposicaoView) PesquisarStatusTramitacaoView, HistoricoProposicaoView)
from sapl.materia.onlyoffice_views import (onlyoffice_config, onlyoffice_download, from sapl.materia.onlyoffice_views import (onlyoffice_config, onlyoffice_download,
@ -163,6 +164,8 @@ urlpatterns_materia = [
name='zip_completo_materia'), name='zip_completo_materia'),
url(r'^materia/pdf-completo/(?P<pk>\d+)$', get_pdf_completo, url(r'^materia/pdf-completo/(?P<pk>\d+)$', get_pdf_completo,
name='pdf_completo_materia'), name='pdf_completo_materia'),
url(r'^materia/pdf-multiplos/$', get_pdf_multiplos,
name='pdf_multiplos_materias'),
# OnlyOffice endpoints para Matéria Legislativa # OnlyOffice endpoints para Matéria Legislativa
url(r'^materia/(?P<pk>\d+)/onlyoffice/editor$', materia_onlyoffice_editor, url(r'^materia/(?P<pk>\d+)/onlyoffice/editor$', materia_onlyoffice_editor,

91
sapl/materia/views.py

@ -2800,17 +2800,26 @@ class MateriaLegislativaPesquisaView(MultiFormatOutputMixin, FilterView):
status_assinatura = self.request.GET.get('status_assinatura') status_assinatura = self.request.GET.get('status_assinatura')
if status_assinatura == 'pendente' and context['show_results']: if status_assinatura == 'pendente' and context['show_results']:
from django.db.models import Q as _Q from django.db.models import Q as _Q
qs_lote = self.object_list.filter( # object_list já foi filtrado pelo filter_status_assinatura —
# precisamos obter os IDs primeiro para evitar problemas com
# querysets compostos por union (|) que não suportam .filter() extra
try:
ids_lote = list(self.object_list.values_list('id', flat=True)[:200])
from .models import MateriaLegislativa
qs_lote = MateriaLegislativa.objects.filter(
pk__in=ids_lote,
texto_original__isnull=False, texto_original__isnull=False,
).filter( ).exclude(texto_original='').filter(
_Q(pdf_assinado__isnull=True) | _Q(pdf_assinado='') _Q(pdf_assinado__isnull=True) | _Q(pdf_assinado='')
).exclude(texto_original='').select_related('tipo').values_list( ).select_related('tipo').values_list(
'id', 'tipo__sigla', 'numero', 'ano' 'id', 'tipo__sigla', 'numero', 'ano'
)[:200] )
context['materias_pendentes_lote'] = [ context['materias_pendentes_lote'] = [
{'id': pk, 'descricao': f'{sigla} {numero}/{ano}'} {'id': pk, 'descricao': f'{sigla} {numero}/{ano}'}
for pk, sigla, numero, ano in qs_lote for pk, sigla, numero, ano in qs_lote
] ]
except Exception:
context['materias_pendentes_lote'] = []
else: else:
context['materias_pendentes_lote'] = [] context['materias_pendentes_lote'] = []
@ -3882,3 +3891,77 @@ def configEtiquetaMateriaLegislativaCrud(request):
else: else:
form = ConfigEtiquetaMateriaLegislativaForms(instance=config) form = ConfigEtiquetaMateriaLegislativaForms(instance=config)
return render(request, 'materia/config_etiqueta_materia.html', {'form': form}) return render(request, 'materia/config_etiqueta_materia.html', {'form': form})
def get_pdf_multiplos(request):
"""
Gera PDF unificado com os documentos das matérias informadas via
GET ?ids=1,2,3 ou POST body JSON {"ids": [1,2,3]}.
Retorna o PDF inline para impressão direta no browser.
Limite: 50 matérias por chamada.
"""
logger_local = logging.getLogger(__name__)
username = 'Usuário anônimo' if request.user.is_anonymous else request.user.username
if request.method == 'POST':
import json as _json
try:
body = _json.loads(request.body)
ids_raw = body.get('ids', '')
except Exception:
ids_raw = request.POST.get('ids', '')
else:
ids_raw = request.GET.get('ids', '')
try:
import json as _json
if isinstance(ids_raw, list):
ids = [int(i) for i in ids_raw]
elif isinstance(ids_raw, str) and ids_raw.startswith('['):
ids = [int(i) for i in _json.loads(ids_raw)]
else:
ids = [int(i.strip()) for i in str(ids_raw).split(',') if i.strip().isdigit()]
except Exception:
return JsonResponse({'error': 'IDs invalidos'}, status=400)
if not ids:
return JsonResponse({'error': 'Nenhum ID informado'}, status=400)
ids = ids[:50]
MEDIA_ROOT_local = settings.MEDIA_ROOT
materias = MateriaLegislativa.objects.filter(pk__in=ids).select_related('tipo')
pdf_files = []
for materia in materias:
if materia.pdf_assinado:
f = os.path.join(MEDIA_ROOT_local, str(materia.pdf_assinado))
if os.path.exists(f) and f.lower().endswith('.pdf'):
pdf_files.append(f)
continue
if materia.texto_original:
f = os.path.join(MEDIA_ROOT_local, str(materia.texto_original))
if os.path.exists(f) and f.lower().endswith('.pdf'):
pdf_files.append(f)
if not pdf_files:
return JsonResponse({'error': 'Nenhum PDF disponivel para as materias selecionadas.'}, status=404)
try:
merger = PdfFileMerger(strict=False)
for f in pdf_files:
merger.append(fileobj=f)
data = BytesIO()
merger.write(data)
merger.close()
pdf_bytes = data.getvalue()
except Exception as e:
logger_local.error("user={}. Erro ao gerar PDF multiplos: {}".format(username, str(e)))
return JsonResponse({'error': 'Erro ao gerar PDF: ' + str(e)}, status=500)
logger_local.info("user={}. Gerou PDF multiplos ({} materias, {} PDFs)".format(
username, len(materias), len(pdf_files)))
response = HttpResponse(pdf_bytes, content_type='application/pdf')
response['Content-Disposition'] = 'inline; filename="materias_selecionadas.pdf"'
return response

293
sapl/static/js/materia_impressao_em_massa.js

@ -0,0 +1,293 @@
/**
* materia_impressao_em_massa.js
*
* Impressão / download em massa de matérias legislativas.
*
* Funcionalidades:
* - Ativa modo de seleção ao clicar em "Imprimir Selecionados" (barra de ações)
* - Exibe checkbox em cada linha de resultado
* - Toolbar flutuante mostra contagem e ações (Imprimir / Baixar PDF / Limpar)
* - Botão "Todos" seleciona/desseleciona todos da página atual
* - Ao clicar Imprimir: chama /materia/pdf-multiplos/?ids=... abre PDF no
* browser para impressão direta via window.print() em novo tab
* - Ao clicar Baixar PDF: mesmo endpoint mas força download via blob
*/
(function () {
'use strict';
var URL_PDF_MULTIPLOS = '/materia/pdf-multiplos/';
var MAX_SELECAO = 200;
var modoAtivo = false;
// ── Elementos ────────────────────────────────────────────────────────────
var btnImprimirSelecionados = null; // botão na barra de ações (topo)
var btnSelecionarTodos = null;
var toolbar = null;
var toolbarCount = null;
var toolbarBtnImprimir = null;
var toolbarBtnDownload = null;
var toolbarBtnLimpar = null;
var toolbarLoading = null;
// ── Inicialização ─────────────────────────────────────────────────────────
function init() {
btnImprimirSelecionados = document.getElementById('btn-imprimir-selecionados');
btnSelecionarTodos = document.getElementById('btn-selecionar-todos-print');
toolbar = document.getElementById('print-toolbar');
toolbarCount = document.getElementById('print-toolbar-count');
toolbarBtnImprimir = document.getElementById('print-toolbar-btn-imprimir');
toolbarBtnDownload = document.getElementById('print-toolbar-btn-download');
toolbarBtnLimpar = document.getElementById('print-toolbar-btn-limpar');
toolbarLoading = document.getElementById('print-toolbar-loading');
if (!btnImprimirSelecionados || !toolbar) return; // não está na página de resultados
// Botões já visíveis no novo layout — garante estado inicial correto
btnImprimirSelecionados.style.display = '';
if (btnSelecionarTodos) btnSelecionarTodos.style.display = 'none'; // aparece só quando modo ativo
btnImprimirSelecionados.addEventListener('click', function () {
if (!modoAtivo) {
ativarModo();
} else {
var ids = getIdsSelecionados();
if (ids.length === 0) {
mostrarAlerta('Selecione ao menos um documento para imprimir.');
return;
}
abrirPDF(ids, true);
}
});
btnSelecionarTodos.addEventListener('click', function () {
if (!modoAtivo) { ativarModo(); }
var checks = document.querySelectorAll('.print-chk');
var todasMarcadas = Array.from(checks).every(function (c) { return c.checked; });
checks.forEach(function (c) { c.checked = !todasMarcadas; });
atualizarContagem();
});
toolbarBtnImprimir.addEventListener('click', function () {
var ids = getIdsSelecionados();
if (ids.length === 0) { mostrarAlerta('Selecione ao menos um documento.'); return; }
abrirPDF(ids, true);
});
toolbarBtnDownload.addEventListener('click', function () {
var ids = getIdsSelecionados();
if (ids.length === 0) { mostrarAlerta('Selecione ao menos um documento.'); return; }
baixarPDF(ids);
});
toolbarBtnLimpar.addEventListener('click', function () {
desativarModo();
});
// Delegação de eventos nos checkboxes (gerados dinamicamente)
document.addEventListener('change', function (e) {
if (e.target && e.target.classList.contains('print-chk')) {
atualizarContagem();
}
});
// Clique na linha inteira (quando modo ativo) seleciona o checkbox
document.addEventListener('click', function (e) {
if (!modoAtivo) return;
var row = e.target.closest('.materia-row');
if (!row) return;
// Evita toggle duplo se clicou direto no checkbox ou num link
if (e.target.classList.contains('print-chk')) return;
if (e.target.closest('a')) return;
var chk = row.querySelector('.print-chk');
if (chk) {
chk.checked = !chk.checked;
atualizarContagem();
}
});
}
// ── Modo de seleção ───────────────────────────────────────────────────────
function ativarModo() {
modoAtivo = true;
// Mostra checkboxes em todas as linhas
document.querySelectorAll('.print-select-col').forEach(function (el) {
el.style.display = 'inline-block';
});
// Estilo visual nas linhas
document.querySelectorAll('.materia-row').forEach(function (row) {
row.style.cursor = 'pointer';
});
// Atualiza botão de ações — destaca em vermelho sólido
btnImprimirSelecionados.classList.replace('btn-outline-danger', 'btn-danger');
// Mostra badge e botão Todos
var badge = document.getElementById('badge-print-total');
if (badge) badge.style.display = '';
if (btnSelecionarTodos) btnSelecionarTodos.style.display = '';
toolbar.style.display = 'block';
atualizarContagem();
}
function desativarModo() {
modoAtivo = false;
// Desmarca todos e oculta checkboxes
document.querySelectorAll('.print-chk').forEach(function (c) { c.checked = false; });
document.querySelectorAll('.print-select-col').forEach(function (el) {
el.style.display = 'none';
});
document.querySelectorAll('.materia-row').forEach(function (row) {
row.style.cursor = '';
row.classList.remove('table-active');
});
// Restaura botão para outline
btnImprimirSelecionados.classList.replace('btn-danger', 'btn-outline-danger');
// Oculta badge e botão Todos
var badge = document.getElementById('badge-print-total');
if (badge) { badge.style.display = 'none'; badge.textContent = '0'; }
if (btnSelecionarTodos) btnSelecionarTodos.style.display = 'none';
toolbar.style.display = 'none';
ocultarAlertaToolbar();
}
// ── Contagem ──────────────────────────────────────────────────────────────
function getIdsSelecionados() {
return Array.from(document.querySelectorAll('.print-chk:checked'))
.map(function (c) { return parseInt(c.getAttribute('data-materia-id'), 10); })
.slice(0, MAX_SELECAO);
}
function atualizarContagem() {
var ids = getIdsSelecionados();
var n = ids.length;
// Badge na toolbar flutuante
if (toolbarCount) toolbarCount.textContent = n;
// Badge no botão de ações
var badge = document.getElementById('badge-print-total');
if (badge) badge.textContent = n;
// Destaque visual nas linhas selecionadas
document.querySelectorAll('.materia-row').forEach(function (row) {
var chk = row.querySelector('.print-chk');
if (chk && chk.checked) {
row.classList.add('table-active');
} else {
row.classList.remove('table-active');
}
});
// Aviso de limite
if (n >= MAX_SELECAO) {
mostrarAlertaToolbar('Limite de ' + MAX_SELECAO + ' documentos atingido. Desmarque alguns para selecionar outros.');
} else {
ocultarAlertaToolbar();
}
}
// ── PDF: imprimir ─────────────────────────────────────────────────────────
function abrirPDF(ids, imprimir) {
setLoading(true);
var url = URL_PDF_MULTIPLOS + '?ids=' + ids.join(',');
// Abre em nova aba; quando carregado o browser oferece impressão
var win = window.open(url, '_blank');
if (!win) {
// Pop-up bloqueado — fallback: link direto
var a = document.createElement('a');
a.href = url;
a.target = '_blank';
a.rel = 'noopener';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
// Aguarda pequeno delay e remove loading (não temos evento de "carregou" na outra aba)
setTimeout(function () { setLoading(false); }, 2000);
}
// ── PDF: baixar como arquivo ──────────────────────────────────────────────
function baixarPDF(ids) {
setLoading(true);
var url = URL_PDF_MULTIPLOS + '?ids=' + ids.join(',');
fetch(url, { credentials: 'same-origin' })
.then(function (resp) {
if (!resp.ok) {
return resp.json().then(function (d) {
throw new Error(d.error || ('Erro HTTP ' + resp.status));
});
}
return resp.blob();
})
.then(function (blob) {
var blobUrl = window.URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = blobUrl;
a.download = 'materias_selecionadas.pdf';
a.style.display = 'none';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(blobUrl);
setLoading(false);
})
.catch(function (err) {
setLoading(false);
mostrarAlerta('Erro ao gerar PDF: ' + err.message);
});
}
// ── Helpers UI ────────────────────────────────────────────────────────────
function setLoading(on) {
if (!toolbarLoading) return;
toolbarLoading.style.display = on ? 'block' : 'none';
if (toolbarBtnImprimir) toolbarBtnImprimir.disabled = on;
if (toolbarBtnDownload) toolbarBtnDownload.disabled = on;
}
function mostrarAlerta(msg) {
// Toast simples usando Bootstrap alert
var div = document.createElement('div');
div.className = 'alert alert-warning alert-dismissible fade show';
div.style.cssText = 'position:fixed;top:20px;right:20px;z-index:9999;min-width:300px;max-width:500px;';
div.innerHTML = '<i class="fas fa-exclamation-triangle"></i> ' + escHtml(msg) +
'<button type="button" class="close" data-dismiss="alert"><span>&times;</span></button>';
document.body.appendChild(div);
setTimeout(function () {
if (div.parentNode) div.parentNode.removeChild(div);
}, 5000);
}
function mostrarAlertaToolbar(msg) {
var existing = document.getElementById('print-toolbar-limit-alert');
if (existing) return;
var div = document.createElement('div');
div.id = 'print-toolbar-limit-alert';
div.className = 'alert alert-warning py-1 px-2 mt-2 mb-0 small';
div.innerHTML = '<i class="fas fa-exclamation-triangle"></i> ' + escHtml(msg);
if (toolbar) toolbar.appendChild(div);
}
function ocultarAlertaToolbar() {
var el = document.getElementById('print-toolbar-limit-alert');
if (el && el.parentNode) el.parentNode.removeChild(el);
}
function escHtml(s) {
return String(s)
.replace(/&/g, '&amp;').replace(/</g, '&lt;')
.replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
// ── Arranque ──────────────────────────────────────────────────────────────
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();

205
sapl/templates/materia/materialegislativa_filter.html

@ -7,37 +7,137 @@
{% load static %} {% load static %}
{% block actions %} {% block actions %}
<style>
.sapl-ab {
display: flex;
flex-direction: column;
gap: 6px;
width: 100%;
padding-bottom: .75rem;
}
/* Linha 1: botões de ação (esquerda) + navegação (direita) */
.sapl-ab-row1 {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 6px;
}
/* Linha 2: filtro de assinatura + assinar em lote — só aparece quando necessário */
.sapl-ab-row2 {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 6px;
}
.sapl-ab-left {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 4px;
}
.sapl-ab-right {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 4px;
}
.sapl-ab-sep {
width: 1px;
height: 28px;
background: #dee2e6;
flex-shrink: 0;
margin: 0 2px;
}
#select-status-assinatura { min-width: 190px; max-width: 230px; }
@media (max-width: 576px) {
#select-status-assinatura { min-width: 140px; }
.sapl-ab-sep { display: none; }
}
</style>
<div class="sapl-ab">
{# ── Linha 1: ações + navegação ─────────────────────────────────── #}
<div class="sapl-ab-row1">
<div class="sapl-ab-left">
{% if show_results %} {% if show_results %}
<div class="float-left"> {# Exportações (CSV, XLS, etc) #}
{% with 'sapl.materia:pesquisar_materia' as url_reverse %} {% with 'sapl.materia:pesquisar_materia' as url_reverse %}
{% include "crud/format_options.html" %} {% include "crud/format_options.html" %}
{% endwith %} {% endwith %}
<button id="btn-download-todos-pdfs" class="btn btn-outline-secondary" title="Baixar todos os PDFs das matérias filtradas">
{# Download todos os PDFs desta página #}
<button id="btn-download-todos-pdfs"
class="btn btn-outline-secondary btn-sm"
title="Baixar todos os PDFs das matérias filtradas desta página">
<i class="fas fa-download"></i> <i class="fas fa-download"></i>
<span> Baixar PDFs</span>
</button>
<div class="sapl-ab-sep"></div>
{# Impressão em massa #}
<button id="btn-imprimir-selecionados"
class="btn btn-outline-danger btn-sm"
title="Ativar seleção para impressão em massa">
<i class="fas fa-print"></i>
<span> Imprimir Matérias</span>
<span class="badge badge-danger ml-1" id="badge-print-total" style="display:none;">0</span>
</button>
<button id="btn-selecionar-todos-print"
class="btn btn-outline-secondary btn-sm"
title="Selecionar / desselecionar todos desta página"
style="display:none;">
<i class="fas fa-check-square"></i>
<span> Todos</span>
</button> </button>
{% endif %}
</div> </div>
<div class="sapl-ab-right">
{% switch "SOLR_SWITCH" %}
<a href="{% url 'sapl.base:haystack_search' %}" class="btn btn-outline-primary btn-sm">
<i class="fas fa-search"></i>
<span class="d-none d-md-inline"> Pesquisa Textual</span>
</a>
{% endswitch %}
{% if perms.materia.add_materialegislativa %}
<a href="{% url 'sapl.materia:materialegislativa_create' %}" class="btn btn-outline-primary btn-sm">
<i class="fas fa-plus"></i>
<span class="d-none d-md-inline"> {% blocktrans with verbose_name=view.verbose_name %}Adicionar Matéria Legislativa{% endblocktrans %}</span>
</a>
{% endif %} {% endif %}
{% if show_results %}
<a href="{% url 'sapl.materia:pesquisar_materia' %}" class="btn btn-outline-primary btn-sm">
<i class="fas fa-redo-alt"></i>
<span class="d-none d-sm-inline"> {% trans 'Nova pesquisa' %}</span>
</a>
{% endif %}
</div>
{# ── Filtro rápido de assinatura ─────────────────────────────────── #} </div>
<div class="d-inline-flex align-items-center ml-2" style="vertical-align:middle;"> {# ── /Linha 1 ────────────────────────────────────────────────────── #}
<label class="mb-0 mr-1 font-weight-bold text-secondary" style="white-space:nowrap;font-size:.85rem;">
<i class="fas fa-signature mr-1"></i> Assinatura: {# ── Linha 2: filtro de assinatura + assinar em lote ─────────────── #}
<div class="sapl-ab-row2">
<label class="mb-0 text-secondary font-weight-bold" style="white-space:nowrap; font-size:.82rem;">
<i class="fas fa-signature mr-1"></i>Assinatura:
</label> </label>
{% with status_atual=request.GET.status_assinatura %} {% with status_atual=request.GET.status_assinatura %}
<select id="select-status-assinatura" <select id="select-status-assinatura" class="custom-select custom-select-sm">
class="custom-select custom-select-sm"
style="min-width:210px;">
<option value="" {% if not status_atual %}selected{% endif %}>Todas as matérias</option> <option value="" {% if not status_atual %}selected{% endif %}>Todas as matérias</option>
<option value="pendente" {% if status_atual == 'pendente' %}selected{% endif %}>⚠ Pendente de Assinatura</option> <option value="pendente" {% if status_atual == 'pendente' %}selected{% endif %}>⚠ Pendente de Assinatura</option>
<option value="assinada" {% if status_atual == 'assinada' %}selected{% endif %}>✔ Assinada</option> <option value="assinada" {% if status_atual == 'assinada' %}selected{% endif %}>✔ Assinada</option>
</select> </select>
{% endwith %} {% endwith %}
{# Assinar em Lote — só aparece quando há pendentes #}
{% if materias_pendentes_lote %} {% if materias_pendentes_lote %}
<button type="button" <button type="button"
id="btn-assinar-em-lote" id="btn-assinar-em-lote"
class="btn btn-outline-warning ml-2" class="btn btn-outline-warning btn-sm"
style="min-width:200px"
title="Assinar digitalmente as {{ materias_pendentes_lote|length }} matéria(s) pendentes desta pesquisa"> title="Assinar digitalmente as {{ materias_pendentes_lote|length }} matéria(s) pendentes desta pesquisa">
<i class="fa fa-certificate"></i> <i class="fa fa-certificate"></i>
Assinar em Lote Assinar em Lote
@ -45,23 +145,10 @@
</button> </button>
{% endif %} {% endif %}
</div> </div>
{# ─────────────────────────────────────────────────────────────────── #} {# ── /Linha 2 ────────────────────────────────────────────────────── #}
<div class="actions btn-group float-right pb-4" role="group">
{% switch "SOLR_SWITCH" %}
<a href="{% url 'sapl.base:haystack_search' %}" class="btn btn-outline-primary">
Pesquisa Textual
</a>
{% endswitch %}
{% if perms.materia.add_materialegislativa %}
<a href="{% url 'sapl.materia:materialegislativa_create' %}" class="btn btn-outline-primary">
{% blocktrans with verbose_name=view.verbose_name %} Adicionar Matéria Legislativa {% endblocktrans %}
</a>
{% endif %}
{% if show_results %}
<a href="{% url 'sapl.materia:pesquisar_materia' %}" class="btn btn-outline-primary">{% trans 'Fazer nova pesquisa' %}</a>
{% endif %}
</div> </div>
{% endblock %} {% endblock %}
{% block detail_content %} {% block detail_content %}
@ -87,8 +174,16 @@
{% endif %} {% endif %}
{% for m in page_obj %} {% for m in page_obj %}
<tr> <tr class="materia-row" data-materia-id="{{ m.id }}">
<td> <td>
{# Checkbox para seleção de impressão em massa #}
<div class="print-select-col" style="display:none; float:left; margin-right:8px; margin-top:2px;">
<input type="checkbox"
class="print-chk form-check-input"
data-materia-id="{{ m.id }}"
title="Selecionar para impressão"
style="width:18px;height:18px;cursor:pointer;">
</div>
<strong><a href="{% url 'sapl.materia:materialegislativa_detail' m.id %}">{{m.tipo.sigla}} {{m.numero}}/{{m.ano}} - {{m.tipo}}</strong></a> <strong><a href="{% url 'sapl.materia:materialegislativa_detail' m.id %}">{{m.tipo.sigla}} {{m.numero}}/{{m.ano}} - {{m.tipo}}</strong></a>
<a href="{% url 'sapl.relatorios:etiqueta_materia_legislativa' m.pk %}"><img src="{% webpack_static 'img/etiqueta.png' %}" alt="Etiqueta Individual"></a> <a href="{% url 'sapl.relatorios:etiqueta_materia_legislativa' m.pk %}"><img src="{% webpack_static 'img/etiqueta.png' %}" alt="Etiqueta Individual"></a>
</br> </br>
@ -292,11 +387,67 @@
</table> </table>
{% include "paginacao.html" %} {% include "paginacao.html" %}
{% endif %} {% endif %}
{# ── Toolbar flutuante de impressão em massa ─────────────────────────── #}
{% if show_results %}
<div id="print-toolbar"
style="display:none; position:fixed; bottom:20px; left:50%; transform:translateX(-50%);
z-index:1050; background:#fff; border:2px solid #dc3545;
border-radius:50px; padding:10px 20px;
box-shadow:0 8px 32px rgba(220,53,69,.25), 0 2px 8px rgba(0,0,0,.12);
white-space:nowrap;">
<div class="d-flex align-items-center" style="gap:10px;">
{# Contagem #}
<span class="text-danger font-weight-bold" style="font-size:.95rem;">
<i class="fas fa-print mr-1"></i>
<span id="print-toolbar-count">0</span> selecionado(s)
</span>
<div class="border-left" style="height:22px;"></div>
{# Ações #}
<button id="print-toolbar-btn-imprimir"
class="btn btn-danger btn-sm rounded-pill px-3"
title="Gerar PDF unificado e abrir para impressão">
<i class="fas fa-print mr-1"></i> Imprimir
</button>
<button id="print-toolbar-btn-download"
class="btn btn-outline-danger btn-sm rounded-pill px-3"
title="Baixar PDF unificado">
<i class="fas fa-download mr-1"></i> Baixar PDF
</button>
<div class="border-left" style="height:22px;"></div>
<button id="print-toolbar-btn-limpar"
class="btn btn-link btn-sm text-secondary p-0"
title="Cancelar seleção">
<i class="fas fa-times-circle fa-lg"></i>
</button>
</div>
{# Info + loading (ocultos por padrão) #}
<div class="text-center mt-1" style="font-size:.72rem; color:#888;">
<i class="fas fa-info-circle"></i>
Máx. 200 docs. Apenas matérias com PDF disponível serão incluídas.
</div>
<div id="print-toolbar-loading" style="display:none;" class="mt-2">
<div class="progress" style="height:4px; border-radius:2px;">
<div class="progress-bar progress-bar-striped progress-bar-animated bg-danger" style="width:100%"></div>
</div>
<div class="text-center mt-1" style="font-size:.75rem; color:#888;">Gerando PDF, aguarde…</div>
</div>
</div>
{% endif %}
{# ────────────────────────────────────────────────────────────────────── #}
{% endblock detail_content %} {% endblock detail_content %}
{% block table_content %} {% block table_content %}
{% endblock table_content %} {% endblock table_content %}
{% block extra_js %} {% block extra_js %}
<script src="{% static 'js/materia_pesquisa_download_pdfs.js' %}"></script> <script src="{% static 'js/materia_pesquisa_download_pdfs.js' %}"></script>
<script src="{% static 'js/materia_impressao_em_massa.js' %}"></script>
{% if materias_pendentes_lote %} {% if materias_pendentes_lote %}
{# ── Modal de Assinatura em Lote ─────────────────────────────────────── #} {# ── Modal de Assinatura em Lote ─────────────────────────────────────── #}

Loading…
Cancel
Save