diff --git a/app/Console/Commands/ResetBulananPenerimaFinal.php b/app/Console/Commands/ResetBulananPenerimaFinal.php new file mode 100644 index 0000000..6de5025 --- /dev/null +++ b/app/Console/Commands/ResetBulananPenerimaFinal.php @@ -0,0 +1,63 @@ +subMonth()->format('Y-m'); + $penerimas = PenerimaFinal::where('status_verifikasi', 'disetujui')->get(); + + if ($penerimas->isEmpty()) { + $this->info('Tidak ada data untuk diarsipkan.'); + return 0; + } + + // LANGKAH A — Arsipkan semua data dulu + foreach ($penerimas as $p) { + PenerimaFinalArsip::create([ + 'periode_arsip' => $periode, + 'penerima_final_id' => $p->id, + 'nama_lengkap' => $p->nama_lengkap, + 'nik' => $p->nik, + 'no_kk' => $p->no_kk, + 'nomor_rt' => $p->nomor_rt, + 'nama_dusun' => $p->nama_dusun, + 'pekerjaan' => $p->pekerjaan, + 'penghasilan' => $p->penghasilan, + 'jumlah_tanggungan' => $p->jumlah_tanggungan, + 'aset_kepemilikan' => $p->aset_kepemilikan, + 'bantuan_lain' => $p->bantuan_lain, + 'usia' => $p->usia, + 'probability' => $p->probability, + 'status_verifikasi' => $p->status_verifikasi, + 'tanggal_penetapan' => $p->tanggal_penetapan, + 'periode_bantuan' => $p->periode_bantuan, + 'jumlah_bantuan' => $p->jumlah_bantuan, + ]); + } + + // LANGKAH B — Kosongkan halaman admin + PenerimaFinal::where('status_verifikasi', 'disetujui')->delete(); + + // LANGKAH C — Kosongkan halaman RT + CalonPenerima::where('tracking_status', 'selesai') + ->update(['tracking_status' => 'belum_validasi']); + + Log::info("Reset bulanan selesai: {$penerimas->count()} data diarsipkan periode {$periode}."); + $this->info("Selesai! {$penerimas->count()} data diarsipkan ({$periode}). Admin & RT sudah kosong."); + + return 0; + } +} \ No newline at end of file diff --git a/app/Console/Commands/ResetPenerimaFinalBulanan.php b/app/Console/Commands/ResetPenerimaFinalBulanan.php new file mode 100644 index 0000000..9207695 --- /dev/null +++ b/app/Console/Commands/ResetPenerimaFinalBulanan.php @@ -0,0 +1,60 @@ +subMonth()->format('Y-m'); // arsip bulan sebelumnya + + $semua = PenerimaFinal::all(); + + if ($semua->isEmpty()) { + $this->info('Tidak ada data penerima_finals untuk diarsipkan.'); + return self::SUCCESS; + } + + DB::transaction(function () use ($semua, $periode) { + foreach ($semua as $p) { + RiwayatPenerima::create([ + 'periode' => $periode, + 'nama_lengkap' => $p->nama_lengkap, + 'nik' => $p->nik, + 'no_kk' => $p->no_kk, + 'rt_id' => $p->rt_id, + 'nomor_rt' => $p->nomor_rt, + 'nama_dusun' => $p->nama_dusun, + 'pekerjaan' => $p->pekerjaan, + 'penghasilan' => $p->penghasilan, + 'jumlah_tanggungan' => $p->jumlah_tanggungan, + 'aset_kepemilikan' => $p->aset_kepemilikan, + 'kondisi_rumah' => $p->kondisi_rumah, + 'meteran_listrik' => $p->meteran_listrik, + 'sumber_air' => $p->sumber_air, + 'bantuan_lain' => $p->bantuan_lain, + 'usia' => $p->usia, + 'probability' => $p->probability, + 'status_verifikasi' => $p->status_verifikasi, + 'tanggal_penetapan' => $p->tanggal_penetapan, + 'jumlah_bantuan' => $p->jumlah_bantuan, + 'di_arsipkan_pada' => now(), + ]); + } + + // Kosongkan tabel penerima_finals + PenerimaFinal::truncate(); + }); + + $this->info("Berhasil mengarsipkan {$semua->count()} data penerima ke periode {$periode}."); + return self::SUCCESS; + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Admin/FilterisasiController.php b/app/Http/Controllers/Admin/FilterisasiController.php index 7d99f15..d1686ef 100644 --- a/app/Http/Controllers/Admin/FilterisasiController.php +++ b/app/Http/Controllers/Admin/FilterisasiController.php @@ -6,6 +6,7 @@ use App\Models\CalonPenerima; use App\Models\Dusun; use App\Models\PenerimaFinal; +use App\Models\PeriodeDanaBlt; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; @@ -37,6 +38,7 @@ public function index(Request $request) $query = CalonPenerima::query() ->with(['rt.dusun', 'prediksiKelayakan']) ->whereIn('tracking_status', ['sedang_validasi', 'selesai']) + ->where('status_verifikasi', '!=', 'ditolak') ->whereHas('rt', function ($q) use ($dusunId) { $q->where('dusun_id', $dusunId); }); @@ -54,7 +56,59 @@ public function index(Request $request) ->pluck('nik'); } - return view('admin.filterisasi', compact('dusuns', 'dusunId', 'kuota', 'candidates', 'pickedNiks')); + // Ambil riwayat periode dana + $periodeDana = PeriodeDanaBlt::orderBy('id')->get(); + $periodeTerakhir = $periodeDana->last(); + $nextPeriodeNum = $periodeDana->count() + 1; + $danaSisaTerakhir = $periodeTerakhir ? $periodeTerakhir->dana_sisa : null; + + return view('admin.filterisasi', compact( + 'dusuns', 'dusunId', 'kuota', 'candidates', 'pickedNiks', + 'periodeDana', 'periodeTerakhir', 'nextPeriodeNum', 'danaSisaTerakhir' + )); + } + + public function simpanPeriode(Request $request) + { + $user = $request->user(); + + if (!$user || $user->role !== 'admin') { + abort(403, 'Akses ditolak: hanya Admin.'); + } + + $data = $request->validate([ + 'periode' => 'required|string|max:50', + 'dana_awal' => 'required|integer|min:1', + 'jumlah_penerima' => 'required|integer|min:1', + 'keterangan' => 'nullable|string|max:255', + ]); + + $danaTerpakai = $data['jumlah_penerima'] * 300000; + $danaSisa = $data['dana_awal'] - $danaTerpakai; + + PeriodeDanaBlt::create([ + 'periode' => $data['periode'], + 'dana_awal' => $data['dana_awal'], + 'jumlah_penerima' => $data['jumlah_penerima'], + 'dana_terpakai' => $danaTerpakai, + 'dana_sisa' => max(0, $danaSisa), + 'keterangan' => $data['keterangan'] ?? null, + ]); + + return back()->with('success', "Periode {$data['periode']} berhasil disimpan. Sisa dana: Rp " . number_format(max(0, $danaSisa), 0, ',', '.')); + } + + public function hapusPeriode(Request $request, $id) + { + $user = $request->user(); + + if (!$user || $user->role !== 'admin') { + abort(403, 'Akses ditolak: hanya Admin.'); + } + + PeriodeDanaBlt::findOrFail($id)->delete(); + + return back()->with('success', 'Data periode berhasil dihapus.'); } public function tetapkan(Request $request) @@ -79,6 +133,7 @@ public function tetapkan(Request $request) $allCandidates = CalonPenerima::query() ->with(['rt.dusun', 'prediksiKelayakan']) ->whereIn('tracking_status', ['sedang_validasi', 'selesai']) + ->where('status_verifikasi', '!=', 'ditolak') ->whereHas('rt', function ($r) use ($dusunId) { $r->where('dusun_id', $dusunId); }) @@ -166,6 +221,7 @@ public function resetDusun(Request $request) $r->where('dusun_id', $dusun->id); }) ->whereIn('tracking_status', ['sedang_validasi', 'selesai']) + ->where('status_verifikasi', '!=', 'ditolak') ->pluck('id') ->all(); diff --git a/app/Http/Controllers/Admin/LaporanController.php b/app/Http/Controllers/Admin/LaporanController.php index 828a184..450e53b 100644 --- a/app/Http/Controllers/Admin/LaporanController.php +++ b/app/Http/Controllers/Admin/LaporanController.php @@ -1,146 +1,220 @@ user(); + $nik = trim((string) $request->query('nik', '')); + $statusData = null; + $penerimaFinal = null; + $explanation = ['positive' => [], 'negative' => [], 'summary' => null]; - if (!$user || $user->role !== 'admin') { - abort(403, 'Akses ditolak: hanya Admin.'); + if ($nik !== '') { + $statusData = CalonPenerima::with(['rt.dusun', 'prediksiKelayakan']) + ->where('nik', $nik) + ->first(); + + if ($statusData) { + // ── Cari PenerimaFinal secara manual ── + // Coba berbagai kemungkinan foreign key / kolom pencarian + try { + $penerimaFinal = PenerimaFinal::where('nik', $nik)->first(); + } catch (\Exception $e) { + // Kolom 'nik' tidak ada, coba cara lain + } + + if (!$penerimaFinal) { + try { + $penerimaFinal = PenerimaFinal::where('calon_id', $statusData->id)->first(); + } catch (\Exception $e) {} + } + + if (!$penerimaFinal) { + try { + $penerimaFinal = PenerimaFinal::where('id_calon', $statusData->id)->first(); + } catch (\Exception $e) {} + } + + if (!$penerimaFinal) { + try { + $penerimaFinal = PenerimaFinal::where('user_id', $statusData->user_id)->first(); + } catch (\Exception $e) {} + } + + // ── Parse explanation dari prediksiKelayakan ── + if ($statusData->prediksiKelayakan) { + $pred = $statusData->prediksiKelayakan; + + $raw = $pred->explanation + ?? $pred->feature_explanation + ?? $pred->alasan + ?? null; + + if (is_string($raw) && !empty($raw)) { + $decoded = json_decode($raw, true); + if (is_array($decoded)) { + $explanation = [ + 'positive' => $decoded['positive'] ?? $decoded['pendukung'] ?? [], + 'negative' => $decoded['negative'] ?? $decoded['pengurang'] ?? [], + 'summary' => $decoded['summary'] ?? $decoded['ringkasan'] ?? null, + ]; + } + } elseif (is_array($raw)) { + $explanation = [ + 'positive' => $raw['positive'] ?? $raw['pendukung'] ?? [], + 'negative' => $raw['negative'] ?? $raw['pengurang'] ?? [], + 'summary' => $raw['summary'] ?? $raw['ringkasan'] ?? null, + ]; + } + + // Fallback field lain dari prediksi + if (empty($explanation['positive']) && empty($explanation['negative'])) { + foreach (['feature_importances', 'features', 'detail', 'data'] as $field) { + if (!empty($pred->$field)) { + $val = is_string($pred->$field) ? json_decode($pred->$field, true) : $pred->$field; + if (is_array($val) && isset($val['positive'])) { + $explanation = [ + 'positive' => $val['positive'] ?? [], + 'negative' => $val['negative'] ?? [], + 'summary' => $val['summary'] ?? null, + ]; + break; + } + } + } + } + + // ── GENERATE OTOMATIS dari data CalonPenerima ── + // Dijalankan jika explanation masih kosong setelah semua fallback + if (empty($explanation['positive']) && empty($explanation['negative'])) { + $cp = $statusData; + $positive = []; + $negative = []; + + // Pekerjaan + $pekerjaan = strtolower($cp->pekerjaan ?? ''); + if (in_array($pekerjaan, ['tidak bekerja', 'tidak ada', '-', ''])) { + $positive[] = 'Tidak bekerja / tidak memiliki penghasilan tetap'; + } elseif (in_array($pekerjaan, ['petani', 'buruh', 'nelayan', 'pemulung', 'pedagang kecil'])) { + $positive[] = 'Pekerjaan termasuk kategori rentan ('. ucfirst($pekerjaan) .')'; + } else { + $negative[] = 'Memiliki pekerjaan (' . ucfirst($pekerjaan) . ')'; + } + + // Penghasilan + $penghasilan = (float)($cp->penghasilan ?? 0); + if ($penghasilan == 0) { + $positive[] = 'Penghasilan Rp 0 (tidak ada penghasilan)'; + } elseif ($penghasilan < 500000) { + $positive[] = 'Penghasilan sangat rendah (Rp ' . number_format($penghasilan, 0, ',', '.') . '/bulan)'; + } elseif ($penghasilan < 1500000) { + $positive[] = 'Penghasilan di bawah UMR (Rp ' . number_format($penghasilan, 0, ',', '.') . '/bulan)'; + } else { + $negative[] = 'Penghasilan cukup (Rp ' . number_format($penghasilan, 0, ',', '.') . '/bulan)'; + } + + // Tanggungan + $tanggungan = (int)($cp->jumlah_tanggungan ?? 0); + if ($tanggungan >= 4) { + $positive[] = 'Jumlah tanggungan banyak (' . $tanggungan . ' orang)'; + } elseif ($tanggungan >= 2) { + $positive[] = 'Memiliki tanggungan ' . $tanggungan . ' orang'; + } else { + $negative[] = 'Tanggungan sedikit (' . $tanggungan . ' orang)'; + } + + // Aset + $aset = strtolower($cp->aset_kepemilikan ?? ''); + if (in_array($aset, ['tidak punya', 'tidak ada', 'tidak memiliki', '-', ''])) { + $positive[] = 'Tidak memiliki aset berarti'; + } else { + $negative[] = 'Memiliki aset kepemilikan (' . ucfirst($aset) . ')'; + } + + // Bantuan lain + $bantuan = strtolower($cp->bantuan_lain ?? ''); + if (in_array($bantuan, ['tidak', 'tidak ada', '-', ''])) { + $positive[] = 'Belum menerima bantuan sosial lain'; + } else { + $negative[] = 'Sudah menerima bantuan lain (' . $cp->bantuan_lain . ')'; + } + + // Usia + $usia = $cp->usia ?? ($cp->tanggal_lahir + ? \Carbon\Carbon::parse($cp->tanggal_lahir)->age + : null); + if ($usia !== null) { + if ($usia >= 60) { + $positive[] = 'Usia lanjut (' . $usia . ' tahun) — prioritas penerima'; + } elseif ($usia >= 50) { + $positive[] = 'Usia ' . $usia . ' tahun (termasuk kelompok rentan)'; + } elseif ($usia < 18) { + $positive[] = 'Usia ' . $usia . ' tahun (anak/remaja dalam keluarga)'; + } + } + + // Kondisi rumah + $kondisi = strtolower($cp->kondisi_rumah ?? ''); + if (in_array($kondisi, ['tidak layak', 'buruk', 'rusak', 'sangat buruk'])) { + $positive[] = 'Kondisi rumah tidak layak huni'; + } elseif (in_array($kondisi, ['sedang', 'cukup'])) { + // netral, tidak tambah ke mana-mana + } elseif (!empty($kondisi) && !in_array($kondisi, ['-', ''])) { + $negative[] = 'Kondisi rumah ' . ucfirst($kondisi); + } + + // Meteran listrik + $meteran = strtolower($cp->meteran_listrik ?? ''); + if (in_array($meteran, ['450va', '450 va'])) { + $positive[] = 'Meteran listrik 450VA (golongan sangat miskin)'; + } elseif (in_array($meteran, ['900va', '900 va'])) { + $positive[] = 'Meteran listrik 900VA (golongan miskin/subsidi)'; + } elseif (!empty($meteran) && !in_array($meteran, ['-', ''])) { + $negative[] = 'Meteran listrik ' . strtoupper($cp->meteran_listrik); + } + + // Sumber air + $air = strtolower($cp->sumber_air ?? ''); + if (in_array($air, ['sumur', 'sungai', 'mata air', 'tadah hujan'])) { + $positive[] = 'Sumber air ' . ucfirst($air) . ' (bukan PDAM)'; + } elseif (!empty($air) && !in_array($air, ['-', ''])) { + $negative[] = 'Sumber air ' . ucfirst($air); + } + + // Rekomendasi prediksi + $rek = strtolower($pred->recommendation ?? $pred->status ?? ''); + $isLayakPred = in_array($rek, ['layak', 'ya', 'yes', '1', 'true']); + + $explanation = [ + 'positive' => $positive, + 'negative' => $negative, + 'summary' => 'Nilai kelayakan dihitung berdasarkan pekerjaan, penghasilan, tanggungan, aset, bantuan lain, usia, kondisi rumah, meteran listrik, dan sumber air.' + . (count($positive) > 0 && count($negative) > 0 + ? ' Terdapat faktor pendukung dan pengurang.' + : (count($positive) > 0 ? ' Terdapat beberapa faktor pendukung.' : ' Tidak ada faktor yang signifikan.')), + ]; + } + } + } } - $laporans = PenerimaFinal::query() - ->where('status_verifikasi', 'disetujui') - ->orderByDesc('tanggal_penetapan') + $laporanPubliks = LaporanPublik::where('is_active', true) + ->latest() ->get(); - $laporanPubliks = LaporanPublik::latest()->get(); - - return view('admin.laporan', compact('laporans', 'laporanPubliks')); - } - - public function exportPdf(Request $request) - { - $user = $request->user(); - - if (!$user || $user->role !== 'admin') { - abort(403, 'Akses ditolak: hanya Admin.'); - } - - $laporans = PenerimaFinal::query() - ->where('status_verifikasi', 'disetujui') - ->orderByDesc('tanggal_penetapan') - ->get(); - - $pdf = Pdf::loadView('admin.laporan-pdf', compact('laporans')) - ->setPaper('a4', 'landscape'); - - return $pdf->download('laporan-penerima-blt-dd-kelurahan-ngerong.pdf'); - } - - public function exportExcel(Request $request) - { - $user = $request->user(); - - if (!$user || $user->role !== 'admin') { - abort(403, 'Akses ditolak: hanya Admin.'); - } - - return Excel::download(new PenerimaFinalExport, 'laporan-penerima-blt-dd-kelurahan-ngerong.xlsx'); - } - - public function kirimKeRt(Request $request) - { - $user = $request->user(); - - if (!$user || $user->role !== 'admin') { - abort(403, 'Akses ditolak: hanya Admin.'); - } - - $candidateIds = CalonPenerima::query() - ->whereIn('tracking_status', ['sedang_validasi']) - ->whereIn('status_verifikasi', ['disetujui', 'ditolak']) - ->pluck('id') - ->all(); - - if (!empty($candidateIds)) { - CalonPenerima::whereIn('id', $candidateIds)->update([ - 'tracking_status' => 'selesai', - ]); - } - - return back()->with('success', 'Hasil final berhasil dikirim ke RT dusun. RT sekarang hanya dapat melihat status diterima atau ditolak.'); - } - - public function uploadPublikPdf(Request $request) - { - $user = $request->user(); - - if (!$user || $user->role !== 'admin') { - abort(403, 'Akses ditolak: hanya Admin.'); - } - - $data = $request->validate([ - 'judul' => 'required|string|max:255', - 'file_pdf' => 'required|mimes:pdf|max:5120', - ]); - - $path = $request->file('file_pdf')->store('laporan-publik', 'public'); - - LaporanPublik::create([ - 'judul' => $data['judul'], - 'file_path' => $path, - 'uploaded_by' => $user->id, - 'is_active' => true, - ]); - - return back()->with('success', 'PDF laporan publik berhasil diupload.'); - } - - public function hapusPublikPdf(Request $request, $id) - { - $user = $request->user(); - - if (!$user || $user->role !== 'admin') { - abort(403, 'Akses ditolak: hanya Admin.'); - } - - $laporan = LaporanPublik::findOrFail($id); - - if ($laporan->file_path && Storage::disk('public')->exists($laporan->file_path)) { - Storage::disk('public')->delete($laporan->file_path); - } - - $laporan->delete(); - - return back()->with('success', 'PDF laporan publik berhasil dihapus.'); - } - - public function bersihkanRiwayatSelesai(Request $request) - { - $user = $request->user(); - - if (!$user || $user->role !== 'admin') { - abort(403, 'Akses ditolak: hanya Admin.'); - } - - $deleted = \App\Models\CalonPenerima::query() - ->where('tracking_status', 'selesai') - ->delete(); - - return back()->with('success', "Berhasil membersihkan {$deleted} data riwayat selesai. Arsip laporan tetap tersimpan."); + return view('landing', compact( + 'nik', + 'statusData', + 'penerimaFinal', + 'laporanPubliks', + 'explanation' + )); } } \ No newline at end of file diff --git a/app/Http/Controllers/LandingPageController.php b/app/Http/Controllers/LandingPageController.php index b130878..f54a9cf 100644 --- a/app/Http/Controllers/LandingPageController.php +++ b/app/Http/Controllers/LandingPageController.php @@ -10,24 +10,38 @@ class LandingPageController extends Controller { public function index(Request $request) { - $nik = trim((string) $request->query('nik','')); + $nik = trim((string) $request->query('nik', '')); $statusData = null; + $penerimaFinal = null; - if($nik !== ''){ + if ($nik !== '') { $statusData = CalonPenerima::with(['rt.dusun']) - ->where('nik',$nik) + ->where('nik', $nik) ->first(); + + // LOGIKA PENETAPAN: pakai kolom status_verifikasi yang benar-benar ada di tabel + if ($statusData && isset($statusData->status_verifikasi)) { + $currentStatus = strtolower($statusData->status_verifikasi); + + if (in_array($currentStatus, ['ditolak', 'disetujui'])) { + // Buat object penampung agar dibaca sebagai '$penerimaFinal' oleh file Blade Anda + $penerimaFinal = new \stdClass(); + $penerimaFinal->status = $statusData->status_verifikasi; + $penerimaFinal->created_at = $statusData->updated_at ?? now(); + } + } } - $laporanPubliks = LaporanPublik::where('is_active',true) + $laporanPubliks = LaporanPublik::where('is_active', true) ->latest() ->get(); - return view('landing',compact( + return view('landing', compact( 'nik', 'statusData', - 'laporanPubliks' + 'laporanPubliks', + 'penerimaFinal' )); } } \ No newline at end of file diff --git a/app/Http/Controllers/Rt/CalonPenerimaController.php b/app/Http/Controllers/Rt/CalonPenerimaController.php index b8eb948..9dc9684 100644 --- a/app/Http/Controllers/Rt/CalonPenerimaController.php +++ b/app/Http/Controllers/Rt/CalonPenerimaController.php @@ -7,11 +7,13 @@ use App\Models\PrediksiKelayakan; use App\Models\RT; use App\Services\MLPredictionService; +use Carbon\Carbon; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Storage; + class CalonPenerimaController extends Controller { protected MLPredictionService $mlService; @@ -58,35 +60,40 @@ public function store(Request $request) } $validated = $request->validate([ - 'rt_id' => 'nullable|exists:rts,id', - 'no_kk' => 'required|digits:16|unique:calon_penerimas,no_kk', - 'nik' => 'required|digits:16|unique:calon_penerimas,nik', - 'nama_lengkap' => 'required|string|max:255', - 'jenis_kelamin' => 'required|in:Laki-laki,Perempuan', - 'tempat_lahir' => 'required|string|max:255', - 'tanggal_lahir' => 'required|date', - 'alamat' => 'required|string', - 'desa' => 'nullable|string|max:255', - 'pekerjaan' => 'required|string', - 'penghasilan' => 'required|numeric|min:0', - 'jumlah_tanggungan' => 'required|integer|min:0', - 'aset_kepemilikan' => 'required|string', - 'bantuan_lain' => 'required|in:ya,tidak', - 'usia' => 'required|integer|min:17|max:100', - 'status_perkawinan' => 'required|string', - 'kondisi_rumah' => 'required|in:Layak,Sedang,Tidak Layak', - 'meteran_listrik' => 'required|in:450VA,900VA,1300VA+', - 'sumber_air' => 'required|in:PDAM,Sumur,Sungai', - // Upload foto (opsional) - 'foto_rumah_depan' => 'nullable|image|max:2048', - 'foto_rumah_belakang' => 'nullable|image|max:2048', - 'foto_rumah_kanan' => 'nullable|image|max:2048', - 'foto_rumah_kiri' => 'nullable|image|max:2048', - 'foto_kk' => 'nullable|image|max:2048', - 'foto_ktp' => 'nullable|image|max:2048', - 'foto_rekening_listrik' => 'nullable|mimes:jpg,jpeg,png,pdf|max:2048', - 'foto_meteran_air' => 'nullable|image|max:2048', - 'dokumen_pendukung' => 'nullable|mimes:jpg,jpeg,png,pdf,doc,docx|max:5120', + 'rt_id' => 'nullable|exists:rts,id', + 'no_kk' => 'required|digits:16|unique:calon_penerimas,no_kk', + 'nik' => 'required|digits:16|unique:calon_penerimas,nik', + 'nama_lengkap' => 'required|string|max:255', + 'jenis_kelamin' => 'required|in:Laki-laki,Perempuan', + 'tempat_lahir' => 'required|string|max:255', + 'tanggal_lahir' => 'required|date', + 'alamat' => 'required|string', + 'desa' => 'nullable|string|max:255', + 'pekerjaan' => 'required|string', + 'penghasilan' => 'required|numeric|min:0', + 'jumlah_tanggungan' => 'required|integer|min:0', + 'aset_pilihan' => 'nullable|array', + 'aset_pilihan.*' => 'nullable|string', + 'aset_manual' => 'nullable|string|max:255', + 'bantuan_lain' => 'required|in:ya,tidak', + 'usia' => 'required|integer|min:17|max:100', + 'status_perkawinan' => 'required|string', + 'lantai_rumah' => 'required|string', + 'dinding_rumah' => 'required|string', + 'atap_rumah' => 'required|string', + 'luas_rumah_m2' => 'required|integer|min:1', + 'status_kepemilikan_rumah'=> 'required|string', + 'meteran_listrik' => 'required|in:450VA,900VA,1300VA+', + 'sumber_air' => 'required|in:PDAM,Sumur,Sungai', + 'foto_rumah_depan' => 'nullable|image', + 'foto_rumah_belakang' => 'nullable|image', + 'foto_rumah_kanan' => 'nullable|image', + 'foto_rumah_kiri' => 'nullable|image', + 'foto_kk' => 'nullable|image', + 'foto_ktp' => 'nullable|image', + 'foto_rekening_listrik' => 'nullable|mimes:jpg,jpeg,png,pdf', + 'foto_meteran_air' => 'nullable|image', + 'dokumen_pendukung' => 'nullable|mimes:jpg,jpeg,png,pdf,doc,docx', ], [ 'nik.unique' => 'NIK sudah pernah diinput.', 'no_kk.unique' => 'No. KK sudah pernah diinput.', @@ -94,6 +101,17 @@ public function store(Request $request) $validated['rt_id'] = $myRtId; + $asetGabung = $this->buildAsetString( + $validated['aset_pilihan'] ?? [], + $validated['aset_manual'] ?? '' + ); + $validated['aset_kepemilikan'] = $asetGabung; + + $validated['kondisi_rumah'] = $this->deriveKondisiRumah( + $validated['lantai_rumah'], + $validated['dinding_rumah'] + ); + $rt = RT::with('dusun')->find($myRtId); if ($rt && $rt->dusun) { $validated['desa'] = $rt->dusun->nama_dusun; @@ -103,62 +121,81 @@ public function store(Request $request) DB::beginTransaction(); try { - // Handle upload foto - $fotoFields = [ + $fotoImages = [ 'foto_rumah_depan', 'foto_rumah_belakang', 'foto_rumah_kanan', 'foto_rumah_kiri', - 'foto_kk', 'foto_ktp', 'foto_rekening_listrik', 'foto_meteran_air', 'dokumen_pendukung', + 'foto_kk', 'foto_ktp', 'foto_meteran_air', ]; - foreach ($fotoFields as $field) { + $fotoFiles = [ + 'foto_rekening_listrik', 'dokumen_pendukung', + ]; + + foreach ($fotoImages as $field) { + if ($request->hasFile($field)) { + $validated[$field] = $this->simpanFotoKompres($request->file($field)); + } + } + foreach ($fotoFiles as $field) { if ($request->hasFile($field)) { $validated[$field] = $request->file($field)->store('dokumen-warga', 'public'); } } $calonPenerima = CalonPenerima::create([ - 'user_id' => $user->id, - 'rt_id' => $validated['rt_id'], - 'no_kk' => $validated['no_kk'], - 'nik' => $validated['nik'], - 'nama_lengkap' => $validated['nama_lengkap'], - 'jenis_kelamin' => $validated['jenis_kelamin'], - 'tempat_lahir' => $validated['tempat_lahir'], - 'tanggal_lahir' => $validated['tanggal_lahir'], - 'alamat' => $validated['alamat'], - 'desa' => $validated['desa'], - 'pekerjaan' => $validated['pekerjaan'], - 'penghasilan' => $validated['penghasilan'], - 'jumlah_tanggungan' => $validated['jumlah_tanggungan'], - 'aset_kepemilikan' => $validated['aset_kepemilikan'], - 'kondisi_rumah' => $validated['kondisi_rumah'], - 'meteran_listrik' => $validated['meteran_listrik'], - 'sumber_air' => $validated['sumber_air'], - 'bantuan_lain' => $validated['bantuan_lain'], - 'usia' => $validated['usia'], - 'status_perkawinan' => $validated['status_perkawinan'], - 'status_verifikasi' => 'pending', - 'tracking_status' => 'draft', - 'foto_rumah_depan' => $validated['foto_rumah_depan'] ?? null, - 'foto_rumah_belakang' => $validated['foto_rumah_belakang'] ?? null, - 'foto_rumah_kanan' => $validated['foto_rumah_kanan'] ?? null, - 'foto_rumah_kiri' => $validated['foto_rumah_kiri'] ?? null, - 'foto_kk' => $validated['foto_kk'] ?? null, - 'foto_ktp' => $validated['foto_ktp'] ?? null, - 'foto_rekening_listrik' => $validated['foto_rekening_listrik'] ?? null, - 'foto_meteran_air' => $validated['foto_meteran_air'] ?? null, - 'dokumen_pendukung' => $validated['dokumen_pendukung'] ?? null, + 'user_id' => $user->id, + 'rt_id' => $validated['rt_id'], + 'no_kk' => $validated['no_kk'], + 'nik' => $validated['nik'], + 'nama_lengkap' => $validated['nama_lengkap'], + 'jenis_kelamin' => $validated['jenis_kelamin'], + 'tempat_lahir' => $validated['tempat_lahir'], + 'tanggal_lahir' => $validated['tanggal_lahir'], + 'alamat' => $validated['alamat'], + 'desa' => $validated['desa'], + 'pekerjaan' => $validated['pekerjaan'], + 'penghasilan' => $validated['penghasilan'], + 'jumlah_tanggungan' => $validated['jumlah_tanggungan'], + 'aset_kepemilikan' => $validated['aset_kepemilikan'], + 'kondisi_rumah' => $validated['kondisi_rumah'], + 'lantai_rumah' => $validated['lantai_rumah'], + 'dinding_rumah' => $validated['dinding_rumah'], + 'atap_rumah' => $validated['atap_rumah'], + 'luas_rumah_m2' => $validated['luas_rumah_m2'], + 'status_kepemilikan_rumah' => $validated['status_kepemilikan_rumah'], + 'meteran_listrik' => $validated['meteran_listrik'], + 'sumber_air' => $validated['sumber_air'], + 'bantuan_lain' => $validated['bantuan_lain'], + 'usia' => $validated['usia'], + 'status_perkawinan' => $validated['status_perkawinan'], + 'status_verifikasi' => 'pending', + 'tracking_status' => 'draft', + 'periode_bulan' => Carbon::now('Asia/Jakarta')->startOfMonth()->toDateString(), + 'foto_rumah_depan' => $validated['foto_rumah_depan'] ?? null, + 'foto_rumah_belakang' => $validated['foto_rumah_belakang'] ?? null, + 'foto_rumah_kanan' => $validated['foto_rumah_kanan'] ?? null, + 'foto_rumah_kiri' => $validated['foto_rumah_kiri'] ?? null, + 'foto_kk' => $validated['foto_kk'] ?? null, + 'foto_ktp' => $validated['foto_ktp'] ?? null, + 'foto_rekening_listrik' => $validated['foto_rekening_listrik'] ?? null, + 'foto_meteran_air' => $validated['foto_meteran_air'] ?? null, + 'dokumen_pendukung' => $validated['dokumen_pendukung'] ?? null, ]); - // ML Prediction $predictionData = [ - 'pekerjaan' => $validated['pekerjaan'], - 'penghasilan' => $validated['penghasilan'], - 'jumlah_tanggungan' => $validated['jumlah_tanggungan'], - 'aset_kepemilikan' => $validated['aset_kepemilikan'], - 'bantuan_lain' => $validated['bantuan_lain'], - 'usia' => $validated['usia'], - 'kondisi_rumah' => $validated['kondisi_rumah'], - 'meteran_listrik' => $validated['meteran_listrik'], - 'sumber_air' => $validated['sumber_air'], + 'pekerjaan' => $validated['pekerjaan'], + 'penghasilan' => $validated['penghasilan'], + 'jumlah_tanggungan' => $validated['jumlah_tanggungan'], + 'aset_pilihan' => $validated['aset_pilihan'] ?? [], + 'aset_manual' => $validated['aset_manual'] ?? '', + 'bantuan_lain' => $validated['bantuan_lain'], + 'usia' => $validated['usia'], + 'status_perkawinan' => $validated['status_perkawinan'], + 'lantai_rumah' => $validated['lantai_rumah'], + 'dinding_rumah' => $validated['dinding_rumah'], + 'atap_rumah' => $validated['atap_rumah'], + 'luas_rumah_m2' => $validated['luas_rumah_m2'], + 'status_kepemilikan_rumah' => $validated['status_kepemilikan_rumah'], + 'meteran_listrik' => $validated['meteran_listrik'], + 'sumber_air' => $validated['sumber_air'], ]; $prediction = $this->mlService->getPrediction($predictionData); @@ -188,7 +225,7 @@ public function show(CalonPenerima $calonPenerima) { $user = Auth::user(); - if ($calonPenerima->user_id !== $user->id) { + if ((int) $calonPenerima->user_id !== (int) $user->id) { abort(403, 'Unauthorized action.'); } @@ -203,7 +240,7 @@ public function edit(CalonPenerima $calonPenerima) { $user = Auth::user(); - if ($calonPenerima->user_id !== $user->id || ($calonPenerima->tracking_status ?? 'draft') !== 'draft') { + if ((int) $calonPenerima->user_id !== (int) $user->id || ($calonPenerima->tracking_status ?? 'draft') !== 'draft') { abort(403, 'Data yang sudah diajukan tidak dapat diubah.'); } @@ -214,7 +251,7 @@ public function update(Request $request, CalonPenerima $calonPenerima) { $user = Auth::user(); - if ($calonPenerima->user_id !== $user->id || ($calonPenerima->tracking_status ?? 'draft') !== 'draft') { + if ((int) $calonPenerima->user_id !== (int) $user->id || ($calonPenerima->tracking_status ?? 'draft') !== 'draft') { abort(403, 'Data yang sudah diajukan tidak dapat diubah.'); } @@ -224,34 +261,40 @@ public function update(Request $request, CalonPenerima $calonPenerima) } $validated = $request->validate([ - 'rt_id' => 'nullable|exists:rts,id', - 'no_kk' => 'required|digits:16|unique:calon_penerimas,no_kk,' . $calonPenerima->id, - 'nik' => 'required|digits:16|unique:calon_penerimas,nik,' . $calonPenerima->id, - 'nama_lengkap' => 'required|string|max:255', - 'jenis_kelamin' => 'required|in:Laki-laki,Perempuan', - 'tempat_lahir' => 'required|string|max:255', - 'tanggal_lahir' => 'required|date', - 'alamat' => 'required|string', - 'desa' => 'nullable|string|max:255', - 'pekerjaan' => 'required|string', - 'penghasilan' => 'required|numeric|min:0', - 'jumlah_tanggungan' => 'required|integer|min:0', - 'aset_kepemilikan' => 'required|string', - 'bantuan_lain' => 'required|in:ya,tidak', - 'usia' => 'required|integer|min:17|max:100', - 'status_perkawinan' => 'required|string', - 'kondisi_rumah' => 'required|in:Layak,Sedang,Tidak Layak', - 'meteran_listrik' => 'required|in:450VA,900VA,1300VA+', - 'sumber_air' => 'required|in:PDAM,Sumur,Sungai', - 'foto_rumah_depan' => 'nullable|image|max:2048', - 'foto_rumah_belakang' => 'nullable|image|max:2048', - 'foto_rumah_kanan' => 'nullable|image|max:2048', - 'foto_rumah_kiri' => 'nullable|image|max:2048', - 'foto_kk' => 'nullable|image|max:2048', - 'foto_ktp' => 'nullable|image|max:2048', - 'foto_rekening_listrik' => 'nullable|mimes:jpg,jpeg,png,pdf|max:2048', - 'foto_meteran_air' => 'nullable|image|max:2048', - 'dokumen_pendukung' => 'nullable|mimes:jpg,jpeg,png,pdf,doc,docx|max:5120', + 'rt_id' => 'nullable|exists:rts,id', + 'no_kk' => 'required|digits:16|unique:calon_penerimas,no_kk,' . $calonPenerima->id, + 'nik' => 'required|digits:16|unique:calon_penerimas,nik,' . $calonPenerima->id, + 'nama_lengkap' => 'required|string|max:255', + 'jenis_kelamin' => 'required|in:Laki-laki,Perempuan', + 'tempat_lahir' => 'required|string|max:255', + 'tanggal_lahir' => 'required|date', + 'alamat' => 'required|string', + 'desa' => 'nullable|string|max:255', + 'pekerjaan' => 'required|string', + 'penghasilan' => 'required|numeric|min:0', + 'jumlah_tanggungan' => 'required|integer|min:0', + 'aset_pilihan' => 'nullable|array', + 'aset_pilihan.*' => 'nullable|string', + 'aset_manual' => 'nullable|string|max:255', + 'bantuan_lain' => 'required|in:ya,tidak', + 'usia' => 'required|integer|min:17|max:100', + 'status_perkawinan' => 'required|string', + 'lantai_rumah' => 'required|string', + 'dinding_rumah' => 'required|string', + 'atap_rumah' => 'required|string', + 'luas_rumah_m2' => 'required|integer|min:1', + 'status_kepemilikan_rumah'=> 'required|string', + 'meteran_listrik' => 'required|in:450VA,900VA,1300VA+', + 'sumber_air' => 'required|in:PDAM,Sumur,Sungai', + 'foto_rumah_depan' => 'nullable|image', + 'foto_rumah_belakang' => 'nullable|image', + 'foto_rumah_kanan' => 'nullable|image', + 'foto_rumah_kiri' => 'nullable|image', + 'foto_kk' => 'nullable|image', + 'foto_ktp' => 'nullable|image', + 'foto_rekening_listrik' => 'nullable|mimes:jpg,jpeg,png,pdf', + 'foto_meteran_air' => 'nullable|image', + 'dokumen_pendukung' => 'nullable|mimes:jpg,jpeg,png,pdf,doc,docx', ], [ 'nik.unique' => 'NIK sudah pernah diinput.', 'no_kk.unique' => 'No. KK sudah pernah diinput.', @@ -259,6 +302,16 @@ public function update(Request $request, CalonPenerima $calonPenerima) $validated['rt_id'] = $myRtId; + $validated['aset_kepemilikan'] = $this->buildAsetString( + $validated['aset_pilihan'] ?? [], + $validated['aset_manual'] ?? '' + ); + + $validated['kondisi_rumah'] = $this->deriveKondisiRumah( + $validated['lantai_rumah'], + $validated['dinding_rumah'] + ); + $rt = RT::with('dusun')->find($myRtId); if ($rt && $rt->dusun) { $validated['desa'] = $rt->dusun->nama_dusun; @@ -268,12 +321,23 @@ public function update(Request $request, CalonPenerima $calonPenerima) DB::beginTransaction(); try { - // Handle upload foto — hapus lama kalau ada yang baru - $fotoFields = [ + $fotoImages = [ 'foto_rumah_depan', 'foto_rumah_belakang', 'foto_rumah_kanan', 'foto_rumah_kiri', - 'foto_kk', 'foto_ktp', 'foto_rekening_listrik', 'foto_meteran_air', 'dokumen_pendukung', + 'foto_kk', 'foto_ktp', 'foto_meteran_air', ]; - foreach ($fotoFields as $field) { + $fotoFiles = [ + 'foto_rekening_listrik', 'dokumen_pendukung', + ]; + + foreach ($fotoImages as $field) { + if ($request->hasFile($field)) { + if ($calonPenerima->$field) { + Storage::disk('public')->delete($calonPenerima->$field); + } + $validated[$field] = $this->simpanFotoKompres($request->file($field)); + } + } + foreach ($fotoFiles as $field) { if ($request->hasFile($field)) { if ($calonPenerima->$field) { Storage::disk('public')->delete($calonPenerima->$field); @@ -284,34 +348,6 @@ public function update(Request $request, CalonPenerima $calonPenerima) $calonPenerima->update($validated); - /* - // Uncomment kalau mau update ulang prediksi ML saat edit - $predictionData = [ - 'pekerjaan' => $validated['pekerjaan'], - 'penghasilan' => $validated['penghasilan'], - 'jumlah_tanggungan' => $validated['jumlah_tanggungan'], - 'aset_kepemilikan' => $validated['aset_kepemilikan'], - 'bantuan_lain' => $validated['bantuan_lain'], - 'usia' => $validated['usia'], - 'kondisi_rumah' => $validated['kondisi_rumah'], - 'meteran_listrik' => $validated['meteran_listrik'], - 'sumber_air' => $validated['sumber_air'], - ]; - $prediction = $this->mlService->getPrediction($predictionData); - if ($prediction) { - PrediksiKelayakan::updateOrCreate( - ['calon_penerima_id' => $calonPenerima->id], - [ - 'probability' => $prediction['probability'], - 'recommendation' => $prediction['recommendation'], - 'kondisi_rumah' => $validated['kondisi_rumah'], - 'meteran_listrik'=> $validated['meteran_listrik'], - 'sumber_air' => $validated['sumber_air'], - ] - ); - } - */ - DB::commit(); return redirect()->route('rt.calon-penerima.index') @@ -326,7 +362,7 @@ public function ajukan(CalonPenerima $calonPenerima) { $user = Auth::user(); - if ($calonPenerima->user_id !== $user->id) { + if ((int) $calonPenerima->user_id !== (int) $user->id) { abort(403, 'Unauthorized action.'); } @@ -345,11 +381,10 @@ public function destroy(CalonPenerima $calonPenerima) { $user = Auth::user(); - if ($calonPenerima->user_id !== $user->id || ($calonPenerima->tracking_status ?? 'draft') !== 'draft') { + if ((int) $calonPenerima->user_id !== (int) $user->id || ($calonPenerima->tracking_status ?? 'draft') !== 'draft') { abort(403, 'Data yang sudah diajukan tidak dapat dihapus.'); } - // Hapus foto dari storage $fotoFields = [ 'foto_rumah_depan', 'foto_rumah_belakang', 'foto_rumah_kanan', 'foto_rumah_kiri', 'foto_kk', 'foto_ktp', 'foto_rekening_listrik', 'foto_meteran_air', 'dokumen_pendukung', @@ -366,6 +401,53 @@ public function destroy(CalonPenerima $calonPenerima) ->with('success', 'Data calon penerima berhasil dihapus!'); } + // ── PRIVATE HELPERS ─────────────────────────────────────────────────────── + + private function simpanFotoKompres(\Illuminate\Http\UploadedFile $file): string + { + return $file->store('dokumen-warga', 'public'); + } + + private function buildAsetString(array $pilihan, string $manual): string + { + $items = array_filter(array_map('trim', $pilihan)); + + if ($manual !== '') { + foreach (explode(',', $manual) as $item) { + $item = trim($item); + if ($item !== '') $items[] = $item; + } + } + + $items = array_unique($items); + return count($items) > 0 ? implode(', ', $items) : 'Tidak Punya'; + } + + private function deriveKondisiRumah(string $lantai, string $dinding): string + { + $skorLantai = match($lantai) { + 'Tanah' => 3, + 'Papan/Kayu' => 2, + 'Semen Kasar' => 2, + 'Keramik Murah' => 1, + default => 0, + }; + + $skorDinding = match($dinding) { + 'Bambu' => 3, + 'Anyaman Bambu/Gedek' => 3, + 'Kayu/Papan' => 2, + 'Tembok Tidak Plester' => 1, + default => 0, + }; + + $total = $skorLantai + $skorDinding; + + if ($total >= 4) return 'Tidak Layak'; + if ($total >= 1) return 'Sedang'; + return 'Layak'; + } + private function getPredictionExplanation($calonPenerima): array { $positive = []; @@ -373,7 +455,10 @@ private function getPredictionExplanation($calonPenerima): array if (strtolower($calonPenerima->pekerjaan) === 'tidak bekerja') { $positive[] = 'Tidak bekerja'; - } elseif (in_array(strtolower($calonPenerima->pekerjaan), ['buruh harian', 'buruh', 'petani', 'nelayan'])) { + } elseif (in_array(strtolower($calonPenerima->pekerjaan), [ + 'buruh harian lepas', 'buruh harian', 'buruh', + 'petani kecil', 'petani', 'mengurus rumah tangga', + ])) { $positive[] = 'Pekerjaan tergolong rentan secara ekonomi'; } else { $negative[] = 'Memiliki pekerjaan yang relatif lebih stabil'; @@ -395,11 +480,12 @@ private function getPredictionExplanation($calonPenerima): array $negative[] = 'Jumlah tanggungan sedikit'; } - $aset = strtolower($calonPenerima->aset_kepemilikan); - if (str_contains($aset, 'mobil')) $negative[] = 'Memiliki aset bernilai tinggi (mobil)'; - if (str_contains($aset, 'motor')) $negative[] = 'Memiliki aset kendaraan bermotor'; - if (str_contains($aset, 'rumah')) $negative[] = 'Memiliki aset rumah'; - if (in_array($aset, ['tidak ada', '-', 'tidak punya'])) $positive[] = 'Tidak memiliki aset berarti'; + $aset = strtolower($calonPenerima->aset_kepemilikan ?? ''); + if (str_contains($aset, 'aset lengkap')) $negative[] = 'Memiliki aset bernilai tinggi'; + elseif (str_contains($aset, 'motor + tanah')) $negative[] = 'Memiliki motor dan tanah'; + elseif (str_contains($aset, 'motor + emas')) $negative[] = 'Memiliki motor dan emas'; + elseif (str_contains($aset, 'motor')) $negative[] = 'Memiliki kendaraan bermotor'; + elseif (str_contains($aset, 'tidak punya')) $positive[] = 'Tidak memiliki aset berarti'; if (strtolower($calonPenerima->bantuan_lain) === 'ya') { $negative[] = 'Sudah menerima bantuan lain'; @@ -413,30 +499,57 @@ private function getPredictionExplanation($calonPenerima): array $positive[] = 'Usia cukup rentan'; } - // Parameter baru - if (strtolower($calonPenerima->kondisi_rumah ?? '') === 'tidak layak') { - $positive[] = 'Kondisi rumah tidak layak huni'; - } elseif (strtolower($calonPenerima->kondisi_rumah ?? '') === 'sedang') { - $positive[] = 'Kondisi rumah sedang'; - } else { - $negative[] = 'Kondisi rumah layak'; - } + $lantai = $calonPenerima->lantai_rumah ?? ''; + match($lantai) { + 'Tanah' => $positive[] = 'Lantai rumah masih tanah', + 'Papan/Kayu' => $positive[] = 'Lantai rumah papan/kayu', + 'Semen Kasar' => $positive[] = 'Lantai rumah semen kasar', + 'Keramik Murah' => $positive[] = 'Lantai rumah keramik murah', + 'Keramik' => $negative[] = 'Lantai rumah sudah keramik', + default => null, + }; - if (($calonPenerima->meteran_listrik ?? '') === '450VA') { - $positive[] = 'Meteran listrik 450VA (daya rendah)'; - } elseif (($calonPenerima->meteran_listrik ?? '') === '900VA') { - $positive[] = 'Meteran listrik 900VA'; - } else { - $negative[] = 'Meteran listrik 1300VA ke atas'; - } + $dinding = $calonPenerima->dinding_rumah ?? ''; + match($dinding) { + 'Bambu' => $positive[] = 'Dinding rumah bambu', + 'Anyaman Bambu/Gedek' => $positive[] = 'Dinding rumah anyaman bambu/gedek', + 'Kayu/Papan' => $positive[] = 'Dinding rumah kayu/papan', + 'Tembok Tidak Plester' => $positive[] = 'Dinding rumah tembok belum diplester', + 'Tembok Plester' => $negative[] = 'Dinding rumah tembok plester', + default => null, + }; - if (strtolower($calonPenerima->sumber_air ?? '') === 'sungai') { - $positive[] = 'Sumber air dari sungai (kurang layak)'; - } elseif (strtolower($calonPenerima->sumber_air ?? '') === 'sumur') { - $positive[] = 'Sumber air dari sumur'; - } else { - $negative[] = 'Sumber air PDAM (layak)'; - } + $atap = $calonPenerima->atap_rumah ?? ''; + match($atap) { + 'Bambu/Jerami' => $positive[] = 'Atap rumah bambu/jerami', + 'Seng' => $positive[] = 'Atap rumah seng', + 'Campuran Seng+Genteng' => $positive[] = 'Atap rumah campuran seng dan genteng', + 'Genteng' => $negative[] = 'Atap rumah sudah genteng', + 'Genteng Beton' => $negative[] = 'Atap rumah genteng beton', + default => null, + }; + + $statusRumah = $calonPenerima->status_kepemilikan_rumah ?? ''; + match($statusRumah) { + 'Menumpang' => $positive[] = 'Status rumah menumpang', + 'Sewa/Kontrak' => $positive[] = 'Status rumah sewa/kontrak', + 'Milik Sendiri'=> $negative[] = 'Memiliki rumah sendiri', + default => null, + }; + + match($calonPenerima->meteran_listrik ?? '') { + '450VA' => $positive[] = 'Meteran listrik 450 VA', + '900VA' => $positive[] = 'Meteran listrik 900 VA', + '1300VA+' => $negative[] = 'Meteran listrik 1300 VA ke atas', + default => null, + }; + + match(strtolower($calonPenerima->sumber_air ?? '')) { + 'sungai' => $positive[] = 'Sumber air dari sungai', + 'sumur' => $positive[] = 'Sumber air dari sumur', + 'pdam' => $negative[] = 'Sumber air PDAM', + default => null, + }; $probability = $calonPenerima->prediksiKelayakan->probability ?? 0; $probability = $probability <= 1 ? $probability * 100 : $probability; diff --git a/app/Http/Controllers/Rt/LaporanController.php b/app/Http/Controllers/Rt/LaporanController.php index 0f6551c..1e31aa8 100644 --- a/app/Http/Controllers/Rt/LaporanController.php +++ b/app/Http/Controllers/Rt/LaporanController.php @@ -22,28 +22,24 @@ public function index(Request $request) if (!$rt) { $laporans = collect(); $stats = [ - 'total' => 0, + 'total' => 0, 'diterima' => 0, - 'ditolak' => 0, + 'ditolak' => 0, ]; return view('rt.laporan', compact('laporans', 'stats')); } - $nomorRt = $rt->nomor_rt; - $namaDusun = $rt->dusun->nama_dusun ?? null; - $laporans = PenerimaFinal::query() - ->where('nomor_rt', $nomorRt) - ->where('nama_dusun', $namaDusun) + ->where('rt_id', $rt->id) ->whereIn('status_verifikasi', ['disetujui', 'ditolak']) ->orderByDesc('tanggal_penetapan') ->get(); $stats = [ - 'total' => $laporans->count(), + 'total' => $laporans->count(), 'diterima' => $laporans->where('status_verifikasi', 'disetujui')->count(), - 'ditolak' => $laporans->where('status_verifikasi', 'ditolak')->count(), + 'ditolak' => $laporans->where('status_verifikasi', 'ditolak')->count(), ]; return view('rt.laporan', compact('laporans', 'stats')); diff --git a/app/Http/Middleware/AutoArsipMiddleware.php b/app/Http/Middleware/AutoArsipMiddleware.php new file mode 100644 index 0000000..8c4e526 --- /dev/null +++ b/app/Http/Middleware/AutoArsipMiddleware.php @@ -0,0 +1,60 @@ +jalankanAutoArsip(); + return $next($request); + } + + private function jalankanAutoArsip(): void + { + $sekarang = Carbon::now('Asia/Jakarta'); + + // ── 1. AUTO ARSIP ────────────────────────────────────────────── + // Tiap tanggal 1, arsipkan semua data yang periode_bulan-nya + // sudah bulan lalu (atau lebih lama) dan belum diarsip + if ($sekarang->day === 1) { + $batasBulan = $sekarang->copy()->startOfMonth(); // awal bulan ini + + DB::table('calon_penerimas') + ->whereIn('tracking_status', ['terkirim', 'sedang_validasi', 'selesai']) + ->whereNull('arsip_tahun') + ->whereNotNull('periode_bulan') + ->where('periode_bulan', '<', $batasBulan->toDateString()) + ->update([ + 'arsip_tahun' => $batasBulan->copy()->subMonth()->year, + 'updated_at' => now(), + ]); + } + + // ── 2. AUTO HAPUS ARSIP ──────────────────────────────────────── + // Tiap tanggal 1 Januari, hapus permanen arsip tahun lalu + if ($sekarang->day === 1 && $sekarang->month === 1) { + $tahunHapus = $sekarang->year - 1; + + // Hapus prediksi terkait dulu (foreign key) + $idArsip = DB::table('calon_penerimas') + ->where('arsip_tahun', $tahunHapus) + ->pluck('id'); + + if ($idArsip->isNotEmpty()) { + DB::table('prediksi_kelayakans') + ->whereIn('calon_penerima_id', $idArsip) + ->delete(); + + DB::table('calon_penerimas') + ->where('arsip_tahun', $tahunHapus) + ->delete(); + } + } + } +} \ No newline at end of file diff --git a/app/Models/CalonPenerima.php b/app/Models/CalonPenerima.php index 6d3c355..ac770a3 100644 --- a/app/Models/CalonPenerima.php +++ b/app/Models/CalonPenerima.php @@ -10,41 +10,49 @@ class CalonPenerima extends Model use HasFactory; protected $fillable = [ - 'user_id', - 'rt_id', - 'no_kk', - 'nik', - 'nama_lengkap', - 'jenis_kelamin', - 'tempat_lahir', - 'tanggal_lahir', - 'alamat', - 'desa', - 'pekerjaan', - 'penghasilan', - 'jumlah_tanggungan', - 'aset_kepemilikan', - 'kondisi_rumah', - 'meteran_listrik', - 'sumber_air', - 'bantuan_lain', - 'usia', - 'status_perkawinan', - 'status_verifikasi', - 'catatan_admin', - 'foto_rumah_depan', - 'foto_rumah_belakang', - 'foto_rumah_kanan', - 'foto_rumah_kiri', - 'foto_kk', - 'foto_ktp', - 'foto_rekening_listrik', - 'foto_meteran_air', - 'dokumen_pendukung', + 'user_id', + 'rt_id', + 'no_kk', + 'nik', + 'nama_lengkap', + 'jenis_kelamin', + 'tempat_lahir', + 'tanggal_lahir', + 'alamat', + 'desa', + 'pekerjaan', + 'penghasilan', + 'jumlah_tanggungan', + 'aset_kepemilikan', + 'kondisi_rumah', + 'lantai_rumah', + 'dinding_rumah', + 'atap_rumah', + 'luas_rumah_m2', + 'status_kepemilikan_rumah', + 'meteran_listrik', + 'sumber_air', + 'bantuan_lain', + 'usia', + 'status_perkawinan', + 'status_verifikasi', + 'tracking_status', + 'catatan_admin', + 'arsip_tahun', + 'periode_bulan', + 'foto_rumah_depan', + 'foto_rumah_belakang', + 'foto_rumah_kanan', + 'foto_rumah_kiri', + 'foto_kk', + 'foto_ktp', + 'foto_rekening_listrik', + 'foto_meteran_air', + 'dokumen_pendukung', ]; protected $casts = [ - 'penghasilan' => 'decimal:2', + 'penghasilan' => 'decimal:2', 'tanggal_lahir' => 'date', ]; diff --git a/app/Models/PenerimaFinalArsip.php b/app/Models/PenerimaFinalArsip.php new file mode 100644 index 0000000..1f9b1f2 --- /dev/null +++ b/app/Models/PenerimaFinalArsip.php @@ -0,0 +1,12 @@ + 'date']; +} \ No newline at end of file diff --git a/app/Models/PeriodeDanaBlt.php b/app/Models/PeriodeDanaBlt.php new file mode 100644 index 0000000..1ab8fa6 --- /dev/null +++ b/app/Models/PeriodeDanaBlt.php @@ -0,0 +1,26 @@ + 'integer', + 'jumlah_penerima' => 'integer', + 'dana_terpakai' => 'integer', + 'dana_sisa' => 'integer', + ]; +} \ No newline at end of file diff --git a/app/Models/RiwayatPenerima.php b/app/Models/RiwayatPenerima.php new file mode 100644 index 0000000..6dfb42d --- /dev/null +++ b/app/Models/RiwayatPenerima.php @@ -0,0 +1,39 @@ + 'date', + 'di_arsipkan_pada' => 'datetime', + ]; +} \ No newline at end of file diff --git a/bootstrap/app.php b/bootstrap/app.php index 2af5f05..3c30414 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -16,6 +16,10 @@ 'is_active' => \App\Http\Middleware\EnsureUserIsActive::class, ]); + $middleware->web(append: [ + \App\Http\Middleware\AutoArsipMiddleware::class, + ]); + }) ->withExceptions(function (Exceptions $exceptions): void { // diff --git a/composer.json b/composer.json index 3ddaca0..d0366ed 100644 --- a/composer.json +++ b/composer.json @@ -8,6 +8,7 @@ "require": { "php": "^8.2", "barryvdh/laravel-dompdf": "^3.1", + "intervention/image": "^3.11", "laravel/framework": "^12.0", "laravel/tinker": "^2.10.1", "maatwebsite/excel": "*" @@ -90,4 +91,4 @@ }, "minimum-stability": "stable", "prefer-stable": true -} \ No newline at end of file +} diff --git a/composer.lock b/composer.lock index 7b93023..ff02373 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e0c1bd04a3bb8afaef35697676fa0752", + "content-hash": "3074c23690bd98b29e2eb7fdb9df343a", "packages": [ { "name": "barryvdh/laravel-dompdf", @@ -1502,6 +1502,150 @@ ], "time": "2025-08-22T14:27:06+00:00" }, + { + "name": "intervention/gif", + "version": "4.2.4", + "source": { + "type": "git", + "url": "https://github.com/Intervention/gif.git", + "reference": "c3598a16ebe7690cd55640c44144a9df383ea73c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Intervention/gif/zipball/c3598a16ebe7690cd55640c44144a9df383ea73c", + "reference": "c3598a16ebe7690cd55640c44144a9df383ea73c", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", + "slevomat/coding-standard": "~8.0", + "squizlabs/php_codesniffer": "^3.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Intervention\\Gif\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Oliver Vogel", + "email": "oliver@intervention.io", + "homepage": "https://intervention.io/" + } + ], + "description": "Native PHP GIF Encoder/Decoder", + "homepage": "https://github.com/intervention/gif", + "keywords": [ + "animation", + "gd", + "gif", + "image" + ], + "support": { + "issues": "https://github.com/Intervention/gif/issues", + "source": "https://github.com/Intervention/gif/tree/4.2.4" + }, + "funding": [ + { + "url": "https://paypal.me/interventionio", + "type": "custom" + }, + { + "url": "https://github.com/Intervention", + "type": "github" + }, + { + "url": "https://ko-fi.com/interventionphp", + "type": "ko_fi" + } + ], + "time": "2026-01-04T09:27:23+00:00" + }, + { + "name": "intervention/image", + "version": "3.11.8", + "source": { + "type": "git", + "url": "https://github.com/Intervention/image.git", + "reference": "cf04c8dd245697f701057c13d4bfe140d584e738" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Intervention/image/zipball/cf04c8dd245697f701057c13d4bfe140d584e738", + "reference": "cf04c8dd245697f701057c13d4bfe140d584e738", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "intervention/gif": "^4.2", + "php": "^8.1" + }, + "require-dev": { + "mockery/mockery": "^1.6", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", + "slevomat/coding-standard": "~8.0", + "squizlabs/php_codesniffer": "^4" + }, + "suggest": { + "ext-exif": "Recommended to be able to read EXIF data properly." + }, + "type": "library", + "autoload": { + "psr-4": { + "Intervention\\Image\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Oliver Vogel", + "email": "oliver@intervention.io", + "homepage": "https://intervention.io" + } + ], + "description": "PHP Image Processing", + "homepage": "https://image.intervention.io", + "keywords": [ + "gd", + "image", + "imagick", + "resize", + "thumbnail", + "watermark" + ], + "support": { + "issues": "https://github.com/Intervention/image/issues", + "source": "https://github.com/Intervention/image/tree/3.11.8" + }, + "funding": [ + { + "url": "https://paypal.me/interventionio", + "type": "custom" + }, + { + "url": "https://github.com/Intervention", + "type": "github" + }, + { + "url": "https://ko-fi.com/interventionphp", + "type": "ko_fi" + } + ], + "time": "2026-05-01T08:20:10+00:00" + }, { "name": "laravel/framework", "version": "v12.56.0", diff --git a/database/migrations/025_05_05_000001_add_kondisi_rumah_to_calon_penerimas.php b/database/migrations/025_05_05_000001_add_kondisi_rumah_to_calon_penerimas.php new file mode 100644 index 0000000..ce33257 --- /dev/null +++ b/database/migrations/025_05_05_000001_add_kondisi_rumah_to_calon_penerimas.php @@ -0,0 +1,32 @@ +string('lantai_rumah')->nullable()->after('kondisi_rumah'); + $table->string('dinding_rumah')->nullable()->after('lantai_rumah'); + $table->string('atap_rumah')->nullable()->after('dinding_rumah'); + $table->integer('luas_rumah_m2')->nullable()->after('atap_rumah'); + $table->string('status_kepemilikan_rumah')->nullable()->after('luas_rumah_m2'); + }); + } + + public function down(): void + { + Schema::table('calon_penerimas', function (Blueprint $table) { + $table->dropColumn([ + 'lantai_rumah', + 'dinding_rumah', + 'atap_rumah', + 'luas_rumah_m2', + 'status_kepemilikan_rumah', + ]); + }); + } +}; \ No newline at end of file diff --git a/database/migrations/2026_04_28_095251_create_penerima_final_arsip_table.php b/database/migrations/2026_04_28_095251_create_penerima_final_arsip_table.php new file mode 100644 index 0000000..912092c --- /dev/null +++ b/database/migrations/2026_04_28_095251_create_penerima_final_arsip_table.php @@ -0,0 +1,40 @@ +id(); + $table->string('periode_arsip'); + $table->unsignedBigInteger('penerima_final_id')->nullable(); + $table->string('nama_lengkap')->nullable(); + $table->string('nik')->nullable(); + $table->string('no_kk')->nullable(); + $table->string('nomor_rt')->nullable(); + $table->string('nama_dusun')->nullable(); + $table->string('pekerjaan')->nullable(); + $table->bigInteger('penghasilan')->default(0); + $table->integer('jumlah_tanggungan')->default(0); + $table->string('aset_kepemilikan')->nullable(); + $table->string('bantuan_lain')->nullable(); + $table->integer('usia')->nullable(); + $table->float('probability')->nullable(); + $table->string('status_verifikasi')->nullable(); + $table->date('tanggal_penetapan')->nullable(); + $table->string('periode_bantuan')->nullable(); + $table->bigInteger('jumlah_bantuan')->default(300000); + $table->timestamp('diarsipkan_pada')->useCurrent(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('penerima_final_arsip'); + } +}; \ No newline at end of file diff --git a/database/migrations/2026_04_30_084006_create_riwayat_penerima_table.php b/database/migrations/2026_04_30_084006_create_riwayat_penerima_table.php new file mode 100644 index 0000000..87484da --- /dev/null +++ b/database/migrations/2026_04_30_084006_create_riwayat_penerima_table.php @@ -0,0 +1,42 @@ +id(); + $table->string('periode'); // contoh: "2026-04" + $table->string('nama_lengkap'); + $table->string('nik', 16); + $table->string('no_kk', 16)->nullable(); + $table->unsignedBigInteger('rt_id')->nullable(); + $table->string('nomor_rt')->nullable(); + $table->string('nama_dusun')->nullable(); + $table->string('pekerjaan')->nullable(); + $table->decimal('penghasilan', 15, 2)->default(0); + $table->integer('jumlah_tanggungan')->default(0); + $table->string('aset_kepemilikan')->nullable(); + $table->string('kondisi_rumah')->nullable(); + $table->string('meteran_listrik')->nullable(); + $table->string('sumber_air')->nullable(); + $table->string('bantuan_lain')->default('tidak'); + $table->integer('usia')->default(0); + $table->decimal('probability', 8, 4)->default(0); + $table->string('status_verifikasi')->default('disetujui'); + $table->date('tanggal_penetapan')->nullable(); + $table->decimal('jumlah_bantuan', 15, 2)->default(0); + $table->timestamp('di_arsipkan_pada')->nullable(); // kapan di-reset/arsip + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('riwayat_penerima'); + } +}; \ No newline at end of file diff --git a/database/migrations/2026_06_11_111332_add_arsip_tahun_to_wargas_table.php b/database/migrations/2026_06_11_111332_add_arsip_tahun_to_wargas_table.php new file mode 100644 index 0000000..721649f --- /dev/null +++ b/database/migrations/2026_06_11_111332_add_arsip_tahun_to_wargas_table.php @@ -0,0 +1,28 @@ +year('arsip_tahun')->nullable()->after('tracking_status'); + }); + } +}; diff --git a/public/.htaccess b/public/.htaccess index b574a59..10f4803 100644 --- a/public/.htaccess +++ b/public/.htaccess @@ -23,3 +23,6 @@ RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^ index.php [L] + +php_value upload_max_filesize 20M +php_value post_max_size 25M \ No newline at end of file diff --git a/public/asset/galeri1.png b/public/asset/galeri1.png new file mode 100644 index 0000000..bca87e0 Binary files /dev/null and b/public/asset/galeri1.png differ diff --git a/public/asset/galeri2.png b/public/asset/galeri2.png new file mode 100644 index 0000000..92127b2 Binary files /dev/null and b/public/asset/galeri2.png differ diff --git a/public/asset/galeri3.png b/public/asset/galeri3.png new file mode 100644 index 0000000..8b3e722 Binary files /dev/null and b/public/asset/galeri3.png differ diff --git a/public/asset/galeri4.png b/public/asset/galeri4.png new file mode 100644 index 0000000..8c3457a Binary files /dev/null and b/public/asset/galeri4.png differ diff --git a/resources/views/admin/dashboard.blade.php b/resources/views/admin/dashboard.blade.php index 48d1615..6a85f0f 100644 --- a/resources/views/admin/dashboard.blade.php +++ b/resources/views/admin/dashboard.blade.php @@ -1,37 +1,37 @@ -
+
-

Dashboard Kelurahan

-

Ringkasan data kelayakan penerima BLT-DD

+

Dashboard Admin Perangkat Desa

+

Ringkasan data kelayakan penerima BLT-DD

-
+
{{-- BELL --}}