Files
E-Vote/resources/assets/js/components/demo/admin/voter/import-keanggotaan.vue
T

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>