DONE: settle disable vote for voter with saham under 500, fix issue on...

This commit is contained in:
ISMAIL MASSERAN
2026-04-19 07:52:11 +00:00
parent ed369aec46
commit f5b831a857
9 changed files with 285 additions and 24 deletions
@@ -163,6 +163,14 @@
<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>
@@ -170,6 +178,9 @@
: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>
@@ -206,14 +217,15 @@
<div class="col-md-4">
<label>Rujukan (optional)</label>
<input type="text" class="form-control" v-model.trim="payoutReference"
maxlength="80" />
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" />
<input type="text" class="form-control" v-model.trim="payoutNote"
placeholder="Nama Wakil" />
</div>
</div>
@@ -251,7 +263,13 @@
</button>
</div>
<div class="modal-body" v-if="verifyModalVoter">
<p class="kehadiran-verify-lead">
<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">
@@ -272,6 +290,14 @@
<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;">
@@ -615,19 +641,53 @@ export default {
canPayCash: function (voter) {
if (!voter) return false;
if (this.isCashPaid(voter)) return false;
// Must be fizikal, must have voted
return Number(voter.kehadiran) === 1 && Number(voter.votes_count) > 0;
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 = '';
this.payoutNote = '';
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 = '';
@@ -660,8 +720,17 @@ export default {
this.refreshVoter();
// keep modal open but reflect paid state (best effort)
if (response && response.data && response.data.payout) {
this.selectedVoter.cash_paid_at = response.data.payout.paid_at;
this.selectedVoter.cash_payout_id = response.data.payout.id;
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();
}
@@ -703,6 +772,10 @@ export default {
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) => {
@@ -787,6 +860,19 @@ export default {
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)