DONE: remove verified by ajk, fix bug on pagination, improve UI

This commit is contained in:
ISMAIL MASSERAN
2026-04-27 12:06:54 +08:00
parent bd2306b073
commit 4425fa6176
20 changed files with 77081 additions and 535 deletions
@@ -1,92 +1,24 @@
<template>
<div class="attendance-page row col-md-10 col-md-offset-1">
<div class="col-md-12">
<div class="attendance-hero">
<div class="attendance-section-kicker">Pengesahan Kehadiran</div>
<h4 class="attendance-page-title">Sahkan Kehadiran Mesyuarat Agung</h4>
<p class="attendance-page-subtitle">
Sila semak maklumat anda, pilih kaedah kehadiran, dan sahkan penyertaan sebelum meneruskan.
</p>
<a href="https://drive.google.com/uc?export=download&id=1O5DVDIbGy-w0bejV6RqtJGIty-_5LJD8"
target="_blank" class="attendance-download-button">
Muat Turun Etika Mesyuarat Agung
</a>
<div v-if="Number(data.user.kehadiran) === 0" class="attendance-info-top">
<div class="attendance-info-top__kicker">Makluman</div>
<div class="attendance-info-top__body">
<p><strong>*</strong> Pendaftaran akan dibuka dari <strong>jam 7.00 pagi hingga 10.00
pagi pada hari pengundian.</strong></p>
<p>
<strong>*</strong> Elaun kehadiran berjumlah <strong>RM300.00 (secara fizikal)</strong>
akan
diberikan secara TUNAI selepas mesyuarat. <br />
Manakala <strong>RM150.00 (secara atas talian)</strong> akan dikreditkan ke akaun
anggota
yang
hadir penuh.
</p>
</div>
</div>
<form @submit.prevent="edit()" id="edit_form" class="attendance-card">
<div class="attendance-card-header">
<div>
<div class="attendance-section-kicker">Maklumat Anggota</div>
<h5 class="attendance-card-title">Butiran Kehadiran</h5>
</div>
</div>
<div class="attendance-info-grid">
<div class="form-group attendance-field">
<label for="name">Nama</label>
<input type="text" name="name" class="form-control attendance-input" readonly
:value="data.user.name" required />
</div>
<div class="form-group attendance-field">
<label for="no_kp">No. Kad Pengenalan</label>
<input type="text" name="no_kp" class="form-control attendance-input" readonly
:value="data.user.no_kp" required />
</div>
<div class="form-group attendance-field">
<label for="no_anggota">No. Anggota</label>
<input type="text" name="no_anggota" class="form-control attendance-input" readonly
:value="data.user.no_anggota" required />
</div>
<div class="form-group attendance-field">
<label for="unit">Unit</label>
<input type="text" name="unit" class="form-control attendance-input" readonly
:value="data.user.unit" required />
</div>
</div>
<!-- Sembunyikan borang pendaftaran selepas kehadiran telah dihantar -->
<template v-if="Number(data.user.kehadiran) === 0">
<div class="form-group attendance-choice-box">
<label for="kehadiran">Kehadiran</label>
<select v-model="selectedKehadiran" class="form-control attendance-select" id="kehadiran"
name="kehadiran" required>
<option value="0">Sila Pilih :</option>
<option value="1">Fizikal</option>
<option value="2">Maya</option>
</select>
<small class="form-text text-muted attendance-helper-text">
Sila pilih kehadiran (Maya/Fizikal) kemudian klik SAYA TERIMA.
</small>
</div>
<div v-if="selectedKehadiran === '1'" class="form-group attendance-field">
<label for="physical_gate_code">Kod skrin kaunter pendaftaran</label>
<input type="text" name="physical_gate_code" id="physical_gate_code"
class="form-control attendance-input" autocomplete="off" autocapitalize="characters"
inputmode="text" placeholder="8 aksara, semak skrin di kaunter" maxlength="64"
@input="onPhysicalGateCodeInput" />
<small class="form-text text-muted attendance-helper-text">
Kod berputar setiap minit masukkan kod semasa yang dipaparkan di kaunter sebelum
menghantar.
</small>
</div>
<div class="alert alert-info mt-4 small attendance-info-notice">
<p><strong>*</strong> Pendaftaran akan dibuka dari <strong>jam 7.00 pagi hingga 10.00 pagi (25
April
2025)</strong>.</p>
<p>
<strong>*</strong> Elaun kehadiran berjumlah <strong>RM300.00 (secara fizikal)</strong> akan
diberikan secara TUNAI selepas mesyuarat. <br />
Manakala <strong>RM150.00 (secara atas talian)</strong> akan dikreditkan ke akaun anggota
yang
hadir penuh.
</p>
</div>
</template>
<div class="text-center mt-4 attendance-action-area">
<div v-if="data.user.kehadiran != 0 && canProceedToVote" class="attendance-success-card">
@@ -128,21 +60,96 @@
</button>
</div>
<button type="submit" class="btn btn-lg btn-success attendance-submit-button"
v-else-if="electionVotingActive">
DAFTAR KEHADIRAN
</button>
<div v-else class="alert alert-info text-left attendance-voting-notice">
<p class="attendance-voting-notice__title"><strong>Butang DAFTAR KEHADIRAN belum
tersedia</strong>
</p>
<p class="attendance-voting-notice__body">
Buat masa ini proses undian / mesyuarat <strong>belum bermula</strong>.
</p>
<template v-else>
<!-- Title -->
<h5 class="attendance-section-kicker">Butiran Kehadiran</h5>
<div class="attendance-register-box">
<p class="attendance-precheck">
Sila semak maklumat anda di bawah terlebih dahulu sebelum mendaftar kehadiran.
</p>
<div class="form-group attendance-choice-box">
<label for="kehadiran">Kehadiran</label>
<select v-model="selectedKehadiran" class="form-control attendance-select"
id="kehadiran" name="kehadiran" required>
<option value="0">Sila Pilih :</option>
<option value="1">Fizikal</option>
<option value="2">Maya</option>
</select>
<small class="form-text text-muted attendance-helper-text">
Sila pilih kehadiran (Maya/Fizikal) kemudian klik SAYA TERIMA.
</small>
</div>
<div v-if="selectedKehadiran === '1'" class="form-group attendance-field">
<label for="physical_gate_code">Kod skrin kaunter pendaftaran</label>
<input type="text" name="physical_gate_code" id="physical_gate_code"
class="form-control attendance-input" autocomplete="off" autocapitalize="characters"
inputmode="text" placeholder="8 aksara, semak skrin di kaunter" maxlength="64"
@input="onPhysicalGateCodeInput" />
<small class="form-text text-muted attendance-helper-text">
Kod berputar setiap minit masukkan kod semasa yang dipaparkan di kaunter sebelum
menghantar.
</small>
</div>
<div class="attendance-register-box__actions">
<button type="submit" class="btn btn-lg btn-success attendance-submit-button"
v-if="electionVotingActive">
DAFTAR KEHADIRAN
</button>
<div v-else class="alert alert-info attendance-voting-notice">
<p class="attendance-voting-notice__title"><strong>Butang DAFTAR KEHADIRAN belum
tersedia</strong>
</p>
<p class="attendance-voting-notice__body">
Buat masa ini proses undian / mesyuarat <strong>belum bermula</strong>.
</p>
</div>
</div>
</div>
</template>
</div>
<div class="attendance-info-grid">
<div class="form-group attendance-field">
<label for="name">Nama</label>
<input type="text" name="name" class="form-control attendance-input" readonly
:value="data.user.name" required />
</div>
<div class="form-group attendance-field">
<label for="no_kp">No. Kad Pengenalan</label>
<input type="text" name="no_kp" class="form-control attendance-input" readonly
:value="data.user.no_kp" required />
</div>
<div class="form-group attendance-field">
<label for="no_anggota">No. Anggota</label>
<input type="text" name="no_anggota" class="form-control attendance-input" readonly
:value="data.user.no_anggota" required />
</div>
<div class="form-group attendance-field">
<label for="unit">Unit</label>
<input type="text" name="unit" class="form-control attendance-input" readonly
:value="data.user.unit" required />
</div>
</div>
</form>
<div class="attendance-hero">
<div class="attendance-section-kicker">Pengesahan Kehadiran</div>
<h4 class="attendance-page-title">Sahkan Kehadiran Mesyuarat Agung</h4>
<p class="attendance-page-subtitle">
Sila semak maklumat anda, pilih kaedah kehadiran, dan sahkan penyertaan sebelum meneruskan.
</p>
<a href="https://drive.google.com/uc?export=download&id=1O5DVDIbGy-w0bejV6RqtJGIty-_5LJD8"
target="_blank" class="attendance-download-button">
Muat Turun Etika Mesyuarat Agung
</a>
</div>
</div>
</div>
</template>
@@ -155,6 +162,7 @@
.attendance-hero {
margin-bottom: 22px;
margin-top: 22px;
padding: 26px 28px;
border-radius: 22px;
background: linear-gradient(135deg, #ffffff 0%, #eef6ff 100%);
@@ -173,12 +181,6 @@
}
.attendance-page-title,
.attendance-card-title {
margin: 0 0 8px;
color: #17324d;
font-weight: 700;
}
.attendance-page-subtitle {
max-width: 700px;
margin: 0 auto 18px;
@@ -206,10 +208,6 @@
box-shadow: 0 16px 34px rgba(35, 64, 97, 0.08);
}
.attendance-card-header {
margin-bottom: 18px;
}
.attendance-info-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -255,6 +253,49 @@
margin-top: 10px;
}
.attendance-precheck {
margin: 0 0 12px;
color: #5f7388;
line-height: 1.5;
}
.attendance-register-box {
text-align: left;
padding: 18px;
border-radius: 18px;
border: 1px solid #e6edf6;
background: linear-gradient(180deg, #ffffff 0%, #f7fbff 100%);
box-shadow: 0 14px 28px rgba(35, 64, 97, 0.08);
}
.attendance-register-box__actions {
margin-top: 16px;
text-align: center;
}
.attendance-info-top {
margin-bottom: 14px;
padding: 16px 18px;
border-radius: 16px;
border: 1px solid #cfe2ff;
background: linear-gradient(180deg, #f2f8ff 0%, #ffffff 100%);
box-shadow: 0 10px 24px rgba(13, 110, 253, 0.08);
text-align: left;
}
.attendance-info-top__kicker {
margin-bottom: 8px;
color: #0d6efd;
font-weight: 800;
letter-spacing: 0.06em;
text-transform: uppercase;
font-size: 0.95rem;
}
.attendance-info-top__body p:last-child {
margin-bottom: 0;
}
.attendance-info-notice {
margin-bottom: 0;
padding: 16px 18px;
@@ -422,7 +463,8 @@ export default {
},
/**
* Maya (2) can vote once attendance is set. Fizikal (1) needs admin verification timestamp.
* Maya (2) can vote once attendance is set. Fizikal (1) can proceed once verified timestamp exists.
* (Backend now auto-verifies Fizikal on submit.)
*/
canProceedToVote: function () {
var u = this.data && this.data.user;
@@ -9,12 +9,10 @@
<div class="home-hero-banner-shine" aria-hidden="true"></div>
</div>
<div class="hero-banner-caption home-hero-caption">
<div class="hero-banner-kicker">Portal Pengundian</div>
<h1 class="hero-banner-title">Maklumat Anggota</h1>
<h1 class="hero-banner-title">Portal Pengundian</h1>
<p class="hero-banner-subtitle">
Semak butiran keahlian anda. Gunakan
<strong class="home-hero-strong">pautan pantas</strong> di bawah untuk navigasi pantas ke halaman utama
portal.
Gunakan <strong class="home-hero-strong">pautan pantas</strong> di bawah untuk navigasi pantas ke
halaman utama portal.
</p>
<div class="home-hero-badges">
<span class="home-badge home-badge--location">
@@ -31,6 +29,88 @@
</div>
</div>
<!-- Butiran Keahlian and Coupon Claim Code -->
<div class="row home-content-row">
<div class="col-xs-12 col-md-6 home-column home-column--primary">
<!-- Member Information Card -->
<div class="home-section-container">
<h2 class="home-section-heading">
<span class="home-section-heading-icon" aria-hidden="true"><i class="fa fa-list-alt"></i></span>
Maklumat Anggota
</h2>
<div v-if="showRegisterCta" class="home-member-cta">
<div class="home-member-cta-hint home-member-cta-hint--warning" role="alert">
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
<span>Sila semak butiran keahlian anda sebelum mendaftar kehadiran.</span>
</div>
<div class="home-member-cta-btn-container">
<router-link :to="{ name: 'Attendance' }" class="btn btn-primary btn-block home-member-cta-btn">
<i class="fa fa-pencil-square-o" aria-hidden="true"></i>
Daftar MAT Sekarang
</router-link>
</div>
</div>
<div class="member-info-card home-member-card home-member-card--in-section">
<div v-for="row in memberRows" :key="row.label" class="member-field">
<span class="member-field-icon" aria-hidden="true"><i class="fa" :class="row.icon"></i></span>
<div class="member-field-body">
<span class="member-field-label">{{ row.label }}</span>
<span class="member-field-value">{{ row.value }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- Coupon Claim Code Card -->
<div class="col-xs-12 col-md-6 home-column home-column--secondary">
<div class="home-claim-row">
<div class="home-section-container home-section-container--claim">
<h2 class="home-section-heading">
<span class="home-section-heading-icon home-section-heading-icon--teal" aria-hidden="true"><i
class="fa fa-ticket"></i></span>
Kod Tuntutan Elaun
</h2>
<div class="home-claim-card">
<div v-if="!canGenerateClaimCode" class="home-claim-locked">
<div class="home-claim-locked-icon"><i class="fa fa-lock"></i></div>
<p class="home-claim-locked-text">
Kod tuntutan hanya tersedia untuk kehadiran <strong>Fizikal</strong> selepas anda
<strong>selesai mengundi</strong>.
</p>
</div>
<div v-else class="home-claim-active">
<p class="home-claim-hint">
Tunjukkan kod ini kepada petugas di kaunter untuk pengesahan bayaran tunai.
</p>
<div v-if="claimError" class="alert alert-danger home-claim-alert">
{{ claimError }}
</div>
<div v-if="claimCode" class="home-claim-code-box">
<div class="home-claim-code">{{ claimCode }}</div>
<div class="home-claim-expiry">
Tamat tempoh: <strong>{{ claimExpiresAt || '—' }}</strong>
</div>
</div>
<button type="button" class="btn btn-primary btn-block home-claim-btn" :disabled="claimLoading"
@click="generateClaimCode()">
<i class="fa fa-refresh" :class="{ 'fa-spin': claimLoading }"></i>
{{ claimLoading ? ' Menjana...' : (claimCode ? ' Jana Semula Kod' : ' Jana Kod Tuntutan') }}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Pautan Pantas -->
<div class="row home-quick-links-row">
<div class="col-xs-12">
<div class="home-section-container">
@@ -63,6 +143,54 @@
</div>
</div>
<!-- Kad Ahli Digital -->
<div class="col-xs-12">
<div class="home-section-container home-section-container--kad">
<h2 class="home-section-heading">
<span class="home-section-heading-icon" aria-hidden="true"><i class="fa fa-credit-card"></i></span>
Kad Ahli Digital
</h2>
<div class="home-kad-wrap">
<div class="kad-keahlian">
<div class="member-card-vertical">MEMBER CARD</div>
<div class="kad-logo">
<img src="/images/logo_kad.png" alt="Logo" class="kad-logo-img">
</div>
<div class="kad-content">
<img src="images/KOPKB_Final.png" class="watermark" alt="">
<div class="member-details">
<div class="detail-item">
<span class="detail-label">NAMA:</span>
<span class="detail-value">{{ data.user.name }}</span>
</div>
<div class="detail-item">
<span class="detail-label">NO KAD PENGENALAN:</span>
<span class="detail-value">{{ data.user.no_kp }}</span>
</div>
<div class="detail-item">
<span class="detail-label">NO ANGGOTA:</span>
<span class="detail-value">{{ data.user.no_anggota }}</span>
</div>
<div class="detail-item">
<span class="detail-label">UNIT:</span>
<span class="detail-value">{{ data.user.unit }}</span>
</div>
<div class="detail-item detail-item--barcode">
<svg id="barcode" class="detail-value">{{ data.user.no_kp }}</svg>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Saham Vote Blocked Message -->
<div v-if="showSahamVoteBlockedMessage" class="row home-saham-block-row">
<div class="col-xs-12">
<div class="alert alert-warning home-saham-block-alert" role="alert">
@@ -71,112 +199,6 @@
</div>
</div>
</div>
<div class="row home-content-row">
<div class="col-xs-12 col-md-6 home-column home-column--primary">
<div class="home-section-container">
<h2 class="home-section-heading">
<span class="home-section-heading-icon" aria-hidden="true"><i class="fa fa-list-alt"></i></span>
Butiran Keahlian
</h2>
<div class="member-info-card home-member-card home-member-card--in-section">
<div v-for="row in memberRows" :key="row.label" class="member-field">
<span class="member-field-icon" aria-hidden="true"><i class="fa" :class="row.icon"></i></span>
<div class="member-field-body">
<span class="member-field-label">{{ row.label }}</span>
<span class="member-field-value">{{ row.value }}</span>
</div>
</div>
</div>
</div>
</div>
<div class="col-xs-12 col-md-6 home-column home-column--secondary">
<div class="home-section-container home-section-container--claim">
<h2 class="home-section-heading">
<span class="home-section-heading-icon home-section-heading-icon--teal" aria-hidden="true"><i
class="fa fa-ticket"></i></span>
Kod Tuntutan Elaun
</h2>
<div class="home-claim-card">
<div v-if="!canGenerateClaimCode" class="home-claim-locked">
<div class="home-claim-locked-icon"><i class="fa fa-lock"></i></div>
<p class="home-claim-locked-text">
Kod tuntutan hanya tersedia untuk kehadiran <strong>Fizikal</strong> selepas anda
<strong>selesai mengundi</strong>.
</p>
</div>
<div v-else class="home-claim-active">
<p class="home-claim-hint">
Tunjukkan kod ini kepada petugas di kaunter untuk pengesahan bayaran tunai.
</p>
<div v-if="claimError" class="alert alert-danger home-claim-alert">
{{ claimError }}
</div>
<div v-if="claimCode" class="home-claim-code-box">
<div class="home-claim-code">{{ claimCode }}</div>
<div class="home-claim-expiry">
Tamat tempoh: <strong>{{ claimExpiresAt || '—' }}</strong>
</div>
</div>
<button type="button" class="btn btn-primary btn-block home-claim-btn" :disabled="claimLoading"
@click="generateClaimCode()">
<i class="fa fa-refresh" :class="{ 'fa-spin': claimLoading }"></i>
{{ claimLoading ? ' Menjana...' : (claimCode ? ' Jana Semula Kod' : ' Jana Kod Tuntutan') }}
</button>
</div>
</div>
</div>
<div class="home-section-container home-section-container--kad">
<h2 class="home-section-heading">
<span class="home-section-heading-icon" aria-hidden="true"><i class="fa fa-credit-card"></i></span>
Kad Ahli Digital
</h2>
<div class="home-kad-wrap">
<div class="kad-keahlian">
<div class="member-card-vertical">MEMBER CARD</div>
<div class="kad-logo">
<img src="/images/logo_kad.png" alt="Logo" class="kad-logo-img">
</div>
<div class="kad-content">
<img src="images/KOPKB_Final.png" class="watermark" alt="">
<div class="member-details">
<div class="detail-item">
<span class="detail-label">NAMA:</span>
<span class="detail-value">{{ data.user.name }}</span>
</div>
<div class="detail-item">
<span class="detail-label">NO KAD PENGENALAN:</span>
<span class="detail-value">{{ data.user.no_kp }}</span>
</div>
<div class="detail-item">
<span class="detail-label">NO ANGGOTA:</span>
<span class="detail-value">{{ data.user.no_anggota }}</span>
</div>
<div class="detail-item">
<span class="detail-label">UNIT:</span>
<span class="detail-value">{{ data.user.unit }}</span>
</div>
<div class="detail-item detail-item--barcode">
<svg id="barcode" class="detail-value">{{ data.user.no_kp }}</svg>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
@@ -242,6 +264,15 @@ export default {
? this.portalSettings.home_mat_label
: "MAT KE-29";
const links = [
{
key: "attendance",
title: "Daftar MAT",
desc: "Daftar kehadiran fizikal atau maya",
icon: "fa-pencil-square-o",
route: { name: "Attendance" },
variant: "home-cta-link--teal",
external: false
},
{
key: "calon",
title: "Maklumat Calon",
@@ -259,15 +290,6 @@ export default {
href: this.matLinkUrl,
variant: "home-cta-link--violet",
external: true
},
{
key: "attendance",
title: "Daftar MAT",
desc: "Daftar kehadiran fizikal atau maya",
icon: "fa-pencil-square-o",
route: { name: "Attendance" },
variant: "home-cta-link--teal",
external: false
}
];
@@ -330,6 +352,11 @@ export default {
return "Belum daftar kehadiran";
},
showRegisterCta() {
// Only show the registration CTA when user hasn't registered attendance yet.
return Number(this.data?.user?.kehadiran) === 0;
},
memberRows() {
const u = this.data?.user || {};
const fmt = (n) => {
@@ -438,11 +465,26 @@ export default {
}
.home-content-row {
margin-top: 8px;
margin-left: 0;
margin-right: 0;
}
.home-quick-links-row {
margin-left: 0;
margin-right: 0;
margin-top: 18px;
}
.home-claim-row {
margin-left: 0;
margin-right: 0;
margin-top: 18px;
}
.home-content-row .home-column--secondary .home-claim-row {
margin-top: 0;
}
.home-column--primary {
padding-top: 4px;
}
@@ -456,6 +498,10 @@ export default {
margin-top: 8px;
padding-top: 0;
}
.home-content-row .home-column--secondary .home-claim-row {
margin-top: 18px;
}
}
@media (min-width: 992px) {
@@ -529,15 +575,6 @@ export default {
0 1px 0 rgba(255, 255, 255, 0.8) inset;
}
.hero-banner-kicker {
margin-bottom: 10px;
color: #2563eb;
font-size: 1.15rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.hero-banner-title {
margin: 0 0 10px;
color: #0f172a;
@@ -567,6 +604,73 @@ export default {
margin-top: 4px;
}
.home-hero-actions {
margin: 14px 0 8px;
display: flex;
flex-direction: column;
gap: 8px;
max-width: 520px;
}
.home-hero-register-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
padding: 12px 16px;
border-radius: 12px;
font-weight: 800;
font-size: 1.35rem;
box-shadow: 0 10px 24px rgba(13, 110, 253, 0.35);
}
.home-hero-register-hint {
color: #64748b;
font-size: 1.2rem;
line-height: 1.35;
}
.home-member-cta {
margin: 0 0 14px;
}
.home-member-cta-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
padding: 12px 16px;
border-radius: 12px;
font-weight: 800;
font-size: 1.35rem;
box-shadow: 0 10px 24px rgba(13, 110, 253, 0.35);
}
.home-member-cta-hint {
margin-top: 8px;
color: #64748b;
font-size: 1.2rem;
line-height: 1.35;
}
.home-member-cta-hint--warning {
margin-top: 0;
margin-bottom: 10px;
display: flex;
gap: 10px;
align-items: flex-start;
padding: 10px 12px;
border-radius: 12px;
background: #fffbeb;
border: 1px solid rgba(245, 158, 11, 0.45);
color: #92400e;
}
.home-member-cta-hint--warning .fa {
flex-shrink: 0;
margin-top: 2px;
}
.home-badge {
display: inline-flex;
align-items: center;
@@ -56,9 +56,6 @@
<img :src="nomineeViewed.photo" class="thumbnail nominee-viewed-photo" />
<div class="nominee-photo-meta">
<div class="nominee-name">{{ nomineeViewed.name }}</div>
<div class="nominee-subtitle">
Calon No. {{ nomineeViewed.no_calon || '-' }}
</div>
</div>
</div>
</div>
@@ -326,7 +323,8 @@ export default {
.nominee-viewed-photo {
width: 100%;
height: 320px;
object-fit: cover;
object-fit: contain;
object-position: center;
border: 0;
border-radius: 14px;
margin: 0 0 16px;
@@ -459,11 +457,19 @@ export default {
gap: 8px;
}
.nominee-table thead th:nth-child(4),
.nominee-table tbody td:nth-child(4),
.nominee-table thead th:nth-child(5),
.nominee-table tbody td:nth-child(5) {
display: none;
/* Show all columns on mobile; allow horizontal scroll via table-responsive */
.nominee-thumb {
width: 52px;
height: 52px;
border-radius: 14px;
}
.nominee-table thead th {
font-size: 1.15rem;
}
.nominee-table tbody td {
padding: 10px 8px;
}
}
@@ -6,36 +6,6 @@
<span>{{ sahamVoteBlockedMessage }}</span>
</div>
</div>
<div class="col-md-4 vote-sidebar-column">
<div class="vote-summary-card">
<div class="vote-section-kicker">Ringkasan Undian</div>
<h4 class="vote-page-title">UNDIAN CALON ANGGOTA LEMBAGA</h4>
<p class="vote-page-subtitle">Semak calon yang telah dipilih sebelum menghantar undi anda.</p>
<div class="vote-limit-pill">
Anda hanya boleh memilih <b>satu (1) orang calon</b> sahaja.
</div>
<ul class="list-group vote-summary-list">
<li class="list-group-item vote-summary-item">
<div class="vote-position-name">Calon Dipilih</div>
<div v-if="allSelectedNomineeIds.length" class="vote-selected-list">
<div class="vote-selected-item" v-for="nominee_id in allSelectedNomineeIds" :key="nominee_id">
<span class="vote-selected-badge">#{{ getNomineeNo(nominee_id) }}</span>
<span>{{ getSelectedNomineeName(nominee_id) }}</span>
</div>
</div>
<div v-else class="vote-empty-selection">
Belum ada calon dipilih.
</div>
</li>
<li class="list-group-item vote-summary-action">
<button class="btn btn-success btn-block vote-submit-button" :disabled="!canVoteBySaham"
@click="checkUndi()">UNDI</button>
</li>
</ul>
</div>
</div>
<div class="col-md-8 vote-content-column">
<div class="vote-content-header">
<div>
@@ -65,11 +35,12 @@
<tbody>
<tr v-for="(nominee, index) in data.nominees" v-if="nominee.position_id == position.id"
:key="nominee.id" class="vote-row">
<td class="vote-checkbox-cell"><input type="checkbox" :id="nominee.id" :disabled="disabledCheckboxes[index]"
@change="vote(position.id, nominee.id)" />
<td class="vote-checkbox-cell"><input type="checkbox" :id="nominee.id"
:disabled="disabledCheckboxes[index]" @change="vote(position.id, nominee.id)" />
</td>
<td>{{ nominee.no_calon }}</td>
<td><img @click="view(i)" :src="createBase64ImageUrl(nominee.photo)" class="thumbnail vote-nominee-photo" />
<td><img @click="view(i)" :src="createBase64ImageUrl(nominee.photo)"
class="thumbnail vote-nominee-photo" />
</td>
<td>{{ nominee.name }}</td>
<td>{{ nominee.unit }}</td>
@@ -80,8 +51,54 @@
</div>
</div>
<div class="modal fade app-modal vote-confirm-modal" id="vote-modal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel"
aria-hidden="true">
<div class="col-xs-12 vote-summary-bottom">
<div class="vote-summary-card">
<div class="vote-section-kicker">Ringkasan Undian</div>
<h4 class="vote-page-title">UNDIAN CALON ANGGOTA LEMBAGA</h4>
<p class="vote-page-subtitle">Semak calon yang telah dipilih sebelum menghantar undi anda.</p>
<div class="vote-limit-pill">
Anda hanya boleh memilih <b>satu (1) orang calon</b> sahaja.
</div>
<ul class="list-group vote-summary-list">
<li class="list-group-item vote-summary-item">
<div class="vote-position-name">Calon Dipilih</div>
<div v-if="allSelectedNomineeIds.length" class="vote-selected-list">
<div class="vote-selected-item" v-for="nominee_id in allSelectedNomineeIds"
:key="nominee_id">
<span class="vote-selected-badge">#{{ getNomineeNo(nominee_id) }}</span>
<span>{{ getSelectedNomineeName(nominee_id) }}</span>
</div>
</div>
<div v-else class="vote-empty-selection">
Belum ada calon dipilih.
</div>
</li>
<li class="list-group-item vote-summary-action">
<button class="btn btn-success btn-block vote-submit-button" :disabled="!canVoteBySaham"
@click="checkUndi()">UNDI</button>
</li>
</ul>
</div>
</div>
<div class="vote-mobile-actionbar" v-if="canVoteBySaham">
<div class="vote-mobile-actionbar__inner">
<div class="vote-mobile-actionbar__meta">
<div class="vote-mobile-actionbar__title">Undi</div>
<div class="vote-mobile-actionbar__hint">
Dipilih: <b>{{ allSelectedNomineeIds.length }}</b> / {{ max }}
</div>
</div>
<button class="btn btn-success vote-mobile-actionbar__btn" @click="checkUndi()">
UNDI
</button>
</div>
</div>
<div class="modal fade app-modal vote-confirm-modal" id="vote-modal" tabindex="-1" role="dialog"
aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content vote-modal-content">
<div class="modal-header vote-modal-header">
@@ -97,7 +114,8 @@
<h5 class="vote-modal-note">Nota: Sila pastikan anda telah membuat undian dengan betul.</h5>
<div class="vote-modal-selection" v-if="allSelectedNomineeIds.length">
<div class="vote-modal-selection-title">Calon dipilih</div>
<div class="vote-selected-item" v-for="nominee_id in allSelectedNomineeIds" :key="nominee_id">
<div class="vote-selected-item" v-for="nominee_id in allSelectedNomineeIds"
:key="nominee_id">
<span class="vote-selected-badge">#{{ getNomineeNo(nominee_id) }}</span>
<span>{{ getSelectedNomineeName(nominee_id) }}</span>
</div>
@@ -105,7 +123,8 @@
</div>
<div class="modal-footer vote-modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Tutup</button>
<button type="button" class="btn btn-primary" :disabled="!canVoteBySaham" @click="submit()">Hantar</button>
<button type="button" class="btn btn-primary" :disabled="!canVoteBySaham"
@click="submit()">Hantar</button>
</div>
</div>
</div>
@@ -371,17 +390,17 @@ export default {
.vote-sidebar-column,
.vote-content-column {
margin-bottom: 24px;
margin-bottom: 6px;
}
.vote-summary-card {
padding: 22px 20px;
border-radius: 20px;
background: linear-gradient(180deg, #ffffff 0%, #f7fbff 100%);
border: 1px solid #e2ebf5;
box-shadow: 0 16px 34px rgba(35, 64, 97, 0.08);
position: sticky;
top: 120px;
position: static;
}
.vote-section-kicker {
@@ -652,9 +671,56 @@ export default {
gap: 8px;
}
.vote-table thead th:nth-child(5),
.vote-table tbody td:nth-child(5) {
/* Keep Unit visible on mobile; hide photo column instead */
.vote-table thead th:nth-child(3),
.vote-table tbody td:nth-child(3) {
display: none;
}
.vote-content-column {
padding-bottom: 86px;
/* room for sticky action bar */
}
.vote-mobile-actionbar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 1040;
/* below modal (1050), above page */
padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
background: rgba(255, 255, 255, 0.92);
backdrop-filter: blur(10px);
border-top: 1px solid #e5edf7;
box-shadow: 0 -10px 24px rgba(35, 64, 97, 0.12);
}
.vote-mobile-actionbar__inner {
max-width: 960px;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.vote-mobile-actionbar__title {
font-weight: 800;
color: #17324d;
}
.vote-mobile-actionbar__hint {
color: #5f7388;
font-size: 12px;
}
.vote-mobile-actionbar__btn {
min-width: 140px;
border-radius: 999px;
font-weight: 800;
letter-spacing: 0.04em;
box-shadow: 0 12px 24px rgba(40, 167, 69, 0.24);
}
}
</style>