Files
E-Vote/resources/assets/js/components/demo/admin/voter/kehadiran.vue
T

1148 lines
34 KiB
Vue

<template>
<div class="panel panel-default">
<div class="panel-body">
<div class="kehadiran-actions clearfix">
<button class="btn btn-success" @click="refreshVoter()">
<i class="fa fa-refresh"></i> Kemaskini Pengundi
</button>
<a class="btn btn-warning pull-right" :href="getPDFKehadiranurl(1)">
<i class="fa fa-download"></i> Muat Turun PDF
</a>
</div>
<div class="kehadiran-search">
<div class="kehadiran-search__label">Carian pantas (kaunter):</div>
<input class="form-control kehadiran-search__input" v-model.trim="searchNoAnggota"
placeholder="No. Anggota" />
<input class="form-control kehadiran-search__input" v-model.trim="searchNoKp"
placeholder="No. Kad Pengenalan" />
<button class="btn btn-primary" @click="applySearch()">Cari</button>
<button class="btn btn-default" @click="clearSearch()" :disabled="!hasAnySearch">Reset</button>
</div>
<div class="kehadiran-filter-panel">
<div class="kehadiran-filter-panel__title">Penapis</div>
<div class="kehadiran-filter-groups">
<div class="kehadiran-filter-group">
<div class="kehadiran-filter-group__title">Kaedah kehadiran</div>
<div class="kehadiran-filter-group__chips">
<button type="button" class="kehadiran-chip" :class="{ 'is-active': !current_kehadiran }"
@click="setKehadiranFilter(null)">
Semua
</button>
<button type="button" class="kehadiran-chip"
:class="{ 'is-active': String(current_kehadiran) === 'unset' }"
@click="setKehadiranFilter('unset')">
Belum Ditetapkan
</button>
<button type="button" class="kehadiran-chip"
:class="{ 'is-active': Number(current_kehadiran) === 1 }"
@click="setKehadiranFilter(1)">
Fizikal
</button>
<button type="button" class="kehadiran-chip"
:class="{ 'is-active': Number(current_kehadiran) === 2 }"
@click="setKehadiranFilter(2)">
Maya
</button>
</div>
</div>
<div class="kehadiran-filter-group">
<div class="kehadiran-filter-group__title">Pendaftaran fizikal</div>
<div class="kehadiran-filter-group__chips">
<button type="button" class="kehadiran-chip" :class="{ 'is-active': !current_fizikal_reg }"
@click="setFizikalRegFilter(null)">
Semua
</button>
<button type="button" class="kehadiran-chip"
:class="{ 'is-active': current_fizikal_reg === 'pending' }"
@click="setFizikalRegFilter('pending')">
Menunggu pengesahan
</button>
<button type="button" class="kehadiran-chip"
:class="{ 'is-active': current_fizikal_reg === 'verified' }"
@click="setFizikalRegFilter('verified')">
Disahkan
</button>
</div>
</div>
<div class="kehadiran-filter-group">
<div class="kehadiran-filter-group__title">Undi</div>
<div class="kehadiran-filter-group__chips">
<button type="button" class="kehadiran-chip" :class="{ 'is-active': !current_has_voted }"
@click="setHasVotedFilter(null)">
Semua
</button>
<button type="button" class="kehadiran-chip"
:class="{ 'is-active': current_has_voted === '1' }"
@click="setHasVotedFilter('1')">
Undi
</button>
<button type="button" class="kehadiran-chip"
:class="{ 'is-active': current_has_voted === '0' }"
@click="setHasVotedFilter('0')">
Belum
</button>
</div>
</div>
<div class="kehadiran-filter-group">
<div class="kehadiran-filter-group__title">Bayaran tunai (elaun)</div>
<div class="kehadiran-filter-group__chips">
<button type="button" class="kehadiran-chip" :class="{ 'is-active': !current_cash_status }"
@click="setCashStatusFilter(null)">
Semua
</button>
<button type="button" class="kehadiran-chip"
:class="{ 'is-active': current_cash_status === 'paid' }"
@click="setCashStatusFilter('paid')">
Sudah Bayar
</button>
<button type="button" class="kehadiran-chip"
:class="{ 'is-active': current_cash_status === 'unpaid' }"
@click="setCashStatusFilter('unpaid')">
Belum Bayar
</button>
</div>
</div>
</div>
</div>
<!-- Attendance stats -->
<div class="kehadiran-stats">
<div class="kehadiran-stat">
<div class="kehadiran-stat__label">Fizikal</div>
<div class="kehadiran-stat__value">{{ attendanceStats.fizikalCount }}</div>
<div class="kehadiran-stat__meta">{{ attendanceStats.fizikalPct }}%</div>
</div>
<div class="kehadiran-stat">
<div class="kehadiran-stat__label">Maya</div>
<div class="kehadiran-stat__value">{{ attendanceStats.mayaCount }}</div>
<div class="kehadiran-stat__meta">{{ attendanceStats.mayaPct }}%</div>
</div>
<div class="kehadiran-stat kehadiran-stat--muted">
<div class="kehadiran-stat__label">Jumlah Kehadiran</div>
<div class="kehadiran-stat__value">{{ attendanceStats.totalAttendance }}</div>
<div class="kehadiran-stat__meta">{{ attendanceStats.totalPct }}%</div>
</div>
</div>
<!-- <a class="btn btn-warning" :href="getPDFKehadiranurl(1)">Muat Turun PDF</a> -->
<admin-data-table :headers="kehadiranTableHeaders" :items="voterListItems" :loading="voterLoading"
:show-pagination="true" index-title="Bil." empty-text="Tiada rekod kehadiran" :exportable="true">
<template v-slot:item-kehadiran="{ item }">
{{ getKehadiranString(item.kehadiran) }}
</template>
<template v-slot:item-fizikal_reg="{ item }">
<span v-if="Number(item.kehadiran) !== 1" class="text-muted"></span>
<span v-else-if="item.fizikal_registration_verified_at"
class="kehadiran-pill kehadiran-pill--success">
<i class="fa fa-check-circle" aria-hidden="true"></i>
<span>Disahkan</span>
</span>
<span v-else class="kehadiran-pill kehadiran-pill--warn">
<i class="fa fa-clock-o" aria-hidden="true"></i>
<span>Menunggu</span>
</span>
</template>
<template v-slot:item-has_voted="{ item }">
<span class="kehadiran-pill"
:class="Number(item.votes_count) > 0 ? 'kehadiran-pill--success' : 'kehadiran-pill--muted'">
<i class="fa" :class="Number(item.votes_count) > 0 ? 'fa-check-circle' : 'fa-times-circle'"
aria-hidden="true"></i>
<span>{{ Number(item.votes_count) > 0 ? 'Undi' : 'Belum' }}</span>
</span>
</template>
<template v-slot:item-allowance="{ item }">
<span class="kehadiran-pill"
:class="isCashPaid(item) ? 'kehadiran-pill--success' : 'kehadiran-pill--warn'">
<i class="fa" :class="isCashPaid(item) ? 'fa-money' : 'fa-exclamation-circle'"
aria-hidden="true"></i>
<span>{{ isCashPaid(item) ? 'Sudah Bayar' : 'Belum Bayar' }}</span>
</span>
</template>
<template v-slot:item-action="{ item }">
<div class="kehadiran-action-cell">
<button type="button" class="btn btn-xs btn-primary kehadiran-action-btn"
@click.stop="openDetail(item)">
<i class="fa fa-eye" aria-hidden="true"></i>
<span>Detail</span>
</button>
<button v-if="canVerifyFizikalRegistration(item)" type="button"
class="btn btn-xs btn-success kehadiran-action-btn"
@click.stop="openFizikalVerifyModal(item)">
<i class="fa fa-check" aria-hidden="true"></i>
<span>Sahkan</span>
</button>
</div>
</template>
</admin-data-table>
<!-- Allowance Modal -->
<div class="modal fade app-modal" id="allowance-modal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Butiran Pembayaran Elaun</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body" v-if="selectedVoter">
<div class="row">
<div class="col-md-6">
<table class="table table-condensed">
<tbody>
<tr>
<th style="width: 160px;">Nama</th>
<td>{{ selectedVoter.name }}</td>
</tr>
<tr>
<th>No. KP</th>
<td>{{ selectedVoter.no_kp }}</td>
</tr>
<tr>
<th>No. Anggota</th>
<td>{{ selectedVoter.no_anggota }}</td>
</tr>
<tr>
<th>Unit</th>
<td>{{ selectedVoter.unit }}</td>
</tr>
</tbody>
</table>
</div>
<div class="col-md-6">
<table class="table table-condensed">
<tbody>
<tr>
<th style="width: 160px;">Kehadiran</th>
<td><b>{{ getKehadiranString(selectedVoter.kehadiran) }}</b></td>
</tr>
<tr v-if="Number(selectedVoter.kehadiran) === 1">
<th>Pendaftaran</th>
<td>
<span v-if="selectedVoter.fizikal_registration_verified_at"
class="label label-success">Disahkan</span>
<span v-else class="label label-warning">Menunggu pengesahan</span>
</td>
</tr>
<tr>
<th>Saham (RM)</th>
<td>{{ formatRmDisplay(selectedVoter.saham) }}</td>
</tr>
<tr>
<th>Yuran (RM)</th>
<td>{{ formatRmDisplay(selectedVoter.yuran) }}</td>
</tr>
<tr>
<th>Status Undi</th>
<td>
<span class="label"
:class="Number(selectedVoter.votes_count) > 0 ? 'label-success' : 'label-default'">
{{ Number(selectedVoter.votes_count) > 0 ? 'Ya' : 'Tidak' }}
</span>
<span v-if="selectedVoterShowsNoVoteEligibleNote"
class="label label-info" style="margin-left: 6px;">Tidak layak
mengundi (saham)</span>
</td>
</tr>
<tr>
<th>Bayaran Tunai</th>
<td>
<span class="label"
:class="isCashPaid(selectedVoter) ? 'label-success' : 'label-warning'">
{{ isCashPaid(selectedVoter) ? ('Sudah (' +
selectedVoter.cash_paid_at +
')') : 'Belum' }}
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<hr />
<div class="row">
<div class="col-md-4">
<label>Kod Tuntutan (Claim Code)</label>
<input type="text" class="form-control" v-model.trim="claimCodeInput"
placeholder="Contoh: 482913" />
<small class="text-muted">Voter akan tunjuk kod ini. Wajib untuk bayaran
tunai.</small>
</div>
<div class="col-md-4">
<label>Jumlah Tunai (RM)</label>
<input type="text" class="form-control kehadiran-tunai-readonly" readonly disabled
:value="allowanceTunaiDisplay" />
</div>
<div class="col-md-4">
<label>Rujukan (optional)</label>
<input type="text" class="form-control" v-model.trim="payoutReference"
maxlength="80" placeholder="Wakil/Sendiri" />
</div>
</div>
<div class="row" style="margin-top: 10px;">
<div class="col-md-12">
<label>Nota (optional)</label>
<input type="text" class="form-control" v-model.trim="payoutNote"
placeholder="Nama Wakil" />
</div>
</div>
<div v-if="isCashPaid(selectedVoter)" class="well well-sm" style="margin-top: 14px;">
<div style="font-weight: 800; margin-bottom: 8px;">Maklumat Bayaran</div>
<div>
Dibayar oleh:
<b>{{ selectedVoter.cash_paid_by_admin_name || '—' }}</b>
<span v-if="selectedVoter.cash_paid_by_admin_email" class="text-muted">
({{ selectedVoter.cash_paid_by_admin_email }})
</span>
</div>
</div>
<div class="alert alert-danger" v-if="detailError" style="margin-top: 12px;">
{{ detailError }}
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Tutup</button>
<button type="button" class="btn btn-danger"
v-if="selectedVoter && isCashPaid(selectedVoter)" :disabled="detailLoading"
@click="voidCashPayout()">
Batal Bayaran Tunai
</button>
<button type="button" class="btn btn-success"
v-if="selectedVoter && canPayCash(selectedVoter)" :disabled="detailLoading"
@click="payCash()">
Bayar Tunai
</button>
</div>
</div>
</div>
</div>
<!-- Fizikal Verify Modal -->
<div class="modal fade app-modal" id="fizikal-verify-modal" tabindex="-1" role="dialog"
aria-labelledby="fizikal-verify-title" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="fizikal-verify-title">Pengesahan pendaftaran fizikal</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body" v-if="verifyModalVoter">
<p class="kehadiran-verify-lead" v-if="verifyModalVoterNeedsLowSahamNote">
Pengundi fizikal ini mempunyai saham di bawah <strong>RM 500</strong> dan
<strong>tidak dibenarkan mengundi</strong>. Sahkan pendaftaran untuk mengesahkan
kehadiran
di kaunter dan membolehkan mereka menuntut elaun tunai (mengikut peraturan portal).
</p>
<p class="kehadiran-verify-lead" v-else>
Sahkan bahawa pengundi ini hadir di kaunter. Selepas ini, pengundi boleh mengundi.
</p>
<table class="table table-condensed table-bordered kehadiran-verify-summary">
<tbody>
<tr>
<th style="width: 140px;">Nama</th>
<td>{{ verifyModalVoter.name }}</td>
</tr>
<tr>
<th>No. KP</th>
<td>{{ verifyModalVoter.no_kp }}</td>
</tr>
<tr>
<th>No. Anggota</th>
<td>{{ verifyModalVoter.no_anggota }}</td>
</tr>
<tr>
<th>Unit</th>
<td>{{ verifyModalVoter.unit }}</td>
</tr>
<tr>
<th>Saham (RM)</th>
<td>{{ formatRmDisplay(verifyModalVoter.saham) }}</td>
</tr>
<tr>
<th>Yuran (RM)</th>
<td>{{ formatRmDisplay(verifyModalVoter.yuran) }}</td>
</tr>
</tbody>
</table>
<div class="alert alert-danger" v-if="verifyModalError" style="margin-bottom: 0;">
{{ verifyModalError }}
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Tutup</button>
<button type="button" class="btn btn-primary" :disabled="verifyModalLoading"
@click="submitFizikalVerify">
<i class="fa fa-check" aria-hidden="true"></i> Sahkan pendaftaran
</button>
</div>
</div>
</div>
</div>
<!-- Display attendance percentage -->
<div class="text-right">
<h5><b>{{ attendancePercentage }}% Kehadiran Pengundi</b></h5>
</div>
<ul class="pagination" v-if="pages.length > 1">
<router-link tag="li" v-for="page in pages" :key="page.page"
:to="{ query: paginationQueryForPage(page.page) }"
:class="{ active: Number(current_page) === page.page }" exact>
<a href="#">{{ page.page }}</a>
</router-link>
</ul>
</div>
</div>
</template>
<style>
.kehadiran-actions {
margin-bottom: 10px;
}
.kehadiran-stats {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
margin: 10px 0 14px;
}
.kehadiran-stat {
border: 1px solid #e5e7eb;
background: #fff;
border-radius: 10px;
padding: 10px 12px;
}
.kehadiran-stat--muted {
background: #f9fafb;
}
.kehadiran-stat__label {
color: #6b7280;
font-size: 12px;
font-weight: 800;
letter-spacing: 0.2px;
}
.kehadiran-stat__value {
margin-top: 4px;
font-size: 22px;
font-weight: 900;
color: #111827;
line-height: 1.1;
}
.kehadiran-stat__meta {
margin-top: 4px;
color: #374151;
font-size: 12px;
font-weight: 700;
}
.kehadiran-search {
margin: 10px 0 12px;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
}
.kehadiran-search__label {
color: #6b7280;
font-size: 12px;
font-weight: 700;
}
.kehadiran-search__input {
max-width: 220px;
height: 34px;
}
.kehadiran-filter-panel {
margin: 10px 0 14px;
border: 1px solid #e5e7eb;
border-radius: 10px;
background: #f9fafb;
padding: 12px 14px;
}
.kehadiran-filter-panel__title {
font-size: 11px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.06em;
color: #6b7280;
margin-bottom: 10px;
}
.kehadiran-filter-groups {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 12px;
}
.kehadiran-filter-group {
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 10px 12px;
}
.kehadiran-filter-group__title {
font-size: 12px;
font-weight: 700;
color: #374151;
margin-bottom: 8px;
line-height: 1.3;
}
.kehadiran-filter-group__chips {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
}
.kehadiran-chip {
border: 1px solid #d1d5db;
background: #fff;
color: #374151;
padding: 6px 12px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
line-height: 1;
}
.kehadiran-chip:hover {
background: #f3f4f6;
}
.kehadiran-chip.is-active {
background: #1976d2;
border-color: #1976d2;
color: #fff;
}
.kehadiran-action-cell {
display: inline-flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
}
.kehadiran-action-btn {
display: inline-flex;
align-items: center;
gap: 6px;
font-weight: 700;
}
.kehadiran-action-btn>i {
line-height: 1;
}
.kehadiran-verify-lead {
margin-bottom: 12px;
color: #374151;
line-height: 1.5;
}
.kehadiran-verify-summary {
margin-bottom: 0;
}
.kehadiran-tunai-readonly {
background-color: #f3f4f6 !important;
cursor: not-allowed;
color: #1f2937;
}
.kehadiran-pill {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 3px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 800;
line-height: 1.4;
border: 1px solid transparent;
white-space: nowrap;
}
.kehadiran-pill>i {
line-height: 1;
}
.kehadiran-pill--success {
color: #0f5132;
background: #d1e7dd;
border-color: #badbcc;
}
.kehadiran-pill--warn {
color: #664d03;
background: #fff3cd;
border-color: #ffecb5;
}
.kehadiran-pill--muted {
color: #374151;
background: #f3f4f6;
border-color: #e5e7eb;
}
</style>
<script>
export default {
data: function () {
var vm = this;
return {
id: 0,
voterLoading: false,
searchNoAnggota: '',
searchNoKp: '',
selectedVoter: null,
verifyModalVoter: null,
verifyModalLoading: false,
verifyModalError: '',
detailLoading: false,
detailError: '',
claimCodeInput: '',
payoutReference: '',
payoutNote: '',
kehadiranTableHeaders: [
{ title: 'Nama Anggota', key: 'name', sortable: true },
{ title: 'No. Kad Pengenalan', key: 'no_kp', sortable: true },
{ title: 'No. Anggota', key: 'no_anggota', sortable: true },
{ title: 'Unit', key: 'unit', sortable: true },
{
title: 'Kehadiran',
key: 'kehadiran',
sortable: true,
exportValue: function (item) {
return vm.getKehadiranString(item ? item.kehadiran : null);
}
},
{
title: 'Pendaftaran',
key: 'fizikal_reg',
sortable: false,
exportValue: function (item) {
if (!item) return '';
if (Number(item.kehadiran) !== 1) return '—';
return item.fizikal_registration_verified_at ? 'Disahkan' : 'Menunggu';
}
},
{
title: 'Undi',
key: 'has_voted',
sortable: false,
exportValue: function (item) {
return Number(item && item.votes_count) > 0 ? 'Undi' : 'Belum';
}
},
{
title: 'Status',
key: 'allowance',
sortable: false,
exportValue: function (item) {
return vm.isCashPaid(item) ? 'Sudah Bayar' : 'Belum Bayar';
}
},
{ title: 'Tindakan', key: 'action', sortable: false },
]
}
},
created: function () {
this.refreshVoter();
},
methods: {
search: function () {
},
refreshVoter: function () {
var vm = this;
this.voterLoading = true;
this.util.notify('Refreshing Voter', 'loading');
axios.get(config.API + 'voter', {
params: {
page: this.current_page,
kehadiran: this.current_kehadiran || undefined,
fizikal_reg: this.current_fizikal_reg || undefined,
has_voted: this.current_has_voted || undefined,
cash_status: this.current_cash_status || undefined,
no_anggota: this.searchNoAnggota || undefined,
no_kp: this.searchNoKp || undefined
}
})
.then(response => {
console.log(response)
$.notifyClose();
vm.data.voters = response.data;
})
.catch(error => {
$.notifyClose();
vm.util.showResult(error);
})
.finally(function () {
vm.voterLoading = false;
})
},
applySearch: function () {
// Reset to first page when searching
var q = Object.assign({}, this.$route.query);
q.page = 1;
this.$router.replace({ query: q });
this.refreshVoter();
},
clearSearch: function () {
this.searchNoAnggota = '';
this.searchNoKp = '';
this.applySearch();
},
setKehadiranFilter: function (val) {
var q = Object.assign({}, this.$route.query);
if (val === 1 || val === 2) q.kehadiran = String(val);
else if (val === 'unset') q.kehadiran = 'unset';
else delete q.kehadiran;
q.page = 1;
this.$router.replace({ query: q });
},
setFizikalRegFilter: function (val) {
var q = Object.assign({}, this.$route.query);
if (val === 'pending' || val === 'verified') q.fizikal_reg = val;
else delete q.fizikal_reg;
q.page = 1;
this.$router.replace({ query: q });
},
setHasVotedFilter: function (val) {
var q = Object.assign({}, this.$route.query);
if (val === '1' || val === '0') q.has_voted = val;
else delete q.has_voted;
q.page = 1;
this.$router.replace({ query: q });
},
setCashStatusFilter: function (val) {
var q = Object.assign({}, this.$route.query);
if (val === 'paid' || val === 'unpaid') q.cash_status = val;
else delete q.cash_status;
q.page = 1;
this.$router.replace({ query: q });
},
paginationQueryForPage: function (pageNum) {
var q = { page: pageNum };
if (this.current_kehadiran) q.kehadiran = this.current_kehadiran;
if (this.current_fizikal_reg) q.fizikal_reg = this.current_fizikal_reg;
if (this.current_has_voted) q.has_voted = this.current_has_voted;
if (this.current_cash_status) q.cash_status = this.current_cash_status;
return q;
},
canVerifyFizikalRegistration: function (voter) {
if (!voter) return false;
return Number(voter.kehadiran) === 1 && !voter.fizikal_registration_verified_at;
},
openFizikalVerifyModal: function (item) {
if (!item || !this.canVerifyFizikalRegistration(item)) return;
this.verifyModalError = '';
this.verifyModalVoter = Object.assign({}, item);
this.util.showModal('#fizikal-verify-modal');
},
submitFizikalVerify: function () {
var vm = this;
if (!this.verifyModalVoter) return;
this.verifyModalError = '';
this.verifyModalLoading = true;
axios.post(config.API + 'voter/' + this.verifyModalVoter.id + '/verify-fizikal-registration')
.then(function (response) {
if (response && response.data && response.data.status && response.data.status !== 'success') {
vm.verifyModalError = response.data.message || 'Permintaan gagal.';
vm.util.notify(vm.verifyModalError, 'error');
return;
}
if (vm.util.showResult(response, 'success')) {
var v = response.data.voter;
if (v && vm.selectedVoter && vm.selectedVoter.id === v.id) {
vm.selectedVoter.fizikal_registration_verified_at = v.fizikal_registration_verified_at;
}
vm.util.hideModal('#fizikal-verify-modal');
vm.verifyModalVoter = null;
vm.refreshVoter();
}
})
.catch(function (error) {
vm.verifyModalError =
(error && error.response && error.response.data && error.response.data.message)
? error.response.data.message
: (error && error.message ? error.message : 'Gagal mengesahkan pendaftaran.');
if (error && error.response) vm.util.showResult(error, 'error');
else vm.util.notify(vm.verifyModalError, 'error');
})
.finally(function () {
vm.verifyModalLoading = false;
});
},
getKehadiranString(kehadiran) {
// Map integer values to corresponding string representations
switch (kehadiran) {
case 1:
return 'Fizikal';
case 2:
return 'Maya';
default:
return '-';
}
},
getPDFKehadiranurl(Id) {
// Construct and return the URL based on the item ID
return `/kehadiranpdf/${Id}`;
},
isCashPaid: function (voter) {
return Boolean(voter && voter.cash_paid_at);
},
canPayCash: function (voter) {
if (!voter) return false;
if (this.isCashPaid(voter)) return false;
if (Number(voter.kehadiran) !== 1) return false;
if (Number(voter.votes_count) > 0) return true;
return this.canClaimAllowanceWithoutVoteAdmin(voter);
},
openDetail: function (item) {
this.detailError = '';
this.selectedVoter = Object.assign({}, item);
this.claimCodeInput = '';
this.payoutReference =
item.cash_payout_reference != null && item.cash_payout_reference !== undefined
? String(item.cash_payout_reference)
: '';
this.payoutNote =
item.cash_payout_note != null && item.cash_payout_note !== undefined
? String(item.cash_payout_note)
: '';
this.util.showModal('#allowance-modal');
},
/** Saham / Yuran — same RM formatting as voter portal. */
formatRmDisplay: function (val) {
if (val === null || val === undefined || val === '') {
return '—';
}
var n = Number(val);
if (Number.isNaN(n)) {
return String(val);
}
return n.toLocaleString('en-MY', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
},
canClaimAllowanceWithoutVoteAdmin: function (voter) {
if (!voter || Number(voter.kehadiran) !== 1) {
return false;
}
if (Number(voter.votes_count) > 0) {
return false;
}
var s = Number(voter.saham);
var canVoteBySaham = !Number.isNaN(s) && s >= 500;
var canProceed =
Number(voter.kehadiran) !== 0 &&
(Number(voter.kehadiran) !== 1 || voter.fizikal_registration_verified_at);
return canProceed && !canVoteBySaham;
},
payCash: function () {
if (!this.selectedVoter) return;
this.detailError = '';
this.detailLoading = true;
let payload = {
voter_id: this.selectedVoter.id,
method: 'cash',
claim_code: this.claimCodeInput || undefined,
reference: this.payoutReference || undefined,
note: this.payoutNote || undefined,
};
var rm = this.allowanceTunaiAmountRM;
if (rm !== null && rm !== undefined && !Number.isNaN(Number(rm))) {
payload.amount_cents = Math.round(Number(rm) * 100);
}
axios.post(config.API + 'allowance/payout', payload)
.then((response) => {
// Some endpoints return 200 with {status:'failed', message:'...'}
if (response && response.data && response.data.status && response.data.status !== 'success') {
this.detailError = response.data.message || 'Permintaan gagal.';
this.util.notify(this.detailError, 'error');
return;
}
if (this.util.showResult(response, 'success')) {
// refresh list so cash_paid_at & cash_payout_id updates
this.refreshVoter();
// keep modal open but reflect paid state (best effort)
if (response && response.data && response.data.payout) {
var p = response.data.payout;
this.selectedVoter.cash_paid_at = p.paid_at;
this.selectedVoter.cash_payout_id = p.id;
this.selectedVoter.cash_paid_by_admin_name =
(response && response.data && response.data.paid_by_admin_name)
? response.data.paid_by_admin_name
: this.selectedVoter.cash_paid_by_admin_name;
this.selectedVoter.cash_paid_by_admin_email =
(response && response.data && response.data.paid_by_admin_email)
? response.data.paid_by_admin_email
: this.selectedVoter.cash_paid_by_admin_email;
if (p.reference != null) {
this.selectedVoter.cash_payout_reference = p.reference;
this.payoutReference = p.reference ? String(p.reference) : '';
}
if (p.note != null) {
this.selectedVoter.cash_payout_note = p.note;
this.payoutNote = p.note ? String(p.note) : '';
}
} else {
this.selectedVoter.cash_paid_at = (new Date()).toISOString();
}
}
})
.catch((error) => {
this.detailError =
(error && error.response && error.response.data && error.response.data.message)
? error.response.data.message
: (error && error.message ? error.message : 'Gagal merekod bayaran.');
// Only pass actual axios error objects into showResult
if (error && error.response) this.util.showResult(error, 'error');
else this.util.notify(this.detailError, 'error');
})
.finally(() => {
this.detailLoading = false;
});
},
voidCashPayout: function () {
if (!this.selectedVoter || !this.selectedVoter.cash_payout_id) {
this.detailError = 'Rekod bayaran tidak dijumpai untuk dibatalkan.';
return;
}
this.detailError = '';
this.detailLoading = true;
axios.post(config.API + 'allowance/void/' + this.selectedVoter.cash_payout_id, {
note: this.payoutNote || undefined
})
.then((response) => {
if (response && response.data && response.data.status && response.data.status !== 'success') {
this.detailError = response.data.message || 'Permintaan gagal.';
this.util.notify(this.detailError, 'error');
return;
}
if (this.util.showResult(response, 'success')) {
this.refreshVoter();
this.selectedVoter.cash_paid_at = null;
this.selectedVoter.cash_payout_id = null;
this.selectedVoter.cash_payout_reference = null;
this.selectedVoter.cash_payout_note = null;
this.payoutReference = '';
this.payoutNote = '';
}
})
.catch((error) => {
this.detailError =
(error && error.response && error.response.data && error.response.data.message)
? error.response.data.message
: (error && error.message ? error.message : 'Gagal membatalkan bayaran.');
if (error && error.response) this.util.showResult(error, 'error');
else this.util.notify(this.detailError, 'error');
})
.finally(() => {
this.detailLoading = false;
});
}
},
watch: {
'$route.query.page': function () {
$.notifyClose();
this.refreshVoter();
},
'$route.query.kehadiran': function () {
$.notifyClose();
this.refreshVoter();
},
'$route.query.fizikal_reg': function () {
$.notifyClose();
this.refreshVoter();
},
'$route.query.has_voted': function () {
$.notifyClose();
this.refreshVoter();
},
'$route.query.cash_status': function () {
$.notifyClose();
this.refreshVoter();
}
},
computed: {
/** RM300 fizikal, RM150 Maya — used for tunai payout amount. */
allowanceTunaiAmountRM: function () {
if (!this.selectedVoter) return null;
var k = Number(this.selectedVoter.kehadiran);
if (k === 1) return 300;
if (k === 2) return 150;
return null;
},
allowanceTunaiDisplay: function () {
var rm = this.allowanceTunaiAmountRM;
if (rm === null || rm === undefined) return '';
return Number(rm).toFixed(2);
},
hasAnySearch: function () {
return Boolean((this.searchNoAnggota && this.searchNoAnggota.length) || (this.searchNoKp && this.searchNoKp.length));
},
voterListItems: function () {
var v = this.data.voters;
if (v && Array.isArray(v.data)) {
return v.data;
}
return [];
},
pages: function () {
var pages = [];
if (this.data.voters.last_page)
for (var i = 1; i <= this.data.voters.last_page; i++) {
let x = {};
x['page'] = i;
pages.push(x);
}
return pages;
},
current_page: function () {
return this.$route.query.page ? this.$route.query.page : 1;
},
current_kehadiran: function () {
return this.$route.query.kehadiran ? this.$route.query.kehadiran : null;
},
current_fizikal_reg: function () {
return this.$route.query.fizikal_reg ? this.$route.query.fizikal_reg : null;
},
current_has_voted: function () {
var v = this.$route.query.has_voted;
if (v === '1' || v === '0') {
return v;
}
return null;
},
current_cash_status: function () {
var v = this.$route.query.cash_status;
if (v === 'paid' || v === 'unpaid') {
return v;
}
return null;
},
verifyModalVoterNeedsLowSahamNote: function () {
if (!this.verifyModalVoter) return false;
var v = this.verifyModalVoter;
if (Number(v.kehadiran) !== 1) return false;
var s = Number(v.saham);
return !Number.isNaN(s) && s < 500;
},
selectedVoterShowsNoVoteEligibleNote: function () {
if (!this.selectedVoter) return false;
return this.canClaimAllowanceWithoutVoteAdmin(this.selectedVoter);
},
// Calculate total attendance percentage
attendancePercentage() {
// Count total members who attended physically (Fizikal)
const fizikalCount = this.voterListItems.reduce((total, voter) => {
return Number(voter && voter.kehadiran) === 1 ? total + 1 : total;
}, 0);
// Count total members who attended virtually (Maya)
const mayaCount = this.voterListItems.reduce((total, voter) => {
return Number(voter && voter.kehadiran) === 2 ? total + 1 : total;
}, 0);
// Calculate total attendance percentage
const totalAttendance = fizikalCount + mayaCount;
const totalMembers = this.voterListItems.length;
return totalMembers === 0 ? 0 : ((totalAttendance / totalMembers) * 100).toFixed(2);
},
attendanceStats: function () {
var items = this.voterListItems || [];
var fizikalCount = items.reduce(function (total, voter) {
return Number(voter && voter.kehadiran) === 1 ? total + 1 : total;
}, 0);
var mayaCount = items.reduce(function (total, voter) {
return Number(voter && voter.kehadiran) === 2 ? total + 1 : total;
}, 0);
var totalAttendance = fizikalCount + mayaCount;
var totalMembers = items.length;
var denom = totalMembers === 0 ? 1 : totalMembers;
return {
fizikalCount: fizikalCount,
mayaCount: mayaCount,
totalAttendance: totalAttendance,
fizikalPct: totalMembers === 0 ? '0.00' : ((fizikalCount / denom) * 100).toFixed(2),
mayaPct: totalMembers === 0 ? '0.00' : ((mayaCount / denom) * 100).toFixed(2),
totalPct: totalMembers === 0 ? '0.00' : ((totalAttendance / denom) * 100).toFixed(2),
};
},
}
}
</script>