mirror of https://github.com/interlegis/sapl.git
35 changed files with 2206 additions and 14572 deletions
@ -0,0 +1,3 @@ |
|||||
|
docker run --rm -p 6379:6379 redis:7-alpine redis-server --save "" --appendonly no |
||||
|
|
||||
|
daphne -b 127.0.0.1 -p 8000 sapl.asgi:application |
||||
@ -0,0 +1,38 @@ |
|||||
|
import './scss/painel.scss' |
||||
|
import Vue from 'vue' |
||||
|
import { FormSelectPlugin } from 'bootstrap-vue' |
||||
|
import axios from 'axios' |
||||
|
|
||||
|
//TODO: incluir painel-controle dentro da app de painel, colocando rotas diferentes
|
||||
|
|
||||
|
axios.defaults.xsrfCookieName = 'csrftoken' |
||||
|
axios.defaults.xsrfHeaderName = 'X-CSRFToken' |
||||
|
|
||||
|
Vue.use(FormSelectPlugin) |
||||
|
|
||||
|
console.log('painel controle main.js carregado') |
||||
|
|
||||
|
const v = new Vue({ // eslint-disable-line
|
||||
|
delimiters: ['[[', ']]'], |
||||
|
el: '#painel-controle', |
||||
|
data () { |
||||
|
return { |
||||
|
sessao_plenaria: "74ª Sessão Ordinária da 1ª Sessão Legislativa da 18ª Legislatura", |
||||
|
message: "", |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
watch: {}, |
||||
|
|
||||
|
computed: { |
||||
|
|
||||
|
}, |
||||
|
|
||||
|
created () {}, |
||||
|
|
||||
|
methods: {}, |
||||
|
|
||||
|
mounted () { |
||||
|
console.log("Painel controle app mounted!") |
||||
|
} |
||||
|
}) |
||||
@ -0,0 +1,43 @@ |
|||||
|
|
||||
|
.painel-principal { |
||||
|
background: #1c1b1b; |
||||
|
font-family: Verdana; |
||||
|
font-size: x-large; |
||||
|
.text-title { |
||||
|
color: #4fa64d; |
||||
|
margin: 0.5rem; |
||||
|
font-weight: bold; |
||||
|
} |
||||
|
.text-subtitle { |
||||
|
color: #459170; |
||||
|
font-weight: bold; |
||||
|
} |
||||
|
.data-hora { |
||||
|
font-size: 180%; |
||||
|
} |
||||
|
|
||||
|
.text-value { |
||||
|
color: white; |
||||
|
} |
||||
|
|
||||
|
.logo-painel { |
||||
|
max-width: 100%; |
||||
|
} |
||||
|
.painels { |
||||
|
flex-wrap: wrap; |
||||
|
} |
||||
|
.painel{ |
||||
|
margin-top: 1rem; |
||||
|
table { |
||||
|
width: 100%; |
||||
|
} |
||||
|
h2 { |
||||
|
margin-bottom: 0.5rem; |
||||
|
} |
||||
|
#votacao, #oradores_list { |
||||
|
text-align: left; |
||||
|
display: inline-block; |
||||
|
margin-bottom: 1rem; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1 +1,295 @@ |
|||||
import './scss/painel.scss' |
import './scss/painel.scss' |
||||
|
|
||||
|
// main.js (Vue 2)
|
||||
|
import Vue from 'vue' |
||||
|
import Vuex from 'vuex' |
||||
|
import { mapState } from 'vuex'; |
||||
|
import { mapMutations } from 'vuex' |
||||
|
|
||||
|
import Cronometro from '../../components/painel/Cronometro.vue' |
||||
|
import CronometroList from '../../components/painel/CronometroList.vue' |
||||
|
import PainelHeader from '../../components/painel/PainelHeader.vue' |
||||
|
import PainelParlamentares from '../../components/painel/PainelParlamentares.vue' |
||||
|
import PainelOradores from '../../components/painel/PainelOradores.vue' |
||||
|
import PainelMateria from '../../components/painel/PainelMateria.vue' |
||||
|
import PainelResultado from '../../components/painel/PainelResultado.vue' |
||||
|
import alarm from '../../assets/audio/ring.mp3' |
||||
|
|
||||
|
// register components
|
||||
|
Vue.component('painel-cronometro', Cronometro) |
||||
|
Vue.component('painel-cronometro-list', CronometroList) |
||||
|
Vue.component('painel-header', PainelHeader) |
||||
|
Vue.component('painel-parlamentares', PainelParlamentares) |
||||
|
Vue.component('painel-oradores', PainelOradores) |
||||
|
Vue.component('painel-materia', PainelMateria) |
||||
|
Vue.component('painel-resultado', PainelResultado) |
||||
|
|
||||
|
// global store
|
||||
|
Vue.use(Vuex) |
||||
|
const store = new Vuex.Store({ |
||||
|
state: { |
||||
|
sessao_aberta: false, |
||||
|
painel_aberto: false, |
||||
|
mostrar_voto: false, |
||||
|
sessao: {}, |
||||
|
parlamentares: [], |
||||
|
oradores: [], |
||||
|
materia: {}, |
||||
|
resultado: {}, |
||||
|
message: '', |
||||
|
}, |
||||
|
mutations: { |
||||
|
sessaoStatus(state, value) { |
||||
|
state.sessao_aberta = value; |
||||
|
}, |
||||
|
painelStatus(state, value) { |
||||
|
state.painel_aberto = value; |
||||
|
}, |
||||
|
setParlamentares(state, parlamentares) { |
||||
|
state.parlamentares = parlamentares; |
||||
|
}, |
||||
|
updateParlamentares(state, votos_parlamentares) { |
||||
|
if (votos_parlamentares) { |
||||
|
state.parlamentares.forEach((p)=>{ |
||||
|
if (p.parlamentar_id in votos_parlamentares) { |
||||
|
p.voto = votos_parlamentares[p.parlamentar_id].voto |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
}, |
||||
|
setOradores(state, oradores) { |
||||
|
state.oradores = oradores; |
||||
|
}, |
||||
|
setMateria(state, materia) { |
||||
|
state.materia = materia; |
||||
|
}, |
||||
|
setResultado(state, resultado) { |
||||
|
state.resultado = resultado; |
||||
|
}, |
||||
|
setMessage(state, message) { |
||||
|
state.message = message; |
||||
|
}, |
||||
|
setMostrarVoto(state, mostrar_voto) { |
||||
|
state.mostrar_voto = mostrar_voto; |
||||
|
} |
||||
|
}, |
||||
|
actions: {}, |
||||
|
getters: {} |
||||
|
}) |
||||
|
|
||||
|
const BACKOFF = 500 |
||||
|
const PING_INTERVAL = 30000 // 30s
|
||||
|
|
||||
|
new Vue({ |
||||
|
store, |
||||
|
el: '#painel', |
||||
|
delimiters: ['[[', ']]'], |
||||
|
data() { |
||||
|
return { |
||||
|
controllerId: null, |
||||
|
ws: null, |
||||
|
isOpen: false, |
||||
|
error: null, |
||||
|
pingTimer: null, |
||||
|
reconnectTimer: null, |
||||
|
} |
||||
|
}, |
||||
|
mounted() { |
||||
|
console.log('Painel principal mounted!') |
||||
|
// $el is guaranteed here
|
||||
|
const el = this.$el |
||||
|
// prefer data-attr; fallback to global if you set it
|
||||
|
this.controllerId = el.dataset.controllerId || window.controllerId |
||||
|
console.log(`ControllerId: ${this.controllerId}`) |
||||
|
this.connectWS() |
||||
|
|
||||
|
}, |
||||
|
computed: { |
||||
|
...mapState(["painel_aberto", "sessao_aberta"]), |
||||
|
canRender () { |
||||
|
return this.sessao_aberta && this.painel_aberto; |
||||
|
}, |
||||
|
}, |
||||
|
methods: { |
||||
|
...mapMutations(['sessaoStatus', 'painelStatus','setParlamentares', |
||||
|
'updateParlamentares', 'setOradores', 'setMateria', |
||||
|
'setResultado', 'setMessage', 'setMostrarVoto']), |
||||
|
wsURL() { |
||||
|
const proto = location.protocol === 'https:' ? 'wss' : 'ws' |
||||
|
return `${proto}://${location.host}/ws/painel/${this.controllerId}/` |
||||
|
}, |
||||
|
handleStopWatchEvent(data) { |
||||
|
console.log("Received a stopwatch update event"); |
||||
|
//TODO: check if action is valid
|
||||
|
//TODO: check if stopwatch ID is valid
|
||||
|
|
||||
|
const stopwatch = this.$refs[data.id] |
||||
|
//TODO: stopwatch has to buzz at 00:30 AND 00:00
|
||||
|
if (data.action == "start" || data.action == "stop") { |
||||
|
console.log(`Stopwatch Event: ${data}`) |
||||
|
stopwatch.handleStartStop(); |
||||
|
} else if (data.action == "reset") { |
||||
|
stopwatch.handleReset(); |
||||
|
} else if (data.action == "set") { |
||||
|
//TODO: check if time is passed and valid
|
||||
|
stopwatch.initialTime = stopwatch.time |
||||
|
stopwatch.time = stopwatch.time |
||||
|
} else { |
||||
|
console.log(`Invalid stopwatch action ${data.action}`); |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// TODO: check if WebSocket is open and handle submission errors
|
||||
|
// TODO: save stopwatch state on localStorage?
|
||||
|
// Return updated stopwatch status
|
||||
|
// "stopwatch": {
|
||||
|
// "type": "stopwatch.state",
|
||||
|
// "id": "sw:main",
|
||||
|
// "status": "running",
|
||||
|
// "started_at_ms": 1699990000123,
|
||||
|
// "elapsed_ms": 5320
|
||||
|
// }
|
||||
|
// TODO: change started_at and elapsed to milliseconds
|
||||
|
this.ws.send(JSON.stringify({type:'stopwatch.state', |
||||
|
id: stopwatch.id, |
||||
|
status: stopwatch.isRunning ? "running" : "stopped", |
||||
|
started_at_s: stopwatch.initialTime, |
||||
|
elapsed_s: stopwatch.initialTime - stopwatch.time, |
||||
|
})) |
||||
|
|
||||
|
}, |
||||
|
updateState(data) { |
||||
|
try { |
||||
|
// CONFIG GERAIS
|
||||
|
this.sessaoStatus(data.sessao_aberta); |
||||
|
this.painelStatus(data.painel_aberto); |
||||
|
this.setMostrarVoto(data.mostrar_voto); |
||||
|
|
||||
|
// PARLAMENTARES
|
||||
|
if (data.parlamentares) { |
||||
|
// pre-popula para Vuex capturar mudanca de estado de 'voto'
|
||||
|
data.parlamentares.forEach(p => p.voto = ''); |
||||
|
this.setParlamentares(data.parlamentares); |
||||
|
} |
||||
|
|
||||
|
// HEADER DO PAINEL
|
||||
|
// SESSAO_PLENARIA
|
||||
|
//TODO: group in a single SessaoPlenaria object
|
||||
|
const headerInstance = this.$refs.painelHeader; |
||||
|
//TODO: setup as child's props?
|
||||
|
headerInstance.sessao_plenaria = data.sessao.sessao_plenaria |
||||
|
headerInstance.sessao_plenaria_data = data.sessao.sessao_plenaria_data |
||||
|
headerInstance.sessao_plenaria_hora_inicio = data.sessao.sessao_plenaria_hora_inicio |
||||
|
headerInstance.brasao = data.sessao.brasao |
||||
|
|
||||
|
if (data.message) { |
||||
|
this.setMessage(data.message); |
||||
|
} |
||||
|
|
||||
|
// ORADORES
|
||||
|
if (data.oradores) { |
||||
|
this.setOradores(data.oradores); |
||||
|
} |
||||
|
|
||||
|
// MATERIA
|
||||
|
if (data.materia) { |
||||
|
this.setMateria(data.materia); |
||||
|
} |
||||
|
|
||||
|
// RESULTADO
|
||||
|
if (data.materia.resultado) { |
||||
|
this.setResultado(data.materia.resultado); |
||||
|
} |
||||
|
|
||||
|
if (data.materia.resultado.votos_parlamentares) { |
||||
|
this.updateParlamentares(data.materia.resultado.votos_parlamentares); |
||||
|
} |
||||
|
} catch (e) { |
||||
|
console.error('Error', e); |
||||
|
} |
||||
|
}, |
||||
|
connectWS() { |
||||
|
const url = this.wsURL() |
||||
|
this.ws = new WebSocket(url) |
||||
|
|
||||
|
this.ws.addEventListener('open', () => { |
||||
|
this.isOpen = true; |
||||
|
this.error = null |
||||
|
|
||||
|
console.log(`✅ WebSocket connected to ${url}`); |
||||
|
|
||||
|
// send an initial message to the server
|
||||
|
this.ws.send(JSON.stringify({ type: "echo", message: "Client connected" })); |
||||
|
|
||||
|
// ping keep-alive timer
|
||||
|
this.pingTimer = setInterval(() => { |
||||
|
const ping = JSON.stringify({ type: "ping", ts: Date.now()}); |
||||
|
console.log(`Sending ping ${ping}`) |
||||
|
// TODO: check if this.ws object is still usable!!!
|
||||
|
this.ws.send(ping); |
||||
|
}, PING_INTERVAL); |
||||
|
}) |
||||
|
|
||||
|
this.ws.addEventListener('message', (message) => { |
||||
|
try { |
||||
|
const data = JSON.parse(message.data) |
||||
|
console.debug(`${JSON.stringify(data)}`) |
||||
|
|
||||
|
if (data.type === 'data') { |
||||
|
this.updateState(data); |
||||
|
} else if (data.type == 'echo') { |
||||
|
console.log(`Received ack from server: ${JSON.stringify(data)}`); |
||||
|
} else if (data.type == 'ping') { |
||||
|
console.log(`Received ping from server: ${JSON.stringify(data)}`); |
||||
|
} else if (data.type == 'stopwatch.update') { |
||||
|
this.handleStopWatchEvent(data); |
||||
|
} |
||||
|
} catch (e) { |
||||
|
console.error('WS parse error:', e); |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
this.ws.addEventListener('close', (e) => { |
||||
|
console.log("❌ WebSocket closed:", e); |
||||
|
this.isOpen = false; |
||||
|
this.reconnectTimer = setTimeout(() => this.connectWS(), BACKOFF); |
||||
|
}) |
||||
|
|
||||
|
this.ws.addEventListener('error', (e) => { |
||||
|
this.error = e |
||||
|
console.error('❌ WebSocket error:', e) |
||||
|
}) |
||||
|
}, |
||||
|
closeWS() { |
||||
|
try { |
||||
|
console.log(`⚠️ Closing Websocket connection: ${this.wsURL}`); |
||||
|
this.ws && this.ws.close() |
||||
|
} catch (_) { |
||||
|
console.log("Error closing WS connection"); |
||||
|
} |
||||
|
}, |
||||
|
beforeDestroy() { |
||||
|
// Clear the interval before the component is destroyed
|
||||
|
if (this.pingTimer) { |
||||
|
clearInterval(this.pingTimer); |
||||
|
console.log('pingTimer Interval cleared'); |
||||
|
} |
||||
|
|
||||
|
if (this.reconnectTimer) { |
||||
|
clearTimeout(this.reconnectTimer); |
||||
|
console.log('reconnectTimer cleared'); |
||||
|
} |
||||
|
|
||||
|
this.closeWS(); |
||||
|
}, |
||||
|
changeFontSize(value) { |
||||
|
for (var name in this.$refs){ |
||||
|
if (name.startsWith("sw")) { |
||||
|
const cronometro = this.$refs[name] |
||||
|
cronometro.changeFontSize(value) |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
startStopwatch() { if (this.isOpen) this.ws.send(JSON.stringify({ type:'notify', stopwatch:'start' })) }, |
||||
|
} |
||||
|
}) |
||||
|
|||||
@ -0,0 +1,238 @@ |
|||||
|
import './scss/votacao.scss' |
||||
|
import Vue from 'vue' |
||||
|
import { FormSelectPlugin } from 'bootstrap-vue' |
||||
|
import axios from 'axios' |
||||
|
|
||||
|
axios.defaults.xsrfCookieName = 'csrftoken' |
||||
|
axios.defaults.xsrfHeaderName = 'X-CSRFToken' |
||||
|
|
||||
|
Vue.use(FormSelectPlugin) |
||||
|
|
||||
|
console.log('votacao main.js carregado') |
||||
|
|
||||
|
const v = new Vue({ // eslint-disable-line
|
||||
|
delimiters: ['[[', ']]'], |
||||
|
el: '#votacao', |
||||
|
data () { |
||||
|
return { |
||||
|
votacao_aberta: true, |
||||
|
edit_votes: true, |
||||
|
disable_resultado: false, |
||||
|
resultado_selected: "", |
||||
|
observacoes: "", |
||||
|
error_message: "", |
||||
|
tipo_votacao: 2, |
||||
|
tipo_votacao_descricao: "Votação Nominal", |
||||
|
materia: "Projeto de Lei Ordinária nº 3 de 2025", |
||||
|
ementa: "Institui no Município de Pato Branco o Projeto Chá de Fralda Social. ", |
||||
|
parlamentares: [ |
||||
|
{ |
||||
|
"parlamentar_id": 197, |
||||
|
"nome_parlamentar": "Alexandre Zoche", |
||||
|
"filiacao": "PRD" |
||||
|
}, |
||||
|
{ |
||||
|
"parlamentar_id": 196, |
||||
|
"nome_parlamentar": "Anne Cristine Gomes da Silva Cavali", |
||||
|
"filiacao": "PSD" |
||||
|
}, |
||||
|
{ |
||||
|
"parlamentar_id": 194, |
||||
|
"nome_parlamentar": "Diogo Domingos Grando", |
||||
|
"filiacao": "PRD" |
||||
|
}, |
||||
|
{ |
||||
|
"parlamentar_id": 186, |
||||
|
"nome_parlamentar": "Eduardo Albani Dala Costa", |
||||
|
"filiacao": "Republicanos" |
||||
|
}, |
||||
|
{ |
||||
|
"parlamentar_id": 3, |
||||
|
"nome_parlamentar": "Fabricio Preis de Mello", |
||||
|
"filiacao": "PL" |
||||
|
}, |
||||
|
{ |
||||
|
"parlamentar_id": 6, |
||||
|
"nome_parlamentar": "Joecir Bernardi", |
||||
|
"filiacao": "PSD" |
||||
|
}, |
||||
|
{ |
||||
|
"parlamentar_id": 187, |
||||
|
"nome_parlamentar": "Lindomar Rodrigo Brandão", |
||||
|
"filiacao": "PP" |
||||
|
}, |
||||
|
{ |
||||
|
"parlamentar_id": 195, |
||||
|
"nome_parlamentar": "Rafael Foss", |
||||
|
"filiacao": "União" |
||||
|
}, |
||||
|
{ |
||||
|
"parlamentar_id": 11, |
||||
|
"nome_parlamentar": "Rodrigo José Correia", |
||||
|
"filiacao": "União" |
||||
|
}, |
||||
|
{ |
||||
|
"parlamentar_id": 192, |
||||
|
"nome_parlamentar": "Thania Maria Caminski Gehlen", |
||||
|
"filiacao": "PP" |
||||
|
} |
||||
|
], |
||||
|
//TODO: check if votos_parlamentares is null
|
||||
|
votos_parlamentares: { |
||||
|
186: { |
||||
|
"voto": "Não", |
||||
|
"materia_id": 31919, |
||||
|
"parlamentar_id": 186, |
||||
|
"parlamentar_nome": "Eduardo Albani Dala Costa" |
||||
|
}, |
||||
|
195: { |
||||
|
"voto": "Não", |
||||
|
"materia_id": 31919, |
||||
|
"parlamentar_id": 195, |
||||
|
"parlamentar_nome": "Rafael Foss" |
||||
|
}, |
||||
|
196: { |
||||
|
"voto": "Não", |
||||
|
"materia_id": 31919, |
||||
|
"parlamentar_id": 196, |
||||
|
"parlamentar_nome": "Anne Cristine Gomes da Silva Cavali" |
||||
|
}, |
||||
|
3: { |
||||
|
"voto": "Sim", |
||||
|
"materia_id": 31919, |
||||
|
"parlamentar_id": 3, |
||||
|
"parlamentar_nome": "Fabricio Preis de Mello" |
||||
|
}, |
||||
|
11: { |
||||
|
"voto": "Não", |
||||
|
"materia_id": 31919, |
||||
|
"parlamentar_id": 11, |
||||
|
"parlamentar_nome": "Rodrigo José Correia" |
||||
|
}, |
||||
|
194: { |
||||
|
"voto": "Não", |
||||
|
"materia_id": 31919, |
||||
|
"parlamentar_id": 194, |
||||
|
"parlamentar_nome": "Diogo Domingos Grando" |
||||
|
}, |
||||
|
197: { |
||||
|
"voto": "Não", |
||||
|
"materia_id": 31919, |
||||
|
"parlamentar_id": 197, |
||||
|
"parlamentar_nome": "Alexandre Zoche" |
||||
|
}, |
||||
|
6: { |
||||
|
"voto": "Não", |
||||
|
"materia_id": 31919, |
||||
|
"parlamentar_id": 6, |
||||
|
"parlamentar_nome": "Joecir Bernardi" |
||||
|
}, |
||||
|
187: { |
||||
|
"voto": "Abstenção", |
||||
|
"materia_id": 31919, |
||||
|
"parlamentar_id": 187, |
||||
|
"parlamentar_nome": "Lindomar Rodrigo Brandão" |
||||
|
}, |
||||
|
192: { |
||||
|
"voto": "Sim", |
||||
|
"materia_id": 31919, |
||||
|
"parlamentar_id": 192, |
||||
|
"parlamentar_nome": "Thania Maria Caminski Gehlen" |
||||
|
} |
||||
|
}, |
||||
|
options: [ |
||||
|
{ text: 'Sim', value: 'voto_sim' }, |
||||
|
{ text: 'Não', value: 'voto_nao' }, |
||||
|
{ text: 'Abstenção', value: 'abstencao' }, |
||||
|
{ text: 'Não Votou', value: 'nao_votou' }, |
||||
|
], |
||||
|
tipos_resultados: [ |
||||
|
{ |
||||
|
"id": 13, |
||||
|
"nome": "Aprovada a retirada de pauta" |
||||
|
}, |
||||
|
{ |
||||
|
"id": 10, |
||||
|
"nome": "Aprovada por dois terços" |
||||
|
}, |
||||
|
{ |
||||
|
"id": 2, |
||||
|
"nome": "Aprovada por maioria absoluta" |
||||
|
}, |
||||
|
{ |
||||
|
"id": 1, |
||||
|
"nome": "Aprovada por maioria simples - conforme o art. 37 do RI o presidente não vota" |
||||
|
}, |
||||
|
{ |
||||
|
"id": 8, |
||||
|
"nome": "Aprovada por maioria simples - conforme o art. 37 do RI o presidente votou pelo desempate" |
||||
|
}, |
||||
|
{ |
||||
|
"id": 15, |
||||
|
"nome": "Aprovada." |
||||
|
}, |
||||
|
{ |
||||
|
"id": 7, |
||||
|
"nome": "Empate - conforme o art. 37 do RI o presidente vota para desempate" |
||||
|
}, |
||||
|
{ |
||||
|
"id": 16, |
||||
|
"nome": "IMPROCEDENTE" |
||||
|
}, |
||||
|
{ |
||||
|
"id": 12, |
||||
|
"nome": "Leitura em Plenário" |
||||
|
}, |
||||
|
{ |
||||
|
"id": 17, |
||||
|
"nome": "PROCEDENTE" |
||||
|
}, |
||||
|
{ |
||||
|
"id": 11, |
||||
|
"nome": "Prejudicada" |
||||
|
}, |
||||
|
{ |
||||
|
"id": 5, |
||||
|
"nome": "Rejeitada" |
||||
|
}, |
||||
|
{ |
||||
|
"id": 14, |
||||
|
"nome": "Rejeitada a retirada de pauta" |
||||
|
}, |
||||
|
{ |
||||
|
"id": 9, |
||||
|
"nome": "Rejeitada por maioria simples - conforme o art. 37 do RI o presidente votou pelo desempate" |
||||
|
} |
||||
|
], |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
watch: {}, |
||||
|
|
||||
|
computed: { |
||||
|
total_votos() { |
||||
|
// TODO: use number index instead of string ("sim", "não") as keys.
|
||||
|
var groupedVotes = Map.groupBy(Object.values(this.votos_parlamentares), ({ voto }) => voto ) |
||||
|
// initialize total_votos
|
||||
|
total_votos = [ |
||||
|
{"tipo": "Sim", "total": 0}, |
||||
|
{"tipo": "Não", "total": 0}, |
||||
|
{"tipo": "Abstenção", "total": 0}, |
||||
|
{"tipo": "Não Votou", total: 0} |
||||
|
] |
||||
|
for (const [key, value] of groupedVotes.entries()) { |
||||
|
const index = total_votos.findIndex(item => item.tipo === key); |
||||
|
total_votos[index].total = value.length |
||||
|
} |
||||
|
return total_votos |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
created () {}, |
||||
|
|
||||
|
methods: {}, |
||||
|
|
||||
|
mounted () { |
||||
|
console.log("Votacao app mounted!") |
||||
|
} |
||||
|
}) |
||||
@ -0,0 +1,99 @@ |
|||||
|
<template> |
||||
|
<div> |
||||
|
<audio |
||||
|
ref="player" |
||||
|
:src="audioSrc" |
||||
|
preload="auto" |
||||
|
></audio> |
||||
|
<span ref="time">{{ title }}: {{ formatTime(time) }}<br/></span> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
export default { |
||||
|
name: 'Cronometro', |
||||
|
props: ['id', 'title'], |
||||
|
data() { |
||||
|
return { |
||||
|
time: 300, |
||||
|
isRunning: false, |
||||
|
initialTime: 300, |
||||
|
intervalId: null, |
||||
|
audioSrc: require('@/assets/audio/ring.mp3'), |
||||
|
} |
||||
|
}, |
||||
|
mounted() { |
||||
|
console.log('Cronometro mounted'); |
||||
|
console.log(this.audioSrc); |
||||
|
this.$emit('child-mounted'); // Emit a custom event |
||||
|
}, |
||||
|
methods: { |
||||
|
changeFontSize(value) { |
||||
|
const el = this.$refs.time; |
||||
|
if (!el) return; |
||||
|
let fontSize = window.getComputedStyle(el).fontSize; |
||||
|
fontSize = parseFloat(fontSize); // safely convert "16px" → 16 |
||||
|
el.style.fontSize = (fontSize + value) + 'px'; |
||||
|
}, |
||||
|
handleStartStop() { |
||||
|
this.isRunning = !this.isRunning; |
||||
|
|
||||
|
if (this.isRunning) { |
||||
|
this.intervalId = setInterval(() => { |
||||
|
if (this.time > 0) { |
||||
|
this.time--; |
||||
|
// play buzz at 00:00:30 |
||||
|
if (this.time == 30000) { |
||||
|
this.playSound(); |
||||
|
} |
||||
|
} else { |
||||
|
this.isRunning = false; |
||||
|
clearInterval(this.intervalId); |
||||
|
this.playSound(); |
||||
|
} |
||||
|
}, 1000); |
||||
|
} else { |
||||
|
clearInterval(this.intervalId); |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
handleReset() { |
||||
|
this.isRunning = false; |
||||
|
clearInterval(this.intervalId); |
||||
|
this.time = this.initialTime; |
||||
|
}, |
||||
|
|
||||
|
playSound() { |
||||
|
const audio = this.$refs.player |
||||
|
if (!audio) return |
||||
|
|
||||
|
const playPromise = audio.play() |
||||
|
}, |
||||
|
|
||||
|
formatTime(seconds) { |
||||
|
const hrs = Math.floor(seconds / 3600); |
||||
|
const mins = Math.floor((seconds % 3600) / 60); |
||||
|
const secs = seconds % 60; |
||||
|
|
||||
|
if (hrs > 0) { |
||||
|
return `${hrs.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; |
||||
|
} |
||||
|
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; |
||||
|
} |
||||
|
}, |
||||
|
watch: { |
||||
|
initialTime(newVal) { |
||||
|
if (!this.isRunning) { |
||||
|
this.time = newVal; |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
beforeDestroy() { |
||||
|
clearInterval(this.intervalId); |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
/* Add your own styles here */ |
||||
|
</style> |
||||
@ -0,0 +1,51 @@ |
|||||
|
.<template> |
||||
|
<div class="col-md-6 text-left painel" v-if="canRender"> |
||||
|
<div class="d-flex align-items-left justify-content-left mb-2"> |
||||
|
<h2 class="text-subtitle mb-0">Cronômetros</h2> |
||||
|
</div> |
||||
|
<div class="text-value" id="box_cronometros"> |
||||
|
<Cronometro v-for="(title, idx) in titles" :key="idx" :title="title" :ref="'childRef_' + idx" @child-mounted="handleChildMounted"/> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import { ref, onMounted } from 'vue'; |
||||
|
import { mapState } from 'vuex'; |
||||
|
import Cronometro from './Cronometro.vue'; |
||||
|
|
||||
|
export default { |
||||
|
name: 'CronometroList', |
||||
|
components: { |
||||
|
Cronometro, |
||||
|
}, |
||||
|
data() { |
||||
|
return { |
||||
|
titles: ["Discurso", "Aparte", "Questão de Ordem", "Considerações Finais"], |
||||
|
itemRefs: ref([]), // An array to store the refs |
||||
|
} |
||||
|
}, |
||||
|
mounted() { |
||||
|
console.log('CronometroList mounted'); |
||||
|
}, |
||||
|
computed: { |
||||
|
canRender () { |
||||
|
return this.sessao_aberta && this.painel_aberto; |
||||
|
}, |
||||
|
...mapState(["painel_aberto", "sessao_aberta"]) |
||||
|
}, |
||||
|
methods: { |
||||
|
handleStartStop() { |
||||
|
console.log("start/stop stopwatch"); |
||||
|
//console.log(this.$refs.itemRefs); |
||||
|
}, |
||||
|
handleChildMounted() { |
||||
|
console.log('ChildComponent has finished mounting in the parent!'); |
||||
|
// Perform actions in the parent that depend on the child being fully mounted |
||||
|
const childId = 0; |
||||
|
const childComponent = this.$refs['childRef_' + childId]; |
||||
|
childComponent[0].handleStartStop(); |
||||
|
}, |
||||
|
}, |
||||
|
}; |
||||
|
</script> |
||||
@ -0,0 +1,72 @@ |
|||||
|
<template v-if="sessao_aberta"> |
||||
|
<div> |
||||
|
<div class="d-flex justify-content-center"> |
||||
|
<h1 id="sessao_plenaria" class="title text-title">{{ sessao_plenaria }} </h1> |
||||
|
</div> |
||||
|
<div class="row "> |
||||
|
<div class="col text-center"> |
||||
|
<span id="sessao_plenaria_data" class="text-value"> Data Início: {{ sessao_plenaria_data }} </span> |
||||
|
</div> |
||||
|
<div class="col text-center"> |
||||
|
<span id="sessao_plenaria_hora_inicio" class="text-value"> Hora Início: {{ sessao_plenaria_hora_inicio }} </span> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="row justify-content-center"> |
||||
|
<div class="col-1"> |
||||
|
<img v-bind:src="brasao" id="logo-painel" class="logo-painel" alt=""/> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="row justify-content-center"> |
||||
|
<h2 class="text-danger"><span id="message">{{ message }}</span></h2> |
||||
|
</div> |
||||
|
|
||||
|
<div class="row"> |
||||
|
<div class="col text-center"><span class="text-value data-hora" id="date">{{ data_atual }}</span></div> |
||||
|
<div class="col text-center"><span class="text-value data-hora" id="relogio">{{ relogio }}</span></div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import { mapState } from 'vuex'; |
||||
|
export default { |
||||
|
name: 'PainelHeader', |
||||
|
props: { |
||||
|
}, |
||||
|
|
||||
|
data() { |
||||
|
return { |
||||
|
sessao_plenaria: "Sessao Plenaria Teste", |
||||
|
sessao_plenaria_data: "22/10/2025", |
||||
|
sessao_plenaria_hora_inicio: "13:30", |
||||
|
brasao: "", |
||||
|
data_atual: "", |
||||
|
relogio: "", |
||||
|
currentDateTimeId: null, // stores the id returned by setInterval() |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
methods: { |
||||
|
startCurrentDateTime() { |
||||
|
this.data_atual = moment().utcOffset(-3).format("DD/MM/YY"); // RECOVER UTC OFFSET!!!! |
||||
|
this.currentDateTimeId = setInterval(() => { |
||||
|
this.relogio = moment.utc().utcOffset(-3).format("HH:mm:ss"); |
||||
|
}, 500); |
||||
|
}, |
||||
|
beforeDestroy() { |
||||
|
// Clear the interval before the component is destroyed |
||||
|
if (this.currentDateTimeId) { |
||||
|
clearInterval(this.currentDateTimeId); |
||||
|
console.log('currentDateTimeId Interval cleared.'); |
||||
|
} |
||||
|
}, |
||||
|
}, |
||||
|
computed: { |
||||
|
...mapState(["sessao_aberta", "painel_aberto", "message", "mostrar_voto"]) |
||||
|
}, |
||||
|
mounted() { |
||||
|
console.log('PainelHeader component mounted'); |
||||
|
this.startCurrentDateTime(); |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
@ -0,0 +1,42 @@ |
|||||
|
<template> |
||||
|
<div class="col-md-6 text-center painel" id="obs_materia_div" v-if="canRender"> |
||||
|
<h2 class="text-subtitle" id="mat_em_votacao">Matéria em Votação</h2> |
||||
|
<span id="materia_legislativa_texto" class="text-value">{{ materia.texto }}</span> |
||||
|
<br> |
||||
|
<span id="materia_legislativa_ementa" class="text-value">{{ materia.ementa }} </span> |
||||
|
<br> |
||||
|
<span id="observacao_materia" class="text-value">{{ materia.observacao }}</span> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import { mapState } from 'vuex'; |
||||
|
export default { |
||||
|
name: 'PainelMateria', |
||||
|
data() { |
||||
|
return { |
||||
|
/* |
||||
|
materia: { |
||||
|
texto: '', |
||||
|
ementa: '', |
||||
|
observacao: '', |
||||
|
} |
||||
|
*/ |
||||
|
}; |
||||
|
}, |
||||
|
mounted() { |
||||
|
console.log('PainelMateria mounted'); |
||||
|
}, |
||||
|
beforeDestroy() {}, |
||||
|
computed: { |
||||
|
canRender () { |
||||
|
return this.sessao_aberta && this.painel_aberto; |
||||
|
}, |
||||
|
...mapState(["painel_aberto", "sessao_aberta", "materia"]) |
||||
|
} |
||||
|
}; |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
/* Optional styling */ |
||||
|
</style> |
||||
@ -0,0 +1,36 @@ |
|||||
|
<template> |
||||
|
<div class="col-md-6 text-center painel" id="aparecer_oradores" v-if="canRender"> |
||||
|
<h2 class="text-subtitle">Oradores</h2> |
||||
|
<table id="oradores_list"> |
||||
|
<tr v-for="o in oradores" :key="o.ordem_pronunciamento"><td style="padding-right:20px; color:white"> |
||||
|
{{ o.ordem_pronunciamento }}º   {{ o.nome_parlamentar }}</td> |
||||
|
</tr> |
||||
|
</table> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import { mapState } from 'vuex'; |
||||
|
export default { |
||||
|
name: 'PainelOradores', |
||||
|
data() { |
||||
|
return { |
||||
|
// oradores: [], |
||||
|
}; |
||||
|
}, |
||||
|
mounted() { |
||||
|
console.log('PainelOradores mounted'); |
||||
|
}, |
||||
|
beforeDestroy() {}, |
||||
|
computed: { |
||||
|
canRender () { |
||||
|
return this.sessao_aberta && this.painel_aberto; |
||||
|
}, |
||||
|
...mapState(["painel_aberto", "sessao_aberta", "oradores"]) |
||||
|
} |
||||
|
}; |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
/* Optional styling */ |
||||
|
</style> |
||||
@ -0,0 +1,41 @@ |
|||||
|
<template> |
||||
|
<div class="col-md-4" v-if="canRender"> |
||||
|
<div class="text-center painel"> |
||||
|
<h2 class="text-subtitle">Parlamentares</h2> |
||||
|
<span id="parlamentares" class="text-value text-center"></span> |
||||
|
<table id="parlamentares_list"> |
||||
|
<tr v-for="p in parlamentares" :key="p.parlamentar_id" class="text-value text-center"> |
||||
|
<td style="padding-right:20px; color:yellow" > {{ p.nome_parlamentar }}</td> |
||||
|
<td style="padding-right:20px; color:yellow"> {{ p.filiacao }}</td> |
||||
|
<td style="padding-right:20px; color:yellow" v-if="mostrar_voto">{{ p.voto }}</td> |
||||
|
</tr> |
||||
|
</table> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import { mapState } from 'vuex'; |
||||
|
export default { |
||||
|
name: 'PainelParlamentares', |
||||
|
data() { |
||||
|
return { |
||||
|
//parlamentares: [], |
||||
|
}; |
||||
|
}, |
||||
|
mounted() { |
||||
|
console.log('PainelParlamentares mounted'); |
||||
|
}, |
||||
|
beforeDestroy() {}, |
||||
|
computed: { |
||||
|
canRender () { |
||||
|
return this.sessao_aberta && this.painel_aberto; |
||||
|
}, |
||||
|
...mapState(["painel_aberto", "sessao_aberta", "parlamentares", "mostrar_voto"]) |
||||
|
}, |
||||
|
}; |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
/* Optional styling */ |
||||
|
</style> |
||||
@ -0,0 +1,69 @@ |
|||||
|
<template> |
||||
|
<div class="col-md-6 text-left painel" id="resultado_votacao_div" v-if="canRender"> |
||||
|
<div class="d-flex align-items-left justify-content-left mb-2"> |
||||
|
<h2 class="text-subtitle mb-0">Resultado</h2> |
||||
|
<button class="btn btn-sm btn-secondary ms-2" v-on:click="changeFontSize(-1)"> |
||||
|
A- |
||||
|
</button> |
||||
|
<button class="btn btn-sm btn-secondary ms-2" v-on:click="changeFontSize(1)"> |
||||
|
A+ |
||||
|
</button> |
||||
|
</div> |
||||
|
<div ref="votacao" id="box_votacao"> |
||||
|
<div id="votacao" class="text-value"> |
||||
|
<li>Sim: {{ resultado.numero_votos.votos_sim }}</li> |
||||
|
<li>Não: {{ resultado.numero_votos.votos_nao }}</li> |
||||
|
<li>Abstenções: {{ resultado.numero_votos.abstencoes }}</li> |
||||
|
<li>Presentes: {{ resultado.numero_votos.num_presentes }}</li> |
||||
|
<li>Total votos: {{ resultado.numero_votos.total_votos }}</li> |
||||
|
</div> |
||||
|
<div id="resultado_votacao" class="text-title">{{ resultado.resultado_votacao }}</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import { mapState } from 'vuex'; |
||||
|
export default { |
||||
|
name: 'PainelResultado', |
||||
|
data() { |
||||
|
return { |
||||
|
/* |
||||
|
resultado: { |
||||
|
numero_votos: { |
||||
|
votos_sim: 0, |
||||
|
votos_nao: 0, |
||||
|
abstencoes: 0, |
||||
|
total_votos: 0, |
||||
|
num_presentes: 0, |
||||
|
}, |
||||
|
resultado_votacao: '', |
||||
|
} |
||||
|
*/ |
||||
|
}; |
||||
|
}, |
||||
|
mounted() { |
||||
|
console.log('PainelResultado mounted'); |
||||
|
}, |
||||
|
beforeDestroy() {}, |
||||
|
computed: { |
||||
|
canRender () { |
||||
|
return this.sessao_aberta && this.painel_aberto; |
||||
|
}, |
||||
|
...mapState(["painel_aberto", "sessao_aberta", "resultado"]) |
||||
|
}, |
||||
|
methods: { |
||||
|
changeFontSize(value) { |
||||
|
const el = this.$refs.votacao; |
||||
|
if (!el) return; |
||||
|
let fontSize = window.getComputedStyle(el).fontSize; |
||||
|
fontSize = parseFloat(fontSize); // safely convert "16px" → 16 |
||||
|
el.style.fontSize = (fontSize + value) + 'px'; |
||||
|
}, |
||||
|
} |
||||
|
}; |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
/* Optional styling */ |
||||
|
</style> |
||||
@ -0,0 +1,11 @@ |
|||||
|
<template> |
||||
|
<div class="VotacaoNominal"> |
||||
|
<pre v-text="$attrs"/> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
export default { |
||||
|
name: 'Votacaonominal' |
||||
|
}; |
||||
|
</script> |
||||
File diff suppressed because it is too large
@ -0,0 +1,21 @@ |
|||||
|
# sapl/asgi.py |
||||
|
import os |
||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sapl.settings") |
||||
|
|
||||
|
import django |
||||
|
django.setup() |
||||
|
|
||||
|
from channels.routing import ProtocolTypeRouter, URLRouter |
||||
|
from channels.auth import AuthMiddlewareStack |
||||
|
from channels.http import AsgiHandler # Django 2.2 uses AsgiHandler, not django.core.asgi |
||||
|
from django.urls import path |
||||
|
|
||||
|
from sapl.painel.consumers import PainelConsumer, HealthConsumer |
||||
|
|
||||
|
application = ProtocolTypeRouter({ |
||||
|
"http": AsgiHandler(), |
||||
|
"websocket": AuthMiddlewareStack(URLRouter([ |
||||
|
# path("ws/painel/", PainelConsumer.as_asgi()), |
||||
|
path("ws/painel/<int:controller_id>/", PainelConsumer.as_asgi()), |
||||
|
])), |
||||
|
}) |
||||
@ -0,0 +1,54 @@ |
|||||
|
|
||||
|
# Websockets |
||||
|
|
||||
|
Rodar o container antes de iniciar o SAPL: |
||||
|
|
||||
|
```commandline |
||||
|
sudo docker run --rm -p 6379:6379 redis:7-alpine redis-server --save "" --appendonly no |
||||
|
``` |
||||
|
|
||||
|
Executar o SAPL |
||||
|
|
||||
|
Instalar dependências do Websockets e Redis: |
||||
|
|
||||
|
```commandline |
||||
|
pip install -r requirements/dev-requirements.txt |
||||
|
``` |
||||
|
|
||||
|
Abrir um terminal e rodar `yarn` para servir as páginas VueJS: |
||||
|
|
||||
|
```commandline |
||||
|
yarn serve |
||||
|
``` |
||||
|
|
||||
|
Executar o SAPL (duas formas): |
||||
|
|
||||
|
DAPHNE: |
||||
|
|
||||
|
Em outro terminal, no diretório raiz, execute como Daphne abaixo: |
||||
|
```commandline |
||||
|
daphne -b 127.0.0.1 -p 8000 sapl.asgi:application |
||||
|
``` |
||||
|
|
||||
|
Daphne é excelente para debugar a parte de WebSockets, pois contém melhores mensagens de erro e log. |
||||
|
O runserver geralmente só vai dar crash ou falhar ao enviar as mensagens via WebSocket. |
||||
|
|
||||
|
**MAS atenção: Daphne não faz reload automático após mudanças na página!** |
||||
|
|
||||
|
Para isso é que parar e reiniciar o Daphne ou usar `.manage.py runserver` |
||||
|
|
||||
|
|
||||
|
RUNSERVER: |
||||
|
Em outro terminal, no diretório raiz, execute como Daphne abaixo para debugar os websockets (melhores mensagens de log) |
||||
|
```commandline |
||||
|
./manage.py runserver |
||||
|
``` |
||||
|
|
||||
|
Logar no SAPL e acessar a página `http://localhost:8000/painel/v2` |
||||
|
|
||||
|
|
||||
|
Ferramentas: |
||||
|
|
||||
|
`wscat`: permite fazer chamadas a websockets via CLI, mas para acessar o WS endpoint do painel precisa estar autenticado. |
||||
|
|
||||
|
**Redis Insight:** GUI que é tipo um pgAdmin para o Redis; |
||||
@ -0,0 +1,201 @@ |
|||||
|
import json |
||||
|
import json |
||||
|
import logging |
||||
|
import time |
||||
|
|
||||
|
from channels.db import database_sync_to_async |
||||
|
from channels.generic.websocket import AsyncWebsocketConsumer, AsyncJsonWebsocketConsumer |
||||
|
|
||||
|
from sapl.base.models import CasaLegislativa, AppConfig |
||||
|
from sapl.sessao.models import SessaoPlenaria, SessaoPresencasView, \ |
||||
|
SessaoOradoresView, SessaoMateriasVotacoesView |
||||
|
|
||||
|
|
||||
|
def get_materia_votacao(votacao): |
||||
|
return { |
||||
|
# TOO UGLY! FIX THIS if-else |
||||
|
"materia_id": votacao.materia.id if votacao and votacao.materia else "", |
||||
|
"texto": str(votacao.materia) if votacao and votacao.materia else "", |
||||
|
"ementa": votacao.materia.ementa if votacao and votacao.materia and votacao.materia.ementa else "", |
||||
|
"resultado": { |
||||
|
"resultado_votacao": votacao.resultado_votacao if votacao and votacao.resultado_votacao else "", |
||||
|
"resultado": votacao.resultado if votacao and votacao.resultado else "", |
||||
|
"numero_votos": votacao.numero_votos if votacao and votacao.numero_votos else {}, |
||||
|
"votos_parlamentares": votacao.votos_parlamentares if votacao and votacao.votos_parlamentares else [], |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
|
||||
|
def get_dados_painel(sessao_plenaria_id: int) -> dict: |
||||
|
app_config = AppConfig.objects.first() |
||||
|
sessao = SessaoPlenaria.objects.get(id=sessao_plenaria_id) |
||||
|
casa = CasaLegislativa.objects.first() |
||||
|
brasao = casa.logotipo.url \ |
||||
|
if app_config.mostrar_brasao_painel else None |
||||
|
|
||||
|
# Painel |
||||
|
# TODO: recuperar outra matéria quando não existir nenhuma materia_votacao aberta! |
||||
|
materia_votacao = SessaoMateriasVotacoesView.objects. \ |
||||
|
filter(sessao_plenaria_id=sessao_plenaria_id, votacao_aberta=True).first() |
||||
|
|
||||
|
if not materia_votacao: |
||||
|
return { |
||||
|
"type": "data", |
||||
|
"sessao_aberta": sessao.iniciada and not sessao.finalizada, |
||||
|
"painel_aberto": sessao.painel_aberto, |
||||
|
"mostrar_voto": app_config.mostrar_voto, |
||||
|
"message": "PAINEL ENCONTRA-SE FECHADO" if not sessao.painel_aberto else "", |
||||
|
"sessao": { |
||||
|
"sessao_plenaria_id": sessao.id, |
||||
|
"brasao": brasao, |
||||
|
"sessao_plenaria": str(sessao), |
||||
|
"sessao_plenaria_data": sessao.data_inicio.strftime("%d/%m/%Y"), |
||||
|
"sessao_plenaria_hora_inicio": sessao.hora_inicio, |
||||
|
"sessao_solene": sessao.tipo.nome == "Solene", |
||||
|
"sessao_finalizada": sessao.finalizada, |
||||
|
"tema_solene": sessao.tema_solene, |
||||
|
}, |
||||
|
"message": "Nenhuma matéria aberta para votação!", |
||||
|
} |
||||
|
|
||||
|
presentes = SessaoPresencasView.objects.filter(sessao_plenaria_id=sessao_plenaria_id, |
||||
|
etapa_sessao=materia_votacao.etapa_sessao, |
||||
|
ativo=True).values_list( |
||||
|
'parlamentar_id', |
||||
|
'nome_parlamentar', |
||||
|
'filiacao', ) |
||||
|
parlamentares = [dict(zip(['parlamentar_id', 'nome_parlamentar', 'filiacao'], p)) for p in presentes] |
||||
|
if materia_votacao and materia_votacao.numero_votos: |
||||
|
materia_votacao.numero_votos.update({"num_presentes": len(parlamentares)}) |
||||
|
|
||||
|
oradores = SessaoOradoresView.objects.filter(sessao_plenaria_id=sessao_plenaria_id, |
||||
|
etapa_sessao=materia_votacao.etapa_sessao).values_list( |
||||
|
'ordem_pronunciamento', |
||||
|
'nome_parlamentar', |
||||
|
) |
||||
|
oradores = [dict(zip(['ordem_pronunciamento', 'nome_parlamentar'], o)) for o in oradores] |
||||
|
|
||||
|
# TODO: recover stopwatch state from DB/Cache |
||||
|
stopwatch = { |
||||
|
"type": "stopwatch.state", |
||||
|
"id": "sw:main", |
||||
|
"status": "running", # "running" | "paused" | "stopped" |
||||
|
"started_at_ms": 1699990000123, # epoch ms when (re)started |
||||
|
"elapsed_ms": 5320 |
||||
|
} |
||||
|
|
||||
|
dados_sessao = { |
||||
|
"type": "data", |
||||
|
"sessao_aberta": sessao.iniciada and not sessao.finalizada, |
||||
|
"painel_aberto": sessao.painel_aberto, |
||||
|
"mostrar_voto": app_config.mostrar_voto, |
||||
|
"message": "PAINEL ENCONTRA-SE FECHADO" if not sessao.painel_aberto else "", |
||||
|
"sessao": { |
||||
|
"sessao_plenaria_id": sessao.id, |
||||
|
"brasao": brasao, |
||||
|
"sessao_plenaria": str(sessao), |
||||
|
"sessao_plenaria_data": sessao.data_inicio.strftime("%d/%m/%Y"), |
||||
|
"sessao_plenaria_hora_inicio": sessao.hora_inicio, |
||||
|
"sessao_solene": sessao.tipo.nome == "Solene", |
||||
|
"sessao_finalizada": sessao.finalizada, |
||||
|
"tema_solene": sessao.tema_solene, |
||||
|
}, |
||||
|
"parlamentares": parlamentares, |
||||
|
"oradores": oradores, |
||||
|
"materia": get_materia_votacao(materia_votacao), |
||||
|
"stopwatch": [stopwatch], # TODO: array of stopwatches |
||||
|
} |
||||
|
|
||||
|
print(json.dumps(dados_sessao, indent=4)) |
||||
|
return dados_sessao |
||||
|
|
||||
|
|
||||
|
class PainelConsumer(AsyncJsonWebsocketConsumer): |
||||
|
logger = logging.getLogger(__name__) |
||||
|
|
||||
|
# def __init__(self): |
||||
|
# self.group = set() |
||||
|
# self.controller_id = None |
||||
|
|
||||
|
async def connect(self): |
||||
|
# TODO: transformar prints em log messages |
||||
|
user = self.scope.get("user") |
||||
|
controller_id = self.scope["url_route"]["kwargs"]["controller_id"] |
||||
|
print(f"user: {user}, controller_id: {controller_id}") |
||||
|
|
||||
|
if not (user and user.is_authenticated): |
||||
|
self.logger.info(f"{user} is not authenticated user!") |
||||
|
await self.close(code=4401) # explicit, graceful close |
||||
|
return |
||||
|
|
||||
|
if not await self.user_can_view(user.id, controller_id): |
||||
|
await self.close(code=4403) |
||||
|
return |
||||
|
|
||||
|
self.controller_id = controller_id |
||||
|
self.group = f"controller_{controller_id}" |
||||
|
print(self.group) |
||||
|
await self.channel_layer.group_add(self.group, self.channel_name) |
||||
|
|
||||
|
await self.accept() |
||||
|
# await self.send_json({"type": "data", |
||||
|
# "text": "Connection established!"}) |
||||
|
|
||||
|
print("SENDING bootstrap DATA DO CONSUMER ") |
||||
|
print(get_dados_painel(controller_id)) |
||||
|
await self.send_json(get_dados_painel(controller_id)) |
||||
|
|
||||
|
async def disconnect(self, code): |
||||
|
if hasattr(self, "group"): |
||||
|
await self.channel_layer.group_discard(self.group, self.channel_name) |
||||
|
|
||||
|
# Called by server via channel_layer.group_send |
||||
|
# async def notify(self, event): |
||||
|
# # event: {"type": "notify", "data": {...}} |
||||
|
# await self.send_json(event) |
||||
|
|
||||
|
async def receive(self, text_data=None, bytes_data=None): |
||||
|
data = json.loads(text_data or "{}") |
||||
|
print("Received from client:", data) # TODO: turn into log messages |
||||
|
msg_type = data.get("type") |
||||
|
if msg_type == "ping": |
||||
|
print("PING") |
||||
|
await self.send_json({"type": "ping", "ts": time.time()}) |
||||
|
return |
||||
|
elif msg_type == "echo": |
||||
|
await self.send_json(data) |
||||
|
return |
||||
|
elif msg_type == "stopwatch.state": |
||||
|
print(data) |
||||
|
return |
||||
|
await self.send_json({"type": "error", "message": "Misformed message"}) |
||||
|
return |
||||
|
|
||||
|
async def stopwatch_update(self, event): |
||||
|
await self.send_json(event) |
||||
|
return |
||||
|
|
||||
|
@database_sync_to_async |
||||
|
def user_can_view(self, user_id, controller_id) -> bool: |
||||
|
# Replace with your ACL check (ORM must be in a sync wrapper) |
||||
|
# return Controller.objects.filter(id=controller_id, owners__id=user_id).exists() |
||||
|
return True |
||||
|
|
||||
|
|
||||
|
class HealthConsumer(AsyncWebsocketConsumer): |
||||
|
""" |
||||
|
WebSockets consumer that doesn"t require authentication to |
||||
|
debug with wscat (wscat -c ws://127.0.0.1:8000/ws/painel/) |
||||
|
""" |
||||
|
logger = logging.getLogger(__name__) |
||||
|
|
||||
|
async def connect(self): |
||||
|
try: |
||||
|
await self.accept() |
||||
|
await self.send(json.dumps({"ok": True})) |
||||
|
except Exception as e: |
||||
|
self.logger.exception("connect failed: %s", e) |
||||
|
# Let Channels close with 1011 if we got here |
||||
|
|
||||
|
async def receive(self, text_data=None, bytes_data=None): |
||||
|
await self.send(text_data or "") |
||||
@ -0,0 +1,265 @@ |
|||||
|
# Generated by Django 2.2.28 on 2025-11-17 17:55 |
||||
|
|
||||
|
from django.db import migrations |
||||
|
|
||||
|
|
||||
|
class Migration(migrations.Migration): |
||||
|
dependencies = [ |
||||
|
('sessao', '0069_auto_20220919_1705'), |
||||
|
] |
||||
|
|
||||
|
operations = [ |
||||
|
migrations.RunSQL( |
||||
|
""" |
||||
|
CREATE OR REPLACE VIEW sessao_presencas_view AS |
||||
|
-- |
||||
|
-- PRESENCAS DE PARLAMENTARES COM FILIACAO (SE HOUVER) |
||||
|
-- MANDATO DEVE ESTAR ATIVO NA LEGISLATIURA |
||||
|
-- PARLAMENTAR DEVE ESTAR ATIVO |
||||
|
-- |
||||
|
-- EXPEDIENTE |
||||
|
SELECT |
||||
|
p.id, |
||||
|
presenca.sessao_plenaria_id, |
||||
|
'expediente' AS etapa_sessao, |
||||
|
p.id as parlamentar_id, |
||||
|
p.nome_parlamentar as nome_parlamentar, |
||||
|
COALESCE(af.sigla, 'SEM PARTIDO') AS filiacao, |
||||
|
p.ativo |
||||
|
FROM sessao_sessaoplenariapresenca AS presenca |
||||
|
JOIN sessao_sessaoplenaria AS sp |
||||
|
ON presenca.sessao_plenaria_id = sp.id |
||||
|
JOIN parlamentares_parlamentar AS p |
||||
|
ON presenca.parlamentar_id = p.id |
||||
|
JOIN parlamentares_mandato AS m |
||||
|
ON p.id = m.parlamentar_id |
||||
|
AND m.legislatura_id = sp.legislatura_id |
||||
|
AND m.data_inicio_mandato <= sp.data_inicio |
||||
|
AND (m.data_fim_mandato IS NULL OR m.data_fim_mandato >= sp.data_inicio) |
||||
|
-- recupera uma filiacao partidaria, se existir |
||||
|
LEFT JOIN LATERAL ( |
||||
|
SELECT pa.sigla |
||||
|
FROM parlamentares_filiacao f |
||||
|
JOIN parlamentares_partido pa ON pa.id = f.partido_id |
||||
|
WHERE f.parlamentar_id = p.id |
||||
|
AND f.data <= sp.data_inicio |
||||
|
AND (f.data_desfiliacao IS NULL OR f.data_desfiliacao >= sp.data_inicio) |
||||
|
ORDER BY f.data DESC |
||||
|
LIMIT 1 |
||||
|
) AS af ON TRUE |
||||
|
WHERE p.ativo = TRUE |
||||
|
|
||||
|
UNION ALL |
||||
|
|
||||
|
-- ORDEM DO DIA |
||||
|
SELECT |
||||
|
p.id, |
||||
|
presenca.sessao_plenaria_id, |
||||
|
'ordemdia' AS etapa_sessao, |
||||
|
p.id as parlamentar_id, |
||||
|
p.nome_parlamentar as nome_parlamentar, |
||||
|
COALESCE(af.sigla, 'SEM PARTIDO') AS filiacao, |
||||
|
p.ativo |
||||
|
FROM sessao_presencaordemdia AS presenca |
||||
|
JOIN sessao_sessaoplenaria AS sp |
||||
|
ON presenca.sessao_plenaria_id = sp.id |
||||
|
JOIN parlamentares_parlamentar AS p |
||||
|
ON presenca.parlamentar_id = p.id |
||||
|
JOIN parlamentares_mandato AS m |
||||
|
ON p.id = m.parlamentar_id |
||||
|
AND m.legislatura_id = sp.legislatura_id |
||||
|
AND m.data_inicio_mandato <= sp.data_inicio |
||||
|
AND (m.data_fim_mandato IS NULL OR m.data_fim_mandato >= sp.data_inicio) |
||||
|
-- recupera uma filiacao partidaria, se existir |
||||
|
LEFT JOIN LATERAL ( |
||||
|
SELECT pa.sigla |
||||
|
FROM parlamentares_filiacao f |
||||
|
JOIN parlamentares_partido pa ON pa.id = f.partido_id |
||||
|
WHERE f.parlamentar_id = p.id |
||||
|
AND f.data <= sp.data_inicio |
||||
|
AND (f.data_desfiliacao IS NULL OR f.data_desfiliacao >= sp.data_inicio) |
||||
|
ORDER BY f.data DESC |
||||
|
LIMIT 1 |
||||
|
) AS af ON TRUE |
||||
|
WHERE p.ativo = TRUE |
||||
|
ORDER BY sessao_plenaria_id, etapa_sessao, nome_parlamentar |
||||
|
""" |
||||
|
), |
||||
|
migrations.RunSQL( |
||||
|
""" |
||||
|
-- |
||||
|
-- ORADORES DE ORDEMDIA/EXPEDIENTE |
||||
|
-- * PARLAMENTARES ATIVOS |
||||
|
-- |
||||
|
CREATE OR REPLACE VIEW sessao_oradores_view AS |
||||
|
SELECT od.id, |
||||
|
sessao_plenaria_id, |
||||
|
'ordemdia' as etapa_sessao, |
||||
|
numero_ordem as ordem_pronunciamento, |
||||
|
parlamentar_id, |
||||
|
nome_parlamentar, |
||||
|
COALESCE(af.sigla, 'SEM PARTIDO') AS filiacao |
||||
|
FROM sessao_oradorordemdia od |
||||
|
JOIN sessao_sessaoplenaria sp ON (od.sessao_plenaria_id = sp.id) |
||||
|
JOIN parlamentares_parlamentar p ON (od.parlamentar_id = p.id) |
||||
|
-- recupera uma filiacao partidaria, se existir |
||||
|
LEFT JOIN LATERAL ( |
||||
|
SELECT pa.sigla |
||||
|
FROM parlamentares_filiacao f |
||||
|
JOIN parlamentares_partido pa ON pa.id = f.partido_id |
||||
|
WHERE f.parlamentar_id = p.id |
||||
|
AND f.data <= sp.data_inicio |
||||
|
AND (f.data_desfiliacao IS NULL OR f.data_desfiliacao >= sp.data_inicio) |
||||
|
ORDER BY f.data DESC |
||||
|
LIMIT 1 |
||||
|
) AS af ON TRUE |
||||
|
AND p.ativo = TRUE |
||||
|
UNION ALL |
||||
|
SELECT ex.id, |
||||
|
sessao_plenaria_id, |
||||
|
'expediente' as etapa_sessao, |
||||
|
numero_ordem as ordem_pronunciamento, |
||||
|
parlamentar_id, |
||||
|
nome_parlamentar, |
||||
|
COALESCE(af.sigla, 'SEM PARTIDO') AS filiacao |
||||
|
FROM sessao_oradorexpediente ex |
||||
|
JOIN sessao_sessaoplenaria sp ON (ex.sessao_plenaria_id = sp.id) |
||||
|
JOIN parlamentares_parlamentar p on (ex.parlamentar_id = p.id) |
||||
|
-- recupera uma filiacao partidaria, se existir |
||||
|
LEFT JOIN LATERAL ( |
||||
|
SELECT pa.sigla |
||||
|
FROM parlamentares_filiacao f |
||||
|
JOIN parlamentares_partido pa ON pa.id = f.partido_id |
||||
|
WHERE f.parlamentar_id = p.id |
||||
|
AND f.data <= sp.data_inicio |
||||
|
AND (f.data_desfiliacao IS NULL OR f.data_desfiliacao >= sp.data_inicio) |
||||
|
ORDER BY f.data DESC |
||||
|
LIMIT 1 |
||||
|
) AS af ON TRUE |
||||
|
AND p.ativo = TRUE |
||||
|
ORDER BY sessao_plenaria_id, etapa_sessao, ordem_pronunciamento |
||||
|
""" |
||||
|
), |
||||
|
migrations.RunSQL( |
||||
|
""" |
||||
|
CREATE OR REPLACE VIEW sessao_materias_votacoes_view AS |
||||
|
-- |
||||
|
-- Votacao/Leitura de Materias |
||||
|
-- |
||||
|
-- EXPEDIENTE |
||||
|
WITH votacao_materias AS ( |
||||
|
SELECT em.sessao_plenaria_id, |
||||
|
em.id id, |
||||
|
'expediente' etapa_sessao, |
||||
|
em.numero_ordem, |
||||
|
em.materia_id, |
||||
|
tm.descricao||' nº '||ml.numero||' de '||ml.ano as materia_texto, |
||||
|
ml.ementa materia_ementa, |
||||
|
tipo_votacao, |
||||
|
CASE tipo_votacao |
||||
|
WHEN 1 THEN 'Simbólica' |
||||
|
WHEN 2 THEN 'Nominal' |
||||
|
WHEN 3 THEN 'Secreta' |
||||
|
WHEN 4 THEN 'Leitura' |
||||
|
ELSE '' |
||||
|
END as tipo_votacao_descricao, |
||||
|
resultado_votacao, |
||||
|
em.resultado, |
||||
|
numero_votos, |
||||
|
votos_parlamentares, |
||||
|
votacao_aberta |
||||
|
FROM sessao_expedientemateria em |
||||
|
JOIN materia_materialegislativa ml ON (em.materia_id = ml.id) |
||||
|
JOIN materia_tipomaterialegislativa tm ON (ml.tipo_id = tm.id) |
||||
|
LEFT JOIN sessao_registroleitura rl on (rl.expediente_id = em.id) |
||||
|
LEFT JOIN LATERAL ( |
||||
|
SELECT jsonb_build_object( |
||||
|
'votos_sim', coalesce(rv.numero_votos_sim, 0), |
||||
|
'votos_nao', coalesce(rv.numero_votos_nao, 0), |
||||
|
'abstencoes', coalesce(rv.numero_abstencoes, 0), |
||||
|
'total_votos', coalesce(rv.numero_votos_sim, 0) + coalesce(rv.numero_votos_nao, 0) + coalesce(rv.numero_abstencoes, 0) |
||||
|
) as numero_votos, |
||||
|
trv.nome resultado_votacao |
||||
|
FROM sessao_registrovotacao rv |
||||
|
JOIN sessao_tiporesultadovotacao trv on (rv.tipo_resultado_votacao_id = trv.id) |
||||
|
WHERE rv.expediente_id = em.id AND tipo_votacao != 4) rv ON TRUE |
||||
|
LEFT JOIN LATERAL ( |
||||
|
SELECT em.sessao_plenaria_id, |
||||
|
em.numero_ordem, |
||||
|
jsonb_agg(jsonb_build_object( |
||||
|
vp.parlamentar_id, |
||||
|
jsonb_build_object( |
||||
|
'materia_id', em.materia_id, |
||||
|
'parlamentar_id', vp.parlamentar_id, |
||||
|
'parlamentar_nome', p.nome_parlamentar, |
||||
|
'voto', vp.voto |
||||
|
)) ORDER BY p.nome_parlamentar) as votos_parlamentares |
||||
|
FROM sessao_votoparlamentar vp |
||||
|
JOIN parlamentares_parlamentar p ON (vp.parlamentar_id = p.id) |
||||
|
WHERE vp.expediente_id = em.id AND em.tipo_votacao != 4 |
||||
|
GROUP BY em.sessao_plenaria_id, em.numero_ordem |
||||
|
ORDER BY em.sessao_plenaria_id, em.numero_ordem |
||||
|
) vp ON TRUE |
||||
|
|
||||
|
UNION ALL |
||||
|
|
||||
|
-- ORDEM DIA |
||||
|
SELECT od.sessao_plenaria_id, |
||||
|
od.id id, |
||||
|
'ordemdia' etapa_sessao, |
||||
|
od.numero_ordem, |
||||
|
od.materia_id, |
||||
|
tm.descricao||' nº '||ml.numero||' de '||ml.ano as materia_texto, |
||||
|
ml.ementa, |
||||
|
tipo_votacao, |
||||
|
CASE tipo_votacao |
||||
|
WHEN 1 THEN 'Simbólica' |
||||
|
WHEN 2 THEN 'Nominal' |
||||
|
WHEN 3 THEN 'Secreta' |
||||
|
WHEN 4 THEN 'Leitura' |
||||
|
ELSE '' |
||||
|
END as tipo_votacao_descricao, |
||||
|
resultado_votacao, |
||||
|
od.resultado, |
||||
|
numero_votos, |
||||
|
votos_parlamentares, |
||||
|
votacao_aberta |
||||
|
FROM sessao_ordemdia od |
||||
|
JOIN materia_materialegislativa ml ON (od.materia_id = ml.id) |
||||
|
JOIN materia_tipomaterialegislativa tm ON (ml.tipo_id = tm.id) |
||||
|
LEFT JOIN sessao_registroleitura rl on (od.id = rl.expediente_id) |
||||
|
LEFT JOIN LATERAL ( |
||||
|
SELECT jsonb_build_object( |
||||
|
'votos_sim', coalesce(rv.numero_votos_sim, 0), |
||||
|
'votos_nao', coalesce(rv.numero_votos_nao, 0), |
||||
|
'abstencoes', coalesce(rv.numero_abstencoes, 0), |
||||
|
'total_votos', coalesce(rv.numero_votos_sim, 0) + coalesce(rv.numero_votos_nao, 0) + coalesce(rv.numero_abstencoes, 0) |
||||
|
) as numero_votos, |
||||
|
trv.nome resultado_votacao |
||||
|
FROM sessao_registrovotacao rv |
||||
|
JOIN sessao_tiporesultadovotacao trv on (rv.tipo_resultado_votacao_id = trv.id) |
||||
|
WHERE rv.expediente_id = od.id AND tipo_votacao != 4) rv ON TRUE |
||||
|
LEFT JOIN LATERAL ( |
||||
|
SELECT od.sessao_plenaria_id, |
||||
|
od.numero_ordem, |
||||
|
jsonb_agg(jsonb_build_object( |
||||
|
vp.parlamentar_id, |
||||
|
jsonb_build_object( |
||||
|
'materia_id', od.materia_id, |
||||
|
'parlamentar_id', vp.parlamentar_id, |
||||
|
'parlamentar_nome', p.nome_parlamentar, |
||||
|
'voto', vp.voto |
||||
|
)) ORDER BY p.nome_parlamentar) as votos_parlamentares |
||||
|
FROM sessao_votoparlamentar vp |
||||
|
JOIN parlamentares_parlamentar p ON (vp.parlamentar_id = p.id) |
||||
|
WHERE vp.expediente_id = od.id AND od.tipo_votacao != 4 |
||||
|
GROUP BY od.sessao_plenaria_id, od.numero_ordem |
||||
|
ORDER BY od.sessao_plenaria_id, od.numero_ordem |
||||
|
) vp ON TRUE |
||||
|
) |
||||
|
SELECT * |
||||
|
FROM votacao_materias |
||||
|
ORDER BY sessao_plenaria_id, etapa_sessao, numero_ordem |
||||
|
""" |
||||
|
) |
||||
|
] |
||||
@ -0,0 +1,98 @@ |
|||||
|
{% load i18n %} |
||||
|
{% load common_tags %} |
||||
|
|
||||
|
{% load render_bundle from webpack_loader %} |
||||
|
{% load webpack_static from webpack_loader %} |
||||
|
|
||||
|
<!DOCTYPE HTML> |
||||
|
<!--[if IE 8]> |
||||
|
<html class="no-js lt-ie9" lang="pt-br"> <![endif]--> |
||||
|
<!--[if gt IE 8]><!--> |
||||
|
<html lang="pt-br"> |
||||
|
<!--<![endif]--> |
||||
|
<head> |
||||
|
<meta charset="UTF-8"> |
||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||
|
<!-- TODO: does it need this head_title here? --> |
||||
|
<title>{% block head_title %}{% trans 'SAPL - Sistema de Apoio ao Processo Legislativo' %}{% endblock %}</title> |
||||
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> |
||||
|
|
||||
|
{% block webpack_loader_css %} |
||||
|
{% render_chunk_vendors 'css' %} |
||||
|
{% render_bundle 'global' 'css' %} |
||||
|
{% render_bundle 'painel' 'css' %} |
||||
|
{% endblock webpack_loader_css %} |
||||
|
|
||||
|
|
||||
|
<style type="text/css"> |
||||
|
html, body { |
||||
|
max-width: 100%; |
||||
|
overflow-x: hidden; |
||||
|
} |
||||
|
@media screen { |
||||
|
ul, li { |
||||
|
list-style-type: none; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
</style> |
||||
|
</head> |
||||
|
<body class="painel-principal"> |
||||
|
|
||||
|
{% block vue_content %} |
||||
|
|
||||
|
<style>[v-cloak]{display:none}</style> |
||||
|
<div id="painel" v-cloak class="col text-center" data-controller-id="{{ controller_id }}"> |
||||
|
|
||||
|
<painel-header ref="painelHeader"></painel-header> |
||||
|
|
||||
|
<div class="row justify-content-center"> |
||||
|
|
||||
|
<div class="d-flex justify-content-start"> |
||||
|
<painel-parlamentares ref="parlamentares"></painel-parlamentares> |
||||
|
|
||||
|
<div class="d-flex col-md-8 painels"> |
||||
|
<painel-oradores ref="oradores"></painel-oradores> |
||||
|
|
||||
|
<div class="col-md-6 text-left painel" v-if="canRender"> |
||||
|
<div class="d-flex align-items-left justify-content-left mb-2"> |
||||
|
<h2 class="text-subtitle mb-0">Cronômetros</h2> |
||||
|
</div> |
||||
|
<div class="text-value" id="box_cronometros"> |
||||
|
<button class="btn btn-sm btn-secondary ms-2" v-on:click="changeFontSize(-1)"> |
||||
|
A- |
||||
|
</button> |
||||
|
<button class="btn btn-sm btn-secondary ms-2" v-on:click="changeFontSize(1)"> |
||||
|
A+ |
||||
|
</button> |
||||
|
<painel-cronometro ref="sw:discurso" id="sw:discurso" title="Discurso"></painel-cronometro> |
||||
|
<painel-cronometro ref="sw:aparte" id="sw:aparte" title="Aparte"></painel-cronometro> |
||||
|
<painel-cronometro ref="sw:questao" id="sw:questao" title="Questão de Ordem"></painel-cronometro> |
||||
|
<painel-cronometro ref="sw:consideracoes" id="sw:consideracoes" title="Considerações Finais"></painel-cronometro> |
||||
|
</div> |
||||
|
</div> |
||||
|
<!-- <painel-cronometro-list ref="cronometro_list"></painel-cronometro-list>--> |
||||
|
|
||||
|
<painel-resultado ref="resultado"></painel-resultado> |
||||
|
|
||||
|
<painel-materia ref="materia"></painel-materia> |
||||
|
|
||||
|
<div class="col-md-6 text-center painel" id="tema_solene_div" style="display: none"> |
||||
|
<h2 class="text-subtitle">Tema da Sessão Solene</h2> |
||||
|
<span id="sessao_solene_tema" class="text-value"></span> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
{% endblock %} |
||||
|
</body> |
||||
|
{% block webpack_loader_js %} |
||||
|
{% render_chunk_vendors 'js' %} |
||||
|
{% render_bundle 'global' 'js' %} |
||||
|
{% render_bundle 'painel' 'js' %} |
||||
|
{% endblock webpack_loader_js %} |
||||
|
|
||||
|
{% block webpack_loader_chunks_js %} |
||||
|
{% endblock webpack_loader_chunks_js %} |
||||
|
</html> |
||||
@ -0,0 +1,106 @@ |
|||||
|
{% extends "crud/detail.html" %} |
||||
|
{% load i18n %} |
||||
|
{% load staticfiles %} |
||||
|
{% load common_tags %} |
||||
|
{% load render_bundle from webpack_loader %} |
||||
|
{% load webpack_static from webpack_loader %} |
||||
|
|
||||
|
{% block actions %} {% endblock %} |
||||
|
|
||||
|
{% block title %} |
||||
|
{% endblock %} |
||||
|
|
||||
|
{% block vue_content %} |
||||
|
<div id="painel-controle"> |
||||
|
<h1 class="page-header"> |
||||
|
Painel Eletrônico <small>([[ sessao_plenaria ]])</small> |
||||
|
</h1> |
||||
|
[[ message ]] |
||||
|
<div class="row"> |
||||
|
<div class="col-md-6"><a href="" onclick="window.open('{% url 'sapl.painel:painel_principal' pk %}','Comprovante','width=800, height=800, scrollbars=yes'); return false;" class="btn btn-primary btn-sm active">Iniciar painel completo</a></div> |
||||
|
<div class="col-md-3"><button onclick="switch_painel(true)" id="id_abrir_painel" class="btn btn-primary btn-sm active" style="display: none">Abrir Painel</button></div> |
||||
|
<div class="col-md-3"><button onclick="switch_painel(false)" id="id_fechar_painel" class="btn btn-danger btn-sm active" style="display: none;">Fechar Painel</button></div> |
||||
|
</div> |
||||
|
<br /> |
||||
|
|
||||
|
<h1>Operação do Painel Eletrônico</h1> |
||||
|
<h2><span id="relogio"></span></h2> |
||||
|
<br /> |
||||
|
<div class="row"> |
||||
|
<div class="col-md-12 mb-2"><h3>Cronômetro do Discurso</h3></div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="row"> |
||||
|
<div class="col-md-2"><input size="2" id="discurso" name="discurso" value="" readyonly="true" class="form-control"></div> |
||||
|
</div> |
||||
|
<br /> |
||||
|
|
||||
|
<div class="row"> |
||||
|
<div class="col-md-6"><button type="button" id="discursoStart" class="btn btn-success">Iniciar</button></div> |
||||
|
<div class="col-md-6"><button type="button" id="discursoReset" class="btn btn-success">Reiniciar</button></div> |
||||
|
</div> |
||||
|
|
||||
|
<br /><br > |
||||
|
<div class="row"> |
||||
|
<div class="col-md-12 mb-2"><h3>Cronômetro do Aparte</h3></div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="row"> |
||||
|
<div class="col-md-2"><input size="2" id="aparte" name="aparte" value="" readyonly="true" class="form-control"></div> |
||||
|
</div> |
||||
|
<br /> |
||||
|
|
||||
|
<div class="row"> |
||||
|
<div class="col-md-6"><button type="button" id="aparteStart" class="btn btn-success">Iniciar</button></div> |
||||
|
<div class="col-md-6"><button type="button" id="aparteReset" class="btn btn-success" class="btn btn-success">Reiniciar</button></div> |
||||
|
</div> |
||||
|
|
||||
|
<br /><br > |
||||
|
<div class="row"> |
||||
|
<div class="col-md-12 mb-2"><h3>Cronômetro da Questão de Ordem </h3></div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="row"> |
||||
|
<div class="col-md-2"><input size="2" id="ordem" name="ordem" value="" readyonly="true" class="form-control"></div> |
||||
|
</div> |
||||
|
<br /> |
||||
|
|
||||
|
<div class="row"> |
||||
|
<div class="col-md-6"><button type="button" id="ordemStart" class="btn btn-success">Iniciar</button></div> |
||||
|
<div class="col-md-6"><button type="button" id="ordemReset" class="btn btn-success">Reiniciar</button></div> |
||||
|
</div> |
||||
|
|
||||
|
<br/> |
||||
|
<br/> |
||||
|
|
||||
|
<div class="row"> |
||||
|
<div class="col-md-12 mb-2"><h3>Cronômetro de Considerações Finais</h3></div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="row"> |
||||
|
<div class="col-md-4"><input size="2" id="consideracoes" name="consideracoes" value="" readyonly="true" class="form-control"></div> |
||||
|
</div> |
||||
|
<br /> |
||||
|
|
||||
|
<div class="row"> |
||||
|
<div class="col-md-6"><button type="button" id="consideracoesStart" class="btn btn-success">Iniciar</button></div> |
||||
|
<div class="col-md-6"><button type="button" id="consideracoesReset" class="btn btn-success">Reiniciar</button></div> |
||||
|
</div> |
||||
|
<br /><br > |
||||
|
|
||||
|
<div class="row"> |
||||
|
<div class="col-md-6"><button type="button" id="sinalSonoro" class="btn btn-success" onclick="document.getElementById('audio').play();">Sinal Sonoro</button></div> |
||||
|
</div> |
||||
|
</div> |
||||
|
{% endblock vue_content %} |
||||
|
|
||||
|
{% block webpack_loader_css %} |
||||
|
{{ block.super }} |
||||
|
{% render_bundle 'votacao' 'css' %} |
||||
|
{% endblock %} |
||||
|
|
||||
|
{% block webpack_loader_js %} |
||||
|
{% render_chunk_vendors 'js' %} |
||||
|
{% render_bundle 'global' 'js' %} |
||||
|
{% render_bundle 'painel-controle' 'js' %} |
||||
|
{% endblock %} |
||||
@ -0,0 +1,78 @@ |
|||||
|
{% extends "crud/detail.html" %} |
||||
|
{% load i18n %} |
||||
|
{% load crispy_forms_tags cropping%} |
||||
|
{% load common_tags %} |
||||
|
{% load render_bundle from webpack_loader %} |
||||
|
{% load webpack_static from webpack_loader %} |
||||
|
|
||||
|
{% block vue_content %} |
||||
|
<style>[v-cloak]{display:none}</style> |
||||
|
<div id="votacao"> |
||||
|
<fieldset class="form-group"> |
||||
|
<legend>[[ tipo_votacao_descricao ]]</legend> |
||||
|
[[ error_message ]] |
||||
|
|
||||
|
<!-- Component 1 --> |
||||
|
<div> |
||||
|
<b>Matéria:</b> [[ materia ]] |
||||
|
<br/> |
||||
|
<b>Ementa:</b> [[ ementa ]] |
||||
|
<br/> |
||||
|
</div> |
||||
|
<br/> |
||||
|
<!-- Votacao Nominal --> |
||||
|
<fieldset class="form-group" v-if="tipo_votacao == 1"> |
||||
|
<legend><strong>Votos</strong></legend> |
||||
|
<div class="row" v-for="p in parlamentares"> |
||||
|
<div class="col-md-4" id="nome_parlamentar">[[ p.nome_parlamentar ]]</div> |
||||
|
<div class="col-md-2" id="filiacao">[[ p.filiacao ]]</div> |
||||
|
<div class="col-md-4" v-if="edit_votes"> |
||||
|
<span v-for="o in options" :key="o.value"> |
||||
|
<input type="radio" :id="o.value" :value="o.text" :name="p.parlamentar_id" v-model="votos_parlamentares[p.parlamentar_id].voto"/> |
||||
|
<label :for="o.value"> [[ o.text ]]</label> |
||||
|
|
||||
|
</span> |
||||
|
</div> |
||||
|
<div v-else> |
||||
|
[[ votos_parlamentares[p.parlamentar_id].voto ]] |
||||
|
</div> |
||||
|
</div> |
||||
|
<br/> |
||||
|
<br/> |
||||
|
</fieldset> |
||||
|
<legend><strong>Total de votos:</strong></legend> |
||||
|
<div id="total_votos" v-for="voto in total_votos"> |
||||
|
<div class='row'><div class='col-md-12'>[[ voto.tipo ]]: [[ voto.total ]]</div></div> |
||||
|
</div> |
||||
|
<br/> |
||||
|
<!-- component --> |
||||
|
<div class="col-md-6"> |
||||
|
<b>Resultado da Votação: </b> |
||||
|
<select v-model="resultado_selected" class="select form-control" :disabled="disable_resultado" > |
||||
|
<option :key="t.id" :value="t.id" v-for="t in tipos_resultados">[[ t.nome ]]</option> |
||||
|
</select> |
||||
|
</div> |
||||
|
<!-- component --> |
||||
|
<div class="row"> |
||||
|
<div class="col-md-12"> |
||||
|
Observações: |
||||
|
<textarea v-model="observacoes" cols="10" rows="10" class="form-control"></textarea> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="alert alert-info alert-dismissible " role="alert" v-else> |
||||
|
<div>Não existe nenhum parlamentar presente para que a votação ocorra.</div> |
||||
|
</div> |
||||
|
</fieldset> |
||||
|
</div> |
||||
|
{% endblock vue_content %} |
||||
|
|
||||
|
{% block webpack_loader_css %} |
||||
|
{{ block.super }} |
||||
|
{% render_bundle 'votacao' 'css' %} |
||||
|
{% endblock %} |
||||
|
|
||||
|
{% block webpack_loader_js %} |
||||
|
{% render_chunk_vendors 'js' %} |
||||
|
{% render_bundle 'global' 'js' %} |
||||
|
{% render_bundle 'votacao' 'js' %} |
||||
|
{% endblock %} |
||||
@ -0,0 +1,6 @@ |
|||||
|
# DEBUG |
||||
|
daphne -b 127.0.0.1 -p 8000 sapl.asgi:application |
||||
|
|
||||
|
# Redis-CLI |
||||
|
redis-cli -h localhost -p 6379 |
||||
|
|
||||
Loading…
Reference in new issue