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