DONE: add allowance payout trace for admin, improve UI on voting side, increase pagination number
This commit is contained in:
@@ -3,10 +3,13 @@
|
||||
[x] set the env back to production in prod
|
||||
[x] saham < rm500 cannot vote
|
||||
[x] note/ rujukan cannot be fetch on modal
|
||||
|
||||
[X] butang daftar letak atas
|
||||
[x] pengesahan kehadiran perlu letak di atas (popup)
|
||||
[x] buang "disingikirkan daripada pusingan ..."
|
||||
[X] pindah kod tuntutan elaun ke atas
|
||||
[x] bug on pagination page (kehadiran)
|
||||
[x] hardcode on daftar kehadiran
|
||||
[x] buang flow pengesahan
|
||||
[x] buang flow pengesahan
|
||||
|
||||
[ ] tambah siapa yang bagi duit (activity log)
|
||||
@@ -23,5 +23,20 @@ class AllowanceClaimCode extends Model
|
||||
'expires_at' => 'datetime',
|
||||
'used_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function election()
|
||||
{
|
||||
return $this->belongsTo(Election::class);
|
||||
}
|
||||
|
||||
public function voter()
|
||||
{
|
||||
return $this->belongsTo(Voter::class);
|
||||
}
|
||||
|
||||
public function usedByAdmin()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'used_by_admin_id');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,5 +24,20 @@ class AllowancePayout extends Model
|
||||
protected $casts = [
|
||||
'paid_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function election()
|
||||
{
|
||||
return $this->belongsTo(Election::class);
|
||||
}
|
||||
|
||||
public function voter()
|
||||
{
|
||||
return $this->belongsTo(Voter::class);
|
||||
}
|
||||
|
||||
public function paidByAdmin()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'paid_by_admin_id');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -177,6 +177,8 @@ class PayoutController extends Controller
|
||||
'status' => 'success',
|
||||
'message' => 'Allowance payout recorded.',
|
||||
'payout' => $payout,
|
||||
'paid_by_admin_name' => $request->user() ? $request->user()->name : null,
|
||||
'paid_by_admin_email' => $request->user() ? $request->user()->email : null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API\v1\Admin\Allowance;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Util;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class SummaryController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
$electionId = (int) Util::getCurrentElection();
|
||||
|
||||
$q = DB::table('allowance_payouts as ap')
|
||||
->leftJoin('users as u', 'u.id', '=', 'ap.paid_by_admin_id')
|
||||
->where('ap.election_id', $electionId)
|
||||
->where('ap.status', 'paid')
|
||||
->selectRaw('ap.paid_by_admin_id as admin_id')
|
||||
->selectRaw('MAX(u.name) as admin_name')
|
||||
->selectRaw('MAX(u.email) as admin_email')
|
||||
->selectRaw('COUNT(*) as payouts_count')
|
||||
->selectRaw('COALESCE(SUM(ap.amount_cents), 0) as total_amount_cents')
|
||||
->selectRaw("COALESCE(SUM(CASE WHEN ap.method = 'cash' THEN ap.amount_cents ELSE 0 END), 0) as cash_amount_cents")
|
||||
->selectRaw("COALESCE(SUM(CASE WHEN ap.method = 'cash' THEN 1 ELSE 0 END), 0) as cash_count")
|
||||
->selectRaw("COALESCE(SUM(CASE WHEN ap.method = 'bank' THEN ap.amount_cents ELSE 0 END), 0) as bank_amount_cents")
|
||||
->selectRaw("COALESCE(SUM(CASE WHEN ap.method = 'bank' THEN 1 ELSE 0 END), 0) as bank_count")
|
||||
->groupBy('ap.paid_by_admin_id')
|
||||
->orderByDesc('total_amount_cents');
|
||||
|
||||
// Optional: filter by method (?method=cash|bank)
|
||||
$method = $request->query('method');
|
||||
if ($method === 'cash' || $method === 'bank') {
|
||||
$q->where('ap.method', $method);
|
||||
}
|
||||
|
||||
$rows = $q->get();
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'election_id' => $electionId,
|
||||
'rows' => $rows,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,8 @@ class GetController extends Controller
|
||||
$q->addSelect([
|
||||
DB::raw("(SELECT ap.reference FROM allowance_payouts ap WHERE ap.voter_id = voter.id AND ap.election_id = {$eid} AND ap.method = 'cash' AND ap.status = 'paid' ORDER BY ap.id DESC LIMIT 1) as cash_payout_reference"),
|
||||
DB::raw("(SELECT ap.note FROM allowance_payouts ap WHERE ap.voter_id = voter.id AND ap.election_id = {$eid} AND ap.method = 'cash' AND ap.status = 'paid' ORDER BY ap.id DESC LIMIT 1) as cash_payout_note"),
|
||||
DB::raw("(SELECT u.name FROM allowance_payouts ap LEFT JOIN users u ON u.id = ap.paid_by_admin_id WHERE ap.voter_id = voter.id AND ap.election_id = {$eid} AND ap.method = 'cash' AND ap.status = 'paid' ORDER BY ap.id DESC LIMIT 1) as cash_paid_by_admin_name"),
|
||||
DB::raw("(SELECT u.email FROM allowance_payouts ap LEFT JOIN users u ON u.id = ap.paid_by_admin_id WHERE ap.voter_id = voter.id AND ap.election_id = {$eid} AND ap.method = 'cash' AND ap.status = 'paid' ORDER BY ap.id DESC LIMIT 1) as cash_paid_by_admin_email"),
|
||||
]);
|
||||
|
||||
$q->selectSub(function ($sq) use ($electionId) {
|
||||
|
||||
@@ -40,4 +40,14 @@ class User extends Authenticatable
|
||||
// Do not allow impersonating main admin.
|
||||
return (int) $this->id !== 1;
|
||||
}
|
||||
|
||||
public function allowanceClaimCodes()
|
||||
{
|
||||
return $this->hasMany(AllowanceClaimCode::class, 'used_by_admin_id');
|
||||
}
|
||||
|
||||
public function allowancePayouts()
|
||||
{
|
||||
return $this->hasMany(AllowancePayout::class, 'paid_by_admin_id');
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+2
-76383
File diff suppressed because one or more lines are too long
@@ -185,7 +185,7 @@ export default {
|
||||
localPerPage: this.itemsPerPage,
|
||||
sortKey: null,
|
||||
sortDir: 'asc',
|
||||
perPageOptions: [10, 25, 50, 100]
|
||||
perPageOptions: [10, 25, 50, 100, 500, 1000]
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@@ -1,90 +1,77 @@
|
||||
<template>
|
||||
<div class="panel panel-default">
|
||||
<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()" :disabled="!canViewAdminList">
|
||||
<i class="fa fa-refresh"></i> Refresh</button>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
|
||||
<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>
|
||||
<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()" :disabled="!canViewAdminList">
|
||||
<i class="fa fa-refresh"></i> Refresh</button>
|
||||
</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 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>
|
||||
<modal-body>
|
||||
<h3>Are you sure to delete this admin?</h3>
|
||||
</modal-body>
|
||||
<modal-footer>
|
||||
<button @click="deleteAdmin()" class="btn btn-danger">Padam</button>
|
||||
<button @click="util.hideModal('#delete-admin-modal')" class="btn btn-default">Batal</button>
|
||||
</modal-footer>
|
||||
</modal>
|
||||
<modal id="impersonate-admin-modal">
|
||||
<modal-header>Menyamar Admin</modal-header>
|
||||
<modal-body>
|
||||
<h4 v-if="impersonateTarget && impersonateTarget.email">
|
||||
Menyamar <b>{{ impersonateTarget.email }}</b> ?
|
||||
</h4>
|
||||
<p class="text-muted" style="margin-bottom:0;">
|
||||
Anda akan log masuk sebagai akaun tersebut.
|
||||
</p>
|
||||
</modal-body>
|
||||
<modal-footer>
|
||||
<button @click="startImpersonate()" class="btn btn-primary">Menyamar</button>
|
||||
<button @click="util.hideModal('#impersonate-admin-modal')" class="btn btn-default">Batal</button>
|
||||
</modal-footer>
|
||||
</modal>
|
||||
</div>
|
||||
<modal id="delete-admin-modal">
|
||||
<modal-header>Delete Admin</modal-header>
|
||||
<modal-body>
|
||||
<h3>Are you sure to delete this admin?</h3>
|
||||
</modal-body>
|
||||
<modal-footer>
|
||||
<button @click="deleteAdmin()" class="btn btn-danger">Padam</button>
|
||||
<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{
|
||||
export default {
|
||||
components: { AdminDataTable },
|
||||
|
||||
data: () => ({
|
||||
id:0,
|
||||
data: () => ({
|
||||
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: 'Menyamar', key: 'impersonate', sortable: false, exportValue: function () { return ''; } },
|
||||
{ title: 'Padam', key: 'delete', sortable: false, exportValue: function () { return ''; } },
|
||||
]
|
||||
}),
|
||||
@@ -139,7 +126,7 @@ export default{
|
||||
var vm = this;
|
||||
if (!vm.impersonateTarget || !vm.impersonateTarget.id) return;
|
||||
vm.util.hideModal('#impersonate-admin-modal');
|
||||
vm.util.notify('Impersonating admin', 'loading');
|
||||
vm.util.notify('Menyamar admin', 'loading');
|
||||
|
||||
axios.post(config.API + 'admin/impersonate', { user_id: vm.impersonateTarget.id })
|
||||
.then(function (response) {
|
||||
@@ -163,7 +150,7 @@ export default{
|
||||
}
|
||||
}
|
||||
|
||||
vm.util.notify('Impersonate berjaya', 'success');
|
||||
vm.util.notify('Menyamar berjaya', 'success');
|
||||
// Ensure admin shell is refreshed (nav header shows new user)
|
||||
if (vm.$router) vm.$router.go(0);
|
||||
})
|
||||
@@ -175,15 +162,15 @@ export default{
|
||||
|
||||
deleteAdmin: function () {
|
||||
this.util.hideModal('#delete-admin-modal');
|
||||
this.util.notify('Deleting admin', 'loading');
|
||||
this.util.notify('Memadam admin', 'loading');
|
||||
var vm = this;
|
||||
axios.delete(config.API+'admin/'+this.id)
|
||||
.then(response=>{
|
||||
axios.delete(config.API + 'admin/' + this.id)
|
||||
.then(response => {
|
||||
$.notifyClose();
|
||||
if (vm.util.showResult(response, 'success'))
|
||||
vm.refreshAdmin();
|
||||
})
|
||||
.catch(error=>{
|
||||
.catch(error => {
|
||||
$.notifyClose();
|
||||
vm.util.showResult(error, 'error');
|
||||
})
|
||||
@@ -195,16 +182,16 @@ export default{
|
||||
return;
|
||||
}
|
||||
vm.loading = true;
|
||||
this.util.notify('Refreshing admin', 'loading');
|
||||
axios.get(config.API+'admin')
|
||||
.then(response=>{
|
||||
this.util.notify('Mengemaskini admin', 'loading');
|
||||
axios.get(config.API + 'admin')
|
||||
.then(response => {
|
||||
$.notifyClose();
|
||||
if (response.data.status == 'failed')
|
||||
vm.util.showResult(response, 'success');
|
||||
else
|
||||
vm.data.admins = response.data;
|
||||
})
|
||||
.catch(error=>{
|
||||
.catch(error => {
|
||||
$.notifyClose();
|
||||
vm.util.showResult(error, 'error');
|
||||
})
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
<div class="clearfix" style="margin-bottom: 12px;">
|
||||
<h4 style="margin: 0; display: inline-block;">Ringkasan Elaun</h4>
|
||||
<!-- <div class="pull-right" style="display:flex; gap:8px; align-items:center;">
|
||||
<select v-model="methodFilter" class="form-control input-sm" style="width: 160px;">
|
||||
<option value="">Semua kaedah</option>
|
||||
<option value="cash">Tunai</option>
|
||||
<option value="bank">Bank</option>
|
||||
</select>
|
||||
<button class="btn btn-default btn-sm" @click="refresh()">
|
||||
<i class="fa fa-refresh"></i> Refresh
|
||||
</button>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="alert alert-danger">{{ error }}</div>
|
||||
|
||||
<admin-data-table :headers="headers" :items="items" :loading="loading" :show-pagination="false"
|
||||
index-title="Bil." empty-text="Tiada rekod bayaran elaun" :exportable="true">
|
||||
<template v-slot:item-admin_name="{ item }">
|
||||
<div style="font-weight: 800;">{{ item.admin_name || '—' }}</div>
|
||||
<div class="text-muted" style="font-size: 12px;">{{ item.admin_email || '' }}</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item-total_amount_cents="{ item }">
|
||||
<b>{{ formatCentsToRM(item.total_amount_cents) }}</b>
|
||||
<div class="text-muted" style="font-size: 12px;">{{ item.payouts_count }} bayaran</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item-cash_amount_cents="{ item }">
|
||||
{{ formatCentsToRM(item.cash_amount_cents) }}
|
||||
<div class="text-muted" style="font-size: 12px;">{{ item.cash_count }} tunai</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item-bank_amount_cents="{ item }">
|
||||
{{ formatCentsToRM(item.bank_amount_cents) }}
|
||||
<div class="text-muted" style="font-size: 12px;">{{ item.bank_count }}</div>
|
||||
</template>
|
||||
|
||||
</admin-data-table>
|
||||
|
||||
<div v-if="!loading && items.length" class="well well-sm" style="margin-top: 12px;">
|
||||
<b>Jumlah keseluruhan:</b> {{ formatCentsToRM(grandTotalCents) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data: function () {
|
||||
return {
|
||||
loading: false,
|
||||
error: '',
|
||||
rows: [],
|
||||
methodFilter: '',
|
||||
headers: [
|
||||
{
|
||||
title: 'Admin',
|
||||
key: 'admin_name',
|
||||
sortable: true,
|
||||
exportValue: function (item) {
|
||||
var name = item && item.admin_name ? String(item.admin_name) : '—';
|
||||
var email = item && item.admin_email ? String(item.admin_email) : '';
|
||||
return email ? (name + ' (' + email + ')') : name;
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Tunai (RM300.00)',
|
||||
key: 'cash_amount_cents',
|
||||
sortable: false,
|
||||
exportValue: function (item) {
|
||||
return item && item.cash_amount_cents != null ? (Number(item.cash_amount_cents) / 100).toFixed(2) : '';
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Maya (RM150.00)',
|
||||
key: 'bank_amount_cents',
|
||||
sortable: false,
|
||||
exportValue: function (item) {
|
||||
return item && item.bank_amount_cents != null ? (Number(item.bank_amount_cents) / 100).toFixed(2) : '';
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Jumlah (RM)',
|
||||
key: 'total_amount_cents',
|
||||
sortable: true,
|
||||
exportValue: function (item) {
|
||||
return item && item.total_amount_cents != null ? (Number(item.total_amount_cents) / 100).toFixed(2) : '';
|
||||
}
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
|
||||
created: function () {
|
||||
this.refresh();
|
||||
},
|
||||
|
||||
computed: {
|
||||
items: function () {
|
||||
return Array.isArray(this.rows) ? this.rows : [];
|
||||
},
|
||||
|
||||
grandTotalCents: function () {
|
||||
return this.items.reduce((sum, r) => sum + Number(r.total_amount_cents || 0), 0);
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
refresh: function () {
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
|
||||
var params = {};
|
||||
if (this.methodFilter) params.method = this.methodFilter;
|
||||
|
||||
axios.get(config.API + 'allowance/summary', { params: params })
|
||||
.then((response) => {
|
||||
if (!response || !response.data || response.data.status !== 'success') {
|
||||
this.error = (response && response.data && response.data.message) ? response.data.message : 'Gagal memuatkan ringkasan.';
|
||||
return;
|
||||
}
|
||||
this.rows = response.data.rows || [];
|
||||
})
|
||||
.catch((error) => {
|
||||
this.error =
|
||||
(error && error.response && error.response.data && error.response.data.message)
|
||||
? error.response.data.message
|
||||
: (error && error.message ? error.message : 'Gagal memuatkan ringkasan.');
|
||||
if (error && error.response) this.util.showResult(error, 'error');
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
|
||||
formatCentsToRM: function (cents) {
|
||||
var n = Number(cents || 0) / 100;
|
||||
return n.toLocaleString('en-MY', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -16,49 +16,73 @@
|
||||
</div>
|
||||
<div class="collapse navbar-collapse" id="myNavbar">
|
||||
|
||||
<ul class="nav navbar-nav">
|
||||
<!-- {{ users }} -->
|
||||
<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 Sesi' }" v-if="data.user.role == 1" tag="li">
|
||||
<a href="#"><b>Sesi</b></a>
|
||||
<ul class="nav navbar-nav admin-nav-main">
|
||||
<router-link v-if="adminRole !== 3" :to="{ name: 'Admin Home' }" tag="li" exact>
|
||||
<a href="#"><b>Laman Utama</b></a>
|
||||
</router-link>
|
||||
|
||||
<!-- <router-link :to="{name: 'Manage Partylist'}" v-if="data.user.role==1" tag="li">
|
||||
<a href="#">Manage Partylist</a>
|
||||
</router-link> -->
|
||||
<li class="dropdown" v-if="adminRole === 1">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
|
||||
aria-haspopup="true" aria-expanded="false">
|
||||
<b>Pengurusan</b> <span class="caret"></span>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<router-link :to="{ name: 'Kemaskini Sesi' }" tag="li">
|
||||
<a href="#">Sesi</a>
|
||||
</router-link>
|
||||
<router-link :to="{ name: 'Maklumat Calon' }" tag="li">
|
||||
<a href="#">Calon</a>
|
||||
</router-link>
|
||||
<router-link :to="{ name: 'Manage Voter' }" tag="li" exact>
|
||||
<a href="#">Anggota Koperasi</a>
|
||||
</router-link>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<router-link :to="{ name: 'Maklumat Calon' }" v-if="data.user.role == 1" tag="li">
|
||||
<a href="#"><b>Calon</b></a>
|
||||
</router-link>
|
||||
|
||||
<router-link :to="{ name: 'Manage Voter' }" v-if="data.user.role == 1" tag="li" exact>
|
||||
<a href="#"><b>Anggota Koperasi</b></a>
|
||||
</router-link>
|
||||
|
||||
<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>
|
||||
|
||||
<router-link :to="{ name: 'Pembiayaan' }" v-if="data.user.role == 1" tag="li">
|
||||
<a href="#"><b>Pembiayaan Anggota</b></a>
|
||||
</router-link>
|
||||
|
||||
<router-link :to="{ name: 'Penyata Anggota' }" v-if="data.user.role == 1" tag="li">
|
||||
<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>
|
||||
|
||||
<router-link :to="{ name: 'Tetapan Portal' }" v-if="data.user.role == 1" tag="li">
|
||||
<a href="#"><b>Tetapan Portal</b></a>
|
||||
</router-link>
|
||||
<li class="dropdown" v-if="adminRole === 1 || adminRole === 3">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
|
||||
aria-haspopup="true" aria-expanded="false">
|
||||
<b>Operasi</b> <span class="caret"></span>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<router-link :to="{ name: 'Kehadiran Calon' }" tag="li">
|
||||
<a href="#">Kehadiran</a>
|
||||
</router-link>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li class="dropdown" v-if="adminRole === 1">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
|
||||
aria-haspopup="true" aria-expanded="false">
|
||||
<b>Kewangan</b> <span class="caret"></span>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<router-link :to="{ name: 'Ringkasan Elaun' }" tag="li">
|
||||
<a href="#">Ringkasan Elaun</a>
|
||||
</router-link>
|
||||
<router-link :to="{ name: 'Pembiayaan' }" tag="li">
|
||||
<a href="#">Pembiayaan Anggota</a>
|
||||
</router-link>
|
||||
<router-link :to="{ name: 'Penyata Anggota' }" tag="li">
|
||||
<a href="#">Penyata Anggota</a>
|
||||
</router-link>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li class="dropdown" v-if="adminRole === 1">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
|
||||
aria-haspopup="true" aria-expanded="false">
|
||||
<b>Sistem</b> <span class="caret"></span>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<router-link :to="{ name: 'Activity Log' }" tag="li">
|
||||
<a href="#">Log Aktiviti</a>
|
||||
</router-link>
|
||||
<router-link :to="{ name: 'Tetapan Portal' }" tag="li">
|
||||
<a href="#">Tetapan Portal</a>
|
||||
</router-link>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="nav navbar-right navbar-nav">
|
||||
<li class="dropdown">
|
||||
@@ -144,6 +168,17 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
adminRole: function () {
|
||||
// `data` is populated asynchronously; keep this defensive.
|
||||
try {
|
||||
return Number(this.data && this.data.user ? this.data.user.role : null);
|
||||
} catch (e) {
|
||||
return NaN;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
isImpersonating: function () {
|
||||
try {
|
||||
@@ -288,7 +323,7 @@ export default {
|
||||
#nav .nav>li.active>a:hover,
|
||||
#nav .nav>li.active>a:focus,
|
||||
#nav .navbar-brand:hover {
|
||||
color: #fff;
|
||||
color: #ffffff;
|
||||
background: rgba(204, 0, 0, 0.833);
|
||||
}
|
||||
|
||||
@@ -379,6 +414,23 @@ export default {
|
||||
margin: 4px 10px;
|
||||
}
|
||||
|
||||
/* Mobile submenu styling: keep dropdowns readable inside collapsed menu */
|
||||
#nav .navbar-nav .open .dropdown-menu {
|
||||
position: static;
|
||||
float: none;
|
||||
width: auto;
|
||||
margin: 0 10px 10px;
|
||||
padding: 6px;
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
box-shadow: 0 10px 24px rgba(35, 64, 64, 0.12);
|
||||
}
|
||||
|
||||
#nav .navbar-nav .open .dropdown-menu>li>a {
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.admin-main {
|
||||
padding-top: 102px;
|
||||
}
|
||||
|
||||
@@ -229,6 +229,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isCashPaid(selectedVoter)" class="well well-sm" style="margin-top: 14px;">
|
||||
<div style="font-weight: 800; margin-bottom: 8px;">Maklumat Bayaran</div>
|
||||
<div>
|
||||
Dibayar oleh:
|
||||
<b>{{ selectedVoter.cash_paid_by_admin_name || '—' }}</b>
|
||||
<span v-if="selectedVoter.cash_paid_by_admin_email" class="text-muted">
|
||||
({{ selectedVoter.cash_paid_by_admin_email }})
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-danger" v-if="detailError" style="margin-top: 12px;">
|
||||
{{ detailError }}
|
||||
</div>
|
||||
@@ -468,6 +479,7 @@
|
||||
<script>
|
||||
export default {
|
||||
data: function () {
|
||||
var vm = this;
|
||||
return {
|
||||
id: 0,
|
||||
voterLoading: false,
|
||||
@@ -487,10 +499,40 @@ export default {
|
||||
{ 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: 'Kehadiran',
|
||||
key: 'kehadiran',
|
||||
sortable: true,
|
||||
exportValue: function (item) {
|
||||
return vm.getKehadiranString(item ? item.kehadiran : null);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Pendaftaran',
|
||||
key: 'fizikal_reg',
|
||||
sortable: false,
|
||||
exportValue: function (item) {
|
||||
if (!item) return '';
|
||||
if (Number(item.kehadiran) !== 1) return '—';
|
||||
return item.fizikal_registration_verified_at ? 'Disahkan' : 'Menunggu';
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Undi',
|
||||
key: 'has_voted',
|
||||
sortable: false,
|
||||
exportValue: function (item) {
|
||||
return Number(item && item.votes_count) > 0 ? 'Undi' : 'Belum';
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
key: 'allowance',
|
||||
sortable: false,
|
||||
exportValue: function (item) {
|
||||
return vm.isCashPaid(item) ? 'Sudah Bayar' : 'Belum Bayar';
|
||||
}
|
||||
},
|
||||
{ title: 'Tindakan', key: 'action', sortable: false },
|
||||
]
|
||||
}
|
||||
@@ -723,6 +765,14 @@ export default {
|
||||
var p = response.data.payout;
|
||||
this.selectedVoter.cash_paid_at = p.paid_at;
|
||||
this.selectedVoter.cash_payout_id = p.id;
|
||||
this.selectedVoter.cash_paid_by_admin_name =
|
||||
(response && response.data && response.data.paid_by_admin_name)
|
||||
? response.data.paid_by_admin_name
|
||||
: this.selectedVoter.cash_paid_by_admin_name;
|
||||
this.selectedVoter.cash_paid_by_admin_email =
|
||||
(response && response.data && response.data.paid_by_admin_email)
|
||||
? response.data.paid_by_admin_email
|
||||
: this.selectedVoter.cash_paid_by_admin_email;
|
||||
if (p.reference != null) {
|
||||
this.selectedVoter.cash_payout_reference = p.reference;
|
||||
this.payoutReference = p.reference ? String(p.reference) : '';
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
<div>
|
||||
<div class="vote-section-kicker">Senarai Calon</div>
|
||||
<h4 class="vote-content-title">Pilih Calon Anda</h4>
|
||||
<p class="vote-page-hint">
|
||||
Tekan kad calon untuk melihat maklumat penuh (pendidikan & pengalaman).
|
||||
</p>
|
||||
</div>
|
||||
<div class="alert alert-warning vote-alert" role="alert">
|
||||
Anda hanya boleh memilih <b>satu (1) orang calon</b> sahaja.
|
||||
@@ -21,32 +24,35 @@
|
||||
<span>{{ position.name }}</span>
|
||||
<span class="vote-panel-badge">Calon Aktif</span>
|
||||
</div>
|
||||
<div class="panel-body table-responsive vote-panel-body">
|
||||
<table class="table table-hover vote-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>No. Calon</th>
|
||||
<th></th>
|
||||
<th>Nama</th>
|
||||
<th>Unit</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(nominee, index) in data.nominees" v-if="nominee.position_id == position.id"
|
||||
:key="nominee.id" class="vote-row">
|
||||
<td class="vote-checkbox-cell"><input type="checkbox" :id="nominee.id"
|
||||
:disabled="disabledCheckboxes[index]" @change="vote(position.id, nominee.id)" />
|
||||
</td>
|
||||
<td>{{ nominee.no_calon }}</td>
|
||||
<td><img @click="view(i)" :src="createBase64ImageUrl(nominee.photo)"
|
||||
class="thumbnail vote-nominee-photo" />
|
||||
</td>
|
||||
<td>{{ nominee.name }}</td>
|
||||
<td>{{ nominee.unit }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="panel-body vote-panel-body vote-nominee-cards-wrap">
|
||||
<div class="vote-nominee-card-list">
|
||||
<div v-for="(nominee, index) in data.nominees" v-if="nominee.position_id == position.id"
|
||||
:key="nominee.id" class="vote-nominee-card"
|
||||
:class="{ 'vote-nominee-card--selected': allSelectedNomineeIds.includes(nominee.id) }"
|
||||
@click="viewNomineeDetails(nominee)">
|
||||
<div class="vote-nominee-card__pick" @click.stop>
|
||||
<input type="checkbox" class="vote-nominee-card__checkbox" :id="'vote-nominee-' + nominee.id"
|
||||
:disabled="disabledCheckboxes[index]"
|
||||
:checked="allSelectedNomineeIds.includes(nominee.id)"
|
||||
@change="vote(position.id, nominee.id)" />
|
||||
</div>
|
||||
<img :src="createBase64ImageUrl(nominee.photo)" class="vote-nominee-card__photo" alt="" />
|
||||
<div class="vote-nominee-card__body">
|
||||
<span class="vote-nominee-card__no">#{{ nominee.no_calon }}</span>
|
||||
<div class="vote-nominee-card__name">{{ nominee.name }}</div>
|
||||
<div class="vote-nominee-card__unit">
|
||||
<span class="vote-nominee-card__label">Unit</span>
|
||||
{{ nominee.unit }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="vote-nominee-card__chev" aria-hidden="true">
|
||||
<i class="fa fa-angle-right"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!data.nominees.some(n => n.position_id == position.id)" class="vote-empty-state">
|
||||
Tiada calon untuk jawatan ini.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -97,6 +103,83 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showNomineeModal" class="vote-nominee-modal-backdrop" @click.self="closeNomineeModal">
|
||||
<div class="modal-dialog vote-nominee-modal-dialog" role="dialog" aria-modal="true"
|
||||
aria-labelledby="vote-nominee-modal-title">
|
||||
<div class="modal-content vote-nominee-modal-content">
|
||||
<div class="modal-header vote-nominee-modal-header">
|
||||
<div>
|
||||
<h5 class="modal-title vote-nominee-modal-title" id="vote-nominee-modal-title">Maklumat
|
||||
Calon</h5>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-body vote-nominee-modal-body">
|
||||
<div class="vote-nominee-photo-section">
|
||||
<div class="vote-nominee-photo-card">
|
||||
<img :src="nomineeViewed.photo" class="thumbnail vote-nominee-viewed-photo" alt="" />
|
||||
<div class="vote-nominee-photo-meta">
|
||||
<div class="vote-nominee-modal-name">{{ nomineeViewed.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="vote-nominee-detail-section">
|
||||
<div class="vote-nominee-summary-grid">
|
||||
<div class="vote-nominee-summary-item">
|
||||
<span class="vote-nominee-summary-label">Nama</span>
|
||||
<span class="vote-nominee-summary-value">{{ nomineeViewed.name || '-' }}</span>
|
||||
</div>
|
||||
<div class="vote-nominee-summary-item">
|
||||
<span class="vote-nominee-summary-label">No. Calon</span>
|
||||
<span class="vote-nominee-summary-value">{{ nomineeViewed.no_calon || '-' }}</span>
|
||||
</div>
|
||||
<div class="vote-nominee-summary-item">
|
||||
<span class="vote-nominee-summary-label">No. Anggota</span>
|
||||
<span class="vote-nominee-summary-value">{{ nomineeViewed.no_anggota || '-'
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="vote-nominee-summary-item">
|
||||
<span class="vote-nominee-summary-label">Unit</span>
|
||||
<span class="vote-nominee-summary-value">{{ nomineeViewed.unit || '-' }}</span>
|
||||
</div>
|
||||
<div class="vote-nominee-summary-item">
|
||||
<span class="vote-nominee-summary-label">Umur</span>
|
||||
<span class="vote-nominee-summary-value">{{ nomineeViewed.umur || '-' }}</span>
|
||||
</div>
|
||||
<div class="vote-nominee-summary-item">
|
||||
<span class="vote-nominee-summary-label">Jawatan Sekarang</span>
|
||||
<span class="vote-nominee-summary-value">{{ nomineeViewed.jawatan_sekarang || '-'
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="vote-nominee-section-card">
|
||||
<div class="vote-nominee-section-title">Taraf Pendidikan</div>
|
||||
<ol v-if="nomineeViewed.education && nomineeViewed.education.length"
|
||||
class="vote-nominee-detail-list">
|
||||
<li v-for="(item, idx) in nomineeViewed.education" :key="'ed-' + idx">{{ item }}
|
||||
</li>
|
||||
</ol>
|
||||
<p v-else class="vote-nominee-detail-empty">Tiada maklumat pendidikan.</p>
|
||||
</div>
|
||||
|
||||
<div class="vote-nominee-section-card">
|
||||
<div class="vote-nominee-section-title">Pengalaman Kerja</div>
|
||||
<ol v-if="nomineeViewed.experience && nomineeViewed.experience.length"
|
||||
class="vote-nominee-detail-list">
|
||||
<li v-for="(item, idx) in nomineeViewed.experience" :key="'ex-' + idx">{{ item }}
|
||||
</li>
|
||||
</ol>
|
||||
<p v-else class="vote-nominee-detail-empty">Tiada maklumat pengalaman kerja.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer vote-nominee-modal-footer">
|
||||
<button type="button" class="btn btn-success" @click="closeNomineeModal">Tutup</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade app-modal vote-confirm-modal" id="vote-modal" tabindex="-1" role="dialog"
|
||||
aria-labelledby="exampleModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
@@ -159,7 +242,9 @@ export default {
|
||||
name: {},
|
||||
selectedNominees: {
|
||||
},
|
||||
max: 1
|
||||
max: 1,
|
||||
nomineeViewed: {},
|
||||
showNomineeModal: false
|
||||
}
|
||||
},
|
||||
|
||||
@@ -328,9 +413,14 @@ export default {
|
||||
return 'No Partylist';
|
||||
},
|
||||
|
||||
view: function (index) {
|
||||
this.x = this.data.nominees[index];
|
||||
this.util.showModal('#nominee-information-modal');
|
||||
viewNomineeDetails: function (nominee) {
|
||||
this.nomineeViewed = { ...nominee };
|
||||
this.nomineeViewed.photo = this.createBase64ImageUrl(nominee.photo);
|
||||
this.showNomineeModal = true;
|
||||
},
|
||||
|
||||
closeNomineeModal: function () {
|
||||
this.showNomineeModal = false;
|
||||
},
|
||||
|
||||
createBase64ImageUrl: function (base64ImageData) {
|
||||
@@ -425,6 +515,14 @@ export default {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.vote-page-hint {
|
||||
margin: 8px 0 0;
|
||||
font-size: 1.35rem;
|
||||
color: #5f7388;
|
||||
line-height: 1.5;
|
||||
max-width: 520px;
|
||||
}
|
||||
|
||||
.vote-limit-pill {
|
||||
margin-bottom: 18px;
|
||||
padding: 12px 14px;
|
||||
@@ -553,54 +651,134 @@ export default {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.vote-table {
|
||||
margin-bottom: 0;
|
||||
.vote-nominee-cards-wrap {
|
||||
padding: 16px 18px 20px;
|
||||
background: #f7fafc;
|
||||
}
|
||||
|
||||
.vote-table thead {
|
||||
background: #f4f8fd;
|
||||
.vote-nominee-card-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.vote-table thead th {
|
||||
border-bottom: 1px solid #dce7f5;
|
||||
color: #48627e;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.vote-table tbody td {
|
||||
vertical-align: middle;
|
||||
border-top: 1px solid #edf2f7;
|
||||
color: #22384f;
|
||||
}
|
||||
|
||||
.vote-row {
|
||||
transition: background-color 0.18s ease, box-shadow 0.18s ease;
|
||||
}
|
||||
|
||||
.vote-row:hover {
|
||||
background: #f8fbff;
|
||||
box-shadow: inset 4px 0 0 #2f80ed;
|
||||
}
|
||||
|
||||
.vote-checkbox-cell input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
.vote-nominee-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 14px 16px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid #e6edf5;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.18s ease, box-shadow 0.18s ease, background-color 0.18s ease;
|
||||
}
|
||||
|
||||
.vote-nominee-photo {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin-bottom: 0;
|
||||
.vote-nominee-card:hover {
|
||||
border-color: #b8d4f5;
|
||||
box-shadow: 0 8px 22px rgba(47, 128, 237, 0.12);
|
||||
}
|
||||
|
||||
.vote-nominee-card--selected {
|
||||
border-color: #2f80ed;
|
||||
box-shadow: 0 0 0 2px rgba(47, 128, 237, 0.28);
|
||||
background: #f5f9ff;
|
||||
}
|
||||
|
||||
.vote-nominee-card__pick {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.vote-nominee-card__checkbox {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.vote-nominee-card__pick .vote-nominee-card__checkbox:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.vote-nominee-card__photo {
|
||||
flex-shrink: 0;
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
margin: 0;
|
||||
border-radius: 16px;
|
||||
border: 2px solid #dbe8ff;
|
||||
object-fit: cover;
|
||||
box-shadow: 0 8px 18px rgba(47, 128, 237, 0.12);
|
||||
}
|
||||
|
||||
.vote-nominee-card__body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.vote-nominee-card__no {
|
||||
display: inline-block;
|
||||
margin-bottom: 4px;
|
||||
padding: 3px 10px;
|
||||
border-radius: 999px;
|
||||
background: #dcecff;
|
||||
color: #1e5fbf;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.vote-nominee-card__name {
|
||||
font-size: 1.65rem;
|
||||
font-weight: 700;
|
||||
color: #17324d;
|
||||
line-height: 1.35;
|
||||
margin-bottom: 6px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.vote-nominee-card__unit {
|
||||
font-size: 1.35rem;
|
||||
color: #48627e;
|
||||
line-height: 1.45;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.vote-nominee-card__label {
|
||||
display: block;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: #8a9caf;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.vote-nominee-card__chev {
|
||||
flex-shrink: 0;
|
||||
color: #b0c4de;
|
||||
font-size: 2rem;
|
||||
line-height: 1;
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.vote-empty-state {
|
||||
padding: 22px 16px;
|
||||
text-align: center;
|
||||
color: #6f8193;
|
||||
font-size: 1.4rem;
|
||||
border-radius: 14px;
|
||||
border: 1px dashed #cfddec;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.vote-modal-content {
|
||||
border: 0;
|
||||
border-radius: 18px;
|
||||
@@ -671,9 +849,26 @@ export default {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Keep Unit visible on mobile; hide photo column instead */
|
||||
.vote-table thead th:nth-child(3),
|
||||
.vote-table tbody td:nth-child(3) {
|
||||
.vote-nominee-cards-wrap {
|
||||
padding: 12px 12px 16px;
|
||||
}
|
||||
|
||||
.vote-nominee-card {
|
||||
gap: 12px;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
.vote-nominee-card__photo {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.vote-nominee-card__name {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.vote-nominee-card__chev {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -723,4 +918,188 @@ export default {
|
||||
box-shadow: 0 12px 24px rgba(40, 167, 69, 0.24);
|
||||
}
|
||||
}
|
||||
|
||||
/* Nominee detail overlay — above voter/index navbar (.floating-header z-index 2000) */
|
||||
.vote-nominee-modal-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 2500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px 16px;
|
||||
background: rgba(7, 25, 48, 0.72);
|
||||
backdrop-filter: blur(3px);
|
||||
}
|
||||
|
||||
.vote-nominee-modal-dialog {
|
||||
width: 100%;
|
||||
max-width: 960px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.vote-nominee-modal-content {
|
||||
border: 0;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 22px 60px rgba(0, 0, 0, 0.28);
|
||||
}
|
||||
|
||||
.vote-nominee-modal-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
padding: 20px 24px 16px;
|
||||
border-bottom: 1px solid #e7edf5;
|
||||
background: linear-gradient(135deg, #0d6efd, #2f80ed);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.vote-nominee-modal-title {
|
||||
margin: 4px 0 0;
|
||||
font-size: 2.2rem;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.vote-nominee-modal-body {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
padding: 24px;
|
||||
background: #f5f8fc;
|
||||
}
|
||||
|
||||
.vote-nominee-photo-section {
|
||||
flex: 0 0 300px;
|
||||
}
|
||||
|
||||
.vote-nominee-photo-card {
|
||||
padding: 18px;
|
||||
border-radius: 18px;
|
||||
background: #fff;
|
||||
border: 1px solid #e5ebf3;
|
||||
box-shadow: 0 10px 24px rgba(35, 64, 97, 0.08);
|
||||
}
|
||||
|
||||
.vote-nominee-viewed-photo {
|
||||
width: 100%;
|
||||
height: 320px;
|
||||
object-fit: contain;
|
||||
object-position: center;
|
||||
border: 0;
|
||||
border-radius: 14px;
|
||||
margin: 0 0 16px;
|
||||
background: #eef3f8;
|
||||
}
|
||||
|
||||
.vote-nominee-photo-meta {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.vote-nominee-modal-name {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: #17324d;
|
||||
}
|
||||
|
||||
.vote-nominee-detail-section {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.vote-nominee-summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.vote-nominee-summary-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 14px 16px;
|
||||
border-radius: 14px;
|
||||
background: #fff;
|
||||
border: 1px solid #e5ebf3;
|
||||
box-shadow: 0 8px 22px rgba(35, 64, 97, 0.06);
|
||||
}
|
||||
|
||||
.vote-nominee-summary-label {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: #6f8193;
|
||||
}
|
||||
|
||||
.vote-nominee-summary-value {
|
||||
font-size: 1.6rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
color: #17324d;
|
||||
}
|
||||
|
||||
.vote-nominee-section-card {
|
||||
margin-bottom: 16px;
|
||||
padding: 18px 20px;
|
||||
border-radius: 16px;
|
||||
background: #fff;
|
||||
border: 1px solid #e5ebf3;
|
||||
box-shadow: 0 8px 22px rgba(35, 64, 97, 0.06);
|
||||
}
|
||||
|
||||
.vote-nominee-section-title {
|
||||
margin-bottom: 12px;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #17324d;
|
||||
}
|
||||
|
||||
.vote-nominee-detail-list {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
color: #30475f;
|
||||
}
|
||||
|
||||
.vote-nominee-detail-list li {
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.vote-nominee-detail-empty {
|
||||
margin: 0;
|
||||
color: #6f8193;
|
||||
}
|
||||
|
||||
.vote-nominee-modal-footer {
|
||||
padding: 16px 24px 22px;
|
||||
border-top: 1px solid #e7edf5;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.vote-nominee-modal-backdrop {
|
||||
align-items: flex-start;
|
||||
overflow-y: auto;
|
||||
padding: 16px 10px;
|
||||
}
|
||||
|
||||
.vote-nominee-modal-body {
|
||||
flex-direction: column;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.vote-nominee-photo-section {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.vote-nominee-viewed-photo {
|
||||
height: 260px;
|
||||
}
|
||||
|
||||
.vote-nominee-summary-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Vendored
+7
@@ -43,6 +43,7 @@ import KehadiranCalon from "./components/demo/admin/voter/kehadiran.vue";
|
||||
import KehadiranCalonFizikal from "./components/demo/admin/voter/fizikal.vue";
|
||||
import KehadiranCalonMaya from "./components/demo/admin/voter/maya.vue";
|
||||
import ViewKehadiranCalon from "./components/demo/admin/voter/viewkehadiran.vue";
|
||||
import AllowanceSummary from "./components/demo/admin/allowance/summary.vue";
|
||||
|
||||
import ManageNominee from "./components/demo/admin/nominee/nominee.vue";
|
||||
import ManageNomineeIndex from "./components/demo/admin/nominee/index.vue";
|
||||
@@ -287,6 +288,12 @@ export default [
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
path: "allowance-summary",
|
||||
component: AllowanceSummary,
|
||||
name: "Ringkasan Elaun",
|
||||
},
|
||||
|
||||
{
|
||||
path: "nominee",
|
||||
component: ManageNominee,
|
||||
|
||||
@@ -96,6 +96,7 @@ Route::prefix('v1')->group(function () { //Version 1 of my Rest API
|
||||
Route::post('payout', 'API\v1\Admin\Allowance\PayoutController');
|
||||
Route::post('verify-code', 'API\v1\Admin\Allowance\VerifyCodeController');
|
||||
Route::post('void/{id}', 'API\v1\Admin\Allowance\VoidController');
|
||||
Route::get('summary', 'API\v1\Admin\Allowance\SummaryController');
|
||||
});
|
||||
|
||||
Route::prefix('nominee')->group(function () {
|
||||
|
||||
Reference in New Issue
Block a user