314 lines
9.9 KiB
Vue
314 lines
9.9 KiB
Vue
<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>
|