Files
WebGIS-Poverty-MAP/admin/import.php
T

365 lines
19 KiB
PHP

<?php
// ================================================================
// Admin: Import Data Massal (CSV)
// ================================================================
require_once __DIR__ . '/../config/config.php';
require_once __DIR__ . '/../config/database.php';
require_once __DIR__ . '/../includes/auth.php';
require_once __DIR__ . '/../includes/functions.php';
requireLogin();
$pageTitle = 'Import Data';
require_once __DIR__ . '/../includes/header.php';
$db = getDB();
$ibadahList = $db->query("SELECT id, nama, jenis FROM rumah_ibadah ORDER BY nama")->fetchAll();
?>
<!-- ── Page Header ─────────────────────────────────────────── -->
<div class="page-header">
<div class="page-title-block">
<h1><i class="bi bi-upload"></i> Import Data</h1>
<div class="page-subtitle">Import massal data penduduk miskin atau rumah ibadah via CSV</div>
</div>
<div class="page-actions">
<a href="<?= BASE_URL ?>/admin/" class="btn btn-ghost btn-sm">
<i class="bi bi-arrow-left"></i> Kembali
</a>
</div>
</div>
<!-- ── Info Cards ─────────────────────────────────────────── -->
<div class="stats-row" style="margin-bottom:24px;">
<div class="stat-card blue" style="cursor:pointer;" onclick="setImportType('penduduk')">
<div class="stat-icon">👤</div>
<div class="stat-value" style="font-size:16px;">Penduduk Miskin</div>
<div class="stat-label">Import data KK</div>
</div>
<div class="stat-card green" style="cursor:pointer;" onclick="setImportType('ibadah')">
<div class="stat-icon">🏛️</div>
<div class="stat-value" style="font-size:16px;">Rumah Ibadah</div>
<div class="stat-label">Import data ibadah</div>
</div>
</div>
<!-- ── Import Card ─────────────────────────────────────────── -->
<div class="card" style="margin-bottom:20px;">
<div class="card-header">
<div class="card-title" id="import-type-title">📤 Pilih Jenis Import</div>
</div>
<div class="card-body" style="padding:24px;">
<!-- Type Selector -->
<div style="display:flex;gap:12px;margin-bottom:24px;flex-wrap:wrap;">
<button class="btn btn-primary btn-sm import-type-btn active" id="btn-type-penduduk" onclick="setImportType('penduduk')">
<i class="bi bi-people-fill"></i> Penduduk Miskin
</button>
<button class="btn btn-ghost btn-sm import-type-btn" id="btn-type-ibadah" onclick="setImportType('ibadah')">
<i class="bi bi-building"></i> Rumah Ibadah
</button>
</div>
<!-- Format Kolom Guide -->
<div id="format-penduduk">
<div class="card" style="background:rgba(91,127,255,0.06);border:1px solid rgba(91,127,255,0.2);margin-bottom:20px;">
<div class="card-body" style="padding:16px;">
<div style="font-weight:700;color:var(--accent);margin-bottom:10px;font-size:13px;">
<i class="bi bi-info-circle-fill"></i> Format CSV Penduduk Miskin
</div>
<div style="font-size:12px;color:var(--text2);line-height:1.8;">
Kolom (urutan wajib): <code>kk_nama, nik, jumlah_anggota, alamat, kelurahan, kecamatan, lat, lng, status_bantuan, jenis_bantuan, tanggal_bantuan, nominal_bantuan</code>
</div>
<div style="margin-top:10px;">
<strong style="font-size:12px;">Contoh baris:</strong><br>
<code style="font-size:11px;display:block;margin-top:4px;padding:8px;background:rgba(0,0,0,0.2);border-radius:6px;">
Ahmad Sulaiman,6171012345678901,4,Jl. Veteran No. 12,Akcaya,Pontianak Selatan,-0.0430,109.3340,belum,
</code>
</div>
<div style="margin-top:10px;font-size:11px;color:var(--text3);">
⚠️ status_bantuan: belum / sudah / menunggu &nbsp;|&nbsp; Baris pertama bisa berupa header (akan dideteksi otomatis)
</div>
<a href="<?= BASE_URL ?>/admin/template_penduduk.csv" class="btn btn-ghost btn-sm" style="margin-top:12px;" onclick="downloadTemplate('penduduk');return false;">
<i class="bi bi-download"></i> Download Template CSV
</a>
</div>
</div>
</div>
<div id="format-ibadah" style="display:none;">
<div class="card" style="background:rgba(34,197,94,0.06);border:1px solid rgba(34,197,94,0.2);margin-bottom:20px;">
<div class="card-body" style="padding:16px;">
<div style="font-weight:700;color:var(--green);margin-bottom:10px;font-size:13px;">
<i class="bi bi-info-circle-fill"></i> Format CSV Rumah Ibadah
</div>
<div style="font-size:12px;color:var(--text2);line-height:1.8;">
Kolom (urutan wajib): <code>nama, jenis, kontak, alamat, kelurahan, kecamatan, lat, lng, radius</code>
</div>
<div style="margin-top:10px;">
<strong style="font-size:12px;">Contoh baris:</strong><br>
<code style="font-size:11px;display:block;margin-top:4px;padding:8px;background:rgba(0,0,0,0.2);border-radius:6px;">
Masjid Al-Ikhlas,Masjid,0561-123456,Jl. Ahmad Yani,Akcaya,Pontianak Selatan,-0.0412,109.3325,300
</code>
</div>
<div style="margin-top:10px;font-size:11px;color:var(--text3);">
⚠️ jenis: Masjid / Musholla / Gereja / Katolik / Pura / Vihara / Klenteng / Lainnya
</div>
<a href="#" class="btn btn-ghost btn-sm" style="margin-top:12px;" onclick="downloadTemplate('ibadah');return false;">
<i class="bi bi-download"></i> Download Template CSV
</a>
</div>
</div>
</div>
<!-- Upload Form -->
<div id="upload-section">
<div class="form-group" style="margin-bottom:16px;">
<label class="form-label" style="font-weight:600;">Pilih File CSV <span style="color:var(--red)">*</span></label>
<div id="drop-zone" style="border:2px dashed var(--border);border-radius:12px;padding:32px;text-align:center;transition:all .3s;cursor:pointer;">
<div style="font-size:36px;margin-bottom:8px;">📂</div>
<div style="font-weight:600;margin-bottom:4px;">Drag & Drop file CSV di sini</div>
<div style="font-size:12px;color:var(--text3);margin-bottom:12px;">atau klik untuk browse file</div>
<input type="file" id="csv-file" accept=".csv,.txt" style="display:none;">
<button class="btn btn-outline-accent btn-sm" onclick="document.getElementById('csv-file').click()">
<i class="bi bi-folder2-open"></i> Browse File
</button>
</div>
<div id="file-info" style="display:none;margin-top:10px;padding:10px 14px;background:rgba(34,197,94,0.1);border:1px solid rgba(34,197,94,0.3);border-radius:8px;font-size:12px;">
<i class="bi bi-file-earmark-text" style="color:var(--green)"></i>
<span id="file-name"></span> — <span id="file-size"></span>
</div>
</div>
<div style="display:flex;gap:12px;flex-wrap:wrap;">
<button class="btn btn-outline-accent btn-sm" onclick="previewCSV()" id="btn-preview">
<i class="bi bi-eye"></i> Preview Data
</button>
<button class="btn btn-success btn-sm" onclick="doImport()" id="btn-import" disabled>
<i class="bi bi-cloud-upload"></i> Import Data
</button>
</div>
</div>
</div>
</div>
<!-- ── Preview Table ─────────────────────────────────────────── -->
<div id="preview-section" style="display:none;">
<div class="card">
<div class="card-header">
<div class="card-title">👁️ Preview Data (<span id="preview-count">0</span> baris)</div>
<div style="display:flex;gap:8px;">
<span id="preview-valid-badge" style="font-size:12px;color:var(--green);font-weight:700;"></span>
<span id="preview-err-badge" style="font-size:12px;color:var(--red);font-weight:700;"></span>
</div>
</div>
<div class="table-responsive" id="preview-table-wrap"></div>
</div>
</div>
<!-- ── Result Section ─────────────────────────────────────────── -->
<div id="result-section" style="display:none;margin-top:20px;">
<div class="card" id="result-card">
<div class="card-body" style="padding:24px;text-align:center;">
<div style="font-size:48px;margin-bottom:12px;" id="result-icon">✅</div>
<div style="font-size:18px;font-weight:700;margin-bottom:8px;" id="result-title">Import Selesai</div>
<div id="result-detail" style="font-size:14px;color:var(--text2);line-height:1.8;"></div>
<div style="display:flex;gap:12px;justify-content:center;margin-top:20px;flex-wrap:wrap;">
<button class="btn btn-ghost btn-sm" onclick="resetImport()"><i class="bi bi-arrow-repeat"></i> Import Lagi</button>
<a href="<?= BASE_URL ?>/admin/penduduk_miskin.php" class="btn btn-primary btn-sm" id="result-link">
<i class="bi bi-list-ul"></i> Lihat Data
</a>
</div>
</div>
</div>
</div>
<script>
const BASE = '<?= BASE_URL ?>';
let currentType = 'penduduk';
let parsedRows = [];
let hasHeader = false;
// ── Type Selector ──────────────────────────────────────────────
function setImportType(type) {
currentType = type;
document.getElementById('format-penduduk').style.display = type === 'penduduk' ? 'block' : 'none';
document.getElementById('format-ibadah').style.display = type === 'ibadah' ? 'block' : 'none';
document.getElementById('btn-type-penduduk').className = 'btn btn-sm import-type-btn ' + (type === 'penduduk' ? 'btn-primary' : 'btn-ghost');
document.getElementById('btn-type-ibadah').className = 'btn btn-sm import-type-btn ' + (type === 'ibadah' ? 'btn-primary' : 'btn-ghost');
document.getElementById('import-type-title').textContent = type === 'penduduk' ? '👤 Import Penduduk Miskin' : '🏛️ Import Rumah Ibadah';
resetPreview();
}
// ── File Input ─────────────────────────────────────────────────
const dropZone = document.getElementById('drop-zone');
const csvFile = document.getElementById('csv-file');
dropZone.addEventListener('click', () => csvFile.click());
dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.style.borderColor = 'var(--accent)'; });
dropZone.addEventListener('dragleave', () => { dropZone.style.borderColor = 'var(--border)'; });
dropZone.addEventListener('drop', e => {
e.preventDefault(); dropZone.style.borderColor = 'var(--border)';
const f = e.dataTransfer.files[0]; if (!f) return;
csvFile.files = e.dataTransfer.files; showFileInfo(f);
});
csvFile.addEventListener('change', () => { if (csvFile.files[0]) showFileInfo(csvFile.files[0]); });
function showFileInfo(f) {
document.getElementById('file-info').style.display = 'block';
document.getElementById('file-name').textContent = f.name;
document.getElementById('file-size').textContent = (f.size / 1024).toFixed(1) + ' KB';
resetPreview();
}
// ── CSV Parser ─────────────────────────────────────────────────
function parseCSV(text) {
const lines = text.split(/\r?\n/).filter(l => l.trim());
return lines.map(line => {
const cols = [];
let cur = '', inQ = false;
for (let i = 0; i < line.length; i++) {
const ch = line[i];
if (ch === '"') { inQ = !inQ; }
else if (ch === ',' && !inQ) { cols.push(cur.trim()); cur = ''; }
else { cur += ch; }
}
cols.push(cur.trim());
return cols;
});
}
// ── Preview ────────────────────────────────────────────────────
async function previewCSV() {
const f = csvFile.files[0];
if (!f) { Swal.fire('Error', 'Pilih file CSV terlebih dahulu!', 'error'); return; }
const text = await f.text();
const rows = parseCSV(text);
if (!rows.length) { Swal.fire('Error', 'File kosong atau format tidak valid', 'error'); return; }
const COLS_P = ['kk_nama','nik','jumlah_anggota','alamat','kelurahan','kecamatan','lat','lng','status_bantuan','jenis_bantuan','tanggal_bantuan','nominal_bantuan'];
const COLS_I = ['nama','jenis','kontak','alamat','kelurahan','kecamatan','lat','lng','radius'];
const expectedCols = currentType === 'penduduk' ? COLS_P : COLS_I;
// Deteksi header
const first = rows[0].join(',').toLowerCase();
hasHeader = first.includes('nama') || first.includes('kk_nama') || first.includes('lat');
parsedRows = hasHeader ? rows.slice(1) : rows;
// Build preview table
let valid = 0, errors = 0;
let html = '<table class="admin-table"><thead><tr>';
expectedCols.forEach(c => html += `<th>${c}</th>`);
html += '<th>Status</th></tr></thead><tbody>';
parsedRows.slice(0, 50).forEach((row, idx) => {
const ok = row.length >= (currentType === 'penduduk' ? 8 : 8) && row[currentType === 'penduduk' ? 6 : 6] && row[currentType === 'penduduk' ? 7 : 7];
if (ok) valid++; else errors++;
html += `<tr style="${ok ? '' : 'background:rgba(239,68,68,0.08);'}">`;
for (let i = 0; i < expectedCols.length; i++) {
const val = row[i] || '';
html += `<td style="font-size:11px;max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${val.replace(/"/g, '')}">${val || '<span style="color:var(--text3)">—</span>'}</td>`;
}
html += `<td>${ok ? '<span style="color:var(--green);font-size:11px;">✅ OK</span>' : '<span style="color:var(--red);font-size:11px;">❌ Error</span>'}</td></tr>`;
});
html += '</tbody></table>';
document.getElementById('preview-table-wrap').innerHTML = html;
document.getElementById('preview-count').textContent = parsedRows.length;
document.getElementById('preview-valid-badge').textContent = `✅ Valid: ${valid}`;
document.getElementById('preview-err-badge').textContent = errors ? `❌ Error: ${errors}` : '';
document.getElementById('preview-section').style.display = 'block';
document.getElementById('btn-import').disabled = valid === 0;
document.getElementById('result-section').style.display = 'none';
}
// ── Do Import ─────────────────────────────────────────────────
async function doImport() {
const f = csvFile.files[0];
if (!f || !parsedRows.length) return;
const res = await Swal.fire({
title: 'Mulai Import?',
html: `Import <b>${parsedRows.length}</b> baris data <b>${currentType === 'penduduk' ? 'Penduduk Miskin' : 'Rumah Ibadah'}</b>?`,
icon: 'question', showCancelButton: true,
confirmButtonText: 'Ya, Import!', cancelButtonText: 'Batal',
background: '#161b27', color: '#e8ecf4'
});
if (!res.isConfirmed) return;
document.getElementById('btn-import').disabled = true;
document.getElementById('btn-import').innerHTML = '<span class="spinner-border spinner-border-sm"></span> Mengimport...';
const fd = new FormData();
fd.append('file', f);
fd.append('type', currentType);
try {
const resp = await fetch(`${BASE}/api/import.php`, { method: 'POST', body: fd });
const json = await resp.json();
document.getElementById('result-section').style.display = 'block';
document.getElementById('preview-section').style.display = 'none';
if (json.success) {
document.getElementById('result-icon').textContent = '✅';
document.getElementById('result-title').textContent = 'Import Berhasil!';
document.getElementById('result-detail').innerHTML = `
<div style="display:flex;justify-content:center;gap:24px;flex-wrap:wrap;margin-top:10px;">
<div><span style="color:var(--green);font-size:24px;font-weight:800;">${json.imported}</span><br>Berhasil diimpor</div>
<div><span style="color:var(--red);font-size:24px;font-weight:800;">${json.failed}</span><br>Gagal</div>
<div><span style="color:var(--text2);font-size:24px;font-weight:800;">${json.skipped||0}</span><br>Dilewati</div>
</div>
${json.errors?.length ? '<div style="margin-top:14px;font-size:11px;color:var(--red);text-align:left;">'+json.errors.map(e=>`• ${e}`).join('<br>')+'</div>' : ''}`;
document.getElementById('result-link').href = currentType === 'penduduk'
? `${BASE}/admin/penduduk_miskin.php`
: `${BASE}/admin/rumah_ibadah.php`;
} else {
document.getElementById('result-icon').textContent = '❌';
document.getElementById('result-title').textContent = 'Import Gagal';
document.getElementById('result-detail').textContent = json.message || 'Terjadi kesalahan saat import.';
}
} catch (e) {
Swal.fire('Error', 'Gagal mengirim data ke server', 'error');
} finally {
document.getElementById('btn-import').disabled = false;
document.getElementById('btn-import').innerHTML = '<i class="bi bi-cloud-upload"></i> Import Data';
}
}
// ── Reset ─────────────────────────────────────────────────────
function resetPreview() {
parsedRows = [];
document.getElementById('preview-section').style.display = 'none';
document.getElementById('result-section').style.display = 'none';
document.getElementById('btn-import').disabled = true;
}
function resetImport() {
csvFile.value = '';
document.getElementById('file-info').style.display = 'none';
resetPreview();
}
// ── Download Template ──────────────────────────────────────────
function downloadTemplate(type) {
let content, filename;
if (type === 'penduduk') {
content = 'kk_nama,nik,jumlah_anggota,alamat,kelurahan,kecamatan,lat,lng,status_bantuan,jenis_bantuan,tanggal_bantuan,nominal_bantuan\r\n';
content += 'Ahmad Sulaiman,6171012345678901,4,Jl. Veteran No. 12,Akcaya,Pontianak Selatan,-0.0430,109.3340,belum,,,\r\n';
content += 'Budi Santoso,,3,Jl. Imam Bonjol No. 5,Dalam Bugis,Pontianak Kota,-0.0270,109.3440,sudah,Sembako,2026-06-10,500000\r\n';
filename = 'template_penduduk_miskin.csv';
} else {
content = 'nama,jenis,kontak,alamat,kelurahan,kecamatan,lat,lng,radius\r\n';
content += 'Masjid Al-Ikhlas,Masjid,0561-123456,Jl. Ahmad Yani,Akcaya,Pontianak Selatan,-0.0412,109.3325,300\r\n';
content += 'Gereja Bethel,Gereja,0561-654321,Jl. Diponegoro,Dalam Bugis,Pontianak Kota,-0.0280,109.3450,350\r\n';
filename = 'template_rumah_ibadah.csv';
}
const blob = new Blob([content], { type: 'text/csv;charset=utf-8;' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = filename;
a.click();
}
</script>
<?php require_once __DIR__ . '/../includes/footer.php'; ?>