435 lines
12 KiB
Vue
435 lines
12 KiB
Vue
<template>
|
|
<div class="final-dashboard">
|
|
<div class="container-fluid final-dashboard__container">
|
|
<div class="row final-split-row">
|
|
<div class="col-md-6 final-split-col final-split-col--dashboard">
|
|
<div class="final-dashboard__shell">
|
|
<div class="final-header">
|
|
<div>
|
|
<div class="final-kicker">Keputusan akhir</div>
|
|
<h4 class="final-title">Dashboard keputusan</h4>
|
|
<div class="final-subtitle">Carta interaktif mengikut jawatan</div>
|
|
</div>
|
|
<div class="final-actions">
|
|
<button type="button" class="btn btn-info" :disabled="finalLoading" @click="loadData">
|
|
<i class="fa" :class="finalLoading ? 'fa-refresh fa-spin' : 'fa-refresh'"></i>
|
|
Kemaskini
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="final-vote-summary">
|
|
<div class="final-section-title">Ringkasan undian</div>
|
|
<div class="final-stats">
|
|
<div class="final-stat">
|
|
<div class="final-stat__label">Jumlah undi</div>
|
|
<div class="final-stat__value">{{ dashboardTotalVotes }}</div>
|
|
</div>
|
|
<div class="final-stat">
|
|
<div class="final-stat__label">Bil. jawatan</div>
|
|
<div class="final-stat__value">{{ positions.length }}</div>
|
|
</div>
|
|
<div class="final-stat">
|
|
<div class="final-stat__label">Calon terunggul</div>
|
|
<div class="final-stat__value final-stat__value--text">{{ topOverallLabel }}</div>
|
|
<div class="final-stat__hint">{{ topOverallVotes }} undi</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="final-filters">
|
|
<span class="final-filters__label">Jawatan:</span>
|
|
<button type="button" class="final-chip" :class="{ 'is-active': selectedPositionId === 0 }" @click="setPosition(0)">
|
|
Semua
|
|
</button>
|
|
<button
|
|
v-for="position in positions"
|
|
:key="position.id"
|
|
type="button"
|
|
class="final-chip"
|
|
:class="{ 'is-active': selectedPositionId === position.id }"
|
|
@click="setPosition(position.id)"
|
|
>
|
|
{{ position.name }}
|
|
</button>
|
|
</div>
|
|
|
|
<div v-if="!finalLoading && positions.length === 0" class="final-empty">
|
|
Tiada jawatan untuk pilihan raya ini.
|
|
</div>
|
|
|
|
<div v-else class="row final-charts-row final-charts-row--split">
|
|
<div class="col-xs-12">
|
|
<div class="panel panel-default final-card">
|
|
<div class="panel-heading final-card__heading">
|
|
<div class="final-card__title">{{ mainChartTitle }}</div>
|
|
<div class="final-card__meta">{{ mainChartSubtitle }}</div>
|
|
</div>
|
|
<div class="panel-body final-card__body">
|
|
<div class="final-chart-wrap">
|
|
<bar-chart v-if="mainChartReady" v-bind="mainBarChartProps" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-xs-12">
|
|
<div class="panel panel-default final-card">
|
|
<div class="panel-heading final-card__heading final-card__heading--alt">
|
|
<div class="final-card__title">{{ sideChartTitle }}</div>
|
|
<div class="final-card__meta">{{ sideChartSubtitle }}</div>
|
|
</div>
|
|
<div class="panel-body final-card__body">
|
|
<div class="final-chart-wrap final-chart-wrap--doughnut">
|
|
<pie-chart v-if="mainChartReady" v-bind="sidePieChartProps" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6 final-split-col final-split-col--attendance">
|
|
<div class="final-attendance-shell">
|
|
<div class="final-attendance-shell__head">
|
|
<div>
|
|
<div class="final-attendance-shell__kicker">Kehadiran</div>
|
|
<h4 class="final-attendance-shell__title">Ringkasan kehadiran</h4>
|
|
<div v-if="attendanceSummary" class="final-attendance__meta">
|
|
Kadar daftar: <b>{{ attendanceSummary.attendance_rate }}%</b>
|
|
<span class="final-attendance__meta-sep">·</span>
|
|
{{ attendanceSummary.marked_present }} / {{ attendanceSummary.total_registered }} pengundi
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-if="!attendanceSummaryReady" class="final-attendance__state">Memuatkan kehadiran…</div>
|
|
<div v-else-if="attendanceSummary" class="final-attendance__body">
|
|
<div
|
|
v-if="attendanceSummary.total_registered > 0"
|
|
class="final-attendance__charts final-attendance__charts--standalone"
|
|
>
|
|
<div class="final-attendance__chart-block">
|
|
<div class="final-attendance__chart-caption">Taburan kaedah kehadiran</div>
|
|
<div class="final-attendance__chart-wrap final-attendance__chart-wrap--donut">
|
|
<pie-chart v-bind="attendanceBreakdownPieProps" />
|
|
</div>
|
|
</div>
|
|
<div v-if="showAttendanceFizikalChart" class="final-attendance__chart-block">
|
|
<div class="final-attendance__chart-caption">Status pendaftaran fizikal</div>
|
|
<div class="final-attendance__chart-wrap final-attendance__chart-wrap--bar">
|
|
<bar-chart v-bind="attendanceFizikalBarProps" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-else class="final-attendance__state final-attendance__state--muted">
|
|
Ringkasan kehadiran tidak dapat dimuatkan. Semak sambungan atau log masuk pentadbir.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import BarChart from '../../../common/BarChart.vue';
|
|
import PieChart from '../../../common/PieChart.vue';
|
|
|
|
export default {
|
|
components: {
|
|
BarChart,
|
|
PieChart
|
|
},
|
|
|
|
data: function () {
|
|
return {
|
|
nominees: [],
|
|
results: [],
|
|
positions: [],
|
|
finalLoading: false,
|
|
selectedPositionId: 0,
|
|
attendanceSummary: null,
|
|
attendanceSummaryReady: false
|
|
};
|
|
},
|
|
|
|
computed: {
|
|
election_id: function () {
|
|
return this.$route.params.election_id;
|
|
},
|
|
|
|
no_votes: function () {
|
|
var nominees = this.nominees;
|
|
var results = this.results;
|
|
var no_votes = [];
|
|
for (var i in nominees) {
|
|
var hasvote = false;
|
|
for (var y in results) {
|
|
if (results[y]['nominee_id'] == nominees[i]['id']) {
|
|
hasvote = true;
|
|
}
|
|
}
|
|
if (!hasvote) {
|
|
no_votes.push(nominees[i]);
|
|
}
|
|
}
|
|
return no_votes;
|
|
},
|
|
|
|
dashboardTotalVotes: function () {
|
|
return this.results.reduce(function (s, r) {
|
|
return s + Number(r.votes || 0);
|
|
}, 0);
|
|
},
|
|
|
|
topOverallLabel: function () {
|
|
if (!this.results.length) {
|
|
return '—';
|
|
}
|
|
var best = this.results.reduce(function (a, b) {
|
|
return Number(b.votes || 0) > Number(a.votes || 0) ? b : a;
|
|
});
|
|
var n = this.getNominee(best.nominee_id);
|
|
return (n && n.name) ? n.name : '—';
|
|
},
|
|
|
|
topOverallVotes: function () {
|
|
if (!this.results.length) {
|
|
return 0;
|
|
}
|
|
return this.results.reduce(function (m, r) {
|
|
return Math.max(m, Number(r.votes || 0));
|
|
}, 0);
|
|
},
|
|
|
|
mainChartTitle: function () {
|
|
if (this.selectedPositionId === 0) {
|
|
return 'Jumlah undi mengikut jawatan';
|
|
}
|
|
var p = this.positions.find(function (x) {
|
|
return Number(x.id) === Number(this.selectedPositionId);
|
|
}, this);
|
|
return (p && p.name) ? 'Undi calon — ' + p.name : 'Undi calon';
|
|
},
|
|
|
|
mainChartSubtitle: function () {
|
|
return this.selectedPositionId === 0
|
|
? 'Perbandingan antara jawatan'
|
|
: 'Carta mengikut calon';
|
|
},
|
|
|
|
sideChartTitle: function () {
|
|
return this.selectedPositionId === 0 ? 'Taburan undi (jawatan)' : 'Peratus undi';
|
|
},
|
|
|
|
sideChartSubtitle: function () {
|
|
return this.selectedPositionId === 0 ? 'Perkongsian jumlah undi' : 'Bahagian setiap calon';
|
|
},
|
|
|
|
showAttendanceFizikalChart: function () {
|
|
if (!this.attendanceSummary) {
|
|
return false;
|
|
}
|
|
return Number(this.attendanceSummary.fizikal || 0) > 0;
|
|
},
|
|
|
|
mainChartReady: function () {
|
|
return !this.finalLoading && this.positions.length > 0;
|
|
},
|
|
|
|
positionTotalRows: function () {
|
|
var vm = this;
|
|
return this.positions.map(function (pos) {
|
|
var t = vm.results
|
|
.filter(function (r) {
|
|
return Number(r.position_id) === Number(pos.id);
|
|
})
|
|
.reduce(function (s, r) {
|
|
return s + Number(r.votes || 0);
|
|
}, 0);
|
|
return { id: pos.id, name: pos.name, total: t };
|
|
});
|
|
},
|
|
|
|
mainBarChartProps: function () {
|
|
if (!this.mainChartReady) {
|
|
return {};
|
|
}
|
|
if (this.selectedPositionId === 0) {
|
|
var rows = this.positionTotalRows;
|
|
return {
|
|
labels: rows.map(function (r) {
|
|
return r.name.length > 22 ? r.name.slice(0, 20) + '…' : r.name;
|
|
}),
|
|
values: rows.map(function (r) {
|
|
return r.total;
|
|
}),
|
|
datasetLabel: 'Jumlah undi'
|
|
};
|
|
}
|
|
var rows = this.getSinglePositionChartRows();
|
|
return {
|
|
labels: rows.map(function (r) {
|
|
return r.label;
|
|
}),
|
|
values: rows.map(function (r) {
|
|
return r.votes;
|
|
}),
|
|
datasetLabel: 'Undi'
|
|
};
|
|
},
|
|
|
|
sidePieChartProps: function () {
|
|
if (!this.mainChartReady) {
|
|
return {};
|
|
}
|
|
if (this.selectedPositionId === 0) {
|
|
var rows = this.positionTotalRows;
|
|
return {
|
|
labels: rows.map(function (r) {
|
|
return r.name;
|
|
}),
|
|
values: rows.map(function (r) {
|
|
return r.total;
|
|
})
|
|
};
|
|
}
|
|
var rows = this.getSinglePositionChartRows();
|
|
return {
|
|
labels: rows.map(function (r) {
|
|
return r.label;
|
|
}),
|
|
values: rows.map(function (r) {
|
|
return r.votes;
|
|
})
|
|
};
|
|
},
|
|
|
|
attendanceBreakdownPieProps: function () {
|
|
if (!this.attendanceSummary || this.attendanceSummary.total_registered <= 0) {
|
|
return { labels: [], values: [] };
|
|
}
|
|
var s = this.attendanceSummary;
|
|
return {
|
|
labels: ['Fizikal', 'Maya', 'Belum ditetapkan'],
|
|
values: [
|
|
Number(s.fizikal || 0),
|
|
Number(s.maya || 0),
|
|
Number(s.unset || 0)
|
|
]
|
|
};
|
|
},
|
|
|
|
attendanceFizikalBarProps: function () {
|
|
if (!this.showAttendanceFizikalChart || !this.attendanceSummary) {
|
|
return { labels: [], values: [] };
|
|
}
|
|
var s = this.attendanceSummary;
|
|
return {
|
|
labels: ['Menunggu pengesahan', 'Disahkan'],
|
|
values: [
|
|
Number(s.fizikal_registration_pending || 0),
|
|
Number(s.fizikal_registration_verified || 0)
|
|
],
|
|
datasetLabel: 'Pengundi'
|
|
};
|
|
}
|
|
},
|
|
|
|
methods: {
|
|
setPosition: function (id) {
|
|
this.selectedPositionId = id === 0 ? 0 : Number(id);
|
|
},
|
|
|
|
getSinglePositionChartRows: function () {
|
|
var pid = this.selectedPositionId;
|
|
var vm = this;
|
|
var rows = this.results
|
|
.filter(function (r) {
|
|
return Number(r.position_id) === Number(pid);
|
|
})
|
|
.map(function (r) {
|
|
var n = vm.getNominee(r.nominee_id) || {};
|
|
var label = n.name || ('#' + r.nominee_id);
|
|
if (label.length > 28) {
|
|
label = label.slice(0, 26) + '…';
|
|
}
|
|
return {
|
|
label: label,
|
|
votes: Number(r.votes || 0)
|
|
};
|
|
});
|
|
this.no_votes.forEach(function (nv) {
|
|
if (Number(nv.position_id) === Number(pid)) {
|
|
var label = nv.name || '';
|
|
if (label.length > 28) {
|
|
label = label.slice(0, 26) + '…';
|
|
}
|
|
rows.push({
|
|
label: label || 'Calon',
|
|
votes: 0
|
|
});
|
|
}
|
|
});
|
|
rows.sort(function (a, b) {
|
|
return b.votes - a.votes;
|
|
});
|
|
return rows;
|
|
},
|
|
|
|
loadData: function () {
|
|
var vm = this;
|
|
this.util.notify('Loading please wait...', 'loading');
|
|
this.finalLoading = true;
|
|
this.attendanceSummaryReady = false;
|
|
this.attendanceSummary = null;
|
|
|
|
axios
|
|
.get(config.API + 'election/' + this.election_id + '/attendance-summary')
|
|
.then(function (r) {
|
|
vm.attendanceSummary = r.data;
|
|
})
|
|
.catch(function () {
|
|
vm.attendanceSummary = null;
|
|
})
|
|
.finally(function () {
|
|
vm.attendanceSummaryReady = true;
|
|
});
|
|
|
|
axios
|
|
.get(config.API + 'election/result/' + this.election_id)
|
|
.then(function (response) {
|
|
$.notifyClose();
|
|
vm.nominees = response.data.nominee || [];
|
|
vm.results = response.data.result || [];
|
|
vm.positions = response.data.position || [];
|
|
})
|
|
.catch(function (error) {
|
|
$.notifyClose();
|
|
vm.util.showResult(error, 'error');
|
|
})
|
|
.finally(function () {
|
|
vm.finalLoading = false;
|
|
});
|
|
},
|
|
|
|
getNominee: function (id) {
|
|
var nominees = this.nominees;
|
|
for (var i in nominees) {
|
|
if (id == nominees[i]['id']) {
|
|
return nominees[i];
|
|
}
|
|
}
|
|
return {};
|
|
}
|
|
},
|
|
|
|
created: function () {
|
|
this.util.setTitle('MyKoPKB - Keputusan akhir');
|
|
this.loadData();
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<style scoped src="./final.css"></style>
|