mirror of https://github.com/interlegis/sapl.git
26 changed files with 789 additions and 1154 deletions
@ -0,0 +1,28 @@ |
|||
import './scss/votacao-nominal-bloco.scss' |
|||
import Vue from 'vue' |
|||
|
|||
import VotacaoNominalBloco from '../../components/votacao-nominal-bloco/VotacaoNominalBloco.vue' |
|||
|
|||
Vue.component('votacao-nominal-bloco', VotacaoNominalBloco) |
|||
|
|||
// Sem Pinia/WebSocket aqui de propósito: este formulário é preenchido e
|
|||
// submetido de uma vez (POST nativo pra VotacaoEmBlocoNominalView, que
|
|||
// continua sem mudanças), não uma tela de estado ao vivo — a única coisa
|
|||
// que precisava parar de usar polling era a soma dos votos, que já vira
|
|||
// reatividade normal do Vue dentro de VotacaoNominalBloco.vue.
|
|||
const parlamentaresEl = document.getElementById('parlamentares-data') |
|||
let parlamentares = [] |
|||
if (parlamentaresEl) { |
|||
try { |
|||
parlamentares = JSON.parse(parlamentaresEl.textContent) |
|||
} catch (e) { |
|||
console.error('Erro ao ler parlamentares-data:', e) |
|||
} |
|||
} |
|||
|
|||
new Vue({ // eslint-disable-line
|
|||
el: '#app-votacao-nominal-bloco', |
|||
data () { |
|||
return { parlamentares } |
|||
} |
|||
}) |
|||
@ -0,0 +1 @@ |
|||
/* estilos específicos da votação nominal em bloco, se necessário */ |
|||
@ -0,0 +1,74 @@ |
|||
<template> |
|||
<div> |
|||
<fieldset class="form-group"> |
|||
<legend>Votos:</legend> |
|||
<div class="row"> |
|||
<template v-for="p in parlamentares"> |
|||
<div class="col-md-4" id="styleparlamentar" :key="'nome_' + p.parlamentar_id"> |
|||
{{ p.nome_parlamentar }} |
|||
</div> |
|||
<div class="col-md-5" :key="'voto_' + p.parlamentar_id"> |
|||
<input v-if="p.voto" type="hidden" name="voto_parlamentar" |
|||
:value="p.voto + ':' + p.parlamentar_id"> |
|||
<select class="form-control" |
|||
:name="p.voto ? null : 'voto_parlamentar'" |
|||
:disabled="!!p.voto" |
|||
v-model="votos[p.parlamentar_id]"> |
|||
<option :value="'Não Votou:' + p.parlamentar_id">Não Votou</option> |
|||
<option :value="'Sim:' + p.parlamentar_id">Sim</option> |
|||
<option :value="'Não:' + p.parlamentar_id">Não</option> |
|||
<option :value="'Abstenção:' + p.parlamentar_id">Abstenção</option> |
|||
</select> |
|||
</div> |
|||
</template> |
|||
</div> |
|||
<br> |
|||
<legend>Situação da Votação:</legend> |
|||
<div id="soma_votos"> |
|||
<div class="row"><div class="col-md-12">Sim: {{ tally.sim }}</div></div> |
|||
<div class="row"><div class="col-md-12">Não: {{ tally.nao }}</div></div> |
|||
<div class="row"><div class="col-md-12">Abstenções: {{ tally.abstencao }}</div></div> |
|||
<div class="row"><div class="col-md-12">Ainda não votaram: {{ tally.naoVotou }}</div></div> |
|||
</div> |
|||
<input type="hidden" name="votos_sim" :value="tally.sim"> |
|||
<input type="hidden" name="votos_nao" :value="tally.nao"> |
|||
<input type="hidden" name="abstencoes" :value="tally.abstencao"> |
|||
<input type="hidden" name="nao_votou" :value="tally.naoVotou"> |
|||
</fieldset> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: 'VotacaoNominalBloco', |
|||
props: { |
|||
parlamentares: { |
|||
type: Array, |
|||
default: () => [] |
|||
} |
|||
}, |
|||
data () { |
|||
const votos = {} |
|||
this.parlamentares.forEach((p) => { |
|||
votos[p.parlamentar_id] = (p.voto || 'Não Votou') + ':' + p.parlamentar_id |
|||
}) |
|||
return { votos } |
|||
}, |
|||
computed: { |
|||
// Substitui o antigo setTimeout(conta_votos, 500), que recontava o |
|||
// DOM inteiro a cada 500ms — a reatividade do Vue já refaz esta conta |
|||
// sozinha quando qualquer <select> muda, sem timer nenhum. |
|||
tally () { |
|||
const t = { sim: 0, nao: 0, abstencao: 0, naoVotou: 0 } |
|||
this.parlamentares.forEach((p) => { |
|||
const valor = (this.votos[p.parlamentar_id] || '').split(':')[0] |
|||
if (valor === 'Sim') t.sim++ |
|||
else if (valor === 'Não') t.nao++ |
|||
else if (valor === 'Abstenção') t.abstencao++ |
|||
else t.naoVotou++ |
|||
}) |
|||
return t |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
@ -1,282 +0,0 @@ |
|||
{% extends "crud/detail.html" %} |
|||
{% load i18n crispy_forms_tags %} |
|||
|
|||
{% block detail_content %} |
|||
<div id="ws-status-banner" style="display:none; position:fixed; top:0; left:0; right:0; z-index:2000; padding:10px 16px; text-align:center; font-weight:bold; color:#fff;"></div> |
|||
<form method="post"> |
|||
{% csrf_token %} |
|||
|
|||
<fieldset> |
|||
<legend>Votação Nominal</legend> |
|||
<div> |
|||
Matéria: {{materia.materia}} |
|||
<br /> |
|||
Ementa: {{materia.ementa|safe}} |
|||
</div> |
|||
<br /> |
|||
<div class="alert {% if registro_aberto %}alert-warning{% else %}alert-info{% endif %}" role="alert" id="status_votacao"> |
|||
<strong>Status da votação:</strong> em aberto · |
|||
{% if registro_aberto %} |
|||
novos votos <strong>bloqueados</strong> — os vereadores que ainda não votaram não conseguem mais votar até a Mesa reabrir |
|||
{% else %} |
|||
novos votos <strong>permitidos</strong> — os vereadores ainda podem votar pelo tablet enquanto a Mesa registra |
|||
{% endif %} |
|||
· {{ total }} parlamentar(es) presente(s) |
|||
</div> |
|||
{% if total == 0 %} |
|||
<div class="alert alert-info alert-dismissible " role="alert"> |
|||
<div>Não existe nenhum parlamentar presente para que a votação ocorra.</div> |
|||
</div> |
|||
<a href="{% url 'sapl.sessao:sessaoplenaria_detail' object.pk %}" class="btn btn-warning">Voltar</a> |
|||
{% else %} |
|||
<fieldset class="form-group"> |
|||
|
|||
<legend>Votos</legend> |
|||
<div class="row" id="linhas_parlamentares"> |
|||
{% for parlamentar in parlamentares %} |
|||
<div class="col-md-4" id="styleparlamentar">{{parlamentar.0.nome_parlamentar}}</div> |
|||
<div class="col-md-5" data-parlamentar-row="{{parlamentar.0.id}}"> |
|||
{% if parlamentar.1 %} <input type="hidden" name="voto_parlamentar" value="{{parlamentar.1}}:{{parlamentar.0.id}}" /> {% endif %} |
|||
<select id="voto_parlamentar" name="voto_parlamentar" class="form-control" data-parlamentar-select="{{parlamentar.0.id}}" {% if parlamentar.1 %} disabled {% endif %}> |
|||
<option value="Não Votou:{{parlamentar.0.id}}">Não Votou</option> |
|||
<option value="Sim:{{parlamentar.0.id}}" {% if parlamentar.1 == 'Sim' %} selected {% endif %}>Sim</option> |
|||
<option value="Não:{{parlamentar.0.id}}" {% if parlamentar.1 == 'Não' %} selected {% endif %}>Não</option> |
|||
<option value="Abstenção:{{parlamentar.0.id}}" {% if parlamentar.1 == 'Abstenção' %} selected {% endif %}>Abstenção</option> |
|||
</select> |
|||
<span data-parlamentar-tag="{{parlamentar.0.id}}" class="badge badge-info" style="{% if not parlamentar.1 %}display:none;{% endif %}">Já votado (tablet)</span> |
|||
</div> |
|||
{% endfor %} |
|||
</div> |
|||
|
|||
<legend>Situação da Votação:</legend> |
|||
<div id="soma_votos"></div> |
|||
|
|||
</fieldset> |
|||
|
|||
<div class="row"> |
|||
<div class="col-md-12"> |
|||
{{ form.resultado_votacao|as_crispy_field }} |
|||
</div> |
|||
</div> |
|||
|
|||
<br /> |
|||
<div class="row"> |
|||
<div class="col-md-12"> |
|||
Observações<br/> |
|||
<textarea id="observacao" name="observacao" style="width:100%;" rows="7"></textarea> |
|||
</div> |
|||
</div> |
|||
|
|||
<br /><br /> |
|||
<div class="row"> |
|||
<div class="col-md-12"> |
|||
<div class="form-group row justify-content-between"> |
|||
<input type="submit" id="cancelar-votacao" name="cancelar-votacao" value="Cancelar Votação" class="btn btn-warning" |
|||
data-toggle="tooltip" data-placement="top" title="Apaga todos os votos já registrados desta matéria e reabre a votação do zero." /> |
|||
{% if registro_aberto %} |
|||
<input type="submit" id="reabrir-votacao" name="reabrir-votacao" value="Reabrir Votação para Novos Votos" class="btn btn-secondary" |
|||
data-toggle="tooltip" data-placement="top" title="Volta a aceitar votos dos vereadores que ainda não votaram — nenhum voto já registrado é alterado." /> |
|||
{% else %} |
|||
<input type="submit" id="bloquear-registro-votacao" name="bloquear-registro-votacao" value="Bloquear Novos Votos" class="btn btn-info" |
|||
data-toggle="tooltip" data-placement="top" title="Impede novos votos dos vereadores enquanto a Mesa apura o resultado. Não fecha a votação." /> |
|||
{% endif %} |
|||
<input type="submit" id="salvar-votacao" name="salvar-votacao" value="Encerrar Votação" class="btn btn-primary" |
|||
data-toggle="tooltip" data-placement="top" title="Apura o resultado final e encerra definitivamente a votação desta matéria." /> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
{% endif %} |
|||
</fieldset> |
|||
</form> |
|||
{% endblock detail_content %} |
|||
|
|||
{% block extra_js %} |
|||
<script> |
|||
function voltar() { |
|||
window.history.back(); |
|||
} |
|||
|
|||
$('[data-toggle="tooltip"]').tooltip(); |
|||
|
|||
function conta_votos() { |
|||
var votos_sim = 0; |
|||
var votos_nao = 0; |
|||
var votos_abstencao = 0; |
|||
var nao_votou = 0; |
|||
$('[name=voto_parlamentar]').each(function() { |
|||
if (($(this).is(':hidden')) == false) { |
|||
switch ($(this).val().substring(0,4)) { |
|||
case "Sim:": |
|||
votos_sim = votos_sim + 1; |
|||
break; |
|||
case "Não:": |
|||
votos_nao = votos_nao + 1; |
|||
break; |
|||
case "Abst": |
|||
votos_abstencao = votos_abstencao + 1; |
|||
break; |
|||
case "Não ": |
|||
nao_votou = nao_votou + 1; |
|||
break; |
|||
}; |
|||
}; |
|||
}); |
|||
|
|||
$("#soma_votos").empty(); |
|||
$("#soma_votos").append("<div class='row'><div class='col-md-12'>Sim: " + votos_sim + "</div></div>"); |
|||
$("#soma_votos").append("<div class='row'><div class='col-md-12'>Não: " + votos_nao + "</div></div>"); |
|||
$("#soma_votos").append("<div class='row'><div class='col-md-12'>Abstenções: " + votos_abstencao + "</div></div>"); |
|||
$("#soma_votos").append("<div class='row'><div class='col-md-12'>Ainda não votaram: " + nao_votou + "</div></div>"); |
|||
} |
|||
// Único disparo automático real removido: um setTimeout(conta_votos, 500) |
|||
// se re-agendava para sempre, sondando o próprio DOM a cada 500ms — a |
|||
// única coisa "polling" nesta tela (não os fetches abaixo, que são |
|||
// disparados por evento, não por temporizador). Substituído por dois |
|||
// gatilhos por evento: troca manual do operador num <select> (abaixo) |
|||
// e chegada de voto por WS (dentro de atualiza_votos_ao_vivo(), que já |
|||
// chama conta_votos() quando algo muda) — mais a chamada única no |
|||
// carregamento logo abaixo. |
|||
conta_votos(); |
|||
$(document).on('change', '[name=voto_parlamentar]', conta_votos); |
|||
|
|||
$(window).on('beforeunload', function () { |
|||
$("input[type=submit], input[type=button]").prop("disabled", "disabled"); |
|||
}); |
|||
|
|||
// Busca os votos que já chegaram pelo tablet — usa o próprio |
|||
// endpoint desta tela (?status=1), não o payload público do telão |
|||
// (build_dados_painel(), entregue via WebSocket): aquele mascara o |
|||
// valor real do voto quando a Casa configura "mostrar_voto=False" |
|||
// (regra pensada para o público, não para a Mesa) e a tela pública |
|||
// exige a permissão do módulo painel, que um Operador de Sessão |
|||
// Plenária pode não ter. |
|||
// Chamada uma vez no carregamento e de novo a cada broadcast do WS |
|||
// (não em intervalo fixo — ver mais abaixo), sempre reaplicando o |
|||
// valor (não só a primeira vez) para refletir um vereador que troca |
|||
// o voto durante a janela de votação — a linha permanece travada |
|||
// para edição pelo operador, já que o voto do vereador sempre |
|||
// prevalece (VotacaoNominalAbstract.post). |
|||
function atualiza_votos_ao_vivo() { |
|||
$.ajax({ |
|||
url: window.location.pathname + '?status=1', |
|||
type: "GET", |
|||
dataType: "json", |
|||
}).done(function(data) { |
|||
if (!data || !data.votos) { |
|||
return; |
|||
} |
|||
var mudou = false; |
|||
Object.keys(data.votos).forEach(function(parlamentarId) { |
|||
var voto = data.votos[parlamentarId]; |
|||
if (!voto) { |
|||
return; |
|||
} |
|||
var select = $('[data-parlamentar-select="' + parlamentarId + '"]'); |
|||
if (!select.length) { |
|||
return; |
|||
} |
|||
var optionValue = voto + ':' + parlamentarId; |
|||
if (select.val() !== optionValue) { |
|||
select.val(optionValue); |
|||
mudou = true; |
|||
} |
|||
if (!select.prop('disabled')) { |
|||
select.prop('disabled', true); |
|||
} |
|||
$('[data-parlamentar-tag="' + parlamentarId + '"]').show(); |
|||
}); |
|||
if (mudou) { |
|||
conta_votos(); |
|||
} |
|||
}); |
|||
} |
|||
// Chamada única (sem poll) — o estado atual já é o que veio do |
|||
// carregamento da página; daí em diante só o WebSocket avisa |
|||
// quando reconsultar. |
|||
atualiza_votos_ao_vivo(); |
|||
|
|||
// O broadcast do WebSocket carrega o payload do painel público |
|||
// (build_dados_painel), não o formato de ?status=1 que |
|||
// atualiza_votos_ao_vivo() já sabe interpretar — em vez de duplicar |
|||
// essa lógica com outro formato, qualquer broadcast só dispara a |
|||
// checagem. Sem poll de segurança: se a conexão cair, o banner de |
|||
// erro abaixo avisa em vez de a tela ficar presa sem atualizar. |
|||
(function() { |
|||
var banner = document.getElementById('ws-status-banner'); |
|||
|
|||
function showWsBanner(message, isError) { |
|||
if (!banner) return; |
|||
banner.textContent = message; |
|||
banner.style.display = 'block'; |
|||
banner.style.background = isError ? '#b00020' : '#b8860b'; |
|||
} |
|||
|
|||
function hideWsBanner() { |
|||
if (!banner) return; |
|||
banner.style.display = 'none'; |
|||
} |
|||
|
|||
var PERMANENT_CLOSE_MESSAGES = { |
|||
4401: 'Sessão expirada ou usuário não autenticado. Faça login novamente e recarregue a página.', |
|||
4403: 'Seu usuário não tem permissão para acessar esta tela.', |
|||
4404: 'Sessão plenária não encontrada.' |
|||
}; |
|||
|
|||
var wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; |
|||
var wsUrl = wsProtocol + '//' + window.location.host + '/ws/painel/{{ object.pk }}/'; |
|||
var MIN_RECONNECT_DELAY_MS = 1000; |
|||
var MAX_RECONNECT_DELAY_MS = 30000; |
|||
var reconnectDelayMs = MIN_RECONNECT_DELAY_MS; |
|||
|
|||
function nextDelay() { |
|||
var base = Math.min(reconnectDelayMs * 2, MAX_RECONNECT_DELAY_MS); |
|||
return Math.round(base * (0.7 + Math.random() * 0.6)); |
|||
} |
|||
|
|||
function connect() { |
|||
var socket = new WebSocket(wsUrl); |
|||
|
|||
socket.onopen = function() { |
|||
reconnectDelayMs = MIN_RECONNECT_DELAY_MS; |
|||
hideWsBanner(); |
|||
atualiza_votos_ao_vivo(); |
|||
}; |
|||
|
|||
socket.onmessage = function(event) { |
|||
var message; |
|||
try { |
|||
message = JSON.parse(event.data); |
|||
} catch (e) { |
|||
return; |
|||
} |
|||
if (message && message.type === 'data') { |
|||
atualiza_votos_ao_vivo(); |
|||
} |
|||
}; |
|||
|
|||
socket.onclose = function(event) { |
|||
var permanentMessage = PERMANENT_CLOSE_MESSAGES[event.code]; |
|||
if (permanentMessage) { |
|||
showWsBanner(permanentMessage, true); |
|||
return; |
|||
} |
|||
showWsBanner( |
|||
'Conexão em tempo real perdida. Verifique se o Redis e o ' + |
|||
'Daphne (ou "./manage.py runserver") estão em execução. ' + |
|||
'Tentando reconectar…', |
|||
false |
|||
); |
|||
setTimeout(connect, reconnectDelayMs); |
|||
reconnectDelayMs = nextDelay(); |
|||
}; |
|||
|
|||
socket.onerror = function() { |
|||
socket.close(); |
|||
}; |
|||
} |
|||
|
|||
connect(); |
|||
})(); |
|||
|
|||
</script> |
|||
{% endblock extra_js%} |
|||
Loading…
Reference in new issue