DONE: centralize theme color, interactive dashboard, increase winner result, refer TODO
This commit is contained in:
@@ -13,3 +13,16 @@
|
|||||||
[x] buang flow pengesahan
|
[x] buang flow pengesahan
|
||||||
[x] tambah siapa yang bagi duit (activity log)
|
[x] tambah siapa yang bagi duit (activity log)
|
||||||
[ ] there is certain part/files that is hardcoded
|
[ ] there is certain part/files that is hardcoded
|
||||||
|
|
||||||
|
Penambahan Untuk Sistem Mengundi
|
||||||
|
[x] Pendaftaran kehadiran (fizikal/maya)
|
||||||
|
[x] elaun kehadiran untuk pendaftaran (kupon pendaftaran)
|
||||||
|
[x] buang pautan pantas yang tidak perlu
|
||||||
|
[x] roulette wheel
|
||||||
|
[x] online zoom (jika ada)
|
||||||
|
[ ] tambah checking bajet tally atau tak
|
||||||
|
[x] saiful adli tukar jadi fizikal
|
||||||
|
[x] tambah total yang kena bayar untuk (maya/fizikal)
|
||||||
|
[ ] tidak boleh hantar wakil untuk tebus elaun
|
||||||
|
[x] tambah statistik untuk bilangan mengundi di tab kehadiran
|
||||||
|
[x] guna data dummy (sekiranya ada)
|
||||||
@@ -37,10 +37,65 @@ class SummaryController extends Controller
|
|||||||
|
|
||||||
$rows = $q->get();
|
$rows = $q->get();
|
||||||
|
|
||||||
|
$defaultCashCents = 30000;
|
||||||
|
$defaultBankCents = 15000;
|
||||||
|
|
||||||
|
$outstandingCash = (int) DB::table('voter as v')
|
||||||
|
->where('v.election_id', $electionId)
|
||||||
|
->where('v.kehadiran', 1)
|
||||||
|
->where(function ($outer) use ($electionId) {
|
||||||
|
$outer->whereExists(function ($sub) use ($electionId) {
|
||||||
|
$sub->select(DB::raw(1))
|
||||||
|
->from('result as r')
|
||||||
|
->whereColumn('r.voter_id', 'v.id')
|
||||||
|
->where('r.election_id', $electionId);
|
||||||
|
})->orWhere(function ($q) {
|
||||||
|
$q->whereNotNull('v.fizikal_registration_verified_at')
|
||||||
|
->whereRaw('COALESCE(v.saham, 0) < 500');
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->whereNotExists(function ($sub) use ($electionId) {
|
||||||
|
$sub->select(DB::raw(1))
|
||||||
|
->from('allowance_payouts as ap')
|
||||||
|
->whereColumn('ap.voter_id', 'v.id')
|
||||||
|
->where('ap.election_id', $electionId)
|
||||||
|
->where('ap.method', 'cash')
|
||||||
|
->where('ap.status', 'paid');
|
||||||
|
})
|
||||||
|
->count();
|
||||||
|
|
||||||
|
$outstandingBank = (int) DB::table('voter as v')
|
||||||
|
->where('v.election_id', $electionId)
|
||||||
|
->where('v.kehadiran', 2)
|
||||||
|
->whereExists(function ($sub) use ($electionId) {
|
||||||
|
$sub->select(DB::raw(1))
|
||||||
|
->from('result as r')
|
||||||
|
->whereColumn('r.voter_id', 'v.id')
|
||||||
|
->where('r.election_id', $electionId);
|
||||||
|
})
|
||||||
|
->whereNotExists(function ($sub) use ($electionId) {
|
||||||
|
$sub->select(DB::raw(1))
|
||||||
|
->from('allowance_payouts as ap')
|
||||||
|
->whereColumn('ap.voter_id', 'v.id')
|
||||||
|
->where('ap.election_id', $electionId)
|
||||||
|
->where('ap.method', 'bank')
|
||||||
|
->where('ap.status', 'paid');
|
||||||
|
})
|
||||||
|
->count();
|
||||||
|
|
||||||
|
$outstandingAllowanceCents = ($outstandingCash * $defaultCashCents) + ($outstandingBank * $defaultBankCents);
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'status' => 'success',
|
'status' => 'success',
|
||||||
'election_id' => $electionId,
|
'election_id' => $electionId,
|
||||||
'rows' => $rows,
|
'rows' => $rows,
|
||||||
|
'outstanding_allowance' => [
|
||||||
|
'cents' => $outstandingAllowanceCents,
|
||||||
|
'cash_count' => $outstandingCash,
|
||||||
|
'bank_count' => $outstandingBank,
|
||||||
|
'default_cash_cents' => $defaultCashCents,
|
||||||
|
'default_bank_cents' => $defaultBankCents,
|
||||||
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\API\v1\Election;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Voter;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class AttendanceSummaryController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Aggregate attendance for a specific election (by primary key), for admin reporting.
|
||||||
|
* Kehadiran: null/0 = belum ditetapkan, 1 = fizikal, 2 = maya.
|
||||||
|
*/
|
||||||
|
public function __invoke($id)
|
||||||
|
{
|
||||||
|
$electionId = (int) $id;
|
||||||
|
|
||||||
|
$row = Voter::query()
|
||||||
|
->where('election_id', $electionId)
|
||||||
|
->selectRaw('COUNT(*) as total_registered')
|
||||||
|
->selectRaw('COALESCE(SUM(CASE WHEN kehadiran IS NULL OR kehadiran = 0 THEN 1 ELSE 0 END), 0) as unset')
|
||||||
|
->selectRaw('COALESCE(SUM(CASE WHEN kehadiran = 1 THEN 1 ELSE 0 END), 0) as fizikal')
|
||||||
|
->selectRaw('COALESCE(SUM(CASE WHEN kehadiran = 2 THEN 1 ELSE 0 END), 0) as maya')
|
||||||
|
->selectRaw('COALESCE(SUM(CASE WHEN kehadiran IN (1, 2) THEN 1 ELSE 0 END), 0) as marked_present')
|
||||||
|
->selectRaw('COALESCE(SUM(CASE WHEN kehadiran = 1 AND fizikal_registration_verified_at IS NULL THEN 1 ELSE 0 END), 0) as fizikal_reg_pending')
|
||||||
|
->selectRaw('COALESCE(SUM(CASE WHEN kehadiran = 1 AND fizikal_registration_verified_at IS NOT NULL THEN 1 ELSE 0 END), 0) as fizikal_reg_verified')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$total = (int) ($row ? $row->total_registered : 0);
|
||||||
|
$marked = (int) ($row ? $row->marked_present : 0);
|
||||||
|
|
||||||
|
$votersWhoVoted = (int) DB::table('result')
|
||||||
|
->where('election_id', $electionId)
|
||||||
|
->select(DB::raw('COUNT(DISTINCT voter_id) as c'))
|
||||||
|
->value('c');
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'election_id' => $electionId,
|
||||||
|
'total_registered' => $total,
|
||||||
|
'unset' => (int) ($row ? $row->unset : 0),
|
||||||
|
'fizikal' => (int) ($row ? $row->fizikal : 0),
|
||||||
|
'maya' => (int) ($row ? $row->maya : 0),
|
||||||
|
'marked_present' => $marked,
|
||||||
|
'attendance_rate' => $total > 0 ? round(($marked / $total) * 100, 2) : 0.0,
|
||||||
|
'fizikal_registration_pending' => (int) ($row ? $row->fizikal_reg_pending : 0),
|
||||||
|
'fizikal_registration_verified' => (int) ($row ? $row->fizikal_reg_verified : 0),
|
||||||
|
'voters_who_voted' => $votersWhoVoted,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\API\v1\Election;
|
||||||
|
|
||||||
|
use App\Election;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Controllers\Util;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
class RegisterNextSessionController extends Controller
|
||||||
|
{
|
||||||
|
public function __invoke(Request $request)
|
||||||
|
{
|
||||||
|
$this->validateRequest($request);
|
||||||
|
|
||||||
|
if (Util::getElectionStatus() !== 3) {
|
||||||
|
return response()->json([
|
||||||
|
'status' => 'failed',
|
||||||
|
'message' => 'Daftar sesi baharu hanya tersedia selepas undian telah tamat.',
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
Util::ensureDraftElectionAfterCompletedCycle();
|
||||||
|
|
||||||
|
$election = Election::find(Util::getCurrentElection());
|
||||||
|
if ($request->filled('name')) {
|
||||||
|
$election->update(['name' => $request->name]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $request->user();
|
||||||
|
activity()
|
||||||
|
->withProperties([
|
||||||
|
'election_id' => (int) $election->id,
|
||||||
|
'election_name' => (string) $request->input('name', ''),
|
||||||
|
'registered_by_admin_id' => $user ? (int) $user->id : null,
|
||||||
|
'registered_by_admin_email' => $user ? $user->email : null,
|
||||||
|
'ip' => $request->ip(),
|
||||||
|
'user_agent' => $request->userAgent(),
|
||||||
|
])
|
||||||
|
->log('election register next session');
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'status' => 'success',
|
||||||
|
'message' => 'Sesi mengundi baharu didaftarkan. Sila lengkapkan maklumat calon sebelum memulakan undian.',
|
||||||
|
'election' => $election->fresh(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function validateRequest($request)
|
||||||
|
{
|
||||||
|
$id = Auth::id();
|
||||||
|
$this->validate($request, [
|
||||||
|
'password' => 'required|password:users,password,' . $id,
|
||||||
|
'name' => 'nullable|string|max:60',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,8 @@ class StartController extends Controller
|
|||||||
{
|
{
|
||||||
$this->validateRequest($request);
|
$this->validateRequest($request);
|
||||||
|
|
||||||
|
Util::ensureDraftElectionAfterCompletedCycle();
|
||||||
|
|
||||||
if ($this->hasNoNominee())
|
if ($this->hasNoNominee())
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'status' => 'failed',
|
'status' => 'failed',
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ class StopController extends Controller
|
|||||||
'name'=>$request->name,
|
'name'=>$request->name,
|
||||||
'end'=>date('Y-m-d H:i:s')
|
'end'=>date('Y-m-d H:i:s')
|
||||||
]);
|
]);
|
||||||
Election::create();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function validateRequest($request)
|
private function validateRequest($request)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ class AddController extends Controller
|
|||||||
|
|
||||||
public function __invoke (Request $request)
|
public function __invoke (Request $request)
|
||||||
{
|
{
|
||||||
|
Util::ensureDraftElectionAfterCompletedCycle();
|
||||||
$this->validateRequest($request);
|
$this->validateRequest($request);
|
||||||
$nominee = $this->insertNominee($request);
|
$nominee = $this->insertNominee($request);
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ class AddController extends Controller
|
|||||||
{
|
{
|
||||||
public function __invoke(Request $request)
|
public function __invoke(Request $request)
|
||||||
{
|
{
|
||||||
|
Util::ensureDraftElectionAfterCompletedCycle();
|
||||||
$this->validateRequest($request);
|
$this->validateRequest($request);
|
||||||
$this->insertPartylist($request);
|
$this->insertPartylist($request);
|
||||||
|
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ use Illuminate\Http\Request;
|
|||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Controllers\Util;
|
use App\Http\Controllers\Util;
|
||||||
use App\Election;
|
|
||||||
use App\Position;
|
use App\Position;
|
||||||
|
|
||||||
class AddPositionController extends Controller
|
class AddPositionController extends Controller
|
||||||
{
|
{
|
||||||
public function __invoke(Request $request)
|
public function __invoke(Request $request)
|
||||||
{
|
{
|
||||||
|
Util::ensureDraftElectionAfterCompletedCycle();
|
||||||
$this->validateRequest($request);
|
$this->validateRequest($request);
|
||||||
$this->insertPosition($request);
|
$this->insertPosition($request);
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ class AddController extends Controller
|
|||||||
{
|
{
|
||||||
public function __invoke(Request $request)
|
public function __invoke(Request $request)
|
||||||
{
|
{
|
||||||
|
Util::ensureDraftElectionAfterCompletedCycle();
|
||||||
$this->validateRequest($request);
|
$this->validateRequest($request);
|
||||||
$voter = $this->insertVoter($request);
|
$voter = $this->insertVoter($request);
|
||||||
|
|
||||||
|
|||||||
@@ -113,6 +113,47 @@ class GetController extends Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vote status: 1 = has voted, 0 = has not voted (for this election)
|
||||||
|
$hasVoted = request('has_voted');
|
||||||
|
if ($hasVoted !== null && $hasVoted !== '') {
|
||||||
|
$hv = (string) $hasVoted;
|
||||||
|
if ($hv === '1' || $hv === 'true' || $hv === 'yes') {
|
||||||
|
$q->whereExists(function ($sub) use ($electionId) {
|
||||||
|
$sub->from('result')
|
||||||
|
->whereColumn('result.voter_id', 'voter.id')
|
||||||
|
->where('result.election_id', $electionId);
|
||||||
|
});
|
||||||
|
} elseif ($hv === '0' || $hv === 'false' || $hv === 'no') {
|
||||||
|
$q->whereNotExists(function ($sub) use ($electionId) {
|
||||||
|
$sub->from('result')
|
||||||
|
->whereColumn('result.voter_id', 'voter.id')
|
||||||
|
->where('result.election_id', $electionId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cash allowance payment status (matches admin "Status" column: cash_paid_at)
|
||||||
|
$cashStatus = request('cash_status');
|
||||||
|
if ($cashStatus !== null && $cashStatus !== '') {
|
||||||
|
if ($cashStatus === 'paid') {
|
||||||
|
$q->whereExists(function ($sub) use ($electionId) {
|
||||||
|
$sub->from('allowance_payouts')
|
||||||
|
->whereColumn('allowance_payouts.voter_id', 'voter.id')
|
||||||
|
->where('allowance_payouts.election_id', $electionId)
|
||||||
|
->where('allowance_payouts.method', 'cash')
|
||||||
|
->where('allowance_payouts.status', 'paid');
|
||||||
|
});
|
||||||
|
} elseif ($cashStatus === 'unpaid') {
|
||||||
|
$q->whereNotExists(function ($sub) use ($electionId) {
|
||||||
|
$sub->from('allowance_payouts')
|
||||||
|
->whereColumn('allowance_payouts.voter_id', 'voter.id')
|
||||||
|
->where('allowance_payouts.election_id', $electionId)
|
||||||
|
->where('allowance_payouts.method', 'cash')
|
||||||
|
->where('allowance_payouts.status', 'paid');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Return all voters with pagination
|
// Return all voters with pagination
|
||||||
return $q->paginate(1000);
|
return $q->paginate(1000);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ class SyncApplicableVotersController extends Controller
|
|||||||
'selected_no_kp.*' => 'nullable|string|max:60',
|
'selected_no_kp.*' => 'nullable|string|max:60',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
Util::ensureDraftElectionAfterCompletedCycle();
|
||||||
$electionId = Util::getCurrentElection();
|
$electionId = Util::getCurrentElection();
|
||||||
$selected = collect($request->input('selected_no_kp', []))
|
$selected = collect($request->input('selected_no_kp', []))
|
||||||
->map(function ($v) {
|
->map(function ($v) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Election;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
@@ -37,6 +38,18 @@ class Util extends Controller
|
|||||||
return \App\Election::find(Util::getCurrentElection())->status;
|
return \App\Election::find(Util::getCurrentElection())->status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the latest election has finished (status 3), the next setup actions
|
||||||
|
* must attach to a new draft row. Call before writes that bind to getCurrentElection().
|
||||||
|
*/
|
||||||
|
public static function ensureDraftElectionAfterCompletedCycle(): void
|
||||||
|
{
|
||||||
|
if (self::getElectionStatus() !== 3) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Election::create();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Upload image and return it's path
|
* Upload image and return it's path
|
||||||
* @param {Request} $request
|
* @param {Request} $request
|
||||||
|
|||||||
Generated
+19
@@ -8,6 +8,7 @@
|
|||||||
"@bachdgvn/vue-otp-input": "^1.0.8",
|
"@bachdgvn/vue-otp-input": "^1.0.8",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"bootstrap": "^5.3.8",
|
"bootstrap": "^5.3.8",
|
||||||
|
"chart.js": "^4.5.1",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"jsbarcode": "^3.12.3",
|
"jsbarcode": "^3.12.3",
|
||||||
"jspdf": "^4.2.1",
|
"jspdf": "^4.2.1",
|
||||||
@@ -2055,6 +2056,12 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@kurkle/color": {
|
||||||
|
"version": "0.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
|
||||||
|
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@leichtgewicht/ip-codec": {
|
"node_modules/@leichtgewicht/ip-codec": {
|
||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz",
|
||||||
@@ -3946,6 +3953,18 @@
|
|||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/chart.js": {
|
||||||
|
"version": "4.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
|
||||||
|
"integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@kurkle/color": "^0.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"pnpm": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chokidar": {
|
"node_modules/chokidar": {
|
||||||
"version": "1.7.0",
|
"version": "1.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz",
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
"@bachdgvn/vue-otp-input": "^1.0.8",
|
"@bachdgvn/vue-otp-input": "^1.0.8",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"bootstrap": "^5.3.8",
|
"bootstrap": "^5.3.8",
|
||||||
|
"chart.js": "^4.5.1",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"jsbarcode": "^3.12.3",
|
"jsbarcode": "^3.12.3",
|
||||||
"jspdf": "^4.2.1",
|
"jspdf": "^4.2.1",
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 149 KiB After Width: | Height: | Size: 149 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 899 KiB |
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -1,3 +1,17 @@
|
|||||||
|
/*!
|
||||||
|
* @kurkle/color v0.3.4
|
||||||
|
* https://github.com/kurkle/color#readme
|
||||||
|
* (c) 2024 Jukka Kurkela
|
||||||
|
* Released under the MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* Chart.js v4.5.1
|
||||||
|
* https://www.chartjs.org
|
||||||
|
* (c) 2025 Chart.js Contributors
|
||||||
|
* Released under the MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* The buffer module from node.js, for the browser.
|
* The buffer module from node.js, for the browser.
|
||||||
*
|
*
|
||||||
|
|||||||
Vendored
+6
-1
@@ -1,8 +1,9 @@
|
|||||||
require('./bootstrap.js');
|
require('./bootstrap.js');
|
||||||
|
import './styles/theme.css';
|
||||||
require('./helper/autocloseMenu.js');
|
require('./helper/autocloseMenu.js');
|
||||||
//window.VueRouter = require('vue-router').default;
|
//window.VueRouter = require('vue-router').default;
|
||||||
//import VueRouter from 'vue-router';
|
//import VueRouter from 'vue-router';
|
||||||
import Util from './util.js';
|
import Util, { methods as utilMethods } from './util.js';
|
||||||
import routes from './routesIndex.js';
|
import routes from './routesIndex.js';
|
||||||
import OtpInput from "@bachdgvn/vue-otp-input";
|
import OtpInput from "@bachdgvn/vue-otp-input";
|
||||||
|
|
||||||
@@ -19,6 +20,10 @@ const router = new VueRouter({
|
|||||||
});
|
});
|
||||||
|
|
||||||
router.beforeEach(function (to, from, next) {
|
router.beforeEach(function (to, from, next) {
|
||||||
|
if (to.name === 'Voter Login' && utilMethods.isAdminPortalSession()) {
|
||||||
|
var dest = utilMethods.getAdminEntryRoute();
|
||||||
|
return next(Object.assign({}, dest, { replace: true }));
|
||||||
|
}
|
||||||
if (!to.path.startsWith('/admin') || to.name === 'Admin Login') {
|
if (!to.path.startsWith('/admin') || to.name === 'Admin Login') {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -462,19 +462,20 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
.admin-data-table__toolbar {
|
.admin-data-table__toolbar {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-data-table {
|
.admin-data-table {
|
||||||
--adt-header-bg: #1976d2;
|
--adt-header-bg: var(--admin-table-header-bg);
|
||||||
--adt-header-color: #fff;
|
--adt-header-color: var(--admin-table-header-color);
|
||||||
--adt-radius: 12px;
|
--adt-radius: 12px;
|
||||||
--adt-border: 1px solid #e0e0e0;
|
--adt-border: 1px solid var(--admin-table-border-color);
|
||||||
--adt-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
--adt-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||||
--adt-row-hover: #e3f2fd;
|
--adt-row-hover: var(--admin-table-row-hover-bg);
|
||||||
--adt-footer-bg: #fafafa;
|
--adt-footer-bg: var(--admin-table-footer-bg);
|
||||||
--adt-text-muted: #757575;
|
--adt-text-muted: var(--admin-table-text-muted);
|
||||||
--adt-font-size: 16px;
|
--adt-font-size: 16px;
|
||||||
--adt-header-font-size: 16px;
|
--adt-header-font-size: 16px;
|
||||||
}
|
}
|
||||||
@@ -484,7 +485,7 @@ export default {
|
|||||||
border: var(--adt-border);
|
border: var(--adt-border);
|
||||||
box-shadow: var(--adt-shadow);
|
box-shadow: var(--adt-shadow);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #fff;
|
background: var(--admin-table-shell-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-data-table__scroll {
|
.admin-data-table__scroll {
|
||||||
@@ -582,12 +583,12 @@ export default {
|
|||||||
.admin-data-table__table tbody td {
|
.admin-data-table__table tbody td {
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
border-top: 1px solid #eee;
|
border-top: 1px solid var(--admin-table-row-border-color);
|
||||||
font-size: var(--adt-font-size);
|
font-size: var(--adt-font-size);
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-data-table__table tbody tr:first-child:not(.admin-data-table__state-row) td {
|
.admin-data-table__table tbody tr:first-child:not(.admin-data-table__state-row) td {
|
||||||
border-top: 1px solid #e0e0e0;
|
border-top: 1px solid var(--admin-table-border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-data-table__table tbody tr.admin-data-table__state-row td {
|
.admin-data-table__table tbody tr.admin-data-table__state-row td {
|
||||||
@@ -617,7 +618,7 @@ export default {
|
|||||||
.admin-data-table__state-title {
|
.admin-data-table__state-title {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #424242;
|
color: var(--admin-table-text-strong);
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -663,7 +664,7 @@ export default {
|
|||||||
.admin-data-table__footer-mid {
|
.admin-data-table__footer-mid {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #616161;
|
color: var(--admin-table-footer-mid-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-data-table__footer-right {
|
.admin-data-table__footer-right {
|
||||||
@@ -681,7 +682,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.admin-data-table__pagination>li.disabled>a {
|
.admin-data-table__pagination>li.disabled>a {
|
||||||
color: #bbb;
|
color: var(--admin-table-pagination-disabled);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
<template>
|
||||||
|
<div class="bar-chart" :style="wrapperStyle">
|
||||||
|
<canvas ref="canvas"></canvas>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Chart from 'chart.js/auto';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'BarChart',
|
||||||
|
|
||||||
|
props: {
|
||||||
|
labels: { type: Array, default: () => [] },
|
||||||
|
values: { type: Array, default: () => [] },
|
||||||
|
datasetLabel: { type: String, default: 'Undi' },
|
||||||
|
height: { type: [Number, String], default: 220 },
|
||||||
|
backgroundColor: { type: [String, Array], default: 'rgba(54, 162, 235, 0.75)' },
|
||||||
|
borderColor: { type: [String, Array], default: 'rgba(54, 162, 235, 1)' },
|
||||||
|
borderWidth: { type: Number, default: 0 },
|
||||||
|
borderRadius: { type: [Number, Array], default: 0 },
|
||||||
|
options: { type: Object, default: () => ({}) }
|
||||||
|
},
|
||||||
|
|
||||||
|
data: () => ({
|
||||||
|
chart: null
|
||||||
|
}),
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
wrapperStyle: function () {
|
||||||
|
var h = this.height;
|
||||||
|
var px = (typeof h === 'number') ? (h + 'px') : String(h);
|
||||||
|
return { height: px };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
labels: function () { this.renderChart(); },
|
||||||
|
values: function () { this.renderChart(); },
|
||||||
|
options: {
|
||||||
|
handler: function () { this.renderChart(); },
|
||||||
|
deep: true
|
||||||
|
},
|
||||||
|
backgroundColor: function () { this.renderChart(); },
|
||||||
|
borderColor: function () { this.renderChart(); },
|
||||||
|
borderWidth: function () { this.renderChart(); },
|
||||||
|
borderRadius: function () { this.renderChart(); }
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted: function () {
|
||||||
|
this.renderChart();
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeDestroy: function () {
|
||||||
|
this.destroyChart();
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
destroyChart: function () {
|
||||||
|
if (!this.chart) return;
|
||||||
|
try { this.chart.destroy(); } catch (e) { }
|
||||||
|
this.chart = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
renderChart: function () {
|
||||||
|
this.destroyChart();
|
||||||
|
if (!this.$refs || !this.$refs.canvas) return;
|
||||||
|
|
||||||
|
var ctx = this.$refs.canvas.getContext('2d');
|
||||||
|
var labels = (this.labels || []).map(function (x) { return String(x); });
|
||||||
|
var values = (this.values || []).map(function (v) { return Number(v) || 0; });
|
||||||
|
|
||||||
|
var baseOptions = {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: { display: false },
|
||||||
|
tooltip: { enabled: true }
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: { ticks: { maxRotation: 45, minRotation: 45, autoSkip: false } },
|
||||||
|
y: { beginAtZero: true, ticks: { precision: 0 } }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Shallow merge is enough for our use-cases (caller can override nested keys).
|
||||||
|
var mergedOptions = Object.assign({}, baseOptions, this.options || {});
|
||||||
|
|
||||||
|
this.chart = new Chart(ctx, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: labels,
|
||||||
|
datasets: [{
|
||||||
|
label: this.datasetLabel,
|
||||||
|
data: values,
|
||||||
|
backgroundColor: this.backgroundColor,
|
||||||
|
borderColor: this.borderColor,
|
||||||
|
borderWidth: this.borderWidth,
|
||||||
|
borderRadius: this.borderRadius
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: mergedOptions
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.bar-chart {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
<template>
|
||||||
|
<nav class="navbar navbar-default navbar-fixed-top floating-header logo-header" id="nav">
|
||||||
|
<div class="container">
|
||||||
|
<div class="navbar-header logo-section">
|
||||||
|
<button type="button" class="navbar-toggle" data-toggle="collapse" :data-target="'#' + collapseId">
|
||||||
|
<span class="icon-bar"></span>
|
||||||
|
<span class="icon-bar"></span>
|
||||||
|
<span class="icon-bar"></span>
|
||||||
|
</button>
|
||||||
|
<Logo :href="logoHref" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="collapse navbar-collapse" :id="collapseId">
|
||||||
|
<ul class="nav navbar-nav" :class="mainNavClass">
|
||||||
|
<slot name="left" />
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<ul class="nav navbar-right navbar-nav">
|
||||||
|
<slot name="right" />
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Logo from './Logo.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'FloatingNavbar',
|
||||||
|
components: { Logo },
|
||||||
|
props: {
|
||||||
|
logoHref: { type: String, required: true },
|
||||||
|
collapseId: { type: String, default: 'myNavbar' },
|
||||||
|
mainNavClass: { type: [String, Object, Array], default: null }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Layout & structure — color tokens from theme.css (:root), loaded in app.js */
|
||||||
|
.floating-header {
|
||||||
|
position: fixed !important;
|
||||||
|
top: 16px !important;
|
||||||
|
left: 16px !important;
|
||||||
|
right: 16px !important;
|
||||||
|
width: calc(100% - 32px) !important;
|
||||||
|
margin: 0;
|
||||||
|
border-radius: 16px !important;
|
||||||
|
z-index: 2000 !important;
|
||||||
|
transition: all 0.3s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-header {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
var(--floating-header-bg-1) 0%,
|
||||||
|
var(--floating-header-bg-2) 38%,
|
||||||
|
var(--floating-header-bg-3) 72%,
|
||||||
|
var(--floating-header-bg-4) 100%
|
||||||
|
) !important;
|
||||||
|
border: 1px solid var(--floating-header-border) !important;
|
||||||
|
box-shadow:
|
||||||
|
0 8px 32px var(--floating-header-shadow-1),
|
||||||
|
0 4px 16px var(--floating-header-shadow-2) !important;
|
||||||
|
backdrop-filter: blur(12px) !important;
|
||||||
|
position: fixed;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-header::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
var(--floating-header-overlay-1) 0%,
|
||||||
|
transparent 50%,
|
||||||
|
var(--floating-header-overlay-2) 100%
|
||||||
|
);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-header > * {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav.navbar {
|
||||||
|
border: none;
|
||||||
|
min-height: 78px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav .container {
|
||||||
|
width: 100%;
|
||||||
|
padding-left: 22px;
|
||||||
|
padding-right: 22px;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav .navbar-nav {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav .navbar-brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
height: auto;
|
||||||
|
padding: 14px 0;
|
||||||
|
color: var(--floating-header-brand-text);
|
||||||
|
font-weight: 700;
|
||||||
|
text-shadow: 0 1px 2px var(--floating-header-brand-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-title {
|
||||||
|
display: inline-block;
|
||||||
|
color: var(--floating-header-brand-text) !important;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-shadow: 0 1px 2px var(--floating-header-brand-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav .nav > li > a,
|
||||||
|
#nav .navbar-toggle {
|
||||||
|
color: var(--floating-header-nav-text);
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav .nav > li > a:hover,
|
||||||
|
#nav .nav > li.active > a,
|
||||||
|
#nav .nav > li.active > a:hover,
|
||||||
|
#nav .nav > li.active > a:focus,
|
||||||
|
#nav .navbar-brand:hover {
|
||||||
|
color: var(--floating-header-brand-text);
|
||||||
|
background: var(--floating-header-hover-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav .nav > li > a {
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-top: 18px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
padding-top: 12px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav .navbar-right {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav .navbar-nav > li,
|
||||||
|
#nav .dropdown {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav .dropdown-menu {
|
||||||
|
margin-top: 6px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 12px 28px var(--floating-header-dropdown-menu-shadow);
|
||||||
|
z-index: 2101;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav .dropdown-menu > li > a {
|
||||||
|
color: var(--admin-table-text-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav .dropdown-menu > li > a:hover {
|
||||||
|
background-color: var(--floating-header-dropdown-hover-bg);
|
||||||
|
color: var(--floating-header-dropdown-hover-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav .navbar-toggle {
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-right: 18px;
|
||||||
|
border-color: var(--floating-header-toggle-border);
|
||||||
|
background: var(--floating-header-toggle-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav .navbar-toggle .icon-bar {
|
||||||
|
background-color: var(--floating-header-icon-bar);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand img {
|
||||||
|
background: transparent !important;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.floating-header {
|
||||||
|
top: 10px !important;
|
||||||
|
left: 10px !important;
|
||||||
|
right: 10px !important;
|
||||||
|
width: calc(100% - 20px) !important;
|
||||||
|
border-radius: 14px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-title {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav .navbar-brand img {
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav .navbar-collapse {
|
||||||
|
border-top: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav .nav > li > a {
|
||||||
|
margin: 4px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav .navbar-nav .open .dropdown-menu {
|
||||||
|
position: static;
|
||||||
|
float: none;
|
||||||
|
width: auto;
|
||||||
|
margin: 0 10px 10px;
|
||||||
|
padding: 6px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--floating-header-mobile-submenu-bg);
|
||||||
|
box-shadow: 0 10px 24px var(--floating-header-mobile-submenu-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav .navbar-nav .open .dropdown-menu > li > a {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<template>
|
||||||
|
<a class="navbar-brand" :href="href">
|
||||||
|
<img class="app-logo" :src="src" :alt="alt" :width="width" :height="height" />
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'Logo',
|
||||||
|
props: {
|
||||||
|
href: { type: String, default: '/' },
|
||||||
|
src: { type: String, default: '/images/logo-kopkb.png' },
|
||||||
|
alt: { type: String, default: 'MyKoPKB Logo' },
|
||||||
|
width: { type: [Number, String], default: 150 },
|
||||||
|
height: { type: [Number, String], default: 44 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.app-logo {
|
||||||
|
display: block;
|
||||||
|
width: 100px;
|
||||||
|
height: 55px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
<template>
|
||||||
|
<div class="pie-chart" :style="wrapperStyle">
|
||||||
|
<canvas ref="canvas"></canvas>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Chart from 'chart.js/auto';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PieChart',
|
||||||
|
|
||||||
|
props: {
|
||||||
|
labels: { type: Array, default: () => [] },
|
||||||
|
values: { type: Array, default: () => [] },
|
||||||
|
datasetLabel: { type: String, default: '' },
|
||||||
|
height: { type: [Number, String], default: 240 },
|
||||||
|
colors: { type: Array, default: () => [] },
|
||||||
|
/** When true, renders a doughnut (ring) chart; `cutout` is applied. */
|
||||||
|
doughnut: { type: Boolean, default: false },
|
||||||
|
cutout: { type: [String, Number], default: '58%' },
|
||||||
|
options: { type: Object, default: () => ({}) }
|
||||||
|
},
|
||||||
|
|
||||||
|
data: () => ({
|
||||||
|
chart: null
|
||||||
|
}),
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
wrapperStyle: function () {
|
||||||
|
var h = this.height;
|
||||||
|
var px = (typeof h === 'number') ? (h + 'px') : String(h);
|
||||||
|
return { height: px };
|
||||||
|
},
|
||||||
|
|
||||||
|
resolvedColors: function () {
|
||||||
|
var base = this.colors || [];
|
||||||
|
// Fallback palette (Chart.js-ish defaults) if caller doesn't provide colors.
|
||||||
|
var palette = [
|
||||||
|
'#4dc9f6', '#f67019', '#f53794', '#537bc4', '#acc236',
|
||||||
|
'#166a8f', '#00a950', '#58595b', '#8549ba',
|
||||||
|
];
|
||||||
|
var out = [];
|
||||||
|
var n = Math.max((this.labels || []).length, (this.values || []).length);
|
||||||
|
for (var i = 0; i < n; i++) {
|
||||||
|
out.push(base[i] || palette[i % palette.length]);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
labels: function () { this.renderChart(); },
|
||||||
|
values: function () { this.renderChart(); },
|
||||||
|
colors: function () { this.renderChart(); },
|
||||||
|
doughnut: function () { this.renderChart(); },
|
||||||
|
cutout: function () { this.renderChart(); },
|
||||||
|
options: {
|
||||||
|
handler: function () { this.renderChart(); },
|
||||||
|
deep: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted: function () {
|
||||||
|
this.renderChart();
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeDestroy: function () {
|
||||||
|
this.destroyChart();
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
destroyChart: function () {
|
||||||
|
if (!this.chart) return;
|
||||||
|
try { this.chart.destroy(); } catch (e) { }
|
||||||
|
this.chart = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
renderChart: function () {
|
||||||
|
this.destroyChart();
|
||||||
|
if (!this.$refs || !this.$refs.canvas) return;
|
||||||
|
|
||||||
|
var ctx = this.$refs.canvas.getContext('2d');
|
||||||
|
var labels = (this.labels || []).map(function (x) { return String(x); });
|
||||||
|
var values = (this.values || []).map(function (v) { return Number(v) || 0; });
|
||||||
|
|
||||||
|
var baseOptions = {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: { position: 'bottom' },
|
||||||
|
tooltip: { enabled: true }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (this.doughnut) {
|
||||||
|
baseOptions.cutout = this.cutout;
|
||||||
|
}
|
||||||
|
|
||||||
|
var mergedOptions = Object.assign({}, baseOptions, this.options || {});
|
||||||
|
|
||||||
|
this.chart = new Chart(ctx, {
|
||||||
|
type: this.doughnut ? 'doughnut' : 'pie',
|
||||||
|
data: {
|
||||||
|
labels: labels,
|
||||||
|
datasets: [{
|
||||||
|
label: this.datasetLabel,
|
||||||
|
data: values,
|
||||||
|
backgroundColor: this.resolvedColors,
|
||||||
|
borderColor: '#ffffff',
|
||||||
|
borderWidth: 1
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: mergedOptions
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.pie-chart {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -72,15 +72,15 @@
|
|||||||
|
|
||||||
<div class="modal fade app-modal roulette-winner-modal" id="roulette-winner-modal" tabindex="-1" role="dialog"
|
<div class="modal fade app-modal roulette-winner-modal" id="roulette-winner-modal" tabindex="-1" role="dialog"
|
||||||
aria-labelledby="roulette-winner-title" aria-hidden="true">
|
aria-labelledby="roulette-winner-title" aria-hidden="true">
|
||||||
<div class="modal-dialog" role="document">
|
<div class="modal-dialog modal-lg roulette-winner-modal__dialog" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<button type="button" class="close" data-dismiss="modal" aria-label="Tutup">
|
<button type="button" class="close" data-dismiss="modal" aria-label="Tutup">
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
<h4 class="modal-title" id="roulette-winner-title">
|
<h3 class="modal-title roulette-winner-modal__title" id="roulette-winner-title">
|
||||||
Tahniah
|
Tahniah
|
||||||
</h4>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body text-center" v-if="modalWinnerDisplay">
|
<div class="modal-body text-center" v-if="modalWinnerDisplay">
|
||||||
<p class="roulette-winner-modal-round text-muted">
|
<p class="roulette-winner-modal-round text-muted">
|
||||||
@@ -1273,21 +1273,84 @@ export default {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.roulette-winner-modal .modal-title {
|
/* Hall / projection: wide dialog, very large type */
|
||||||
|
.roulette-winner-modal__dialog {
|
||||||
|
width: 96%;
|
||||||
|
max-width: min(96vw, 1320px);
|
||||||
|
margin: 12px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.roulette-winner-modal__dialog {
|
||||||
|
width: 94%;
|
||||||
|
max-width: min(94vw, 1400px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
.roulette-winner-modal__dialog {
|
||||||
|
max-width: 1440px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.roulette-winner-modal .modal-content {
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
border-width: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.roulette-winner-modal .modal-header {
|
||||||
|
padding: clamp(20px, 3vw, 36px) clamp(18px, 3vw, 40px);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.roulette-winner-modal .modal-header .close {
|
||||||
|
font-size: clamp(2rem, 4vw, 3.25rem);
|
||||||
|
font-weight: 300;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin-top: -4px;
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.roulette-winner-modal .modal-body {
|
||||||
|
padding: clamp(32px, 5vw, 72px) clamp(20px, 4vw, 48px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.roulette-winner-modal .modal-footer {
|
||||||
|
padding: clamp(16px, 2.5vw, 28px) clamp(18px, 3vw, 40px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.roulette-winner-modal .modal-footer .btn {
|
||||||
|
font-size: clamp(1.15rem, 2.2vw, 1.65rem);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
padding: clamp(10px, 1.5vw, 16px) clamp(18px, 2.5vw, 28px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.roulette-winner-modal__title {
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: clamp(2.25rem, 5.5vw, 5rem);
|
||||||
|
line-height: 1.15;
|
||||||
|
margin: 0;
|
||||||
|
padding-right: 2.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.roulette-winner-modal-round {
|
.roulette-winner-modal-round {
|
||||||
font-size: 14px;
|
font-size: clamp(1.5rem, 3.5vw, 3.25rem);
|
||||||
margin-bottom: 8px;
|
margin-bottom: clamp(16px, 2.5vw, 28px);
|
||||||
|
font-weight: 700;
|
||||||
|
/* Stronger than .text-muted for projection / rear seats */
|
||||||
|
color: #4b5563 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.roulette-winner-modal-name {
|
.roulette-winner-modal-name {
|
||||||
font-size: 1.75rem;
|
font-size: clamp(3rem, 12vw, 8.5rem);
|
||||||
font-weight: 700;
|
font-weight: 800;
|
||||||
line-height: 1.3;
|
line-height: 1.12;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
text-shadow: 0 2px 0 rgba(0, 0, 0, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
.roulette-winner-absent-hint {
|
.roulette-winner-absent-hint {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="roulette-demo-page container-fluid">
|
<div class="roulette-demo-page container-fluid">
|
||||||
<header class="roulette-page-header">
|
<header class="roulette-page-header">
|
||||||
<div class="roulette-page-header-inner">
|
<div class="roulette-page-header-inner">
|
||||||
<img src="/images/MyKoPKB-logo.png" alt="MyKoPKB" class="roulette-page-header-logo" />
|
<Logo :href="'/admin/'" />
|
||||||
<h1 class="roulette-demo-title">Sesi Cabutan Bertuah — Kehadiran Fizikal</h1>
|
<h1 class="roulette-demo-title">Sesi Cabutan Bertuah — Kehadiran Fizikal</h1>
|
||||||
<button type="button" class="btn btn-default btn-sm roulette-bg-music-btn"
|
<button type="button" class="btn btn-default btn-sm roulette-bg-music-btn"
|
||||||
:aria-pressed="bgMusicPlaying ? 'true' : 'false'"
|
:aria-pressed="bgMusicPlaying ? 'true' : 'false'"
|
||||||
@@ -81,6 +81,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import RouletteEliminationWheel from './RouletteEliminationWheel.vue';
|
import RouletteEliminationWheel from './RouletteEliminationWheel.vue';
|
||||||
|
import Logo from '../common/Logo.vue';
|
||||||
|
|
||||||
function buildLabelsFromVoters(voters) {
|
function buildLabelsFromVoters(voters) {
|
||||||
if (!voters || !voters.length) {
|
if (!voters || !voters.length) {
|
||||||
@@ -134,6 +135,7 @@ export default {
|
|||||||
|
|
||||||
components: {
|
components: {
|
||||||
RouletteEliminationWheel,
|
RouletteEliminationWheel,
|
||||||
|
Logo,
|
||||||
},
|
},
|
||||||
|
|
||||||
data: function () {
|
data: function () {
|
||||||
|
|||||||
@@ -1,40 +1,43 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<h4>Kemaskini Akaun</h4>
|
<h4>Kemaskini Akaun</h4>
|
||||||
<form @submit.prevent="edit()" id="edit_form">
|
<form @submit.prevent="edit()" id="edit_form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="name">Nama</label>
|
<label for="name">Nama</label>
|
||||||
<input type="text" name="name" class="form-control" :value="data.user.name" required/>
|
<input type="text" name="name" class="form-control" :value="data.user.name" required />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="email">E-mail</label>
|
<label for="email">Emel</label>
|
||||||
<input type="email" name="email" class="form-control" :value="data.user.email" required/>
|
<input type="email" name="email" class="form-control" :value="data.user.email" required
|
||||||
</div>
|
pattern="[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$" title="Emel tidak sah" />
|
||||||
|
<small class="text-muted">Contoh: admin@example.com</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input type="submit" value="Submit" class="btn btn-info"/>
|
<input type="submit" value="Hantar" class="btn btn-info" />
|
||||||
<router-link :to="{name: 'Update Password'}" class="btn btn-default">Tukar Kata Laluan</router-link>
|
<router-link :to="{ name: 'Update Password' }" class="btn btn-default">Tukar Kata
|
||||||
</div>
|
Laluan</router-link>
|
||||||
</form>
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default{
|
export default {
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
loading: false
|
loading: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods:{
|
methods: {
|
||||||
refreshUser: function () {
|
refreshUser: function () {
|
||||||
var vm = this;
|
var vm = this;
|
||||||
axios.get(config.API+'admin/'+this.data.user.id)
|
axios.get(config.API + 'admin/' + this.data.user.id)
|
||||||
.then(response=>{
|
.then(response => {
|
||||||
vm.data.user = response.data;
|
vm.data.user = response.data;
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -44,16 +47,16 @@ export default{
|
|||||||
this.loading = true;
|
this.loading = true;
|
||||||
var vm = this;
|
var vm = this;
|
||||||
this.util.notify('Updating account', 'loading');
|
this.util.notify('Updating account', 'loading');
|
||||||
axios.put(config.API+'admin/'+this.data.user.id, $('#edit_form').serialize())
|
axios.put(config.API + 'admin/' + this.data.user.id, $('#edit_form').serialize())
|
||||||
.then(response=>{
|
.then(response => {
|
||||||
$.notifyClose();
|
$.notifyClose();
|
||||||
vm.loading = false;
|
vm.loading = false;
|
||||||
if (vm.util.showResult(response, 'success')) {
|
if (vm.util.showResult(response, 'success')) {
|
||||||
vm.refreshUser();
|
vm.refreshUser();
|
||||||
vm.$router.push({name:'Admin Home'});
|
vm.$router.push({ name: 'Admin Home' });
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error=>{
|
.catch(error => {
|
||||||
$.notifyClose();
|
$.notifyClose();
|
||||||
vm.loading = false;
|
vm.loading = false;
|
||||||
vm.util.showResult(error.response, 'error');
|
vm.util.showResult(error.response, 'error');
|
||||||
|
|||||||
@@ -17,32 +17,66 @@
|
|||||||
|
|
||||||
<div v-if="error" class="alert alert-danger">{{ error }}</div>
|
<div v-if="error" class="alert alert-danger">{{ error }}</div>
|
||||||
|
|
||||||
<admin-data-table :headers="headers" :items="items" :loading="loading" :show-pagination="false"
|
<div v-if="!error" class="allowance-section">
|
||||||
index-title="Bil." empty-text="Tiada rekod bayaran elaun" :exportable="true">
|
<h5 class="allowance-section__title">Anggaran elaun yang perlu dibayar</h5>
|
||||||
<template v-slot:item-admin_name="{ item }">
|
<div v-if="!loading" class="row allowance-stat-row">
|
||||||
<div style="font-weight: 800;">{{ item.admin_name || '—' }}</div>
|
<div class="col-sm-4">
|
||||||
<div class="text-muted" style="font-size: 12px;">{{ item.admin_email || '' }}</div>
|
<div class="allowance-stat-card">
|
||||||
</template>
|
<div class="allowance-stat-card__label">Fizikal (tunai RM300.00)</div>
|
||||||
|
<div class="allowance-stat-card__value">RM {{ formatCentsToRM(outstandingFizikalCents) }}
|
||||||
|
</div>
|
||||||
|
<div class="allowance-stat-card__hint">
|
||||||
|
<b>{{ outstandingCashCount }}</b> belum dibayar
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-4">
|
||||||
|
<div class="allowance-stat-card">
|
||||||
|
<div class="allowance-stat-card__label">Maya (RM150.00)</div>
|
||||||
|
<div class="allowance-stat-card__value">RM {{ formatCentsToRM(outstandingMayaCents) }}</div>
|
||||||
|
<div class="allowance-stat-card__hint">
|
||||||
|
<b>{{ outstandingBankCount }}</b> belum dibayar
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-4">
|
||||||
|
<div class="allowance-stat-card allowance-stat-card--primary">
|
||||||
|
<div class="allowance-stat-card__label">Jumlah baki</div>
|
||||||
|
<div class="allowance-stat-card__value">RM {{ formatCentsToRM(outstandingCents) }}</div>
|
||||||
|
<div class="allowance-stat-card__hint">Fizikal + Maya</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<template v-slot:item-total_amount_cents="{ item }">
|
<div v-if="!error" class="allowance-section allowance-section--table">
|
||||||
<b>{{ formatCentsToRM(item.total_amount_cents) }}</b>
|
<h5 class="allowance-section__title">Bayaran mengikut pentadbir</h5>
|
||||||
<div class="text-muted" style="font-size: 12px;">{{ item.payouts_count }} bayaran</div>
|
<admin-data-table :headers="headers" :items="items" :loading="loading" :show-pagination="false"
|
||||||
</template>
|
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-cash_amount_cents="{ item }">
|
<template v-slot:item-total_amount_cents="{ item }">
|
||||||
{{ formatCentsToRM(item.cash_amount_cents) }}
|
<b>{{ formatCentsToRM(item.total_amount_cents) }}</b>
|
||||||
<div class="text-muted" style="font-size: 12px;">{{ item.cash_count }} tunai</div>
|
<div class="text-muted" style="font-size: 12px;">{{ item.payouts_count }} bayaran</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-slot:item-bank_amount_cents="{ item }">
|
<template v-slot:item-cash_amount_cents="{ item }">
|
||||||
{{ formatCentsToRM(item.bank_amount_cents) }}
|
{{ formatCentsToRM(item.cash_amount_cents) }}
|
||||||
<div class="text-muted" style="font-size: 12px;">{{ item.bank_count }}</div>
|
<div class="text-muted" style="font-size: 12px;">{{ item.cash_count }} tunai</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
</admin-data-table>
|
<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;">
|
<div v-if="!loading && items.length" class="well well-sm allowance-section__footer-total">
|
||||||
<b>Jumlah keseluruhan:</b> {{ formatCentsToRM(grandTotalCents) }}
|
<b>Jumlah keseluruhan:</b> {{ formatCentsToRM(grandTotalCents) }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -55,6 +89,11 @@ export default {
|
|||||||
loading: false,
|
loading: false,
|
||||||
error: '',
|
error: '',
|
||||||
rows: [],
|
rows: [],
|
||||||
|
outstandingCents: 0,
|
||||||
|
outstandingCashCount: 0,
|
||||||
|
outstandingBankCount: 0,
|
||||||
|
defaultCashCents: 30000,
|
||||||
|
defaultBankCents: 15000,
|
||||||
methodFilter: '',
|
methodFilter: '',
|
||||||
headers: [
|
headers: [
|
||||||
{
|
{
|
||||||
@@ -107,6 +146,14 @@ export default {
|
|||||||
grandTotalCents: function () {
|
grandTotalCents: function () {
|
||||||
return this.items.reduce((sum, r) => sum + Number(r.total_amount_cents || 0), 0);
|
return this.items.reduce((sum, r) => sum + Number(r.total_amount_cents || 0), 0);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
outstandingFizikalCents: function () {
|
||||||
|
return Number(this.outstandingCashCount || 0) * Number(this.defaultCashCents || 30000);
|
||||||
|
},
|
||||||
|
|
||||||
|
outstandingMayaCents: function () {
|
||||||
|
return Number(this.outstandingBankCount || 0) * Number(this.defaultBankCents || 15000);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
@@ -121,11 +168,14 @@ export default {
|
|||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (!response || !response.data || response.data.status !== 'success') {
|
if (!response || !response.data || response.data.status !== 'success') {
|
||||||
this.error = (response && response.data && response.data.message) ? response.data.message : 'Gagal memuatkan ringkasan.';
|
this.error = (response && response.data && response.data.message) ? response.data.message : 'Gagal memuatkan ringkasan.';
|
||||||
|
this.applySummaryMeta(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.rows = response.data.rows || [];
|
this.rows = response.data.rows || [];
|
||||||
|
this.applySummaryMeta(response.data);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
this.applySummaryMeta(null);
|
||||||
this.error =
|
this.error =
|
||||||
(error && error.response && error.response.data && error.response.data.message)
|
(error && error.response && error.response.data && error.response.data.message)
|
||||||
? error.response.data.message
|
? error.response.data.message
|
||||||
@@ -137,6 +187,23 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
applySummaryMeta: function (payload) {
|
||||||
|
if (!payload) {
|
||||||
|
this.outstandingCents = 0;
|
||||||
|
this.outstandingCashCount = 0;
|
||||||
|
this.outstandingBankCount = 0;
|
||||||
|
this.defaultCashCents = 30000;
|
||||||
|
this.defaultBankCents = 15000;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var o = payload.outstanding_allowance || {};
|
||||||
|
this.outstandingCents = Number(o.cents || 0);
|
||||||
|
this.outstandingCashCount = Number(o.cash_count || 0);
|
||||||
|
this.outstandingBankCount = Number(o.bank_count || 0);
|
||||||
|
if (o.default_cash_cents != null) this.defaultCashCents = Number(o.default_cash_cents);
|
||||||
|
if (o.default_bank_cents != null) this.defaultBankCents = Number(o.default_bank_cents);
|
||||||
|
},
|
||||||
|
|
||||||
formatCentsToRM: function (cents) {
|
formatCentsToRM: function (cents) {
|
||||||
var n = Number(cents || 0) / 100;
|
var n = Number(cents || 0) / 100;
|
||||||
return n.toLocaleString('en-MY', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
return n.toLocaleString('en-MY', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||||
@@ -144,3 +211,71 @@ export default {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.allowance-stat-card {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: #fafafa;
|
||||||
|
min-height: 118px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.allowance-stat-card--primary {
|
||||||
|
border-color: #bce8f1;
|
||||||
|
background: #f4fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.allowance-stat-card__label {
|
||||||
|
font-size: 12px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
color: #777;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.allowance-stat-card__value {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #222;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.allowance-stat-card--primary .allowance-stat-card__value {
|
||||||
|
color: #31708f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.allowance-stat-card__hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #888;
|
||||||
|
margin-top: 8px;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.allowance-stat-card__breakdown {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #555;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.allowance-stat-row {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.allowance-section__title {
|
||||||
|
margin: 0 0 6px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.allowance-section--table {
|
||||||
|
margin-top: 24px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid #e8e8e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.allowance-section__footer-total {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,372 @@
|
|||||||
|
.final-dashboard {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding-bottom: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-dashboard__container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1760px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
padding-left: 20px;
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-split-row {
|
||||||
|
margin-left: -10px;
|
||||||
|
margin-right: -10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-split-col {
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
.final-split-col {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-dashboard__shell,
|
||||||
|
.final-attendance-shell {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-split-col--dashboard .final-stats {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-split-col--attendance .final-attendance__charts--standalone {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-split-col--dashboard .final-chart-wrap {
|
||||||
|
height: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-split-col--dashboard .final-chart-wrap--doughnut {
|
||||||
|
height: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-split-col--attendance .final-attendance__chart-wrap--donut {
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-dashboard__shell {
|
||||||
|
padding: 22px 22px 26px;
|
||||||
|
border: 1px solid #e6edf5;
|
||||||
|
border-radius: 18px;
|
||||||
|
background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
|
||||||
|
box-shadow: 0 14px 36px rgba(35, 64, 97, 0.08);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-attendance-shell {
|
||||||
|
padding: 22px 22px 26px;
|
||||||
|
border: 1px solid #dce8e4;
|
||||||
|
border-radius: 18px;
|
||||||
|
background: linear-gradient(180deg, #ffffff 0%, #f4faf7 100%);
|
||||||
|
box-shadow: 0 14px 36px rgba(35, 64, 97, 0.08);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-attendance-shell__head {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-attendance-shell__kicker {
|
||||||
|
color: #00897b;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-attendance-shell__title {
|
||||||
|
margin: 0 0 6px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #17324d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-vote-summary {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-section-title {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 900;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #17324d;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-kicker {
|
||||||
|
color: #2f80ed;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-title {
|
||||||
|
margin: 0 0 6px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #17324d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-subtitle {
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
margin: 0 0 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-stat__label {
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-stat__value {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 900;
|
||||||
|
color: #17324d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-stat__value--text {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-stat__hint {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6b7280;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-attendance__meta {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-attendance__meta-sep {
|
||||||
|
margin: 0 6px;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-attendance__body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-attendance__charts {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-attendance__charts--standalone {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 20px 24px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-attendance__chart-caption {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
color: #64748b;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-attendance__chart-wrap {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-attendance__chart-wrap--donut {
|
||||||
|
height: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-attendance__chart-wrap--bar {
|
||||||
|
height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-attendance__state {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #374151;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-attendance__state--muted {
|
||||||
|
color: #6b7280;
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-filters {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 10px 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-filters__label {
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-chip {
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
background: #fff;
|
||||||
|
color: #374151;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-chip:hover {
|
||||||
|
background: #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-chip.is-active {
|
||||||
|
background: #1976d2;
|
||||||
|
border-color: #1976d2;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-empty {
|
||||||
|
text-align: center;
|
||||||
|
color: #6b7280;
|
||||||
|
font-style: italic;
|
||||||
|
margin: 24px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-charts-row {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-charts-row--split > .col-xs-12 + .col-xs-12 {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-card {
|
||||||
|
border: 0;
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 12px 28px rgba(35, 64, 97, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-card__heading {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
background: linear-gradient(135deg, #1d4e89, #2f80ed);
|
||||||
|
color: #fff;
|
||||||
|
border: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-card__heading--alt {
|
||||||
|
background: linear-gradient(135deg, #0d47a1, #1976d2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-card__title {
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-card__meta {
|
||||||
|
opacity: 0.95;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-card__body {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-chart-wrap {
|
||||||
|
position: relative;
|
||||||
|
height: 320px;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-chart-wrap--doughnut {
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 991px) {
|
||||||
|
.final-header {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-stats {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-attendance__charts--standalone {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-chart-wrap,
|
||||||
|
.final-chart-wrap--doughnut {
|
||||||
|
height: 260px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 575px) {
|
||||||
|
.final-dashboard__container {
|
||||||
|
padding-left: 12px;
|
||||||
|
padding-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-dashboard__shell,
|
||||||
|
.final-attendance-shell {
|
||||||
|
padding: 16px 14px 20px;
|
||||||
|
border-radius: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,24 +1,130 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container">
|
<div class="final-dashboard">
|
||||||
<div class="row">
|
<div class="container-fluid final-dashboard__container">
|
||||||
<div class="col-md-12">
|
<div class="row final-split-row">
|
||||||
<div v-for="position in positions" :key="position.id">
|
<div class="col-md-6 final-split-col final-split-col--dashboard">
|
||||||
<h5>{{ position.name }}</h5>
|
<div class="final-dashboard__shell">
|
||||||
<admin-data-table
|
<div class="final-header">
|
||||||
:headers="finalTableHeaders"
|
<div>
|
||||||
:items="finalRowsByPosition(position.id)"
|
<div class="final-kicker">Keputusan akhir</div>
|
||||||
:loading="finalLoading"
|
<h4 class="final-title">Dashboard keputusan</h4>
|
||||||
:items-per-page="10"
|
<div class="final-subtitle">Carta interaktif mengikut jawatan</div>
|
||||||
:show-pagination="true"
|
</div>
|
||||||
empty-text="Tiada keputusan"
|
<div class="final-actions">
|
||||||
:exportable="true"
|
<button type="button" class="btn btn-info" :disabled="finalLoading" @click="loadData">
|
||||||
:export-file-name="'keputusan-akhir-' + position.id"
|
<i class="fa" :class="finalLoading ? 'fa-refresh fa-spin' : 'fa-refresh'"></i>
|
||||||
>
|
Kemaskini
|
||||||
<template v-slot:item-percentage="{ item }">
|
</button>
|
||||||
{{ item.percentage }}
|
</div>
|
||||||
</template>
|
</div>
|
||||||
</admin-data-table>
|
|
||||||
<hr />
|
<div class="final-vote-summary">
|
||||||
|
<div class="final-section-title">Ringkasan undian</div>
|
||||||
|
<div class="final-stats">
|
||||||
|
<div class="final-stat">
|
||||||
|
<div class="final-stat__label">Jumlah undi</div>
|
||||||
|
<div class="final-stat__value">{{ dashboardTotalVotes }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="final-stat">
|
||||||
|
<div class="final-stat__label">Bil. jawatan</div>
|
||||||
|
<div class="final-stat__value">{{ positions.length }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="final-stat">
|
||||||
|
<div class="final-stat__label">Calon terunggul</div>
|
||||||
|
<div class="final-stat__value final-stat__value--text">{{ topOverallLabel }}</div>
|
||||||
|
<div class="final-stat__hint">{{ topOverallVotes }} undi</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="final-filters">
|
||||||
|
<span class="final-filters__label">Jawatan:</span>
|
||||||
|
<button type="button" class="final-chip" :class="{ 'is-active': selectedPositionId === 0 }" @click="setPosition(0)">
|
||||||
|
Semua
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-for="position in positions"
|
||||||
|
:key="position.id"
|
||||||
|
type="button"
|
||||||
|
class="final-chip"
|
||||||
|
:class="{ 'is-active': selectedPositionId === position.id }"
|
||||||
|
@click="setPosition(position.id)"
|
||||||
|
>
|
||||||
|
{{ position.name }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!finalLoading && positions.length === 0" class="final-empty">
|
||||||
|
Tiada jawatan untuk pilihan raya ini.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="row final-charts-row final-charts-row--split">
|
||||||
|
<div class="col-xs-12">
|
||||||
|
<div class="panel panel-default final-card">
|
||||||
|
<div class="panel-heading final-card__heading">
|
||||||
|
<div class="final-card__title">{{ mainChartTitle }}</div>
|
||||||
|
<div class="final-card__meta">{{ mainChartSubtitle }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body final-card__body">
|
||||||
|
<div class="final-chart-wrap">
|
||||||
|
<bar-chart v-if="mainChartReady" v-bind="mainBarChartProps" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-12">
|
||||||
|
<div class="panel panel-default final-card">
|
||||||
|
<div class="panel-heading final-card__heading final-card__heading--alt">
|
||||||
|
<div class="final-card__title">{{ sideChartTitle }}</div>
|
||||||
|
<div class="final-card__meta">{{ sideChartSubtitle }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body final-card__body">
|
||||||
|
<div class="final-chart-wrap final-chart-wrap--doughnut">
|
||||||
|
<pie-chart v-if="mainChartReady" v-bind="sidePieChartProps" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 final-split-col final-split-col--attendance">
|
||||||
|
<div class="final-attendance-shell">
|
||||||
|
<div class="final-attendance-shell__head">
|
||||||
|
<div>
|
||||||
|
<div class="final-attendance-shell__kicker">Kehadiran</div>
|
||||||
|
<h4 class="final-attendance-shell__title">Ringkasan kehadiran</h4>
|
||||||
|
<div v-if="attendanceSummary" class="final-attendance__meta">
|
||||||
|
Kadar daftar: <b>{{ attendanceSummary.attendance_rate }}%</b>
|
||||||
|
<span class="final-attendance__meta-sep">·</span>
|
||||||
|
{{ attendanceSummary.marked_present }} / {{ attendanceSummary.total_registered }} pengundi
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="!attendanceSummaryReady" class="final-attendance__state">Memuatkan kehadiran…</div>
|
||||||
|
<div v-else-if="attendanceSummary" class="final-attendance__body">
|
||||||
|
<div
|
||||||
|
v-if="attendanceSummary.total_registered > 0"
|
||||||
|
class="final-attendance__charts final-attendance__charts--standalone"
|
||||||
|
>
|
||||||
|
<div class="final-attendance__chart-block">
|
||||||
|
<div class="final-attendance__chart-caption">Taburan kaedah kehadiran</div>
|
||||||
|
<div class="final-attendance__chart-wrap final-attendance__chart-wrap--donut">
|
||||||
|
<pie-chart v-bind="attendanceBreakdownPieProps" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="showAttendanceFizikalChart" class="final-attendance__chart-block">
|
||||||
|
<div class="final-attendance__chart-caption">Status pendaftaran fizikal</div>
|
||||||
|
<div class="final-attendance__chart-wrap final-attendance__chart-wrap--bar">
|
||||||
|
<bar-chart v-bind="attendanceFizikalBarProps" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="final-attendance__state final-attendance__state--muted">
|
||||||
|
Ringkasan kehadiran tidak dapat dimuatkan. Semak sambungan atau log masuk pentadbir.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -26,125 +132,25 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import BarChart from '../../../common/BarChart.vue';
|
||||||
|
import PieChart from '../../../common/PieChart.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
BarChart,
|
||||||
|
PieChart
|
||||||
|
},
|
||||||
|
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
nominees: [],
|
nominees: [],
|
||||||
results: [],
|
results: [],
|
||||||
partylists: [],
|
|
||||||
positions: [],
|
positions: [],
|
||||||
finalLoading: false,
|
finalLoading: false,
|
||||||
finalTableHeaders: [
|
selectedPositionId: 0,
|
||||||
{ title: 'No. Anggota', key: 'no_anggota', sortable: true },
|
attendanceSummary: null,
|
||||||
{ title: 'Nama', key: 'name', sortable: true },
|
attendanceSummaryReady: false
|
||||||
{ 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)
|
|
||||||
if (id == nominees[i]['id'])
|
|
||||||
return nominees[i];
|
|
||||||
return {};
|
|
||||||
},
|
|
||||||
|
|
||||||
getPartylist: function (id) {
|
|
||||||
let partylists = this.partylists;
|
|
||||||
for (var i in partylists)
|
|
||||||
if (id == partylists[i]['id'])
|
|
||||||
return partylists[i]['name'];
|
|
||||||
return 'No Partylist';
|
|
||||||
},
|
|
||||||
|
|
||||||
calculatePercentage(votes, positionId) {
|
|
||||||
const totalVotes = this.results
|
|
||||||
.filter(result => result.position_id === positionId)
|
|
||||||
.reduce((total, result) => total + result.votes, 0);
|
|
||||||
|
|
||||||
return totalVotes === 0 ? '0.00' : ((votes / totalVotes) * 100).toFixed(2);
|
|
||||||
},
|
|
||||||
|
|
||||||
calculateTotalVotes(positionId) {
|
|
||||||
return this.results
|
|
||||||
.filter(result => result.position_id === positionId)
|
|
||||||
.reduce((total, result) => total + result.votes, 0);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
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();
|
|
||||||
vm.nominees = response.data.nominee;
|
|
||||||
vm.results = response.data.result;
|
|
||||||
vm.partylists = response.data.partylist;
|
|
||||||
vm.positions = response.data.position;
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
$.notifyClose();
|
|
||||||
vm.util.showResult(error, 'error');
|
|
||||||
})
|
|
||||||
.finally(function () {
|
|
||||||
vm.finalLoading = false;
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
@@ -153,21 +159,276 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
no_votes: function () {
|
no_votes: function () {
|
||||||
let nominees = this.nominees;
|
var nominees = this.nominees;
|
||||||
let results = this.results;
|
var results = this.results;
|
||||||
let no_votes = [];
|
var no_votes = [];
|
||||||
for (var i in nominees) {
|
for (var i in nominees) {
|
||||||
var hasvote = false;
|
var hasvote = false;
|
||||||
for (var y in results) {
|
for (var y in results) {
|
||||||
if (results[y]['nominee_id'] == nominees[i]['id'])
|
if (results[y]['nominee_id'] == nominees[i]['id']) {
|
||||||
hasvote = true;
|
hasvote = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!hasvote)
|
if (!hasvote) {
|
||||||
no_votes.push(nominees[i]);
|
no_votes.push(nominees[i]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return no_votes;
|
return no_votes;
|
||||||
}
|
},
|
||||||
|
|
||||||
|
dashboardTotalVotes: function () {
|
||||||
|
return this.results.reduce(function (s, r) {
|
||||||
|
return s + Number(r.votes || 0);
|
||||||
|
}, 0);
|
||||||
|
},
|
||||||
|
|
||||||
|
topOverallLabel: function () {
|
||||||
|
if (!this.results.length) {
|
||||||
|
return '—';
|
||||||
|
}
|
||||||
|
var best = this.results.reduce(function (a, b) {
|
||||||
|
return Number(b.votes || 0) > Number(a.votes || 0) ? b : a;
|
||||||
|
});
|
||||||
|
var n = this.getNominee(best.nominee_id);
|
||||||
|
return (n && n.name) ? n.name : '—';
|
||||||
|
},
|
||||||
|
|
||||||
|
topOverallVotes: function () {
|
||||||
|
if (!this.results.length) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return this.results.reduce(function (m, r) {
|
||||||
|
return Math.max(m, Number(r.votes || 0));
|
||||||
|
}, 0);
|
||||||
|
},
|
||||||
|
|
||||||
|
mainChartTitle: function () {
|
||||||
|
if (this.selectedPositionId === 0) {
|
||||||
|
return 'Jumlah undi mengikut jawatan';
|
||||||
|
}
|
||||||
|
var p = this.positions.find(function (x) {
|
||||||
|
return Number(x.id) === Number(this.selectedPositionId);
|
||||||
|
}, this);
|
||||||
|
return (p && p.name) ? 'Undi calon — ' + p.name : 'Undi calon';
|
||||||
|
},
|
||||||
|
|
||||||
|
mainChartSubtitle: function () {
|
||||||
|
return this.selectedPositionId === 0
|
||||||
|
? 'Perbandingan antara jawatan'
|
||||||
|
: 'Carta mengikut calon';
|
||||||
|
},
|
||||||
|
|
||||||
|
sideChartTitle: function () {
|
||||||
|
return this.selectedPositionId === 0 ? 'Taburan undi (jawatan)' : 'Peratus undi';
|
||||||
|
},
|
||||||
|
|
||||||
|
sideChartSubtitle: function () {
|
||||||
|
return this.selectedPositionId === 0 ? 'Perkongsian jumlah undi' : 'Bahagian setiap calon';
|
||||||
|
},
|
||||||
|
|
||||||
|
showAttendanceFizikalChart: function () {
|
||||||
|
if (!this.attendanceSummary) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return Number(this.attendanceSummary.fizikal || 0) > 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
mainChartReady: function () {
|
||||||
|
return !this.finalLoading && this.positions.length > 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
positionTotalRows: function () {
|
||||||
|
var vm = this;
|
||||||
|
return this.positions.map(function (pos) {
|
||||||
|
var t = vm.results
|
||||||
|
.filter(function (r) {
|
||||||
|
return Number(r.position_id) === Number(pos.id);
|
||||||
|
})
|
||||||
|
.reduce(function (s, r) {
|
||||||
|
return s + Number(r.votes || 0);
|
||||||
|
}, 0);
|
||||||
|
return { id: pos.id, name: pos.name, total: t };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
mainBarChartProps: function () {
|
||||||
|
if (!this.mainChartReady) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (this.selectedPositionId === 0) {
|
||||||
|
var rows = this.positionTotalRows;
|
||||||
|
return {
|
||||||
|
labels: rows.map(function (r) {
|
||||||
|
return r.name.length > 22 ? r.name.slice(0, 20) + '…' : r.name;
|
||||||
|
}),
|
||||||
|
values: rows.map(function (r) {
|
||||||
|
return r.total;
|
||||||
|
}),
|
||||||
|
datasetLabel: 'Jumlah undi'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
var rows = this.getSinglePositionChartRows();
|
||||||
|
return {
|
||||||
|
labels: rows.map(function (r) {
|
||||||
|
return r.label;
|
||||||
|
}),
|
||||||
|
values: rows.map(function (r) {
|
||||||
|
return r.votes;
|
||||||
|
}),
|
||||||
|
datasetLabel: 'Undi'
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
sidePieChartProps: function () {
|
||||||
|
if (!this.mainChartReady) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (this.selectedPositionId === 0) {
|
||||||
|
var rows = this.positionTotalRows;
|
||||||
|
return {
|
||||||
|
labels: rows.map(function (r) {
|
||||||
|
return r.name;
|
||||||
|
}),
|
||||||
|
values: rows.map(function (r) {
|
||||||
|
return r.total;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
var rows = this.getSinglePositionChartRows();
|
||||||
|
return {
|
||||||
|
labels: rows.map(function (r) {
|
||||||
|
return r.label;
|
||||||
|
}),
|
||||||
|
values: rows.map(function (r) {
|
||||||
|
return r.votes;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
attendanceBreakdownPieProps: function () {
|
||||||
|
if (!this.attendanceSummary || this.attendanceSummary.total_registered <= 0) {
|
||||||
|
return { labels: [], values: [] };
|
||||||
|
}
|
||||||
|
var s = this.attendanceSummary;
|
||||||
|
return {
|
||||||
|
labels: ['Fizikal', 'Maya', 'Belum ditetapkan'],
|
||||||
|
values: [
|
||||||
|
Number(s.fizikal || 0),
|
||||||
|
Number(s.maya || 0),
|
||||||
|
Number(s.unset || 0)
|
||||||
|
]
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
attendanceFizikalBarProps: function () {
|
||||||
|
if (!this.showAttendanceFizikalChart || !this.attendanceSummary) {
|
||||||
|
return { labels: [], values: [] };
|
||||||
|
}
|
||||||
|
var s = this.attendanceSummary;
|
||||||
|
return {
|
||||||
|
labels: ['Menunggu pengesahan', 'Disahkan'],
|
||||||
|
values: [
|
||||||
|
Number(s.fizikal_registration_pending || 0),
|
||||||
|
Number(s.fizikal_registration_verified || 0)
|
||||||
|
],
|
||||||
|
datasetLabel: 'Pengundi'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
setPosition: function (id) {
|
||||||
|
this.selectedPositionId = id === 0 ? 0 : Number(id);
|
||||||
|
},
|
||||||
|
|
||||||
|
getSinglePositionChartRows: function () {
|
||||||
|
var pid = this.selectedPositionId;
|
||||||
|
var vm = this;
|
||||||
|
var rows = this.results
|
||||||
|
.filter(function (r) {
|
||||||
|
return Number(r.position_id) === Number(pid);
|
||||||
|
})
|
||||||
|
.map(function (r) {
|
||||||
|
var n = vm.getNominee(r.nominee_id) || {};
|
||||||
|
var label = n.name || ('#' + r.nominee_id);
|
||||||
|
if (label.length > 28) {
|
||||||
|
label = label.slice(0, 26) + '…';
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
label: label,
|
||||||
|
votes: Number(r.votes || 0)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
this.no_votes.forEach(function (nv) {
|
||||||
|
if (Number(nv.position_id) === Number(pid)) {
|
||||||
|
var label = nv.name || '';
|
||||||
|
if (label.length > 28) {
|
||||||
|
label = label.slice(0, 26) + '…';
|
||||||
|
}
|
||||||
|
rows.push({
|
||||||
|
label: label || 'Calon',
|
||||||
|
votes: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
rows.sort(function (a, b) {
|
||||||
|
return b.votes - a.votes;
|
||||||
|
});
|
||||||
|
return rows;
|
||||||
|
},
|
||||||
|
|
||||||
|
loadData: function () {
|
||||||
|
var vm = this;
|
||||||
|
this.util.notify('Loading please wait...', 'loading');
|
||||||
|
this.finalLoading = true;
|
||||||
|
this.attendanceSummaryReady = false;
|
||||||
|
this.attendanceSummary = null;
|
||||||
|
|
||||||
|
axios
|
||||||
|
.get(config.API + 'election/' + this.election_id + '/attendance-summary')
|
||||||
|
.then(function (r) {
|
||||||
|
vm.attendanceSummary = r.data;
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
vm.attendanceSummary = null;
|
||||||
|
})
|
||||||
|
.finally(function () {
|
||||||
|
vm.attendanceSummaryReady = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
axios
|
||||||
|
.get(config.API + 'election/result/' + this.election_id)
|
||||||
|
.then(function (response) {
|
||||||
|
$.notifyClose();
|
||||||
|
vm.nominees = response.data.nominee || [];
|
||||||
|
vm.results = response.data.result || [];
|
||||||
|
vm.positions = response.data.position || [];
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
$.notifyClose();
|
||||||
|
vm.util.showResult(error, 'error');
|
||||||
|
})
|
||||||
|
.finally(function () {
|
||||||
|
vm.finalLoading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getNominee: function (id) {
|
||||||
|
var nominees = this.nominees;
|
||||||
|
for (var i in nominees) {
|
||||||
|
if (id == nominees[i]['id']) {
|
||||||
|
return nominees[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
created: function () {
|
||||||
|
this.util.setTitle('MyKoPKB - Keputusan akhir');
|
||||||
|
this.loadData();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped src="./final.css"></style>
|
||||||
|
|||||||
@@ -1,35 +1,76 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<h4>Undian Terkini</h4>
|
<ul class="nav nav-tabs election-tabs">
|
||||||
<div class="form-group">
|
<li :class="{ active: activeTab === 'current' }">
|
||||||
<button class="btn btn-success" v-if="data.election.status == 1 && data.user.role == 1"
|
<a href="#" @click.prevent="activeTab = 'current'">Terkini</a>
|
||||||
@click="util.showModal('#start-election-modal')">Mula Undian</button>
|
</li>
|
||||||
|
<li :class="{ active: activeTab === 'history' }">
|
||||||
|
<a href="#" @click.prevent="activeTab = 'history'">Sejarah Undian</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
<router-link :to="{ name: 'Current Result' }" class="btn btn-info" v-if="data.election.status == 2">
|
<div class="tab-content election-tab-content">
|
||||||
Papar Keputusan
|
<div class="tab-pane" :class="{ active: activeTab === 'current' }">
|
||||||
</router-link>
|
<div class="election-header">
|
||||||
|
<div class="election-header__title">
|
||||||
|
<div class="election-header__kicker">Undian Terkini</div>
|
||||||
|
<div class="election-header__name">{{ electionLatestHeading }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="election-header__meta">
|
||||||
|
<span class="election-status" :class="electionStatusClass">{{ electionStatusLabel }}</span>
|
||||||
|
<label v-if="data.election.status == 2" class="election-label">
|
||||||
|
Undian bermula pada {{ data.election.start }}
|
||||||
|
</label>
|
||||||
|
<div class="election-header__meta-actions">
|
||||||
|
<button class="btn btn-success btn-xs"
|
||||||
|
v-if="Number(data.election.status) === 1 && data.user.role == 1"
|
||||||
|
@click="openStartElectionModal">
|
||||||
|
Mula Undian
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button class="btn btn-danger" v-if="data.election.status == 2"
|
<div class="election-actions form-group">
|
||||||
@click="util.showModal('#stop-election-modal')">Tamat Undian</button>
|
<button class="btn btn-primary" v-if="Number(data.election.status) === 3 && data.user.role == 1"
|
||||||
|
@click="util.showModal('#register-next-session-modal')">Daftar Sesi Mengundi Baru</button>
|
||||||
|
|
||||||
<label v-if="data.election.status == 2">Undian bermula {{ start_date }}</label>
|
<router-link :to="{ name: 'Current Result' }" class="btn btn-info"
|
||||||
|
v-if="data.election.status == 2">
|
||||||
|
Keputusan Semasa
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
<button class="btn btn-danger" v-if="data.election.status == 2"
|
||||||
|
@click="util.showModal('#stop-election-modal')">Tamat Undian</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-info election-hint" v-if="Number(data.user.role) === 1">
|
||||||
|
<b>Makluman:</b> Sila semak & kemaskini <b>Maklumat Calon</b> dan <b>Anggota Koperasi</b>
|
||||||
|
(butiran
|
||||||
|
pengundi) untuk sesi ini sebelum memulakan undian.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-pane" :class="{ active: activeTab === 'history' }">
|
||||||
|
<div class="election-history">
|
||||||
|
<div class="election-history__title">Sejarah Undian</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">
|
||||||
|
Ringkasan Keputusan
|
||||||
|
</router-link>
|
||||||
|
<a class="btn btn-warning" :href="getPDFurl(item.id)">Muat Turun PDF</a>
|
||||||
|
</template>
|
||||||
|
</admin-data-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<admin-data-table :headers="electionTableHeaders" :items="data.elections" :loading="electionLoading"
|
<form @submit.prevent="start">
|
||||||
: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"
|
<div class="modal fade" id="start-election-modal" tabindex="-1" role="dialog"
|
||||||
aria-labelledby="start-election-modal" aria-hidden="true">
|
aria-labelledby="start-election-modal" aria-hidden="true">
|
||||||
<div class="modal-dialog" role="document">
|
<div class="modal-dialog" role="document">
|
||||||
@@ -42,19 +83,60 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="name">Nama Undian</label>
|
<label for="start-election-name">Nama Undian</label>
|
||||||
<input type="text" name="name" autocomplete="on" class="form-control"
|
<input id="start-election-name" v-model.trim="startForm.name" type="text"
|
||||||
placeholder="(Optional)" />
|
name="name" autocomplete="off" class="form-control" placeholder="(Pilihan)"
|
||||||
|
readonly tabindex="-1" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="password">Kata Laluan</label>
|
<label for="start-election-password">Kata Laluan</label>
|
||||||
<input type="password" name="password" class="form-control" required />
|
<input id="start-election-password" v-model="startForm.password" type="password"
|
||||||
|
name="password" class="form-control" required autocomplete="current-password"
|
||||||
|
:disabled="startSubmitting" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<input type="submit" class="btn btn-info" value="Start" />
|
<button type="submit" class="btn btn-info" :disabled="startSubmitting">
|
||||||
<input type="button" data-dismiss="modal" class="btn btn-default" value="Cancel" />
|
{{ startSubmitting ? 'Memproses…' : 'Mula' }}
|
||||||
|
</button>
|
||||||
|
<input type="button" data-dismiss="modal" class="btn btn-default" value="Batal" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form @submit.prevent="registerNextSession" id="register_next_form">
|
||||||
|
<div class="modal fade" id="register-next-session-modal" tabindex="-1" role="dialog"
|
||||||
|
aria-labelledby="register-next-session-modal" aria-hidden="true">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="registerNextModalLabel">Daftar Sesi Mengundi Baru</h5>
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p class="text-muted">Sesi baharu akan dibuka untuk pusingan undian seterusnya. Undian
|
||||||
|
yang
|
||||||
|
telah tamat kekal dalam rekod.</p>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="register-next-name">Nama Undian</label>
|
||||||
|
<input id="register-next-name" type="text" name="name" autocomplete="on"
|
||||||
|
class="form-control" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="register-next-password">Kata Laluan</label>
|
||||||
|
<input id="register-next-password" type="password" name="password"
|
||||||
|
class="form-control" required autocomplete="current-password" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<input type="submit" class="btn btn-primary" value="Daftar" />
|
||||||
|
<input type="button" data-dismiss="modal" class="btn btn-default" value="Batal" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -76,7 +158,7 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="name">Nama Undian</label>
|
<label for="name">Nama Undian</label>
|
||||||
<input type="text" name="name" :value="data.election.name" autocomplete="on"
|
<input type="text" name="name" :value="data.election.name" autocomplete="on"
|
||||||
class="form-control" placeholder="(Optional)" />
|
class="form-control" placeholder="(Optional)" readonly tabindex="-1" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -86,19 +168,129 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<input type="submit" class="btn btn-info" value="Stop Election" />
|
<input type="submit" class="btn btn-info" value="Tamat Undian" />
|
||||||
<input type="button" data-dismiss="modal" class="btn btn-default" value="Cancel" />
|
<input type="button" data-dismiss="modal" class="btn btn-default" value="Batal" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.election-label {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #ffffff;
|
||||||
|
background-color: #0a4c9c;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.election-tabs {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.election-tab-content>.tab-pane {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.election-tab-content>.tab-pane.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.election-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 14px 15px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.election-header__kicker {
|
||||||
|
font-size: 12px;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #777;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.election-header__name {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.election-status {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.election-status--draft {
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #555;
|
||||||
|
border-color: #e6e6e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.election-status--active {
|
||||||
|
background: #e8f5e9;
|
||||||
|
color: #2e7d32;
|
||||||
|
border-color: #c8e6c9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.election-status--done {
|
||||||
|
background: #ffebee;
|
||||||
|
color: #c62828;
|
||||||
|
border-color: #ffcdd2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.election-header__meta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.election-header__meta-actions .btn {
|
||||||
|
padding: 4px 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.election-actions {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.election-hint {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.election-history {
|
||||||
|
margin-top: 4px;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.election-history__title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #666;
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
.center-header {
|
.center-header {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
@@ -116,25 +308,58 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
data: () => ({
|
data: () => ({
|
||||||
start_date: null,
|
activeTab: 'current',
|
||||||
electionLoading: false,
|
electionLoading: false,
|
||||||
|
startSubmitting: false,
|
||||||
|
startForm: {
|
||||||
|
name: '',
|
||||||
|
password: ''
|
||||||
|
},
|
||||||
electionTableHeaders: [
|
electionTableHeaders: [
|
||||||
{ title: 'Nama Undian', key: 'name', sortable: true },
|
{ title: 'Nama Undian', key: 'name', sortable: true },
|
||||||
{ title: 'Undian Mula', key: 'start', sortable: true },
|
{ title: 'Undian Mula', key: 'start', sortable: true },
|
||||||
{ title: 'Undian Tamat', key: 'end', sortable: true },
|
{ title: 'Undian Tamat', key: 'end', sortable: true },
|
||||||
{ title: 'Papar', key: 'actions', sortable: false }
|
{ title: 'Tindakan', key: 'actions', sortable: false }
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
created: function () {
|
created: function () {
|
||||||
this.refreshElection();
|
this.refreshElection();
|
||||||
var vm = this;
|
},
|
||||||
setInterval(() => {
|
|
||||||
vm.start_date = moment(vm.data.election.start).fromNow();
|
computed: {
|
||||||
}, 1000);
|
electionLatestHeading: function () {
|
||||||
|
var e = this.data.election;
|
||||||
|
var n = e && e.name != null ? String(e.name).trim() : '';
|
||||||
|
return n !== '' ? n : 'Undian Terkini';
|
||||||
|
},
|
||||||
|
|
||||||
|
electionStatusLabel: function () {
|
||||||
|
var s = Number(this.data && this.data.election ? this.data.election.status : NaN);
|
||||||
|
if (s === 1) return 'Draf';
|
||||||
|
if (s === 2) return 'Sesi Mengundi Sedang Berjalan';
|
||||||
|
if (s === 3) return 'Tamat';
|
||||||
|
return '—';
|
||||||
|
},
|
||||||
|
|
||||||
|
electionStatusClass: function () {
|
||||||
|
var s = Number(this.data && this.data.election ? this.data.election.status : NaN);
|
||||||
|
if (s === 1) return 'election-status--draft';
|
||||||
|
if (s === 2) return 'election-status--active';
|
||||||
|
if (s === 3) return 'election-status--done';
|
||||||
|
return '';
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
|
openStartElectionModal: function () {
|
||||||
|
var e = this.data.election;
|
||||||
|
this.startForm.name = e && e.name != null ? String(e.name) : '';
|
||||||
|
this.startForm.password = '';
|
||||||
|
this.startSubmitting = false;
|
||||||
|
this.util.showModal('#start-election-modal');
|
||||||
|
},
|
||||||
|
|
||||||
stop: function () {
|
stop: function () {
|
||||||
var vm = this;
|
var vm = this;
|
||||||
// this.util.hideModal('#stop-election-modal');
|
// this.util.hideModal('#stop-election-modal');
|
||||||
@@ -145,11 +370,6 @@ export default {
|
|||||||
if (vm.util.showResult(response, 'success')) {
|
if (vm.util.showResult(response, 'success')) {
|
||||||
vm.refreshElection();
|
vm.refreshElection();
|
||||||
vm.data.election = response.data.election;
|
vm.data.election = response.data.election;
|
||||||
vm.data.positions = [];
|
|
||||||
vm.data.partylists = [];
|
|
||||||
vm.data.voters = [];
|
|
||||||
vm.data.nominees = [];
|
|
||||||
vm.data.results = [];
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
@@ -160,15 +380,50 @@ export default {
|
|||||||
|
|
||||||
start: function () {
|
start: function () {
|
||||||
var vm = this;
|
var vm = this;
|
||||||
// this.util.hideModal('#start-election-modal');
|
if (vm.startSubmitting) {
|
||||||
console.log('this');
|
return;
|
||||||
this.util.notify('Loading please wait', 'loading');
|
}
|
||||||
axios.post(config.API + 'election/start', $('#start_form').serialize())
|
vm.startSubmitting = true;
|
||||||
|
this.util.notify('Memproses…', 'loading');
|
||||||
|
axios.post(config.API + 'election/start', {
|
||||||
|
name: vm.startForm.name,
|
||||||
|
password: vm.startForm.password
|
||||||
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
$.notifyClose();
|
$.notifyClose();
|
||||||
if (vm.util.showResult(response, 'success')) {
|
if (vm.util.showResult(response, 'success')) {
|
||||||
vm.refreshElection();
|
vm.refreshElection();
|
||||||
vm.data.election = response.data.election;
|
vm.data.election = response.data.election;
|
||||||
|
vm.startForm.password = '';
|
||||||
|
vm.util.hideModal('#start-election-modal');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
$.notifyClose();
|
||||||
|
vm.util.showResult(error, 'error');
|
||||||
|
})
|
||||||
|
.finally(function () {
|
||||||
|
vm.startSubmitting = false;
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
registerNextSession: function () {
|
||||||
|
var vm = this;
|
||||||
|
this.util.notify('Memproses…', 'loading');
|
||||||
|
axios.post(config.API + 'election/register-next', $('#register_next_form').serialize())
|
||||||
|
.then(response => {
|
||||||
|
$.notifyClose();
|
||||||
|
if (vm.util.showResult(response, 'success')) {
|
||||||
|
vm.refreshElection();
|
||||||
|
vm.data.election = response.data.election;
|
||||||
|
vm.util.hideModal('#register-next-session-modal');
|
||||||
|
axios.get(config.API + 'admin/information').then(function (info) {
|
||||||
|
vm.data.election = info.data.election;
|
||||||
|
vm.data.partylists = info.data.partylist;
|
||||||
|
vm.data.positions = info.data.position;
|
||||||
|
}).catch(function (err) {
|
||||||
|
vm.util.showResult(err, 'error');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-7">
|
<div class="col-md-12">
|
||||||
<div class="panel panel-default result-card">
|
<div class="panel panel-default result-card">
|
||||||
<div class="panel-heading result-card__heading">
|
<div class="panel-heading result-card__heading">
|
||||||
<div class="result-card__title">
|
<div class="result-card__title">
|
||||||
@@ -99,7 +99,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-5">
|
</div>
|
||||||
|
|
||||||
|
<div class="row" style="margin-top: 12px;">
|
||||||
|
<div class="col-md-12">
|
||||||
<div class="panel panel-default result-card">
|
<div class="panel panel-default result-card">
|
||||||
<div class="panel-heading result-card__heading">
|
<div class="panel-heading result-card__heading">
|
||||||
<div class="result-card__title">
|
<div class="result-card__title">
|
||||||
@@ -114,11 +117,15 @@
|
|||||||
<div class="panel-body result-card__body">
|
<div class="panel-body result-card__body">
|
||||||
<template v-if="String(position_id) === '0'">
|
<template v-if="String(position_id) === '0'">
|
||||||
<div class="result-mini-title">Top Calon (Overall)</div>
|
<div class="result-mini-title">Top Calon (Overall)</div>
|
||||||
<div id="top-chart" class="result-top-chart"></div>
|
<BarChart class="result-top-chart" :labels="topNominees.map(n => n.label)"
|
||||||
|
:values="topNominees.map(n => n.votes)" background-color="rgba(54, 162, 235, 0.75)"
|
||||||
|
border-color="rgba(54, 162, 235, 1)" />
|
||||||
<div v-if="!topNominees.length" class="result-empty">Tiada data untuk dipaparkan.</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 class="result-mini-title" style="margin-top: 14px;">Jumlah Undi Mengikut Jawatan</div>
|
||||||
<div id="votes-by-position-chart" class="result-top-chart"></div>
|
<BarChart class="result-top-chart" :labels="positionTotals.map(p => p.label)"
|
||||||
|
:values="positionTotals.map(p => p.votes)" background-color="rgba(75, 192, 192, 0.55)"
|
||||||
|
border-color="rgba(75, 192, 192, 1)" />
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="result-empty">
|
<div class="result-empty">
|
||||||
@@ -134,21 +141,18 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import BarChart from '../../../common/BarChart.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: { BarChart },
|
||||||
|
|
||||||
created: function () {
|
created: function () {
|
||||||
this.refreshNominees();
|
this.refreshNominees();
|
||||||
this.$nextTick(function () {
|
|
||||||
this.initCharts();
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
position_id: function () {
|
position_id: function () {
|
||||||
var vm = this;
|
// charts are reactive via <BarChart>
|
||||||
this.$nextTick(function () {
|
|
||||||
vm.initCharts();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -188,9 +192,6 @@ export default {
|
|||||||
$.notifyClose();
|
$.notifyClose();
|
||||||
vm.data.results = response.data;
|
vm.data.results = response.data;
|
||||||
vm.data.last_update = new Date();
|
vm.data.last_update = new Date();
|
||||||
vm.$nextTick(function () {
|
|
||||||
vm.initCharts();
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
$.notifyClose();
|
$.notifyClose();
|
||||||
@@ -198,47 +199,6 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
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) {
|
getPosition: function (id) {
|
||||||
let positions = this.data.positions;
|
let positions = this.data.positions;
|
||||||
for (var i in positions)
|
for (var i in positions)
|
||||||
|
|||||||
@@ -1,121 +1,104 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div v-if="!loading">
|
<div v-if="!loading">
|
||||||
<nav class="navbar navbar-default navbar-fixed-top floating-header logo-header" id="nav">
|
<FloatingNavbar logo-href="/admin" main-nav-class="admin-nav-main">
|
||||||
<div class="container">
|
<template #left>
|
||||||
<div class="navbar-header logo-section">
|
<router-link v-if="adminRole !== 3" :to="{ name: 'Admin Home' }" tag="li" exact>
|
||||||
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#myNavbar">
|
<a href="#"><b>Undian</b></a>
|
||||||
<span class="icon-bar"></span>
|
</router-link>
|
||||||
<span class="icon-bar"></span>
|
|
||||||
<span class="icon-bar"></span>
|
|
||||||
</button>
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
<div class="collapse navbar-collapse" id="myNavbar">
|
|
||||||
|
|
||||||
<ul class="nav navbar-nav admin-nav-main">
|
<li class="dropdown" v-if="adminRole === 1">
|
||||||
<router-link v-if="adminRole !== 3" :to="{ name: 'Admin Home' }" tag="li" exact>
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true"
|
||||||
<a href="#"><b>Laman Utama</b></a>
|
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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #right>
|
||||||
|
<li class="dropdown">
|
||||||
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true"
|
||||||
|
aria-expanded="false">
|
||||||
|
<span class="fa fa-user"></span> {{ data.user.name }} <span class="caret"></span>
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<router-link v-if="Number(data.user.role) !== 3" :to="{ name: 'Update Account' }" tag="li"
|
||||||
|
exact>
|
||||||
|
<a href="#">Kemaskini Akaun</a>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<li class="dropdown" v-if="adminRole === 1">
|
<li v-if="isImpersonating()" @click="leaveImpersonation()">
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
|
<a>Kembali Akaun Asal</a>
|
||||||
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>
|
</li>
|
||||||
|
|
||||||
<li class="dropdown" v-if="adminRole === 1 || adminRole === 3">
|
<router-link :to="{ name: 'Manage Account' }" tag="li" v-if="data.user.id == 1">
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
|
<a href="#">Pengurusan Akaun</a>
|
||||||
aria-haspopup="true" aria-expanded="false">
|
</router-link>
|
||||||
<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">
|
<li @click="isImpersonating() ? null : logout()" :class="{ disabled: isImpersonating() }"
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
|
:style="isImpersonating() ? 'opacity:0.5; cursor:not-allowed; pointer-events:none;' : ''">
|
||||||
aria-haspopup="true" aria-expanded="false">
|
<a>Log Keluar</a>
|
||||||
<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>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="nav navbar-right navbar-nav">
|
</li>
|
||||||
<li class="dropdown">
|
</template>
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
|
</FloatingNavbar>
|
||||||
aria-haspopup="true" aria-expanded="false">
|
|
||||||
<span class="fa fa-user"></span> {{ data.user.name }} <span class="caret"></span>
|
|
||||||
</a>
|
|
||||||
<ul class="dropdown-menu">
|
|
||||||
|
|
||||||
<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="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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div class="admin-main">
|
<div class="admin-main">
|
||||||
<router-view></router-view>
|
<router-view></router-view>
|
||||||
@@ -130,7 +113,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import FloatingNavbar from '../../common/FloatingNavbar.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: { FloatingNavbar },
|
||||||
|
|
||||||
// data: () =>({
|
// data: () =>({
|
||||||
// loading: true,
|
// loading: true,
|
||||||
@@ -145,6 +131,7 @@ export default {
|
|||||||
axios.get(config.API + 'admin/information')
|
axios.get(config.API + 'admin/information')
|
||||||
.then(response => {
|
.then(response => {
|
||||||
console.log(response);
|
console.log(response);
|
||||||
|
localStorage.setItem('admin_session', '1');
|
||||||
vm.data.user = response.data.user;
|
vm.data.user = response.data.user;
|
||||||
vm.data.election = response.data.election;
|
vm.data.election = response.data.election;
|
||||||
vm.data.partylists = response.data.partylist;
|
vm.data.partylists = response.data.partylist;
|
||||||
@@ -229,208 +216,11 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.floating-header {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 16px !important;
|
|
||||||
left: 16px !important;
|
|
||||||
right: 16px !important;
|
|
||||||
width: calc(100% - 32px) !important;
|
|
||||||
margin: 0;
|
|
||||||
border-radius: 16px !important;
|
|
||||||
z-index: 2000 !important;
|
|
||||||
transition: all 0.3s ease !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-header {
|
|
||||||
background: linear-gradient(135deg,
|
|
||||||
rgba(255, 255, 255, 0.96) 0%,
|
|
||||||
rgb(8, 0, 126) 48%,
|
|
||||||
rgb(255, 0, 0) 100%) !important;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.25) !important;
|
|
||||||
box-shadow:
|
|
||||||
0 8px 32px rgba(33, 150, 243, 0.35),
|
|
||||||
0 4px 16px rgba(2, 19, 36, 0.28) !important;
|
|
||||||
backdrop-filter: blur(12px) !important;
|
|
||||||
position: fixed;
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-header::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.12) 0%, transparent 50%, rgba(255, 255, 255, 0.05) 100%);
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-header>* {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-section {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-right: 24px;
|
|
||||||
margin-left: 24px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav.navbar {
|
|
||||||
border: none;
|
|
||||||
min-height: 78px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav .container {
|
|
||||||
width: 100%;
|
|
||||||
padding-left: 12px;
|
|
||||||
padding-right: 12px;
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav .navbar-brand {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
height: auto;
|
|
||||||
padding: 14px 0;
|
|
||||||
color: #1f2937;
|
|
||||||
font-weight: 700;
|
|
||||||
text-shadow: 0 1px 2px rgba(255, 255, 255, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-title {
|
|
||||||
display: inline-block;
|
|
||||||
color: #1f2937 !important;
|
|
||||||
font-size: 1.8rem;
|
|
||||||
font-weight: 700;
|
|
||||||
text-shadow: 0 1px 2px rgba(255, 255, 255, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav .nav>li>a,
|
|
||||||
#nav .navbar-toggle {
|
|
||||||
color: #f8fbff;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 0.02em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav .nav>li>a:hover,
|
|
||||||
#nav .nav>li.active>a,
|
|
||||||
#nav .nav>li.active>a:hover,
|
|
||||||
#nav .nav>li.active>a:focus,
|
|
||||||
#nav .navbar-brand:hover {
|
|
||||||
color: #ffffff;
|
|
||||||
background: rgba(204, 0, 0, 0.833);
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav .nav>li>a {
|
|
||||||
border-radius: 10px;
|
|
||||||
margin-top: 18px;
|
|
||||||
margin-bottom: 18px;
|
|
||||||
padding-top: 12px;
|
|
||||||
padding-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav .navbar-right {
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav .navbar-nav>li,
|
|
||||||
#nav .dropdown {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav .dropdown-menu {
|
|
||||||
margin-top: 6px;
|
|
||||||
border: 0;
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.16);
|
|
||||||
z-index: 2101;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav .dropdown-menu>li>a {
|
|
||||||
color: #2c3e50;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav .dropdown-menu>li>a:hover {
|
|
||||||
background-color: #f39c12;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav .navbar-toggle {
|
|
||||||
margin-top: 20px;
|
|
||||||
margin-right: 18px;
|
|
||||||
border-color: rgba(255, 255, 255, 0.35);
|
|
||||||
background: rgba(255, 255, 255, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav .navbar-toggle .icon-bar {
|
|
||||||
background-color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-brand img {
|
|
||||||
background: transparent !important;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-main {
|
.admin-main {
|
||||||
padding-top: 112px;
|
padding-top: 112px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
.floating-header {
|
|
||||||
top: 10px !important;
|
|
||||||
left: 10px !important;
|
|
||||||
right: 10px !important;
|
|
||||||
width: calc(100% - 20px) !important;
|
|
||||||
border-radius: 14px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-section {
|
|
||||||
margin-left: 16px;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-title {
|
|
||||||
font-size: 1.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav .navbar-brand img {
|
|
||||||
width: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav .navbar-collapse {
|
|
||||||
border-top: 0;
|
|
||||||
box-shadow: none;
|
|
||||||
padding-bottom: 12px;
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav .nav>li>a {
|
|
||||||
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 {
|
.admin-main {
|
||||||
padding-top: 102px;
|
padding-top: 102px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ export default {
|
|||||||
vm.stopLoading();
|
vm.stopLoading();
|
||||||
if (this.util.showResult(response, 'success')) {
|
if (this.util.showResult(response, 'success')) {
|
||||||
localStorage['Access Token'] = `Bearer ${response.data.token}`;
|
localStorage['Access Token'] = `Bearer ${response.data.token}`;
|
||||||
|
localStorage.setItem('admin_session', '1');
|
||||||
if (response.data.user && response.data.user.role !== undefined && response.data.user.role !== null) {
|
if (response.data.user && response.data.user.role !== undefined && response.data.user.role !== null) {
|
||||||
localStorage.setItem('admin_role', String(response.data.user.role));
|
localStorage.setItem('admin_role', String(response.data.user.role));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,9 @@
|
|||||||
<button class="btn btn-default" @click="refreshVoter()">
|
<button class="btn btn-default" @click="refreshVoter()">
|
||||||
<i class="fa fa-refresh"></i> Kemaskini Pengundi
|
<i class="fa fa-refresh"></i> Kemaskini Pengundi
|
||||||
</button>
|
</button>
|
||||||
<router-link class="btn btn-info" :to="{ name: 'Penyata Anggota' }">
|
<!-- <router-link class="btn btn-info" :to="{ name: 'Penyata Anggota' }">
|
||||||
<i class="fa fa-list"></i> Database Anggota
|
<i class="fa fa-list"></i> Database Anggota
|
||||||
</router-link>
|
</router-link> -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="kehadiran-search">
|
<div class="kehadiran-search">
|
||||||
|
|||||||
@@ -21,45 +21,112 @@
|
|||||||
<button class="btn btn-default" @click="clearSearch()" :disabled="!hasAnySearch">Reset</button>
|
<button class="btn btn-default" @click="clearSearch()" :disabled="!hasAnySearch">Reset</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="kehadiran-filters">
|
<div class="kehadiran-filter-panel">
|
||||||
<span class="kehadiran-filters__label">Penapis:</span>
|
<div class="kehadiran-filter-panel__title">Penapis</div>
|
||||||
<button type="button" class="kehadiran-chip" :class="{ 'is-active': !current_kehadiran }"
|
<div class="kehadiran-filter-groups">
|
||||||
@click="setKehadiranFilter(null)">
|
<div class="kehadiran-filter-group">
|
||||||
Semua
|
<div class="kehadiran-filter-group__title">Kaedah kehadiran</div>
|
||||||
</button>
|
<div class="kehadiran-filter-group__chips">
|
||||||
<button type="button" class="kehadiran-chip"
|
<button type="button" class="kehadiran-chip" :class="{ 'is-active': !current_kehadiran }"
|
||||||
:class="{ 'is-active': String(current_kehadiran) === 'unset' }"
|
@click="setKehadiranFilter(null)">
|
||||||
@click="setKehadiranFilter('unset')">
|
Semua
|
||||||
Belum Ditetapkan
|
</button>
|
||||||
</button>
|
<button type="button" class="kehadiran-chip"
|
||||||
<button type="button" class="kehadiran-chip" :class="{ 'is-active': Number(current_kehadiran) === 1 }"
|
:class="{ 'is-active': String(current_kehadiran) === 'unset' }"
|
||||||
@click="setKehadiranFilter(1)">
|
@click="setKehadiranFilter('unset')">
|
||||||
Fizikal
|
Belum Ditetapkan
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="kehadiran-chip" :class="{ 'is-active': Number(current_kehadiran) === 2 }"
|
<button type="button" class="kehadiran-chip"
|
||||||
@click="setKehadiranFilter(2)">
|
:class="{ 'is-active': Number(current_kehadiran) === 1 }"
|
||||||
Maya
|
@click="setKehadiranFilter(1)">
|
||||||
</button>
|
Fizikal
|
||||||
|
</button>
|
||||||
|
<button type="button" class="kehadiran-chip"
|
||||||
|
:class="{ 'is-active': Number(current_kehadiran) === 2 }"
|
||||||
|
@click="setKehadiranFilter(2)">
|
||||||
|
Maya
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="kehadiran-filter-group">
|
||||||
|
<div class="kehadiran-filter-group__title">Pendaftaran fizikal</div>
|
||||||
|
<div class="kehadiran-filter-group__chips">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<div class="kehadiran-filter-group">
|
||||||
|
<div class="kehadiran-filter-group__title">Undi</div>
|
||||||
|
<div class="kehadiran-filter-group__chips">
|
||||||
|
<button type="button" class="kehadiran-chip" :class="{ 'is-active': !current_has_voted }"
|
||||||
|
@click="setHasVotedFilter(null)">
|
||||||
|
Semua
|
||||||
|
</button>
|
||||||
|
<button type="button" class="kehadiran-chip"
|
||||||
|
:class="{ 'is-active': current_has_voted === '1' }"
|
||||||
|
@click="setHasVotedFilter('1')">
|
||||||
|
Undi
|
||||||
|
</button>
|
||||||
|
<button type="button" class="kehadiran-chip"
|
||||||
|
:class="{ 'is-active': current_has_voted === '0' }"
|
||||||
|
@click="setHasVotedFilter('0')">
|
||||||
|
Belum
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="kehadiran-filter-group">
|
||||||
|
<div class="kehadiran-filter-group__title">Bayaran tunai (elaun)</div>
|
||||||
|
<div class="kehadiran-filter-group__chips">
|
||||||
|
<button type="button" class="kehadiran-chip" :class="{ 'is-active': !current_cash_status }"
|
||||||
|
@click="setCashStatusFilter(null)">
|
||||||
|
Semua
|
||||||
|
</button>
|
||||||
|
<button type="button" class="kehadiran-chip"
|
||||||
|
:class="{ 'is-active': current_cash_status === 'paid' }"
|
||||||
|
@click="setCashStatusFilter('paid')">
|
||||||
|
Sudah Bayar
|
||||||
|
</button>
|
||||||
|
<button type="button" class="kehadiran-chip"
|
||||||
|
:class="{ 'is-active': current_cash_status === 'unpaid' }"
|
||||||
|
@click="setCashStatusFilter('unpaid')">
|
||||||
|
Belum Bayar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="kehadiran-filters kehadiran-filters--secondary">
|
<!-- Attendance stats -->
|
||||||
<span class="kehadiran-filters__label">Pendaftaran fizikal:</span>
|
<div class="kehadiran-stats">
|
||||||
<button type="button" class="kehadiran-chip" :class="{ 'is-active': !current_fizikal_reg }"
|
<div class="kehadiran-stat">
|
||||||
@click="setFizikalRegFilter(null)">
|
<div class="kehadiran-stat__label">Fizikal</div>
|
||||||
Semua
|
<div class="kehadiran-stat__value">{{ attendanceStats.fizikalCount }}</div>
|
||||||
</button>
|
<div class="kehadiran-stat__meta">{{ attendanceStats.fizikalPct }}%</div>
|
||||||
<button type="button" class="kehadiran-chip" :class="{ 'is-active': current_fizikal_reg === 'pending' }"
|
</div>
|
||||||
@click="setFizikalRegFilter('pending')">
|
<div class="kehadiran-stat">
|
||||||
Menunggu pengesahan
|
<div class="kehadiran-stat__label">Maya</div>
|
||||||
</button>
|
<div class="kehadiran-stat__value">{{ attendanceStats.mayaCount }}</div>
|
||||||
<button type="button" class="kehadiran-chip"
|
<div class="kehadiran-stat__meta">{{ attendanceStats.mayaPct }}%</div>
|
||||||
:class="{ 'is-active': current_fizikal_reg === 'verified' }"
|
</div>
|
||||||
@click="setFizikalRegFilter('verified')">
|
<div class="kehadiran-stat kehadiran-stat--muted">
|
||||||
Disahkan
|
<div class="kehadiran-stat__label">Jumlah Kehadiran</div>
|
||||||
</button>
|
<div class="kehadiran-stat__value">{{ attendanceStats.totalAttendance }}</div>
|
||||||
|
<div class="kehadiran-stat__meta">{{ attendanceStats.totalPct }}%</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- <a class="btn btn-warning" :href="getPDFKehadiranurl(1)">Muat Turun PDF</a> -->
|
<!-- <a class="btn btn-warning" :href="getPDFKehadiranurl(1)">Muat Turun PDF</a> -->
|
||||||
|
|
||||||
<admin-data-table :headers="kehadiranTableHeaders" :items="voterListItems" :loading="voterLoading"
|
<admin-data-table :headers="kehadiranTableHeaders" :items="voterListItems" :loading="voterLoading"
|
||||||
@@ -347,6 +414,46 @@
|
|||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.kehadiran-stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
margin: 10px 0 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kehadiran-stat {
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kehadiran-stat--muted {
|
||||||
|
background: #f9fafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kehadiran-stat__label {
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kehadiran-stat__value {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 900;
|
||||||
|
color: #111827;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kehadiran-stat__meta {
|
||||||
|
margin-top: 4px;
|
||||||
|
color: #374151;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
.kehadiran-search {
|
.kehadiran-search {
|
||||||
margin: 10px 0 12px;
|
margin: 10px 0 12px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -366,23 +473,49 @@
|
|||||||
height: 34px;
|
height: 34px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.kehadiran-filters {
|
.kehadiran-filter-panel {
|
||||||
margin: 6px 0 14px;
|
margin: 10px 0 14px;
|
||||||
display: flex;
|
border: 1px solid #e5e7eb;
|
||||||
align-items: center;
|
border-radius: 10px;
|
||||||
flex-wrap: wrap;
|
background: #f9fafb;
|
||||||
gap: 8px;
|
padding: 12px 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.kehadiran-filters--secondary {
|
.kehadiran-filter-panel__title {
|
||||||
margin-top: 0;
|
font-size: 11px;
|
||||||
}
|
font-weight: 800;
|
||||||
|
text-transform: uppercase;
|
||||||
.kehadiran-filters__label {
|
letter-spacing: 0.06em;
|
||||||
color: #6b7280;
|
color: #6b7280;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kehadiran-filter-groups {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kehadiran-filter-group {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kehadiran-filter-group__title {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
margin-right: 4px;
|
color: #374151;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kehadiran-filter-group__chips {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.kehadiran-chip {
|
.kehadiran-chip {
|
||||||
@@ -556,6 +689,8 @@ export default {
|
|||||||
page: this.current_page,
|
page: this.current_page,
|
||||||
kehadiran: this.current_kehadiran || undefined,
|
kehadiran: this.current_kehadiran || undefined,
|
||||||
fizikal_reg: this.current_fizikal_reg || undefined,
|
fizikal_reg: this.current_fizikal_reg || undefined,
|
||||||
|
has_voted: this.current_has_voted || undefined,
|
||||||
|
cash_status: this.current_cash_status || undefined,
|
||||||
no_anggota: this.searchNoAnggota || undefined,
|
no_anggota: this.searchNoAnggota || undefined,
|
||||||
no_kp: this.searchNoKp || undefined
|
no_kp: this.searchNoKp || undefined
|
||||||
}
|
}
|
||||||
@@ -605,10 +740,28 @@ export default {
|
|||||||
this.$router.replace({ query: q });
|
this.$router.replace({ query: q });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setHasVotedFilter: function (val) {
|
||||||
|
var q = Object.assign({}, this.$route.query);
|
||||||
|
if (val === '1' || val === '0') q.has_voted = val;
|
||||||
|
else delete q.has_voted;
|
||||||
|
q.page = 1;
|
||||||
|
this.$router.replace({ query: q });
|
||||||
|
},
|
||||||
|
|
||||||
|
setCashStatusFilter: function (val) {
|
||||||
|
var q = Object.assign({}, this.$route.query);
|
||||||
|
if (val === 'paid' || val === 'unpaid') q.cash_status = val;
|
||||||
|
else delete q.cash_status;
|
||||||
|
q.page = 1;
|
||||||
|
this.$router.replace({ query: q });
|
||||||
|
},
|
||||||
|
|
||||||
paginationQueryForPage: function (pageNum) {
|
paginationQueryForPage: function (pageNum) {
|
||||||
var q = { page: pageNum };
|
var q = { page: pageNum };
|
||||||
if (this.current_kehadiran) q.kehadiran = this.current_kehadiran;
|
if (this.current_kehadiran) q.kehadiran = this.current_kehadiran;
|
||||||
if (this.current_fizikal_reg) q.fizikal_reg = this.current_fizikal_reg;
|
if (this.current_fizikal_reg) q.fizikal_reg = this.current_fizikal_reg;
|
||||||
|
if (this.current_has_voted) q.has_voted = this.current_has_voted;
|
||||||
|
if (this.current_cash_status) q.cash_status = this.current_cash_status;
|
||||||
return q;
|
return q;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -856,6 +1009,14 @@ export default {
|
|||||||
'$route.query.fizikal_reg': function () {
|
'$route.query.fizikal_reg': function () {
|
||||||
$.notifyClose();
|
$.notifyClose();
|
||||||
this.refreshVoter();
|
this.refreshVoter();
|
||||||
|
},
|
||||||
|
'$route.query.has_voted': function () {
|
||||||
|
$.notifyClose();
|
||||||
|
this.refreshVoter();
|
||||||
|
},
|
||||||
|
'$route.query.cash_status': function () {
|
||||||
|
$.notifyClose();
|
||||||
|
this.refreshVoter();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -910,6 +1071,22 @@ export default {
|
|||||||
return this.$route.query.fizikal_reg ? this.$route.query.fizikal_reg : null;
|
return this.$route.query.fizikal_reg ? this.$route.query.fizikal_reg : null;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
current_has_voted: function () {
|
||||||
|
var v = this.$route.query.has_voted;
|
||||||
|
if (v === '1' || v === '0') {
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
current_cash_status: function () {
|
||||||
|
var v = this.$route.query.cash_status;
|
||||||
|
if (v === 'paid' || v === 'unpaid') {
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
verifyModalVoterNeedsLowSahamNote: function () {
|
verifyModalVoterNeedsLowSahamNote: function () {
|
||||||
if (!this.verifyModalVoter) return false;
|
if (!this.verifyModalVoter) return false;
|
||||||
var v = this.verifyModalVoter;
|
var v = this.verifyModalVoter;
|
||||||
@@ -927,12 +1104,12 @@ export default {
|
|||||||
attendancePercentage() {
|
attendancePercentage() {
|
||||||
// Count total members who attended physically (Fizikal)
|
// Count total members who attended physically (Fizikal)
|
||||||
const fizikalCount = this.voterListItems.reduce((total, voter) => {
|
const fizikalCount = this.voterListItems.reduce((total, voter) => {
|
||||||
return voter.kehadiran === 1 ? total + 1 : total;
|
return Number(voter && voter.kehadiran) === 1 ? total + 1 : total;
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
// Count total members who attended virtually (Maya)
|
// Count total members who attended virtually (Maya)
|
||||||
const mayaCount = this.voterListItems.reduce((total, voter) => {
|
const mayaCount = this.voterListItems.reduce((total, voter) => {
|
||||||
return voter.kehadiran === 2 ? total + 1 : total;
|
return Number(voter && voter.kehadiran) === 2 ? total + 1 : total;
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
// Calculate total attendance percentage
|
// Calculate total attendance percentage
|
||||||
@@ -941,6 +1118,29 @@ export default {
|
|||||||
return totalMembers === 0 ? 0 : ((totalAttendance / totalMembers) * 100).toFixed(2);
|
return totalMembers === 0 ? 0 : ((totalAttendance / totalMembers) * 100).toFixed(2);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
attendanceStats: function () {
|
||||||
|
var items = this.voterListItems || [];
|
||||||
|
var fizikalCount = items.reduce(function (total, voter) {
|
||||||
|
return Number(voter && voter.kehadiran) === 1 ? total + 1 : total;
|
||||||
|
}, 0);
|
||||||
|
var mayaCount = items.reduce(function (total, voter) {
|
||||||
|
return Number(voter && voter.kehadiran) === 2 ? total + 1 : total;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
var totalAttendance = fizikalCount + mayaCount;
|
||||||
|
var totalMembers = items.length;
|
||||||
|
var denom = totalMembers === 0 ? 1 : totalMembers;
|
||||||
|
|
||||||
|
return {
|
||||||
|
fizikalCount: fizikalCount,
|
||||||
|
mayaCount: mayaCount,
|
||||||
|
totalAttendance: totalAttendance,
|
||||||
|
fizikalPct: totalMembers === 0 ? '0.00' : ((fizikalCount / denom) * 100).toFixed(2),
|
||||||
|
mayaPct: totalMembers === 0 ? '0.00' : ((mayaCount / denom) * 100).toFixed(2),
|
||||||
|
totalPct: totalMembers === 0 ? '0.00' : ((totalAttendance / denom) * 100).toFixed(2),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -133,19 +133,7 @@
|
|||||||
:value="data.user.unit" required />
|
:value="data.user.unit" required />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="attendance-hero">
|
|
||||||
<div class="attendance-section-kicker">Pengesahan Kehadiran</div>
|
|
||||||
<h4 class="attendance-page-title">Sahkan Kehadiran Mesyuarat Agung</h4>
|
|
||||||
<p class="attendance-page-subtitle">
|
|
||||||
Sila semak maklumat anda, pilih kaedah kehadiran, dan sahkan penyertaan sebelum meneruskan.
|
|
||||||
</p>
|
|
||||||
<a :href="etikaUrl" target="_blank" class="attendance-download-button">
|
|
||||||
Muat Turun Etika Mesyuarat Agung
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -526,8 +514,6 @@ export default {
|
|||||||
// })
|
// })
|
||||||
// },
|
// },
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
edit: function () {
|
edit: function () {
|
||||||
if (this.loading) return;
|
if (this.loading) return;
|
||||||
|
|
||||||
|
|||||||
@@ -144,7 +144,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Kad Ahli Digital -->
|
<!-- Kad Ahli Digital -->
|
||||||
<div class="col-xs-12">
|
<!-- <div class="col-xs-12">
|
||||||
<div class="home-section-container home-section-container--kad">
|
<div class="home-section-container home-section-container--kad">
|
||||||
<h2 class="home-section-heading">
|
<h2 class="home-section-heading">
|
||||||
<span class="home-section-heading-icon" aria-hidden="true"><i class="fa fa-credit-card"></i></span>
|
<span class="home-section-heading-icon" aria-hidden="true"><i class="fa fa-credit-card"></i></span>
|
||||||
@@ -188,7 +188,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> -->
|
||||||
|
|
||||||
<!-- Saham Vote Blocked Message -->
|
<!-- Saham Vote Blocked Message -->
|
||||||
<div v-if="showSahamVoteBlockedMessage" class="row home-saham-block-row">
|
<div v-if="showSahamVoteBlockedMessage" class="row home-saham-block-row">
|
||||||
@@ -220,6 +220,13 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
etikaUrl: function () {
|
||||||
|
var url = this.portalSettings && this.portalSettings.attendance_etika_url
|
||||||
|
? String(this.portalSettings.attendance_etika_url).trim()
|
||||||
|
: '';
|
||||||
|
return url;
|
||||||
|
},
|
||||||
|
|
||||||
bannerUrl() {
|
bannerUrl() {
|
||||||
const p = this.portalSettings && this.portalSettings.home_banner_image_path
|
const p = this.portalSettings && this.portalSettings.home_banner_image_path
|
||||||
? this.portalSettings.home_banner_image_path
|
? this.portalSettings.home_banner_image_path
|
||||||
@@ -296,7 +303,17 @@ export default {
|
|||||||
href: this.matLinkUrl,
|
href: this.matLinkUrl,
|
||||||
variant: "home-cta-link--violet",
|
variant: "home-cta-link--violet",
|
||||||
external: true
|
external: true
|
||||||
}
|
},
|
||||||
|
// Muat Turun Etika Mesyuarat Agung
|
||||||
|
{
|
||||||
|
key: "etika-mesyuarat",
|
||||||
|
title: "Etika Mesyuarat Agung",
|
||||||
|
desc: "Muat Turun etika mesyuarat agung",
|
||||||
|
icon: "fa-file-pdf-o",
|
||||||
|
href: this.etikaUrl,
|
||||||
|
variant: "home-cta-link--grey",
|
||||||
|
external: true
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -328,15 +345,15 @@ export default {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
links.push({
|
// links.push({
|
||||||
key: "penyata",
|
// key: "penyata",
|
||||||
title: "Penyata Ahli",
|
// title: "Penyata Ahli",
|
||||||
desc: "Lihat dan kemaskini penyata ahli",
|
// desc: "Lihat dan kemaskini penyata ahli",
|
||||||
icon: "fa-file-text-o",
|
// icon: "fa-file-text-o",
|
||||||
route: { name: "penyata", params: { no_anggota: noAnggota } },
|
// route: { name: "penyata", params: { no_anggota: noAnggota } },
|
||||||
variant: "home-cta-link--amber",
|
// variant: "home-cta-link--amber",
|
||||||
external: false
|
// external: false
|
||||||
});
|
// });
|
||||||
|
|
||||||
return links;
|
return links;
|
||||||
},
|
},
|
||||||
@@ -884,6 +901,15 @@ export default {
|
|||||||
box-shadow: 0 18px 44px rgba(245, 158, 11, 0.45);
|
box-shadow: 0 18px 44px rgba(245, 158, 11, 0.45);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.home-cta-link--grey {
|
||||||
|
background: linear-gradient(135deg, #64748b 0%, #94a3b8 50%, #64748b 100%);
|
||||||
|
box-shadow: 0 14px 36px rgba(100, 116, 139, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-cta-link--grey:hover {
|
||||||
|
box-shadow: 0 18px 44px rgba(100, 116, 139, 0.42);
|
||||||
|
}
|
||||||
|
|
||||||
.home-cta-icon {
|
.home-cta-icon {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 52px;
|
width: 52px;
|
||||||
|
|||||||
@@ -1,36 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div v-if="!loading">
|
<div v-if="!loading">
|
||||||
<nav class="navbar navbar-default navbar-fixed-top floating-header logo-header" id="nav">
|
<FloatingNavbar :logo-href="data.baseURL" main-nav-class="voter-nav-main">
|
||||||
<div class="container">
|
<template #right>
|
||||||
<div class="navbar-header logo-section">
|
<li class="dropdown">
|
||||||
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#myNavbar">
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true"
|
||||||
<span class="icon-bar"></span>
|
aria-expanded="false">
|
||||||
<span class="icon-bar"></span>
|
<span class="fa fa-user"></span> {{ data.user.name }} <span class="caret"></span>
|
||||||
<span class="icon-bar"></span>
|
|
||||||
</button>
|
|
||||||
<a class="navbar-brand" :href="data.baseURL">
|
|
||||||
<img src='/images/mykopkb.png' border='0' width="150" height="auto" alt="MyKoPKB Logo">
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
<ul class="dropdown-menu">
|
||||||
<div class="collapse navbar-collapse" id="myNavbar">
|
<li @click="logout()"><a>Log Keluar</a></li>
|
||||||
|
|
||||||
<ul class="nav navbar-nav voter-nav-main">
|
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="nav navbar-right navbar-nav">
|
</li>
|
||||||
<li class="dropdown">
|
</template>
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
|
</FloatingNavbar>
|
||||||
aria-haspopup="true" aria-expanded="false">
|
|
||||||
<span class="fa fa-user"></span> {{ data.user.name }} <span class="caret"></span>
|
|
||||||
</a>
|
|
||||||
<ul class="dropdown-menu">
|
|
||||||
<li @click="logout()"><a>Log Keluar</a></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div class="voter-main">
|
<div class="voter-main">
|
||||||
<div class="voter-breadcrumb-wrap">
|
<div class="voter-breadcrumb-wrap">
|
||||||
@@ -55,7 +38,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import FloatingNavbar from '../../common/FloatingNavbar.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: { FloatingNavbar },
|
||||||
data: () => ({
|
data: () => ({
|
||||||
loading: true
|
loading: true
|
||||||
}),
|
}),
|
||||||
@@ -132,154 +118,6 @@ export default {
|
|||||||
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.floating-header {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 16px !important;
|
|
||||||
left: 16px !important;
|
|
||||||
right: 16px !important;
|
|
||||||
width: calc(100% - 32px) !important;
|
|
||||||
margin: 0;
|
|
||||||
border-radius: 16px !important;
|
|
||||||
z-index: 2000 !important;
|
|
||||||
transition: all 0.3s ease !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-header {
|
|
||||||
background: linear-gradient(135deg,
|
|
||||||
rgba(255, 255, 255, 0.96) 0%,
|
|
||||||
rgb(8, 0, 126) 48%,
|
|
||||||
rgb(255, 0, 0) 100%) !important;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.25) !important;
|
|
||||||
box-shadow:
|
|
||||||
0 8px 32px rgba(33, 150, 243, 0.35),
|
|
||||||
0 4px 16px rgba(2, 19, 36, 0.28) !important;
|
|
||||||
backdrop-filter: blur(12px) !important;
|
|
||||||
position: fixed;
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-header::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.12) 0%, transparent 50%, rgba(255, 255, 255, 0.05) 100%);
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-header>* {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-section {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-right: 24px;
|
|
||||||
margin-left: 24px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav.navbar {
|
|
||||||
border: none;
|
|
||||||
min-height: 78px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav .container {
|
|
||||||
width: 100%;
|
|
||||||
padding-left: 12px;
|
|
||||||
padding-right: 12px;
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav .navbar-brand {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
height: auto;
|
|
||||||
padding: 14px 0;
|
|
||||||
color: #1f2937;
|
|
||||||
font-weight: 700;
|
|
||||||
text-shadow: 0 1px 2px rgba(255, 255, 255, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-title {
|
|
||||||
display: inline-block;
|
|
||||||
color: #1f2937 !important;
|
|
||||||
font-size: 1.8rem;
|
|
||||||
font-weight: 700;
|
|
||||||
text-shadow: 0 1px 2px rgba(255, 255, 255, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav .nav>li>a,
|
|
||||||
#nav .navbar-toggle {
|
|
||||||
color: #f8fbff;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 0.02em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav .nav>li>a:hover,
|
|
||||||
#nav .nav>li.active>a,
|
|
||||||
#nav .nav>li.active>a:hover,
|
|
||||||
#nav .nav>li.active>a:focus,
|
|
||||||
#nav .navbar-brand:hover {
|
|
||||||
color: #fff;
|
|
||||||
background: rgba(204, 0, 0, 0.833);
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav .nav>li>a {
|
|
||||||
border-radius: 10px;
|
|
||||||
margin-top: 18px;
|
|
||||||
margin-bottom: 18px;
|
|
||||||
padding-top: 12px;
|
|
||||||
padding-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav .navbar-right {
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav .navbar-nav>li,
|
|
||||||
#nav .dropdown {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav .dropdown-menu {
|
|
||||||
margin-top: 6px;
|
|
||||||
border: 0;
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.16);
|
|
||||||
z-index: 2101;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav .dropdown-menu>li>a {
|
|
||||||
color: #2c3e50;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav .dropdown-menu>li>a:hover {
|
|
||||||
background-color: #f39c12;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav .navbar-toggle {
|
|
||||||
margin-top: 20px;
|
|
||||||
margin-right: 18px;
|
|
||||||
border-color: rgba(255, 255, 255, 0.35);
|
|
||||||
background: rgba(255, 255, 255, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav .navbar-toggle .icon-bar {
|
|
||||||
background-color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-brand img {
|
|
||||||
background: transparent !important;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.voter-main {
|
.voter-main {
|
||||||
padding-top: 112px;
|
padding-top: 112px;
|
||||||
}
|
}
|
||||||
@@ -336,38 +174,6 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
.floating-header {
|
|
||||||
top: 10px !important;
|
|
||||||
left: 10px !important;
|
|
||||||
right: 10px !important;
|
|
||||||
width: calc(100% - 20px) !important;
|
|
||||||
border-radius: 14px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-section {
|
|
||||||
margin-left: 16px;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-title {
|
|
||||||
font-size: 1.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav .navbar-brand img {
|
|
||||||
width: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav .navbar-collapse {
|
|
||||||
border-top: 0;
|
|
||||||
box-shadow: none;
|
|
||||||
padding-bottom: 12px;
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav .nav>li>a {
|
|
||||||
margin: 4px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.voter-main {
|
.voter-main {
|
||||||
padding-top: 102px;
|
padding-top: 102px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,13 @@
|
|||||||
loading: false
|
loading: false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
if (this.util.isAdminPortalSession()) {
|
||||||
|
this.$router.replace(this.util.getAdminEntryRoute());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
login() {
|
login() {
|
||||||
this.startLoading();
|
this.startLoading();
|
||||||
|
|||||||
@@ -148,6 +148,8 @@ export default {
|
|||||||
.then(response => {
|
.then(response => {
|
||||||
vm.stopLoading();
|
vm.stopLoading();
|
||||||
if (this.util.showResult(response, 'success')) {
|
if (this.util.showResult(response, 'success')) {
|
||||||
|
localStorage.removeItem('admin_role');
|
||||||
|
localStorage.removeItem('admin_session');
|
||||||
localStorage['Access Token'] = `Bearer ${response.data.token}`;
|
localStorage['Access Token'] = `Bearer ${response.data.token}`;
|
||||||
this.util.setAuthorization();
|
this.util.setAuthorization();
|
||||||
vm.$router.push({ name: 'Voter Home' });
|
vm.$router.push({ name: 'Voter Home' });
|
||||||
|
|||||||
Vendored
+72
@@ -0,0 +1,72 @@
|
|||||||
|
:root {
|
||||||
|
/*
|
||||||
|
* Centralized semantic palette — literal colors live here only.
|
||||||
|
* Primary / Secondary / Accent / Background / Surface / Text / Border
|
||||||
|
*/
|
||||||
|
--color-primary: #4F7DB8;
|
||||||
|
--color-secondary: #2F5D9A;
|
||||||
|
--color-accent: #C62828;
|
||||||
|
--color-background: #F5F9FD;
|
||||||
|
--color-surface: #ffffff;
|
||||||
|
--color-text: #1F2937;
|
||||||
|
--color-border: #D6E0EA;
|
||||||
|
|
||||||
|
/* Content on filled regions (no extra literals) */
|
||||||
|
--color-on-primary: var(--color-surface);
|
||||||
|
--color-on-accent: var(--color-surface);
|
||||||
|
|
||||||
|
/* Derived tones — color-mix + semantic vars only */
|
||||||
|
--color-accent-strong: color-mix(in srgb, var(--color-accent) 78%, var(--color-text) 22%);
|
||||||
|
--color-accent-deep: color-mix(in srgb, var(--color-accent) 52%, var(--color-text) 48%);
|
||||||
|
--color-primary-soft-glow: color-mix(in srgb, var(--color-primary) 22%, transparent);
|
||||||
|
--color-accent-soft-glow: color-mix(in srgb, var(--color-accent) 18%, transparent);
|
||||||
|
--color-secondary-soft-glow: color-mix(in srgb, var(--color-secondary) 12%, transparent);
|
||||||
|
--color-text-soft-shadow: color-mix(in srgb, var(--color-text) 16%, transparent);
|
||||||
|
--color-surface-veil-08: color-mix(in srgb, var(--color-surface) 8%, transparent);
|
||||||
|
--color-surface-veil-10: color-mix(in srgb, var(--color-surface) 10%, transparent);
|
||||||
|
--color-surface-veil-14: color-mix(in srgb, var(--color-surface) 14%, transparent);
|
||||||
|
--color-surface-veil-25: color-mix(in srgb, var(--color-surface) 25%, transparent);
|
||||||
|
--color-surface-veil-35: color-mix(in srgb, var(--color-surface) 35%, transparent);
|
||||||
|
--color-surface-veil-92: color-mix(in srgb, var(--color-surface) 92%, transparent);
|
||||||
|
--color-accent-veil-08: color-mix(in srgb, var(--color-accent) 8%, transparent);
|
||||||
|
--color-accent-veil-92: color-mix(in srgb, var(--color-accent) 92%, transparent);
|
||||||
|
--color-nav-ink: color-mix(in srgb, var(--color-surface) 97%, var(--color-border) 3%);
|
||||||
|
|
||||||
|
/* Admin data table — maps to semantic palette */
|
||||||
|
--admin-table-header-bg: color-mix(in srgb, var(--color-secondary) 42%, var(--color-text) 58%);
|
||||||
|
--admin-table-header-color: var(--color-on-primary);
|
||||||
|
--admin-table-border-color: var(--color-border);
|
||||||
|
--admin-table-row-border-color: color-mix(in srgb, var(--color-border) 52%, var(--color-surface) 48%);
|
||||||
|
--admin-table-shell-bg: var(--color-surface);
|
||||||
|
--admin-table-row-hover-bg: color-mix(in srgb, var(--color-primary) 9%, var(--color-surface) 91%);
|
||||||
|
--admin-table-footer-bg: var(--color-background);
|
||||||
|
--admin-table-text-muted: color-mix(in srgb, var(--color-text) 58%, var(--color-border) 42%);
|
||||||
|
--admin-table-text-strong: var(--color-text);
|
||||||
|
--admin-table-footer-mid-text: color-mix(in srgb, var(--color-text) 74%, var(--color-border) 26%);
|
||||||
|
--admin-table-pagination-disabled: color-mix(in srgb, var(--color-border) 66%, var(--color-background) 34%);
|
||||||
|
|
||||||
|
/* Floating header — gradient + glass (semantic + derived only) */
|
||||||
|
--floating-header-bg-1: var(--color-primary);
|
||||||
|
--floating-header-bg-2: var(--color-secondary);
|
||||||
|
--floating-header-bg-3: var(--color-accent-strong);
|
||||||
|
--floating-header-bg-4: var(--color-accent-deep);
|
||||||
|
--floating-header-border: var(--color-surface-veil-14);
|
||||||
|
--floating-header-shadow-1: var(--color-primary-soft-glow);
|
||||||
|
--floating-header-shadow-2: var(--color-accent-soft-glow);
|
||||||
|
--floating-header-overlay-1: var(--color-surface-veil-10);
|
||||||
|
--floating-header-overlay-2: var(--color-accent-veil-08);
|
||||||
|
--floating-header-hover-bg: var(--color-accent-veil-92);
|
||||||
|
--floating-header-nav-text: var(--color-nav-ink);
|
||||||
|
--floating-header-brand-text: var(--color-on-primary);
|
||||||
|
--floating-header-brand-shadow: var(--color-surface-veil-25);
|
||||||
|
|
||||||
|
/* Floating navbar — dropdowns & chrome */
|
||||||
|
--floating-header-dropdown-hover-bg: var(--color-accent);
|
||||||
|
--floating-header-dropdown-hover-text: var(--color-on-accent);
|
||||||
|
--floating-header-dropdown-menu-shadow: var(--color-text-soft-shadow);
|
||||||
|
--floating-header-toggle-border: var(--color-surface-veil-35);
|
||||||
|
--floating-header-toggle-bg: var(--color-surface-veil-08);
|
||||||
|
--floating-header-icon-bar: var(--color-surface);
|
||||||
|
--floating-header-mobile-submenu-bg: var(--color-surface-veil-92);
|
||||||
|
--floating-header-mobile-submenu-shadow: var(--color-secondary-soft-glow);
|
||||||
|
}
|
||||||
Vendored
+33
-15
@@ -17,6 +17,23 @@ const methods = {
|
|||||||
return localStorage['Access Token'] ? true : false;
|
return localStorage['Access Token'] ? true : false;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when the browser session is for the admin portal (not a voter-only token).
|
||||||
|
*/
|
||||||
|
isAdminPortalSession: function () {
|
||||||
|
return localStorage.getItem('admin_role') != null || localStorage.getItem('admin_session') === '1';
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Route name to open for the current admin session (role 3 is restricted to kehadiran).
|
||||||
|
*/
|
||||||
|
getAdminEntryRoute: function () {
|
||||||
|
if (localStorage.getItem('admin_role') === '3') {
|
||||||
|
return { name: 'Kehadiran Calon' };
|
||||||
|
}
|
||||||
|
return { name: 'Admin Home' };
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log a messages
|
* Log a messages
|
||||||
* @param {String} Message
|
* @param {String} Message
|
||||||
@@ -70,7 +87,7 @@ const methods = {
|
|||||||
}, {
|
}, {
|
||||||
type: type,
|
type: type,
|
||||||
delay: delay,
|
delay: delay,
|
||||||
placement: {from: 'top', align: 'center'}
|
placement: { from: 'top', align: 'center' }
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -81,7 +98,7 @@ const methods = {
|
|||||||
*/
|
*/
|
||||||
setAuthorization: function () {
|
setAuthorization: function () {
|
||||||
axios.defaults.headers.common['Authorization'] = localStorage['Access Token'];
|
axios.defaults.headers.common['Authorization'] = localStorage['Access Token'];
|
||||||
$.ajaxSetup({headers:{'Authorization': localStorage['Access Token']}});
|
$.ajaxSetup({ headers: { 'Authorization': localStorage['Access Token'] } });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
@@ -100,8 +117,7 @@ const methods = {
|
|||||||
* @param {Int} Response Status
|
* @param {Int} Response Status
|
||||||
* @return {String} Error Message
|
* @return {String} Error Message
|
||||||
*/
|
*/
|
||||||
getErrorMessage: function (data, status)
|
getErrorMessage: function (data, status) {
|
||||||
{
|
|
||||||
var message = '';
|
var message = '';
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 200:
|
case 200:
|
||||||
@@ -148,10 +164,10 @@ const methods = {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 401:
|
case 401:
|
||||||
message = 'You need to login first.';
|
message = 'Sila log masuk terlebih dahulu.';
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
message = 'An error occured.';
|
message = 'Ralat telah berlaku.';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return message;
|
return message;
|
||||||
@@ -173,7 +189,7 @@ const methods = {
|
|||||||
* @param {library} Library used (ajax|axios) Default : axios
|
* @param {library} Library used (ajax|axios) Default : axios
|
||||||
*/
|
*/
|
||||||
showResult: function (response, type, library = 'axios') {
|
showResult: function (response, type, library = 'axios') {
|
||||||
this.log (response);
|
this.log(response);
|
||||||
var isSuccess = data => data.status == 'success';
|
var isSuccess = data => data.status == 'success';
|
||||||
if (library == 'axios') {
|
if (library == 'axios') {
|
||||||
if (type == 'success') {
|
if (type == 'success') {
|
||||||
@@ -184,7 +200,7 @@ const methods = {
|
|||||||
this.notify(response.data.message, 'error');
|
this.notify(response.data.message, 'error');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var status = response.response ? response.response.status : 500;
|
var status = response.response ? response.response.status : 500;
|
||||||
var message = this.getErrorMessage(response.response.data, status);
|
var message = this.getErrorMessage(response.response.data, status);
|
||||||
this.notify(message, 'error');
|
this.notify(message, 'error');
|
||||||
return status;
|
return status;
|
||||||
@@ -198,7 +214,7 @@ const methods = {
|
|||||||
this.notify(response.message, 'error');
|
this.notify(response.message, 'error');
|
||||||
}
|
}
|
||||||
} else if (type == 'error') {
|
} else if (type == 'error') {
|
||||||
var status = response.status;
|
var status = response.status;
|
||||||
var message = this.getErrorMessage(response.responseText, status);
|
var message = this.getErrorMessage(response.responseText, status);
|
||||||
this.notify(message, 'error');
|
this.notify(message, 'error');
|
||||||
return status;
|
return status;
|
||||||
@@ -217,12 +233,12 @@ const data = {
|
|||||||
positions: [],
|
positions: [],
|
||||||
position: {},
|
position: {},
|
||||||
partylists: [],
|
partylists: [],
|
||||||
partylist:{},
|
partylist: {},
|
||||||
voters:{},
|
voters: {},
|
||||||
voter:{},
|
voter: {},
|
||||||
nominees:[],
|
nominees: [],
|
||||||
nominee:{},
|
nominee: {},
|
||||||
admins:[],
|
admins: [],
|
||||||
information: {},
|
information: {},
|
||||||
result: [],
|
result: [],
|
||||||
results: [],
|
results: [],
|
||||||
@@ -237,3 +253,5 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { methods };
|
||||||
@@ -72,9 +72,11 @@ Route::prefix('v1')->group(function () { //Version 1 of my Rest API
|
|||||||
|
|
||||||
Route::prefix('election')->group(function () {
|
Route::prefix('election')->group(function () {
|
||||||
Route::get('result/{id}', 'API\v1\Election\ResultController@finalResult');
|
Route::get('result/{id}', 'API\v1\Election\ResultController@finalResult');
|
||||||
|
Route::get('{id}/attendance-summary', 'API\v1\Election\AttendanceSummaryController');
|
||||||
Route::get('results', 'API\v1\Election\ResultController');
|
Route::get('results', 'API\v1\Election\ResultController');
|
||||||
Route::post('start', 'API\v1\Election\StartController');
|
Route::post('start', 'API\v1\Election\StartController');
|
||||||
Route::post('stop', 'API\v1\Election\StopController');
|
Route::post('stop', 'API\v1\Election\StopController');
|
||||||
|
Route::post('register-next', 'API\v1\Election\RegisterNextSessionController');
|
||||||
Route::get('', 'API\v1\Election\GetController');
|
Route::get('', 'API\v1\Election\GetController');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": "."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user