898 lines
27 KiB
Vue
898 lines
27 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-filters">
|
|
<span class="kehadiran-filters__label">Penapis:</span>
|
|
<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 class="kehadiran-filters kehadiran-filters--secondary">
|
|
<span class="kehadiran-filters__label">Pendaftaran fizikal:</span>
|
|
<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>
|
|
|
|
|
|
<!-- <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">×</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 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">×</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-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-filters {
|
|
margin: 6px 0 14px;
|
|
display: flex;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
}
|
|
|
|
.kehadiran-filters--secondary {
|
|
margin-top: 0;
|
|
}
|
|
|
|
.kehadiran-filters__label {
|
|
color: #6b7280;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
margin-right: 4px;
|
|
}
|
|
|
|
.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 () {
|
|
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 },
|
|
{ title: 'Pendaftaran', key: 'fizikal_reg', sortable: false },
|
|
{ title: 'Undi', key: 'has_voted', sortable: false },
|
|
{ title: 'Status', key: 'allowance', sortable: false },
|
|
{ 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,
|
|
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 });
|
|
},
|
|
|
|
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;
|
|
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;
|
|
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();
|
|
}
|
|
},
|
|
|
|
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;
|
|
},
|
|
|
|
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 voter.kehadiran === 1 ? total + 1 : total;
|
|
}, 0);
|
|
|
|
// Count total members who attended virtually (Maya)
|
|
const mayaCount = this.voterListItems.reduce((total, voter) => {
|
|
return 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);
|
|
},
|
|
|
|
|
|
}
|
|
|
|
}
|
|
</script> |