DONE: add flexible json update saham, yuran, and pelaburan ui

This commit is contained in:
ISMAIL MASSERAN
2026-07-19 11:54:13 +08:00
parent fa2eaef6c4
commit cd1dc4b9d8
18 changed files with 4010 additions and 15 deletions
Vendored
BIN
View File
Binary file not shown.
@@ -12,7 +12,7 @@ class UpdateVoterPelaburanFromJson extends Command
{--election_id= : Override election_id (default: latest in voter table)}
{--dry-run : Show what would change without updating}';
protected $description = 'Update voter pelaburan (dividen) from list-pelabur.json for latest election only';
protected $description = 'Update voter dividen_pelaburan (from JSON pelaburan) and accumulated_amount from list-pelabur.json for latest election only';
public function handle()
{
@@ -51,8 +51,9 @@ class UpdateVoterPelaburanFromJson extends Command
continue;
}
// JSON key "pelaburan" is the dividend amount → voter.dividen_pelaburan
$map[$noAnggota] = [
'pelaburan' => $this->cleanNumber($row['pelaburan'] ?? 0),
'dividen_pelaburan' => $this->cleanNumber($row['pelaburan'] ?? 0),
'accumulated_amount' => $this->cleanNumber($row['accumulated_amount'] ?? 0),
];
}
@@ -67,6 +68,7 @@ class UpdateVoterPelaburanFromJson extends Command
$this->info("Target election_id: {$electionId}");
$this->info("JSON members loaded: " . count($map));
$this->info($dryRun ? "Mode: DRY RUN (no DB updates)" : "Mode: UPDATE");
$this->info("Mapping: JSON.pelaburan → voter.dividen_pelaburan, JSON.accumulated_amount → voter.accumulated_amount");
$totalInElection = (int) DB::table('voter')->where('election_id', $electionId)->count();
$this->info("Voters in election: {$totalInElection}");
@@ -80,7 +82,7 @@ class UpdateVoterPelaburanFromJson extends Command
$bar->start();
DB::table('voter')
->select(['id', 'no_anggota', 'pelaburan', 'accumulated_amount'])
->select(['id', 'no_anggota', 'dividen_pelaburan', 'accumulated_amount'])
->where('election_id', $electionId)
->orderBy('id')
->chunkById(500, function ($chunk) use ($map, $dryRun, &$updated, &$matched, &$missingInJson, &$unchanged, $bar) {
@@ -96,11 +98,11 @@ class UpdateVoterPelaburanFromJson extends Command
$matched++;
$jsonRow = $map[$noAnggota];
$oldPelaburan = $voter->pelaburan !== null ? (float) $voter->pelaburan : 0.0;
$oldDividen = $voter->dividen_pelaburan !== null ? (float) $voter->dividen_pelaburan : 0.0;
$oldAccumulated = $voter->accumulated_amount !== null ? (float) $voter->accumulated_amount : 0.0;
if (
abs($oldPelaburan - $jsonRow['pelaburan']) < 0.00001
abs($oldDividen - $jsonRow['dividen_pelaburan']) < 0.00001
&& abs($oldAccumulated - $jsonRow['accumulated_amount']) < 0.00001
) {
$unchanged++;
@@ -112,7 +114,7 @@ class UpdateVoterPelaburanFromJson extends Command
DB::table('voter')
->where('id', $voter->id)
->update([
'pelaburan' => $jsonRow['pelaburan'],
'dividen_pelaburan' => $jsonRow['dividen_pelaburan'],
'accumulated_amount' => $jsonRow['accumulated_amount'],
'updated_at' => now(),
]);
@@ -145,7 +147,7 @@ class UpdateVoterPelaburanFromJson extends Command
private function cleanNumber($value): float
{
if ($value === null) {
if ($value === null || $value === '') {
return 0.0;
}
@@ -0,0 +1,176 @@
<?php
namespace App\Http\Controllers\API\v1\Voter;
use App\Http\Controllers\Controller;
use App\Http\Controllers\Util;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
/**
* Import keanggotaan JSON (match by no_anggota) into voter money columns for the current election.
* Expected row keys: no_anggota, pelaburan, pelaburan_2, saham, yuran. Null/empty → 0.
*/
class ImportKeanggotaanJsonController extends Controller
{
private const UPDATABLE_FIELDS = [
'pelaburan',
'pelaburan_2',
'saham',
'yuran',
];
public function __invoke(Request $request)
{
$this->validate($request, [
'file' => 'required|file|max:10240',
]);
$file = $request->file('file');
$extension = strtolower($file->getClientOriginalExtension() ?: '');
if ($extension !== 'json') {
return response()->json([
'status' => 'failed',
'message' => 'Fail mestilah format .json.',
], 422);
}
$raw = file_get_contents($file->getRealPath());
$rows = json_decode($raw, true);
if (!is_array($rows)) {
return response()->json([
'status' => 'failed',
'message' => 'JSON tidak sah. Jangkaan array objek.',
], 422);
}
$map = [];
foreach ($rows as $row) {
if (!is_array($row)) {
continue;
}
$noAnggota = isset($row['no_anggota']) ? trim((string) $row['no_anggota']) : '';
if ($noAnggota === '') {
continue;
}
$payload = [];
foreach (self::UPDATABLE_FIELDS as $field) {
if (array_key_exists($field, $row)) {
$payload[$field] = $this->cleanNumber($row[$field]);
}
}
if (count($payload) === 0) {
continue;
}
$map[$noAnggota] = $payload;
}
if (count($map) === 0) {
return response()->json([
'status' => 'failed',
'message' => 'Tiada baris sah dalam JSON (perlu no_anggota dan sekurang-kurangnya satu medan).',
], 422);
}
$electionId = (int) Util::getCurrentElection();
if ($electionId <= 0) {
return response()->json([
'status' => 'failed',
'message' => 'Tidak dapat mengenal pasti election semasa.',
], 422);
}
$stats = [
'election_id' => $electionId,
'json_rows' => count($map),
'matched' => 0,
'updated' => 0,
'unchanged' => 0,
'missing_in_voter' => 0,
'missing_in_json' => 0,
];
$matchedKeys = [];
DB::table('voter')
->select(array_merge(['id', 'no_anggota'], self::UPDATABLE_FIELDS))
->where('election_id', $electionId)
->orderBy('id')
->chunkById(500, function ($chunk) use ($map, &$stats, &$matchedKeys) {
foreach ($chunk as $voter) {
$noAnggota = $voter->no_anggota !== null ? trim((string) $voter->no_anggota) : '';
if ($noAnggota === '' || !array_key_exists($noAnggota, $map)) {
$stats['missing_in_json']++;
continue;
}
$stats['matched']++;
$matchedKeys[$noAnggota] = true;
$jsonRow = $map[$noAnggota];
$update = [];
foreach ($jsonRow as $field => $newValue) {
$oldValue = $voter->{$field} !== null ? (float) $voter->{$field} : 0.0;
if (abs($oldValue - $newValue) >= 0.00001) {
$update[$field] = $newValue;
}
}
if (count($update) === 0) {
$stats['unchanged']++;
continue;
}
$update['updated_at'] = now();
DB::table('voter')->where('id', $voter->id)->update($update);
$stats['updated']++;
}
}, 'id');
foreach ($map as $noAnggota => $_payload) {
if (!isset($matchedKeys[$noAnggota])) {
$stats['missing_in_voter']++;
}
}
activity()
->withProperties([
'election_id' => $electionId,
'filename' => $file->getClientOriginalName(),
'stats' => $stats,
'ip' => $request->ip(),
'user_agent' => $request->userAgent(),
])
->log('import keanggotaan json');
return response()->json([
'status' => 'success',
'message' => 'Import keanggotaan berjaya.',
'data' => $stats,
]);
}
private function cleanNumber($value): float
{
if ($value === null || $value === '') {
return 0.0;
}
$value = (string) $value;
$value = str_replace(['RM', ',', ' '], '', $value);
if (preg_match('/^\((.*)\)$/', $value, $matches)) {
$value = $matches[1];
}
$value = preg_replace('/[^0-9\.\-]/', '', $value);
return is_numeric($value) ? (float) $value : 0.0;
}
}
@@ -41,6 +41,8 @@ class ElectionMiddleware
&& !($request->is('api/v1/voter') && $request->isMethod('post'))
// Allow updating applicable voters list during election (add page uses POST /api/v1/voter/sync-applicable)
&& !($request->is('api/v1/voter/sync-applicable') && $request->isMethod('post'))
// Allow keanggotaan JSON import during election
&& !($request->is('api/v1/voter/import-keanggotaan-json') && $request->isMethod('post'))
&& !$request->is('api/v1/voter/*/verify-fizikal-registration')
// Pentadbir utama: urus akaun (tambah/kemaskini/padam) semasa mengundi
&& !$request->is('api/v1/admin')
+1 -1
View File
@@ -11,7 +11,7 @@ class Voter extends Authenticatable
protected $table = 'voter';
protected $fillable = ['name', 'no_kp', 'no_anggota','unit', 'election_id', 'alamat', 'telefon', 'saham', 'yuran', 'dividen_saham', 'dividen_yuran', 'saham_terkini', 'yuran_terkini', 'kehadiran','status_penyata','persetujuan','tarikh_sah','cadangan','pelaburan', 'accumulated_amount', 'tergempar','barangan','peribadi','berjamin_yuran','roadtax','pelbagai'];
protected $fillable = ['name', 'no_kp', 'no_anggota','unit', 'election_id', 'alamat', 'telefon', 'saham', 'yuran', 'dividen_saham', 'dividen_yuran', 'saham_terkini', 'yuran_terkini', 'kehadiran','status_penyata','persetujuan','tarikh_sah','cadangan','pelaburan', 'dividen_pelaburan', 'pelaburan_terkini', 'pelaburan_2', 'dividen_pelaburan_2', 'pelaburan_terkini_2', 'accumulated_amount', 'tergempar','barangan','peribadi','berjamin_yuran','roadtax','pelbagai'];
protected $casts = [
'fizikal_registration_verified_at' => 'datetime',
BIN
View File
Binary file not shown.
@@ -0,0 +1,33 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddDividenPelaburanAndPelaburanTerkiniToVoterTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('voter', function (Blueprint $table) {
$table->decimal('dividen_pelaburan', 12, 2)->default(0)->after('pelaburan');
$table->decimal('pelaburan_terkini', 12, 2)->default(0)->after('dividen_pelaburan');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('voter', function (Blueprint $table) {
$table->dropColumn(['dividen_pelaburan', 'pelaburan_terkini']);
});
}
}
@@ -0,0 +1,35 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddPelaburan2ColumnsToVoterTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('voter', function (Blueprint $table) {
$table->decimal('pelaburan_2', 12, 2)->default(0)->after('pelaburan_terkini');
$table->decimal('dividen_pelaburan_2', 12, 2)->default(0)->after('pelaburan_2');
$table->decimal('pelaburan_terkini_2', 12, 2)->default(0)->after('dividen_pelaburan_2');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('voter', function (Blueprint $table) {
$table->dropColumn(['pelaburan_2', 'dividen_pelaburan_2', 'pelaburan_terkini_2']);
});
}
}
BIN
View File
Binary file not shown.
File diff suppressed because it is too large Load Diff
+1 -1
View File
File diff suppressed because one or more lines are too long
@@ -52,6 +52,9 @@
<router-link :to="{ name: 'Penyata Anggota' }" tag="li">
<a href="#">Penyata Anggota</a>
</router-link>
<router-link :to="{ name: 'Import Keanggotaan' }" tag="li">
<a href="#">Import Keanggotaan</a>
</router-link>
</ul>
</li>
@@ -0,0 +1,313 @@
<template>
<div class="container col-md-10 col-md-offset-1">
<h4>Import Keanggotaan (JSON)</h4>
<div class="panel panel-default">
<div class="panel-body">
<p class="help-block">
Muat naik fail JSON dengan format yang sama seperti
<code>keanggotaan_jun-2026.json</code>. Data dikemas kini mengikut
<code>no_anggota</code> untuk pilihan raya semasa. Medan kosong/null diganti dengan
<code>0</code>.
</p>
<div class="panel panel-default" style="margin-bottom: 20px;">
<div class="panel-heading">Contoh format JSON</div>
<div class="panel-body">
<p class="help-block" style="margin-top: 0;">
Fail mestilah <strong>array objek</strong> dengan medan di bawah.
Nilai kosong (<code>""</code>) atau <code>null</code> akan diganti dengan <code>0</code>.
</p>
<div class="table-responsive" style="margin-bottom: 12px;">
<table class="table table-striped table-bordered table-condensed" style="margin-bottom: 0;">
<thead>
<tr>
<th v-for="col in sampleColumns" :key="col">{{ col }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, idx) in sampleRows" :key="idx">
<td v-for="col in sampleColumns" :key="col">
{{ formatPreviewCell(row[col]) }}
</td>
</tr>
</tbody>
</table>
</div>
<pre
style="margin: 0; max-height: 220px; overflow: auto; background: #f5f5f5; padding: 10px; border-radius: 3px;">{{ sampleJson }}</pre>
</div>
</div>
<div class="form-group">
<label>Fail JSON</label>
<input ref="fileInput" type="file" class="form-control" accept=".json,application/json"
@change="onFileChange">
<small class="help-block">
Medan yang dikemas kini: <code>pelaburan</code>, <code>pelaburan_2</code>,
<code>saham</code>, <code>yuran</code>.
</small>
</div>
<div v-if="selectedFileName" class="alert alert-info" style="margin-bottom: 15px;">
Fail dipilih: <strong>{{ selectedFileName }}</strong>
<span v-if="previewLoading"> membaca fail...</span>
</div>
<div v-if="previewError" class="alert alert-danger">
{{ previewError }}
</div>
<div v-if="previewRows.length" class="panel panel-info" style="margin-bottom: 15px;">
<div class="panel-heading">
Pratonton JSON
<span class="pull-right">
{{ previewTotal }} baris
<span v-if="previewTotal > previewLimit">
(menunjukkan {{ previewLimit }} pertama)
</span>
</span>
</div>
<div class="panel-body" style="padding: 0;">
<div class="table-responsive" style="max-height: 360px; overflow: auto;">
<table class="table table-striped table-bordered table-condensed" style="margin-bottom: 0;">
<thead>
<tr>
<th>#</th>
<th v-for="col in previewColumns" :key="col">{{ col }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, idx) in previewRows" :key="idx">
<td>{{ idx + 1 }}</td>
<td v-for="col in previewColumns" :key="col">
{{ formatPreviewCell(row[col]) }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="panel-footer">
<a href="#" @click.prevent="showRaw = !showRaw">
{{ showRaw ? 'Sembunyikan' : 'Tunjuk' }} raw JSON
</a>
<pre v-if="showRaw"
style="margin: 10px 0 0; max-height: 280px; overflow: auto; background: #f5f5f5; padding: 10px; border-radius: 3px;">{{ previewRaw }}</pre>
</div>
</div>
<button class="btn btn-primary" :disabled="!canImport" @click="upload">
<i class="fa fa-upload"></i>
{{ uploading ? 'Mengimport...' : 'Import' }}
</button>
<button v-if="file" class="btn btn-default" style="margin-left: 8px;" :disabled="uploading" @click="clearFile">
Kosongkan
</button>
<div v-if="result" class="panel panel-success" style="margin-top: 20px;">
<div class="panel-heading">Hasil Import</div>
<div class="panel-body">
<ul class="list-unstyled" style="margin-bottom: 0;">
<li>Election ID: <strong>{{ result.election_id }}</strong></li>
<li>Baris JSON: <strong>{{ result.json_rows }}</strong></li>
<li>Padanan (matched): <strong>{{ result.matched }}</strong></li>
<li>Dikemas kini: <strong>{{ result.updated }}</strong></li>
<li>Tiada perubahan: <strong>{{ result.unchanged }}</strong></li>
<li>Dalam JSON tetapi tiada dalam voter: <strong>{{ result.missing_in_voter }}</strong></li>
<li>Dalam voter tetapi tiada dalam JSON: <strong>{{ result.missing_in_json }}</strong></li>
</ul>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
const PREVIEW_LIMIT = 50;
const SAMPLE_ROWS = [
{
no_anggota: "7",
pelaburan: "473776.8",
pelaburan_2: "",
saham: "6600",
yuran: "4671.7"
},
{
no_anggota: "12",
pelaburan: "527090.7",
pelaburan_2: "",
saham: "10000",
yuran: "19463.9"
},
{
no_anggota: "22",
pelaburan: "",
pelaburan_2: "",
saham: "5000",
yuran: "5260"
}
];
export default {
data() {
return {
file: null,
selectedFileName: "",
uploading: false,
result: null,
previewLoading: false,
previewError: "",
previewRows: [],
previewColumns: [],
previewTotal: 0,
previewLimit: PREVIEW_LIMIT,
previewRaw: "",
showRaw: false,
sampleColumns: ["no_anggota", "pelaburan", "pelaburan_2", "saham", "yuran"],
sampleRows: SAMPLE_ROWS,
sampleJson: JSON.stringify(SAMPLE_ROWS, null, 2)
};
},
computed: {
canImport() {
return !!this.file && !this.uploading && !this.previewLoading && !this.previewError && this.previewRows.length > 0;
}
},
created() {
this.util.setTitle("MyKoPKB - Import Keanggotaan");
},
methods: {
onFileChange(e) {
const files = e && e.target ? e.target.files : null;
this.file = files && files.length ? files[0] : null;
this.selectedFileName = this.file ? this.file.name : "";
this.result = null;
this.resetPreview();
if (this.file) {
this.loadPreview(this.file);
}
},
resetPreview() {
this.previewLoading = false;
this.previewError = "";
this.previewRows = [];
this.previewColumns = [];
this.previewTotal = 0;
this.previewRaw = "";
this.showRaw = false;
},
loadPreview(file) {
this.previewLoading = true;
this.previewError = "";
const reader = new FileReader();
reader.onload = (event) => {
try {
const text = event.target.result;
const parsed = JSON.parse(text);
if (!Array.isArray(parsed)) {
throw new Error("JSON mestilah array objek.");
}
if (parsed.length === 0) {
throw new Error("JSON kosong (tiada baris).");
}
const columns = [];
parsed.forEach((row) => {
if (row && typeof row === "object" && !Array.isArray(row)) {
Object.keys(row).forEach((key) => {
if (columns.indexOf(key) === -1) {
columns.push(key);
}
});
}
});
if (columns.indexOf("no_anggota") === -1) {
throw new Error("JSON tiada medan no_anggota.");
}
this.previewColumns = columns;
this.previewTotal = parsed.length;
this.previewRows = parsed.slice(0, PREVIEW_LIMIT);
this.previewRaw = JSON.stringify(parsed.slice(0, PREVIEW_LIMIT), null, 2);
this.previewError = "";
} catch (err) {
this.resetPreview();
this.previewError = err && err.message
? "Gagal membaca JSON: " + err.message
: "Gagal membaca JSON.";
} finally {
this.previewLoading = false;
}
};
reader.onerror = () => {
this.resetPreview();
this.previewError = "Gagal membaca fail.";
this.previewLoading = false;
};
reader.readAsText(file);
},
formatPreviewCell(value) {
if (value === null || value === undefined || value === "") {
return "0";
}
return value;
},
clearFile() {
this.file = null;
this.selectedFileName = "";
this.result = null;
this.resetPreview();
if (this.$refs.fileInput) {
this.$refs.fileInput.value = "";
}
},
async upload() {
if (!this.canImport) return;
this.uploading = true;
this.result = null;
try {
this.util.setAuthorization();
const fd = new FormData();
fd.append("file", this.file);
const res = await axios.post(config.API + "voter/import-keanggotaan-json", fd, {
headers: { "Content-Type": "multipart/form-data" }
});
if (res && res.data && res.data.status === "success") {
this.util.notify(res.data.message || "Import berjaya.", "success");
this.result = res.data.data || null;
} else {
this.util.showResult(res, "error");
}
} catch (e) {
this.util.showResult(e, "error");
} finally {
this.uploading = false;
}
}
}
};
</script>
@@ -439,10 +439,16 @@ export default {
]
},
{
key: "pelaburan",
icon: "fa-bank",
label: "Simpanan Khas (Tabung 1) - (Sehingga Dis 2025)",
value: "RM " + fmt(u.accumulated_amount),
subvalue: dividendSub(u.accumulated_amount, u.pelaburan, 12)
label: "Pelaburan Simpanan Khas (Tabung 1) - (Sehingga Dis 2025)",
value: "RM " + fmt(u.pelaburan),
subvalue: dividendSub(u.pelaburan, u.dividen_pelaburan, 12)
},
{
icon: "fa-bank",
label: "Pelaburan Simpanan Khas (Tabung 2) - (Sehingga " + this.formatMalayMonthYear() + ")",
value: "RM " + fmt(u.pelaburan_2),
}
];
}
@@ -460,6 +466,20 @@ export default {
},
methods: {
/**
* Malay short month + year for "Sehingga …" labels (defaults to previous month).
*/
formatMalayMonthYear(date) {
const d = date ? new Date(date) : new Date();
// End of previous calendar month
d.setDate(0);
const months = [
"Jan", "Feb", "Mac", "Apr", "Mei", "Jun",
"Jul", "Ogo", "Sep", "Okt", "Nov", "Dis"
];
return months[d.getMonth()] + " " + d.getFullYear();
},
async fetchPortalSettings() {
try {
this.util.setAuthorization();
@@ -51,7 +51,33 @@
</div>
</transition>
<div class="sso-only-notice">
<!-- Local / development: OTP login via IC number -->
<form v-if="isLocalDev" @submit.prevent="login" id="login_form" class="login-form" novalidate>
<div class="form-group mb-3">
<label class="login-label" for="no_kp">No. Kad Pengenalan</label>
<div class="login-field">
<span class="login-field-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" focusable="false">
<path
d="M4 7.5A3.5 3.5 0 0 1 7.5 4h9A3.5 3.5 0 0 1 20 7.5v9A3.5 3.5 0 0 1 16.5 20h-9A3.5 3.5 0 0 1 4 16.5v-9Zm3.5-1.5A1.5 1.5 0 0 0 6 7.5v9A1.5 1.5 0 0 0 7.5 18h9a1.5 1.5 0 0 0 1.5-1.5v-9A1.5 1.5 0 0 0 16.5 6h-9Zm1.5 3h6v2H9V9Zm0 4h10v2H9v-2Z" />
</svg>
</span>
<input v-model.trim="no_kp" @input="onNoKpInput" :disabled="loading" type="text" id="no_kp"
name="no_kp" class="form-control login-input" placeholder="Contoh: 901010101010"
autocomplete="off" inputmode="numeric" enterkeyhint="go" required />
</div>
<small class="login-help">Masukkan tanpa dash (-) atau jarak.</small>
</div>
<button type="submit" class="btn login-btn" :disabled="loading || !no_kp">
<span v-if="loading" class="spinner-border spinner-border-sm mr-2" role="status"
aria-hidden="true"></span>
<span>{{ loading ? 'Sedang log masuk…' : 'Masuk' }}</span>
</button>
</form>
<!-- Production: SSO only -->
<div v-else class="sso-only-notice">
<div class="sso-only-notice__icon" aria-hidden="true">
<svg viewBox="0 0 24 24" focusable="false">
<path
@@ -73,12 +99,15 @@
<script>
const SSO_LOADING_MS = 2500;
const SSO_SUCCESS_MS = 2500;
const LOCAL_ENVS = ['local', 'development'];
export default {
data() {
return {
SSO_LOADING_MS,
SSO_SUCCESS_MS,
loading: false,
no_kp: '',
ssoAlert: {
visible: false,
title: '',
@@ -96,6 +125,13 @@ export default {
};
},
computed: {
isLocalDev() {
const env = (window.config && window.config.env) || '';
return LOCAL_ENVS.indexOf(String(env).toLowerCase()) !== -1;
}
},
beforeDestroy() {
if (this.ssoRedirectTimer) {
window.clearTimeout(this.ssoRedirectTimer);
@@ -112,6 +148,45 @@ export default {
},
methods: {
onNoKpInput() {
this.no_kp = (this.no_kp || '').replace(/[\s-]+/g, '').replace(/[^\d]/g, '');
},
login() {
if (!this.isLocalDev) {
return;
}
this.startLoading();
const noKp = (this.no_kp || '').trim();
axios
.post(config.API + 'voter/login', {
no_kp: noKp,
send_otp: false
})
.then(response => {
this.stopLoading();
if (this.util.showResult(response, 'success')) {
this.$router.push({
name: 'Voter Verify',
params: {
nokp: response.data.nokp,
notel: response.data.notel,
debug_otp: response.data.debug_otp || null
}
});
}
})
.catch(error => {
this.stopLoading();
this.util.showResult(error, 'error');
});
},
startLoading() {
this.loading = true;
},
stopLoading() {
this.loading = false;
},
handleSsoCallback() {
const params = new URLSearchParams(window.location.search);
const ssoError = params.get('sso_error');
@@ -723,4 +798,4 @@ export default {
max-width: 200px;
}
}
</style>
</style>
+7
View File
@@ -43,6 +43,7 @@ import KehadiranCalon from "./components/demo/admin/voter/kehadiran.vue";
import KehadiranCalonFizikal from "./components/demo/admin/voter/fizikal.vue";
import KehadiranCalonMaya from "./components/demo/admin/voter/maya.vue";
import ViewKehadiranCalon from "./components/demo/admin/voter/viewkehadiran.vue";
import ImportKeanggotaan from "./components/demo/admin/voter/import-keanggotaan.vue";
import AllowanceSummary from "./components/demo/admin/allowance/summary.vue";
import ManageNominee from "./components/demo/admin/nominee/nominee.vue";
@@ -294,6 +295,12 @@ export default [
name: "Ringkasan Elaun",
},
{
path: "import-keanggotaan",
component: ImportKeanggotaan,
name: "Import Keanggotaan",
},
{
path: "nominee",
component: ManageNominee,
+2 -1
View File
@@ -32,7 +32,8 @@
"API": "/api/v1/", //URL OF YOUR API LOCATED
"baseURL": "/", //URL OF YOUR WEBSITE
"storageURL": "{{ config('app.cloudinary_enabled') ? '' : '/storage/' }}", //URL WHERE YOUR IMAGES and OTHER FILEs stored
"debug": {{ env('APP_DEBUG') }}
"debug": {{ env('APP_DEBUG') ? 'true' : 'false' }},
"env": @json(config('app.env'))
}
</script>
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
+1
View File
@@ -113,6 +113,7 @@ Route::prefix('v1')->group(function () { //Version 1 of my Rest API
Route::get('', 'API\v1\Voter\GetController');
Route::get('member-catalog', 'API\v1\Voter\AllMembersCatalogController');
Route::post('sync-applicable', 'API\v1\Voter\SyncApplicableVotersController');
Route::post('import-keanggotaan-json', 'API\v1\Voter\ImportKeanggotaanJsonController');
Route::post('{id}/verify-fizikal-registration', 'API\v1\Voter\VerifyFizikalRegistrationController');
Route::delete('{id}', 'API\v1\Voter\DeleteController');
Route::put('{id}', 'API\v1\Voter\UpdateController');