feat: add CSV import for ibadah and miskin (admin-only, round-trip format)

This commit is contained in:
GuavaPopper
2026-06-03 21:00:28 +07:00
parent 6d157a17c9
commit 2d4533683d
3 changed files with 174 additions and 0 deletions
+101
View File
@@ -0,0 +1,101 @@
<?php
namespace App\Http\Controllers;
use App\Models\ActivityLog;
use App\Models\RumahIbadah;
use App\Models\RumahMiskin;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class ImportController extends Controller
{
public function ibadah(Request $request)
{
$request->validate(['file' => 'required|file|mimes:csv,txt|max:5120']);
$rows = $this->parseCsv($request->file('file')->getRealPath());
$imported = 0; $failed = 0;
DB::beginTransaction();
try {
foreach ($rows as $r) {
// [0]=ID(diabaikan) [1]=Nama [2]=Jenis [3]=Alamat [4]=Radius [5]=Jemaah [6]=Pengurus [7]=Telepon [8]=Lat [9]=Lon
if (count($r) < 10 || trim($r[1]) === '' || !is_numeric($r[8]) || !is_numeric($r[9])) { $failed++; continue; }
RumahIbadah::create([
'user_id' => auth()->id(),
'nama' => $r[1],
'jenis' => $r[2] ?: null,
'alamat' => $r[3] ?: null,
'radius' => is_numeric($r[4]) ? (int) $r[4] : 500,
'jumlah_jemaah' => is_numeric($r[5]) ? (int) $r[5] : null,
'pengurus' => $r[6] ?: null,
'telepon' => $r[7] ?: null,
'latitude' => (float) $r[8],
'longitude' => (float) $r[9],
]);
$imported++;
}
DB::commit();
} catch (\Throwable $e) {
DB::rollBack();
return response()->json(['message' => 'Import gagal: ' . $e->getMessage()], 422);
}
ActivityLog::record('import', "Import CSV rumah ibadah: {$imported} berhasil, {$failed} gagal", 'ibadah');
return response()->json(['success' => true, 'imported' => $imported, 'failed' => $failed]);
}
public function miskin(Request $request)
{
$request->validate(['file' => 'required|file|mimes:csv,txt|max:5120']);
$rows = $this->parseCsv($request->file('file')->getRealPath());
$imported = 0; $failed = 0;
DB::beginTransaction();
try {
foreach ($rows as $r) {
// [0]=ID Rumah [1]=Nama KK [2]=Alamat [3]=KK [4]=Jiwa [5]=Pekerjaan [6]=Pendapatan [7]=Kondisi [8]=Lat [9]=Lon
if (count($r) < 10 || trim($r[0]) === '' || !is_numeric($r[8]) || !is_numeric($r[9])) { $failed++; continue; }
RumahMiskin::updateOrCreate(
['id_rumah' => $r[0]],
[
'nama_kk' => $r[1] ?: null,
'alamat' => $r[2] ?: null,
'jumlah_kk' => is_numeric($r[3]) ? (int) $r[3] : 1,
'jumlah_orang' => is_numeric($r[4]) ? (int) $r[4] : 1,
'pekerjaan' => $r[5] ?: null,
'pendapatan' => is_numeric($r[6]) ? (int) $r[6] : null,
'kondisi_rumah' => $r[7] ?: null,
'latitude' => (float) $r[8],
'longitude' => (float) $r[9],
]
);
$imported++;
}
DB::commit();
} catch (\Throwable $e) {
DB::rollBack();
return response()->json(['message' => 'Import gagal: ' . $e->getMessage()], 422);
}
ActivityLog::record('import', "Import CSV keluarga miskin: {$imported} berhasil, {$failed} gagal", 'miskin');
return response()->json(['success' => true, 'imported' => $imported, 'failed' => $failed]);
}
/** Baca CSV, lewati baris header, kembalikan array baris data. */
private function parseCsv(string $path): array
{
$rows = [];
$handle = fopen($path, 'r');
$first = true;
while (($data = fgetcsv($handle)) !== false) {
if ($first) { $first = false; continue; } // skip header
if (count($data) === 1 && trim((string) $data[0]) === '') continue; // skip blank
$rows[] = $data;
}
fclose($handle);
return $rows;
}
}
+69
View File
@@ -103,6 +103,11 @@
<i class="fa-solid fa-file-pdf text-error"></i> Ekspor PDF
</a>
@endif
@if(auth()->user()->hasRole('administrator'))
<button onclick="document.getElementById('modal_import').showModal()" class="btn btn-md btn-outline border-base-300 gap-2">
<i class="fa-solid fa-file-import text-info"></i> Import CSV
</button>
@endif
<a href="{{ url('/map') }}" class="btn btn-md btn-primary shadow-lg gap-2">
<i class="fa-solid fa-plus"></i> Tambah Data
</a>
@@ -317,6 +322,33 @@
</div>
</dialog>
<!-- Modal: Import CSV -->
<dialog id="modal_import" class="modal">
<div class="modal-box w-11/12 max-w-md">
<h3 class="font-bold text-lg mb-4"><i class="fa-solid fa-file-import mr-2 text-info"></i>Import Data dari CSV</h3>
<div class="form-control mb-3">
<label class="label"><span class="label-text font-bold">Jenis Data</span></label>
<select id="import_type" class="select select-bordered w-full">
<option value="miskin">Keluarga Miskin</option>
<option value="ibadah">Rumah Ibadah</option>
</select>
</div>
<div class="form-control mb-3">
<label class="label"><span class="label-text font-bold">File CSV</span></label>
<input id="import_file" type="file" accept=".csv,text/csv" class="file-input file-input-bordered w-full">
</div>
<div class="text-xs text-base-content/60 bg-base-200 p-3 rounded">
<strong>Format kolom (sama dengan hasil Ekspor CSV):</strong><br>
<span id="import_format_hint"></span>
</div>
<div class="modal-action">
<button onclick="document.getElementById('modal_import').close()" class="btn btn-ghost">Batal</button>
<button onclick="submitImport()" class="btn btn-info text-white"><i class="fa-solid fa-upload mr-1"></i>Import</button>
</div>
</div>
<form method="dialog" class="modal-backdrop"><button>close</button></form>
</dialog>
<!-- Toast Container -->
<div class="toast toast-end toast-bottom z-[9999]" id="toastBox"></div>
@@ -555,6 +587,43 @@
}
}
// ── CSV IMPORT ──────────────────────────────────────────────
const importHints = {
miskin: 'ID Rumah, Nama KK, Alamat, KK, Jiwa, Pekerjaan, Pendapatan, Kondisi, Lat, Lon',
ibadah: 'ID (diabaikan), Nama, Jenis, Alamat, Radius, Jemaah, Pengurus, Telepon, Lat, Lon',
};
document.getElementById('import_type')?.addEventListener('change', e => {
document.getElementById('import_format_hint').textContent = importHints[e.target.value];
});
document.addEventListener('DOMContentLoaded', () => {
const h = document.getElementById('import_format_hint');
if (h) h.textContent = importHints.miskin;
});
async function submitImport() {
const type = document.getElementById('import_type').value;
const fileInput = document.getElementById('import_file');
if (!fileInput.files.length) { showToast('Pilih file CSV dulu', 'error'); return; }
const fd = new FormData();
fd.append('file', fileInput.files[0]);
const res = await fetch(`{{ url('/api/admin/import') }}/${type}`, {
method: 'POST',
headers: { 'X-CSRF-TOKEN': csrfToken },
body: fd,
});
const json = await res.json();
if (json.success) {
document.getElementById('modal_import').close();
fileInput.value = '';
showToast(`Import selesai: ${json.imported} berhasil, ${json.failed} gagal`, 'success');
loadAllData();
} else {
showToast(json.message || 'Import gagal', 'error');
}
}
// ── BULK DELETE ─────────────────────────────────────────────
function toggleSelect(type, id, checked) {
const set = type === 'ibadah' ? selectedIbadah : selectedMiskin;
+4
View File
@@ -4,6 +4,7 @@ use App\Http\Controllers\AdminUserController;
use App\Http\Controllers\AuthController;
use App\Http\Controllers\ExportController;
use App\Http\Controllers\IbadahController;
use App\Http\Controllers\ImportController;
use App\Http\Controllers\MiskinController;
use Illuminate\Support\Facades\Route;
@@ -60,6 +61,9 @@ Route::middleware('auth')->group(function () {
Route::post('/ibadah/bulk-delete', [IbadahController::class, 'bulkDestroy']);
Route::post('/miskin/bulk-delete', [MiskinController::class, 'bulkDestroy']);
Route::post('/import/ibadah', [ImportController::class, 'ibadah']);
Route::post('/import/miskin', [ImportController::class, 'miskin']);
});
});
});