Browse Source

Refatora interface do painel com novos componentes, layout e fotos

pull/3840/head
RogerKoala 6 days ago
parent
commit
ed29b9b4e2
  1. 52
      frontend/src/__apps/painel/main.js
  2. 59
      frontend/src/__apps/painel/scss/painel.scss
  3. 39
      frontend/src/components/painel/Cronometro.vue
  4. 87
      frontend/src/components/painel/CronometroList.vue
  5. 59
      frontend/src/components/painel/PainelHeader.vue
  6. 36
      frontend/src/components/painel/PainelMateria.vue
  7. 17
      frontend/src/components/painel/PainelOradores.vue
  8. 158
      frontend/src/components/painel/PainelParlamentares.vue
  9. 119
      frontend/src/components/painel/PainelResultado.vue
  10. 71
      sapl/templates/painel/painel_v2.html

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

@ -13,6 +13,7 @@ import PainelParlamentares from '../../components/painel/PainelParlamentares.vue
import PainelOradores from '../../components/painel/PainelOradores.vue' import PainelOradores from '../../components/painel/PainelOradores.vue'
import PainelMateria from '../../components/painel/PainelMateria.vue' import PainelMateria from '../../components/painel/PainelMateria.vue'
import PainelResultado from '../../components/painel/PainelResultado.vue' import PainelResultado from '../../components/painel/PainelResultado.vue'
import PainelFooter from '../../components/painel/PainelFooter.vue'
import alarm from '../../assets/audio/ring.mp3' import alarm from '../../assets/audio/ring.mp3'
// register components // register components
@ -23,6 +24,7 @@ Vue.component('painel-parlamentares', PainelParlamentares)
Vue.component('painel-oradores', PainelOradores) Vue.component('painel-oradores', PainelOradores)
Vue.component('painel-materia', PainelMateria) Vue.component('painel-materia', PainelMateria)
Vue.component('painel-resultado', PainelResultado) Vue.component('painel-resultado', PainelResultado)
Vue.component('painel-footer', PainelFooter)
// global store // global store
Vue.use(Vuex) Vue.use(Vuex)
@ -50,10 +52,22 @@ const store = new Vuex.Store({
}, },
updateParlamentares(state, votos_parlamentares) { updateParlamentares(state, votos_parlamentares) {
if (votos_parlamentares) { if (votos_parlamentares) {
state.parlamentares.forEach((p)=>{ let votosMap = votos_parlamentares;
if (p.parlamentar_id in votos_parlamentares) { if (Array.isArray(votos_parlamentares)) {
p.voto = votos_parlamentares[p.parlamentar_id].voto votosMap = {};
votos_parlamentares.forEach((item) => {
if (item) {
Object.keys(item).forEach((key) => {
votosMap[key] = item[key];
});
}
});
}
state.parlamentares = state.parlamentares.map((p) => {
if (p.parlamentar_id in votosMap) {
return { ...p, voto: votosMap[p.parlamentar_id].voto };
} }
return p;
}); });
} }
}, },
@ -71,6 +85,9 @@ const store = new Vuex.Store({
}, },
setMostrarVoto(state, mostrar_voto) { setMostrarVoto(state, mostrar_voto) {
state.mostrar_voto = mostrar_voto; state.mostrar_voto = mostrar_voto;
},
setSessao(state, sessao) {
state.sessao = sessao;
} }
}, },
actions: {}, actions: {},
@ -105,7 +122,7 @@ new Vue({
}, },
computed: { computed: {
...mapState(["painel_aberto", "sessao_aberta"]), ...mapState(["painel_aberto", "sessao_aberta", "sessao"]),
canRender () { canRender () {
return this.sessao_aberta && this.painel_aberto; return this.sessao_aberta && this.painel_aberto;
}, },
@ -113,7 +130,7 @@ new Vue({
methods: { methods: {
...mapMutations(['sessaoStatus', 'painelStatus','setParlamentares', ...mapMutations(['sessaoStatus', 'painelStatus','setParlamentares',
'updateParlamentares', 'setOradores', 'setMateria', 'updateParlamentares', 'setOradores', 'setMateria',
'setResultado', 'setMessage', 'setMostrarVoto']), 'setResultado', 'setMessage', 'setMostrarVoto', 'setSessao']),
wsURL() { wsURL() {
const proto = location.protocol === 'https:' ? 'wss' : 'ws' const proto = location.protocol === 'https:' ? 'wss' : 'ws'
return `${proto}://${location.host}/ws/painel/${this.controllerId}/` return `${proto}://${location.host}/ws/painel/${this.controllerId}/`
@ -165,6 +182,10 @@ new Vue({
this.painelStatus(data.painel_aberto); this.painelStatus(data.painel_aberto);
this.setMostrarVoto(data.mostrar_voto); this.setMostrarVoto(data.mostrar_voto);
if (data.sessao) {
this.setSessao(data.sessao);
}
// PARLAMENTARES // PARLAMENTARES
if (data.parlamentares) { if (data.parlamentares) {
// pre-popula para Vuex capturar mudanca de estado de 'voto' // pre-popula para Vuex capturar mudanca de estado de 'voto'
@ -173,14 +194,14 @@ new Vue({
} }
// HEADER DO PAINEL // HEADER DO PAINEL
// SESSAO_PLENARIA
//TODO: group in a single SessaoPlenaria object
const headerInstance = this.$refs.painelHeader; const headerInstance = this.$refs.painelHeader;
//TODO: setup as child's props?
headerInstance.sessao_plenaria = data.sessao.sessao_plenaria 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 // FOOTER DO PAINEL (brasão + relógio)
headerInstance.brasao = data.sessao.brasao const footerInstance = this.$refs.painelFooter;
if (footerInstance) {
footerInstance.brasao = data.sessao.brasao;
}
if (data.message) { if (data.message) {
this.setMessage(data.message); this.setMessage(data.message);
@ -194,16 +215,21 @@ new Vue({
// MATERIA // MATERIA
if (data.materia) { if (data.materia) {
this.setMateria(data.materia); this.setMateria(data.materia);
}
// RESULTADO // RESULTADO
if (data.materia.resultado) { if (data.materia.resultado) {
this.setResultado(data.materia.resultado); this.setResultado(data.materia.resultado);
}
if (data.materia.resultado.votos_parlamentares) { if (data.materia.resultado.votos_parlamentares) {
this.updateParlamentares(data.materia.resultado.votos_parlamentares); this.updateParlamentares(data.materia.resultado.votos_parlamentares);
} }
} else {
this.setResultado({});
}
} else {
this.setMateria({});
this.setResultado({});
}
} catch (e) { } catch (e) {
console.error('Error', e); console.error('Error', e);
} }

59
frontend/src/__apps/painel/scss/painel.scss

@ -1,43 +1,24 @@
:root {
--bg-dark: #121212;
--bg-panel: #1e1e1e;
--text-main: #e0e0e0;
--text-highlight: #4fa64d;
}
.painel-principal { body {
background: #1c1b1b; background-color: var(--bg-dark);
font-family: Verdana; color: var(--text-main);
font-size: x-large; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
.text-title { margin-bottom: 0;
color: #4fa64d; }
margin: 0.5rem;
font-weight: bold;
}
.text-subtitle {
color: #459170;
font-weight: bold;
}
.data-hora {
font-size: 180%;
}
.text-value { .separador-vertical {
color: white; border-right: 1px solid #333;
} min-height: 100vh;
margin: 0;
}
.logo-painel { .text-subtitle {
max-width: 100%; color: #81c784;
} font-weight: 600;
.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;
}
}
} }

39
frontend/src/components/painel/Cronometro.vue

@ -1,18 +1,19 @@
<template> <template>
<div> <tr :id="'row_' + id" v-show="visible">
<audio <td :class="['fs-3', titleColorClass]">{{ title }}</td>
ref="player" <td class="text-end">
:src="audioSrc" <audio ref="player" :src="audioSrc" preload="auto"></audio>
preload="auto" <span :id="'cronometro_' + id"
></audio> :class="['fw-bold', 'font-monospace', 'fs-1', titleColorClass]"
<span ref="time">{{ title }}: {{ formatTime(time) }}<br/></span> ref="time">{{ formatTime(time) }}</span>
</div> </td>
</tr>
</template> </template>
<script> <script>
export default { export default {
name: 'Cronometro', name: 'Cronometro',
props: ['id', 'title'], props: ['id', 'title', 'visible', 'colorClass'],
data() { data() {
return { return {
time: 300, time: 300,
@ -22,17 +23,21 @@ export default {
audioSrc: require('@/assets/audio/ring.mp3'), audioSrc: require('@/assets/audio/ring.mp3'),
} }
}, },
computed: {
titleColorClass() {
return this.colorClass || '';
}
},
mounted() { mounted() {
console.log('Cronometro mounted'); console.log('Cronometro mounted');
console.log(this.audioSrc); this.$emit('child-mounted');
this.$emit('child-mounted'); // Emit a custom event
}, },
methods: { methods: {
changeFontSize(value) { changeFontSize(value) {
const el = this.$refs.time; const el = this.$refs.time;
if (!el) return; if (!el) return;
let fontSize = window.getComputedStyle(el).fontSize; let fontSize = window.getComputedStyle(el).fontSize;
fontSize = parseFloat(fontSize); // safely convert "16px" 16 fontSize = parseFloat(fontSize);
el.style.fontSize = (fontSize + value) + 'px'; el.style.fontSize = (fontSize + value) + 'px';
}, },
handleStartStop() { handleStartStop() {
@ -42,8 +47,7 @@ export default {
this.intervalId = setInterval(() => { this.intervalId = setInterval(() => {
if (this.time > 0) { if (this.time > 0) {
this.time--; this.time--;
// play buzz at 00:00:30 if (this.time == 30) {
if (this.time == 30000) {
this.playSound(); this.playSound();
} }
} else { } else {
@ -66,20 +70,15 @@ export default {
playSound() { playSound() {
const audio = this.$refs.player const audio = this.$refs.player
if (!audio) return if (!audio) return
audio.play()
const playPromise = audio.play()
}, },
formatTime(seconds) { formatTime(seconds) {
const hrs = Math.floor(seconds / 3600); const hrs = Math.floor(seconds / 3600);
const mins = Math.floor((seconds % 3600) / 60); const mins = Math.floor((seconds % 3600) / 60);
const secs = seconds % 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 `${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: { watch: {
initialTime(newVal) { initialTime(newVal) {

87
frontend/src/components/painel/CronometroList.vue

@ -1,16 +1,50 @@
.<template> <template>
<div class="col-md-6 text-left painel" v-if="canRender"> <div class="painel d-flex flex-column" v-if="canRender">
<div class="d-flex align-items-left justify-content-left mb-2"> <div id="box_cronometros" class="w-100">
<h2 class="text-subtitle mb-0">Cronômetros</h2> <h2 class="text-center text-subtitle mb-3">Cronômetro</h2>
<div class="d-flex align-items-center justify-content-center" style="height: 80px">
<table class="table-custom w-100 mb-0">
<tbody>
<Cronometro
ref="childRef_0"
id="discurso"
title="Discurso"
:visible="visibleCronometro === 'discurso'"
color-class=""
@child-mounted="handleChildMounted"
/>
<Cronometro
ref="childRef_1"
id="aparte"
title="Aparte"
:visible="visibleCronometro === 'aparte'"
color-class="text-warning"
@child-mounted="handleChildMounted"
/>
<Cronometro
ref="childRef_2"
id="ordem"
title="Questão de Ordem"
:visible="visibleCronometro === 'ordem'"
color-class="text-info"
@child-mounted="handleChildMounted"
/>
<Cronometro
ref="childRef_3"
id="consideracoes"
title="Consid. Finais"
:visible="visibleCronometro === 'consideracoes'"
color-class=""
@child-mounted="handleChildMounted"
/>
</tbody>
</table>
</div> </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>
</div> </div>
</template> </template>
<script> <script>
import { ref, onMounted } from 'vue';
import { mapState } from 'vuex'; import { mapState } from 'vuex';
import Cronometro from './Cronometro.vue'; import Cronometro from './Cronometro.vue';
@ -21,8 +55,7 @@
}, },
data() { data() {
return { return {
titles: ["Discurso", "Aparte", "Questão de Ordem", "Considerações Finais"], visibleCronometro: 'discurso',
itemRefs: ref([]), // An array to store the refs
} }
}, },
mounted() { mounted() {
@ -37,15 +70,39 @@
methods: { methods: {
handleStartStop() { handleStartStop() {
console.log("start/stop stopwatch"); console.log("start/stop stopwatch");
//console.log(this.$refs.itemRefs);
}, },
handleChildMounted() { handleChildMounted() {
console.log('ChildComponent has finished mounting in the parent!'); console.log('Cronometro child mounted');
// 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();
}, },
updateVisibility() {
// Show priority: ordem > aparte > consideracoes > discurso
const refs = [
{ key: 'ordem', ref: 'childRef_2' },
{ key: 'aparte', ref: 'childRef_1' },
{ key: 'consideracoes', ref: 'childRef_3' },
{ key: 'discurso', ref: 'childRef_0' },
];
for (const item of refs) {
const comp = this.$refs[item.ref];
if (comp && comp.isRunning) {
this.visibleCronometro = item.key;
return;
}
}
// Default to discurso if none running
this.visibleCronometro = 'discurso';
}
}, },
}; };
</script> </script>
<style scoped>
.table-custom {
color: #ddd;
}
::v-deep .table-custom tbody td {
padding: 8px;
font-size: 1.1rem;
}
</style>

59
frontend/src/components/painel/PainelHeader.vue

@ -1,29 +1,7 @@
<template v-if="sessao_aberta"> <template>
<div> <div>
<div class="d-flex justify-content-center"> <h1 id="sessao_plenaria" class="text-title fw-bold text-uppercase">{{ sessao_plenaria }}</h1>
<h1 id="sessao_plenaria" class="title text-title">{{ sessao_plenaria }} </h1> <h2 class="text-danger" v-if="message"><span id="message">{{ message }}</span></h2>
</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> </div>
</template> </template>
@ -36,37 +14,24 @@ export default {
data() { data() {
return { return {
sessao_plenaria: "Sessao Plenaria Teste", sessao_plenaria: "",
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: { 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: { computed: {
...mapState(["sessao_aberta", "painel_aberto", "message", "mostrar_voto"]) ...mapState(["sessao_aberta", "message"])
}, },
mounted() { mounted() {
console.log('PainelHeader component mounted'); console.log('PainelHeader component mounted');
this.startCurrentDateTime();
} }
} }
</script> </script>
<style scoped>
.text-title {
color: var(--text-highlight);
letter-spacing: 1px;
}
</style>

36
frontend/src/components/painel/PainelMateria.vue

@ -1,11 +1,21 @@
<template> <template>
<div class="col-md-6 text-center painel" id="obs_materia_div" v-if="canRender"> <div class="painel p-3 d-flex flex-column" id="obs_materia_div" v-if="canRender">
<h2 class="text-subtitle" id="mat_em_votacao">Matéria em Votação</h2> <div class="flex-grow-1">
<span id="materia_legislativa_texto" class="text-value">{{ materia.texto }}</span> <h2 id="mat_em_votacao" class="text-subtitle text-center mb-3 pb-1">
<br> Matéria em Votação
<span id="materia_legislativa_ementa" class="text-value">{{ materia.ementa }} </span> </h2>
<br> <span id="materia_legislativa_texto"
<span id="observacao_materia" class="text-value">{{ materia.observacao }}</span> class="fs-4 text-white d-block mb-1">{{ materia.texto }}</span>
<span id="materia_legislativa_ementa"
class="fs-6 fst-italic opacity-75 text-white">{{ materia.ementa }}</span>
<div class="mt-2 text-warning" style="font-size: 0.9rem">
<span id="observacao_materia">{{ materia.observacao }}</span>
</div>
<div id="resultado_votacao"
class="text-title mt-auto text-center d-block fs-2">{{ materia.resultado_votacao }}</div>
</div>
</div> </div>
</template> </template>
@ -15,13 +25,6 @@ export default {
name: 'PainelMateria', name: 'PainelMateria',
data() { data() {
return { return {
/*
materia: {
texto: '',
ementa: '',
observacao: '',
}
*/
}; };
}, },
mounted() { mounted() {
@ -38,5 +41,8 @@ export default {
</script> </script>
<style scoped> <style scoped>
/* Optional styling */ .text-title {
color: var(--text-highlight);
letter-spacing: 1px;
}
</style> </style>

17
frontend/src/components/painel/PainelOradores.vue

@ -1,12 +1,16 @@
<template> <template>
<div class="col-md-6 text-center painel" id="aparecer_oradores" v-if="canRender"> <div class="text-center painel" id="aparecer_oradores" v-if="canRender && oradores.length > 0">
<h2 class="text-subtitle">Oradores</h2> <h2 class="text-subtitle">Oradores</h2>
<div id="orador">
<table id="oradores_list"> <table id="oradores_list">
<tr v-for="o in oradores" :key="o.ordem_pronunciamento"><td style="padding-right:20px; color:white"> <tr v-for="o in oradores" :key="o.ordem_pronunciamento">
{{ o.ordem_pronunciamento }}º &nbsp {{ o.nome_parlamentar }}</td> <td style="padding-right:20px; color:white">
{{ o.ordem_pronunciamento }}º - {{ o.nome_parlamentar }}
</td>
</tr> </tr>
</table> </table>
</div> </div>
</div>
</template> </template>
<script> <script>
@ -15,7 +19,6 @@ export default {
name: 'PainelOradores', name: 'PainelOradores',
data() { data() {
return { return {
// oradores: [],
}; };
}, },
mounted() { mounted() {
@ -32,5 +35,9 @@ export default {
</script> </script>
<style scoped> <style scoped>
/* Optional styling */ #oradores_list tr td {
font-size: 1.2rem;
padding: 5px;
text-align: start;
}
</style> </style>

158
frontend/src/components/painel/PainelParlamentares.vue

@ -1,26 +1,40 @@
<template> <template>
<div class="col-md-4" v-if="canRender"> <div id="parlamentares">
<div class="text-center painel"> <div class="container-fluid" id="parlamentares_container" v-if="canRender">
<h2 class="text-subtitle">Parlamentares</h2> <div class="parlamentares-grid" id="parlamentares_row">
<span id="parlamentares" class="text-value text-center"></span> <div v-for="p in parlamentares"
<table id="parlamentares_list"> :key="p.parlamentar_id"
<tr v-for="p in parlamentares" :key="p.parlamentar_id" class="text-value text-center"> :class="['parlamentar-card', 'mt-3', votoClass(p)]">
<td style="padding-right:20px; color:yellow" > {{ p.nome_parlamentar }}</td> <div class="voto-faixa" v-if="votoText(p)">
<td style="padding-right:20px; color:yellow"> {{ p.filiacao }}</td> <p>{{ votoText(p) }}</p>
<td style="padding-right:20px; color:yellow" v-if="mostrar_voto">{{ p.voto }}</td> </div>
</tr>
</table> <div class="foto-container">
<img :src="p.fotografia || defaultAvatar" :alt="p.nome_parlamentar">
</div>
<div class="mt-2 text-center">
<div class="nome">{{ p.nome_parlamentar }}</div>
<div class="partido">{{ p.filiacao }}</div>
</div>
</div>
</div> </div>
</div> </div>
<span class="text-white" v-else>
<center>A listagem de parlamentares aparecerá quando o painel estiver aberto.</center>
</span>
</div>
</template> </template>
<script> <script>
import { mapState } from 'vuex'; import { mapState } from 'vuex';
import defaultAvatarImg from '@/assets/img/avatar.png';
export default { export default {
name: 'PainelParlamentares', name: 'PainelParlamentares',
data() { data() {
return { return {
//parlamentares: [], defaultAvatar: defaultAvatarImg,
}; };
}, },
mounted() { mounted() {
@ -33,9 +47,127 @@ export default {
}, },
...mapState(["painel_aberto", "sessao_aberta", "parlamentares", "mostrar_voto"]) ...mapState(["painel_aberto", "sessao_aberta", "parlamentares", "mostrar_voto"])
}, },
methods: {
votoClass(parlamentar) {
const voto = parlamentar.voto;
if (!voto) return '';
if (this.mostrar_voto) {
if (voto === 'Sim') return 'voto-sim';
if (voto === 'Não') return 'voto-nao';
if (voto === 'Abstenção') return 'voto-abstencao';
} else {
if (voto === 'Sim' || voto === 'Não' || voto === 'Abstenção' || voto === 'Voto Informado') {
return 'voto-informado';
}
}
return '';
},
votoText(parlamentar) {
const voto = parlamentar.voto;
if (!voto) return '';
if (this.mostrar_voto) {
if (voto === 'Sim') return 'S';
if (voto === 'Não') return 'N';
if (voto === 'Abstenção') return 'A';
} else {
if (voto === 'Sim' || voto === 'Não' || voto === 'Abstenção' || voto === 'Voto Informado') {
return 'I';
}
}
return '';
},
},
}; };
</script> </script>
<style scoped> <style scoped>
/* Optional styling */ .parlamentares-grid {
display: grid;
grid-template-columns: repeat(auto-fill, 190px);
width: 100%;
}
.parlamentar-card {
width: 150px;
border-radius: 3px;
overflow: hidden;
position: relative;
}
.foto-container {
border-radius: 3px;
overflow: hidden;
height: 150px;
}
.parlamentar-card img {
width: 100%;
height: 150px;
display: block;
object-fit: fill;
}
.parlamentar-card .nome {
font-weight: bold;
font-size: 0.95rem;
color: #fff;
line-height: 1.1;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
margin-bottom: 2px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.parlamentar-card .partido {
font-size: 0.8rem;
color: #ddd;
font-weight: 600;
text-transform: uppercase;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
}
.voto-faixa {
position: absolute;
width: 90px;
height: 90px;
top: -45px;
right: -45px;
transform: rotate(45deg);
display: flex;
align-items: flex-end;
justify-content: center;
color: #fff;
font-weight: 700;
box-shadow: -2px 2px 5px rgba(0, 0, 0, 0.2);
z-index: 10;
line-height: 1;
}
.voto-faixa p {
transform: rotate(-45deg);
font-size: 2rem;
text-transform: uppercase;
}
.voto-sim .voto-faixa {
background-color: #27ae60;
}
.voto-nao .voto-faixa {
background-color: #c0392b;
}
.voto-abstencao .voto-faixa {
background-color: #f39c12;
}
.voto-informado .voto-faixa {
background-color: #d35400;
}
.voto-ausente .voto-faixa {
background-color: #7f8c8d;
}
</style> </style>

119
frontend/src/components/painel/PainelResultado.vue

@ -1,23 +1,30 @@
<template> <template>
<div class="col-md-6 text-left painel" id="resultado_votacao_div" v-if="canRender"> <div class="painel p-3 d-flex flex-column" id="resultado_votacao_div" v-if="canRender">
<div class="d-flex align-items-left justify-content-left mb-2"> <div id="box_votacao" class="w-100">
<h2 class="text-subtitle mb-0">Resultado</h2> <table class="table-custom w-100 mb-0" id="tabela_resultados">
<button class="btn btn-sm btn-secondary ms-2" v-on:click="changeFontSize(-1)"> <tbody id="votacao">
A- <tr>
</button> <td>Presentes</td>
<button class="btn btn-sm btn-secondary ms-2" v-on:click="changeFontSize(1)"> <td>{{ numPresentes }}</td>
A+ </tr>
</button> <tr>
</div> <td>Sim</td>
<div ref="votacao" id="box_votacao"> <td class="table-sim">{{ votosSim }}</td>
<div id="votacao" class="text-value"> </tr>
<li>Sim: {{ resultado.numero_votos.votos_sim }}</li> <tr>
<li>Não: {{ resultado.numero_votos.votos_nao }}</li> <td>Não</td>
<li>Abstenções: {{ resultado.numero_votos.abstencoes }}</li> <td class="table-nao">{{ votosNao }}</td>
<li>Presentes: {{ resultado.numero_votos.num_presentes }}</li> </tr>
<li>Total votos: {{ resultado.numero_votos.total_votos }}</li> <tr>
</div> <td>Abstenções</td>
<div id="resultado_votacao" class="text-title">{{ resultado.resultado_votacao }}</div> <td class="table-abstencao">{{ votosAbstencao }}</td>
</tr>
<tr>
<td><strong>Total votos</strong></td>
<td class="table-total"><strong>{{ totalVotos }}</strong></td>
</tr>
</tbody>
</table>
</div> </div>
</div> </div>
</template> </template>
@ -28,18 +35,6 @@ export default {
name: 'PainelResultado', name: 'PainelResultado',
data() { data() {
return { return {
/*
resultado: {
numero_votos: {
votos_sim: 0,
votos_nao: 0,
abstencoes: 0,
total_votos: 0,
num_presentes: 0,
},
resultado_votacao: '',
}
*/
}; };
}, },
mounted() { mounted() {
@ -50,20 +45,64 @@ export default {
canRender () { canRender () {
return this.sessao_aberta && this.painel_aberto; return this.sessao_aberta && this.painel_aberto;
}, },
...mapState(["painel_aberto", "sessao_aberta", "resultado"]) ...mapState(["painel_aberto", "sessao_aberta", "resultado", "parlamentares"]),
numPresentes() {
if (this.resultado && this.resultado.numero_votos && typeof this.resultado.numero_votos.num_presentes !== 'undefined' && this.resultado.numero_votos.num_presentes !== null) {
return this.resultado.numero_votos.num_presentes;
}
return this.parlamentares ? this.parlamentares.length : 0;
}, },
methods: { votosSim() {
changeFontSize(value) { if (this.resultado && this.resultado.numero_votos && typeof this.resultado.numero_votos.votos_sim !== 'undefined' && this.resultado.numero_votos.votos_sim !== null && this.resultado.numero_votos.votos_sim > 0) {
const el = this.$refs.votacao; return this.resultado.numero_votos.votos_sim;
if (!el) return; }
let fontSize = window.getComputedStyle(el).fontSize; return this.parlamentares ? this.parlamentares.filter(p => p.voto === 'Sim').length : 0;
fontSize = parseFloat(fontSize); // safely convert "16px" 16 },
el.style.fontSize = (fontSize + value) + 'px'; votosNao() {
if (this.resultado && this.resultado.numero_votos && typeof this.resultado.numero_votos.votos_nao !== 'undefined' && this.resultado.numero_votos.votos_nao !== null && this.resultado.numero_votos.votos_nao > 0) {
return this.resultado.numero_votos.votos_nao;
}
return this.parlamentares ? this.parlamentares.filter(p => p.voto === 'Não').length : 0;
}, },
votosAbstencao() {
if (this.resultado && this.resultado.numero_votos && typeof this.resultado.numero_votos.abstencoes !== 'undefined' && this.resultado.numero_votos.abstencoes !== null && this.resultado.numero_votos.abstencoes > 0) {
return this.resultado.numero_votos.abstencoes;
} }
return this.parlamentares ? this.parlamentares.filter(p => p.voto === 'Abstenção').length : 0;
},
totalVotos() {
if (this.resultado && this.resultado.numero_votos && typeof this.resultado.numero_votos.total_votos !== 'undefined' && this.resultado.numero_votos.total_votos !== null && this.resultado.numero_votos.total_votos > 0) {
return this.resultado.numero_votos.total_votos;
}
return this.votosSim + this.votosNao + this.votosAbstencao;
}
},
}; };
</script> </script>
<style scoped> <style scoped>
/* Optional styling */ .table-custom {
color: #ddd;
}
.table-custom tbody td {
padding: 8px;
font-size: 1.1rem;
}
.table-sim {
color: #27ae60;
}
.table-nao {
color: #c0392b;
}
.table-abstencao {
color: #f39c12;
}
.table-total {
color: #194BFA;
}
</style> </style>

71
sapl/templates/painel/painel_v2.html

@ -5,86 +5,57 @@
{% load webpack_static from webpack_loader %} {% load webpack_static from webpack_loader %}
<!DOCTYPE HTML> <!DOCTYPE HTML>
<!--[if IE 8]>
<html class="no-js lt-ie9" lang="pt-br"> <![endif]-->
<!--[if gt IE 8]><!-->
<html lang="pt-br"> <html lang="pt-br">
<!--<![endif]-->
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <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> <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>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
rel="stylesheet"
/>
{% block webpack_loader_css %} {% block webpack_loader_css %}
{% render_chunk_vendors 'css' %} {% render_chunk_vendors 'css' %}
{% render_bundle 'global' 'css' %} {% render_bundle 'global' 'css' %}
{% render_bundle 'painel' 'css' %} {% render_bundle 'painel' 'css' %}
{% endblock webpack_loader_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> </head>
<body class="painel-principal"> <body class="min-vh-100 d-flex flex-column p-2">
{% block vue_content %} {% block vue_content %}
<style>[v-cloak]{display:none}</style> <style>[v-cloak]{display:none}</style>
<div id="painel" v-cloak class="col text-center" data-controller-id="{{ controller_id }}"> <div id="painel" v-cloak class="container-fluid" data-controller-id="{{ controller_id }}">
<div class="row g-3">
<div class="col-md-9 separador-vertical pe-4">
<div class="painel p-3 h-100 d-flex flex-column">
<painel-header ref="painelHeader"></painel-header> <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> <painel-parlamentares ref="parlamentares"></painel-parlamentares>
</div>
</div>
<div class="col-md-3 d-flex flex-column gap-3">
<painel-materia ref="materia"></painel-materia>
<div class="d-flex col-md-8 painels">
<painel-oradores ref="oradores"></painel-oradores> <painel-oradores ref="oradores"></painel-oradores>
<div class="col-md-6 text-left painel" v-if="canRender"> <painel-cronometro-list ref="cronometro_list"></painel-cronometro-list>
<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-resultado ref="resultado"></painel-resultado>
<painel-materia ref="materia"></painel-materia> <div class="painel p-3 d-flex flex-column" id="tema_solene_div" v-if="sessao && sessao.sessao_solene">
<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> <h2 class="text-subtitle">Tema da Sessão Solene</h2>
<span id="sessao_solene_tema" class="text-value"></span> <span id="sessao_solene_tema" class="text-value">[[ sessao.tema_solene ]]</span>
</div> </div>
<painel-footer ref="painelFooter"></painel-footer>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
</body> </body>
{% block webpack_loader_js %} {% block webpack_loader_js %}

Loading…
Cancel
Save