Browse Source

Add more apps

websockets-2025
Edward Ribeiro 7 days ago
parent
commit
728bd4fd65
  1. 38
      frontend/src/__apps/painel-controle/main.js
  2. 43
      frontend/src/__apps/painel-controle/scss/painel.scss
  3. 50
      frontend/src/__apps/painel/main.js
  4. 237
      frontend/src/__apps/votacao/main.js
  5. 0
      frontend/src/__apps/votacao/scss/votacao.scss
  6. 0
      frontend/src/components/painel/Cronometro.vue
  7. 0
      frontend/src/components/painel/CronometroList.vue
  8. 3
      frontend/src/components/painel/PainelHeader.vue
  9. 0
      frontend/src/components/painel/PainelMateria.vue
  10. 0
      frontend/src/components/painel/PainelOradores.vue
  11. 2
      frontend/src/components/painel/PainelParlamentares.vue
  12. 0
      frontend/src/components/painel/PainelResultado.vue
  13. 11
      frontend/src/components/votacao/VotacaoNominal.vue
  14. 112
      sapl/painel/consumers.py
  15. 4
      sapl/painel/urls.py
  16. 6
      sapl/painel/views.py
  17. 14
      sapl/sessao/migrations/0070_views_sessao_plenaria.py
  18. 10
      sapl/sessao/urls.py
  19. 1
      sapl/templates/painel/painel_v2.html
  20. 106
      sapl/templates/sessao/painel_v2.html
  21. 78
      sapl/templates/sessao/votacao/votacao_v2.html
  22. 10
      vue.config.js

38
frontend/src/__apps/painel-controle/main.js

@ -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!")
}
})

43
frontend/src/__apps/painel-controle/scss/painel.scss

@ -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;
}
}
}

50
frontend/src/__apps/painel/main.js

@ -6,13 +6,13 @@ import Vuex from 'vuex'
import { mapState } from 'vuex';
import { mapMutations } from 'vuex'
import Cronometro from '../../components/Cronometro.vue'
import CronometroList from '../../components/CronometroList.vue'
import PainelHeader from '../../components/PainelHeader.vue'
import PainelParlamentares from '../../components/PainelParlamentares.vue'
import PainelOradores from '../../components/PainelOradores.vue'
import PainelMateria from '../../components/PainelMateria.vue'
import PainelResultado from '../../components/PainelResultado.vue'
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
@ -49,11 +49,13 @@ const store = new Vuex.Store({
state.parlamentares = parlamentares;
},
updateParlamentares(state, votos_parlamentares) {
state.parlamentares.forEach((p)=>{
if (p.parlamentar_id in votos_parlamentares) {
p.voto = votos_parlamentares[p.parlamentar_id].voto
}
});
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;
@ -85,17 +87,6 @@ new Vue({
data() {
return {
controllerId: null,
// TODO: state here is really needed?
state: {
sessao_plenaria: '',
sessao_plenaria_data: '',
sessao_plenaria_hora_inicio: '',
votacao: [],
parlamentares: [],
oradores: [],
materia: {},
resultado: {},
},
ws: null,
isOpen: false,
error: null,
@ -104,6 +95,7 @@ new Vue({
}
},
mounted() {
console.log('Painel principal mounted!')
// $el is guaranteed here
const el = this.$el
// prefer data-attr; fallback to global if you set it
@ -206,12 +198,12 @@ new Vue({
}
// RESULTADO
if (data.resultado) {
this.setResultado(data.resultado);
if (data.materia.resultado) {
this.setResultado(data.materia.resultado);
}
if (data.votos_parlamentares) {
this.updateParlamentares(data.votos_parlamentares);
if (data.materia.resultado.votos_parlamentares) {
this.updateParlamentares(data.materia.resultado.votos_parlamentares);
}
} catch (e) {
console.error('Error', e);
@ -225,12 +217,12 @@ new Vue({
this.isOpen = true;
this.error = null
console.log(`✅ WebSocket connected: ${url}`);
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
// ping keep-alive timer
this.pingTimer = setInterval(() => {
const ping = JSON.stringify({ type: "ping", ts: Date.now()});
console.log(`Sending ping ${ping}`)

237
frontend/src/__apps/votacao/main.js

@ -0,0 +1,237 @@
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: "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
frontend/src/__apps/votacao/scss/votacao.scss

0
frontend/src/components/Cronometro.vue → frontend/src/components/painel/Cronometro.vue

0
frontend/src/components/CronometroList.vue → frontend/src/components/painel/CronometroList.vue

3
frontend/src/components/PainelHeader.vue → frontend/src/components/painel/PainelHeader.vue

@ -16,7 +16,7 @@
<img v-bind:src="brasao" id="logo-painel" class="logo-painel" alt=""/>
</div>
</div>
<div class="row justify-content-center" v-if="!painel_aberto">
<div class="row justify-content-center">
<h2 class="text-danger"><span id="message">{{ message }}</span></h2>
</div>
@ -40,7 +40,6 @@ export default {
sessao_plenaria_data: "22/10/2025",
sessao_plenaria_hora_inicio: "13:30",
brasao: "",
message: "",
data_atual: "",
relogio: "",
currentDateTimeId: null, // stores the id returned by setInterval()

0
frontend/src/components/PainelMateria.vue → frontend/src/components/painel/PainelMateria.vue

0
frontend/src/components/PainelOradores.vue → frontend/src/components/painel/PainelOradores.vue

2
frontend/src/components/PainelParlamentares.vue → frontend/src/components/painel/PainelParlamentares.vue

@ -7,7 +7,7 @@
<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>
<td style="padding-right:20px; color:yellow" v-if="mostrar_voto">{{ p.voto }}</td>
</tr>
</table>
</div>

0
frontend/src/components/PainelResultado.vue → frontend/src/components/painel/PainelResultado.vue

11
frontend/src/components/votacao/VotacaoNominal.vue

@ -0,0 +1,11 @@
<template>
<div class="VotacaoNominal">
<pre v-text="$attrs"/>
</div>
</template>
<script>
export default {
name: 'Votacaonominal'
};
</script>

112
sapl/painel/consumers.py

@ -1,58 +1,79 @@
import html
import json
import json
import logging
import time
from channels.db import database_sync_to_async
from channels.generic.websocket import AsyncWebsocketConsumer, AsyncJsonWebsocketConsumer
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Q
from sapl.base.models import CasaLegislativa, AppConfig
from sapl.sessao.models import SessaoPlenaria, OrdemDia, ExpedienteMateria, RegistroVotacao, RegistroLeitura, \
PresencaOrdemDia, SessaoPlenariaPresenca, OradorExpediente, VotoParlamentar, AbstractOrdemDia, SessaoPresencaView, \
from sapl.sessao.models import SessaoPlenaria, SessaoPresencaView, \
SessaoOradorView, SessaoMateriaVotacaoView
logger = logging.getLogger(__name__)
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(pk: int) -> dict:
def get_dados_painel(sessao_plenaria_id: int) -> dict:
app_config = AppConfig.objects.first()
sessao = SessaoPlenaria.objects.get(id=pk)
sessao = SessaoPlenaria.objects.get(id=sessao_plenaria_id)
casa = CasaLegislativa.objects.first()
brasao = casa.logotipo.url \
if app_config.mostrar_brasao_painel else None
# FILIACAO
# { 1: { "id": 1, "nome": "fulano", "filiacao": "aquela"}, ...}
# [ { "id": 1, "nome": "fulano", "filiacao": "aquela"}, ... ]
# sessao_plenaria_id/pk = 2546
# Painel
presentes = SessaoPresencaView.objects.filter(sessao_plenaria_id=pk,
etapa_sessao='expediente').values_list('parlamentar_id',
'nome_parlamentar',
'filiacao', )
# TODO: recuperar outra matéria quando não existir nenhuma materia_votacao aberta!
materia_votacao = SessaoMateriaVotacaoView.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 = SessaoPresencaView.objects.filter(sessao_plenaria_id=sessao_plenaria_id,
etapa_sessao=materia_votacao.etapa_sessao).values_list(
'parlamentar_id',
'nome_parlamentar',
'filiacao', )
parlamentares = [dict(zip(['parlamentar_id', 'nome_parlamentar', 'filiacao'], p)) for p in presentes]
oradores = SessaoOradorView.objects.filter(sessao_plenaria_id=pk,
etapa_sessao='expediente').values_list('ordem_pronunciamento',
'nome_parlamentar',
)
if materia_votacao and materia_votacao.numero_votos:
materia_votacao.numero_votos.update({"num_presentes": len(parlamentares)})
oradores = SessaoOradorView.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]
votacao = SessaoMateriaVotacaoView.objects.get(sessao_plenaria_id=pk,
etapa_sessao='expediente',
materia_id=31910)
votos_parlamentares = {}
if votacao:
if votacao.votos_parlamentares:
votos_parlamentares = {p["parlamentar_id"]: p for p in votacao.votos_parlamentares}
if votacao.numero_votos:
votacao.numero_votos.update({"num_presentes": len(parlamentares)})
# TODO: recover stopwatch state from DB/Cache
stopwatch = {
"type": "stopwatch.state",
@ -80,18 +101,8 @@ def get_dados_painel(pk: int) -> dict:
},
"parlamentares": parlamentares,
"oradores": oradores,
"resultado": {
"resultado_votacao": votacao.resultado_votacao,
"resultado": votacao.resultado,
"numero_votos": votacao.numero_votos,
},
"votos_parlamentares": votos_parlamentares,
"materia": {
"materia_id": votacao.materia.id,
"texto": str(votacao.materia),
"ementa": votacao.materia.ementa,
},
"stopwatch": stopwatch, # TODO: array of stopwatches
"materia": get_materia_votacao(materia_votacao),
"stopwatch": [stopwatch], # TODO: array of stopwatches
}
print(json.dumps(dados_sessao, indent=4))
@ -99,18 +110,20 @@ def get_dados_painel(pk: int) -> dict:
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):
log.info(f"{user} is not authenticated user!")
self.logger.info(f"{user} is not authenticated user!")
await self.close(code=4401) # explicit, graceful close
return
@ -144,7 +157,7 @@ class PainelConsumer(AsyncJsonWebsocketConsumer):
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":
if msg_type == "ping":
print("PING")
await self.send_json({"type": "ping", "ts": time.time()})
return
@ -173,13 +186,14 @@ 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:
log.exception("connect failed: %s", 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):

4
sapl/painel/urls.py

@ -27,7 +27,7 @@ urlpatterns = [
url(r'^voto-individual/$', votante_view,
name='voto_individual'),
path("painel/v2", websocket_view, name='painel_websocket'),
path("painel/v2/controller/<int:controller_id>/stopwatch",
path("v2/painel/<int:controller_id>", websocket_view, name='painel_websocket'),
path("v2/painel/controller/<int:controller_id>/stopwatch",
stopwatch_controller, name='painel_controller'),
]

6
sapl/painel/views.py

@ -634,15 +634,13 @@ def get_dados_painel(request, pk):
return response_nenhuma_materia(get_presentes(pk, response, None))
def websocket_view(request):
def websocket_view(request, controller_id):
now = timezone.localtime(timezone.now())
utc_offset = now.utcoffset().total_seconds() / 60
# controller_id == session_id
context = {'head_title': str(_('Painel Plenário')),
'sessao_id': 2546, # TODO: recover from template call
'utc_offset': utc_offset,
'enable_live_ws': True,
'controller_id': 2546, # TODO: unify with sessao_id
'controller_id': controller_id, # aka, sessao_plenaria_id
}
return render(request, "painel/painel_v2.html", context)

14
sapl/sessao/migrations/0070_views_sessao_plenaria.py

@ -156,7 +156,7 @@ class Migration(migrations.Migration):
ml.ementa materia_ementa,
tipo_votacao,
CASE tipo_votacao
WHEN 1 THEN 'Simbólia'
WHEN 1 THEN 'Simbólica'
WHEN 2 THEN 'Nominal'
WHEN 3 THEN 'Secreta'
WHEN 4 THEN 'Leitura'
@ -184,13 +184,15 @@ class Migration(migrations.Migration):
WHERE rv.expediente_id = em.id AND tipo_votacao != 4) rv ON TRUE
LEFT JOIN LATERAL (
SELECT em.sessao_plenaria_id,
em.numero_ordem,
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 em.materia_id) as votos_parlamentares
)) 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
@ -238,13 +240,15 @@ class Migration(migrations.Migration):
WHERE rv.expediente_id = od.id AND tipo_votacao != 4) rv ON TRUE
LEFT JOIN LATERAL (
SELECT od.sessao_plenaria_id,
od.numero_ordem,
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 od.materia_id) as votos_parlamentares
)) 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

10
sapl/sessao/urls.py

@ -42,6 +42,8 @@ from sapl.sessao.views import (AdicionarVariasMateriasExpediente,
CorrespondenciaCrud, recuperar_documento)
from django.views.generic import TemplateView
from .apps import AppConfig
app_name = AppConfig.name
@ -171,6 +173,14 @@ urlpatterns = [
ResumoAtaView.as_view(), name='resumo_ata'),
url(r'^sessao/pesquisar-sessao$',
PesquisarSessaoPlenariaView.as_view(), name='pesquisar_sessao'),
# TODO: create proper view
url(r'^sessao/(?P<pk>\d+)/v2/votacao$',
TemplateView.as_view(template_name='sessao/votacao/votacao_v2.html'),
name='votacaonominal'),
url(r'^sessao/(?P<pk>\d+)/v2/painel$',
TemplateView.as_view(template_name='sessao/painel_v2.html')),
url(r'^sessao/(?P<pk>\d+)/matordemdia/votnom/(?P<oid>\d+)/(?P<mid>\d+)$',
VotacaoNominalView.as_view(), name='votacaonominal'),
url(r'^sessao/(?P<pk>\d+)/matordemdia/votnom/edit/(?P<oid>\d+)/(?P<mid>\d+)$',

1
sapl/templates/painel/painel_v2.html

@ -45,6 +45,7 @@
<div id="painel" v-cloak class="col text-center" data-controller-id="{{ controller_id }}">
<painel-header ref="painelHeader"></painel-header>
<strong>[[ message ]]</strong>
<div class="row justify-content-center">

106
sapl/templates/sessao/painel_v2.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 %}

78
sapl/templates/sessao/votacao/votacao_v2.html

@ -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 ]]</legend>
[[ error_message ]]
<!-- Component 1 -->
<div>
<b>Matéria:</b> [[ materia ]]
<br/>
<b>Ementa:</b> [[ ementa ]]
<br/>
</div>
<br/>
<!-- Component 2 -->
<fieldset class="form-group" v-if="parlamentares.length > 0">
<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>
&nbsp;
</span>
</div>
<div v-else>
[[ votos_parlamentares[p.parlamentar_id].voto ]]
</div>
</div>
<br/>
<br/>
<legend><strong>Situação da Votação:</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>
</fieldset>
<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 %}

10
vue.config.js

@ -137,11 +137,21 @@ module.exports = {
.add('./frontend/src/__apps/parlamentar/main.js')
.end()
config
.entry('votacao')
.add('./frontend/src/__apps/votacao/main.js')
.end()
config
.entry('painel')
.add('./frontend/src/__apps/painel/main.js')
.end()
config
.entry('painel-controle')
.add('./frontend/src/__apps/painel-controle/main.js')
.end()
config
.entry('compilacao')
.add('./frontend/src/__apps/compilacao/main.js')

Loading…
Cancel
Save