diff --git a/app/Http/Controllers/Admin/FilterisasiController.php b/app/Http/Controllers/Admin/FilterisasiController.php index 4455082..5b9aa1e 100644 --- a/app/Http/Controllers/Admin/FilterisasiController.php +++ b/app/Http/Controllers/Admin/FilterisasiController.php @@ -14,13 +14,18 @@ class FilterisasiController extends Controller public function index(Request $request) { $user = $request->user(); - if (!$user || $user->role !== 'admin') abort(403, 'Akses ditolak: hanya Admin.'); + if (!$user || $user->role !== 'admin') { + abort(403, 'Akses ditolak: hanya Admin.'); + } $dusuns = Dusun::orderBy('nama_dusun')->get(); $dusunId = (int) $request->query('dusun_id', 0); $kuota = (int) $request->query('kuota', 7); - if ($kuota <= 0) $kuota = 7; + + if ($kuota <= 0) { + $kuota = 7; + } $candidates = collect(); $pickedIds = collect(); @@ -28,10 +33,11 @@ public function index(Request $request) if ($dusunId > 0) { $query = CalonPenerima::query() ->with(['rt.dusun', 'prediksiKelayakan']) - ->where('status_verifikasi', 'disetujui') // ✅ ONLY DISETUJUI - ->whereHas('rt', fn($q) => $q->where('dusun_id', $dusunId)); + ->whereIn('tracking_status', ['sedang_validasi', 'selesai']) + ->whereHas('rt', function ($q) use ($dusunId) { + $q->where('dusun_id', $dusunId); + }); - // urut probability DESC (null jadi 0) $query->leftJoin('prediksi_kelayakans', 'prediksi_kelayakans.calon_penerima_id', '=', 'calon_penerimas.id') ->select('calon_penerimas.*', DB::raw('COALESCE(prediksi_kelayakans.probability, 0) as prob')) ->orderByDesc('prob') @@ -39,7 +45,6 @@ public function index(Request $request) $candidates = $query->paginate(15)->withQueryString(); - // tandai yang sudah masuk penerima_final $pickedIds = PenerimaFinal::whereIn('calon_penerima_id', $candidates->pluck('id')->all()) ->pluck('calon_penerima_id'); } @@ -50,7 +55,9 @@ public function index(Request $request) public function tetapkan(Request $request) { $user = $request->user(); - if (!$user || $user->role !== 'admin') abort(403, 'Akses ditolak: hanya Admin.'); + if (!$user || $user->role !== 'admin') { + abort(403, 'Akses ditolak: hanya Admin.'); + } $data = $request->validate([ 'dusun_id' => 'required|exists:dusuns,id', @@ -61,32 +68,65 @@ public function tetapkan(Request $request) $kuota = (int) ($data['kuota'] ?? 7); DB::transaction(function () use ($dusunId, $kuota) { - - $top = CalonPenerima::query() - ->where('status_verifikasi', 'disetujui') // ✅ ONLY DISETUJUI - ->whereHas('rt', fn($r) => $r->where('dusun_id', $dusunId)) + // Ambil semua kandidat dalam dusun yang sedang divalidasi / sudah selesai + $allCandidates = CalonPenerima::query() + ->whereIn('tracking_status', ['sedang_validasi', 'selesai']) + ->whereHas('rt', function ($r) use ($dusunId) { + $r->where('dusun_id', $dusunId); + }) ->leftJoin('prediksi_kelayakans', 'prediksi_kelayakans.calon_penerima_id', '=', 'calon_penerimas.id') ->select('calon_penerimas.id', DB::raw('COALESCE(prediksi_kelayakans.probability, 0) as prob')) ->orderByDesc('prob') ->orderByDesc('calon_penerimas.created_at') - ->limit($kuota) ->get(); - foreach ($top as $row) { + $topIds = $allCandidates->take($kuota)->pluck('id')->all(); + $allIds = $allCandidates->pluck('id')->all(); + + // Hapus hasil final lama untuk kandidat pada dusun ini + if (!empty($allIds)) { + PenerimaFinal::whereIn('calon_penerima_id', $allIds)->delete(); + } + + // Simpan hasil final penerima yang masuk kuota + foreach ($topIds as $id) { PenerimaFinal::updateOrCreate( - ['calon_penerima_id' => $row->id], - ['dusun_id' => $dusunId] // kalau tabel kamu ga punya dusun_id, bilang ya nanti aku rapikan + ['calon_penerima_id' => $id], + [ + 'tanggal_penetapan' => now()->toDateString(), + 'periode_bantuan' => date('Y') . ' Triwulan 1', + 'jumlah_bantuan' => 0, + 'status_pencairan' => 'belum_cair', + 'tanggal_pencairan' => null, + ] ); } + + // Yang masuk kuota = disetujui + if (!empty($topIds)) { + CalonPenerima::whereIn('id', $topIds)->update([ + 'status_verifikasi' => 'disetujui', + ]); + } + + // Yang tidak masuk kuota = ditolak + $notPickedIds = array_diff($allIds, $topIds); + if (!empty($notPickedIds)) { + CalonPenerima::whereIn('id', $notPickedIds)->update([ + 'status_verifikasi' => 'ditolak', + ]); + } }); - return back()->with('success', "Berhasil menetapkan kuota {$kuota} penerima (status disetujui) untuk dusun terpilih."); + return back()->with('success', "Berhasil menetapkan kuota {$kuota} penerima untuk dusun terpilih."); } public function resetDusun(Request $request) { $user = $request->user(); - if (!$user || $user->role !== 'admin') abort(403, 'Akses ditolak: hanya Admin.'); + if (!$user || $user->role !== 'admin') { + abort(403, 'Akses ditolak: hanya Admin.'); + } $data = $request->validate([ 'dusun_id' => 'required|exists:dusuns,id', @@ -94,7 +134,23 @@ public function resetDusun(Request $request) $dusunId = (int) $data['dusun_id']; - PenerimaFinal::where('dusun_id', $dusunId)->delete(); + DB::transaction(function () use ($dusunId) { + $candidateIds = CalonPenerima::query() + ->whereHas('rt', function ($r) use ($dusunId) { + $r->where('dusun_id', $dusunId); + }) + ->whereIn('tracking_status', ['sedang_validasi', 'selesai']) + ->pluck('id') + ->all(); + + if (!empty($candidateIds)) { + PenerimaFinal::whereIn('calon_penerima_id', $candidateIds)->delete(); + + CalonPenerima::whereIn('id', $candidateIds)->update([ + 'status_verifikasi' => 'pending', + ]); + } + }); return back()->with('success', 'Hasil filterisasi dusun berhasil di-reset.'); } diff --git a/app/Http/Controllers/Auth/RegisteredUserController.php b/app/Http/Controllers/Auth/RegisteredUserController.php index 0739e2e..c028aa1 100644 --- a/app/Http/Controllers/Auth/RegisteredUserController.php +++ b/app/Http/Controllers/Auth/RegisteredUserController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; +use App\Models\RT; use App\Models\User; use Illuminate\Auth\Events\Registered; use Illuminate\Http\RedirectResponse; @@ -19,7 +20,13 @@ class RegisteredUserController extends Controller */ public function create(): View { - return view('auth.register'); + // Ambil semua RT + dusun untuk dropdown di halaman register + $rts = RT::with('dusun') + ->orderBy('dusun_id') + ->orderBy('nomor_rt') + ->get(); + + return view('auth.register', compact('rts')); } /** @@ -31,20 +38,30 @@ public function store(Request $request): RedirectResponse { $request->validate([ 'name' => ['required', 'string', 'max:255'], - 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class], + 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:' . User::class], 'password' => ['required', 'confirmed', Rules\Password::defaults()], + + // WAJIB pilih RT biar user punya rt_id + 'rt_id' => ['required', 'exists:rts,id'], ]); $user = User::create([ 'name' => $request->name, 'email' => $request->email, 'password' => Hash::make($request->password), + + // Set default akun hasil register sebagai RT + 'role' => 'rt', + 'rt_id' => $request->rt_id, ]); event(new Registered($user)); Auth::login($user); + // Kamu bisa arahkan ke dashboard RT kalau punya route khusus RT + // return redirect()->route('rt.dashboard'); + return redirect(route('dashboard', absolute: false)); } -} +} \ No newline at end of file diff --git a/app/Http/Controllers/Rt/CalonPenerimaController.php b/app/Http/Controllers/Rt/CalonPenerimaController.php index 0ab4999..620bacf 100644 --- a/app/Http/Controllers/Rt/CalonPenerimaController.php +++ b/app/Http/Controllers/Rt/CalonPenerimaController.php @@ -13,7 +13,7 @@ class CalonPenerimaController extends Controller { - protected $mlService; + protected MLPredictionService $mlService; public function __construct(MLPredictionService $mlService) { @@ -24,10 +24,9 @@ public function index() { $user = Auth::user(); - // KHUSUS RT: hanya data milik RT tersebut $calonPenerimas = CalonPenerima::with(['rt.dusun', 'prediksiKelayakan']) ->where('user_id', $user->id) - ->orderBy('created_at', 'desc') + ->latest() ->paginate(20); return view('rt.calon-penerima.index', compact('calonPenerimas')); @@ -35,9 +34,15 @@ public function index() public function create() { + // RT tidak boleh pilih RT lain + // Tapi kita biarkan view create tetap bisa pakai data RT user (kalau mau ditampilkan) $user = Auth::user(); - $rts = RT::where('id', $user->rt_id)->with('dusun')->get(); + $rts = RT::where('id', $user->rt_id) + ->with('dusun') + ->get(); + + // fallback kalau data RT user belum ada (harusnya jarang terjadi) if ($rts->isEmpty()) { $rts = RT::with('dusun')->get(); } @@ -47,16 +52,27 @@ public function create() public function store(Request $request) { + $user = Auth::user(); + + $myRtId = $user->rt_id ?? null; + if (!$myRtId) { + return back()->withInput()->with('error', 'Akun RT belum terhubung ke data RT.'); + } + $validated = $request->validate([ - 'rt_id' => 'required|exists:rts,id', - 'no_kk' => 'required|string|max:16', - 'nik' => 'required|digits:16|unique:calon_penerimas,nik', + // rt_id boleh ada di form, tapi akan kita override (anti inject) + 'rt_id' => 'nullable|exists:rts,id', + + // ✅ NIK & NO KK TIDAK BOLEH DUPLIKAT + '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' => 'required|string|max:255', + 'desa' => 'nullable|string|max:255', // akan kita set otomatis 'pekerjaan' => 'required|string', 'penghasilan' => 'required|numeric|min:0', 'jumlah_tanggungan' => 'required|integer|min:0', @@ -64,12 +80,27 @@ public function store(Request $request) 'bantuan_lain' => 'required|in:ya,tidak', 'usia' => 'required|integer|min:17|max:100', 'status_perkawinan' => 'required|string', + ], [ + 'nik.unique' => 'NIK sudah pernah diinput.', + 'no_kk.unique' => 'No. KK sudah pernah diinput.', ]); + // 🔒 Kunci RT dari user login (anti inject) + $validated['rt_id'] = $myRtId; + + // 🔒 Kunci Dusun/Desa ikut RT + $rt = RT::with('dusun')->find($myRtId); + if ($rt && $rt->dusun) { + $validated['desa'] = $rt->dusun->nama_dusun; + } else { + // fallback aman kalau relasi dusun kosong + $validated['desa'] = $validated['desa'] ?? '-'; + } + DB::beginTransaction(); try { $calonPenerima = CalonPenerima::create([ - 'user_id' => Auth::id(), + 'user_id' => $user->id, 'rt_id' => $validated['rt_id'], 'no_kk' => $validated['no_kk'], 'nik' => $validated['nik'], @@ -87,8 +118,10 @@ public function store(Request $request) 'usia' => $validated['usia'], 'status_perkawinan' => $validated['status_perkawinan'], 'status_verifikasi' => 'pending', + 'tracking_status' => 'draft', ]); + // 🔮 ML Prediction $predictionData = [ 'pekerjaan' => $validated['pekerjaan'], 'penghasilan' => $validated['penghasilan'], @@ -114,7 +147,8 @@ public function store(Request $request) ->with('success', 'Data calon penerima berhasil ditambahkan!'); } catch (\Exception $e) { DB::rollBack(); - return redirect()->back() + + return back() ->withInput() ->with('error', 'Terjadi kesalahan: ' . $e->getMessage()); } @@ -124,50 +158,57 @@ public function show(CalonPenerima $calonPenerima) { $user = Auth::user(); - // pastikan RT hanya bisa lihat miliknya + // 🔒 RT hanya boleh lihat data miliknya if ($calonPenerima->user_id !== $user->id) { abort(403, 'Unauthorized action.'); } - $calonPenerima->load(['rt.dusun', 'prediksiKelayakan', 'penerimaFinal']); + $calonPenerima->load(['rt.dusun', 'prediksiKelayakan']); - return view('rt.calon-penerima.show', compact('calonPenerima')); + $explanation = $this->getPredictionExplanation($calonPenerima); + + return view('rt.calon-penerima.show', compact('calonPenerima', 'explanation')); } public function edit(CalonPenerima $calonPenerima) { $user = Auth::user(); - if ($calonPenerima->user_id !== $user->id || $calonPenerima->status_verifikasi !== 'pending') { - abort(403, 'Unauthorized action.'); + // 🔒 RT hanya bisa edit kalau miliknya + masih draft + if ($calonPenerima->user_id !== $user->id || ($calonPenerima->tracking_status ?? 'draft') !== 'draft') { + abort(403, 'Data yang sudah diajukan tidak dapat diubah.'); } - $rts = RT::where('id', $user->rt_id)->with('dusun')->get(); - if ($rts->isEmpty()) { - $rts = RT::with('dusun')->get(); - } - - return view('rt.calon-penerima.edit', compact('calonPenerima', 'rts')); + return view('rt.calon-penerima.edit', compact('calonPenerima')); } public function update(Request $request, CalonPenerima $calonPenerima) { $user = Auth::user(); - if ($calonPenerima->user_id !== $user->id || $calonPenerima->status_verifikasi !== 'pending') { - abort(403, 'Unauthorized action.'); + // 🔒 RT hanya bisa update kalau miliknya + masih draft + if ($calonPenerima->user_id !== $user->id || ($calonPenerima->tracking_status ?? 'draft') !== 'draft') { + abort(403, 'Data yang sudah diajukan tidak dapat diubah.'); + } + + $myRtId = $user->rt_id ?? null; + if (!$myRtId) { + return back()->withInput()->with('error', 'Akun RT belum terhubung ke data RT.'); } $validated = $request->validate([ - 'rt_id' => 'required|exists:rts,id', - 'no_kk' => 'required|string|max:16', - 'nik' => 'required|digits:16|unique:calon_penerimas,nik,' . $calonPenerima->id, + 'rt_id' => 'nullable|exists:rts,id', + + // ✅ UNIQUE KECUALI DATA SENDIRI + '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' => 'required|string|max:255', + 'desa' => 'nullable|string|max:255', // akan di-override 'pekerjaan' => 'required|string', 'penghasilan' => 'required|numeric|min:0', 'jumlah_tanggungan' => 'required|integer|min:0', @@ -175,12 +216,30 @@ public function update(Request $request, CalonPenerima $calonPenerima) 'bantuan_lain' => 'required|in:ya,tidak', 'usia' => 'required|integer|min:17|max:100', 'status_perkawinan' => 'required|string', + ], [ + 'nik.unique' => 'NIK sudah pernah diinput.', + 'no_kk.unique' => 'No. KK sudah pernah diinput.', ]); + // 🔒 Kunci RT dari user login (anti inject) + $validated['rt_id'] = $myRtId; + + // 🔒 Kunci Dusun/Desa ikut RT + $rt = RT::with('dusun')->find($myRtId); + if ($rt && $rt->dusun) { + $validated['desa'] = $rt->dusun->nama_dusun; + } else { + $validated['desa'] = $validated['desa'] ?? '-'; + } + DB::beginTransaction(); try { $calonPenerima->update($validated); + // (Opsional) Kalau kamu mau: update ulang prediksi ML saat data diupdate + // Kalau mau aktif, uncomment ini: + + /* $predictionData = [ 'pekerjaan' => $validated['pekerjaan'], 'penghasilan' => $validated['penghasilan'], @@ -193,7 +252,7 @@ public function update(Request $request, CalonPenerima $calonPenerima) $prediction = $this->mlService->getPrediction($predictionData); if ($prediction) { - $calonPenerima->prediksiKelayakan()->updateOrCreate( + PrediksiKelayakan::updateOrCreate( ['calon_penerima_id' => $calonPenerima->id], [ 'probability' => $prediction['probability'], @@ -201,6 +260,7 @@ public function update(Request $request, CalonPenerima $calonPenerima) ] ); } + */ DB::commit(); @@ -208,18 +268,41 @@ public function update(Request $request, CalonPenerima $calonPenerima) ->with('success', 'Data calon penerima berhasil diupdate!'); } catch (\Exception $e) { DB::rollBack(); - return redirect()->back() + + return back() ->withInput() ->with('error', 'Terjadi kesalahan: ' . $e->getMessage()); } } + public function ajukan(CalonPenerima $calonPenerima) + { + $user = Auth::user(); + + if ($calonPenerima->user_id !== $user->id) { + abort(403, 'Unauthorized action.'); + } + + if (($calonPenerima->tracking_status ?? 'draft') !== 'draft') { + return redirect()->route('rt.calon-penerima.index') + ->with('error', 'Data ini sudah diajukan dan tidak dapat diajukan lagi.'); + } + + $calonPenerima->update([ + 'tracking_status' => 'terkirim', + ]); + + return redirect()->route('rt.calon-penerima.index') + ->with('success', 'Data berhasil diajukan ke admin kelurahan.'); + } + public function destroy(CalonPenerima $calonPenerima) { $user = Auth::user(); - if ($calonPenerima->user_id !== $user->id || $calonPenerima->status_verifikasi !== 'pending') { - abort(403, 'Unauthorized action.'); + // 🔒 RT hanya bisa hapus kalau miliknya + masih draft + if ($calonPenerima->user_id !== $user->id || ($calonPenerima->tracking_status ?? 'draft') !== 'draft') { + abort(403, 'Data yang sudah diajukan tidak dapat dihapus.'); } $calonPenerima->delete(); @@ -227,4 +310,91 @@ public function destroy(CalonPenerima $calonPenerima) return redirect()->route('rt.calon-penerima.index') ->with('success', 'Data calon penerima berhasil dihapus!'); } + + private function getPredictionExplanation($calonPenerima): array + { + $positive = []; + $negative = []; + + // PEKERJAAN + if (strtolower($calonPenerima->pekerjaan) === 'tidak bekerja') { + $positive[] = 'Tidak bekerja'; + } elseif (in_array(strtolower($calonPenerima->pekerjaan), ['buruh harian', 'buruh', 'petani', 'nelayan'])) { + $positive[] = 'Pekerjaan tergolong rentan secara ekonomi'; + } else { + $negative[] = 'Memiliki pekerjaan yang relatif lebih stabil'; + } + + // PENGHASILAN + if ($calonPenerima->penghasilan <= 1000000) { + $positive[] = 'Penghasilan rendah'; + } elseif ($calonPenerima->penghasilan <= 2000000) { + $positive[] = 'Penghasilan masih tergolong rendah'; + } else { + $negative[] = 'Penghasilan relatif lebih tinggi'; + } + + // JUMLAH TANGGUNGAN + if ($calonPenerima->jumlah_tanggungan >= 4) { + $positive[] = 'Jumlah tanggungan banyak'; + } elseif ($calonPenerima->jumlah_tanggungan >= 2) { + $positive[] = 'Jumlah tanggungan cukup banyak'; + } else { + $negative[] = 'Jumlah tanggungan sedikit'; + } + + // ASET KEPEMILIKAN + $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 ($aset === 'tidak ada' || $aset === '-' || $aset === 'tidak punya') { + $positive[] = 'Tidak memiliki aset berarti'; + } + + // BANTUAN LAIN + if (strtolower($calonPenerima->bantuan_lain) === 'ya') { + $negative[] = 'Sudah menerima bantuan lain'; + } else { + $positive[] = 'Belum menerima bantuan lain'; + } + + // USIA + if ($calonPenerima->usia >= 60) { + $positive[] = 'Usia lanjut'; + } elseif ($calonPenerima->usia >= 45) { + $positive[] = 'Usia cukup rentan'; + } + + // NARASI PENJELASAN + $probability = $calonPenerima->prediksiKelayakan->probability ?? 0; + $probability = $probability <= 1 ? $probability * 100 : $probability; + $probability = number_format($probability, 1); + + $summary = "Nilai kelayakan {$probability}% diperoleh berdasarkan data pekerjaan, penghasilan, jumlah tanggungan, aset kepemilikan, bantuan lain, dan usia."; + + if (count($positive) > 0 && count($negative) > 0) { + $summary .= " Sistem menilai terdapat beberapa faktor yang mendukung kelayakan, namun ada juga faktor yang mengurangi nilai kelayakan."; + } elseif (count($positive) > 0) { + $summary .= " Sebagian besar faktor yang dinilai cenderung mendukung kelayakan penerima bantuan."; + } elseif (count($negative) > 0) { + $summary .= " Sebagian besar faktor yang dinilai cenderung menurunkan tingkat kelayakan."; + } + + return [ + 'positive' => $positive, + 'negative' => $negative, + 'summary' => $summary, + ]; + } } \ No newline at end of file diff --git a/app/Models/CalonPenerima.php b/app/Models/CalonPenerima.php index a2f7f9d..48f05a2 100644 --- a/app/Models/CalonPenerima.php +++ b/app/Models/CalonPenerima.php @@ -10,10 +10,26 @@ 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', 'bantuan_lain', - 'usia', 'status_perkawinan', 'status_verifikasi', 'catatan_admin', + 'user_id', + 'rt_id', + 'no_kk', + 'nik', + 'nama_lengkap', + 'jenis_kelamin', + 'tempat_lahir', + 'tanggal_lahir', + 'alamat', + 'desa', + 'pekerjaan', + 'penghasilan', + 'jumlah_tanggungan', + 'aset_kepemilikan', + 'bantuan_lain', + 'usia', + 'status_perkawinan', + 'status_verifikasi', + 'tracking_status', // status tracking proses + 'catatan_admin', ]; protected $casts = [ diff --git a/app/Services/MLPredictionService.php b/app/Services/MLPredictionService.php index c98e645..dbea3ee 100644 --- a/app/Services/MLPredictionService.php +++ b/app/Services/MLPredictionService.php @@ -2,57 +2,146 @@ namespace App\Services; -use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; class MLPredictionService { - protected $apiUrl; - - public function __construct() - { - $this->apiUrl = config('services.ml_api.url', 'http://127.0.0.1:5000'); - } - /** - * Dapatkan prediksi dari ML API + * Dapatkan prediksi dari script Python langsung */ public function getPrediction(array $data) { try { - /** @var \Illuminate\Http\Client\Response $response */ - $response = Http::timeout(30)->post($this->apiUrl . '/predict', [ - 'pekerjaan' => $data['pekerjaan'], - 'penghasilan' => (float) $data['penghasilan'], - 'jumlah_tanggungan' => (int) $data['jumlah_tanggungan'], - 'aset_kepemilikan' => $data['aset_kepemilikan'], - 'bantuan_lain' => $data['bantuan_lain'], - 'usia' => (int) $data['usia'], - ]); + $payload = [ + 'pekerjaan' => $data['pekerjaan'] ?? '', + 'penghasilan' => (float) ($data['penghasilan'] ?? 0), + 'jumlah_tanggungan' => (int) ($data['jumlah_tanggungan'] ?? 0), + 'aset_kepemilikan' => $data['aset_kepemilikan'] ?? '', + 'bantuan_lain' => $data['bantuan_lain'] ?? '', + 'usia' => (int) ($data['usia'] ?? 0), + ]; - if ($response->successful()) { - return $response->json(); + $jsonPayload = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + + $pythonPath = 'python'; + $scriptPath = base_path('ml/predict.py'); + + $descriptorspec = [ + 0 => ['pipe', 'r'], // stdin + 1 => ['pipe', 'w'], // stdout + 2 => ['pipe', 'w'], // stderr + ]; + + $process = proc_open( + $pythonPath . ' ' . escapeshellarg($scriptPath), + $descriptorspec, + $pipes, + base_path() + ); + + if (!is_resource($process)) { + Log::error('Gagal menjalankan proses Python ML.'); + return null; } - Log::error('ML API Error: ' . $response->body()); - return null; + fwrite($pipes[0], $jsonPayload); + fclose($pipes[0]); - } catch (\Exception $e) { - Log::error('ML API Exception: ' . $e->getMessage()); + $output = stream_get_contents($pipes[1]); + fclose($pipes[1]); + + $errorOutput = stream_get_contents($pipes[2]); + fclose($pipes[2]); + + $exitCode = proc_close($process); + + if ($exitCode !== 0) { + Log::error('Python ML exit code bukan 0.', [ + 'exit_code' => $exitCode, + 'stderr' => $errorOutput, + 'stdout' => $output, + ]); + return null; + } + + $result = json_decode($output, true); + + if (!$result || isset($result['error'])) { + Log::error('Hasil prediksi ML tidak valid.', [ + 'output' => $output, + 'stderr' => $errorOutput, + 'decoded' => $result, + ]); + return null; + } + + return [ + 'probability' => $result['probability'] ?? 0, + 'recommendation' => $result['recommendation'] ?? 'Tidak Layak', + ]; + } catch (\Throwable $e) { + Log::error('ML Prediction Exception: ' . $e->getMessage()); return null; } } /** - * Cek apakah ML API sedang online + * Cek apakah script ML bisa dijalankan */ public function healthCheck() { try { - /** @var \Illuminate\Http\Client\Response $response */ - $response = Http::timeout(5)->get($this->apiUrl . '/health'); - return $response->successful(); - } catch (\Exception $e) { + $pythonPath = 'python'; + $scriptPath = base_path('ml/predict.py'); + + if (!file_exists($scriptPath)) { + return false; + } + + $testPayload = json_encode([ + 'pekerjaan' => 'buruh', + 'penghasilan' => 500000, + 'jumlah_tanggungan' => 4, + 'aset_kepemilikan' => 'motor', + 'bantuan_lain' => 'tidak', + 'usia' => 50, + ]); + + $descriptorspec = [ + 0 => ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ]; + + $process = proc_open( + $pythonPath . ' ' . escapeshellarg($scriptPath), + $descriptorspec, + $pipes, + base_path() + ); + + if (!is_resource($process)) { + return false; + } + + fwrite($pipes[0], $testPayload); + fclose($pipes[0]); + + $output = stream_get_contents($pipes[1]); + fclose($pipes[1]); + + fclose($pipes[2]); + + $exitCode = proc_close($process); + + if ($exitCode !== 0) { + return false; + } + + $result = json_decode($output, true); + + return is_array($result) && isset($result['probability']) && isset($result['recommendation']); + } catch (\Throwable $e) { return false; } } @@ -62,21 +151,12 @@ public function healthCheck() */ public function getBatchPredictions(array $dataArray) { - try { - /** @var \Illuminate\Http\Client\Response $response */ - $response = Http::timeout(60)->post($this->apiUrl . '/batch-predict', [ - 'data' => $dataArray - ]); + $results = []; - if ($response->successful()) { - return $response->json(); - } - - return null; - - } catch (\Exception $e) { - Log::error('ML Batch API Exception: ' . $e->getMessage()); - return null; + foreach ($dataArray as $item) { + $results[] = $this->getPrediction($item); } + + return $results; } } \ No newline at end of file diff --git a/database/migrations/2026_02_07_150000_create_penerima_finals_table.php b/database/migrations/2026_02_07_150000_create_penerima_finals_table.php index b6ebeda..a6c1412 100644 --- a/database/migrations/2026_02_07_150000_create_penerima_finals_table.php +++ b/database/migrations/2026_02_07_150000_create_penerima_finals_table.php @@ -13,18 +13,33 @@ public function up(): void { Schema::create('penerima_finals', function (Blueprint $table) { $table->id(); - $table->foreignId('calon_penerima_id')->constrained('calon_penerimas')->onDelete('cascade'); + + $table->foreignId('calon_penerima_id') + ->constrained('calon_penerimas') + ->onDelete('cascade'); + $table->date('tanggal_penetapan'); - $table->string('periode_bantuan'); // Contoh: "2024 Triwulan 1" + + $table->string('periode_bantuan'); // contoh: 2026 Triwulan 1 + $table->decimal('jumlah_bantuan', 15, 2); - $table->enum('status_pencairan', ['belum_cair', 'sudah_cair'])->default('belum_cair'); + + $table->enum('status_pencairan', [ + 'belum_cair', + 'sudah_cair' + ])->default('belum_cair'); + $table->date('tanggal_pencairan')->nullable(); + $table->timestamps(); }); } + /** + * Reverse the migrations. + */ public function down(): void { Schema::dropIfExists('penerima_finals'); } -}; +}; \ No newline at end of file diff --git a/database/migrations/2026_03_12_002615_add_tracking_status_to_calon_penerimas_table.php b/database/migrations/2026_03_12_002615_add_tracking_status_to_calon_penerimas_table.php new file mode 100644 index 0000000..fc23a01 --- /dev/null +++ b/database/migrations/2026_03_12_002615_add_tracking_status_to_calon_penerimas_table.php @@ -0,0 +1,31 @@ +enum('tracking_status', [ + 'draft', + 'terkirim', + 'sedang_validasi', + 'selesai' + ])->default('draft')->after('status_verifikasi'); + + }); + } + + public function down(): void + { + Schema::table('calon_penerimas', function (Blueprint $table) { + + $table->dropColumn('tracking_status'); + + }); + } +}; \ No newline at end of file diff --git a/ml/predict.py b/ml/predict.py new file mode 100644 index 0000000..7b9a399 --- /dev/null +++ b/ml/predict.py @@ -0,0 +1,71 @@ +import json +import sys + +def predict(data): + pekerjaan = str(data.get("pekerjaan","")).lower() + penghasilan = float(data.get("penghasilan",0)) + tanggungan = int(data.get("jumlah_tanggungan",0)) + aset = str(data.get("aset_kepemilikan","")).lower() + bantuan_lain = str(data.get("bantuan_lain","")).lower() + usia = int(data.get("usia",0)) + + score = 0 + + if pekerjaan in ["buruh", "tidak bekerja", "petani", "nelayan"]: + score += 0.2 + + if penghasilan < 1000000: + score += 0.3 + elif penghasilan < 2000000: + score += 0.2 + + if tanggungan >= 4: + score += 0.2 + + if "mobil" in aset: + score -= 0.2 + + if bantuan_lain == "ya": + score -= 0.1 + + if usia >= 60: + score += 0.1 + + probability = max(0, min(score,1)) + recommendation = "Layak" if probability >= 0.5 else "Tidak Layak" + + return { + "probability": round(probability,4), + "recommendation": recommendation + } + +def read_input(): + """ + Prioritas baca: + 1) STDIN (kalau ada) + 2) argv[1] (kalau ada) + """ + try: + # Coba baca dari STDIN + if not sys.stdin.isatty(): + raw = sys.stdin.read().strip() + if raw: + return json.loads(raw) + except: + pass + + # Fallback ke argv + if len(sys.argv) > 1: + raw = sys.argv[1] + return json.loads(raw) + + raise ValueError("No JSON input provided") + +if __name__ == "__main__": + try: + data = read_input() + result = predict(data) + print(json.dumps(result)) + except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) \ No newline at end of file diff --git a/resources/views/admin/data-warga.blade.php b/resources/views/admin/data-warga.blade.php index b39ffd9..1bc0f49 100644 --- a/resources/views/admin/data-warga.blade.php +++ b/resources/views/admin/data-warga.blade.php @@ -1,594 +1,497 @@ -
+
-

Data Warga

-

Kelola dan pantau seluruh data warga dari semua dusun.

+

Data Warga

+

Kelola dan pantau seluruh data warga dari semua dusun

-
- - - +
+ {{ \Carbon\Carbon::now('Asia/Jakarta')->translatedFormat('l, d F Y') }} — {{ \Carbon\Carbon::now('Asia/Jakarta')->format('H:i') }} WIB
-
+ + + {{-- FLASH --}} + @if(session('success')) +
+ + {{ session('success') }}
+ @endif + @if(session('error')) +
+ + {{ session('error') }} +
+ @endif - {{-- TABLE CARD --}} -
- - {{-- Table header info --}} -
-
-
- - Daftar Warga - @if(isset($wargas) && method_exists($wargas, 'total')) - ({{ $wargas->total() }} data) - @endif - -
- @if(isset($wargas) && method_exists($wargas, 'currentPage') && $wargas->lastPage() > 1) - - Halaman {{ $wargas->currentPage() }} dari {{ $wargas->lastPage() }} - - @endif + {{-- SEARCH --}} +
+
+
+

Pencarian

+
+
+
+ +
+ + @if(request()->filled('q')) + + + + @endif +
+
-
- - - - - - - - - - - - - - - - - - - - - - - - @forelse($wargas ?? [] as $warga) - @php - $prob = optional($warga->prediksiKelayakan)->probability ?? null; - $probPct = $prob !== null ? ($prob <= 1 ? $prob * 100 : $prob) : null; - - $detailPayload = [ - 'nik' => $warga->nik, - 'no_kk' => $warga->no_kk ?? '-', - 'nama_lengkap' => $warga->nama_lengkap, - 'jenis_kelamin' => $warga->jenis_kelamin ?? '-', - 'tempat_lahir' => $warga->tempat_lahir ?? '-', - 'tanggal_lahir' => $warga->tanggal_lahir - ? \Carbon\Carbon::parse($warga->tanggal_lahir)->translatedFormat('d F Y') - : '-', - 'usia' => $warga->usia ?? '-', - 'status_perkawinan' => $warga->status_perkawinan ?? '-', - 'alamat' => $warga->alamat ?? '-', - 'desa' => $warga->desa ?? '-', - 'aset_kepemilikan' => $warga->aset_kepemilikan ?? '-', - 'pekerjaan' => $warga->pekerjaan ?? '-', - 'penghasilan' => $warga->penghasilan ?? 0, - 'jumlah_tanggungan' => $warga->jumlah_tanggungan ?? '-', - 'bantuan_lain' => $warga->bantuan_lain ?? '-', - 'status_verifikasi' => $warga->status_verifikasi ?? '-', - 'dusun' => $warga->rt->dusun->nama_dusun ?? '-', - 'rt' => $warga->rt->nomor_rt ?? '-', - 'probability' => optional($warga->prediksiKelayakan)->probability, - 'recommendation' => optional($warga->prediksiKelayakan)->recommendation, - 'input_oleh' => optional($warga->user)->name ?? (optional($warga->user)->email ?? '-'), - 'created_at' => optional($warga->created_at)->translatedFormat('d F Y, H:i') ?? '-', - ]; - @endphp - - - - {{-- NIK --}} - - - {{-- NAMA --}} - - - {{-- DUSUN / RT --}} - - - {{-- PENGHASILAN --}} - - - {{-- PROBABILITAS --}} - - - {{-- STATUS --}} - - - {{-- AKSI --}} - - - @empty - - - - @endforelse - -
NIKNamaDusun / RTPenghasilanProbabilitasStatusAksi
- {{ $warga->nik }} - -
-
- {{ strtoupper(substr($warga->nama_lengkap, 0, 1)) }} -
-
-
{{ $warga->nama_lengkap }}
-
{{ $warga->pekerjaan ?? '-' }}
-
-
-
-
{{ $warga->rt->dusun->nama_dusun ?? '-' }}
- - RT {{ str_pad($warga->rt->nomor_rt ?? '-', 3, '0', STR_PAD_LEFT) }} - -
- Rp {{ number_format($warga->penghasilan ?? 0, 0, ',', '.') }} - - @if($probPct !== null) -
-
-
-
- - {{ number_format($probPct, 1) }}% - -
- @else - - @endif -
- @if($warga->status_verifikasi === 'pending') - - Pending - - @elseif($warga->status_verifikasi === 'disetujui') - - Disetujui - - @else - - Ditolak - - @endif - -
- {{-- DETAIL --}} - - - @if($warga->status_verifikasi === 'pending') -
- @csrf - -
-
- @csrf - -
- @else - - @endif -
- - {{-- Data JSON tersembunyi --}} - -
-
-
- - - -
-
-

Belum ada data warga

- @if(request()->filled('q')) -

Tidak ada hasil untuk "{{ request('q') }}"

- ← Hapus pencarian - @endif -
-
-
+ {{-- TABLE --}} +
+
+
+
+ + Daftar Warga + @if(isset($wargas) && method_exists($wargas,'total')) + {{ $wargas->total() }} data + @endif +
- - {{-- PAGINATION --}} - @if(isset($wargas) && method_exists($wargas, 'links') && $wargas->hasPages()) -
- {{ $wargas->links() }} -
+ @if(isset($wargas) && method_exists($wargas,'currentPage') && $wargas->lastPage() > 1) + Hal. {{ $wargas->currentPage() }} / {{ $wargas->lastPage() }} @endif
-
+
+ + + + + + + + + + + + + + + + + + @forelse($wargas ?? [] as $warga) + @php + $prob = optional($warga->prediksiKelayakan)->probability ?? null; + $probPct = $prob !== null ? ($prob <= 1 ? $prob * 100 : $prob) : null; + $sc = $probPct !== null ? ($probPct >= 70 ? '#10b981' : ($probPct >= 40 ? '#f59e0b' : '#f43f5e')) : '#e5e7eb'; - {{-- ═══════════════════════════════════════════ - MODAL DETAIL WARGA - ════════════════════════════════════════════ --}} - {{-- Backdrop --}} - + $positive=[]; $negative=[]; + if(strtolower($warga->pekerjaan??'')==='tidak bekerja') $positive[]='Tidak bekerja'; + elseif(in_array(strtolower($warga->pekerjaan??''),['buruh harian','buruh','petani','nelayan'])) $positive[]='Pekerjaan tergolong rentan'; + else $negative[]='Memiliki pekerjaan relatif stabil'; - {{-- Panel — z-50 di atas backdrop --}} - + - {{-- EKONOMI --}} -
-
-
- Data Ekonomi -
-
-
-
Pekerjaan
-
-
-
-
-
Penghasilan
-
-
-
-
-
Jumlah Tanggungan
-
-
-
-
-
Bantuan Lain
-
-
-
-
-
- - {{-- PREDIKSI --}} -
-
-
- Prediksi Kelayakan -
-
- -
-
-
-
Probabilitas
-
-
-
-
-
-
+
+ + + + + + + + + + + + @empty + + @endforelse + +
NIKNamaDusun / RTPenghasilanProbabilitasTrackingAksi
{{ $warga->nik }} +
+
{{ strtoupper(substr($warga->nama_lengkap,0,1)) }}
+
+
{{ $warga->nama_lengkap }}
+
{{ $warga->pekerjaan??'-' }}
-
-
Rekomendasi
-
+
+
{{ $warga->rt->dusun->nama_dusun??'-' }}
+ RT {{ str_pad($warga->rt->nomor_rt??'-',3,'0',STR_PAD_LEFT) }} +
+ Rp {{ number_format($warga->penghasilan??0,0,',','.') }} + + @if($probPct !== null) +
+
+ {{ number_format($probPct,1) }}% +
+ @else + + @endif +
+ @if($trk==='terkirim') + Terkirim + @elseif($trk==='sedang_validasi') + Validasi + @elseif($trk==='selesai') + Selesai + @else + + @endif + +
+ + + @if($trk==='terkirim') +
+ @csrf + +
+ @endif + + @if($trk==='sedang_validasi') + Lanjutkan lewat filterisasi + @endif + + @if($trk==='selesai') + @if(($warga->status_verifikasi ?? '') === 'disetujui') + Diterima + @elseif(($warga->status_verifikasi ?? '') === 'ditolak') + Tidak Diterima + @else + Pending + @endif + +
+ @csrf + +
+ @endif
+ + +
+
+
+ +
+

Belum ada data warga

+ @if(request()->filled('q')) +

Tidak ada hasil untuk "{{ request('q') }}"

+ ← Hapus pencarian + @endif
- +
+
+ + @if(isset($wargas) && method_exists($wargas,'links') && $wargas->hasPages()) +
{{ $wargas->links() }}
+ @endif +
+ + {{-- MODAL --}} + + \ No newline at end of file diff --git a/resources/views/admin/filterisasi.blade.php b/resources/views/admin/filterisasi.blade.php index bf51ae3..821b16c 100644 --- a/resources/views/admin/filterisasi.blade.php +++ b/resources/views/admin/filterisasi.blade.php @@ -4,7 +4,7 @@

Filterisasi

- Penetapan penerima BLT-DD berdasarkan probabilitas (hanya data yang sudah disetujui). + Penetapan penerima BLT-DD berdasarkan probabilitas tertinggi per dusun.

@@ -16,6 +16,7 @@ {{ session('success') }}
@endif + @if(session('error'))
{{ session('error') }} @@ -25,7 +26,7 @@
-

Pilih Dusun & Kuota (Disetujui saja)

+

Pilih Dusun & Kuota

@@ -69,7 +70,7 @@ class="px-4 py-2 text-sm font-semibold bg-gray-100 text-gray-700 rounded-xl hove @@ -83,7 +84,7 @@ class="px-5 py-2 text-sm font-semibold bg-emerald-600 text-white rounded-xl hove
- Kandidat Disetujui (urut probabilitas) + Kandidat Sedang Divalidasi / Sudah Diproses
@@ -91,27 +92,52 @@ class="px-5 py-2 text-sm font-semibold bg-emerald-600 text-white rounded-xl hove + - + + - @forelse(($candidates ?? []) as $w) + @forelse(($candidates ?? []) as $index => $w) @php - $prob = optional($w->prediksiKelayakan)->probability; + $prob = $w->prob ?? optional($w->prediksiKelayakan)->probability; + $probPct = $prob !== null ? ($prob <= 1 ? $prob * 100 : $prob) : null; $picked = isset($pickedIds) ? $pickedIds->contains($w->id) : false; @endphp - - - - + + + + + + + + + + + @empty - @endforelse @@ -138,8 +164,14 @@ class="px-5 py-2 text-sm font-semibold bg-emerald-600 text-white rounded-xl hove @endif -
- Catatan: Kandidat yang ditampilkan hanya yang status_verifikasi = disetujui. Kuota default 7 per dusun. +
+ Catatan: + Kandidat yang tampil di halaman ini adalah data warga yang berada pada tahap + sedang_validasi atau selesai. + Saat tombol Tetapkan Teratas ditekan, sistem otomatis: +
• memilih warga dengan probabilitas tertinggi sesuai kuota +
• menetapkan yang lolos menjadi disetujui +
• menetapkan yang tidak lolos menjadi ditolak
diff --git a/resources/views/auth/register.blade.php b/resources/views/auth/register.blade.php index e339d91..d56ec9c 100644 --- a/resources/views/auth/register.blade.php +++ b/resources/views/auth/register.blade.php @@ -3,79 +3,152 @@ - Register - Sistem BLT-DD + Daftar Akun — SiBantuDes @vite(['resources/css/app.css', 'resources/js/app.js']) + - -
-
-
-
-
- - - -
-
-

Daftar Akun RT

-

Lengkapi data untuk mendaftar

+ +
+
+
+
+ - - @if($errors->any()) -
-
    - @foreach($errors->all() as $error) -
  • {{ $error }}
  • - @endforeach -
-
- @endif - - - @csrf - -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- - - - -
-

- Sudah punya akun? - Masuk di sini -

+
+

SiBantuDes

+

Daftar Akun RT

+

Lengkapi data untuk mendaftar ke sistem

+
+ @if($errors->any()) +
+ +
    @foreach($errors->all() as $e)
  • {{ $e }}
  • @endforeach
+
+ @endif +
+ @csrf +
+ +
+
+ +
+
+
+ +
+
+ +
+
+
+ +
+
+ +
+
+

Dusun mengikuti RT yang dipilih secara otomatis.

+
+
Keamanan Akun
+
+ +
+
+ + +
+
+
+ +
+
+ + +
+
+ + + +
+ +
+ \ No newline at end of file diff --git a/resources/views/rt/calon-penerima/create.blade.php b/resources/views/rt/calon-penerima/create.blade.php index d0aa211..548edcb 100644 --- a/resources/views/rt/calon-penerima/create.blade.php +++ b/resources/views/rt/calon-penerima/create.blade.php @@ -6,31 +6,31 @@
- {{-- TAMPILKAN ERROR VALIDASI --}} -@if ($errors->any()) -
-
Gagal menyimpan:
-
    - @foreach ($errors->all() as $error) -
  • {{ $error }}
  • - @endforeach -
-
-@endif + {{-- TAMPILKAN ERROR VALIDASI --}} + @if ($errors->any()) +
+
Gagal menyimpan:
+
    + @foreach ($errors->all() as $error) +
  • {{ $error }}
  • + @endforeach +
+
+ @endif -{{-- TAMPILKAN ERROR DARI TRY-CATCH CONTROLLER --}} -@if (session('error')) -
- {{ session('error') }} -
-@endif + {{-- TAMPILKAN ERROR DARI TRY-CATCH CONTROLLER --}} + @if (session('error')) +
+ {{ session('error') }} +
+ @endif -{{-- TAMPILKAN SUCCESS --}} -@if (session('success')) -
- {{ session('success') }} -
-@endif + {{-- TAMPILKAN SUCCESS --}} + @if (session('success')) +
+ {{ session('success') }} +
+ @endif
@csrf @@ -41,60 +41,98 @@

Data Identitas

-
- {{-- RT --}} +
+ @php + $myRtId = auth()->user()->rt_id ?? (auth()->user()->rt->id ?? null); + @endphp +
- - + @foreach(($rts ?? []) as $rt) + + @endforeach + + {{-- yang dikirim --}} + +
{{-- NO KK --}}
- - + No. KK * + + + placeholder="16 digit No. KK" + required + >
{{-- NIK --}}
- - + NIK * + + + placeholder="16 digit NIK" + required + >
{{-- NAMA --}}
- - + Nama Lengkap * + + + placeholder="Nama sesuai KTP" + required + >
{{-- JENIS KELAMIN --}}
- - @@ -102,35 +140,63 @@ class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm fo {{-- TEMPAT LAHIR --}}
- - + Tempat Lahir * + + + placeholder="Kota/Kabupaten" + required + >
{{-- TANGGAL LAHIR --}}
- - + Tanggal Lahir * + + + required + >
{{-- USIA --}}
- - + Usia * + + + placeholder="Otomatis dari tanggal lahir" + required + >
{{-- STATUS PERKAWINAN --}}
- - @@ -139,7 +205,6 @@ class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm fo
-
@@ -149,35 +214,63 @@ class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm fo

Data Tempat Tinggal

-
+
{{-- ALAMAT --}}
- - + Alamat * + + + placeholder="Jalan, nomor rumah, RT/RW" + required + >
- {{-- DUSUN --}} + @php + $myDusunName = auth()->user()->rt->dusun->nama_dusun ?? ''; + @endphp +
- - + + + {{-- tampilkan tapi terkunci --}} + + + {{-- yang dikirim --}} + +
{{-- ASET --}}
- - + Aset Kepemilikan * + + + placeholder="Misal: Rumah, Motor, dll" + required + >
-
@@ -187,59 +280,90 @@ class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm fo

Data Ekonomi

-
+
{{-- PEKERJAAN --}}
- - + Pekerjaan * + + + placeholder="Jenis pekerjaan" + required + >
{{-- PENGHASILAN --}}
- - + Penghasilan (Rp) * + + + placeholder="0" + required + >
{{-- JUMLAH TANGGUNGAN --}}
- - + Jumlah Tanggungan * + + + placeholder="Jumlah orang" + required + >
{{-- BANTUAN LAIN --}}
- -
-
{{-- ACTIONS --}}
- + Batal -
- {{-- TOAST NOTIFIKASI --}} @@ -338,5 +462,4 @@ function showToast(msg) { } }); - \ No newline at end of file diff --git a/resources/views/rt/calon-penerima/edit.blade.php b/resources/views/rt/calon-penerima/edit.blade.php index b6d3534..1dc8afd 100644 --- a/resources/views/rt/calon-penerima/edit.blade.php +++ b/resources/views/rt/calon-penerima/edit.blade.php @@ -5,7 +5,7 @@

Edit Pendataan Warga

Perbarui data dengan benar sebelum menyimpan.

- @@ -32,10 +32,13 @@ class="inline-flex items-center gap-1.5 px-4 py-2 bg-white border border-gray-20 @endif -
+
@csrf @method('PUT') + {{-- hidden aman, walau nanti tetap di-override controller --}} +
@@ -44,19 +47,14 @@ class="inline-flex items-center gap-1.5 px-4 py-2 bg-white border border-gray-20
- {{-- RT --}} + {{-- RT (readonly) --}}
- - - + + +

RT mengikuti akun RT yang sedang login.

{{-- NO KK --}} @@ -113,7 +111,7 @@ class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm fo
@@ -162,13 +160,14 @@ class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm fo
+ {{-- DUSUN (readonly) --}}
- + - + class="w-full border border-gray-200 rounded-xl px-3 py-2 text-sm bg-gray-100 text-gray-700 cursor-not-allowed" + readonly> +

Dusun mengikuti RT dan tidak dapat diubah.

@@ -264,12 +263,14 @@ function showError(id, msg) { if (input) input.classList.add('border-red-400', 'bg-red-50'); if (err) { err.textContent = msg; err.classList.remove('hidden'); } } + function clearError(id) { const input = document.getElementById(id); const err = input?.parentElement?.querySelector('.error-msg'); if (input) input.classList.remove('border-red-400', 'bg-red-50'); if (err) err.classList.add('hidden'); } + function showToast(msg) { const t = document.getElementById('toast'); document.getElementById('toast-msg').textContent = msg; @@ -278,20 +279,19 @@ function showToast(msg) { } const rules = { - rt_id: v => v !== '' ? null : 'RT wajib dipilih.', - nik: v => /^\d{16}$/.test(v) ? null : 'NIK harus tepat 16 digit angka.', - no_kk: v => /^\d{16}$/.test(v) ? null : 'No. KK harus tepat 16 digit angka.', - nama_lengkap: v => v.trim().length >= 3 ? null : 'Nama minimal 3 karakter.', - tempat_lahir: v => v.trim().length >= 2 ? null : 'Tempat lahir wajib diisi.', - tanggal_lahir: v => v !== '' ? null : 'Tanggal lahir wajib diisi.', + nik: v => /^\d{16}$/.test(v) ? null : 'NIK harus tepat 16 digit angka.', + no_kk: v => /^\d{16}$/.test(v) ? null : 'No. KK harus tepat 16 digit angka.', + nama_lengkap: v => v.trim().length >= 3 ? null : 'Nama minimal 3 karakter.', + tempat_lahir: v => v.trim().length >= 2 ? null : 'Tempat lahir wajib diisi.', + tanggal_lahir: v => v !== '' ? null : 'Tanggal lahir wajib diisi.', usia: v => (parseInt(v) >= 17 && parseInt(v) <= 100) ? null : 'Usia harus antara 17–100.', - status_perkawinan: v => v !== '' ? null : 'Status perkawinan wajib dipilih.', - alamat: v => v.trim().length >= 5 ? null : 'Alamat minimal 5 karakter.', - desa: v => v.trim().length >= 2 ? null : 'Dusun wajib diisi.', - aset_kepemilikan: v => v.trim().length >= 2 ? null : 'Aset kepemilikan wajib diisi.', - pekerjaan: v => v.trim().length >= 2 ? null : 'Pekerjaan wajib diisi.', - penghasilan: v => parseFloat(v) >= 0 ? null : 'Penghasilan tidak boleh negatif.', - jumlah_tanggungan: v => parseInt(v) >= 0 ? null : 'Jumlah tanggungan tidak boleh negatif.', + status_perkawinan: v => v !== '' ? null : 'Status perkawinan wajib dipilih.', + alamat: v => v.trim().length >= 5 ? null : 'Alamat minimal 5 karakter.', + desa: v => v.trim().length >= 2 ? null : 'Dusun wajib diisi.', + aset_kepemilikan: v => v.trim().length >= 2 ? null : 'Aset kepemilikan wajib diisi.', + pekerjaan: v => v.trim().length >= 2 ? null : 'Pekerjaan wajib diisi.', + penghasilan: v => parseFloat(v) >= 0 ? null : 'Penghasilan tidak boleh negatif.', + jumlah_tanggungan: v => parseInt(v) >= 0 ? null : 'Jumlah tanggungan tidak boleh negatif.', }; Object.keys(rules).forEach(id => { diff --git a/resources/views/rt/calon-penerima/index.blade.php b/resources/views/rt/calon-penerima/index.blade.php index 0700b4e..0bbda86 100644 --- a/resources/views/rt/calon-penerima/index.blade.php +++ b/resources/views/rt/calon-penerima/index.blade.php @@ -1,176 +1,219 @@ -
+ - {{-- NOTIFIKASI --}} - @if (session('success')) -
- - - + + + {{-- FLASH --}} + @if(session('success')) +
+ {{ session('success') }}
@endif - - @if (session('error')) -
- - - + @if(session('error')) +
+ {{ session('error') }}
@endif -
+
- {{-- TABLE HEADER INFO --}} -
-
-
- - Total: {{ $calonPenerimas->total() }} data + {{-- HEADER BAR --}} +
+
+
+ + Total {{ $calonPenerimas->total() }} data
- - Halaman {{ $calonPenerimas->currentPage() }} dari {{ $calonPenerimas->lastPage() }} + + Halaman {{ $calonPenerimas->currentPage() }} / {{ $calonPenerimas->lastPage() }}
-
-
Ranking Nama NIK DusunProbProbabilitasStatus Final
{{ $w->nama_lengkap }}{{ $w->nik }}{{ optional(optional($w->rt)->dusun)->nama_dusun ?? '-' }} - {{ $prob !== null ? number_format((float)$prob, 4) : '-' }} + + {{ (($candidates->currentPage() - 1) * $candidates->perPage()) + $index + 1 }} {{ $w->nama_lengkap }}{{ $w->nik }}{{ optional(optional($w->rt)->dusun)->nama_dusun ?? '-' }} + @if($probPct !== null) + {{ number_format((float)$probPct, 1) }}% + @else + - + @endif + + @if(($w->status_verifikasi ?? '') === 'pending') + Pending + @elseif(($w->status_verifikasi ?? '') === 'disetujui') + Disetujui + @else + Ditolak + @endif + @if($picked) Terpilih @@ -122,8 +148,8 @@ class="px-5 py-2 text-sm font-semibold bg-emerald-600 text-white rounded-xl hove
- Pilih dusun dulu untuk menampilkan kandidat. + + Pilih dusun dulu untuk menampilkan kandidat filterisasi.
+ {{-- TABLE --}} +
+
- - - - - - - - + + + + + + + + + - - - @forelse ($calonPenerimas as $index => $item) - - - {{-- NO --}} - + @forelse($calonPenerimas as $index => $item) + + {{-- No --}} + - {{-- NAMA --}} - {{-- NIK --}} - {{-- RT --}} - - {{-- DUSUN --}} - - {{-- STATUS --}} - - {{-- AKSI --}} - + + {{-- Aksi --}} + - @empty - @@ -180,9 +223,10 @@ class="inline-flex items-center gap-1 px-2.5 py-1 text-xs font-medium bg-rose-10 {{-- PAGINATION --}} -
+
{{ $calonPenerimas->links() }}
+ \ No newline at end of file diff --git a/resources/views/rt/calon-penerima/show.blade.php b/resources/views/rt/calon-penerima/show.blade.php index e94ab98..1be2896 100644 --- a/resources/views/rt/calon-penerima/show.blade.php +++ b/resources/views/rt/calon-penerima/show.blade.php @@ -1,290 +1,413 @@ -
-
-

Detail Calon Penerima

-

Detail data pendataan warga dan hasil prediksi.

-
-
+
+ + @if(($calonPenerima->tracking_status ?? 'draft') === 'draft') + + + + + Edit Data + + @endif
- {{-- NOTIFIKASI --}} - @if (session('success')) -
- - - + + + @php + $tracking = $calonPenerima->tracking_status ?? 'draft'; + $st = $calonPenerima->status_verifikasi ?? 'pending'; + $pred = $calonPenerima->prediksiKelayakan ?? null; + $prob = $pred ? (float)$pred->probability : null; + $rec = $pred ? ($pred->recommendation ?? '-') : null; + $probPct = $prob !== null ? ($prob <= 1 ? $prob * 100 : $prob) : null; + $posTop = array_slice($explanation['positive'] ?? [], 0, 3); + $negTop = array_slice($explanation['negative'] ?? [], 0, 2); + $circum = round(2 * M_PI * 24, 2); + $offset = $probPct !== null ? round($circum * (1 - $probPct / 100), 2) : $circum; + $sc = $probPct !== null ? ($probPct >= 70 ? '#10b981' : ($probPct >= 40 ? '#f59e0b' : '#f43f5e')) : '#e5e7eb'; + $recBg = $probPct !== null ? ($probPct >= 70 ? 'background:#f0fdf4;color:#166534;border:1px solid #bbf7d0;' : ($probPct >= 40 ? 'background:#fffbeb;color:#b45309;border:1px solid #fde68a;' : 'background:#fff1f2;color:#9f1239;border:1px solid #fecdd3;')) : ''; + + $trackingLabel = match($tracking) { + 'draft' => 'Draft', + 'terkirim' => 'Terkirim ke Kelurahan', + 'sedang_validasi' => 'Sedang Divalidasi', + 'selesai' => 'Data Sudah Tervalidasi', + default => '-' + }; + + $hasilLabel = match($st) { + 'disetujui' => 'Diterima', + 'ditolak' => 'Tidak Diterima', + default => 'Menunggu Hasil' + }; + @endphp + + {{-- FLASH --}} + @if(session('success')) +
+ {{ session('success') }}
@endif - @if (session('error')) -
- - - + @if(session('error')) +
+ {{ session('error') }}
@endif - {{-- IDENTITY BANNER --}} -
-
+ {{-- BANNER --}} +
+
+
+ +
{{ strtoupper(substr($calonPenerima->nama_lengkap ?? 'U', 0, 1)) }}
-
-

{{ $calonPenerima->nama_lengkap ?? '-' }}

-

NIK: {{ $calonPenerima->nik ?? '-' }}

+ +
+

{{ $calonPenerima->nama_lengkap ?? '-' }}

+

NIK: {{ $calonPenerima->nik ?? '-' }}

+
+ + + {{ $trackingLabel }} + +
-
- @php $st = $calonPenerima->status_verifikasi ?? 'pending'; @endphp - @if ($st === 'pending') - - Pending - - @elseif ($st === 'disetujui') - - Disetujui - + +
+ @if($probPct !== null) +
+
{{ number_format($probPct,0) }}%
+
probabilitas
+
+ @endif + + @if($tracking === 'selesai') + @if($st === 'disetujui') + Diterima + @elseif($st === 'ditolak') + Tidak Diterima + @else + Menunggu Hasil + @endif @else - - Ditolak - + {{ $trackingLabel }} @endif
-
+ {{-- MAIN GRID --}} +
- {{-- KOLOM KIRI: DATA UTAMA --}} -
+ {{-- KIRI --}} +
- {{-- IDENTITAS --}} -
-
-
-

Data Identitas

+
+ {{-- Identitas --}} +
+

Identitas

+
+
+

No. KK

{{ $calonPenerima->no_kk ?? '-' }}

+

Jenis Kelamin

{{ $calonPenerima->jenis_kelamin ?? '-' }}

+

Tempat Lahir

{{ $calonPenerima->tempat_lahir ?? '-' }}

+

Tanggal Lahir

{{ $calonPenerima->tanggal_lahir ? \Carbon\Carbon::parse($calonPenerima->tanggal_lahir)->translatedFormat('d F Y') : '-' }}

+

Usia

{{ $calonPenerima->usia ?? '-' }} tahun

+

Status Kawin

{{ $calonPenerima->status_perkawinan ?? '-' }}

+
+
-
- @php - $identitas = [ - 'No. KK' => $calonPenerima->no_kk ?? '-', - 'Jenis Kelamin' => $calonPenerima->jenis_kelamin ?? '-', - 'Tempat Lahir' => $calonPenerima->tempat_lahir ?? '-', - 'Tanggal Lahir' => $calonPenerima->tanggal_lahir - ? \Carbon\Carbon::parse($calonPenerima->tanggal_lahir)->translatedFormat('d F Y') - : '-', - 'Usia' => ($calonPenerima->usia ?? '-') . ' tahun', - 'Status Perkawinan' => $calonPenerima->status_perkawinan ?? '-', - ]; - @endphp - @foreach($identitas as $label => $val) + + {{-- Tempat Tinggal --}} +
+

Tempat Tinggal

+
+

Alamat

{{ $calonPenerima->alamat ?? '-' }}

+
+

Dusun

{{ $calonPenerima->desa ?? '-' }}

+
+

RT

+ RT {{ str_pad($calonPenerima->rt->nomor_rt ?? 0,3,'0',STR_PAD_LEFT) }} +
+
+

Aset Kepemilikan

{{ $calonPenerima->aset_kepemilikan ?? '-' }}

+
+
+
+ + {{-- Ekonomi --}} +
+

Data Ekonomi

+
+
+

Pekerjaan

{{ $calonPenerima->pekerjaan ?? '-' }}

+

Penghasilan

Rp {{ number_format((float)($calonPenerima->penghasilan??0),0,',','.') }}

+

Tanggungan

{{ $calonPenerima->jumlah_tanggungan ?? '-' }} orang

-
{{ $label }}
-
{{ $val }}
+

Bantuan Lain

+ @if(($calonPenerima->bantuan_lain??'') === 'ya') + Ya + @else + Tidak + @endif
- @endforeach -
-
- - {{-- TEMPAT TINGGAL --}} -
-
-
-

Data Tempat Tinggal

-
-
-
-
Alamat
-
{{ $calonPenerima->alamat ?? '-' }}
-
-
-
Dusun
-
{{ $calonPenerima->desa ?? '-' }}
-
-
-
RT
-
- - RT {{ str_pad($calonPenerima->rt->nomor_rt ?? 0, 3, '0', STR_PAD_LEFT) }} - -
-
-
-
Aset Kepemilikan
-
{{ $calonPenerima->aset_kepemilikan ?? '-' }}
- {{-- EKONOMI --}} -
-
-
-

Data Ekonomi

-
-
-
-
Pekerjaan
-
{{ $calonPenerima->pekerjaan ?? '-' }}
-
-
-
Penghasilan
-
- Rp {{ number_format((float)($calonPenerima->penghasilan ?? 0), 0, ',', '.') }} + {{-- Tracking Pengajuan --}} +
+

Tracking Pengajuan

+
+
+
+
1
+
+
Data dibuat RT
+
Data masih bisa diubah dan dihapus sebelum diajukan.
+
+
+
+ +
+
2
+
+
Data diajukan ke kelurahan
+
+ @if(in_array($tracking, ['terkirim','sedang_validasi','selesai'])) + Data sudah dikirim dan tidak bisa diubah lagi. + @else + Menunggu tindakan RT untuk mengajukan data. + @endif +
+
+
+
+ +
+
3
+
+
Proses validasi & filterisasi
+
+ @if(in_array($tracking, ['sedang_validasi','selesai'])) + Admin sedang atau sudah memproses hasil bantuan berdasarkan kuota dan probabilitas. + @else + Menunggu admin memulai proses validasi. + @endif +
+
+
+
+ +
+
4
+
+
Hasil akhir ditetapkan
+
+ @if($tracking === 'selesai') + Hasil akhir bantuan telah ditetapkan: {{ $hasilLabel }}. + @else + Menunggu hasil akhir dari admin kelurahan. + @endif +
+
-
-
Jumlah Tanggungan
-
{{ $calonPenerima->jumlah_tanggungan ?? '-' }} orang
-
-
-
Bantuan Lain
- @if(($calonPenerima->bantuan_lain ?? '') === 'ya') - Ya +
+
+ + {{-- Status Pengajuan --}} +
+

Status Pengajuan

+
+ @if($tracking === 'draft') + Draft + @elseif($tracking === 'terkirim') + Terkirim ke Kelurahan + @elseif($tracking === 'sedang_validasi') + Sedang Divalidasi + @elseif($tracking === 'selesai') + @if($st === 'disetujui') + Diterima + @elseif($st === 'ditolak') + Tidak Diterima @else - Tidak + Menunggu Hasil @endif + @endif + +
+

Dibuat

{{ $calonPenerima->created_at ? \Carbon\Carbon::parse($calonPenerima->created_at)->translatedFormat('d M Y, H:i') : '-' }}

+

Diperbarui

{{ $calonPenerima->updated_at ? \Carbon\Carbon::parse($calonPenerima->updated_at)->translatedFormat('d M Y, H:i') : '-' }}

+ + @if(!empty($calonPenerima->catatan_admin)) +
+ Catatan: {{ $calonPenerima->catatan_admin }} +
+ @endif
- {{-- KOLOM KANAN --}} -
+ {{-- KANAN --}} +
- {{-- STATUS PENGAJUAN --}} -
-
-
-

Status Pengajuan

-
-
- @if ($st === 'pending') -
- - Menunggu Verifikasi -
- @elseif ($st === 'disetujui') -
- - Pengajuan Disetujui -
- @else -
- - Pengajuan Ditolak -
- @endif - -
-
- Dibuat - - {{ $calonPenerima->created_at ? \Carbon\Carbon::parse($calonPenerima->created_at)->translatedFormat('d M Y, H:i') : '-' }} - -
-
- Diperbarui - - {{ $calonPenerima->updated_at ? \Carbon\Carbon::parse($calonPenerima->updated_at)->translatedFormat('d M Y, H:i') : '-' }} - -
-
- - @if(!empty($calonPenerima->catatan_admin)) -
-
Catatan Admin
- {{ $calonPenerima->catatan_admin }} -
- @endif -
-
- - {{-- PREDIKSI --}} -
-
-
-

Prediksi Kelayakan

-
-
- @php - $pred = $calonPenerima->prediksiKelayakan ?? null; - $prob = $pred ? (float)$pred->probability : null; - $rec = $pred ? ($pred->recommendation ?? '-') : null; - $probPct = $prob !== null ? ($prob <= 1 ? $prob * 100 : $prob) : null; - @endphp - - @if($pred) -
-
{{ number_format($probPct, 1) }}%
-
Probabilitas Kelayakan
-
- - {{-- Progress bar --}} -
-
-
+ {{-- Prediksi --}} +
+

Prediksi Kelayakan

+ @if($pred) +
+
+
+ + + + +
+ {{ number_format($probPct,0) }}% +
+
+
+

Rekomendasi

+ {{ $rec }} +
+
-
- {{ $rec }} -
- @else -
-
- - - + @if(!empty($posTop)) +
+

Faktor Pendukung

+
    @foreach($posTop as $item)
  • {{ $item }}
  • @endforeach
-

Belum ada data prediksi

+ @endif + + @if(!empty($negTop)) +
+

Faktor Pengurang

+
    @foreach($negTop as $item)
  • {{ $item }}
  • @endforeach
+
+ @endif + +
+ Penjelasan Lengkap +
+
+ @foreach(['Pekerjaan'=>$calonPenerima->pekerjaan??'-','Penghasilan'=>'Rp '.number_format((float)($calonPenerima->penghasilan??0),0,',','.'),'Tanggungan'=>($calonPenerima->jumlah_tanggungan??'-').' orang','Aset'=>$calonPenerima->aset_kepemilikan??'-','Bantuan Lain'=>ucfirst($calonPenerima->bantuan_lain??'-'),'Usia'=>($calonPenerima->usia??'-').' tahun'] as $lbl=>$val) +
{{ $lbl }}{{ $val }}
+ @endforeach +
+ @if(!empty($explanation['positive'])) +
+

Semua Pendukung

+
    @foreach($explanation['positive'] as $i)
  • {{ $i }}
  • @endforeach
+
+ @endif + @if(!empty($explanation['negative'])) +
+

Semua Pengurang

+
    @foreach($explanation['negative'] as $i)
  • {{ $i }}
  • @endforeach
+
+ @endif + @if(!empty($explanation['summary'])) +
+

Ringkasan

+

{{ $explanation['summary'] }}

+
+ @endif +
+
+
+ @else +
+
+
- @endif -
+

Belum ada data prediksi

+
+ @endif
- {{-- AKSI HAPUS --}} - @if(($calonPenerima->status_verifikasi ?? '') === 'pending') -
-
-
-

Zona Bahaya

+ {{-- Hasil Akhir --}} + @if($tracking === 'selesai') +
+

Hasil Akhir Bantuan

+
+ @if($st === 'disetujui') +
+
Selamat, data Anda diterima.
+
Warga ini termasuk penerima bantuan setelah proses validasi dan filterisasi kuota.
+
+ @elseif($st === 'ditolak') +
+
Data tidak masuk penerima final.
+
Warga ini belum terpilih pada penetapan penerima bantuan untuk periode ini.
+
+ @else +
+
Hasil akhir belum ditetapkan.
+
Masih menunggu keputusan akhir dari admin kelurahan.
+
+ @endif
-
-

Hapus data ini secara permanen. Tindakan ini tidak dapat dibatalkan.

-
- @csrf - @method('DELETE') -
+ @endif + + {{-- Zona Bahaya --}} + @if($tracking === 'draft') +
+

Zona Bahaya

+
+

Hapus data ini secara permanen. Tindakan ini tidak dapat dibatalkan.

+ + @csrf @method('DELETE') + @@ -294,4 +417,5 @@ class="w-full inline-flex items-center justify-center gap-2 px-4 py-2 rounded-xl
+ \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index 70c6bee..4db4b64 100644 --- a/routes/web.php +++ b/routes/web.php @@ -10,13 +10,10 @@ use App\Http\Controllers\Admin\AdminController; use App\Http\Controllers\Admin\FilterisasiController; -// RT Controllers (folder Rt) +// RT Controllers use App\Http\Controllers\Rt\DashboardController as RtDashboardController; use App\Http\Controllers\Rt\CalonPenerimaController as RtCalonPenerimaController; -// Profile default (bawaan Breeze/Jetstream) -use App\Http\Controllers\ProfileController as UserProfileController; - /* |-------------------------------------------------------------------------- | ROOT @@ -49,7 +46,6 @@ abort(403, 'Role tidak dikenali.'); })->middleware(['auth', 'verified'])->name('dashboard'); - /* |-------------------------------------------------------------------------- | ADMIN ROUTES (ADMIN ONLY) @@ -67,18 +63,28 @@ abort(403, 'Akses ditolak: hanya Admin.'); } - $totalWarga = CalonPenerima::count(); - $totalDusun = Dusun::count(); - $totalPenerima = CalonPenerima::where('status_verifikasi', 'disetujui')->count(); - $totalPending = CalonPenerima::where('status_verifikasi', 'pending')->count(); + $visibleStatuses = ['terkirim', 'sedang_validasi', 'selesai']; + + $totalWarga = CalonPenerima::whereIn('tracking_status', $visibleStatuses)->count(); + $totalDusun = Dusun::count(); + $totalPenerima = CalonPenerima::whereIn('tracking_status', $visibleStatuses) + ->where('status_verifikasi', 'disetujui') + ->count(); + $totalPending = CalonPenerima::whereIn('tracking_status', $visibleStatuses) + ->where('status_verifikasi', 'pending') + ->count(); $statusDistribution = CalonPenerima::select('status_verifikasi as status', DB::raw('COUNT(*) as total')) + ->whereIn('tracking_status', $visibleStatuses) ->groupBy('status_verifikasi') ->get(); $wargaPerDusun = DB::table('dusuns') ->leftJoin('rts', 'rts.dusun_id', '=', 'dusuns.id') - ->leftJoin('calon_penerimas', 'calon_penerimas.rt_id', '=', 'rts.id') + ->leftJoin('calon_penerimas', function ($join) use ($visibleStatuses) { + $join->on('calon_penerimas.rt_id', '=', 'rts.id') + ->whereIn('calon_penerimas.tracking_status', $visibleStatuses); + }) ->select('dusuns.nama_dusun as dusun', DB::raw('COUNT(calon_penerimas.id) as total')) ->groupBy('dusuns.id', 'dusuns.nama_dusun') ->orderBy('dusuns.nama_dusun') @@ -97,6 +103,7 @@ 'prediksi_kelayakans.probability as probabilitas', 'calon_penerimas.status_verifikasi as status' ) + ->whereIn('calon_penerimas.tracking_status', $visibleStatuses) ->orderBy('prediksi_kelayakans.probability', 'desc') ->limit(10) ->get(); @@ -112,7 +119,6 @@ )); })->name('dashboard'); - // DATA WARGA + SEARCH Route::get('/data-warga', function (Request $request) { $user = $request->user(); @@ -124,6 +130,7 @@ $q = trim((string) $request->query('q', '')); $wargasQuery = CalonPenerima::with(['rt.dusun', 'prediksiKelayakan', 'user']) + ->whereIn('tracking_status', ['terkirim', 'sedang_validasi', 'selesai']) ->latest(); if ($q !== '') { @@ -139,48 +146,70 @@ } $wargas = $wargasQuery->paginate(10)->withQueryString(); + return view('admin.data-warga', compact('dusuns', 'wargas')); })->name('data-warga'); - - // SETUJUI / TOLAK - Route::post('/data-warga/{id}/setujui', function (Request $request, $id) { + // MULAI VALIDASI + Route::post('/data-warga/{id}/mulai-validasi', function (Request $request, $id) { $user = $request->user(); if (!$user || $user->role !== 'admin') { abort(403, 'Akses ditolak: hanya Admin.'); } $warga = CalonPenerima::findOrFail($id); - $warga->update(['status_verifikasi' => 'disetujui']); - return back()->with('success', 'Data warga berhasil disetujui.'); - })->name('data-warga.setujui'); - Route::post('/data-warga/{id}/tolak', function (Request $request, $id) { + if ($warga->tracking_status !== 'terkirim') { + return back()->with('error', 'Data ini belum bisa masuk tahap validasi.'); + } + + $warga->update([ + 'tracking_status' => 'sedang_validasi', + 'status_verifikasi' => 'pending', + ]); + + return back()->with('success', 'Proses validasi data telah dimulai.'); + })->name('data-warga.mulai-validasi'); + + // KIRIM HASIL VALIDASI KE RT + Route::post('/data-warga/{id}/selesai-validasi', function (Request $request, $id) { $user = $request->user(); if (!$user || $user->role !== 'admin') { abort(403, 'Akses ditolak: hanya Admin.'); } $warga = CalonPenerima::findOrFail($id); - $warga->update(['status_verifikasi' => 'ditolak']); - return back()->with('success', 'Data warga berhasil ditolak.'); - })->name('data-warga.tolak'); + if ($warga->tracking_status !== 'sedang_validasi') { + return back()->with('error', 'Data ini belum berada pada tahap sedang divalidasi.'); + } + + if (!in_array($warga->status_verifikasi, ['disetujui', 'ditolak'])) { + return back()->with('error', 'Hasil akhir belum ditetapkan. Silakan proses filterisasi terlebih dahulu.'); + } + + $warga->update([ + 'tracking_status' => 'selesai', + ]); + + return back()->with('success', 'Hasil validasi berhasil dikirim ke RT.'); + })->name('data-warga.selesai-validasi'); // DATA AKUN (AdminController) Route::get('/data-akun', [AdminController::class, 'dataAkun'])->name('data-akun'); Route::post('/data-akun/{id}/role', [AdminController::class, 'ubahRole'])->name('data-akun.role'); Route::post('/data-akun/{id}/toggle-aktif', [AdminController::class, 'toggleAktif'])->name('data-akun.toggle-aktif'); - Route::post('/data-akun/{id}/hapus', [AdminController::class, 'hapusUser'])->name('data-akun.hapus'); + Route::delete('/data-akun/{id}/hapus', [AdminController::class, 'hapusUser'])->name('data-akun.hapus'); - // Placeholder + // FILTERISASI Route::get('/filterisasi', [FilterisasiController::class, 'index'])->name('filterisasi'); Route::post('/filterisasi/tetapkan', [FilterisasiController::class, 'tetapkan'])->name('filterisasi.tetapkan'); Route::post('/filterisasi/reset', [FilterisasiController::class, 'resetDusun'])->name('filterisasi.reset'); + + // LAPORAN Route::get('/laporan', fn () => view('admin.laporan'))->name('laporan'); }); - /* |-------------------------------------------------------------------------- | RT ROUTES (RT ONLY) @@ -192,23 +221,14 @@ ->group(function () { // DASHBOARD RT - Route::get('/dashboard', [RtDashboardController::class, 'index']) - ->name('dashboard'); + Route::get('/dashboard', [RtDashboardController::class, 'index'])->name('dashboard'); - // Resource RT + // AJUKAN DATA KE ADMIN + Route::patch('/calon-penerima/{calonPenerima}/ajukan', [RtCalonPenerimaController::class, 'ajukan']) + ->name('calon-penerima.ajukan'); + + // RESOURCE RT Route::resource('calon-penerima', RtCalonPenerimaController::class); }); - -/* -|-------------------------------------------------------------------------- -| PROFILE -|-------------------------------------------------------------------------- -*/ -Route::middleware('auth')->group(function () { - Route::get('/profile', [UserProfileController::class, 'edit'])->name('profile.edit'); - Route::patch('/profile', [UserProfileController::class, 'update'])->name('profile.update'); - Route::delete('/profile', [UserProfileController::class, 'destroy'])->name('profile.destroy'); -}); - require __DIR__ . '/auth.php'; \ No newline at end of file
NoNamaNIKRTDusunStatusAksi
#NamaNIKRTDusunTrackingStatusAksi
+
{{ $calonPenerimas->firstItem() + $index }} -
-
- - {{ strtoupper(substr($item->nama_lengkap, 0, 1)) }} - -
- {{ $item->nama_lengkap }} + {{-- Nama --}} +
+
+
{{ strtoupper(substr($item->nama_lengkap, 0, 1)) }}
+ {{ $item->nama_lengkap }}
+ {{ $item->nik }} - - RT {{ str_pad($item->rt->nomor_rt ?? '-', 3, '0', STR_PAD_LEFT) }} - + + RT {{ str_pad($item->rt->nomor_rt ?? '-', 3, '0', STR_PAD_LEFT) }} + {{-- Dusun --}} + {{ $item->rt->dusun->nama_dusun ?? '-' }} - @if ($item->status_verifikasi === 'pending') - - - Pending - - @elseif ($item->status_verifikasi === 'disetujui') - - - Disetujui - + {{-- Tracking --}} + + @php $trk = $item->tracking_status ?? 'draft'; @endphp + @if($trk === 'draft') + Draft + @elseif($trk === 'terkirim') + Terkirim + @elseif($trk === 'sedang_validasi') + Validasi + @elseif($trk === 'selesai') + Selesai @else - - - Ditolak - + @endif - + @if($item->status_verifikasi === 'pending') + Pending + @elseif($item->status_verifikasi === 'disetujui') + Disetujui + @else + Ditolak + @endif + +
+ + Detail - @if ($item->status_verifikasi === 'pending') - - - - + @if($trk === 'draft') + + Edit -
- @csrf - @method('DELETE') - +
+ +
+ @csrf @method('DELETE') +
@endif
-
-
- - - +
+
+
+
-

Belum ada data calon penerima

- + Tambah sekarang +

Belum ada data calon penerima

+ + Tambah sekarang