365 lines
19 KiB
PHP
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 | 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'; ?>
|