Allowance check
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
<div class="clearfix">
|
||||
<h4 class="pull-left" style="margin-top: 0;">Activity Log</h4>
|
||||
<button class="btn btn-default btn-sm pull-right" @click="refresh" :disabled="loading">
|
||||
<i class="fa fa-refresh" :class="{ 'fa-spin': loading }"></i> Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="row" style="margin-top: 12px; margin-bottom: 12px;">
|
||||
<div class="col-sm-8 col-md-6">
|
||||
<div class="input-group">
|
||||
<input v-model.trim="searchText" type="text" class="form-control"
|
||||
placeholder="Cari aktiviti (description)…" @keyup.enter="refresh" />
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-primary" @click="refresh" :disabled="loading">
|
||||
Cari
|
||||
</button>
|
||||
<button class="btn btn-default" @click="clearSearch" :disabled="loading || !searchText">
|
||||
Clear
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<admin-data-table :headers="headers" :items="items" :loading="loading" :items-per-page="25"
|
||||
:show-pagination="true" empty-text="Tiada activity log" :exportable="true"
|
||||
export-file-name="activity-log">
|
||||
<template v-slot:item-id="{ value }">
|
||||
<span style="white-space: nowrap;">{{ value }}</span>
|
||||
</template>
|
||||
<template v-slot:item-created_at="{ value }">
|
||||
<span style="white-space: nowrap;">{{ value }}</span>
|
||||
</template>
|
||||
<template v-slot:item-properties="{ value }">
|
||||
<pre
|
||||
style="margin: 0; white-space: pre-wrap; word-break: break-word; background: transparent; border: 0; padding: 0;">{{ stringify(value) }}</pre>
|
||||
</template>
|
||||
</admin-data-table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AdminDataTable from '../../../AdminDataTable.vue';
|
||||
|
||||
export default {
|
||||
components: { AdminDataTable },
|
||||
|
||||
data: function () {
|
||||
return {
|
||||
loading: false,
|
||||
searchText: '',
|
||||
items: [],
|
||||
headers: [
|
||||
{ title: 'Aktiviti', key: 'description', sortable: true },
|
||||
{ title: 'Subject Type', key: 'subject_type', sortable: true },
|
||||
{ title: 'Subject ID', key: 'subject_id', sortable: true },
|
||||
{ title: 'Causer Type', key: 'causer_type', sortable: true },
|
||||
{ title: 'Causer ID', key: 'causer_id', sortable: true },
|
||||
{ title: 'Properties', key: 'properties', sortable: false },
|
||||
{ title: 'Masa', key: 'created_at', sortable: true },
|
||||
]
|
||||
};
|
||||
},
|
||||
|
||||
created: function () {
|
||||
this.refresh();
|
||||
},
|
||||
|
||||
methods: {
|
||||
stringify: function (v) {
|
||||
if (v == null) return '';
|
||||
try {
|
||||
return JSON.stringify(v, null, 2);
|
||||
} catch (e) {
|
||||
return String(v);
|
||||
}
|
||||
},
|
||||
refresh: function () {
|
||||
var vm = this;
|
||||
vm.loading = true;
|
||||
|
||||
var params = { per_page: 200 };
|
||||
if (vm.searchText) {
|
||||
params.description = vm.searchText;
|
||||
}
|
||||
|
||||
axios.get(config.API + 'admin/activity-log', { params: params })
|
||||
.then(function (response) {
|
||||
vm.items = (response && response.data && response.data.data) ? response.data.data : [];
|
||||
})
|
||||
.catch(function (error) {
|
||||
vm.items = [];
|
||||
if (vm.util && typeof vm.util.showResult === 'function') {
|
||||
vm.util.showResult(error, 'error');
|
||||
}
|
||||
})
|
||||
.finally(function () {
|
||||
vm.loading = false;
|
||||
});
|
||||
},
|
||||
clearSearch: function () {
|
||||
this.searchText = '';
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -29,6 +29,7 @@
|
||||
value="" required>
|
||||
<option value="1">Admin</option>
|
||||
<option value="2">Jawatankuasa Audit</option>
|
||||
<option value="3">Jawatankuasa Kehadiran</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,36 +1,46 @@
|
||||
<template>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body table-responsive">
|
||||
<div class="panel-body">
|
||||
|
||||
<div class="form-group">
|
||||
<router-link :to="{name: 'Add Account'}" class="btn btn-success">
|
||||
<i class="fa fa-plus"></i> Tambah Akaun Pengguna</router-link>
|
||||
<button class="btn btn-default" @click="refreshAdmin()">
|
||||
<button class="btn btn-default" @click="refreshAdmin()" :disabled="!canViewAdminList">
|
||||
<i class="fa fa-refresh"></i> Refresh</button>
|
||||
</div>
|
||||
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Nama</th>
|
||||
<th>Email</th>
|
||||
<th>Padam</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="admin in data.admins">
|
||||
<td>{{ admin.id }}</td>
|
||||
<td>{{ admin.name }}</td>
|
||||
<td>{{ admin.email }}</td>
|
||||
<td>
|
||||
<button class="btn btn-danger" @click="deleteAdmin(admin.id)">
|
||||
<i class="fa fa-trash"></i> Padam
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div v-if="!canViewAdminList" class="alert alert-warning" style="margin-bottom:12px;">
|
||||
Akses ditolak: Pengurusan akaun hanya untuk <b>Main Admin</b> (id=1). Jika sedang impersonate, sila
|
||||
<b>Kembali Akaun Asal</b>.
|
||||
</div>
|
||||
|
||||
<admin-data-table
|
||||
:headers="adminTableHeaders"
|
||||
:items="adminItems"
|
||||
:loading="loading"
|
||||
:show-pagination="true"
|
||||
:exportable="true"
|
||||
export-file-name="admins"
|
||||
:items-per-page="25"
|
||||
:show-index="true"
|
||||
index-title="Bil."
|
||||
empty-text="Tiada akaun admin"
|
||||
>
|
||||
<template v-slot:item-impersonate="{ item }">
|
||||
<button
|
||||
class="btn btn-primary btn-sm"
|
||||
@click="openImpersonate(item)"
|
||||
:disabled="!canImpersonate(item)"
|
||||
>
|
||||
<i class="fa fa-user-secret"></i> Impersonate
|
||||
</button>
|
||||
</template>
|
||||
<template v-slot:item-delete="{ item }">
|
||||
<button class="btn btn-danger btn-sm" @click="deleteAdmin(item.id)">
|
||||
<i class="fa fa-trash"></i> Padam
|
||||
</button>
|
||||
</template>
|
||||
</admin-data-table>
|
||||
</div>
|
||||
<modal id="delete-admin-modal">
|
||||
<modal-header>Delete Admin</modal-header>
|
||||
@@ -42,21 +52,127 @@
|
||||
<button @click="util.hideModal('#delete-admin-modal')" class="btn btn-default">Batal</button>
|
||||
</modal-footer>
|
||||
</modal>
|
||||
<modal id="impersonate-admin-modal">
|
||||
<modal-header>Impersonate Admin</modal-header>
|
||||
<modal-body>
|
||||
<h4 v-if="impersonateTarget && impersonateTarget.email">
|
||||
Impersonate <b>{{ impersonateTarget.email }}</b> ?
|
||||
</h4>
|
||||
<p class="text-muted" style="margin-bottom:0;">
|
||||
Anda akan login sebagai akaun tersebut.
|
||||
</p>
|
||||
</modal-body>
|
||||
<modal-footer>
|
||||
<button @click="startImpersonate()" class="btn btn-primary">Impersonate</button>
|
||||
<button @click="util.hideModal('#impersonate-admin-modal')" class="btn btn-default">Batal</button>
|
||||
</modal-footer>
|
||||
</modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AdminDataTable from '../../../AdminDataTable.vue';
|
||||
|
||||
export default{
|
||||
components: { AdminDataTable },
|
||||
|
||||
data: () => ({
|
||||
id:0
|
||||
id:0,
|
||||
impersonateTarget: null,
|
||||
loading: false,
|
||||
adminTableHeaders: [
|
||||
{ title: 'ID', key: 'id', sortable: true },
|
||||
{ title: 'Nama', key: 'name', sortable: true },
|
||||
{ title: 'Email', key: 'email', sortable: true },
|
||||
{ title: 'Impersonate', key: 'impersonate', sortable: false, exportValue: function () { return ''; } },
|
||||
{ title: 'Padam', key: 'delete', sortable: false, exportValue: function () { return ''; } },
|
||||
]
|
||||
}),
|
||||
|
||||
computed: {
|
||||
canViewAdminList: function () {
|
||||
try {
|
||||
if (!this.data || !this.data.user) return false;
|
||||
if (Number(this.data.user.id) !== 1) return false;
|
||||
return localStorage.getItem('is_impersonating') !== '1';
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
adminItems: function () {
|
||||
var a = this.data && this.data.admins;
|
||||
if (Array.isArray(a)) return a;
|
||||
if (a && Array.isArray(a.data)) return a.data;
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
created: function () {
|
||||
this.refreshAdmin();
|
||||
if (this.canViewAdminList) {
|
||||
this.refreshAdmin();
|
||||
} else {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
canImpersonate: function (admin) {
|
||||
// Backend currently limits to main admin (id=1) and blocks impersonating id=1.
|
||||
try {
|
||||
if (!this.data || !this.data.user) return false;
|
||||
if (Number(this.data.user.id) !== 1) return false;
|
||||
if (!admin || admin.id === undefined || admin.id === null) return false;
|
||||
return Number(admin.id) !== 1;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
openImpersonate: function (admin) {
|
||||
this.impersonateTarget = admin;
|
||||
if (this.util && this.util.showModal) {
|
||||
this.util.showModal('#impersonate-admin-modal');
|
||||
}
|
||||
},
|
||||
|
||||
startImpersonate: function () {
|
||||
var vm = this;
|
||||
if (!vm.impersonateTarget || !vm.impersonateTarget.id) return;
|
||||
vm.util.hideModal('#impersonate-admin-modal');
|
||||
vm.util.notify('Impersonating admin', 'loading');
|
||||
|
||||
axios.post(config.API + 'admin/impersonate', { user_id: vm.impersonateTarget.id })
|
||||
.then(function (response) {
|
||||
$.notifyClose();
|
||||
if (!response || !response.data || response.data.status !== 'success') {
|
||||
vm.util.showResult(response, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Swap admin token + user in local state.
|
||||
var token = response.data.token;
|
||||
if (token) {
|
||||
localStorage['Access Token'] = 'Bearer ' + token;
|
||||
localStorage.setItem('is_impersonating', '1');
|
||||
if (vm.util && vm.util.setAuthorization) vm.util.setAuthorization();
|
||||
}
|
||||
if (response.data.user) {
|
||||
vm.data.user = response.data.user;
|
||||
if (response.data.user.role !== undefined && response.data.user.role !== null) {
|
||||
localStorage.setItem('admin_role', String(response.data.user.role));
|
||||
}
|
||||
}
|
||||
|
||||
vm.util.notify('Impersonate berjaya', 'success');
|
||||
// Ensure admin shell is refreshed (nav header shows new user)
|
||||
if (vm.$router) vm.$router.go(0);
|
||||
})
|
||||
.catch(function (error) {
|
||||
$.notifyClose();
|
||||
vm.util.showResult(error, 'error');
|
||||
});
|
||||
},
|
||||
|
||||
deleteAdmin: function () {
|
||||
this.util.hideModal('#delete-admin-modal');
|
||||
this.util.notify('Deleting admin', 'loading');
|
||||
@@ -74,8 +190,12 @@ export default{
|
||||
},
|
||||
|
||||
refreshAdmin: function () {
|
||||
this.util.notify('Refreshing admin', 'loading');
|
||||
var vm = this;
|
||||
if (!vm.canViewAdminList) {
|
||||
return;
|
||||
}
|
||||
vm.loading = true;
|
||||
this.util.notify('Refreshing admin', 'loading');
|
||||
axios.get(config.API+'admin')
|
||||
.then(response=>{
|
||||
$.notifyClose();
|
||||
@@ -88,6 +208,9 @@ export default{
|
||||
$.notifyClose();
|
||||
vm.util.showResult(error, 'error');
|
||||
})
|
||||
.finally(function () {
|
||||
vm.loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,89 +1,24 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- TABLE -->
|
||||
<table class="table table-bordered table-striped">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>No Anggota</th>
|
||||
<th>Unit</th>
|
||||
<th>Nama</th>
|
||||
<th>Status</th>
|
||||
<th>Tarikh Sah</th>
|
||||
<th>Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr v-for="voter in penyata" :key="voter.id">
|
||||
<td>{{ voter.no_anggota }}</td>
|
||||
<td>{{ voter.unit }}</td>
|
||||
<td>{{ voter.name }}</td>
|
||||
|
||||
<!-- STATUS -->
|
||||
<!-- <td>
|
||||
<span
|
||||
class="badge"
|
||||
:class="{
|
||||
'bg-success': voter.status_penyata === 'DISAHKAN',
|
||||
'bg-warning text-dark': voter.status_penyata === 'PERLU_SEMAK',
|
||||
'bg-secondary': !voter.status_penyata
|
||||
}"
|
||||
>
|
||||
{{ voter.status_penyata ?? 'BELUM SAH' }}
|
||||
</span>
|
||||
</td> -->
|
||||
|
||||
<!-- TARIKH SAH -->
|
||||
<!-- <td>
|
||||
{{ voter.tarikh_sah ?? '-' }}
|
||||
</td> -->
|
||||
|
||||
<!-- PDF BUTTON -->
|
||||
<td>
|
||||
<button
|
||||
class="btn btn-sm btn-success"
|
||||
@click="cetakPDF(voter.no_anggota)"
|
||||
:disabled="!bolehCetak(voter.status_penyata)"
|
||||
>
|
||||
PDF
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- NO DATA -->
|
||||
<tr v-if="!loading && penyata.length === 0">
|
||||
<td colspan="6" class="text-center text-muted">
|
||||
Tiada data penyata
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- LOADING -->
|
||||
<tr v-if="loading">
|
||||
<td colspan="6" class="text-center">
|
||||
Memuatkan data...
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<admin-data-table :headers="penyataTableHeaders" :items="penyata" :loading="loading" :show-pagination="true"
|
||||
index-title="Bil." empty-text="Tiada data penyata" :exportable="true">
|
||||
<template v-slot:item-aksi="{ item }">
|
||||
<button type="button" class="btn btn-success btn-sm" @click="cetakPDF(item.no_anggota)"
|
||||
:disabled="!bolehCetak(item.status_penyata)">
|
||||
PDF
|
||||
</button>
|
||||
</template>
|
||||
</admin-data-table>
|
||||
|
||||
<!-- PAGINATION -->
|
||||
<div
|
||||
class="d-flex justify-content-end gap-2"
|
||||
v-if="meta.last_page > 1"
|
||||
>
|
||||
<button
|
||||
class="btn btn-sm btn-outline-secondary"
|
||||
:disabled="meta.current_page === 1"
|
||||
@click="changePage(meta.current_page - 1)"
|
||||
>
|
||||
<div class="text-right" v-if="meta.last_page > 1">
|
||||
<button class="btn btn-default btn-sm" :disabled="meta.current_page === 1"
|
||||
@click="changePage(meta.current_page - 1)">
|
||||
Sebelumnya
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn btn-sm btn-outline-secondary"
|
||||
:disabled="meta.current_page === meta.last_page"
|
||||
@click="changePage(meta.current_page + 1)"
|
||||
>
|
||||
<button class="btn btn-default btn-sm" :disabled="meta.current_page === meta.last_page"
|
||||
@click="changePage(meta.current_page + 1)">
|
||||
Seterusnya
|
||||
</button>
|
||||
</div>
|
||||
@@ -102,6 +37,12 @@ export default {
|
||||
penyata: [],
|
||||
loading: false,
|
||||
searchNoAnggota: '',
|
||||
penyataTableHeaders: [
|
||||
{ title: 'No Anggota', key: 'no_anggota', sortable: true },
|
||||
{ title: 'Unit', key: 'unit', sortable: true },
|
||||
{ title: 'Nama', key: 'name', sortable: true },
|
||||
{ title: 'Tindakan', key: 'aksi', sortable: false }
|
||||
],
|
||||
meta: {
|
||||
current_page: 1,
|
||||
last_page: 1
|
||||
|
||||
@@ -2,41 +2,22 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div v-for="position in positions">
|
||||
<div v-for="position in positions" :key="position.id">
|
||||
<h5>{{ position.name }}</h5>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="20%">No. Anggota</th>
|
||||
<th width="30%">Nama</th>
|
||||
<th width="20%">Unit</th>
|
||||
<th width="10%">Undian</th>
|
||||
<th width="20%">Peratus (%)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="result in results" v-if="result.position_id == position.id">
|
||||
<td>{{ getNominee(result.nominee_id)['no_anggota'] }}</td>
|
||||
<td>{{ getNominee(result.nominee_id)['name'] }}</td>
|
||||
<td>{{ getNominee(result.nominee_id)['unit'] }}</td>
|
||||
<td>{{ result.votes }}</td>
|
||||
<td>{{ calculatePercentage(result.votes, position.id) }}</td>
|
||||
</tr>
|
||||
<tr v-for="no_vote in no_votes" v-if="no_vote.position_id == position.id">
|
||||
<td>{{ no_vote.no_anggota }}</td>
|
||||
<td>{{ no_vote.name }}</td>
|
||||
<td>{{ no_vote.unit }}</td>
|
||||
<td>0</td>
|
||||
<td>0</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="3"><b>Jumlah Undian</b></td>
|
||||
<td><b>{{ calculateTotalVotes(position.id) }}</b></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<admin-data-table
|
||||
:headers="finalTableHeaders"
|
||||
:items="finalRowsByPosition(position.id)"
|
||||
:loading="finalLoading"
|
||||
:items-per-page="10"
|
||||
:show-pagination="true"
|
||||
empty-text="Tiada keputusan"
|
||||
:exportable="true"
|
||||
:export-file-name="'keputusan-akhir-' + position.id"
|
||||
>
|
||||
<template v-slot:item-percentage="{ item }">
|
||||
{{ item.percentage }}
|
||||
</template>
|
||||
</admin-data-table>
|
||||
<hr />
|
||||
</div>
|
||||
</div>
|
||||
@@ -51,11 +32,69 @@ export default {
|
||||
nominees: [],
|
||||
results: [],
|
||||
partylists: [],
|
||||
positions: []
|
||||
positions: [],
|
||||
finalLoading: false,
|
||||
finalTableHeaders: [
|
||||
{ title: 'No. Anggota', key: 'no_anggota', sortable: true },
|
||||
{ title: 'Nama', key: 'name', sortable: true },
|
||||
{ title: 'Unit', key: 'unit', sortable: true },
|
||||
{ title: 'Undian', key: 'votes', sortable: true },
|
||||
{ title: 'Peratus (%)', key: 'percentage', sortable: true }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
finalRowsByPosition: function (positionId) {
|
||||
var vm = this;
|
||||
var rows = [];
|
||||
|
||||
// rows with votes
|
||||
this.results.forEach(function (result) {
|
||||
if (result.position_id == positionId) {
|
||||
var n = vm.getNominee(result.nominee_id) || {};
|
||||
rows.push({
|
||||
no_anggota: n.no_anggota || '',
|
||||
name: n.name || '',
|
||||
unit: n.unit || '',
|
||||
votes: result.votes || 0,
|
||||
percentage: vm.calculatePercentage(result.votes || 0, positionId)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// rows without votes
|
||||
this.no_votes.forEach(function (nv) {
|
||||
if (nv.position_id == positionId) {
|
||||
rows.push({
|
||||
no_anggota: nv.no_anggota || '',
|
||||
name: nv.name || '',
|
||||
unit: nv.unit || '',
|
||||
votes: 0,
|
||||
percentage: '0.00'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// total row (kept as a normal row so it exports too)
|
||||
rows.push({
|
||||
no_anggota: '',
|
||||
name: 'Jumlah Undian',
|
||||
unit: '',
|
||||
votes: this.calculateTotalVotes(positionId),
|
||||
percentage: ''
|
||||
});
|
||||
|
||||
// Sort by votes desc, but keep total row last
|
||||
var total = rows.pop();
|
||||
rows.sort(function (a, b) {
|
||||
return (b.votes || 0) - (a.votes || 0);
|
||||
});
|
||||
rows.push(total);
|
||||
|
||||
return rows;
|
||||
},
|
||||
|
||||
getNominee: function (id) {
|
||||
let nominees = this.nominees;
|
||||
for (var i in nominees)
|
||||
@@ -77,7 +116,7 @@ export default {
|
||||
.filter(result => result.position_id === positionId)
|
||||
.reduce((total, result) => total + result.votes, 0);
|
||||
|
||||
return ((votes / totalVotes) * 100).toFixed(2);
|
||||
return totalVotes === 0 ? '0.00' : ((votes / totalVotes) * 100).toFixed(2);
|
||||
},
|
||||
|
||||
calculateTotalVotes(positionId) {
|
||||
@@ -90,6 +129,7 @@ export default {
|
||||
created: function () {
|
||||
this.util.notify('Loading please wait...', 'loading');
|
||||
var vm = this;
|
||||
this.finalLoading = true;
|
||||
axios.get(config.API + 'election/result/' + this.election_id)
|
||||
.then(response => {
|
||||
$.notifyClose();
|
||||
@@ -102,6 +142,9 @@ export default {
|
||||
$.notifyClose();
|
||||
vm.util.showResult(error, 'error');
|
||||
})
|
||||
.finally(function () {
|
||||
vm.finalLoading = false;
|
||||
})
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
@@ -17,37 +17,17 @@
|
||||
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Nama Undian</th>
|
||||
<th>Undian Mula</th>
|
||||
<th>Undian Tamat</th>
|
||||
<th>Papar</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="election in data.elections">
|
||||
<td>{{ election.id }}</td>
|
||||
<td>{{ election.name }}</td>
|
||||
<td>{{ election.start }}</td>
|
||||
<td>{{ election.end }}</td>
|
||||
<td>
|
||||
<router-link :to="{ name: 'Election Result', params: { election_id: election.id } }"
|
||||
class="btn btn-info">Keputusan Undian</router-link>
|
||||
|
||||
<a class="btn btn-warning" :href="getPDFurl(election.id)">Muat Turun PDF</a>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="data.elections.length < 1">
|
||||
<td colspan="5">No elections yet</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<admin-data-table :headers="electionTableHeaders" :items="data.elections" :loading="electionLoading"
|
||||
:items-per-page="10" :show-pagination="true" empty-text="Tiada undian" :exportable="true"
|
||||
export-file-name="undian">
|
||||
<template v-slot:item-actions="{ item }">
|
||||
<router-link :to="{ name: 'Election Result', params: { election_id: item.id } }"
|
||||
class="btn btn-info">
|
||||
Keputusan Undian
|
||||
</router-link>
|
||||
<a class="btn btn-warning" :href="getPDFurl(item.id)">Muat Turun PDF</a>
|
||||
</template>
|
||||
</admin-data-table>
|
||||
|
||||
<form @submit.prevent="start" id="start_form">
|
||||
<div class="modal fade" id="start-election-modal" tabindex="-1" role="dialog"
|
||||
@@ -119,15 +99,31 @@
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.center-header {
|
||||
text-align: center;
|
||||
}
|
||||
.center-header {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Ensure Bootstrap modals appear above the admin floating header (z-index: 2000). */
|
||||
.modal {
|
||||
z-index: 2200;
|
||||
}
|
||||
|
||||
.modal-backdrop {
|
||||
z-index: 2190 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data: () => ({
|
||||
start_date: null
|
||||
start_date: null,
|
||||
electionLoading: false,
|
||||
electionTableHeaders: [
|
||||
{ title: 'Nama Undian', key: 'name', sortable: true },
|
||||
{ title: 'Undian Mula', key: 'start', sortable: true },
|
||||
{ title: 'Undian Tamat', key: 'end', sortable: true },
|
||||
{ title: 'Papar', key: 'actions', sortable: false }
|
||||
]
|
||||
}),
|
||||
created: function () {
|
||||
this.refreshElection();
|
||||
@@ -184,6 +180,7 @@ export default {
|
||||
refreshElection: function () {
|
||||
this.util.notify('Refreshing Election', 'loading');
|
||||
var vm = this;
|
||||
this.electionLoading = true;
|
||||
axios.get(config.API + 'election')
|
||||
.then(response => {
|
||||
$.notifyClose();
|
||||
@@ -193,6 +190,9 @@ export default {
|
||||
$.notifyClose();
|
||||
vm.util.showResult(error, 'error');
|
||||
})
|
||||
.finally(function () {
|
||||
vm.electionLoading = false;
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
|
||||
@@ -1,90 +1,242 @@
|
||||
<template>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<h4>Jawatan</h4><hr/>
|
||||
<ul class="list-group">
|
||||
<router-link :key="position.id" v-for="position in data.positions" class="list-group-item" :class="{'active':position.id==position_id}" tag="li" :to="{query:{position_id:position.id}}" exact replace>
|
||||
<template>
|
||||
<div class="result-page">
|
||||
<div class="result-header">
|
||||
<div>
|
||||
<div class="result-kicker">Keputusan Undian</div>
|
||||
<h4 class="result-title">{{ String(position_id) === '0' ? 'Dashboard Keputusan' :
|
||||
(getPosition(position_id) || 'Keputusan') }}</h4>
|
||||
<div class="result-subtitle">Kemaskini terakhir: <b>{{ last_update }}</b></div>
|
||||
</div>
|
||||
<div class="result-actions">
|
||||
<button class="btn btn-info" @click="refreshNominees()">
|
||||
<i class="fa fa-refresh"></i> Kemaskini Keputusan
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="String(position_id) === '0'" class="result-stats">
|
||||
<div class="result-stat">
|
||||
<div class="result-stat__label">Jumlah Undi</div>
|
||||
<div class="result-stat__value">{{ totalVotes }}</div>
|
||||
</div>
|
||||
<div class="result-stat">
|
||||
<div class="result-stat__label">Bil. Jawatan</div>
|
||||
<div class="result-stat__value">{{ (data.positions || []).length }}</div>
|
||||
</div>
|
||||
<div class="result-stat">
|
||||
<div class="result-stat__label">Calon Tertinggi</div>
|
||||
<div class="result-stat__value result-stat__value--text">{{ topNomineeLabel }}</div>
|
||||
<div class="result-stat__hint">{{ topNomineeVotes }} undi</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="result-filters">
|
||||
<span class="result-filters__label">Jawatan:</span>
|
||||
<button type="button" class="result-chip" :class="{ 'is-active': String(position_id) === '0' }"
|
||||
@click="setPosition(0)">
|
||||
Semua
|
||||
</button>
|
||||
<button v-for="position in data.positions" :key="position.id" type="button" class="result-chip"
|
||||
:class="{ 'is-active': String(position.id) === String(position_id) }" @click="setPosition(position.id)">
|
||||
{{ position.name }}
|
||||
</router-link>
|
||||
<li class="list-group-item">
|
||||
<center>
|
||||
<button class="btn btn-info" @click="refreshNominees()">
|
||||
Kemaskini Keputusan<i class="fa fa-refresh"></i>
|
||||
</button>
|
||||
</center>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<h4>Keputusan</h4><hr/>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">{{ getPosition(position_id) }} - Results as of : {{ last_update }}</div>
|
||||
<div class="panel-body">
|
||||
<div id="chart" style="height: 300px; width: 300px"></div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-7">
|
||||
<div class="panel panel-default result-card">
|
||||
<div class="panel-heading result-card__heading">
|
||||
<div class="result-card__title">
|
||||
{{ String(position_id) === '0' ? 'Pemimpin Mengikut Jawatan' : 'Kedudukan Calon' }}
|
||||
</div>
|
||||
<div class="result-card__meta">
|
||||
{{
|
||||
String(position_id) === '0' ? 'Siapa mendahului (dengan beza undi)' :
|
||||
'Mengikut jawatan dipilih'
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body result-card__body">
|
||||
<template v-if="String(position_id) === '0'">
|
||||
<div v-if="!leadersByPosition.length" class="result-empty">
|
||||
Tiada data untuk dipaparkan.
|
||||
</div>
|
||||
<div v-else class="result-leaders">
|
||||
<div v-for="p in leadersByPosition" :key="p.position_id" class="result-leader">
|
||||
<div class="result-leader__top">
|
||||
<div class="result-leader__position">{{ p.position_name }}</div>
|
||||
<div class="result-leader__meta">
|
||||
<span><b>{{ p.winner_votes }}</b> undi</span>
|
||||
<span class="result-leader__delta">(+{{ p.delta }} beza)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="result-leader__name">{{ p.winner_name }}</div>
|
||||
<div class="result-leader__bar">
|
||||
<div class="result-leader__bar-fill" :style="{ width: p.pct + '%' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div v-if="!rankedNominees.length" class="result-empty">
|
||||
Tiada calon untuk jawatan ini.
|
||||
</div>
|
||||
<div v-else class="result-rank">
|
||||
<div v-for="n in rankedNominees" :key="n.id" class="result-rank__row">
|
||||
<div class="result-rank__top">
|
||||
<div class="result-rank__name">{{ n.name }}</div>
|
||||
<div class="result-rank__votes">
|
||||
<b>{{ n.votes }}</b> undi
|
||||
</div>
|
||||
</div>
|
||||
<div class="result-rank__bar">
|
||||
<div class="result-rank__bar-fill" :style="{ width: n.pct + '%' }"></div>
|
||||
</div>
|
||||
<div class="result-rank__meta">{{ n.pct }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<div class="panel panel-default result-card">
|
||||
<div class="panel-heading result-card__heading">
|
||||
<div class="result-card__title">
|
||||
{{ String(position_id) === '0' ? 'Analitik Ringkas' : 'Ringkasan Jawatan' }}
|
||||
</div>
|
||||
<div class="result-card__meta">
|
||||
{{
|
||||
String(position_id) === '0' ? 'Top calon & jumlah undi mengikut jawatan' : ('Jumlah undi:' +
|
||||
totalVotes) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body result-card__body">
|
||||
<template v-if="String(position_id) === '0'">
|
||||
<div class="result-mini-title">Top Calon (Overall)</div>
|
||||
<div id="top-chart" class="result-top-chart"></div>
|
||||
<div v-if="!topNominees.length" class="result-empty">Tiada data untuk dipaparkan.</div>
|
||||
|
||||
<div class="result-mini-title" style="margin-top: 14px;">Jumlah Undi Mengikut Jawatan</div>
|
||||
<div id="votes-by-position-chart" class="result-top-chart"></div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="result-empty">
|
||||
Pilih “Semua” untuk melihat analitik keseluruhan. Untuk jawatan ini, rujuk senarai di
|
||||
kiri.
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default{
|
||||
export default {
|
||||
|
||||
created: function () {
|
||||
console.log(this)
|
||||
created: function () {
|
||||
this.refreshNominees();
|
||||
this.$nextTick(function(){
|
||||
this.initChart();
|
||||
this.$nextTick(function () {
|
||||
this.initCharts();
|
||||
});
|
||||
},
|
||||
|
||||
watch: {
|
||||
position_id : function () {
|
||||
this.initChart();
|
||||
position_id: function () {
|
||||
var vm = this;
|
||||
this.$nextTick(function () {
|
||||
vm.initCharts();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
setPosition: function (id) {
|
||||
// Prevent Vue Router NavigationDuplicated when clicking active chip
|
||||
var target = (!id || String(id) === '0') ? '0' : String(id);
|
||||
if (String(this.position_id) === target) return;
|
||||
|
||||
var q = Object.assign({}, this.$route.query);
|
||||
if (!id || String(id) === '0') delete q.position_id;
|
||||
else q.position_id = String(id);
|
||||
this.$router.replace({ query: q }).catch(function () { });
|
||||
},
|
||||
|
||||
refreshNominees: function () {
|
||||
var vm = this;
|
||||
this.util.notify('Refreshing results', 'loading');
|
||||
axios.get(config.API+'nominee')
|
||||
.then(response=>{
|
||||
// Always fetch all nominees (needed for overall dashboards and name lookups)
|
||||
axios.get(config.API + 'nominee')
|
||||
.then(response => {
|
||||
$.notifyClose();
|
||||
vm.data.nominees = response.data;
|
||||
vm.refreshResults();
|
||||
})
|
||||
.catch(error=>{
|
||||
.catch(error => {
|
||||
$.notifyClose();
|
||||
vm.util.showResult(error);
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
refreshResults: function () {
|
||||
var vm = this;
|
||||
this.util.notify('Refreshing results', 'loading');
|
||||
axios.get(config.API+'election/results')
|
||||
.then(response=>{
|
||||
axios.get(config.API + 'election/results')
|
||||
.then(response => {
|
||||
$.notifyClose();
|
||||
vm.data.results = response.data;
|
||||
vm.data.last_update= new Date();
|
||||
vm.initChart();
|
||||
vm.data.last_update = new Date();
|
||||
vm.$nextTick(function () {
|
||||
vm.initCharts();
|
||||
});
|
||||
})
|
||||
.catch(error=>{
|
||||
.catch(error => {
|
||||
$.notifyClose();
|
||||
vm.util.showResult(error);
|
||||
})
|
||||
},
|
||||
|
||||
initChart: function () {
|
||||
$.plot($('#chart'), this.datas, {
|
||||
series: {
|
||||
pie: {
|
||||
show: true,
|
||||
innerRadius: 0.5
|
||||
}
|
||||
initCharts: function () {
|
||||
// Clear any previously rendered plots to avoid overlap when switching views
|
||||
if ($('#top-chart').length) $('#top-chart').empty();
|
||||
if ($('#votes-by-position-chart').length) $('#votes-by-position-chart').empty();
|
||||
|
||||
// All positions summary
|
||||
if (String(this.position_id) === '0') {
|
||||
if ($('#top-chart').length) {
|
||||
var ticks = this.topNominees.map(function (n, i) { return [i, n.label]; });
|
||||
var series = [{
|
||||
label: 'Undi',
|
||||
data: this.topNominees.map(function (n, i) { return [i, n.votes]; }),
|
||||
bars: { show: true, barWidth: 0.6, align: 'center', fill: 0.85, lineWidth: 0 }
|
||||
}];
|
||||
$.plot($('#top-chart'), series, {
|
||||
xaxis: { ticks: ticks, rotateTicks: 45 },
|
||||
yaxis: { min: 0, tickDecimals: 0 },
|
||||
grid: { hoverable: true, borderColor: '#e5e7eb' }
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
if ($('#votes-by-position-chart').length) {
|
||||
var pt = this.positionTotals;
|
||||
var ptTicks = pt.map(function (p, i) { return [i, p.label]; });
|
||||
var ptSeries = [{
|
||||
label: 'Undi',
|
||||
data: pt.map(function (p, i) { return [i, p.votes]; }),
|
||||
bars: { show: true, barWidth: 0.6, align: 'center', fill: 0.55, lineWidth: 0 }
|
||||
}];
|
||||
$.plot($('#votes-by-position-chart'), ptSeries, {
|
||||
xaxis: { ticks: ptTicks, rotateTicks: 45 },
|
||||
yaxis: { min: 0, tickDecimals: 0 },
|
||||
grid: { hoverable: true, borderColor: '#e5e7eb' }
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// For per-position view, we intentionally do not render charts (UI request).
|
||||
},
|
||||
|
||||
getPosition: function (id) {
|
||||
@@ -113,13 +265,12 @@ export default{
|
||||
computed: {
|
||||
last_update: function () {
|
||||
let x = this.data.last_update;
|
||||
return x.toDateString() +' '+x.toLocaleTimeString();
|
||||
if (!x) return '-';
|
||||
return x.toDateString() + ' ' + x.toLocaleTimeString();
|
||||
},
|
||||
|
||||
position_id: function () {
|
||||
return this.$route.query.position_id ?
|
||||
this.$route.query.position_id :
|
||||
this.data.positions[0]['id'];
|
||||
return this.$route.query.position_id ? this.$route.query.position_id : 0;
|
||||
},
|
||||
|
||||
datas: function () {
|
||||
@@ -127,7 +278,7 @@ export default{
|
||||
var data = [];
|
||||
var nominees = this.data.nominees;
|
||||
for (var i in nominees) {
|
||||
if (nominees[i]['position_id']== this.position_id){
|
||||
if (nominees[i]['position_id'] == this.position_id) {
|
||||
let row = [];
|
||||
row['label'] = nominees[i]['name'];
|
||||
row['data'] = [[1, this.getVotes(nominees[i]['id'])]];
|
||||
@@ -135,7 +286,427 @@ export default{
|
||||
}
|
||||
}
|
||||
return data;
|
||||
},
|
||||
|
||||
positionTotals: function () {
|
||||
// Total votes per position (for "Semua" dashboard)
|
||||
var results = this.data.results || [];
|
||||
var totals = {};
|
||||
for (var i = 0; i < results.length; i++) {
|
||||
var pid = results[i].position_id;
|
||||
var v = Number(results[i].votes || 0);
|
||||
totals[pid] = (totals[pid] || 0) + v;
|
||||
}
|
||||
var positions = this.data.positions || [];
|
||||
var rows = positions.map(function (p) {
|
||||
return { position_id: p.id, label: p.name, votes: totals[p.id] || 0 };
|
||||
});
|
||||
rows.sort(function (a, b) { return (b.votes || 0) - (a.votes || 0); });
|
||||
return rows;
|
||||
},
|
||||
|
||||
topNominees: function () {
|
||||
// Highest vote nominees across all positions (sum by nominee_id)
|
||||
var results = this.data.results || [];
|
||||
var sum = {};
|
||||
for (var i = 0; i < results.length; i++) {
|
||||
var nid = results[i].nominee_id;
|
||||
var v = Number(results[i].votes || 0);
|
||||
sum[nid] = (sum[nid] || 0) + v;
|
||||
}
|
||||
var nominees = this.data.nominees || [];
|
||||
var nameById = {};
|
||||
for (var j = 0; j < nominees.length; j++) {
|
||||
nameById[nominees[j].id] = nominees[j].name;
|
||||
}
|
||||
var rows = Object.keys(sum).map(function (nid) {
|
||||
return { id: Number(nid), label: nameById[nid] || ('Nominee #' + nid), votes: sum[nid] };
|
||||
});
|
||||
rows.sort(function (a, b) { return (b.votes || 0) - (a.votes || 0); });
|
||||
return rows.slice(0, 8);
|
||||
},
|
||||
|
||||
topNomineeLabel: function () {
|
||||
return this.topNominees && this.topNominees[0] ? this.topNominees[0].label : '-';
|
||||
},
|
||||
|
||||
topNomineeVotes: function () {
|
||||
return this.topNominees && this.topNominees[0] ? (this.topNominees[0].votes || 0) : 0;
|
||||
},
|
||||
|
||||
leadersByPosition: function () {
|
||||
// For each position: winner + runner-up + margin
|
||||
var positions = this.data.positions || [];
|
||||
var nominees = this.data.nominees || [];
|
||||
var results = this.data.results || [];
|
||||
|
||||
var posName = {};
|
||||
positions.forEach(function (p) { posName[p.id] = p.name; });
|
||||
|
||||
var nomineeName = {};
|
||||
nominees.forEach(function (n) { nomineeName[n.id] = n.name; });
|
||||
|
||||
var byPos = {};
|
||||
for (var i = 0; i < results.length; i++) {
|
||||
var r = results[i];
|
||||
var pid = r.position_id;
|
||||
if (!byPos[pid]) byPos[pid] = [];
|
||||
byPos[pid].push({ nominee_id: r.nominee_id, votes: Number(r.votes || 0) });
|
||||
}
|
||||
|
||||
var rows = [];
|
||||
for (var j = 0; j < positions.length; j++) {
|
||||
var pid2 = positions[j].id;
|
||||
var arr = (byPos[pid2] || []).slice().sort(function (a, b) { return (b.votes || 0) - (a.votes || 0); });
|
||||
var w = arr[0] || { nominee_id: null, votes: 0 };
|
||||
var r2 = arr[1] || { nominee_id: null, votes: 0 };
|
||||
var total = arr.reduce(function (s, x) { return s + (x.votes || 0); }, 0);
|
||||
var pct = total > 0 ? Math.round((w.votes / total) * 100) : 0;
|
||||
rows.push({
|
||||
position_id: pid2,
|
||||
position_name: posName[pid2] || ('Jawatan #' + pid2),
|
||||
winner_id: w.nominee_id,
|
||||
winner_name: nomineeName[w.nominee_id] || '-',
|
||||
winner_votes: w.votes || 0,
|
||||
runnerup_id: r2.nominee_id,
|
||||
runnerup_name: nomineeName[r2.nominee_id] || '-',
|
||||
runnerup_votes: r2.votes || 0,
|
||||
delta: Math.max(0, (w.votes || 0) - (r2.votes || 0)),
|
||||
pct: pct
|
||||
});
|
||||
}
|
||||
// Sort: show positions with most votes first
|
||||
rows.sort(function (a, b) { return (b.winner_votes || 0) - (a.winner_votes || 0); });
|
||||
return rows;
|
||||
},
|
||||
|
||||
rankedNominees: function () {
|
||||
var nominees = this.data.nominees || [];
|
||||
var rows = [];
|
||||
for (var i = 0; i < nominees.length; i++) {
|
||||
if (String(nominees[i].position_id) !== String(this.position_id)) continue;
|
||||
var votes = this.getVotes(nominees[i].id);
|
||||
rows.push({
|
||||
id: nominees[i].id,
|
||||
name: nominees[i].name,
|
||||
votes: votes
|
||||
});
|
||||
}
|
||||
rows.sort(function (a, b) { return (b.votes || 0) - (a.votes || 0); });
|
||||
var total = rows.reduce(function (s, r) { return s + (r.votes || 0); }, 0);
|
||||
return rows.map(function (r) {
|
||||
var pct = total > 0 ? Math.round((r.votes / total) * 100) : 0;
|
||||
return Object.assign({}, r, { pct: pct });
|
||||
});
|
||||
},
|
||||
|
||||
totalVotes: function () {
|
||||
// In "All" view use all results; otherwise use per-position ranking
|
||||
if (String(this.position_id) === '0') {
|
||||
var results = this.data.results || [];
|
||||
return results.reduce(function (s, r) { return s + Number(r.votes || 0); }, 0);
|
||||
}
|
||||
var rows = this.rankedNominees;
|
||||
return rows.reduce(function (s, r) { return s + (r.votes || 0); }, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.result-page {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.result-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.result-kicker {
|
||||
color: #2f80ed;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.result-title {
|
||||
margin: 0 0 6px;
|
||||
font-weight: 800;
|
||||
color: #17324d;
|
||||
}
|
||||
|
||||
.result-subtitle {
|
||||
color: #6b7280;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.result-actions {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.result-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
margin: 10px 0 14px;
|
||||
}
|
||||
|
||||
.result-stat {
|
||||
border: 1px solid #e6edf5;
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f7fbff 100%);
|
||||
padding: 12px 14px;
|
||||
box-shadow: 0 10px 22px rgba(35, 64, 97, 0.06);
|
||||
}
|
||||
|
||||
.result-stat__label {
|
||||
color: #6b7280;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.result-stat__value {
|
||||
font-size: 22px;
|
||||
font-weight: 900;
|
||||
color: #17324d;
|
||||
}
|
||||
|
||||
.result-stat__value--text {
|
||||
font-size: 14px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.result-stat__hint {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.result-filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin: 10px 0 16px;
|
||||
}
|
||||
|
||||
.result-filters__label {
|
||||
color: #6b7280;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.result-chip {
|
||||
border: 1px solid #d1d5db;
|
||||
background: #fff;
|
||||
color: #374151;
|
||||
padding: 6px 12px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.result-chip:hover {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.result-chip.is-active {
|
||||
background: #1976d2;
|
||||
border-color: #1976d2;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.result-card {
|
||||
border: 0;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 12px 28px rgba(35, 64, 97, 0.08);
|
||||
}
|
||||
|
||||
.result-card__heading {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
background: linear-gradient(135deg, #1d4e89, #2f80ed);
|
||||
color: #fff;
|
||||
border: 0 !important;
|
||||
}
|
||||
|
||||
.result-card__title {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.result-card__meta {
|
||||
opacity: 0.95;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.result-card__body {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.result-top-chart {
|
||||
width: 100%;
|
||||
height: 280px;
|
||||
}
|
||||
|
||||
.result-mini-title {
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: #17324d;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.result-empty {
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
color: #6b7280;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.result-leaders {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.result-leader {
|
||||
padding: 12px 12px;
|
||||
border: 1px solid #eef2f7;
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f7fbff 100%);
|
||||
}
|
||||
|
||||
.result-leader__top {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.result-leader__position {
|
||||
font-weight: 900;
|
||||
color: #17324d;
|
||||
}
|
||||
|
||||
.result-leader__meta {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
font-size: 12px;
|
||||
color: #17324d;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.result-leader__delta {
|
||||
color: #2f80ed;
|
||||
}
|
||||
|
||||
.result-leader__name {
|
||||
font-weight: 800;
|
||||
color: #17324d;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.result-leader__bar {
|
||||
height: 10px;
|
||||
border-radius: 999px;
|
||||
background: #e8f0fb;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.result-leader__bar-fill {
|
||||
height: 100%;
|
||||
border-radius: 999px;
|
||||
background: linear-gradient(90deg, #2f80ed, #1d4e89);
|
||||
}
|
||||
|
||||
.result-leader__sub {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.result-rank {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.result-rank__row {
|
||||
padding: 12px 12px;
|
||||
border: 1px solid #eef2f7;
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f7fbff 100%);
|
||||
}
|
||||
|
||||
.result-rank__top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.result-rank__name {
|
||||
font-weight: 800;
|
||||
color: #17324d;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.result-rank__votes {
|
||||
color: #17324d;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.result-rank__bar {
|
||||
height: 10px;
|
||||
border-radius: 999px;
|
||||
background: #e8f0fb;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.result-rank__bar-fill {
|
||||
height: 100%;
|
||||
border-radius: 999px;
|
||||
background: linear-gradient(90deg, #2f80ed, #1d4e89);
|
||||
}
|
||||
|
||||
.result-rank__meta {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.result-header {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.result-stats {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -9,7 +9,7 @@
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand" :href="data.baseURL"><img
|
||||
<a class="navbar-brand" href="/admin"><img
|
||||
src='https://i.postimg.cc/ZBDD0ZfQ/Whats-App-Image-2024-03-21-at-1-24-46-PM-2.jpg'
|
||||
border='0' width="150" height="auto" alt="MyKoPKB Logo">
|
||||
</a>
|
||||
@@ -18,7 +18,8 @@
|
||||
|
||||
<ul class="nav navbar-nav">
|
||||
<!-- {{ users }} -->
|
||||
<router-link :to="{ name: 'Admin Home' }" tag="li" exact><a href="#"><b>Laman
|
||||
<router-link v-if="Number(data.user.role) !== 3" :to="{ name: 'Admin Home' }" tag="li"
|
||||
exact><a href="#"><b>Laman
|
||||
Utama</b></a></router-link>
|
||||
<router-link :to="{ name: 'Kemaskini Jawatan' }" v-if="data.user.role == 1" tag="li">
|
||||
<a href="#"><b>Jawatan</b></a>
|
||||
@@ -28,7 +29,7 @@
|
||||
<a href="#">Manage Partylist</a>
|
||||
</router-link> -->
|
||||
|
||||
<router-link :to="{ name: 'Manage Voter' }" v-if="data.user.role == 1" tag="li">
|
||||
<router-link :to="{ name: 'Manage Voter' }" v-if="data.user.role == 1" tag="li" exact>
|
||||
<a href="#"><b>Anggota Koperasi</b></a>
|
||||
</router-link>
|
||||
|
||||
@@ -36,7 +37,8 @@
|
||||
<a href="#"><b>Calon</b></a>
|
||||
</router-link>
|
||||
|
||||
<router-link :to="{ name: 'Kehadiran Calon' }" v-if="data.user.role == 1" tag="li">
|
||||
<router-link :to="{ name: 'Kehadiran Calon' }"
|
||||
v-if="data.user.role == 1 || Number(data.user.role) === 3" tag="li">
|
||||
<a href="#"><b>Kehadiran</b></a>
|
||||
</router-link>
|
||||
|
||||
@@ -48,6 +50,10 @@
|
||||
<a href="#"><b>Penyata Anggota</b></a>
|
||||
</router-link>
|
||||
|
||||
<router-link :to="{ name: 'Activity Log' }" v-if="data.user.role == 1" tag="li">
|
||||
<a href="#"><b>Log Aktiviti</b></a>
|
||||
</router-link>
|
||||
|
||||
|
||||
</ul>
|
||||
<ul class="nav navbar-right navbar-nav">
|
||||
@@ -58,15 +64,24 @@
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
|
||||
<router-link :to="{ name: 'Update Account' }" tag="li" exact>
|
||||
<router-link v-if="Number(data.user.role) !== 3" :to="{ name: 'Update Account' }"
|
||||
tag="li" exact>
|
||||
<a href="#">Kemaskini Akaun</a>
|
||||
</router-link>
|
||||
|
||||
<li v-if="isImpersonating()" @click="leaveImpersonation()">
|
||||
<a>Kembali Akaun Asal</a>
|
||||
</li>
|
||||
|
||||
<router-link :to="{ name: 'Manage Account' }" tag="li" v-if="data.user.id == 1">
|
||||
<a href="#">Pengurusan Akaun</a>
|
||||
</router-link>
|
||||
|
||||
<li @click="logout()"><a>Log Keluar</a></li>
|
||||
<li @click="isImpersonating() ? null : logout()"
|
||||
:class="{ disabled: isImpersonating() }"
|
||||
:style="isImpersonating() ? 'opacity:0.5; cursor:not-allowed; pointer-events:none;' : ''">
|
||||
<a>Log Keluar</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -106,6 +121,12 @@ export default {
|
||||
vm.data.election = response.data.election;
|
||||
vm.data.partylists = response.data.partylist;
|
||||
vm.data.positions = response.data.position;
|
||||
if (response.data.user && response.data.user.role !== undefined && response.data.user.role !== null) {
|
||||
localStorage.setItem('admin_role', String(response.data.user.role));
|
||||
}
|
||||
if (Number(response.data.user.role) === 3 && vm.$route.name !== 'Kehadiran Calon') {
|
||||
vm.$router.replace({ name: 'Kehadiran Calon' });
|
||||
}
|
||||
vm.loading = false;
|
||||
})
|
||||
.catch(error => {
|
||||
@@ -120,7 +141,47 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
isImpersonating: function () {
|
||||
try {
|
||||
return localStorage.getItem('is_impersonating') === '1';
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
leaveImpersonation: function () {
|
||||
var vm = this;
|
||||
vm.util.notify('Leaving impersonation', 'loading');
|
||||
axios.post(config.API + 'admin/impersonate/leave')
|
||||
.then(function (response) {
|
||||
$.notifyClose();
|
||||
if (!response || !response.data || response.data.status !== 'success') {
|
||||
vm.util.showResult(response, 'error');
|
||||
return;
|
||||
}
|
||||
if (response.data.token) {
|
||||
localStorage['Access Token'] = 'Bearer ' + response.data.token;
|
||||
if (vm.util && vm.util.setAuthorization) vm.util.setAuthorization();
|
||||
}
|
||||
localStorage.removeItem('is_impersonating');
|
||||
if (response.data.user) {
|
||||
vm.data.user = response.data.user;
|
||||
if (response.data.user.role !== undefined && response.data.user.role !== null) {
|
||||
localStorage.setItem('admin_role', String(response.data.user.role));
|
||||
}
|
||||
}
|
||||
vm.util.notify('Berjaya kembali', 'success');
|
||||
if (vm.$router) vm.$router.go(0);
|
||||
})
|
||||
.catch(function (error) {
|
||||
$.notifyClose();
|
||||
vm.util.showResult(error, 'error');
|
||||
});
|
||||
},
|
||||
|
||||
logout: function () {
|
||||
localStorage.removeItem('admin_role');
|
||||
localStorage.removeItem('is_impersonating');
|
||||
localStorage.clear();
|
||||
this.$router.push({ name: 'Admin Login' });
|
||||
}
|
||||
|
||||
@@ -62,8 +62,13 @@ export default {
|
||||
},
|
||||
|
||||
created: function () {
|
||||
if (this.util.isLogin())
|
||||
return this.$router.push({ name: 'Admin Home' })
|
||||
if (this.util.isLogin()) {
|
||||
var ar = localStorage.getItem('admin_role');
|
||||
if (ar === '3') {
|
||||
return this.$router.push({ name: 'Kehadiran Calon' });
|
||||
}
|
||||
return this.$router.push({ name: 'Admin Home' });
|
||||
}
|
||||
this.util.setTitle('Log Masuk Admin ');
|
||||
},
|
||||
|
||||
@@ -80,8 +85,15 @@ export default {
|
||||
vm.stopLoading();
|
||||
if (this.util.showResult(response, 'success')) {
|
||||
localStorage['Access Token'] = `Bearer ${response.data.token}`;
|
||||
if (response.data.user && response.data.user.role !== undefined && response.data.user.role !== null) {
|
||||
localStorage.setItem('admin_role', String(response.data.user.role));
|
||||
}
|
||||
this.util.setAuthorization();
|
||||
vm.$router.push({ name: 'Admin Home' });
|
||||
if (Number(response.data.user && response.data.user.role) === 3) {
|
||||
vm.$router.push({ name: 'Kehadiran Calon' });
|
||||
} else {
|
||||
vm.$router.push({ name: 'Admin Home' });
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
|
||||
@@ -1,78 +1,193 @@
|
||||
<template>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel panel-default nominee-add-page">
|
||||
<div class="panel-body">
|
||||
<form class="row" method="POST" id="add_form" :action="data.API + 'nominee'" enctype="mutlipart/formdata"
|
||||
@submit.prevent="add()">
|
||||
<div class="col-md-8">
|
||||
<div class="nominee-add-page__header clearfix">
|
||||
<h4 class="pull-left nominee-add-page__title">
|
||||
<i class="fa fa-user-plus text-muted"></i> Tambah Calon
|
||||
</h4>
|
||||
<router-link
|
||||
:to="{ name: 'Maklumat Calon', query: { position_id: position_id } }"
|
||||
class="btn btn-default pull-right"
|
||||
>
|
||||
<i class="fa fa-arrow-left"></i> Kembali
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="name">Nama Calon</label>
|
||||
<input type="text" name="name" class="form-control" required />
|
||||
<p class="text-muted small nominee-add-page__intro">
|
||||
Lengkapkan maklumat calon. Medan bertanda <span class="text-danger">*</span> adalah wajib.
|
||||
</p>
|
||||
|
||||
<form
|
||||
class="nominee-add-form"
|
||||
method="POST"
|
||||
id="add_form"
|
||||
:action="data.API + 'nominee'"
|
||||
enctype="multipart/form-data"
|
||||
@submit.prevent="add()"
|
||||
>
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group">
|
||||
<label for="nominee-name">Nama Calon <span class="text-danger">*</span></label>
|
||||
<input
|
||||
id="nominee-name"
|
||||
type="text"
|
||||
name="name"
|
||||
class="form-control"
|
||||
placeholder="Nama penuh"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group">
|
||||
<label for="nominee-no-anggota">No. Anggota <span class="text-danger">*</span></label>
|
||||
<input
|
||||
id="nominee-no-anggota"
|
||||
type="text"
|
||||
name="no_anggota"
|
||||
class="form-control"
|
||||
placeholder="No. keahlian"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group">
|
||||
<label for="nominee-unit">Unit <span class="text-danger">*</span></label>
|
||||
<input
|
||||
id="nominee-unit"
|
||||
type="text"
|
||||
name="unit"
|
||||
class="form-control"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group">
|
||||
<label for="nominee-umur">Umur <span class="text-danger">*</span></label>
|
||||
<input
|
||||
id="nominee-umur"
|
||||
type="number"
|
||||
name="umur"
|
||||
class="form-control"
|
||||
min="1"
|
||||
max="120"
|
||||
placeholder="Tahun"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="nominee-jawatan-sekarang">Jawatan Sekarang <span class="text-danger">*</span></label>
|
||||
<input
|
||||
id="nominee-jawatan-sekarang"
|
||||
type="text"
|
||||
name="jawatan_sekarang"
|
||||
class="form-control"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="nominee-education">Taraf Pendidikan</label>
|
||||
<input
|
||||
id="nominee-education"
|
||||
type="text"
|
||||
name="education"
|
||||
class="form-control"
|
||||
placeholder="Pilihan"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="nominee-experience">Pengalaman Kerja</label>
|
||||
<textarea
|
||||
id="nominee-experience"
|
||||
name="experience"
|
||||
class="form-control"
|
||||
rows="4"
|
||||
placeholder="Pilihan — senaraikan pengalaman relevan"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="nominee-position-id">Jawatan Dicalonkan <span class="text-danger">*</span></label>
|
||||
<select
|
||||
id="nominee-position-id"
|
||||
class="form-control"
|
||||
v-model="position_id"
|
||||
name="position_id"
|
||||
required
|
||||
>
|
||||
<option value="0" disabled>— Pilih jawatan —</option>
|
||||
<option
|
||||
v-for="position in data.positions"
|
||||
:key="position.id"
|
||||
:value="position.id"
|
||||
>
|
||||
{{ position.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="no_anggota">No. Anggota</label>
|
||||
<input type="text" name="no_anggota" class="form-control" required />
|
||||
<div class="col-md-4">
|
||||
<div class="nominee-add-photo panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong><i class="fa fa-camera"></i> Gambar calon</strong>
|
||||
<span class="text-muted small nominee-add-photo__hint">Pilihan</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div
|
||||
class="nominee-add-photo__preview"
|
||||
:class="{ 'nominee-add-photo__preview--empty': !imageUrl }"
|
||||
>
|
||||
<img v-if="imageUrl" :src="imageUrl" alt="Pratonton gambar calon" />
|
||||
<div v-else class="nominee-add-photo__placeholder">
|
||||
<i class="fa fa-picture-o"></i>
|
||||
<span>Tiada gambar dipilih</span>
|
||||
</div>
|
||||
</div>
|
||||
<label class="btn btn-default btn-block nominee-add-photo__browse" for="nominee-file-input">
|
||||
<i class="fa fa-folder-open"></i> Pilih fail
|
||||
</label>
|
||||
<input
|
||||
id="nominee-file-input"
|
||||
name="photo"
|
||||
type="file"
|
||||
class="nominee-add-photo__input"
|
||||
accept="image/*"
|
||||
@change="handleImageChange"
|
||||
/>
|
||||
<p class="text-muted small nominee-add-photo__formats">PNG, JPG atau GIF</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="unit">Unit</label>
|
||||
<input type="text" name="unit" class="form-control" required />
|
||||
</div>
|
||||
<hr class="nominee-add-form__rule" />
|
||||
|
||||
<div class="form-group">
|
||||
<label for="unit">Umur</label>
|
||||
<input type="text" name="umur" class="form-control" required />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="unit">Jawatan Sekarang</label>
|
||||
<input type="text" name="jawatan_sekarang" class="form-control" required />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="education">Taraf Pendidikan</label>
|
||||
<input type="text" name="education" class="form-control" placeholder="(Optional)" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="pengalaman">Pengalaman Kerja</label>
|
||||
<textarea name="pengalaman" class="form-control" placeholder="(Optional)"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="position_id">Jawatan</label>
|
||||
<select class="form-control" v-model="position_id" name="position_id" required>
|
||||
<option value="0" disabled>--- Pilih Jawatan ---</option>
|
||||
<option v-for="position in data.positions" :key="position.id" :value="position.id">{{ position.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- <div class="form-group">
|
||||
<label for="partylist_id">Partylist</label>
|
||||
<select class="form-control" name="partylist_id">
|
||||
<option value="">--- Select Partylist (Optional) ---</option>
|
||||
<option v-for="partylist in data.partylists" :value="partylist.id">{{ partylist.name }}</option>
|
||||
</select>
|
||||
</div> -->
|
||||
|
||||
<div id="imagePreview">
|
||||
<img :src="imageUrl" v-if="imageUrl" alt="Preview">
|
||||
</div>
|
||||
<div>
|
||||
<label for="image">Muatnaik Gambar</label>
|
||||
<input name="photo" type="file" accept="image/*" id="file-input" @change="handleImageChange">
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-group pull-right">
|
||||
<router-link :to="{ name: 'Maklumat Calon', query: { position_id: position_id } }"
|
||||
class="btn btn-default">
|
||||
Cancel
|
||||
</router-link>
|
||||
<input type="submit" value="Submit" class="btn btn-info">
|
||||
|
||||
</div>
|
||||
<div class="clearfix nominee-add-form__actions">
|
||||
<router-link
|
||||
:to="{ name: 'Maklumat Calon', query: { position_id: position_id } }"
|
||||
class="btn btn-default"
|
||||
>
|
||||
<i class="fa fa-times"></i> Batal
|
||||
</router-link>
|
||||
<button type="submit" class="btn btn-primary" :disabled="loading">
|
||||
<i v-if="loading" class="fa fa-spinner fa-spin"></i>
|
||||
<i v-else class="fa fa-check"></i>
|
||||
{{ loading ? 'Menghantar…' : 'Simpan calon' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -83,7 +198,7 @@
|
||||
export default {
|
||||
data: function () {
|
||||
return {
|
||||
imageUrl: '',
|
||||
imageUrl: '',
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
@@ -102,8 +217,8 @@ export default {
|
||||
vm.$router.push({ name: 'Maklumat Calon' });
|
||||
},
|
||||
error: function (error) {
|
||||
alert('Nominee with the same name has already registered');
|
||||
location.reload();
|
||||
alert('Nominee with the same name has already registered');
|
||||
location.reload();
|
||||
$.notifyClose();
|
||||
vm.loading = false;
|
||||
vm.util.showResult(error, 'error', 'ajax');
|
||||
@@ -114,27 +229,23 @@ export default {
|
||||
})
|
||||
},
|
||||
|
||||
handleImageChange(event) {
|
||||
const file = event.target.files[0]; // Get the selected file
|
||||
const imageType = /image.*/; // RegExp to check if the file is an image
|
||||
handleImageChange(event) {
|
||||
const file = event.target.files[0];
|
||||
const imageType = /image.*/;
|
||||
|
||||
// Check if the selected file is an image
|
||||
if (file && file.type.match(imageType)) {
|
||||
const reader = new FileReader(); // Create a FileReader object
|
||||
if (file && file.type.match(imageType)) {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = (e) => {
|
||||
this.imageUrl = e.target.result; // Set the imageUrl to the data URL of the image
|
||||
document.getElementById('imagePreview').style.display = 'block'; // Display the div
|
||||
};
|
||||
reader.onload = (e) => {
|
||||
this.imageUrl = e.target.result;
|
||||
};
|
||||
|
||||
reader.readAsDataURL(file); // Read the image data as a data URL
|
||||
} else {
|
||||
// Clear the file input and hide the div if the selected file is not an image
|
||||
event.target.value = '';
|
||||
this.imageUrl = '';
|
||||
document.getElementById('imagePreview').style.display = 'none';
|
||||
}
|
||||
}
|
||||
reader.readAsDataURL(file);
|
||||
} else {
|
||||
event.target.value = '';
|
||||
this.imageUrl = '';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
@@ -152,16 +263,115 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#imagePreview {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
border: 1px solid #ccc;
|
||||
margin-bottom: 10px;
|
||||
display: none; /* Initially hide the div */
|
||||
}
|
||||
.nominee-add-page__header {
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
#imagePreview img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
.nominee-add-page__title {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.nominee-add-page__title .fa {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.nominee-add-page__intro {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.nominee-add-photo .panel-heading {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.nominee-add-photo__hint {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.nominee-add-photo__preview {
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
max-height: 240px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background: #f9f9f9;
|
||||
border: 1px dashed #ccc;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.nominee-add-photo__preview--empty {
|
||||
min-height: 180px;
|
||||
}
|
||||
|
||||
.nominee-add-photo__preview img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nominee-add-photo__placeholder {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.nominee-add-photo__placeholder .fa {
|
||||
font-size: 36px;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.nominee-add-photo__placeholder span {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.nominee-add-photo__input {
|
||||
position: absolute;
|
||||
width: 0.1px;
|
||||
height: 0.1px;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.nominee-add-photo__browse {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.nominee-add-photo__formats {
|
||||
margin: 8px 0 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nominee-add-form__rule {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 16px;
|
||||
border-top-color: #eee;
|
||||
}
|
||||
|
||||
.nominee-add-form__actions {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.nominee-add-form__actions .btn + .btn {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.nominee-add-photo {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,155 +1,323 @@
|
||||
<template>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
<form
|
||||
method="POST"
|
||||
class="row"
|
||||
id="edit_form"
|
||||
:action="data.API+'nominee/'+id"
|
||||
enctype="multipart/form-data"
|
||||
@submit.prevent="edit()">
|
||||
<input type="hidden" name="_method" value="PUT"/>
|
||||
|
||||
<div class="col-md-8">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="name">Nama</label>
|
||||
<input type="text" name="name" class="form-control" :value="nominee.name" required/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="student_id">No. Anggota</label>
|
||||
<input type="text" name="no_anggota" class="form-control" :value="nominee.no_anggota" required/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="Unit">Unit</label>
|
||||
<input type="text" name="unit" class="form-control" :value="nominee.unit" required/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="umur">Umur</label>
|
||||
<input type="text" name="umur" class="form-control" :value="nominee.umur" required/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="jawatan_sekarang">Jawatan Sekarang</label>
|
||||
<input type="text" name="jawatan_sekarang" class="form-control" :value="nominee.jawatan_sekarang" required/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="education">Taraf Pendidikan</label>
|
||||
<input type="text" name="education" class="form-control" :value="nominee.education" placeholder="(Optional)" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="experience">Pengalaman Kerja </label>
|
||||
<textarea name="experience" class="form-control" placeholder="(Optional)">{{ nominee.experience}}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="position_id">Jawatan</label>
|
||||
<select class="form-control" name="position_id" :value="nominee.position_id" required>
|
||||
<option value="0" disabled>--- Pilih Jawatan ---</option>
|
||||
<option v-for="position in data.positions" :value="position.id">{{ position.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="partylist_id">Partylist</label>
|
||||
<select class="form-control" name="partylist_id" :value="nominee.partylist_id">
|
||||
<option value="">--- Select Partylist (Optional) ---</option>
|
||||
<option v-for="partylist in data.partylists" :key="partylist.id" :value="partylist.id">{{ partylist.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="imagePreview">
|
||||
<img :src="imageUrl" v-if="imageUrl" alt="Preview">
|
||||
</div>
|
||||
<div>
|
||||
<label for="image">Muat Naik Gambar</label>
|
||||
<input name="photo" type="file" accept="image/*" id="file-input" @change="handleImageChange">
|
||||
</div>
|
||||
|
||||
<div class="form-group pull-right">
|
||||
<input type="submit" value="Simpan" class="btn btn-info">
|
||||
<router-link
|
||||
:to="{name:'Maklumat Calon'}"
|
||||
class="btn btn-default">
|
||||
Kembali
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="panel panel-default nominee-add-page">
|
||||
<div class="panel-body">
|
||||
<div class="nominee-add-page__header clearfix">
|
||||
<h4 class="pull-left nominee-add-page__title">
|
||||
<i class="fa fa-pencil text-muted"></i> Kemaskini Calon
|
||||
</h4>
|
||||
<router-link :to="backToList" class="btn btn-default pull-right">
|
||||
<i class="fa fa-arrow-left"></i> Kembali
|
||||
</router-link>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<p class="text-muted small nominee-add-page__intro">
|
||||
Kemas kini maklumat calon. Medan bertanda <span class="text-danger">*</span> adalah wajib.
|
||||
</p>
|
||||
|
||||
<form
|
||||
class="nominee-add-form"
|
||||
method="POST"
|
||||
id="edit_form"
|
||||
:action="data.API + 'nominee/' + id"
|
||||
enctype="multipart/form-data"
|
||||
@submit.prevent="edit()"
|
||||
>
|
||||
<input type="hidden" name="_method" value="PUT" />
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group">
|
||||
<label for="edit-nominee-name">Nama Calon <span class="text-danger">*</span></label>
|
||||
<input
|
||||
id="edit-nominee-name"
|
||||
v-model="form.name"
|
||||
type="text"
|
||||
name="name"
|
||||
class="form-control"
|
||||
placeholder="Nama penuh"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group">
|
||||
<label for="edit-nominee-no-anggota">No. Anggota <span class="text-danger">*</span></label>
|
||||
<input
|
||||
id="edit-nominee-no-anggota"
|
||||
v-model="form.no_anggota"
|
||||
type="text"
|
||||
name="no_anggota"
|
||||
class="form-control"
|
||||
placeholder="No. keahlian"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group">
|
||||
<label for="edit-nominee-unit">Unit <span class="text-danger">*</span></label>
|
||||
<input
|
||||
id="edit-nominee-unit"
|
||||
v-model="form.unit"
|
||||
type="text"
|
||||
name="unit"
|
||||
class="form-control"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group">
|
||||
<label for="edit-nominee-umur">Umur <span class="text-danger">*</span></label>
|
||||
<input
|
||||
id="edit-nominee-umur"
|
||||
v-model="form.umur"
|
||||
type="number"
|
||||
name="umur"
|
||||
class="form-control"
|
||||
min="1"
|
||||
max="120"
|
||||
placeholder="Tahun"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-nominee-jawatan-sekarang">Jawatan Sekarang <span class="text-danger">*</span></label>
|
||||
<input
|
||||
id="edit-nominee-jawatan-sekarang"
|
||||
v-model="form.jawatan_sekarang"
|
||||
type="text"
|
||||
name="jawatan_sekarang"
|
||||
class="form-control"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-nominee-education">Taraf Pendidikan</label>
|
||||
<input
|
||||
id="edit-nominee-education"
|
||||
v-model="form.education"
|
||||
type="text"
|
||||
name="education"
|
||||
class="form-control"
|
||||
placeholder="Pilihan"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-nominee-experience">Pengalaman Kerja</label>
|
||||
<textarea
|
||||
id="edit-nominee-experience"
|
||||
v-model="form.experience"
|
||||
name="experience"
|
||||
class="form-control"
|
||||
rows="4"
|
||||
placeholder="Pilihan — senaraikan pengalaman relevan"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-nominee-position-id">Jawatan Dicalonkan <span class="text-danger">*</span></label>
|
||||
<select
|
||||
id="edit-nominee-position-id"
|
||||
v-model="form.position_id"
|
||||
class="form-control"
|
||||
name="position_id"
|
||||
required
|
||||
>
|
||||
<option value="" disabled>— Pilih jawatan —</option>
|
||||
<option
|
||||
v-for="position in data.positions"
|
||||
:key="position.id"
|
||||
:value="String(position.id)"
|
||||
>
|
||||
{{ position.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-nominee-partylist-id">Senarai parti</label>
|
||||
<select
|
||||
id="edit-nominee-partylist-id"
|
||||
v-model="form.partylist_id"
|
||||
class="form-control"
|
||||
name="partylist_id"
|
||||
>
|
||||
<option value="">— Pilihan —</option>
|
||||
<option
|
||||
v-for="partylist in data.partylists"
|
||||
:key="partylist.id"
|
||||
:value="String(partylist.id)"
|
||||
>
|
||||
{{ partylist.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="nominee-add-photo panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong><i class="fa fa-camera"></i> Gambar calon</strong>
|
||||
<span class="text-muted small nominee-add-photo__hint">Pilihan</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div
|
||||
class="nominee-add-photo__preview"
|
||||
:class="{ 'nominee-add-photo__preview--empty': !imageUrl }"
|
||||
>
|
||||
<img v-if="imageUrl" :src="imageUrl" alt="Pratonton gambar calon" />
|
||||
<div v-else class="nominee-add-photo__placeholder">
|
||||
<i class="fa fa-picture-o"></i>
|
||||
<span>Tiada gambar</span>
|
||||
</div>
|
||||
</div>
|
||||
<label class="btn btn-default btn-block nominee-add-photo__browse" for="edit-nominee-file-input">
|
||||
<i class="fa fa-folder-open"></i> Pilih fail
|
||||
</label>
|
||||
<input
|
||||
id="edit-nominee-file-input"
|
||||
name="photo"
|
||||
type="file"
|
||||
class="nominee-add-photo__input"
|
||||
accept="image/*"
|
||||
@change="handleImageChange"
|
||||
/>
|
||||
<p class="text-muted small nominee-add-photo__formats">PNG, JPG atau GIF</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="nominee-add-form__rule" />
|
||||
|
||||
<div class="clearfix nominee-add-form__actions">
|
||||
<router-link :to="backToList" class="btn btn-default">
|
||||
<i class="fa fa-times"></i> Batal
|
||||
</router-link>
|
||||
<button type="submit" class="btn btn-primary" :disabled="loading">
|
||||
<i v-if="loading" class="fa fa-spinner fa-spin"></i>
|
||||
<i v-else class="fa fa-check"></i>
|
||||
{{ loading ? 'Menghantar…' : 'Simpan perubahan' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default{
|
||||
export default {
|
||||
data: function () {
|
||||
return {
|
||||
imageUrl: '',
|
||||
loading: false
|
||||
imageUrl: '',
|
||||
loading: false,
|
||||
form: {
|
||||
name: '',
|
||||
no_anggota: '',
|
||||
unit: '',
|
||||
umur: '',
|
||||
jawatan_sekarang: '',
|
||||
education: '',
|
||||
experience: '',
|
||||
position_id: '',
|
||||
partylist_id: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
created: function () {
|
||||
if (!this.nominee.id)
|
||||
this.$router.push({name:'Maklumat Calon'});
|
||||
if (!this.nominee.id) this.$router.push({ name: 'Maklumat Calon' })
|
||||
},
|
||||
|
||||
mounted: function () {
|
||||
if(this.nominee.id) {
|
||||
this.imageUrl = this.createBase64ImageUrl(this.nominee.photo);
|
||||
document.getElementById('imagePreview').style.display = 'block';
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
nominee: {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
handler: function (n) {
|
||||
if (!n || !n.id) return
|
||||
this.form.name = n.name || ''
|
||||
this.form.no_anggota = n.no_anggota || ''
|
||||
this.form.unit = n.unit || ''
|
||||
this.form.umur = n.umur != null && n.umur !== '' ? String(n.umur) : ''
|
||||
this.form.jawatan_sekarang = n.jawatan_sekarang || ''
|
||||
this.form.education = this.formatEducationForInput(n.education)
|
||||
this.form.experience = this.formatExperienceForInput(n.experience)
|
||||
this.form.position_id = n.position_id != null ? String(n.position_id) : ''
|
||||
this.form.partylist_id =
|
||||
n.partylist_id != null && n.partylist_id !== '' ? String(n.partylist_id) : ''
|
||||
this.imageUrl = n.photo ? this.createBase64ImageUrl(n.photo) : ''
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
createBase64ImageUrl: function(base64ImageData) {
|
||||
return "data:image/png;base64," + base64ImageData;
|
||||
},
|
||||
handleImageChange(event) {
|
||||
const file = event.target.files[0]; // Get the selected file
|
||||
const imageType = /image.*/; // RegExp to check if the file is an image
|
||||
createBase64ImageUrl: function (base64ImageData) {
|
||||
return 'data:image/png;base64,' + base64ImageData
|
||||
},
|
||||
|
||||
// Check if the selected file is an image
|
||||
if (file && file.type.match(imageType)) {
|
||||
const reader = new FileReader(); // Create a FileReader object
|
||||
formatEducationForInput: function (val) {
|
||||
if (val == null || val === '') return ''
|
||||
if (Array.isArray(val)) return val.join('\n')
|
||||
return String(val)
|
||||
},
|
||||
|
||||
reader.onload = (e) => {
|
||||
this.imageUrl = e.target.result; // Set the imageUrl to the data URL of the image
|
||||
document.getElementById('imagePreview').style.display = 'block'; // Display the div
|
||||
};
|
||||
formatExperienceForInput: function (val) {
|
||||
if (val == null || val === '') return ''
|
||||
if (Array.isArray(val)) {
|
||||
return val.map(function (x, i) {
|
||||
return i + 1 + '. ' + x
|
||||
}).join('\n')
|
||||
}
|
||||
return String(val)
|
||||
},
|
||||
|
||||
handleImageChange: function (event) {
|
||||
var file = event.target.files[0]
|
||||
var imageType = /image.*/
|
||||
var vm = this
|
||||
|
||||
if (file && file.type.match(imageType)) {
|
||||
var reader = new FileReader()
|
||||
reader.onload = function (e) {
|
||||
vm.imageUrl = e.target.result
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
} else {
|
||||
event.target.value = ''
|
||||
vm.imageUrl = vm.nominee.photo ? vm.createBase64ImageUrl(vm.nominee.photo) : ''
|
||||
}
|
||||
},
|
||||
|
||||
reader.readAsDataURL(file); // Read the image data as a data URL
|
||||
} else {
|
||||
// Clear the file input and hide the div if the selected file is not an image
|
||||
event.target.value = '';
|
||||
this.imageUrl = '';
|
||||
document.getElementById('imagePreview').style.display = 'none';
|
||||
}
|
||||
},
|
||||
edit: function () {
|
||||
if (this.loading) return;
|
||||
var vm = this;
|
||||
this.loading = true;
|
||||
this.util.notify('Kemaskini undian', 'progress', 0);
|
||||
if (this.loading) return
|
||||
var vm = this
|
||||
this.loading = true
|
||||
this.util.notify('Kemaskini calon', 'progress', 0)
|
||||
$('#edit_form').ajaxSubmit({
|
||||
success: function (response) {
|
||||
$.notifyClose();
|
||||
vm.loading = false;
|
||||
$.notifyClose()
|
||||
vm.loading = false
|
||||
if (vm.util.showResult(response, 'success', 'ajax'))
|
||||
vm.$router.push({name: 'Maklumat Calon',query:{refresh:true}});
|
||||
vm.$router.push({ name: 'Maklumat Calon', query: { refresh: true } })
|
||||
},
|
||||
error: function (error) {
|
||||
$.notifyClose();
|
||||
vm.loading = false;
|
||||
vm.util.showResult(error, 'error', 'ajax');
|
||||
$.notifyClose()
|
||||
vm.loading = false
|
||||
vm.util.showResult(error, 'error', 'ajax')
|
||||
},
|
||||
uploadProgress: function (a, b, c, progress) {
|
||||
this.util.notify('Kemaskini undian', 'progress', progress);
|
||||
vm.util.notify('Kemaskini calon', 'progress', progress)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -157,30 +325,136 @@ export default{
|
||||
|
||||
computed: {
|
||||
id: function () {
|
||||
return this.$route.params.id;
|
||||
return this.$route.params.id
|
||||
},
|
||||
|
||||
nominee: function () {
|
||||
for (var i in this.data.nominees)
|
||||
if(this.data.nominees[i].id == this.id)
|
||||
return this.data.nominees[i];
|
||||
if (this.data.nominees[i].id == this.id) return this.data.nominees[i]
|
||||
return {}
|
||||
},
|
||||
|
||||
backToList: function () {
|
||||
var q = {}
|
||||
if (this.nominee && this.nominee.position_id != null && String(this.nominee.position_id) !== '0') {
|
||||
q.position_id = this.nominee.position_id
|
||||
}
|
||||
return { name: 'Maklumat Calon', query: q }
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#imagePreview {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
border: 1px solid #ccc;
|
||||
margin-bottom: 10px;
|
||||
display: none; /* Initially hide the div */
|
||||
}
|
||||
.nominee-add-page__header {
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
#imagePreview img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
.nominee-add-page__title {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.nominee-add-page__title .fa {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.nominee-add-page__intro {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.nominee-add-photo .panel-heading {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.nominee-add-photo__hint {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.nominee-add-photo__preview {
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
max-height: 240px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background: #f9f9f9;
|
||||
border: 1px dashed #ccc;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.nominee-add-photo__preview--empty {
|
||||
min-height: 180px;
|
||||
}
|
||||
|
||||
.nominee-add-photo__preview img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nominee-add-photo__placeholder {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.nominee-add-photo__placeholder .fa {
|
||||
font-size: 36px;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.nominee-add-photo__placeholder span {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.nominee-add-photo__input {
|
||||
position: absolute;
|
||||
width: 0.1px;
|
||||
height: 0.1px;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.nominee-add-photo__browse {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.nominee-add-photo__formats {
|
||||
margin: 8px 0 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nominee-add-form__rule {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 16px;
|
||||
border-top-color: #eee;
|
||||
}
|
||||
|
||||
.nominee-add-form__actions {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.nominee-add-form__actions .btn + .btn {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.nominee-add-photo {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,77 +1,71 @@
|
||||
<template>
|
||||
<div class="row">
|
||||
<div class="col-md-3" style="max-width: 250px;">
|
||||
<ul class="list-group">
|
||||
<router-link tag="li" class="list-group-item" :to="{ name: 'Maklumat Calon' }" exact replace
|
||||
:class="{ 'active': position_id == 0 }">
|
||||
Senarai Jawatan
|
||||
</router-link>
|
||||
<router-link v-for="position in data.positions" :key="position.id" tag="li" class="list-group-item"
|
||||
:to="{ query: { position_id: position.id } }" exact replace>
|
||||
{{ position.name }}
|
||||
</router-link>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-9 panel panel-default">
|
||||
<div class="col-md-12 panel panel-default">
|
||||
<div class="panel-body table-responsive">
|
||||
<div class="nominee-filter-chips">
|
||||
<button
|
||||
type="button"
|
||||
class="nominee-chip btn btn-xs"
|
||||
:class="position_id == 0 ? 'btn-primary' : 'btn-default'"
|
||||
@click="setPosition(0)"
|
||||
>
|
||||
Semua Jawatan
|
||||
</button>
|
||||
<button
|
||||
v-for="position in data.positions"
|
||||
:key="position.id"
|
||||
type="button"
|
||||
class="nominee-chip btn btn-xs"
|
||||
:class="String(position_id) === String(position.id) ? 'btn-primary' : 'btn-default'"
|
||||
@click="setPosition(position.id)"
|
||||
>
|
||||
{{ position.name }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<router-link :to="{ name: 'Tambah Calon', query: { position_id: position_id } }"
|
||||
class="btn btn-success"><i class="fa fa-plus"></i> Tambah Calon</router-link>
|
||||
</div>
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Nama</th>
|
||||
<th>No. Anggota</th>
|
||||
<th>Unit</th>
|
||||
<th>Jawatan</th>
|
||||
<!-- <th>Partylist</th> -->
|
||||
<th>Taraf Pendidikan </th>
|
||||
<th>Pengalaman Kerja</th>
|
||||
<th>Tindakan</th>
|
||||
<admin-data-table
|
||||
:headers="nomineeTableHeaders"
|
||||
:items="nominees"
|
||||
:loading="nomineeLoading"
|
||||
:items-per-page="10"
|
||||
:show-pagination="true"
|
||||
empty-text="Tiada calon"
|
||||
:exportable="true"
|
||||
export-file-name="calon"
|
||||
>
|
||||
<template v-slot:item-photo="{ item }">
|
||||
<img
|
||||
:alt="item.name"
|
||||
:src="createBase64ImageUrl(item.photo)"
|
||||
class="thumbnail"
|
||||
style="height: 60px; width: 60px;"
|
||||
>
|
||||
</template>
|
||||
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="nominee in nominees" :key="nominee.id">
|
||||
<td>
|
||||
<img :alt="nominee.name" :src="createBase64ImageUrl(nominee.photo)" class="thumbnail"
|
||||
style="height: 60px;width: 60px;">
|
||||
</td>
|
||||
<!-- <td>{{ nominee.id }}</td> -->
|
||||
<td>{{ nominee.name }}</td>
|
||||
<td>{{ nominee.no_anggota }}</td>
|
||||
<td>{{ nominee.unit }}</td>
|
||||
<td>{{ nominee.position }}</td>
|
||||
<td>
|
||||
<ol>
|
||||
<li v-for="(item, index) in nominee.education" :key="index">{{ item }}</li>
|
||||
</ol>
|
||||
</td>
|
||||
<td>
|
||||
<ol>
|
||||
<li v-for="(item, index) in nominee.experience" :key="index">{{ item }}</li>
|
||||
</ol>
|
||||
</td>
|
||||
<template v-slot:item-education="{ item }">
|
||||
<ol style="margin: 0; padding-left: 18px;">
|
||||
<li v-for="(x, idx) in (item.education || [])" :key="idx">{{ x }}</li>
|
||||
</ol>
|
||||
</template>
|
||||
|
||||
<template v-slot:item-experience="{ item }">
|
||||
<ol style="margin: 0; padding-left: 18px;">
|
||||
<li v-for="(x, idx) in (item.experience || [])" :key="idx">{{ x }}</li>
|
||||
</ol>
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<router-link :to="{ name: 'Edit Nominee', params: { id: nominee.id } }"
|
||||
class="btn btn-primary">
|
||||
<i class="fa fa-edit"></i>Kemaskini
|
||||
</router-link>
|
||||
<button class="btn btn-danger" @click="openDeleteModal(nominee)">
|
||||
<i class="fa fa-trash"></i>Padam
|
||||
</button>
|
||||
|
||||
</tr>
|
||||
<tr v-if="nominees.length < 1">
|
||||
<td colspan="7">No Calon</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<template v-slot:item-actions="{ item }">
|
||||
<router-link :to="{ name: 'Edit Nominee', params: { id: item.id } }" class="btn btn-primary">
|
||||
<i class="fa fa-edit"></i>Kemaskini
|
||||
</router-link>
|
||||
<button type="button" class="btn btn-danger" @click="openDeleteModal(item)">
|
||||
<i class="fa fa-trash"></i>Padam
|
||||
</button>
|
||||
</template>
|
||||
</admin-data-table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -97,7 +91,32 @@ export default {
|
||||
|
||||
data: function () {
|
||||
return {
|
||||
id: 0
|
||||
id: 0,
|
||||
nomineeLoading: false,
|
||||
nomineeTableHeaders: [
|
||||
{ title: '', key: 'photo', sortable: false },
|
||||
{ title: 'Nama', key: 'name', sortable: true },
|
||||
{ title: 'No. Anggota', key: 'no_anggota', sortable: true },
|
||||
{ title: 'Unit', key: 'unit', sortable: true },
|
||||
{ title: 'Jawatan', key: 'position', sortable: true },
|
||||
{
|
||||
title: 'Taraf Pendidikan',
|
||||
key: 'education',
|
||||
sortable: false,
|
||||
exportValue: function (item) {
|
||||
return Array.isArray(item.education) ? item.education.join(' | ') : '';
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Pengalaman Kerja',
|
||||
key: 'experience',
|
||||
sortable: false,
|
||||
exportValue: function (item) {
|
||||
return Array.isArray(item.experience) ? item.experience.join(' | ') : '';
|
||||
}
|
||||
},
|
||||
{ title: 'Tindakan', key: 'actions', sortable: false }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
@@ -106,6 +125,15 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
setPosition: function (id) {
|
||||
var q = Object.assign({}, this.$route.query);
|
||||
if (!id || String(id) === '0') {
|
||||
delete q.position_id;
|
||||
} else {
|
||||
q.position_id = id;
|
||||
}
|
||||
this.$router.replace({ query: q });
|
||||
},
|
||||
|
||||
openDeleteModal: function (nominee) {
|
||||
this.id = nominee.id;
|
||||
@@ -140,8 +168,13 @@ export default {
|
||||
|
||||
refreshNominee: function () {
|
||||
var vm = this;
|
||||
this.nomineeLoading = true;
|
||||
this.util.notify('Refreshing Nominees', 'loading');
|
||||
axios.get(config.API + 'nominee')
|
||||
axios.get(config.API + 'nominee', {
|
||||
params: {
|
||||
position_id: this.position_id && String(this.position_id) !== '0' ? this.position_id : undefined
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
$.notifyClose();
|
||||
console.log(response);
|
||||
@@ -151,6 +184,9 @@ export default {
|
||||
$.notifyClose();
|
||||
vm.showResult(error);
|
||||
})
|
||||
.finally(function () {
|
||||
vm.nomineeLoading = false;
|
||||
})
|
||||
},
|
||||
|
||||
getPosition: function (id) {
|
||||
@@ -179,7 +215,7 @@ export default {
|
||||
var y = this.data.nominees;
|
||||
for (var nominee in this.data.nominees) {
|
||||
if (y[nominee].position_id == this.position_id || this.position_id == 0) {
|
||||
var x = y[nominee];
|
||||
var x = Object.assign({}, y[nominee]);
|
||||
x.position = this.getPosition(y[nominee].position_id);
|
||||
x.partylist = this.getPartylist(y[nominee].partylist_id);
|
||||
nominees.push(x);
|
||||
@@ -191,9 +227,28 @@ export default {
|
||||
position_id: function () {
|
||||
return this.$route.query.position_id ? this.$route.query.position_id : 0;
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
position_id: function () {
|
||||
this.refreshNominee();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style></style>
|
||||
<style>
|
||||
.nominee-filter-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.nominee-chip {
|
||||
border-radius: 999px;
|
||||
padding: 6px 10px;
|
||||
line-height: 1.1;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -13,34 +13,19 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover" id="position_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Nama</th>
|
||||
<th>Tindakan</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(position, i) in data.positions">
|
||||
<td>{{ position.id }}</td>
|
||||
<td>{{ position.name }}</td>
|
||||
<td>
|
||||
<button class="btn btn-info" @click="edit(i)">
|
||||
<i class="fa fa-edit"></i> Edit
|
||||
</button>
|
||||
<button class="btn btn-danger" @click="util.showModal('#delete-position-modal'); id = position.id">
|
||||
<i class="fa fa-trash"></i> Padam
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="data.positions.length < 1">
|
||||
<td colspan="3">No Jawatan</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<admin-data-table :headers="positionTableHeaders" :items="data.positions" :loading="positionLoading"
|
||||
:items-per-page="10" :show-pagination="true" empty-text="Tiada jawatan" :exportable="true"
|
||||
export-file-name="jawatan">
|
||||
<template v-slot:item-actions="{ item }">
|
||||
<button type="button" class="btn btn-info" @click="edit(item)">
|
||||
<i class="fa fa-edit"></i> Edit
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger"
|
||||
@click="util.showModal('#delete-position-modal'); id = item.id">
|
||||
<i class="fa fa-trash"></i> Padam
|
||||
</button>
|
||||
</template>
|
||||
</admin-data-table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -62,7 +47,12 @@
|
||||
export default {
|
||||
data: function () {
|
||||
return {
|
||||
id: 0
|
||||
id: 0,
|
||||
positionLoading: false,
|
||||
positionTableHeaders: [
|
||||
{ title: 'Nama', key: 'name', sortable: true },
|
||||
{ title: 'Tindakan', key: 'actions', sortable: false }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
@@ -76,10 +66,10 @@ export default {
|
||||
methods: {
|
||||
refreshPosition: function () {
|
||||
var vm = this;
|
||||
this.positionLoading = true;
|
||||
this.util.notify('Refreshing Position', 'loading');
|
||||
axios.get(config.API + 'position')
|
||||
.then(response => {
|
||||
console.log(response)
|
||||
$.notifyClose();
|
||||
vm.data.positions = response.data;
|
||||
})
|
||||
@@ -87,22 +77,14 @@ export default {
|
||||
$.notifyClose();
|
||||
vm.util.showResult(error);
|
||||
})
|
||||
.finally(function () {
|
||||
vm.positionLoading = false;
|
||||
})
|
||||
},
|
||||
|
||||
initDatatable: function () {
|
||||
edit: function (position) {
|
||||
var vm = this;
|
||||
$('#position_table').DataTable({
|
||||
destroy: true,
|
||||
searching: false,
|
||||
info: false,
|
||||
autoWidth: false,
|
||||
dom: 'Bfrtip'
|
||||
});
|
||||
},
|
||||
|
||||
edit: function (i) {
|
||||
var vm = this;
|
||||
this.data.position = this.data.positions[i];
|
||||
this.data.position = position;
|
||||
this.$router.push({ name: 'Edit Position', params: { id: vm.data.position.id } })
|
||||
},
|
||||
|
||||
|
||||
@@ -1,95 +1,468 @@
|
||||
<template>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
<h4>Tambah Pengundi</h4>
|
||||
<form @submit.prevent="add()" id="add-form">
|
||||
<div class="form-group">
|
||||
<label for="name">Nama Anggota</label>
|
||||
<input type="text" name="name" class="form-control" required>
|
||||
<div class="panel panel-default voter-add-page">
|
||||
<div class="panel-body">
|
||||
<div class="voter-add-page__header clearfix">
|
||||
<h4 class="pull-left">Tambah Pengundi</h4>
|
||||
<div class="pull-right voter-add-page__header-actions">
|
||||
<button type="button" class="btn btn-success" @click="openManualModal">
|
||||
<i class="fa fa-user-plus"></i> Daftar pengundi baru
|
||||
</button>
|
||||
<router-link :to="{ name: 'Manage Voter' }" class="btn btn-default">
|
||||
<i class="fa fa-arrow-left"></i> Kembali
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="no_kp">No. Kad Pengenalan</label>
|
||||
<input type="text" name="no_kp" class="form-control" required>
|
||||
<p class="text-muted small voter-add-page__intro">
|
||||
Kiri: keseluruhan pengundi (ikut No. KP, data terkini). Kanan: yang ditanda layak untuk pilihan raya
|
||||
semasa.
|
||||
Yang tidak berada di senarai kanan akan dipadam daripada pilihan raya ini apabila anda simpan.
|
||||
</p>
|
||||
|
||||
<div class="voter-add-split row">
|
||||
<div class="col-md-6 voter-add-split__col">
|
||||
<div class="panel panel-default voter-add-split__panel">
|
||||
<div class="panel-heading clearfix">
|
||||
<strong class="pull-left">Semua pengundi</strong>
|
||||
<span v-if="catalogLoading" class="text-muted small pull-left voter-add-heading__loading">
|
||||
<i class="fa fa-spinner fa-spin"></i>
|
||||
</span>
|
||||
<span class="badge pull-right" title="Jumlah pengundi unik (No. KP)">{{ catalog.length
|
||||
}}</span>
|
||||
<span v-if="catalogFilter"
|
||||
class="text-muted small pull-right voter-add-heading__filter-hint">
|
||||
{{ filteredCatalog.length }} dipaparkan ·
|
||||
</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="form-inline voter-batch-toolbar">
|
||||
<input type="text" class="form-control" v-model.trim="catalogFilter"
|
||||
placeholder="Tapis nama / No. KP / No. anggota…" />
|
||||
<button type="button" class="btn btn-default btn-sm" @click="selectAllFiltered">Tanda
|
||||
semua (ditapis)</button>
|
||||
<button type="button" class="btn btn-default btn-sm" @click="clearAllFiltered">Nyahtanda
|
||||
(ditapis)</button>
|
||||
</div>
|
||||
<div v-if="catalogError" class="alert alert-danger">{{ catalogError }}</div>
|
||||
<div class="table-responsive voter-add-table-wrap">
|
||||
<table class="table table-bordered table-condensed table-striped"
|
||||
v-if="!catalogLoading || catalog.length">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:44px;">
|
||||
<input type="checkbox" :checked="headerChecked"
|
||||
@change="toggleHeader($event)" />
|
||||
</th>
|
||||
<th>Nama</th>
|
||||
<th>No. KP</th>
|
||||
<th>No. Anggota</th>
|
||||
<th>Unit</th>
|
||||
<th style="width:88px;">PR semasa</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in filteredCatalog" :key="row.no_kp">
|
||||
<td>
|
||||
<input type="checkbox" :checked="isSelected(row.no_kp)"
|
||||
@change="toggleRow(row.no_kp, $event)" />
|
||||
</td>
|
||||
<td>{{ row.name }}</td>
|
||||
<td>{{ row.no_kp }}</td>
|
||||
<td>{{ row.no_anggota }}</td>
|
||||
<td>{{ row.unit }}</td>
|
||||
<td>
|
||||
<span v-if="row.in_current_election"
|
||||
class="label label-success">Ya</span>
|
||||
<span v-else class="label label-default">—</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p v-if="!catalogLoading && !catalog.length" class="text-muted">Tiada rekod pengundi dalam
|
||||
pangkalan data.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 voter-add-split__col">
|
||||
<div class="panel panel-info voter-add-split__panel">
|
||||
<div class="panel-heading clearfix">
|
||||
<strong class="pull-left">Layak mengundi (pilihan raya semasa)</strong>
|
||||
<span class="badge pull-right" title="Jumlah ditanda layak">{{ selectedNoKp.length }}</span>
|
||||
<span v-if="selectedFilter"
|
||||
class="text-muted small pull-right voter-add-heading__filter-hint">
|
||||
{{ filteredSelectedRows.length }} dipaparkan ·
|
||||
</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="form-inline voter-batch-toolbar voter-batch-toolbar--right">
|
||||
<input type="text" class="form-control" v-model.trim="selectedFilter"
|
||||
placeholder="Tapis senarai dipilih…" />
|
||||
<button type="button" class="btn btn-default btn-sm" @click="clearAllSelected"
|
||||
:disabled="!selectedNoKp.length">
|
||||
Kosongkan semua
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary"
|
||||
:disabled="catalogSaving || catalogLoading" @click="saveApplicable()">
|
||||
Simpan pilihan
|
||||
</button>
|
||||
</div>
|
||||
<div class="table-responsive voter-add-table-wrap voter-add-table-wrap--selected">
|
||||
<table class="table table-bordered table-condensed table-striped"
|
||||
v-if="filteredSelectedRows.length">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nama</th>
|
||||
<th>No. KP</th>
|
||||
<th>No. Anggota</th>
|
||||
<th style="width:52px;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in filteredSelectedRows" :key="row.no_kp">
|
||||
<td>{{ row.name }}</td>
|
||||
<td>{{ row.no_kp }}</td>
|
||||
<td>{{ row.no_anggota }}</td>
|
||||
<td class="text-center">
|
||||
<button type="button" class="btn btn-xs btn-danger" title="Buang"
|
||||
@click="removeSelected(row.no_kp)">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p v-else class="text-muted voter-add-empty-selected">Tiada pengundi ditanda. Tandakan
|
||||
pada jadual kiri.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="no_anggota">No. Anggota</label>
|
||||
<input type="text" name="no_anggota" class="form-control" required>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-group">
|
||||
<label for="unit">Unit</label>
|
||||
<input type="text" name="unit" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="alamat">Alamat</label>
|
||||
<input type="text" name="alamat" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="status_anggota">Status Anggota</label>
|
||||
<select name="status_anggota" class="form-control" required>
|
||||
<option value="inactive">-- Pilih Status --</option>
|
||||
<option value="active">Aktif</option>
|
||||
<option value="inactive">Berhenti</option>
|
||||
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="telefon">No. Telefon</label>
|
||||
<input type="text" name="telefon" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="saham">Saham</label>
|
||||
<input type="text" name="saham" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="yuran">Yuran</label>
|
||||
<input type="text" name="yuran" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-success">Submit</button>
|
||||
<router-link :to="{name: 'Manage Voter'}" class="btn btn-default">Back</router-link>
|
||||
</div>
|
||||
</form>
|
||||
<!-- Modal for adding new voter -->
|
||||
<modal id="add-voter-manual-modal">
|
||||
<modal-header>Daftar pengundi baru</modal-header>
|
||||
<modal-body>
|
||||
<form @submit.prevent="add()" id="add-form">
|
||||
<div class="form-group">
|
||||
<label for="add-form-name">Nama Anggota</label>
|
||||
<input id="add-form-name" type="text" name="name" class="form-control" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="add-form-no_kp">No. Kad Pengenalan</label>
|
||||
<input id="add-form-no_kp" type="text" name="no_kp" class="form-control" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="add-form-no_anggota">No. Anggota</label>
|
||||
<input id="add-form-no_anggota" type="text" name="no_anggota" class="form-control" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="add-form-unit">Unit</label>
|
||||
<input id="add-form-unit" type="text" name="unit" class="form-control" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="add-form-alamat">Alamat</label>
|
||||
<input id="add-form-alamat" type="text" name="alamat" class="form-control" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="add-form-status_anggota">Status Anggota</label>
|
||||
<select id="add-form-status_anggota" name="status_anggota" class="form-control" required>
|
||||
<option value="inactive">-- Pilih Status --</option>
|
||||
<option value="active">Aktif</option>
|
||||
<option value="inactive">Berhenti</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="add-form-telefon">No. Telefon</label>
|
||||
<input id="add-form-telefon" type="text" name="telefon" class="form-control" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="add-form-saham">Saham</label>
|
||||
<input id="add-form-saham" type="text" name="saham" class="form-control" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="add-form-yuran">Yuran</label>
|
||||
<input id="add-form-yuran" type="text" name="yuran" class="form-control" required>
|
||||
</div>
|
||||
</form>
|
||||
</modal-body>
|
||||
<modal-footer>
|
||||
<button type="button" class="btn btn-success" :disabled="loading" @click="add()">Hantar</button>
|
||||
<button type="button" class="btn btn-default" @click="closeManualModal">Batal</button>
|
||||
</modal-footer>
|
||||
</modal>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.voter-add-page__header {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.voter-add-page__header h4 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.voter-add-page__header-actions .btn {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.voter-add-page__intro {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.voter-add-split__col {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.voter-add-split__panel {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.voter-batch-toolbar .form-control {
|
||||
min-width: 180px;
|
||||
margin-right: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.voter-batch-toolbar .btn {
|
||||
margin-right: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.voter-add-table-wrap {
|
||||
max-height: min(520px, calc(100vh - 280px));
|
||||
overflow: auto;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.voter-add-table-wrap--selected {
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.voter-add-empty-selected {
|
||||
margin: 16px 0 0;
|
||||
}
|
||||
|
||||
.voter-add-heading__loading {
|
||||
margin-left: 8px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.voter-add-heading__filter-hint {
|
||||
margin-right: 8px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default{
|
||||
export default {
|
||||
data: function () {
|
||||
return {
|
||||
loading: false
|
||||
loading: false,
|
||||
catalog: [],
|
||||
catalogLoading: false,
|
||||
catalogSaving: false,
|
||||
catalogError: '',
|
||||
catalogFilter: '',
|
||||
selectedFilter: '',
|
||||
selectedNoKp: []
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
catalogByKp: function () {
|
||||
var map = {};
|
||||
var i;
|
||||
for (i = 0; i < this.catalog.length; i++) {
|
||||
map[this.catalog[i].no_kp] = this.catalog[i];
|
||||
}
|
||||
return map;
|
||||
},
|
||||
|
||||
selectedRows: function () {
|
||||
var map = this.catalogByKp;
|
||||
var out = [];
|
||||
var i;
|
||||
for (i = 0; i < this.selectedNoKp.length; i++) {
|
||||
var kp = this.selectedNoKp[i];
|
||||
if (map[kp]) out.push(map[kp]);
|
||||
}
|
||||
return out;
|
||||
},
|
||||
|
||||
filteredCatalog: function () {
|
||||
var q = (this.catalogFilter || '').toLowerCase();
|
||||
if (!q) return this.catalog;
|
||||
return this.catalog.filter(function (row) {
|
||||
return (row.name && String(row.name).toLowerCase().indexOf(q) !== -1)
|
||||
|| (row.no_kp && String(row.no_kp).toLowerCase().indexOf(q) !== -1)
|
||||
|| (row.no_anggota && String(row.no_anggota).toLowerCase().indexOf(q) !== -1)
|
||||
|| (row.unit && String(row.unit).toLowerCase().indexOf(q) !== -1);
|
||||
});
|
||||
},
|
||||
|
||||
filteredSelectedRows: function () {
|
||||
var rows = this.selectedRows;
|
||||
var q = (this.selectedFilter || '').toLowerCase();
|
||||
if (!q) return rows;
|
||||
return rows.filter(function (row) {
|
||||
return (row.name && String(row.name).toLowerCase().indexOf(q) !== -1)
|
||||
|| (row.no_kp && String(row.no_kp).toLowerCase().indexOf(q) !== -1)
|
||||
|| (row.no_anggota && String(row.no_anggota).toLowerCase().indexOf(q) !== -1)
|
||||
|| (row.unit && String(row.unit).toLowerCase().indexOf(q) !== -1);
|
||||
});
|
||||
},
|
||||
|
||||
headerChecked: function () {
|
||||
var rows = this.filteredCatalog;
|
||||
if (!rows.length) return false;
|
||||
var vm = this;
|
||||
return rows.every(function (r) { return vm.isSelected(r.no_kp); });
|
||||
}
|
||||
},
|
||||
|
||||
created: function () {
|
||||
this.loadMemberCatalog();
|
||||
},
|
||||
|
||||
methods: {
|
||||
openManualModal: function () {
|
||||
this.util.showModal('#add-voter-manual-modal');
|
||||
},
|
||||
|
||||
closeManualModal: function () {
|
||||
this.util.hideModal('#add-voter-manual-modal');
|
||||
},
|
||||
|
||||
resetManualForm: function () {
|
||||
var el = document.getElementById('add-form');
|
||||
if (el) el.reset();
|
||||
},
|
||||
|
||||
loadMemberCatalog: function () {
|
||||
var vm = this;
|
||||
this.catalogLoading = true;
|
||||
this.catalogError = '';
|
||||
axios.get(config.API + 'voter/member-catalog')
|
||||
.then(function (res) {
|
||||
vm.catalog = (res.data && res.data.catalog) ? res.data.catalog : [];
|
||||
vm.selectedNoKp = vm.catalog.filter(function (r) { return r.in_current_election; }).map(function (r) { return r.no_kp; });
|
||||
})
|
||||
.catch(function (err) {
|
||||
vm.catalogError = (err.response && err.response.data && err.response.data.message)
|
||||
? err.response.data.message
|
||||
: 'Gagal memuatkan senarai.';
|
||||
vm.util.showResult(err);
|
||||
})
|
||||
.finally(function () {
|
||||
vm.catalogLoading = false;
|
||||
});
|
||||
},
|
||||
|
||||
isSelected: function (noKp) {
|
||||
return this.selectedNoKp.indexOf(noKp) !== -1;
|
||||
},
|
||||
|
||||
toggleRow: function (noKp, evt) {
|
||||
var arr = this.selectedNoKp.slice();
|
||||
var i = arr.indexOf(noKp);
|
||||
if (evt.target.checked) {
|
||||
if (i === -1) arr.push(noKp);
|
||||
} else {
|
||||
if (i !== -1) arr.splice(i, 1);
|
||||
}
|
||||
this.selectedNoKp = arr;
|
||||
},
|
||||
|
||||
toggleHeader: function (evt) {
|
||||
var want = evt.target.checked;
|
||||
var keys = this.filteredCatalog.map(function (r) { return r.no_kp; });
|
||||
var set = {};
|
||||
var i;
|
||||
for (i = 0; i < this.selectedNoKp.length; i++) {
|
||||
set[this.selectedNoKp[i]] = true;
|
||||
}
|
||||
if (want) {
|
||||
for (i = 0; i < keys.length; i++) set[keys[i]] = true;
|
||||
} else {
|
||||
for (i = 0; i < keys.length; i++) delete set[keys[i]];
|
||||
}
|
||||
this.selectedNoKp = Object.keys(set);
|
||||
},
|
||||
|
||||
selectAllFiltered: function () {
|
||||
var set = {};
|
||||
var i;
|
||||
for (i = 0; i < this.selectedNoKp.length; i++) {
|
||||
set[this.selectedNoKp[i]] = true;
|
||||
}
|
||||
var rows = this.filteredCatalog;
|
||||
for (i = 0; i < rows.length; i++) set[rows[i].no_kp] = true;
|
||||
this.selectedNoKp = Object.keys(set);
|
||||
},
|
||||
|
||||
clearAllFiltered: function () {
|
||||
var remove = {};
|
||||
var i;
|
||||
var rows = this.filteredCatalog;
|
||||
for (i = 0; i < rows.length; i++) remove[rows[i].no_kp] = true;
|
||||
this.selectedNoKp = this.selectedNoKp.filter(function (kp) { return !remove[kp]; });
|
||||
},
|
||||
|
||||
removeSelected: function (noKp) {
|
||||
this.selectedNoKp = this.selectedNoKp.filter(function (kp) { return kp !== noKp; });
|
||||
},
|
||||
|
||||
clearAllSelected: function () {
|
||||
this.selectedNoKp = [];
|
||||
},
|
||||
|
||||
saveApplicable: function () {
|
||||
if (this.catalogSaving) return;
|
||||
var vm = this;
|
||||
this.catalogSaving = true;
|
||||
this.util.notify('Menyimpan…', 'loading');
|
||||
axios.post(config.API + 'voter/sync-applicable', { selected_no_kp: this.selectedNoKp })
|
||||
.then(function (res) {
|
||||
$.notifyClose();
|
||||
if (vm.util.showResult(res, 'success')) {
|
||||
vm.$router.push({ name: 'Manage Voter' });
|
||||
}
|
||||
})
|
||||
.catch(function (err) {
|
||||
$.notifyClose();
|
||||
vm.util.showResult(err);
|
||||
})
|
||||
.finally(function () {
|
||||
vm.catalogSaving = false;
|
||||
});
|
||||
},
|
||||
|
||||
add: function () {
|
||||
if (this.loading) return;
|
||||
this.loading = true;
|
||||
var vm = this;
|
||||
this.util.notify('Adding Voter', 'loading')
|
||||
axios.post(config.API+'voter', $('#add-form').serialize())
|
||||
.then(response=>{
|
||||
this.util.notify('Adding Voter', 'loading');
|
||||
axios.post(config.API + 'voter', $('#add-form').serialize())
|
||||
.then(function (response) {
|
||||
$.notifyClose();
|
||||
vm.loading = false;
|
||||
if (vm.util.showResult(response, 'success')) {
|
||||
vm.$router.push({name:'Manage Voter'});
|
||||
vm.closeManualModal();
|
||||
vm.resetManualForm();
|
||||
vm.$router.push({ name: 'Manage Voter' });
|
||||
}
|
||||
})
|
||||
.catch(error=>{
|
||||
.catch(function (error) {
|
||||
$.notifyClose();
|
||||
vm.loading = false;
|
||||
vm.util.showResult(error);
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -15,51 +15,36 @@
|
||||
<i class="fa fa-list"></i> Database Anggota
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover" id="position_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Bil.</th>
|
||||
<th>Nama Anggota</th>
|
||||
<th>No. Kad Pengenalan</th>
|
||||
<th>No. Anggota</th>
|
||||
<th>Unit</th>
|
||||
<th>Saham</th>
|
||||
<th>Yuran</th>
|
||||
<th>Tindakan</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(voter, i) in data.voters.data">
|
||||
<td>{{ i + 1 }}</td>
|
||||
<td>{{ voter.name }}</td>
|
||||
<td>{{ voter.no_kp }}</td>
|
||||
<td>{{ voter.no_anggota }}</td>
|
||||
<td>{{ voter.unit }}</td>
|
||||
<td>{{ voter.saham }}</td>
|
||||
<td>{{ voter.yuran }}</td>
|
||||
<td>
|
||||
<button class="btn btn-info" @click="edit(i)">
|
||||
<i class="fa fa-edit"></i> Set semula
|
||||
</button>
|
||||
<button class="btn btn-danger"
|
||||
@click="util.showModal('#delete-voter-modal'); id = voter.id">
|
||||
<i class="fa fa-trash"></i> Padam
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="data.voters.data && data.voters.data.length < 1">
|
||||
<td colspan="3">No Voters</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="kehadiran-search">
|
||||
<div class="kehadiran-search__label">Carian pantas:</div>
|
||||
<input class="form-control kehadiran-search__input kehadiran-search__input--name"
|
||||
v-model.trim="searchName" placeholder="Nama Anggota" />
|
||||
<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>
|
||||
|
||||
<admin-data-table :headers="voterTableHeaders" :items="voterListItems" :loading="voterLoading"
|
||||
:show-pagination="true" index-title="Bil." empty-text="Tiada pengundi" :exportable="true">
|
||||
<template v-slot:item-actions="{ item }">
|
||||
<button type="button" class="btn btn-info" @click="edit(item)">
|
||||
<i class="fa fa-edit"></i> Kemaskini
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger"
|
||||
@click="util.showModal('#delete-voter-modal'); id = item.id">
|
||||
<i class="fa fa-trash"></i> Padam
|
||||
</button>
|
||||
</template>
|
||||
</admin-data-table>
|
||||
|
||||
<ul class="pagination" v-if="pages.length > 1">
|
||||
<router-link tag="li" v-for="page in pages" :key="page['pages']"
|
||||
:to="{ query: { page: page['page'] } }" :class="{ 'active': current_page == page['page'] }"
|
||||
exact>
|
||||
<a href="#">{{ page['page'] }}</a>
|
||||
<router-link tag="li" v-for="page in pages" :key="page.page" :to="{ query: { page: page.page } }"
|
||||
:class="{ active: Number(current_page) === page.page }" exact>
|
||||
<a href="#">{{ page.page }}</a>
|
||||
</router-link>
|
||||
</ul>
|
||||
|
||||
@@ -80,11 +65,82 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.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-search__input--name {
|
||||
max-width: 260px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data: function () {
|
||||
return {
|
||||
id: 0
|
||||
id: 0,
|
||||
voterLoading: false,
|
||||
searchName: '',
|
||||
searchNoAnggota: '',
|
||||
searchNoKp: '',
|
||||
voterTableHeaders: [
|
||||
{ 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: 'Saham', key: 'saham', sortable: true },
|
||||
{ title: 'Yuran', key: 'yuran', sortable: true },
|
||||
{ title: 'Tindakan', key: 'actions', sortable: false }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
hasAnySearch: function () {
|
||||
return Boolean(
|
||||
(this.searchName && this.searchName.length)
|
||||
|| (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 = [];
|
||||
var last = this.data.voters && this.data.voters.last_page;
|
||||
if (last) {
|
||||
for (var i = 1; i <= last; i++) {
|
||||
pages.push({ page: i });
|
||||
}
|
||||
}
|
||||
return pages;
|
||||
},
|
||||
|
||||
current_page: function () {
|
||||
return this.$route.query.page ? this.$route.query.page : 1;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -92,17 +148,45 @@ export default {
|
||||
this.refreshVoter();
|
||||
},
|
||||
|
||||
watch: {
|
||||
'$route.query.page': function () {
|
||||
$.notifyClose();
|
||||
this.refreshVoter();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
search: function () {
|
||||
|
||||
},
|
||||
|
||||
applySearch: function () {
|
||||
var q = Object.assign({}, this.$route.query);
|
||||
q.page = 1;
|
||||
this.$router.replace({ query: q });
|
||||
this.refreshVoter();
|
||||
},
|
||||
|
||||
clearSearch: function () {
|
||||
this.searchName = '';
|
||||
this.searchNoAnggota = '';
|
||||
this.searchNoKp = '';
|
||||
this.applySearch();
|
||||
},
|
||||
|
||||
refreshVoter: function () {
|
||||
var vm = this;
|
||||
this.voterLoading = true;
|
||||
this.util.notify('Refreshing Voter', 'loading');
|
||||
axios.get(config.API + 'voter?page=' + this.current_page)
|
||||
axios.get(config.API + 'voter', {
|
||||
params: {
|
||||
page: this.current_page,
|
||||
name: this.searchName || undefined,
|
||||
no_anggota: this.searchNoAnggota || undefined,
|
||||
no_kp: this.searchNoKp || undefined
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
console.log(response)
|
||||
$.notifyClose();
|
||||
vm.data.voters = response.data;
|
||||
})
|
||||
@@ -110,11 +194,14 @@ export default {
|
||||
$.notifyClose();
|
||||
vm.util.showResult(error);
|
||||
})
|
||||
.finally(function () {
|
||||
vm.voterLoading = false;
|
||||
})
|
||||
},
|
||||
|
||||
edit: function (i) {
|
||||
edit: function (voter) {
|
||||
var vm = this;
|
||||
this.data.voter = this.data.voters.data[i];
|
||||
this.data.voter = voter;
|
||||
this.$router.push({ name: 'Edit Voter', params: { id: vm.data.voter.id } })
|
||||
},
|
||||
|
||||
@@ -132,30 +219,6 @@ export default {
|
||||
vm.util.showResult(error);
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
'$route.query.page': function () {
|
||||
$.notifyClose();
|
||||
this.refreshVoter();
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -1,52 +1,292 @@
|
||||
<template>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
<button class="btn btn-success" @click="refreshVoter()">
|
||||
<i class="fa fa-refresh"></i> Kemaskini Pengundi
|
||||
</button>
|
||||
<div class="kehadiran-actions clearfix">
|
||||
<button class="btn btn-success" @click="refreshVoter()">
|
||||
<i class="fa fa-refresh"></i> Kemaskini Pengundi
|
||||
</button>
|
||||
|
||||
<router-link class="btn btn-info" :to="{ name: 'Fizikal' }">
|
||||
Fizikal
|
||||
</router-link>
|
||||
<a class="btn btn-warning pull-right" :href="getPDFKehadiranurl(1)">
|
||||
<i class="fa fa-download"></i> Muat Turun PDF
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<router-link class="btn btn-info" :to="{ name: 'Maya' }">
|
||||
Maya
|
||||
</router-link>
|
||||
<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>
|
||||
|
||||
<a class="pdf-button-container btn btn-warning" :href="getPDFKehadiranurl(1)"><i class="fa fa-download"></i> Muat Turun PDF</a>
|
||||
<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> -->
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Bil.</th>
|
||||
<th>Nama Anggota</th>
|
||||
<th>No. Kad Pengenalan</th>
|
||||
<th>No. Anggota</th>
|
||||
<th>Unit</th>
|
||||
<th>Kehadiran</th>
|
||||
<th>Tarikh/Masa</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Display attendees -->
|
||||
<template v-for="(voter, index) in data.voters.data">
|
||||
<tr>
|
||||
<td>{{ index + 1 }}</td> <!-- Display the serial number -->
|
||||
<td>{{ voter.name }}</td>
|
||||
<td>{{ voter.no_kp }}</td>
|
||||
<td>{{ voter.no_anggota }}</td>
|
||||
<td>{{ voter.unit }}</td>
|
||||
<td>{{ getKehadiranString(voter.kehadiran) }}</td>
|
||||
<td>{{ voter.updated_at }}</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
<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>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>
|
||||
</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" />
|
||||
</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" />
|
||||
</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">
|
||||
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>
|
||||
</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 -->
|
||||
@@ -55,9 +295,10 @@
|
||||
</div>
|
||||
|
||||
<ul class="pagination" v-if="pages.length > 1">
|
||||
<router-link tag="li" v-for="page in pages" :key="page['pages']" :to="{ query: { page: page['page'] } }"
|
||||
:class="{ 'active': current_page == page['page'] }" exact>
|
||||
<a href="#">{{ page['page'] }}</a>
|
||||
<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>
|
||||
@@ -65,17 +306,167 @@
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.pdf-button-container {
|
||||
text-align: right;
|
||||
.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
|
||||
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 },
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
@@ -90,8 +481,17 @@ export default {
|
||||
|
||||
refreshVoter: function () {
|
||||
var vm = this;
|
||||
this.voterLoading = true;
|
||||
this.util.notify('Refreshing Voter', 'loading');
|
||||
axios.get(config.API + 'voter?page=' + this.current_page)
|
||||
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();
|
||||
@@ -101,6 +501,94 @@ export default {
|
||||
$.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) {
|
||||
@@ -118,6 +606,116 @@ export default {
|
||||
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;
|
||||
// Must be fizikal, must have voted
|
||||
return Number(voter.kehadiran) === 1 && Number(voter.votes_count) > 0;
|
||||
},
|
||||
|
||||
openDetail: function (item) {
|
||||
this.detailError = '';
|
||||
this.selectedVoter = Object.assign({}, item);
|
||||
this.claimCodeInput = '';
|
||||
this.payoutReference = '';
|
||||
this.payoutNote = '';
|
||||
this.util.showModal('#allowance-modal');
|
||||
},
|
||||
|
||||
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) {
|
||||
this.selectedVoter.cash_paid_at = response.data.payout.paid_at;
|
||||
this.selectedVoter.cash_payout_id = response.data.payout.id;
|
||||
} 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;
|
||||
}
|
||||
})
|
||||
.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;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -127,10 +725,45 @@ export default {
|
||||
'$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)
|
||||
@@ -146,21 +779,29 @@ export default {
|
||||
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;
|
||||
},
|
||||
|
||||
// Calculate total attendance percentage
|
||||
attendancePercentage() {
|
||||
// Count total members who attended physically (Fizikal)
|
||||
const fizikalCount = this.data.voters.data.reduce((total, voter) => {
|
||||
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.data.voters.data.reduce((total, voter) => {
|
||||
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.data.voters.data.length;
|
||||
const totalMembers = this.voterListItems.length;
|
||||
return totalMembers === 0 ? 0 : ((totalAttendance / totalMembers) * 100).toFixed(2);
|
||||
},
|
||||
|
||||
|
||||
@@ -1,14 +1,55 @@
|
||||
<template>
|
||||
<div class="container col-md-8 col-md-offset-2">
|
||||
<h4>Maklumat Pengundi</h4>
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
<div :class="wrapperClass">
|
||||
<h4 v-if="showHeader">{{ headerTitle }}</h4>
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default{
|
||||
export default {
|
||||
computed: {
|
||||
showHeader: function () {
|
||||
// Keep header hidden only for "Tambah Pengundi" (it has its own page header)
|
||||
return this.$route.name !== 'Tambah Pengundi';
|
||||
},
|
||||
headerTitle: function () {
|
||||
if (this.$route.name === 'Kehadiran Calon') return 'Kehadiran';
|
||||
return 'Maklumat Pengundi';
|
||||
},
|
||||
isWideVoterRoute: function () {
|
||||
// Give more room for heavy pages, but keep it boxed (not edge-to-edge)
|
||||
return this.$route.name === 'Tambah Pengundi'
|
||||
|| this.$route.name === 'Kehadiran Calon';
|
||||
},
|
||||
wrapperClass: function () {
|
||||
if (this.isWideVoterRoute) {
|
||||
return 'container-fluid voter-admin-wrap voter-admin-wrap--boxed voter-admin-wrap--wide';
|
||||
}
|
||||
return 'container col-md-8 col-md-offset-2 voter-admin-wrap voter-admin-wrap--boxed';
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
this.util.setTitle('MyKoPKB - Maklumat Pengundi');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Boxed card-style wrapper for all voter admin pages */
|
||||
.voter-admin-wrap--boxed {
|
||||
background: #fff;
|
||||
border: 1px solid #e6e6e6;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||
padding: 16px;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
/* Wide pages: not full screen, but wider than centered 8-col layout */
|
||||
.voter-admin-wrap--wide {
|
||||
max-width: 1400px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user