perbaikan view controller dan migration untuk admin dan rt

This commit is contained in:
Ryanpratama01 2026-03-14 00:50:27 +07:00
parent 8b9581f103
commit b0140be0a8
16 changed files with 2012 additions and 1237 deletions

View File

@ -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.');
}

View File

@ -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));
}
}
}

View File

@ -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,
];
}
}

View File

@ -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 = [

View File

@ -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;
}
}

View File

@ -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');
}
};
};

View File

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('calon_penerimas', function (Blueprint $table) {
$table->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');
});
}
};

71
ml/predict.py Normal file
View File

@ -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)

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@
<div>
<h2 class="font-semibold text-xl text-gray-800 leading-tight">Filterisasi</h2>
<p class="text-sm text-gray-500 mt-0.5">
Penetapan penerima BLT-DD berdasarkan probabilitas (hanya data yang sudah <b>disetujui</b>).
Penetapan penerima BLT-DD berdasarkan probabilitas tertinggi per dusun.
</p>
</div>
</x-slot>
@ -16,6 +16,7 @@
{{ session('success') }}
</div>
@endif
@if(session('error'))
<div class="rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
{{ session('error') }}
@ -25,7 +26,7 @@
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 p-5">
<div class="flex items-center gap-2 mb-4">
<div class="w-1 h-4 bg-blue-600 rounded-full"></div>
<h3 class="text-sm font-semibold text-gray-700">Pilih Dusun & Kuota (Disetujui saja)</h3>
<h3 class="text-sm font-semibold text-gray-700">Pilih Dusun & Kuota</h3>
</div>
<form method="GET" action="{{ route('admin.filterisasi') }}" class="grid grid-cols-1 md:grid-cols-4 gap-3">
@ -69,7 +70,7 @@ class="px-4 py-2 text-sm font-semibold bg-gray-100 text-gray-700 rounded-xl hove
<input type="hidden" name="dusun_id" value="{{ $dusunId }}">
<input type="hidden" name="kuota" value="{{ (int)($kuota ?? 7) }}">
<button type="submit"
onclick="return confirm('Tetapkan {{ (int)($kuota ?? 7) }} orang teratas untuk dusun ini?')"
onclick="return confirm('Tetapkan {{ (int)($kuota ?? 7) }} orang dengan probabilitas tertinggi untuk dusun ini?')"
class="px-5 py-2 text-sm font-semibold bg-emerald-600 text-white rounded-xl hover:bg-emerald-700 transition">
Tetapkan {{ (int)($kuota ?? 7) }} Teratas
</button>
@ -83,7 +84,7 @@ class="px-5 py-2 text-sm font-semibold bg-emerald-600 text-white rounded-xl hove
<div class="px-5 py-3 border-b border-gray-100 bg-gray-50 flex items-center justify-between">
<div class="flex items-center gap-2">
<div class="w-1 h-4 bg-blue-600 rounded-full"></div>
<span class="text-sm font-semibold text-gray-700">Kandidat Disetujui (urut probabilitas)</span>
<span class="text-sm font-semibold text-gray-700">Kandidat Sedang Divalidasi / Sudah Diproses</span>
</div>
</div>
@ -91,27 +92,52 @@ class="px-5 py-2 text-sm font-semibold bg-emerald-600 text-white rounded-xl hove
<table class="w-full text-sm">
<thead>
<tr class="bg-gray-50 border-b border-gray-100">
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase">Ranking</th>
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase">Nama</th>
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase">NIK</th>
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase">Dusun</th>
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase">Prob</th>
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase">Probabilitas</th>
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase">Status</th>
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase">Final</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-50">
@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
<tr class="hover:bg-blue-50/20 {{ $picked ? 'bg-emerald-50/40' : '' }}">
<td class="px-4 py-3 font-semibold text-gray-800">{{ $w->nama_lengkap }}</td>
<td class="px-4 py-3 text-gray-600">{{ $w->nik }}</td>
<td class="px-4 py-3 text-gray-600">{{ optional(optional($w->rt)->dusun)->nama_dusun ?? '-' }}</td>
<td class="px-4 py-3 text-gray-700">
{{ $prob !== null ? number_format((float)$prob, 4) : '-' }}
<td class="px-4 py-3 text-sm font-semibold text-gray-700">
{{ (($candidates->currentPage() - 1) * $candidates->perPage()) + $index + 1 }}
</td>
<td class="px-4 py-3 font-semibold text-gray-800">{{ $w->nama_lengkap }}</td>
<td class="px-4 py-3 text-gray-600">{{ $w->nik }}</td>
<td class="px-4 py-3 text-gray-600">{{ optional(optional($w->rt)->dusun)->nama_dusun ?? '-' }}</td>
<td class="px-4 py-3 text-gray-700">
@if($probPct !== null)
<span class="font-semibold">{{ number_format((float)$probPct, 1) }}%</span>
@else
-
@endif
</td>
<td class="px-4 py-3">
@if(($w->status_verifikasi ?? '') === 'pending')
<span class="px-2 py-1 rounded-full text-xs bg-amber-100 text-amber-700">Pending</span>
@elseif(($w->status_verifikasi ?? '') === 'disetujui')
<span class="px-2 py-1 rounded-full text-xs bg-emerald-100 text-emerald-700">Disetujui</span>
@else
<span class="px-2 py-1 rounded-full text-xs bg-rose-100 text-rose-700">Ditolak</span>
@endif
</td>
<td class="px-4 py-3">
@if($picked)
<span class="px-2 py-1 rounded-full text-xs bg-emerald-100 text-emerald-700">Terpilih</span>
@ -122,8 +148,8 @@ class="px-5 py-2 text-sm font-semibold bg-emerald-600 text-white rounded-xl hove
</tr>
@empty
<tr>
<td colspan="5" class="px-4 py-12 text-center text-sm text-gray-400">
Pilih dusun dulu untuk menampilkan kandidat.
<td colspan="7" class="px-4 py-12 text-center text-sm text-gray-400">
Pilih dusun dulu untuk menampilkan kandidat filterisasi.
</td>
</tr>
@endforelse
@ -138,8 +164,14 @@ class="px-5 py-2 text-sm font-semibold bg-emerald-600 text-white rounded-xl hove
@endif
</div>
<div class="bg-amber-50 border border-amber-100 rounded-2xl px-5 py-4 text-xs text-amber-700">
<b>Catatan:</b> Kandidat yang ditampilkan hanya yang <b>status_verifikasi = disetujui</b>. Kuota default 7 per dusun.
<div class="bg-amber-50 border border-amber-100 rounded-2xl px-5 py-4 text-xs text-amber-700 leading-6">
<b>Catatan:</b>
Kandidat yang tampil di halaman ini adalah data warga yang berada pada tahap
<b>sedang_validasi</b> atau <b>selesai</b>.
Saat tombol <b>Tetapkan Teratas</b> ditekan, sistem otomatis:
<br> memilih warga dengan probabilitas tertinggi sesuai kuota
<br> menetapkan yang lolos menjadi <b>disetujui</b>
<br> menetapkan yang tidak lolos menjadi <b>ditolak</b>
</div>
</div>

View File

@ -3,79 +3,152 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Register - Sistem BLT-DD</title>
<title>Daftar Akun SiBantuDes</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
min-height: 100vh;
font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
background: linear-gradient(145deg, #eff6ff 0%, #f8fafc 45%, #f0f9ff 100%);
display: flex; align-items: center; justify-content: center;
padding: 12px 16px; /* ← dari 24px */
}
.card { width:100%; max-width:460px; background:#fff; border-radius:24px; overflow:hidden; box-shadow:0 2px 4px rgba(0,0,0,0.04),0 8px 24px rgba(37,99,235,0.08),0 24px 48px rgba(37,99,235,0.06); }
.hero { position:relative; padding:20px 28px 18px; background:linear-gradient(135deg,#1e3a8a 0%,#1d4ed8 55%,#3b82f6 100%); overflow:hidden; } /* ← dari 30px 32px 28px */
.hero-blob-1 { position:absolute; top:-32px; right:-32px; width:130px; height:130px; background:rgba(255,255,255,.09); border-radius:50%; }
.hero-blob-2 { position:absolute; bottom:-40px; left:-16px; width:100px; height:100px; background:rgba(255,255,255,.07); border-radius:50%; }
.hero-blob-3 { position:absolute; top:10px; right:90px; width:40px; height:40px; background:rgba(255,255,255,.10); border-radius:50%; }
.hero-inner { position:relative; z-index:1; display:flex; align-items:center; gap:14px; }
.hero-logo { width:48px; height:48px; flex-shrink:0; background:rgba(255,255,255,.16); border:2px solid rgba(255,255,255,.22); border-radius:14px; display:flex; align-items:center; justify-content:center; } /* ← dari 56px */
.hero-logo svg { width:24px; height:24px; stroke:#fff; }
.hero-app { font-size:10px; font-weight:700; letter-spacing:.14em; text-transform:uppercase; color:rgba(191,219,254,.85); margin-bottom:2px; }
.hero-title { font-size:19px; font-weight:800; color:#fff; line-height:1.2; } /* ← dari 21px */
.hero-sub { font-size:12px; color:rgba(191,219,254,.8); margin-top:1px; }
.body { padding:18px 28px 14px; } /* ← dari 28px 32px 24px */
.error-box { display:flex; gap:10px; align-items:flex-start; background:#fef2f2; border:1.5px solid #fecaca; border-radius:12px; padding:10px 12px; margin-bottom:14px; }
.error-box svg { width:15px; height:15px; stroke:#ef4444; flex-shrink:0; margin-top:1px; }
.error-box ul { list-style:none; }
.error-box li { font-size:12px; color:#dc2626; line-height:1.5; }
.field { margin-bottom:11px; } /* ← dari 16px */
.field-label { display:block; margin-bottom:5px; font-size:10.5px; font-weight:700; text-transform:uppercase; letter-spacing:.08em; color:#6b7280; }
.field-label .req { color:#ef4444; margin-left:2px; }
.field-hint { font-size:11px; color:#9ca3af; margin-top:4px; padding-left:2px; }
.iw { position:relative; display:flex; align-items:center; }
.iw-icon { position:absolute; left:10px; z-index:1; pointer-events:none; width:30px; height:30px; border-radius:8px; display:flex; align-items:center; justify-content:center; }
.iw-icon svg { width:14px; height:14px; fill:none; }
.iw-icon.blue { background:#eff6ff; } .iw-icon.blue svg { stroke:#3b82f6; }
.iw-icon.green { background:#f0fdf4; } .iw-icon.green svg { stroke:#22c55e; }
.iw-icon.amber { background:#fffbeb; } .iw-icon.amber svg { stroke:#f59e0b; }
.iw input, .iw select { width:100%; padding:10px 14px 10px 50px; font-size:13.5px; color:#111827; background:#f9fafb; border:1.5px solid #e5e7eb; border-radius:12px; outline:none; transition:border-color .15s,background .15s,box-shadow .15s; -webkit-appearance:none; appearance:none; } /* ← padding dari 13px */
.iw input::placeholder { color:#9ca3af; font-size:13px; }
.iw input:focus, .iw select:focus { border-color:#3b82f6; background:#fff; box-shadow:0 0 0 3px rgba(59,130,246,.12); }
.iw input.err { border-color:#fca5a5; background:#fef2f2; }
.iw-arrow { position:absolute; right:12px; pointer-events:none; }
.iw-arrow svg { width:14px; height:14px; stroke:#9ca3af; fill:none; }
.iw-toggle { position:absolute; right:9px; width:28px; height:28px; border-radius:7px; background:none; border:none; cursor:pointer; display:flex; align-items:center; justify-content:center; color:#9ca3af; transition:color .15s,background .15s; }
.iw-toggle:hover { color:#6b7280; background:#f3f4f6; }
.iw-toggle svg { width:14px; height:14px; stroke:currentColor; fill:none; }
.divider { display:flex; align-items:center; gap:10px; margin:4px 0 12px; } /* ← dari 6px 0 18px */
.divider::before, .divider::after { content:''; flex:1; height:1px; background:#f1f5f9; }
.divider span { font-size:10.5px; font-weight:600; color:#cbd5e1; white-space:nowrap; }
.btn-submit { width:100%; margin-top:8px; padding:12px; border:none; border-radius:12px; font-size:14px; font-weight:700; color:#fff; cursor:pointer; display:flex; align-items:center; justify-content:center; gap:8px; background:linear-gradient(135deg,#2563eb 0%,#1d4ed8 100%); box-shadow:0 4px 16px rgba(37,99,235,.32); transition:box-shadow .2s,transform .1s; } /* ← padding dari 14px */
.btn-submit:hover { box-shadow:0 6px 24px rgba(37,99,235,.44); }
.btn-submit:active { transform:scale(.98); }
.btn-submit svg { width:16px; height:16px; stroke:#fff; fill:none; flex-shrink:0; }
.login-link { margin-top:14px; padding-top:12px; border-top:1px solid #f1f5f9; text-align:center; font-size:13px; color:#9ca3af; } /* ← dari 20px/18px */
.login-link a { color:#2563eb; font-weight:600; text-decoration:none; margin-left:2px; }
.login-link a:hover { text-decoration:underline; }
.footer { padding:9px 28px; background:#f9fafb; border-top:1px solid #f1f5f9; display:flex; align-items:center; justify-content:center; gap:6px; } /* ← dari 11px */
.footer svg { width:12px; height:12px; fill:#d1d5db; }
.footer span { font-size:11px; color:#d1d5db; font-weight:500; }
</style>
</head>
<body class="bg-gradient-to-br from-primary-50 via-white to-blue-50">
<div class="min-h-screen flex items-center justify-center p-4">
<div class="max-w-md w-full bg-white rounded-2xl shadow-2xl p-8">
<div class="mb-8">
<div class="flex items-center justify-center mb-4">
<div class="w-16 h-16 bg-primary-600 rounded-xl flex items-center justify-center">
<svg class="w-10 h-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path>
</svg>
</div>
</div>
<h2 class="text-3xl font-bold text-gray-800 text-center mb-2">Daftar Akun RT</h2>
<p class="text-gray-600 text-center">Lengkapi data untuk mendaftar</p>
<body>
<div class="card">
<div class="hero">
<div class="hero-blob-1"></div><div class="hero-blob-2"></div><div class="hero-blob-3"></div>
<div class="hero-inner">
<div class="hero-logo">
<svg fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/></svg>
</div>
@if($errors->any())
<div class="mb-6 bg-red-50 border-l-4 border-red-500 p-4 rounded-lg">
<ul class="text-red-700 text-sm space-y-1">
@foreach($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<form action="{{ route('register') }}" method="POST" class="space-y-5">
@csrf
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Nama Lengkap</label>
<input type="text" name="name" value="{{ old('name') }}" required
class="input-field" placeholder="Masukkan nama lengkap">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Email</label>
<input type="email" name="email" value="{{ old('email') }}" required
class="input-field" placeholder="nama@email.com">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Nama Dusun</label>
<input type="text" name="dusun" value="{{ old('dusun') }}" required
class="input-field" placeholder="Contoh: Ngronggo">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Password</label>
<input type="password" name="password" required
class="input-field" placeholder="Minimal 6 karakter">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Konfirmasi Password</label>
<input type="password" name="password_confirmation" required
class="input-field" placeholder="Ulangi password">
</div>
<button type="submit" class="w-full btn-primary py-3 text-lg font-semibold mt-6">
Daftar Sekarang
</button>
</form>
<div class="mt-6 text-center">
<p class="text-sm text-gray-600">
Sudah punya akun?
<a href="{{ route('login') }}" class="text-primary-600 hover:text-primary-700 font-medium">Masuk di sini</a>
</p>
<div>
<p class="hero-app">SiBantuDes</p>
<h1 class="hero-title">Daftar Akun RT</h1>
<p class="hero-sub">Lengkapi data untuk mendaftar ke sistem</p>
</div>
</div>
</div>
<div class="body">
@if($errors->any())
<div class="error-box">
<svg fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01M12 3a9 9 0 100 18A9 9 0 0012 3z"/></svg>
<ul>@foreach($errors->all() as $e)<li>{{ $e }}</li>@endforeach</ul>
</div>
@endif
<form action="{{ route('register') }}" method="POST">
@csrf
<div class="field">
<label class="field-label">Nama Lengkap <span class="req">*</span></label>
<div class="iw">
<div class="iw-icon blue"><svg fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/></svg></div>
<input type="text" name="name" value="{{ old('name') }}" required placeholder="Masukkan nama lengkap" class="{{ $errors->has('name') ? 'err' : '' }}">
</div>
</div>
<div class="field">
<label class="field-label">Email <span class="req">*</span></label>
<div class="iw">
<div class="iw-icon blue"><svg fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/></svg></div>
<input type="email" name="email" value="{{ old('email') }}" required placeholder="nama@email.com" class="{{ $errors->has('email') ? 'err' : '' }}">
</div>
</div>
<div class="field">
<label class="field-label">Pilih RT <span class="req">*</span></label>
<div class="iw">
<div class="iw-icon green"><svg fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/></svg></div>
<select name="rt_id" required style="padding-right:40px;" class="{{ $errors->has('rt_id') ? 'err' : '' }}">
<option value=""> Pilih RT </option>
@forelse(($rts ?? []) as $rt)
<option value="{{ $rt->id }}" {{ (string)old('rt_id') === (string)$rt->id ? 'selected' : '' }}>RT {{ str_pad($rt->nomor_rt, 3, '0', STR_PAD_LEFT) }} {{ $rt->dusun->nama_dusun ?? '-' }}</option>
@empty
<option value="" disabled>Data RT belum tersedia</option>
@endforelse
</select>
<div class="iw-arrow"><svg fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg></div>
</div>
<p class="field-hint">Dusun mengikuti RT yang dipilih secara otomatis.</p>
</div>
<div class="divider"><span>Keamanan Akun</span></div>
<div class="field">
<label class="field-label">Password <span class="req">*</span></label>
<div class="iw">
<div class="iw-icon amber"><svg fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/></svg></div>
<input type="password" name="password" id="pwd" required placeholder="Minimal 6 karakter" style="padding-right:46px;" class="{{ $errors->has('password') ? 'err' : '' }}">
<button type="button" class="iw-toggle" onclick="togglePwd('pwd',this)"><svg fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"/></svg></button>
</div>
</div>
<div class="field">
<label class="field-label">Konfirmasi Password <span class="req">*</span></label>
<div class="iw">
<div class="iw-icon amber"><svg fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/></svg></div>
<input type="password" name="password_confirmation" id="pwd2" required placeholder="Ulangi password" style="padding-right:46px;">
<button type="button" class="iw-toggle" onclick="togglePwd('pwd2',this)"><svg fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"/></svg></button>
</div>
</div>
<button type="submit" class="btn-submit">
<svg fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z"/></svg>
Daftar Sekarang
</button>
</form>
<div class="login-link">Sudah punya akun? <a href="{{ route('login') }}">Masuk di sini </a></div>
</div>
<div class="footer">
<svg viewBox="0 0 20 20"><path fill-rule="evenodd" d="M4 4a2 2 0 012-2h8a2 2 0 012 2v12a1 1 0 110 2h-3a1 1 0 01-1-1v-2a1 1 0 00-1-1H9a1 1 0 00-1 1v2a1 1 0 01-1 1H4a1 1 0 110-2V4zm3 1h2v2H7V5zm2 4H7v2h2V9zm2-4h2v2h-2V5zm2 4h-2v2h2V9z" clip-rule="evenodd"/></svg>
<span>SiBantuDes &middot; Desa Ngerong</span>
</div>
</div>
<script>
function togglePwd(id,btn){const i=document.getElementById(id);const s=i.type==='password';i.type=s?'text':'password';btn.querySelector('svg').innerHTML=s?'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>':'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"/>';}
</script>
</body>
</html>

View File

@ -6,31 +6,31 @@
</div>
</x-slot>
{{-- TAMPILKAN ERROR VALIDASI --}}
@if ($errors->any())
<div class="mb-4 rounded-xl border border-red-200 bg-red-50 p-4 text-sm text-red-700">
<div class="font-semibold mb-2">Gagal menyimpan:</div>
<ul class="list-disc ps-5 space-y-1">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
{{-- TAMPILKAN ERROR VALIDASI --}}
@if ($errors->any())
<div class="mb-4 rounded-xl border border-red-200 bg-red-50 p-4 text-sm text-red-700">
<div class="font-semibold mb-2">Gagal menyimpan:</div>
<ul class="list-disc ps-5 space-y-1">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
{{-- TAMPILKAN ERROR DARI TRY-CATCH CONTROLLER --}}
@if (session('error'))
<div class="mb-4 rounded-xl border border-red-200 bg-red-50 p-4 text-sm text-red-700">
{{ session('error') }}
</div>
@endif
{{-- TAMPILKAN ERROR DARI TRY-CATCH CONTROLLER --}}
@if (session('error'))
<div class="mb-4 rounded-xl border border-red-200 bg-red-50 p-4 text-sm text-red-700">
{{ session('error') }}
</div>
@endif
{{-- TAMPILKAN SUCCESS --}}
@if (session('success'))
<div class="mb-4 rounded-xl border border-green-200 bg-green-50 p-4 text-sm text-green-700">
{{ session('success') }}
</div>
@endif
{{-- TAMPILKAN SUCCESS --}}
@if (session('success'))
<div class="mb-4 rounded-xl border border-green-200 bg-green-50 p-4 text-sm text-green-700">
{{ session('success') }}
</div>
@endif
<form action="{{ route('rt.calon-penerima.store') }}" method="POST" id="formPendataan" novalidate>
@csrf
@ -41,60 +41,98 @@
<div class="w-1 h-4 bg-blue-600 rounded-full"></div>
<h3 class="text-sm font-semibold text-gray-700">Data Identitas</h3>
</div>
<div class="p-5 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{{-- RT --}}
<div class="p-5 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
@php
$myRtId = auth()->user()->rt_id ?? (auth()->user()->rt->id ?? null);
@endphp
<div class="field-group">
<label class="block text-xs font-semibold text-gray-600 mb-1">RT <span class="text-red-500">*</span></label>
<select name="rt_id" id="rt_id" class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-gray-50">
<option value="">-- Pilih RT --</option>
@if(isset($rts) && $rts->count() > 0)
@foreach($rts as $rt)
<option value="{{ $rt->id }}">RT {{ str_pad($rt->nomor_rt, 3, '0', STR_PAD_LEFT) }} - {{ $rt->dusun->nama_dusun ?? '' }}</option>
@endforeach
@else
<option value="1">RT 001</option>
<option value="2">RT 002</option>
<option value="3">RT 003</option>
<option value="4">RT 004</option>
<option value="5">RT 005</option>
<option value="6">RT 006</option>
@endif
<label class="block text-xs font-semibold text-gray-600 mb-1">
RT <span class="text-red-500">*</span>
</label>
{{-- tampilkan tapi terkunci --}}
<select
id="rt_id_display"
class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-gray-100"
disabled
>
@foreach(($rts ?? []) as $rt)
<option
value="{{ $rt->id }}"
{{ (string)$rt->id === (string)$myRtId ? 'selected' : '' }}
>
RT {{ str_pad($rt->nomor_rt, 3, '0', STR_PAD_LEFT) }} - {{ $rt->dusun->nama_dusun ?? '' }}
</option>
@endforeach
</select>
{{-- yang dikirim --}}
<input type="hidden" name="rt_id" id="rt_id" value="{{ $myRtId }}">
<p class="error-msg text-xs text-red-500 mt-1 hidden">RT wajib dipilih.</p>
</div>
{{-- NO KK --}}
<div class="field-group">
<label class="block text-xs font-semibold text-gray-600 mb-1">No. KK <span class="text-red-500">*</span></label>
<input type="text" name="no_kk" id="no_kk" maxlength="16"
<label class="block text-xs font-semibold text-gray-600 mb-1">
No. KK <span class="text-red-500">*</span>
</label>
<input
type="text"
name="no_kk"
id="no_kk"
maxlength="16"
class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-gray-50"
placeholder="16 digit No. KK" required>
placeholder="16 digit No. KK"
required
>
<p class="error-msg text-xs text-red-500 mt-1 hidden">No. KK harus 16 digit angka.</p>
</div>
{{-- NIK --}}
<div class="field-group">
<label class="block text-xs font-semibold text-gray-600 mb-1">NIK <span class="text-red-500">*</span></label>
<input type="text" name="nik" id="nik" maxlength="16"
<label class="block text-xs font-semibold text-gray-600 mb-1">
NIK <span class="text-red-500">*</span>
</label>
<input
type="text"
name="nik"
id="nik"
maxlength="16"
class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-gray-50"
placeholder="16 digit NIK" required>
placeholder="16 digit NIK"
required
>
<p class="error-msg text-xs text-red-500 mt-1 hidden">NIK harus tepat 16 digit angka.</p>
</div>
{{-- NAMA --}}
<div class="field-group">
<label class="block text-xs font-semibold text-gray-600 mb-1">Nama Lengkap <span class="text-red-500">*</span></label>
<input type="text" name="nama_lengkap" id="nama_lengkap"
<label class="block text-xs font-semibold text-gray-600 mb-1">
Nama Lengkap <span class="text-red-500">*</span>
</label>
<input
type="text"
name="nama_lengkap"
id="nama_lengkap"
class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-gray-50"
placeholder="Nama sesuai KTP" required>
placeholder="Nama sesuai KTP"
required
>
<p class="error-msg text-xs text-red-500 mt-1 hidden">Nama lengkap wajib diisi.</p>
</div>
{{-- JENIS KELAMIN --}}
<div class="field-group">
<label class="block text-xs font-semibold text-gray-600 mb-1">Jenis Kelamin <span class="text-red-500">*</span></label>
<select name="jenis_kelamin" class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-gray-50">
<label class="block text-xs font-semibold text-gray-600 mb-1">
Jenis Kelamin <span class="text-red-500">*</span>
</label>
<select
name="jenis_kelamin"
class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-gray-50"
>
<option value="Laki-laki">Laki-laki</option>
<option value="Perempuan">Perempuan</option>
</select>
@ -102,35 +140,63 @@ class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm fo
{{-- TEMPAT LAHIR --}}
<div class="field-group">
<label class="block text-xs font-semibold text-gray-600 mb-1">Tempat Lahir <span class="text-red-500">*</span></label>
<input type="text" name="tempat_lahir" id="tempat_lahir"
<label class="block text-xs font-semibold text-gray-600 mb-1">
Tempat Lahir <span class="text-red-500">*</span>
</label>
<input
type="text"
name="tempat_lahir"
id="tempat_lahir"
class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-gray-50"
placeholder="Kota/Kabupaten" required>
placeholder="Kota/Kabupaten"
required
>
<p class="error-msg text-xs text-red-500 mt-1 hidden">Tempat lahir wajib diisi.</p>
</div>
{{-- TANGGAL LAHIR --}}
<div class="field-group">
<label class="block text-xs font-semibold text-gray-600 mb-1">Tanggal Lahir <span class="text-red-500">*</span></label>
<input type="date" name="tanggal_lahir" id="tanggal_lahir"
<label class="block text-xs font-semibold text-gray-600 mb-1">
Tanggal Lahir <span class="text-red-500">*</span>
</label>
<input
type="date"
name="tanggal_lahir"
id="tanggal_lahir"
class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-gray-50"
required>
required
>
<p class="error-msg text-xs text-red-500 mt-1 hidden">Tanggal lahir wajib diisi.</p>
</div>
{{-- USIA --}}
<div class="field-group">
<label class="block text-xs font-semibold text-gray-600 mb-1">Usia <span class="text-red-500">*</span></label>
<input type="number" name="usia" id="usia" min="0" max="150"
<label class="block text-xs font-semibold text-gray-600 mb-1">
Usia <span class="text-red-500">*</span>
</label>
<input
type="number"
name="usia"
id="usia"
min="0"
max="150"
class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-gray-50"
placeholder="Otomatis dari tanggal lahir" required>
placeholder="Otomatis dari tanggal lahir"
required
>
<p class="error-msg text-xs text-red-500 mt-1 hidden">Usia harus antara 0150 tahun.</p>
</div>
{{-- STATUS PERKAWINAN --}}
<div class="field-group">
<label class="block text-xs font-semibold text-gray-600 mb-1">Status Perkawinan <span class="text-red-500">*</span></label>
<select name="status_perkawinan" id="status_perkawinan" class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-gray-50">
<label class="block text-xs font-semibold text-gray-600 mb-1">
Status Perkawinan <span class="text-red-500">*</span>
</label>
<select
name="status_perkawinan"
id="status_perkawinan"
class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-gray-50"
>
<option value="">-- Pilih --</option>
<option value="Belum Kawin">Belum Kawin</option>
<option value="Kawin">Kawin</option>
@ -139,7 +205,6 @@ class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm fo
</select>
<p class="error-msg text-xs text-red-500 mt-1 hidden">Status perkawinan wajib dipilih.</p>
</div>
</div>
</div>
@ -149,35 +214,63 @@ class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm fo
<div class="w-1 h-4 bg-emerald-500 rounded-full"></div>
<h3 class="text-sm font-semibold text-gray-700">Data Tempat Tinggal</h3>
</div>
<div class="p-5 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div class="p-5 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{{-- ALAMAT --}}
<div class="field-group lg:col-span-2">
<label class="block text-xs font-semibold text-gray-600 mb-1">Alamat <span class="text-red-500">*</span></label>
<input type="text" name="alamat" id="alamat"
<label class="block text-xs font-semibold text-gray-600 mb-1">
Alamat <span class="text-red-500">*</span>
</label>
<input
type="text"
name="alamat"
id="alamat"
class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-gray-50"
placeholder="Jalan, nomor rumah, RT/RW" required>
placeholder="Jalan, nomor rumah, RT/RW"
required
>
<p class="error-msg text-xs text-red-500 mt-1 hidden">Alamat wajib diisi.</p>
</div>
{{-- DUSUN --}}
@php
$myDusunName = auth()->user()->rt->dusun->nama_dusun ?? '';
@endphp
<div class="field-group">
<label class="block text-xs font-semibold text-gray-600 mb-1">Dusun <span class="text-red-500">*</span></label>
<input type="text" name="desa" id="desa"
class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-gray-50"
placeholder="Nama dusun" required>
<label class="block text-xs font-semibold text-gray-600 mb-1">
Dusun <span class="text-red-500">*</span>
</label>
{{-- tampilkan tapi terkunci --}}
<input
type="text"
id="desa_display"
class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-gray-100"
value="{{ $myDusunName }}"
readonly
>
{{-- yang dikirim --}}
<input type="hidden" name="desa" id="desa" value="{{ $myDusunName }}">
<p class="error-msg text-xs text-red-500 mt-1 hidden">Dusun wajib diisi.</p>
</div>
{{-- ASET --}}
<div class="field-group">
<label class="block text-xs font-semibold text-gray-600 mb-1">Aset Kepemilikan <span class="text-red-500">*</span></label>
<input type="text" name="aset_kepemilikan" id="aset_kepemilikan"
<label class="block text-xs font-semibold text-gray-600 mb-1">
Aset Kepemilikan <span class="text-red-500">*</span>
</label>
<input
type="text"
name="aset_kepemilikan"
id="aset_kepemilikan"
class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-gray-50"
placeholder="Misal: Rumah, Motor, dll" required>
placeholder="Misal: Rumah, Motor, dll"
required
>
<p class="error-msg text-xs text-red-500 mt-1 hidden">Aset kepemilikan wajib diisi.</p>
</div>
</div>
</div>
@ -187,59 +280,90 @@ class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm fo
<div class="w-1 h-4 bg-amber-500 rounded-full"></div>
<h3 class="text-sm font-semibold text-gray-700">Data Ekonomi</h3>
</div>
<div class="p-5 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div class="p-5 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{{-- PEKERJAAN --}}
<div class="field-group">
<label class="block text-xs font-semibold text-gray-600 mb-1">Pekerjaan <span class="text-red-500">*</span></label>
<input type="text" name="pekerjaan" id="pekerjaan"
<label class="block text-xs font-semibold text-gray-600 mb-1">
Pekerjaan <span class="text-red-500">*</span>
</label>
<input
type="text"
name="pekerjaan"
id="pekerjaan"
class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-gray-50"
placeholder="Jenis pekerjaan" required>
placeholder="Jenis pekerjaan"
required
>
<p class="error-msg text-xs text-red-500 mt-1 hidden">Pekerjaan wajib diisi.</p>
</div>
{{-- PENGHASILAN --}}
<div class="field-group">
<label class="block text-xs font-semibold text-gray-600 mb-1">Penghasilan (Rp) <span class="text-red-500">*</span></label>
<input type="number" step="0.01" name="penghasilan" id="penghasilan" min="0"
<label class="block text-xs font-semibold text-gray-600 mb-1">
Penghasilan (Rp) <span class="text-red-500">*</span>
</label>
<input
type="number"
step="0.01"
name="penghasilan"
id="penghasilan"
min="0"
class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-gray-50"
placeholder="0" required>
placeholder="0"
required
>
<p class="error-msg text-xs text-red-500 mt-1 hidden">Penghasilan tidak boleh negatif.</p>
</div>
{{-- JUMLAH TANGGUNGAN --}}
<div class="field-group">
<label class="block text-xs font-semibold text-gray-600 mb-1">Jumlah Tanggungan <span class="text-red-500">*</span></label>
<input type="number" name="jumlah_tanggungan" id="jumlah_tanggungan" min="0"
<label class="block text-xs font-semibold text-gray-600 mb-1">
Jumlah Tanggungan <span class="text-red-500">*</span>
</label>
<input
type="number"
name="jumlah_tanggungan"
id="jumlah_tanggungan"
min="0"
class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-gray-50"
placeholder="Jumlah orang" required>
placeholder="Jumlah orang"
required
>
<p class="error-msg text-xs text-red-500 mt-1 hidden">Jumlah tanggungan tidak boleh negatif.</p>
</div>
{{-- BANTUAN LAIN --}}
<div class="field-group">
<label class="block text-xs font-semibold text-gray-600 mb-1">Menerima Bantuan Lain? <span class="text-red-500">*</span></label>
<select name="bantuan_lain" class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-gray-50">
<label class="block text-xs font-semibold text-gray-600 mb-1">
Menerima Bantuan Lain? <span class="text-red-500">*</span>
</label>
<select
name="bantuan_lain"
class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-gray-50"
>
<option value="tidak">Tidak</option>
<option value="ya">Ya</option>
</select>
</div>
</div>
</div>
{{-- ACTIONS --}}
<div class="flex justify-end gap-3">
<a href="{{ route('rt.calon-penerima.index') }}"
class="px-5 py-2 text-sm font-medium bg-white border border-gray-200 text-gray-600 rounded-xl hover:bg-gray-50 transition">
<a
href="{{ route('rt.calon-penerima.index') }}"
class="px-5 py-2 text-sm font-medium bg-white border border-gray-200 text-gray-600 rounded-xl hover:bg-gray-50 transition"
>
Batal
</a>
<button type="submit"
class="px-6 py-2 text-sm font-semibold bg-blue-600 text-white rounded-xl hover:bg-blue-700 transition shadow-md shadow-blue-200">
<button
type="submit"
class="px-6 py-2 text-sm font-semibold bg-blue-600 text-white rounded-xl hover:bg-blue-700 transition shadow-md shadow-blue-200"
>
Simpan Data
</button>
</div>
</form>
{{-- TOAST NOTIFIKASI --}}
@ -338,5 +462,4 @@ function showToast(msg) {
}
});
</script>
</x-app-layout>

View File

@ -5,7 +5,7 @@
<h2 class="font-semibold text-xl text-gray-800 leading-tight">Edit Pendataan Warga</h2>
<p class="text-sm text-gray-500 mt-0.5">Perbarui data dengan benar sebelum menyimpan.</p>
</div>
<a href="{{ route('calon-penerima.index') }}"
<a href="{{ route('rt.calon-penerima.index') }}"
class="inline-flex items-center gap-1.5 px-4 py-2 bg-white border border-gray-200 text-gray-600 text-sm font-medium rounded-xl hover:bg-gray-50 transition">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
@ -32,10 +32,13 @@ class="inline-flex items-center gap-1.5 px-4 py-2 bg-white border border-gray-20
</div>
@endif
<form action="{{ route('calon-penerima.update', $calonPenerima->id) }}" method="POST" id="formEditPendataan" novalidate>
<form action="{{ route('rt.calon-penerima.update', $calonPenerima->id) }}" method="POST" id="formEditPendataan" novalidate>
@csrf
@method('PUT')
{{-- hidden aman, walau nanti tetap di-override controller --}}
<input type="hidden" name="rt_id" value="{{ $calonPenerima->rt_id }}">
{{-- SECTION 1: Data Identitas --}}
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 mb-4 overflow-hidden">
<div class="px-5 py-3 border-b border-gray-100 flex items-center gap-2 bg-gray-50">
@ -44,19 +47,14 @@ class="inline-flex items-center gap-1.5 px-4 py-2 bg-white border border-gray-20
</div>
<div class="p-5 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{{-- RT --}}
{{-- RT (readonly) --}}
<div class="field-group">
<label class="block text-xs font-semibold text-gray-600 mb-1">RT <span class="text-red-500">*</span></label>
<select name="rt_id" id="rt_id"
class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-gray-50">
<option value="">-- Pilih RT --</option>
@foreach($rts as $rt)
<option value="{{ $rt->id }}" {{ (string)old('rt_id', $calonPenerima->rt_id) === (string)$rt->id ? 'selected' : '' }}>
RT {{ str_pad($rt->nomor_rt, 3, '0', STR_PAD_LEFT) }} - {{ $rt->dusun->nama_dusun ?? '' }}
</option>
@endforeach
</select>
<p class="error-msg text-xs text-red-500 mt-1 hidden">RT wajib dipilih.</p>
<label class="block text-xs font-semibold text-gray-600 mb-1">RT</label>
<input type="text"
value="RT {{ str_pad($calonPenerima->rt->nomor_rt ?? 0, 3, '0', STR_PAD_LEFT) }}"
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>
<p class="text-[11px] text-gray-400 mt-1">RT mengikuti akun RT yang sedang login.</p>
</div>
{{-- NO KK --}}
@ -113,7 +111,7 @@ class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm fo
<div class="field-group">
<label class="block text-xs font-semibold text-gray-600 mb-1">Tanggal Lahir <span class="text-red-500">*</span></label>
<input type="date" name="tanggal_lahir" id="tanggal_lahir"
value="{{ old('tanggal_lahir', $calonPenerima->tanggal_lahir) }}"
value="{{ old('tanggal_lahir', $calonPenerima->tanggal_lahir ? \Carbon\Carbon::parse($calonPenerima->tanggal_lahir)->format('Y-m-d') : '') }}"
class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-gray-50"
required>
<p class="error-msg text-xs text-red-500 mt-1 hidden">Tanggal lahir wajib diisi.</p>
@ -162,13 +160,14 @@ class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm fo
<p class="error-msg text-xs text-red-500 mt-1 hidden">Alamat minimal 5 karakter.</p>
</div>
{{-- DUSUN (readonly) --}}
<div class="field-group">
<label class="block text-xs font-semibold text-gray-600 mb-1">Dusun <span class="text-red-500">*</span></label>
<label class="block text-xs font-semibold text-gray-600 mb-1">Dusun</label>
<input type="text" name="desa" id="desa"
value="{{ old('desa', $calonPenerima->desa) }}"
class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-gray-50"
placeholder="Nama dusun" required>
<p class="error-msg text-xs text-red-500 mt-1 hidden">Dusun wajib diisi.</p>
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>
<p class="text-[11px] text-gray-400 mt-1">Dusun mengikuti RT dan tidak dapat diubah.</p>
</div>
<div class="field-group">
@ -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 17100.',
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 => {

View File

@ -1,176 +1,219 @@
<x-app-layout>
<x-slot name="header">
<div class="flex justify-between items-center">
<div style="display:flex;align-items:center;justify-content:space-between;gap:16px;">
<div>
<h2 class="font-semibold text-xl text-gray-800 leading-tight">Data Calon Penerima</h2>
<p class="text-sm text-gray-500 mt-0.5">Daftar seluruh warga yang telah didaftarkan.</p>
<h2 style="font-size:17px;font-weight:800;color:#111827;line-height:1.2;">Data Calon Penerima</h2>
<p style="font-size:11px;color:#9ca3af;margin-top:2px;">Daftar seluruh warga yang telah didaftarkan</p>
</div>
<a href="{{ route('rt.calon-penerima.create') }}"
class="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white text-sm font-semibold rounded-xl hover:bg-blue-700 transition shadow-md shadow-blue-200">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
style="display:inline-flex;align-items:center;gap:7px;padding:8px 16px;background:#2563eb;color:#fff;font-size:12px;font-weight:700;border-radius:11px;text-decoration:none;box-shadow:0 3px 10px rgba(37,99,235,.28);">
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 4v16m8-8H4"/>
</svg>
Tambah Data
</a>
</div>
</x-slot>
{{-- NOTIFIKASI --}}
@if (session('success'))
<div class="mb-4 flex items-center gap-3 rounded-xl border border-green-200 bg-green-50 px-4 py-3 text-sm text-green-700">
<svg class="w-4 h-4 shrink-0 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<style>
/* Flash */
.flash{display:flex;align-items:center;gap:10px;border-radius:12px;padding:9px 14px;margin-bottom:10px;font-size:12px;font-weight:500;}
.flash svg{width:14px;height:14px;flex-shrink:0;}
/* Card */
.idx-card{background:#fff;border-radius:18px;border:1.5px solid #f1f5f9;box-shadow:0 1px 4px rgba(0,0,0,.04),0 4px 16px rgba(0,0,0,.03);overflow:hidden;}
/* Table */
table{width:100%;border-collapse:collapse;}
thead tr{background:#f8fafc;border-bottom:1.5px solid #f1f5f9;}
thead th{padding:10px 14px;text-align:left;font-size:10px;font-weight:700;color:#9ca3af;text-transform:uppercase;letter-spacing:.08em;white-space:nowrap;}
tbody tr{border-bottom:1px solid #f8fafc;transition:background .12s;}
tbody tr:hover{background:#f0f7ff;}
tbody tr:last-child{border-bottom:none;}
tbody td{padding:11px 14px;vertical-align:middle;}
/* Avatar */
.av{width:32px;height:32px;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:800;color:#fff;flex-shrink:0;background:linear-gradient(135deg,#3b82f6,#2563eb);}
/* Badges */
.badge{display:inline-flex;align-items:center;gap:4px;padding:3px 9px;border-radius:20px;font-size:10.5px;font-weight:700;white-space:nowrap;}
.dot{width:5px;height:5px;border-radius:50%;flex-shrink:0;}
.badge-rt{display:inline-flex;align-items:center;padding:2px 8px;border-radius:7px;background:#eff6ff;color:#1d4ed8;font-size:11px;font-weight:700;border:1px solid #bfdbfe;}
/* Tracking badges */
.trk-draft {background:#f3f4f6;color:#374151;} .trk-draft .dot{background:#9ca3af;}
.trk-terkirim {background:#eff6ff;color:#1d4ed8;} .trk-terkirim .dot{background:#3b82f6;}
.trk-validasi {background:#fffbeb;color:#b45309;} .trk-validasi .dot{background:#f59e0b;}
.trk-selesai {background:#f0fdf4;color:#166534;} .trk-selesai .dot{background:#22c55e;}
/* Status badges */
.st-pending {background:#fffbeb;color:#b45309;} .st-pending .dot{background:#f59e0b;}
.st-disetujui {background:#f0fdf4;color:#166534;} .st-disetujui .dot{background:#22c55e;}
.st-ditolak {background:#fff1f2;color:#9f1239;} .st-ditolak .dot{background:#f43f5e;}
/* Action buttons */
.btn-act{display:inline-flex;align-items:center;gap:4px;padding:4px 9px;border-radius:8px;font-size:10.5px;font-weight:600;text-decoration:none;border:none;cursor:pointer;white-space:nowrap;line-height:1.4;}
.btn-act svg{width:11px;height:11px;flex-shrink:0;}
.btn-detail{background:#f3f4f6;color:#374151;} .btn-detail:hover{background:#e5e7eb;}
.btn-edit {background:#eff6ff;color:#1d4ed8;} .btn-edit:hover{background:#dbeafe;}
.btn-ajukan{background:#f0fdf4;color:#166534;} .btn-ajukan:hover{background:#dcfce7;}
.btn-hapus {background:#fff1f2;color:#9f1239;} .btn-hapus:hover{background:#ffe4e6;}
/* Empty state */
.empty-state{padding:48px 16px;text-align:center;}
.empty-icon{width:44px;height:44px;background:#f3f4f6;border-radius:50%;display:flex;align-items:center;justify-content:center;margin:0 auto 10px;}
.empty-icon svg{width:20px;height:20px;stroke:#d1d5db;}
/* Pagination wrapper */
.pagi{padding:10px 16px;background:#fafafa;border-top:1px solid #f1f5f9;}
</style>
{{-- FLASH --}}
@if(session('success'))
<div class="flash" style="background:#f0fdf4;border:1.5px solid #bbf7d0;color:#166534;">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
{{ session('success') }}
</div>
@endif
@if (session('error'))
<div class="mb-4 flex items-center gap-3 rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
<svg class="w-4 h-4 shrink-0 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01M12 3a9 9 0 100 18A9 9 0 0012 3z"/>
</svg>
@if(session('error'))
<div class="flash" style="background:#fff1f2;border:1.5px solid #fecdd3;color:#9f1239;">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01M12 3a9 9 0 100 18A9 9 0 0012 3z"/></svg>
{{ session('error') }}
</div>
@endif
<div class="bg-white shadow-sm rounded-2xl border border-gray-100 overflow-hidden">
<div class="idx-card">
{{-- TABLE HEADER INFO --}}
<div class="px-5 py-3 border-b border-gray-100 bg-gray-50 flex items-center justify-between">
<div class="flex items-center gap-2">
<div class="w-1 h-4 bg-blue-600 rounded-full"></div>
<span class="text-sm font-semibold text-gray-700">
Total: {{ $calonPenerimas->total() }} data
{{-- HEADER BAR --}}
<div style="padding:10px 16px;border-bottom:1px solid #f1f5f9;background:#fafafa;display:flex;align-items:center;justify-content:space-between;gap:12px;">
<div style="display:flex;align-items:center;gap:8px;">
<div style="width:3px;height:16px;background:#2563eb;border-radius:4px;"></div>
<span style="font-size:12.5px;font-weight:700;color:#374151;">
Total <span style="color:#2563eb;">{{ $calonPenerimas->total() }}</span> data
</span>
</div>
<span class="text-xs text-gray-400">
Halaman {{ $calonPenerimas->currentPage() }} dari {{ $calonPenerimas->lastPage() }}
<span style="font-size:11px;color:#9ca3af;">
Halaman {{ $calonPenerimas->currentPage() }} / {{ $calonPenerimas->lastPage() }}
</span>
</div>
<div class="overflow-x-auto">
<table class="min-w-full text-sm text-gray-700">
{{-- TABLE --}}
<div style="overflow-x:auto;">
<table>
<thead>
<tr class="bg-gray-50 border-b border-gray-100">
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wide">No</th>
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wide">Nama</th>
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wide">NIK</th>
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wide">RT</th>
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wide">Dusun</th>
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wide">Status</th>
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wide">Aksi</th>
<tr>
<th style="width:40px;">#</th>
<th>Nama</th>
<th>NIK</th>
<th>RT</th>
<th>Dusun</th>
<th>Tracking</th>
<th>Status</th>
<th>Aksi</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-50">
@forelse ($calonPenerimas as $index => $item)
<tr class="hover:bg-blue-50/30 transition-colors duration-150">
{{-- NO --}}
<td class="px-4 py-3 text-gray-400 text-xs">
<tbody>
@forelse($calonPenerimas as $index => $item)
<tr>
{{-- No --}}
<td style="font-size:11px;color:#9ca3af;font-weight:600;text-align:center;">
{{ $calonPenerimas->firstItem() + $index }}
</td>
{{-- NAMA --}}
<td class="px-4 py-3">
<div class="flex items-center gap-2.5">
<div class="w-8 h-8 rounded-full bg-gradient-to-br from-blue-500 to-blue-600 flex items-center justify-center shrink-0">
<span class="text-xs font-bold text-white">
{{ strtoupper(substr($item->nama_lengkap, 0, 1)) }}
</span>
</div>
<span class="font-medium text-gray-800">{{ $item->nama_lengkap }}</span>
{{-- Nama --}}
<td>
<div style="display:flex;align-items:center;gap:9px;">
<div class="av">{{ strtoupper(substr($item->nama_lengkap, 0, 1)) }}</div>
<span style="font-size:13px;font-weight:600;color:#111827;">{{ $item->nama_lengkap }}</span>
</div>
</td>
{{-- NIK --}}
<td class="px-4 py-3 text-gray-500 font-mono text-xs">
<td style="font-family:monospace;font-size:11.5px;color:#6b7280;letter-spacing:.02em;">
{{ $item->nik }}
</td>
{{-- RT --}}
<td class="px-4 py-3">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-lg bg-blue-50 text-blue-700 text-xs font-semibold border border-blue-100">
RT {{ str_pad($item->rt->nomor_rt ?? '-', 3, '0', STR_PAD_LEFT) }}
</span>
<td>
<span class="badge-rt">RT {{ str_pad($item->rt->nomor_rt ?? '-', 3, '0', STR_PAD_LEFT) }}</span>
</td>
{{-- DUSUN --}}
<td class="px-4 py-3 text-gray-600 text-xs">
{{-- Dusun --}}
<td style="font-size:12px;color:#6b7280;">
{{ $item->rt->dusun->nama_dusun ?? '-' }}
</td>
{{-- STATUS --}}
<td class="px-4 py-3">
@if ($item->status_verifikasi === 'pending')
<span class="inline-flex items-center gap-1 px-2.5 py-1 text-xs font-semibold rounded-full bg-amber-100 text-amber-700">
<span class="w-1.5 h-1.5 rounded-full bg-amber-500"></span>
Pending
</span>
@elseif ($item->status_verifikasi === 'disetujui')
<span class="inline-flex items-center gap-1 px-2.5 py-1 text-xs font-semibold rounded-full bg-emerald-100 text-emerald-700">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>
Disetujui
</span>
{{-- Tracking --}}
<td>
@php $trk = $item->tracking_status ?? 'draft'; @endphp
@if($trk === 'draft')
<span class="badge trk-draft"><span class="dot"></span>Draft</span>
@elseif($trk === 'terkirim')
<span class="badge trk-terkirim"><span class="dot"></span>Terkirim</span>
@elseif($trk === 'sedang_validasi')
<span class="badge trk-validasi"><span class="dot"></span>Validasi</span>
@elseif($trk === 'selesai')
<span class="badge trk-selesai"><span class="dot"></span>Selesai</span>
@else
<span class="inline-flex items-center gap-1 px-2.5 py-1 text-xs font-semibold rounded-full bg-rose-100 text-rose-700">
<span class="w-1.5 h-1.5 rounded-full bg-rose-500"></span>
Ditolak
</span>
<span class="badge trk-draft"><span class="dot"></span></span>
@endif
</td>
{{-- AKSI --}}
<td class="px-4 py-3">
<div class="flex items-center gap-1.5">
<a href="{{ route('rt.calon-penerima.show', $item->id) }}"
class="inline-flex items-center gap-1 px-2.5 py-1 text-xs font-medium bg-gray-100 text-gray-600 rounded-lg hover:bg-gray-200 transition">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
</svg>
{{-- Status --}}
<td>
@if($item->status_verifikasi === 'pending')
<span class="badge st-pending"><span class="dot"></span>Pending</span>
@elseif($item->status_verifikasi === 'disetujui')
<span class="badge st-disetujui"><span class="dot"></span>Disetujui</span>
@else
<span class="badge st-ditolak"><span class="dot"></span>Ditolak</span>
@endif
</td>
{{-- Aksi --}}
<td>
<div style="display:flex;align-items:center;gap:5px;flex-wrap:wrap;">
<a href="{{ route('rt.calon-penerima.show', $item->id) }}" class="btn-act btn-detail">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>
Detail
</a>
@if ($item->status_verifikasi === 'pending')
<a href="{{ route('rt.calon-penerima.edit', $item->id) }}"
class="inline-flex items-center gap-1 px-2.5 py-1 text-xs font-medium bg-blue-100 text-blue-700 rounded-lg hover:bg-blue-200 transition">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
</svg>
@if($trk === 'draft')
<a href="{{ route('rt.calon-penerima.edit', $item->id) }}" class="btn-act btn-edit">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
Edit
</a>
<form action="{{ route('rt.calon-penerima.destroy', $item->id) }}"
method="POST"
onsubmit="return confirm('Yakin ingin menghapus data {{ $item->nama_lengkap }}?')">
@csrf
@method('DELETE')
<button type="submit"
class="inline-flex items-center gap-1 px-2.5 py-1 text-xs font-medium bg-rose-100 text-rose-700 rounded-lg hover:bg-rose-200 transition">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
</svg>
<form method="POST" action="{{ route('rt.calon-penerima.ajukan', $item->id) }}"
onsubmit="return confirm('Yakin ajukan data {{ addslashes($item->nama_lengkap) }}? Setelah diajukan data tidak bisa diubah.')">
@csrf @method('PATCH')
<button type="submit" class="btn-act btn-ajukan">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
Ajukan
</button>
</form>
<form method="POST" action="{{ route('rt.calon-penerima.destroy', $item->id) }}"
onsubmit="return confirm('Yakin hapus data {{ addslashes($item->nama_lengkap) }}?')">
@csrf @method('DELETE')
<button type="submit" class="btn-act btn-hapus">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
Hapus
</button>
</form>
@endif
</div>
</td>
</tr>
@empty
<tr>
<td colspan="7" class="px-4 py-14 text-center">
<div class="flex flex-col items-center gap-2">
<div class="w-12 h-12 bg-gray-100 rounded-full flex items-center justify-center">
<svg class="w-6 h-6 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
<td colspan="8">
<div class="empty-state">
<div class="empty-icon">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
</div>
<p class="text-sm font-medium text-gray-500">Belum ada data calon penerima</p>
<a href="{{ route('rt.calon-penerima.create') }}" class="text-xs text-blue-600 hover:underline">+ Tambah sekarang</a>
<p style="font-size:13px;font-weight:600;color:#6b7280;margin-bottom:6px;">Belum ada data calon penerima</p>
<a href="{{ route('rt.calon-penerima.create') }}" style="font-size:12px;color:#2563eb;font-weight:600;text-decoration:none;">+ Tambah sekarang</a>
</div>
</td>
</tr>
@ -180,9 +223,10 @@ class="inline-flex items-center gap-1 px-2.5 py-1 text-xs font-medium bg-rose-10
</div>
{{-- PAGINATION --}}
<div class="px-5 py-3 border-t border-gray-100 bg-gray-50">
<div class="pagi">
{{ $calonPenerimas->links() }}
</div>
</div>
</x-app-layout>

View File

@ -1,290 +1,413 @@
<x-app-layout>
<x-slot name="header">
<div class="flex items-start justify-between gap-4">
<div>
<h2 class="font-semibold text-xl text-gray-800 leading-tight">Detail Calon Penerima</h2>
<p class="text-sm text-gray-500 mt-0.5">Detail data pendataan warga dan hasil prediksi.</p>
</div>
<div class="flex gap-2">
<div style="display:flex; align-items:center; justify-content:space-between; gap:16px;">
<div style="display:flex; align-items:center; gap:12px;">
<a href="{{ route('rt.calon-penerima.index') }}"
class="inline-flex items-center gap-1.5 px-4 py-2 bg-white border border-gray-200 text-gray-600 text-sm font-medium rounded-xl hover:bg-gray-50 transition">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
style="width:32px; height:32px; display:flex; align-items:center; justify-content:center; border-radius:10px; background:#fff; border:1.5px solid #e5e7eb; color:#6b7280; text-decoration:none;">
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
</svg>
Kembali
</a>
@if(($calonPenerima->status_verifikasi ?? '') === 'pending')
<a href="{{ route('calon-penerima.edit', $calonPenerima->id) }}"
class="inline-flex items-center gap-1.5 px-4 py-2 bg-blue-600 text-white text-sm font-semibold rounded-xl hover:bg-blue-700 transition shadow-md shadow-blue-200">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
</svg>
Edit
</a>
@endif
<div>
<h2 style="font-size:17px; font-weight:800; color:#111827; line-height:1.2;">Detail Calon Penerima</h2>
<p style="font-size:11px; color:#9ca3af; margin-top:2px;">Data pendataan warga &amp; hasil prediksi kelayakan</p>
</div>
</div>
@if(($calonPenerima->tracking_status ?? 'draft') === 'draft')
<a href="{{ route('rt.calon-penerima.edit', $calonPenerima->id) }}"
style="display:inline-flex; align-items:center; gap:6px; padding:7px 14px; background:#2563eb; color:#fff; font-size:12px; font-weight:700; border-radius:10px; text-decoration:none; box-shadow:0 2px 8px rgba(37,99,235,.25);">
<svg width="13" height="13" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
</svg>
Edit Data
</a>
@endif
</div>
</x-slot>
{{-- NOTIFIKASI --}}
@if (session('success'))
<div class="mb-4 flex items-center gap-3 rounded-xl border border-green-200 bg-green-50 px-4 py-3 text-sm text-green-700">
<svg class="w-4 h-4 shrink-0 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<style>
.sp-card{background:#fff;border-radius:16px;border:1.5px solid #f1f5f9;box-shadow:0 1px 3px rgba(0,0,0,.04);overflow:hidden;}
.sp-head{padding:9px 16px;border-bottom:1px solid #f1f5f9;background:#fafafa;display:flex;align-items:center;gap:8px;}
.sp-head .bar{width:3px;height:14px;border-radius:4px;flex-shrink:0;}
.sp-head h3{font-size:10.5px;font-weight:700;color:#6b7280;text-transform:uppercase;letter-spacing:.07em;}
.sp-body{padding:13px 16px;}
.lbl{font-size:10px;color:#9ca3af;text-transform:uppercase;letter-spacing:.07em;margin-bottom:2px;}
.val{font-size:12.5px;font-weight:600;color:#111827;}
.g2{display:grid;grid-template-columns:1fr 1fr;gap:12px;}
.g4{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;}
.pill{display:inline-flex;align-items:center;gap:5px;padding:4px 11px;border-radius:20px;font-size:11px;font-weight:700;}
.dot{width:6px;height:6px;border-radius:50%;flex-shrink:0;}
.fl{list-style:none;padding:0;margin:0;}
.fl li{display:flex;align-items:flex-start;gap:5px;font-size:11.5px;line-height:1.5;margin-bottom:2px;}
.fi{width:13px;height:13px;flex-shrink:0;margin-top:1px;}
details.acc>summary{list-style:none;display:flex;justify-content:space-between;align-items:center;padding:9px 16px;cursor:pointer;background:#f9fafb;border-top:1px solid #f1f5f9;font-size:11px;font-weight:700;color:#374151;user-select:none;}
details.acc>summary::-webkit-details-marker{display:none;}
details.acc>summary .chev{transition:transform .2s;}
details.acc[open]>summary .chev{transform:rotate(180deg);}
details.acc .ab{padding:12px 16px;display:flex;flex-direction:column;gap:9px;}
.trk-wrap{display:flex;flex-direction:column;gap:10px;}
.trk-step{display:flex;align-items:flex-start;gap:10px;}
.trk-point{width:22px;height:22px;border-radius:50%;display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:11px;font-weight:800;}
.trk-line{width:2px;height:18px;margin-left:10px;border-radius:999px;background:#e5e7eb;}
@media(max-width:1024px){.mg{grid-template-columns:1fr!important;}.g4{grid-template-columns:1fr 1fr!important;}}
@media(max-width:600px){.g2,.g4{grid-template-columns:1fr!important;}}
</style>
@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'))
<div style="display:flex;align-items:center;gap:9px;background:#f0fdf4;border:1.5px solid #bbf7d0;border-radius:12px;padding:9px 14px;margin-bottom:10px;font-size:12px;color:#166534;font-weight:500;">
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
{{ session('success') }}
</div>
@endif
@if (session('error'))
<div class="mb-4 flex items-center gap-3 rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
<svg class="w-4 h-4 shrink-0 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01M12 3a9 9 0 100 18A9 9 0 0012 3z"/>
</svg>
@if(session('error'))
<div style="display:flex;align-items:center;gap:9px;background:#fff1f2;border:1.5px solid #fecdd3;border-radius:12px;padding:9px 14px;margin-bottom:10px;font-size:12px;color:#9f1239;font-weight:500;">
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01M12 3a9 9 0 100 18A9 9 0 0012 3z"/></svg>
{{ session('error') }}
</div>
@endif
{{-- IDENTITY BANNER --}}
<div class="bg-gradient-to-br from-blue-600 to-blue-500 rounded-2xl p-5 mb-4 flex items-center gap-4 text-white shadow-lg shadow-blue-200">
<div class="w-14 h-14 rounded-2xl bg-white/20 flex items-center justify-center text-2xl font-bold shrink-0">
{{-- BANNER --}}
<div style="position:relative;overflow:hidden;background:linear-gradient(135deg,#1e40af 0%,#2563eb 55%,#3b82f6 100%);border-radius:18px;padding:16px 20px;margin-bottom:12px;display:flex;align-items:center;gap:14px;box-shadow:0 6px 20px rgba(37,99,235,.22);">
<div style="position:absolute;top:-24px;right:-24px;width:110px;height:110px;background:rgba(255,255,255,.08);border-radius:50%;pointer-events:none;"></div>
<div style="position:absolute;bottom:-28px;left:42%;width:80px;height:80px;background:rgba(255,255,255,.06);border-radius:50%;pointer-events:none;"></div>
<div style="width:46px;height:46px;border-radius:13px;background:rgba(255,255,255,.18);border:2px solid rgba(255,255,255,.25);display:flex;align-items:center;justify-content:center;font-size:20px;font-weight:900;color:#fff;flex-shrink:0;">
{{ strtoupper(substr($calonPenerima->nama_lengkap ?? 'U', 0, 1)) }}
</div>
<div class="flex-1 min-w-0">
<h3 class="text-lg font-bold truncate">{{ $calonPenerima->nama_lengkap ?? '-' }}</h3>
<p class="text-blue-100 text-sm font-mono mt-0.5">NIK: {{ $calonPenerima->nik ?? '-' }}</p>
<div style="flex:1;min-width:0;">
<h3 style="font-size:16px;font-weight:900;color:#fff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;line-height:1.2;">{{ $calonPenerima->nama_lengkap ?? '-' }}</h3>
<p style="font-size:11px;color:rgba(191,219,254,.85);font-family:monospace;margin-top:2px;">NIK: {{ $calonPenerima->nik ?? '-' }}</p>
<div style="margin-top:6px;">
<span style="display:inline-flex;align-items:center;gap:5px;padding:4px 10px;border-radius:999px;background:rgba(255,255,255,.15);color:#fff;font-size:10.5px;font-weight:700;border:1px solid rgba(255,255,255,.22);">
<span style="width:6px;height:6px;border-radius:50%;background:#fff;opacity:.9;"></span>
{{ $trackingLabel }}
</span>
</div>
</div>
<div class="shrink-0">
@php $st = $calonPenerima->status_verifikasi ?? 'pending'; @endphp
@if ($st === 'pending')
<span class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-semibold bg-amber-100 text-amber-700">
<span class="w-1.5 h-1.5 rounded-full bg-amber-500"></span> Pending
</span>
@elseif ($st === 'disetujui')
<span class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-semibold bg-emerald-100 text-emerald-700">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span> Disetujui
</span>
<div style="display:flex;align-items:center;gap:12px;flex-shrink:0;">
@if($probPct !== null)
<div style="text-align:right;">
<div style="font-size:26px;font-weight:900;color:#fff;line-height:1;">{{ number_format($probPct,0) }}<span style="font-size:13px;color:rgba(191,219,254,.7);">%</span></div>
<div style="font-size:10px;color:rgba(191,219,254,.6);margin-top:1px;">probabilitas</div>
</div>
@endif
@if($tracking === 'selesai')
@if($st === 'disetujui')
<span class="pill" style="background:#f0fdf4;color:#166534;border:1px solid #bbf7d0;"><span class="dot" style="background:#22c55e;"></span>Diterima</span>
@elseif($st === 'ditolak')
<span class="pill" style="background:#fff1f2;color:#9f1239;border:1px solid #fecdd3;"><span class="dot" style="background:#f43f5e;"></span>Tidak Diterima</span>
@else
<span class="pill" style="background:#fffbeb;color:#b45309;border:1px solid #fde68a;"><span class="dot" style="background:#f59e0b;"></span>Menunggu Hasil</span>
@endif
@else
<span class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-semibold bg-rose-100 text-rose-700">
<span class="w-1.5 h-1.5 rounded-full bg-rose-500"></span> Ditolak
</span>
<span class="pill" style="background:#eff6ff;color:#1d4ed8;border:1px solid #bfdbfe;"><span class="dot" style="background:#3b82f6;"></span>{{ $trackingLabel }}</span>
@endif
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
{{-- MAIN GRID --}}
<div class="mg" style="display:grid;grid-template-columns:1fr 310px;gap:12px;align-items:start;">
{{-- KOLOM KIRI: DATA UTAMA --}}
<div class="lg:col-span-2 space-y-4">
{{-- KIRI --}}
<div style="display:flex;flex-direction:column;gap:12px;">
{{-- IDENTITAS --}}
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden">
<div class="px-5 py-3 border-b border-gray-100 flex items-center gap-2 bg-gray-50">
<div class="w-1 h-4 bg-blue-600 rounded-full"></div>
<h3 class="text-sm font-semibold text-gray-700">Data Identitas</h3>
<div class="g2">
{{-- Identitas --}}
<div class="sp-card">
<div class="sp-head"><div class="bar" style="background:#2563eb;"></div><h3>Identitas</h3></div>
<div class="sp-body">
<div class="g2">
<div><p class="lbl">No. KK</p><p class="val" style="font-family:monospace;font-size:11px;">{{ $calonPenerima->no_kk ?? '-' }}</p></div>
<div><p class="lbl">Jenis Kelamin</p><p class="val">{{ $calonPenerima->jenis_kelamin ?? '-' }}</p></div>
<div><p class="lbl">Tempat Lahir</p><p class="val">{{ $calonPenerima->tempat_lahir ?? '-' }}</p></div>
<div><p class="lbl">Tanggal Lahir</p><p class="val">{{ $calonPenerima->tanggal_lahir ? \Carbon\Carbon::parse($calonPenerima->tanggal_lahir)->translatedFormat('d F Y') : '-' }}</p></div>
<div><p class="lbl">Usia</p><p class="val">{{ $calonPenerima->usia ?? '-' }} tahun</p></div>
<div><p class="lbl">Status Kawin</p><p class="val">{{ $calonPenerima->status_perkawinan ?? '-' }}</p></div>
</div>
</div>
</div>
<div class="p-5 grid grid-cols-2 md:grid-cols-4 gap-4">
@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 --}}
<div class="sp-card">
<div class="sp-head"><div class="bar" style="background:#10b981;"></div><h3>Tempat Tinggal</h3></div>
<div class="sp-body" style="display:flex;flex-direction:column;gap:11px;">
<div><p class="lbl">Alamat</p><p class="val">{{ $calonPenerima->alamat ?? '-' }}</p></div>
<div class="g2">
<div><p class="lbl">Dusun</p><p class="val">{{ $calonPenerima->desa ?? '-' }}</p></div>
<div>
<p class="lbl">RT</p>
<span style="display:inline-flex;align-items:center;padding:2px 9px;border-radius:8px;background:#eff6ff;color:#1d4ed8;font-size:11px;font-weight:700;border:1px solid #bfdbfe;">RT {{ str_pad($calonPenerima->rt->nomor_rt ?? 0,3,'0',STR_PAD_LEFT) }}</span>
</div>
</div>
<div><p class="lbl">Aset Kepemilikan</p><p class="val">{{ $calonPenerima->aset_kepemilikan ?? '-' }}</p></div>
</div>
</div>
</div>
{{-- Ekonomi --}}
<div class="sp-card">
<div class="sp-head"><div class="bar" style="background:#f59e0b;"></div><h3>Data Ekonomi</h3></div>
<div class="sp-body">
<div class="g4">
<div><p class="lbl">Pekerjaan</p><p class="val">{{ $calonPenerima->pekerjaan ?? '-' }}</p></div>
<div><p class="lbl">Penghasilan</p><p class="val">Rp {{ number_format((float)($calonPenerima->penghasilan??0),0,',','.') }}</p></div>
<div><p class="lbl">Tanggungan</p><p class="val">{{ $calonPenerima->jumlah_tanggungan ?? '-' }} orang</p></div>
<div>
<div class="text-xs text-gray-400 mb-0.5">{{ $label }}</div>
<div class="text-sm font-semibold text-gray-800">{{ $val }}</div>
<p class="lbl">Bantuan Lain</p>
@if(($calonPenerima->bantuan_lain??'') === 'ya')
<span style="display:inline-flex;padding:2px 9px;border-radius:20px;background:#fffbeb;color:#b45309;font-size:11px;font-weight:700;">Ya</span>
@else
<span style="display:inline-flex;padding:2px 9px;border-radius:20px;background:#f3f4f6;color:#6b7280;font-size:11px;font-weight:700;">Tidak</span>
@endif
</div>
@endforeach
</div>
</div>
{{-- TEMPAT TINGGAL --}}
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden">
<div class="px-5 py-3 border-b border-gray-100 flex items-center gap-2 bg-gray-50">
<div class="w-1 h-4 bg-emerald-500 rounded-full"></div>
<h3 class="text-sm font-semibold text-gray-700">Data Tempat Tinggal</h3>
</div>
<div class="p-5 grid grid-cols-2 md:grid-cols-4 gap-4">
<div class="col-span-2">
<div class="text-xs text-gray-400 mb-0.5">Alamat</div>
<div class="text-sm font-semibold text-gray-800">{{ $calonPenerima->alamat ?? '-' }}</div>
</div>
<div>
<div class="text-xs text-gray-400 mb-0.5">Dusun</div>
<div class="text-sm font-semibold text-gray-800">{{ $calonPenerima->desa ?? '-' }}</div>
</div>
<div>
<div class="text-xs text-gray-400 mb-0.5">RT</div>
<div class="text-sm font-semibold text-gray-800">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-lg bg-blue-50 text-blue-700 text-xs font-semibold border border-blue-100">
RT {{ str_pad($calonPenerima->rt->nomor_rt ?? 0, 3, '0', STR_PAD_LEFT) }}
</span>
</div>
</div>
<div class="col-span-2">
<div class="text-xs text-gray-400 mb-0.5">Aset Kepemilikan</div>
<div class="text-sm font-semibold text-gray-800">{{ $calonPenerima->aset_kepemilikan ?? '-' }}</div>
</div>
</div>
</div>
{{-- EKONOMI --}}
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden">
<div class="px-5 py-3 border-b border-gray-100 flex items-center gap-2 bg-gray-50">
<div class="w-1 h-4 bg-amber-500 rounded-full"></div>
<h3 class="text-sm font-semibold text-gray-700">Data Ekonomi</h3>
</div>
<div class="p-5 grid grid-cols-2 md:grid-cols-4 gap-4">
<div>
<div class="text-xs text-gray-400 mb-0.5">Pekerjaan</div>
<div class="text-sm font-semibold text-gray-800">{{ $calonPenerima->pekerjaan ?? '-' }}</div>
</div>
<div>
<div class="text-xs text-gray-400 mb-0.5">Penghasilan</div>
<div class="text-sm font-semibold text-gray-800">
Rp {{ number_format((float)($calonPenerima->penghasilan ?? 0), 0, ',', '.') }}
{{-- Tracking Pengajuan --}}
<div class="sp-card">
<div class="sp-head"><div class="bar" style="background:#6b7280;"></div><h3>Tracking Pengajuan</h3></div>
<div class="sp-body">
<div class="trk-wrap">
<div class="trk-step">
<div class="trk-point" style="{{ in_array($tracking, ['draft','terkirim','sedang_validasi','selesai']) ? 'background:#dbeafe;color:#1d4ed8;' : 'background:#f3f4f6;color:#9ca3af;' }}">1</div>
<div>
<div style="font-size:12px;font-weight:700;color:#111827;">Data dibuat RT</div>
<div style="font-size:11px;color:#9ca3af;">Data masih bisa diubah dan dihapus sebelum diajukan.</div>
</div>
</div>
<div class="trk-line"></div>
<div class="trk-step">
<div class="trk-point" style="{{ in_array($tracking, ['terkirim','sedang_validasi','selesai']) ? 'background:#dbeafe;color:#1d4ed8;' : 'background:#f3f4f6;color:#9ca3af;' }}">2</div>
<div>
<div style="font-size:12px;font-weight:700;color:#111827;">Data diajukan ke kelurahan</div>
<div style="font-size:11px;color:#9ca3af;">
@if(in_array($tracking, ['terkirim','sedang_validasi','selesai']))
Data sudah dikirim dan tidak bisa diubah lagi.
@else
Menunggu tindakan RT untuk mengajukan data.
@endif
</div>
</div>
</div>
<div class="trk-line"></div>
<div class="trk-step">
<div class="trk-point" style="{{ in_array($tracking, ['sedang_validasi','selesai']) ? 'background:#fef3c7;color:#b45309;' : 'background:#f3f4f6;color:#9ca3af;' }}">3</div>
<div>
<div style="font-size:12px;font-weight:700;color:#111827;">Proses validasi & filterisasi</div>
<div style="font-size:11px;color:#9ca3af;">
@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
</div>
</div>
</div>
<div class="trk-line"></div>
<div class="trk-step">
<div class="trk-point" style="{{ $tracking === 'selesai' ? 'background:#dcfce7;color:#166534;' : 'background:#f3f4f6;color:#9ca3af;' }}">4</div>
<div>
<div style="font-size:12px;font-weight:700;color:#111827;">Hasil akhir ditetapkan</div>
<div style="font-size:11px;color:#9ca3af;">
@if($tracking === 'selesai')
Hasil akhir bantuan telah ditetapkan: <strong style="color:#374151;">{{ $hasilLabel }}</strong>.
@else
Menunggu hasil akhir dari admin kelurahan.
@endif
</div>
</div>
</div>
</div>
<div>
<div class="text-xs text-gray-400 mb-0.5">Jumlah Tanggungan</div>
<div class="text-sm font-semibold text-gray-800">{{ $calonPenerima->jumlah_tanggungan ?? '-' }} orang</div>
</div>
<div>
<div class="text-xs text-gray-400 mb-0.5">Bantuan Lain</div>
@if(($calonPenerima->bantuan_lain ?? '') === 'ya')
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold bg-amber-100 text-amber-700">Ya</span>
</div>
</div>
{{-- Status Pengajuan --}}
<div class="sp-card">
<div class="sp-head"><div class="bar" style="background:#6b7280;"></div><h3>Status Pengajuan</h3></div>
<div class="sp-body" style="display:flex;flex-wrap:wrap;align-items:center;gap:14px;">
@if($tracking === 'draft')
<span class="pill" style="background:#f3f4f6;color:#6b7280;border:1px solid #e5e7eb;"><span class="dot" style="background:#9ca3af;"></span>Draft</span>
@elseif($tracking === 'terkirim')
<span class="pill" style="background:#eff6ff;color:#1d4ed8;border:1px solid #bfdbfe;"><span class="dot" style="background:#3b82f6;"></span>Terkirim ke Kelurahan</span>
@elseif($tracking === 'sedang_validasi')
<span class="pill" style="background:#fffbeb;color:#b45309;border:1px solid #fde68a;"><span class="dot" style="background:#f59e0b;"></span>Sedang Divalidasi</span>
@elseif($tracking === 'selesai')
@if($st === 'disetujui')
<span class="pill" style="background:#f0fdf4;color:#166534;border:1px solid #bbf7d0;"><span class="dot" style="background:#22c55e;"></span>Diterima</span>
@elseif($st === 'ditolak')
<span class="pill" style="background:#fff1f2;color:#9f1239;border:1px solid #fecdd3;"><span class="dot" style="background:#f43f5e;"></span>Tidak Diterima</span>
@else
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold bg-gray-100 text-gray-600">Tidak</span>
<span class="pill" style="background:#fffbeb;color:#b45309;border:1px solid #fde68a;"><span class="dot" style="background:#f59e0b;"></span>Menunggu Hasil</span>
@endif
@endif
<div style="display:flex;gap:18px;">
<div><p class="lbl">Dibuat</p><p class="val">{{ $calonPenerima->created_at ? \Carbon\Carbon::parse($calonPenerima->created_at)->translatedFormat('d M Y, H:i') : '-' }}</p></div>
<div><p class="lbl">Diperbarui</p><p class="val">{{ $calonPenerima->updated_at ? \Carbon\Carbon::parse($calonPenerima->updated_at)->translatedFormat('d M Y, H:i') : '-' }}</p></div>
</div>
@if(!empty($calonPenerima->catatan_admin))
<div style="flex:1;min-width:160px;background:#f9fafb;border:1px solid #e5e7eb;border-radius:10px;padding:7px 11px;font-size:11.5px;color:#6b7280;">
<strong style="color:#374151;">Catatan: </strong>{{ $calonPenerima->catatan_admin }}
</div>
@endif
</div>
</div>
</div>
{{-- KOLOM KANAN --}}
<div class="space-y-4">
{{-- KANAN --}}
<div style="display:flex;flex-direction:column;gap:12px;">
{{-- STATUS PENGAJUAN --}}
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden">
<div class="px-5 py-3 border-b border-gray-100 flex items-center gap-2 bg-gray-50">
<div class="w-1 h-4 bg-gray-700 rounded-full"></div>
<h3 class="text-sm font-semibold text-gray-700">Status Pengajuan</h3>
</div>
<div class="p-5 space-y-3">
@if ($st === 'pending')
<div class="flex items-center gap-2 p-3 rounded-xl bg-amber-50 border border-amber-100">
<span class="w-2 h-2 rounded-full bg-amber-500 shrink-0"></span>
<span class="text-sm font-semibold text-amber-700">Menunggu Verifikasi</span>
</div>
@elseif ($st === 'disetujui')
<div class="flex items-center gap-2 p-3 rounded-xl bg-emerald-50 border border-emerald-100">
<span class="w-2 h-2 rounded-full bg-emerald-500 shrink-0"></span>
<span class="text-sm font-semibold text-emerald-700">Pengajuan Disetujui</span>
</div>
@else
<div class="flex items-center gap-2 p-3 rounded-xl bg-rose-50 border border-rose-100">
<span class="w-2 h-2 rounded-full bg-rose-500 shrink-0"></span>
<span class="text-sm font-semibold text-rose-700">Pengajuan Ditolak</span>
</div>
@endif
<div class="text-xs text-gray-400 space-y-1">
<div class="flex justify-between">
<span>Dibuat</span>
<span class="text-gray-600 font-medium">
{{ $calonPenerima->created_at ? \Carbon\Carbon::parse($calonPenerima->created_at)->translatedFormat('d M Y, H:i') : '-' }}
</span>
</div>
<div class="flex justify-between">
<span>Diperbarui</span>
<span class="text-gray-600 font-medium">
{{ $calonPenerima->updated_at ? \Carbon\Carbon::parse($calonPenerima->updated_at)->translatedFormat('d M Y, H:i') : '-' }}
</span>
</div>
</div>
@if(!empty($calonPenerima->catatan_admin))
<div class="rounded-xl border border-gray-100 bg-gray-50 p-3 text-xs text-gray-600">
<div class="font-semibold text-gray-700 mb-1">Catatan Admin</div>
{{ $calonPenerima->catatan_admin }}
</div>
@endif
</div>
</div>
{{-- PREDIKSI --}}
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden">
<div class="px-5 py-3 border-b border-gray-100 flex items-center gap-2 bg-gray-50">
<div class="w-1 h-4 bg-blue-600 rounded-full"></div>
<h3 class="text-sm font-semibold text-gray-700">Prediksi Kelayakan</h3>
</div>
<div class="p-5">
@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)
<div class="text-center py-2">
<div class="text-4xl font-bold text-gray-900">{{ number_format($probPct, 1) }}<span class="text-xl text-gray-400">%</span></div>
<div class="text-xs text-gray-400 mt-0.5">Probabilitas Kelayakan</div>
</div>
{{-- Progress bar --}}
<div class="mt-3 mb-4">
<div class="w-full bg-gray-100 rounded-full h-2">
<div class="h-2 rounded-full transition-all duration-500
{{ $probPct >= 70 ? 'bg-emerald-500' : ($probPct >= 40 ? 'bg-amber-500' : 'bg-rose-500') }}"
style="width: {{ min($probPct, 100) }}%">
{{-- Prediksi --}}
<div class="sp-card">
<div class="sp-head"><div class="bar" style="background:#2563eb;"></div><h3>Prediksi Kelayakan</h3></div>
@if($pred)
<div class="sp-body" style="display:flex;flex-direction:column;gap:11px;">
<div style="display:flex;align-items:center;gap:12px;">
<div style="position:relative;width:60px;height:60px;flex-shrink:0;">
<svg width="60" height="60" viewBox="0 0 56 56" style="transform:rotate(-90deg);">
<circle cx="28" cy="28" r="24" fill="none" stroke="#f1f5f9" stroke-width="6"/>
<circle cx="28" cy="28" r="24" fill="none" stroke="{{ $sc }}" stroke-width="6" stroke-dasharray="{{ $circum }}" stroke-dashoffset="{{ $offset }}" stroke-linecap="round"/>
</svg>
<div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:900;color:#111827;">
{{ number_format($probPct,0) }}<span style="font-size:9px;color:#9ca3af;">%</span>
</div>
</div>
<div style="flex:1;">
<p class="lbl" style="margin-bottom:4px;">Rekomendasi</p>
<span style="{{ $recBg }}display:inline-block;padding:3px 10px;border-radius:8px;font-size:11.5px;font-weight:700;">{{ $rec }}</span>
<div style="margin-top:7px;height:4px;background:#f1f5f9;border-radius:99px;overflow:hidden;">
<div style="height:100%;border-radius:99px;background:{{ $sc }};width:{{ min($probPct,100) }}%;"></div>
</div>
</div>
</div>
<div class="p-3 rounded-xl border text-sm font-semibold text-center
{{ $probPct >= 70 ? 'bg-emerald-50 border-emerald-100 text-emerald-700' : ($probPct >= 40 ? 'bg-amber-50 border-amber-100 text-amber-700' : 'bg-rose-50 border-rose-100 text-rose-700') }}">
{{ $rec }}
</div>
@else
<div class="py-6 text-center">
<div class="w-10 h-10 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-2">
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
</svg>
@if(!empty($posTop))
<div style="background:#f0fdf4;border:1.5px solid #bbf7d0;border-radius:12px;padding:9px 12px;">
<p style="font-size:10px;font-weight:700;color:#166534;text-transform:uppercase;letter-spacing:.07em;margin-bottom:5px;">Faktor Pendukung</p>
<ul class="fl">@foreach($posTop as $item)<li><svg class="fi" fill="none" stroke="#22c55e" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7"/></svg><span style="color:#166534;">{{ $item }}</span></li>@endforeach</ul>
</div>
<p class="text-xs text-gray-400">Belum ada data prediksi</p>
@endif
@if(!empty($negTop))
<div style="background:#fff1f2;border:1.5px solid #fecdd3;border-radius:12px;padding:9px 12px;">
<p style="font-size:10px;font-weight:700;color:#9f1239;text-transform:uppercase;letter-spacing:.07em;margin-bottom:5px;">Faktor Pengurang</p>
<ul class="fl">@foreach($negTop as $item)<li><svg class="fi" fill="none" stroke="#f43f5e" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12"/></svg><span style="color:#9f1239;">{{ $item }}</span></li>@endforeach</ul>
</div>
@endif
<details class="acc">
<summary>Penjelasan Lengkap <svg class="chev" width="13" height="13" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg></summary>
<div class="ab">
<div style="display:flex;flex-direction:column;gap:5px;">
@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)
<div style="display:flex;justify-content:space-between;gap:8px;font-size:11px;"><span style="color:#9ca3af;">{{ $lbl }}</span><span style="font-weight:600;color:#374151;text-align:right;">{{ $val }}</span></div>
@endforeach
</div>
@if(!empty($explanation['positive']))
<div style="background:#f0fdf4;border:1px solid #bbf7d0;border-radius:10px;padding:8px 11px;">
<p style="font-size:10px;font-weight:700;color:#166534;text-transform:uppercase;letter-spacing:.06em;margin-bottom:4px;">Semua Pendukung</p>
<ul class="fl">@foreach($explanation['positive'] as $i)<li style="font-size:11px;color:#166534;"><span></span>{{ $i }}</li>@endforeach</ul>
</div>
@endif
@if(!empty($explanation['negative']))
<div style="background:#fff1f2;border:1px solid #fecdd3;border-radius:10px;padding:8px 11px;">
<p style="font-size:10px;font-weight:700;color:#9f1239;text-transform:uppercase;letter-spacing:.06em;margin-bottom:4px;">Semua Pengurang</p>
<ul class="fl">@foreach($explanation['negative'] as $i)<li style="font-size:11px;color:#9f1239;"><span></span>{{ $i }}</li>@endforeach</ul>
</div>
@endif
@if(!empty($explanation['summary']))
<div style="background:#eff6ff;border:1px solid #bfdbfe;border-radius:10px;padding:8px 11px;">
<p style="font-size:10px;font-weight:700;color:#1e40af;text-transform:uppercase;letter-spacing:.06em;margin-bottom:3px;">Ringkasan</p>
<p style="font-size:11.5px;color:#1e3a8a;line-height:1.6;">{{ $explanation['summary'] }}</p>
</div>
@endif
</div>
</details>
</div>
@else
<div style="padding:28px 16px;text-align:center;">
<div style="width:38px;height:38px;background:#f3f4f6;border-radius:50%;display:flex;align-items:center;justify-content:center;margin:0 auto 7px;">
<svg width="17" height="17" fill="none" stroke="#d1d5db" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/></svg>
</div>
@endif
</div>
<p style="font-size:11px;color:#9ca3af;">Belum ada data prediksi</p>
</div>
@endif
</div>
{{-- AKSI HAPUS --}}
@if(($calonPenerima->status_verifikasi ?? '') === 'pending')
<div class="bg-white rounded-2xl shadow-sm border border-rose-100 overflow-hidden">
<div class="px-5 py-3 border-b border-rose-100 flex items-center gap-2 bg-rose-50">
<div class="w-1 h-4 bg-rose-500 rounded-full"></div>
<h3 class="text-sm font-semibold text-rose-700">Zona Bahaya</h3>
{{-- Hasil Akhir --}}
@if($tracking === 'selesai')
<div class="sp-card">
<div class="sp-head"><div class="bar" style="background:#10b981;"></div><h3>Hasil Akhir Bantuan</h3></div>
<div class="sp-body" style="display:flex;flex-direction:column;gap:10px;">
@if($st === 'disetujui')
<div style="background:#f0fdf4;border:1.5px solid #bbf7d0;border-radius:12px;padding:10px 12px;">
<div style="font-size:12px;font-weight:800;color:#166534;">Selamat, data Anda diterima.</div>
<div style="font-size:11px;color:#166534;margin-top:3px;">Warga ini termasuk penerima bantuan setelah proses validasi dan filterisasi kuota.</div>
</div>
@elseif($st === 'ditolak')
<div style="background:#fff1f2;border:1.5px solid #fecdd3;border-radius:12px;padding:10px 12px;">
<div style="font-size:12px;font-weight:800;color:#9f1239;">Data tidak masuk penerima final.</div>
<div style="font-size:11px;color:#9f1239;margin-top:3px;">Warga ini belum terpilih pada penetapan penerima bantuan untuk periode ini.</div>
</div>
@else
<div style="background:#fffbeb;border:1.5px solid #fde68a;border-radius:12px;padding:10px 12px;">
<div style="font-size:12px;font-weight:800;color:#b45309;">Hasil akhir belum ditetapkan.</div>
<div style="font-size:11px;color:#b45309;margin-top:3px;">Masih menunggu keputusan akhir dari admin kelurahan.</div>
</div>
@endif
</div>
<div class="p-5">
<p class="text-xs text-gray-500 mb-3">Hapus data ini secara permanen. Tindakan ini tidak dapat dibatalkan.</p>
<form action="{{ route('calon-penerima.destroy', $calonPenerima->id) }}"
method="POST"
onsubmit="return confirm('Yakin ingin menghapus data {{ $calonPenerima->nama_lengkap }}? Tindakan ini tidak dapat dibatalkan.')">
@csrf
@method('DELETE')
<button type="submit"
class="w-full inline-flex items-center justify-center gap-2 px-4 py-2 rounded-xl bg-rose-600 text-white text-sm font-semibold hover:bg-rose-700 transition">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
</svg>
</div>
@endif
{{-- Zona Bahaya --}}
@if($tracking === 'draft')
<div class="sp-card" style="border-color:#fecdd3;">
<div class="sp-head" style="background:#fff1f2;border-color:#fecdd3;"><div class="bar" style="background:#f43f5e;"></div><h3 style="color:#9f1239;">Zona Bahaya</h3></div>
<div class="sp-body">
<p style="font-size:11.5px;color:#6b7280;margin-bottom:10px;">Hapus data ini secara permanen. Tindakan ini <strong style="color:#374151;">tidak dapat dibatalkan</strong>.</p>
<form action="{{ route('rt.calon-penerima.destroy', $calonPenerima->id) }}" method="POST" onsubmit="return confirm('Yakin ingin menghapus data {{ $calonPenerima->nama_lengkap }}?')">
@csrf @method('DELETE')
<button type="submit" style="width:100%;display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:9px 16px;background:#e11d48;color:#fff;font-size:12px;font-weight:700;border:none;border-radius:10px;cursor:pointer;box-shadow:0 2px 8px rgba(225,29,72,.22);">
<svg width="13" height="13" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
Hapus Data
</button>
</form>
@ -294,4 +417,5 @@ class="w-full inline-flex items-center justify-center gap-2 px-4 py-2 rounded-xl
</div>
</div>
</x-app-layout>

View File

@ -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';