feat: implement map visualization, layer management, and searching for poverty data records
This commit is contained in:
@@ -552,6 +552,237 @@ function updateSemuaWarnaMiskin() {
|
||||
});
|
||||
}
|
||||
|
||||
// ==========================
|
||||
// ===== Import Feature =====
|
||||
// ==========================
|
||||
let parsedImportData = [];
|
||||
|
||||
window.openImportMiskinModal = function() {
|
||||
parsedImportData = []; // Reset data
|
||||
|
||||
const bodyHTML = `
|
||||
<div class="form-group">
|
||||
<label>Pilih File (CSV, JSON, atau GeoJSON)</label>
|
||||
<input type="file" id="importMiskinFile" accept=".csv,.json,.geojson" style="display:block; margin-bottom:10px; width:100%;">
|
||||
<div style="font-size:12px; color:#666; margin-bottom:15px; line-height:1.4;">
|
||||
<strong>Format CSV yang didukung:</strong><br>
|
||||
Header minimal berisi: <code>nama</code>, <code>kategori_bantuan</code> (atau <code>kategori</code>), <code>jumlah_jiwa</code>, <code>lat</code>, <code>lng</code>.<br>
|
||||
Pemisah: Koma (<code>,</code>), Titik Koma (<code>;</code>), atau Tab.
|
||||
</div>
|
||||
</div>
|
||||
<div id="importPreviewContainer" style="display:none; margin-top: 15px;">
|
||||
<h4 style="margin:0 0 8px 0; font-size: 13px; color: #333; display: flex; justify-content: space-between;">
|
||||
<span>Pratinjau Data (<span id="importPreviewCount">0</span> item)</span>
|
||||
<span id="importPreviewStatus" style="font-weight: normal; font-size: 11px;"></span>
|
||||
</h4>
|
||||
<div style="max-height:180px; overflow-y:auto; border:1px solid #e2e8f0; border-radius:6px; background:#f8fafc;">
|
||||
<table style="width:100%; border-collapse:collapse; font-size:11px; text-align:left;">
|
||||
<thead>
|
||||
<tr style="background:#f1f5f9; border-bottom:1px solid #cbd5e1; color:#475569; position: sticky; top: 0;">
|
||||
<th style="padding:6px 8px;">Nama</th>
|
||||
<th style="padding:6px 8px;">Kategori</th>
|
||||
<th style="padding:6px 8px;">Jiwa</th>
|
||||
<th style="padding:6px 8px;">Koordinat</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="importPreviewTableBody" style="color:#334155;"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div id="importErrorMsg" style="display:none; margin-top:10px; color:#ef4444; font-size:12px; font-weight: 500;"></div>
|
||||
`;
|
||||
|
||||
map.closePopup();
|
||||
|
||||
openModal("Impor Penduduk Miskin", bodyHTML, function() {
|
||||
if (parsedImportData.length === 0) {
|
||||
showToast('Silakan pilih file terlebih dahulu atau pastikan data valid.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// Kirim data ke API bulk_create.php
|
||||
fetch('api/penduduk_miskin/bulk_create.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(parsedImportData)
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
closeModal();
|
||||
loadPendudukMiskin();
|
||||
showToast(data.message, 'success');
|
||||
} else {
|
||||
showToast(data.message || 'Gagal mengimpor data.', 'error');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
showToast('Terjadi kesalahan saat menghubungi server.', 'error');
|
||||
});
|
||||
});
|
||||
|
||||
// Register file input listener
|
||||
document.getElementById('importMiskinFile').addEventListener('change', handleImportFileChange);
|
||||
};
|
||||
|
||||
function parseCSV(text) {
|
||||
const lines = text.split(/\r?\n/);
|
||||
if (lines.length < 2) return [];
|
||||
|
||||
const firstLine = lines[0];
|
||||
let delimiter = ',';
|
||||
if (firstLine.includes(';')) delimiter = ';';
|
||||
else if (firstLine.includes('\t')) delimiter = '\t';
|
||||
|
||||
const headers = firstLine.split(delimiter).map(h => h.trim().replace(/^["']|["']$/g, '').toLowerCase());
|
||||
|
||||
const results = [];
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
if (!line) continue;
|
||||
|
||||
let fields = [];
|
||||
let currentField = '';
|
||||
let insideQuote = false;
|
||||
|
||||
for (let j = 0; j < line.length; j++) {
|
||||
const char = line[j];
|
||||
if (char === '"' || char === "'") {
|
||||
insideQuote = !insideQuote;
|
||||
} else if (char === delimiter && !insideQuote) {
|
||||
fields.push(currentField.trim());
|
||||
currentField = '';
|
||||
} else {
|
||||
currentField += char;
|
||||
}
|
||||
}
|
||||
fields.push(currentField.trim());
|
||||
|
||||
fields = fields.map(f => f.replace(/^["']|["']$/g, ''));
|
||||
|
||||
const row = {};
|
||||
headers.forEach((header, index) => {
|
||||
row[header] = fields[index] || '';
|
||||
});
|
||||
results.push(row);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
function handleImportFileChange(e) {
|
||||
const file = e.target.files[0];
|
||||
const previewContainer = document.getElementById('importPreviewContainer');
|
||||
const previewCount = document.getElementById('importPreviewCount');
|
||||
const previewTableBody = document.getElementById('importPreviewTableBody');
|
||||
const errorMsg = document.getElementById('importErrorMsg');
|
||||
|
||||
if (!file) {
|
||||
previewContainer.style.display = 'none';
|
||||
parsedImportData = [];
|
||||
return;
|
||||
}
|
||||
|
||||
errorMsg.style.display = 'none';
|
||||
previewContainer.style.display = 'none';
|
||||
previewTableBody.innerHTML = '';
|
||||
parsedImportData = [];
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(event) {
|
||||
try {
|
||||
const text = event.target.result;
|
||||
let rawData = [];
|
||||
|
||||
if (file.name.endsWith('.csv')) {
|
||||
rawData = parseCSV(text);
|
||||
} else if (file.name.endsWith('.json') || file.name.endsWith('.geojson')) {
|
||||
const json = JSON.parse(text);
|
||||
if (json.type === 'FeatureCollection' && Array.isArray(json.features)) {
|
||||
rawData = json.features.map(f => {
|
||||
const props = f.properties || {};
|
||||
const coords = f.geometry && f.geometry.coordinates;
|
||||
return {
|
||||
nama: props.nama || props.name || props.penduduk || '',
|
||||
kategori_bantuan: props.kategori_bantuan || props.kategori || props.bantuan || 'Makan',
|
||||
jumlah_jiwa: props.jumlah_jiwa || props.jumlah || props.jiwa || 1,
|
||||
lat: coords ? coords[1] : (props.lat || props.latitude),
|
||||
lng: coords ? coords[0] : (props.lng || props.longitude)
|
||||
};
|
||||
});
|
||||
} else if (Array.isArray(json)) {
|
||||
rawData = json;
|
||||
} else {
|
||||
throw new Error("Format JSON tidak didukung.");
|
||||
}
|
||||
}
|
||||
|
||||
if (rawData.length === 0) {
|
||||
errorMsg.textContent = "File kosong atau format tidak didukung.";
|
||||
errorMsg.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
const validRows = [];
|
||||
let invalidCount = 0;
|
||||
|
||||
rawData.forEach(row => {
|
||||
const nama = row.nama || row.name || row.penduduk || row['nama lengkap'] || row.fullname || '';
|
||||
const kategori_bantuan = row.kategori_bantuan || row.kategori || row.bantuan || row['kategori bantuan'] || 'Makan';
|
||||
const jumlah_jiwa = parseInt(row.jumlah_jiwa || row.jumlah || row.jiwa || row['jumlah jiwa'] || 1, 10) || 1;
|
||||
const lat = parseFloat(row.lat || row.latitude || row.y);
|
||||
const lng = parseFloat(row.lng || row.longitude || row.long || row.x);
|
||||
|
||||
const isValid = !isNaN(lat) && !isNaN(lng) && lat !== 0 && lng !== 0 && nama.trim() !== '';
|
||||
|
||||
if (isValid) {
|
||||
validRows.push({ nama, kategori_bantuan, jumlah_jiwa, lat, lng });
|
||||
} else {
|
||||
invalidCount++;
|
||||
}
|
||||
});
|
||||
|
||||
parsedImportData = validRows;
|
||||
|
||||
if (validRows.length === 0) {
|
||||
errorMsg.textContent = "Tidak ditemukan data valid (pastikan kolom nama, lat, dan lng terisi).";
|
||||
errorMsg.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
validRows.forEach(row => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.style.borderBottom = '1px solid #e2e8f0';
|
||||
tr.innerHTML = `
|
||||
<td style="padding:6px 8px; font-weight: 500;">${row.nama}</td>
|
||||
<td style="padding:6px 8px;">${row.kategori_bantuan}</td>
|
||||
<td style="padding:6px 8px;">${row.jumlah_jiwa}</td>
|
||||
<td style="padding:6px 8px; color: #64748b; font-family: monospace;">${row.lat.toFixed(5)}, ${row.lng.toFixed(5)}</td>
|
||||
`;
|
||||
previewTableBody.appendChild(tr);
|
||||
});
|
||||
|
||||
previewCount.textContent = validRows.length;
|
||||
previewContainer.style.display = 'block';
|
||||
|
||||
const statusEl = document.getElementById('importPreviewStatus');
|
||||
if (statusEl) {
|
||||
if (invalidCount > 0) {
|
||||
statusEl.innerHTML = `<span style="color:#e11d48;"><i class="fas fa-exclamation-triangle"></i> ${invalidCount} baris tidak valid diabaikan</span>`;
|
||||
} else {
|
||||
statusEl.innerHTML = `<span style="color:#16a34a;"><i class="fas fa-check-circle"></i> Semua data valid</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
errorMsg.textContent = "Gagal memproses file. Pastikan format file sesuai.";
|
||||
errorMsg.style.display = 'block';
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
// Initial Load
|
||||
loadRumahIbadah();
|
||||
loadPendudukMiskin();
|
||||
|
||||
Reference in New Issue
Block a user