Compare commits

...

10 Commits

Author SHA1 Message Date
Ryanpratama01 2f6f211649 fix problem in conttroller admin 2026-06-25 20:14:57 +07:00
Ryanpratama01 9d516a5edf push fix project 2026-06-25 19:33:55 +07:00
Ryanpratama01 85181d866b perbaikan semuanya dan conncting hosting 2026-04-21 18:03:56 +07:00
Ryanpratama01 89f2f3dd76 Merge branch 'main' of https://github.com/Ryanpratama01/Sibantudes_Project-Tugas-Akhir 2026-03-16 05:34:10 +07:00
Ryanpratama01 ae7992ac83 refresh all 2026-03-16 05:33:14 +07:00
Ryanpratama01 e2e4c77810
Create CNAME 2026-03-16 05:29:35 +07:00
Ryanpratama01 59c74d7865 run deploy 2026-03-16 05:05:15 +07:00
Ryanpratama01 da8413046d final railway mysql setup 2026-03-16 04:06:54 +07:00
Ryanpratama01 d2dd3dc51c run migration on deploy 2026-03-16 04:01:18 +07:00
Ryanpratama01 fe97c2e373 fix railway db config to mysql 2026-03-16 03:48:42 +07:00
61 changed files with 8696 additions and 4221 deletions

21
.htaccess Normal file
View File

@ -0,0 +1,21 @@
<IfModule mod_rewrite.c>
<IfModule mod_negotiation.c>
Options -MultiViews -Indexes
</IfModule>
RewriteEngine On
RewriteCond %{HTTP:Authorization} .
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
RewriteCond %{HTTP:x-xsrf-token} .
RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}]
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} (.+)/$
RewriteRule ^ %1 [L,R=301]
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [L]
</IfModule>

1
CNAME Normal file
View File

@ -0,0 +1 @@
sibantudes.com

View File

@ -1,30 +0,0 @@
FROM php:8.3-cli
RUN apt-get update && apt-get install -y \
git \
unzip \
curl \
libpng-dev \
libjpeg62-turbo-dev \
libfreetype6-dev \
libzip-dev \
zip \
libonig-dev \
libxml2-dev \
libicu-dev \
nodejs \
npm \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install pdo pdo_mysql mbstring zip intl gd
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /app
COPY . /app
RUN composer install --no-interaction --optimize-autoloader --no-dev
RUN npm install
RUN npm run build
CMD php artisan serve --host=0.0.0.0 --port=${PORT:-8080}

View File

@ -0,0 +1,63 @@
<?php
namespace App\Console\Commands;
use App\Models\PenerimaFinal;
use App\Models\PenerimaFinalArsip;
use App\Models\CalonPenerima;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Schedule;
class ResetBulananPenerimaFinal extends Command
{
protected $signature = 'blt:reset-bulanan';
protected $description = 'Arsipkan data penerima final lalu reset halaman admin dan RT';
public function handle(): int
{
$periode = now()->subMonth()->format('Y-m');
$penerimas = PenerimaFinal::where('status_verifikasi', 'disetujui')->get();
if ($penerimas->isEmpty()) {
$this->info('Tidak ada data untuk diarsipkan.');
return 0;
}
// LANGKAH A — Arsipkan semua data dulu
foreach ($penerimas as $p) {
PenerimaFinalArsip::create([
'periode_arsip' => $periode,
'penerima_final_id' => $p->id,
'nama_lengkap' => $p->nama_lengkap,
'nik' => $p->nik,
'no_kk' => $p->no_kk,
'nomor_rt' => $p->nomor_rt,
'nama_dusun' => $p->nama_dusun,
'pekerjaan' => $p->pekerjaan,
'penghasilan' => $p->penghasilan,
'jumlah_tanggungan' => $p->jumlah_tanggungan,
'aset_kepemilikan' => $p->aset_kepemilikan,
'bantuan_lain' => $p->bantuan_lain,
'usia' => $p->usia,
'probability' => $p->probability,
'status_verifikasi' => $p->status_verifikasi,
'tanggal_penetapan' => $p->tanggal_penetapan,
'periode_bantuan' => $p->periode_bantuan,
'jumlah_bantuan' => $p->jumlah_bantuan,
]);
}
// LANGKAH B — Kosongkan halaman admin
PenerimaFinal::where('status_verifikasi', 'disetujui')->delete();
// LANGKAH C — Kosongkan halaman RT
CalonPenerima::where('tracking_status', 'selesai')
->update(['tracking_status' => 'belum_validasi']);
Log::info("Reset bulanan selesai: {$penerimas->count()} data diarsipkan periode {$periode}.");
$this->info("Selesai! {$penerimas->count()} data diarsipkan ({$periode}). Admin & RT sudah kosong.");
return 0;
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace App\Console\Commands;
use App\Models\PenerimaFinal;
use App\Models\RiwayatPenerima;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class ResetPenerimaFinalBulanan extends Command
{
protected $signature = 'penerima:reset-bulanan';
protected $description = 'Arsipkan semua data penerima_finals ke riwayat_penerima lalu kosongkan tabelnya (dijalankan tiap tanggal 1)';
public function handle(): int
{
$periode = now()->subMonth()->format('Y-m'); // arsip bulan sebelumnya
$semua = PenerimaFinal::all();
if ($semua->isEmpty()) {
$this->info('Tidak ada data penerima_finals untuk diarsipkan.');
return self::SUCCESS;
}
DB::transaction(function () use ($semua, $periode) {
foreach ($semua as $p) {
RiwayatPenerima::create([
'periode' => $periode,
'nama_lengkap' => $p->nama_lengkap,
'nik' => $p->nik,
'no_kk' => $p->no_kk,
'rt_id' => $p->rt_id,
'nomor_rt' => $p->nomor_rt,
'nama_dusun' => $p->nama_dusun,
'pekerjaan' => $p->pekerjaan,
'penghasilan' => $p->penghasilan,
'jumlah_tanggungan' => $p->jumlah_tanggungan,
'aset_kepemilikan' => $p->aset_kepemilikan,
'kondisi_rumah' => $p->kondisi_rumah,
'meteran_listrik' => $p->meteran_listrik,
'sumber_air' => $p->sumber_air,
'bantuan_lain' => $p->bantuan_lain,
'usia' => $p->usia,
'probability' => $p->probability,
'status_verifikasi' => $p->status_verifikasi,
'tanggal_penetapan' => $p->tanggal_penetapan,
'jumlah_bantuan' => $p->jumlah_bantuan,
'di_arsipkan_pada' => now(),
]);
}
// Kosongkan tabel penerima_finals
PenerimaFinal::truncate();
});
$this->info("Berhasil mengarsipkan {$semua->count()} data penerima ke periode {$periode}.");
return self::SUCCESS;
}
}

View File

@ -6,6 +6,7 @@
use App\Models\CalonPenerima;
use App\Models\Dusun;
use App\Models\PenerimaFinal;
use App\Models\PeriodeDanaBlt;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
@ -37,6 +38,7 @@ public function index(Request $request)
$query = CalonPenerima::query()
->with(['rt.dusun', 'prediksiKelayakan'])
->whereIn('tracking_status', ['sedang_validasi', 'selesai'])
->where('status_verifikasi', '!=', 'ditolak')
->whereHas('rt', function ($q) use ($dusunId) {
$q->where('dusun_id', $dusunId);
});
@ -54,7 +56,59 @@ public function index(Request $request)
->pluck('nik');
}
return view('admin.filterisasi', compact('dusuns', 'dusunId', 'kuota', 'candidates', 'pickedNiks'));
// Ambil riwayat periode dana
$periodeDana = PeriodeDanaBlt::orderBy('id')->get();
$periodeTerakhir = $periodeDana->last();
$nextPeriodeNum = $periodeDana->count() + 1;
$danaSisaTerakhir = $periodeTerakhir ? $periodeTerakhir->dana_sisa : null;
return view('admin.filterisasi', compact(
'dusuns', 'dusunId', 'kuota', 'candidates', 'pickedNiks',
'periodeDana', 'periodeTerakhir', 'nextPeriodeNum', 'danaSisaTerakhir'
));
}
public function simpanPeriode(Request $request)
{
$user = $request->user();
if (!$user || $user->role !== 'admin') {
abort(403, 'Akses ditolak: hanya Admin.');
}
$data = $request->validate([
'periode' => 'required|string|max:50',
'dana_awal' => 'required|integer|min:1',
'jumlah_penerima' => 'required|integer|min:1',
'keterangan' => 'nullable|string|max:255',
]);
$danaTerpakai = $data['jumlah_penerima'] * 300000;
$danaSisa = $data['dana_awal'] - $danaTerpakai;
PeriodeDanaBlt::create([
'periode' => $data['periode'],
'dana_awal' => $data['dana_awal'],
'jumlah_penerima' => $data['jumlah_penerima'],
'dana_terpakai' => $danaTerpakai,
'dana_sisa' => max(0, $danaSisa),
'keterangan' => $data['keterangan'] ?? null,
]);
return back()->with('success', "Periode {$data['periode']} berhasil disimpan. Sisa dana: Rp " . number_format(max(0, $danaSisa), 0, ',', '.'));
}
public function hapusPeriode(Request $request, $id)
{
$user = $request->user();
if (!$user || $user->role !== 'admin') {
abort(403, 'Akses ditolak: hanya Admin.');
}
PeriodeDanaBlt::findOrFail($id)->delete();
return back()->with('success', 'Data periode berhasil dihapus.');
}
public function tetapkan(Request $request)
@ -79,6 +133,7 @@ public function tetapkan(Request $request)
$allCandidates = CalonPenerima::query()
->with(['rt.dusun', 'prediksiKelayakan'])
->whereIn('tracking_status', ['sedang_validasi', 'selesai'])
->where('status_verifikasi', '!=', 'ditolak')
->whereHas('rt', function ($r) use ($dusunId) {
$r->where('dusun_id', $dusunId);
})
@ -91,14 +146,12 @@ public function tetapkan(Request $request)
$topIds = $allCandidates->take($kuota)->pluck('id')->all();
$allIds = $allCandidates->pluck('id')->all();
// Top kuota = disetujui
if (!empty($topIds)) {
CalonPenerima::whereIn('id', $topIds)->update([
'status_verifikasi' => 'disetujui',
]);
}
// Sisanya = ditolak
$notPickedIds = array_diff($allIds, $topIds);
if (!empty($notPickedIds)) {
@ -107,7 +160,6 @@ public function tetapkan(Request $request)
]);
}
// Ambil ulang data final setelah status diperbarui
$finalCandidates = CalonPenerima::query()
->with(['rt.dusun', 'prediksiKelayakan'])
->whereIn('id', $allIds)
@ -118,7 +170,7 @@ public function tetapkan(Request $request)
PenerimaFinal::updateOrCreate(
[
'nik' => $candidate->nik,
'nik' => $candidate->nik,
'periode_bantuan' => '2026 Triwulan 1',
],
[
@ -131,6 +183,9 @@ public function tetapkan(Request $request)
'penghasilan' => $candidate->penghasilan ?? 0,
'jumlah_tanggungan' => $candidate->jumlah_tanggungan ?? 0,
'aset_kepemilikan' => $candidate->aset_kepemilikan ?? '-',
'kondisi_rumah' => $candidate->kondisi_rumah ?? 'Tidak Diketahui',
'meteran_listrik' => $candidate->meteran_listrik ?? 'Tidak Diketahui',
'sumber_air' => $candidate->sumber_air ?? 'Tidak Diketahui',
'bantuan_lain' => $candidate->bantuan_lain ?? 'tidak',
'usia' => $candidate->usia ?? 0,
'probability' => $probability,
@ -166,6 +221,7 @@ public function resetDusun(Request $request)
$r->where('dusun_id', $dusun->id);
})
->whereIn('tracking_status', ['sedang_validasi', 'selesai'])
->where('status_verifikasi', '!=', 'ditolak')
->pluck('id')
->all();

View File

@ -3,14 +3,16 @@
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\CalonPenerima;
use App\Models\PenerimaFinal;
use App\Models\LaporanPublik;
use App\Models\CalonPenerima;
use App\Models\Dusun;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Barryvdh\DomPDF\Facade\Pdf;
use App\Exports\PenerimaFinalExport;
use Maatwebsite\Excel\Facades\Excel;
use App\Exports\LaporanExport;
class LaporanController extends Controller
{
@ -22,14 +24,48 @@ public function index(Request $request)
abort(403, 'Akses ditolak: hanya Admin.');
}
$laporans = PenerimaFinal::query()
->where('status_verifikasi', 'disetujui')
->orderByDesc('tanggal_penetapan')
->get();
$dusuns = Dusun::orderBy('nama_dusun')->get();
$filterDusun = $request->query('dusun', '');
$filterStatus = $request->query('status', '');
$filterPeriode = $request->query('periode', '');
$query = PenerimaFinal::query();
if ($filterDusun !== '') {
$query->where('nama_dusun', $filterDusun);
}
if ($filterStatus !== '') {
$query->where('status_verifikasi', $filterStatus);
}
if ($filterPeriode !== '') {
$query->where('periode_bantuan', $filterPeriode);
}
$penerimas = $query->orderBy('nama_dusun')->orderByDesc('probability')->paginate(15)->withQueryString();
$totalPenerima = PenerimaFinal::where('status_verifikasi', 'ditetapkan')->count();
$totalDitolak = PenerimaFinal::where('status_verifikasi', 'ditolak')->count();
$totalBantuan = PenerimaFinal::where('status_verifikasi', 'ditetapkan')->sum('jumlah_bantuan');
$periodes = PenerimaFinal::distinct()->orderByDesc('periode_bantuan')->pluck('periode_bantuan');
$laporanPubliks = LaporanPublik::latest()->get();
return view('admin.laporan', compact('laporans', 'laporanPubliks'));
return view('admin.laporan', compact(
'penerimas',
'dusuns',
'filterDusun',
'filterStatus',
'filterPeriode',
'totalPenerima',
'totalDitolak',
'totalBantuan',
'periodes',
'laporanPubliks'
));
}
public function exportPdf(Request $request)
@ -40,15 +76,30 @@ public function exportPdf(Request $request)
abort(403, 'Akses ditolak: hanya Admin.');
}
$laporans = PenerimaFinal::query()
->where('status_verifikasi', 'disetujui')
->orderByDesc('tanggal_penetapan')
->get();
$filterDusun = $request->query('dusun', '');
$filterStatus = $request->query('status', '');
$filterPeriode = $request->query('periode', '');
$pdf = Pdf::loadView('admin.laporan-pdf', compact('laporans'))
$query = PenerimaFinal::query();
if ($filterDusun !== '') {
$query->where('nama_dusun', $filterDusun);
}
if ($filterStatus !== '') {
$query->where('status_verifikasi', $filterStatus);
}
if ($filterPeriode !== '') {
$query->where('periode_bantuan', $filterPeriode);
}
$penerimas = $query->orderBy('nama_dusun')->orderByDesc('probability')->get();
$pdf = Pdf::loadView('admin.laporan-pdf', compact('penerimas', 'filterDusun', 'filterStatus', 'filterPeriode'))
->setPaper('a4', 'landscape');
return $pdf->download('laporan-penerima-blt-dd-kelurahan-ngerong.pdf');
return $pdf->download('laporan-penerima-blt-dd.pdf');
}
public function exportExcel(Request $request)
@ -59,7 +110,14 @@ public function exportExcel(Request $request)
abort(403, 'Akses ditolak: hanya Admin.');
}
return Excel::download(new PenerimaFinalExport, 'laporan-penerima-blt-dd-kelurahan-ngerong.xlsx');
$filterDusun = $request->query('dusun', '');
$filterStatus = $request->query('status', '');
$filterPeriode = $request->query('periode', '');
return Excel::download(
new LaporanExport($filterDusun, $filterStatus, $filterPeriode),
'laporan-penerima-blt-dd.xlsx'
);
}
public function kirimKeRt(Request $request)
@ -70,19 +128,28 @@ public function kirimKeRt(Request $request)
abort(403, 'Akses ditolak: hanya Admin.');
}
$candidateIds = CalonPenerima::query()
->whereIn('tracking_status', ['sedang_validasi'])
->whereIn('status_verifikasi', ['disetujui', 'ditolak'])
->pluck('id')
->all();
// Tandai semua CalonPenerima yang sudah selesai agar RT bisa lihat hasilnya
CalonPenerima::where('tracking_status', 'selesai')
->update(['tracking_status' => 'selesai']);
if (!empty($candidateIds)) {
CalonPenerima::whereIn('id', $candidateIds)->update([
'tracking_status' => 'selesai',
]);
return back()->with('success', 'Laporan berhasil dikirim ke RT.');
}
public function resetManual(Request $request)
{
$user = $request->user();
if (!$user || $user->role !== 'admin') {
abort(403, 'Akses ditolak: hanya Admin.');
}
return back()->with('success', 'Hasil final berhasil dikirim ke RT dusun. RT sekarang hanya dapat melihat status diterima atau ditolak.');
$request->validate([
'periode_bantuan' => ['required', 'string'],
]);
PenerimaFinal::where('periode_bantuan', $request->periode_bantuan)->delete();
return back()->with('success', 'Data laporan periode ' . $request->periode_bantuan . ' berhasil direset.');
}
public function uploadPublikPdf(Request $request)
@ -93,21 +160,20 @@ public function uploadPublikPdf(Request $request)
abort(403, 'Akses ditolak: hanya Admin.');
}
$data = $request->validate([
'judul' => 'required|string|max:255',
'file_pdf' => 'required|mimes:pdf|max:5120',
$request->validate([
'judul' => ['required', 'string', 'max:255'],
'file' => ['required', 'file', 'mimes:pdf', 'max:10240'],
]);
$path = $request->file('file_pdf')->store('laporan-publik', 'public');
$path = $request->file('file')->store('laporan-publik', 'public');
LaporanPublik::create([
'judul' => $data['judul'],
'judul' => $request->judul,
'file_path' => $path,
'uploaded_by' => $user->id,
'is_active' => true,
]);
return back()->with('success', 'PDF laporan publik berhasil diupload.');
return back()->with('success', 'PDF publik berhasil diupload.');
}
public function hapusPublikPdf(Request $request, $id)
@ -120,27 +186,28 @@ public function hapusPublikPdf(Request $request, $id)
$laporan = LaporanPublik::findOrFail($id);
if ($laporan->file_path && Storage::disk('public')->exists($laporan->file_path)) {
if (Storage::disk('public')->exists($laporan->file_path)) {
Storage::disk('public')->delete($laporan->file_path);
}
$laporan->delete();
return back()->with('success', 'PDF laporan publik berhasil dihapus.');
return back()->with('success', 'PDF publik berhasil dihapus.');
}
public function bersihkanRiwayatSelesai(Request $request)
{
$user = $request->user();
$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.');
}
$deleted = \App\Models\CalonPenerima::query()
->where('tracking_status', 'selesai')
->delete();
// Arsipkan semua CalonPenerima yang tracking_status = selesai
CalonPenerima::where('tracking_status', 'selesai')
->whereNull('arsip_tahun')
->update(['arsip_tahun' => now()->year]);
return back()->with('success', "Berhasil membersihkan {$deleted} data riwayat selesai. Arsip laporan tetap tersimpan.");
return back()->with('success', 'Riwayat data selesai berhasil dibersihkan dan diarsipkan.');
}
}

View File

@ -0,0 +1,83 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules\Password;
class ManualPasswordResetController extends Controller
{
/**
* Tampilkan halaman reset password manual.
*/
public function showForm()
{
return view('auth.forgot-password');
}
/**
* AJAX: cek apakah email terdaftar di sistem.
* POST /password/manual/check
*/
public function checkEmail(Request $request)
{
try {
$validated = $request->validate([
'email' => ['required', 'email'],
]);
$found = User::where('email', $validated['email'])->exists();
return response()->json(['found' => $found]);
} catch (\Illuminate\Validation\ValidationException $e) {
return response()->json([
'found' => false,
'message' => collect($e->errors())->flatten()->first(),
], 422);
} catch (\Exception $e) {
return response()->json([
'found' => false,
'message' => 'Terjadi kesalahan server: ' . $e->getMessage(),
], 500);
}
}
/**
* AJAX: update password berdasarkan email yang sudah diverifikasi.
* POST /password/manual/update
*/
public function updatePassword(Request $request)
{
try {
$validated = $request->validate([
'email' => ['required', 'email', 'exists:users,email'],
'password' => ['required', 'confirmed', Password::min(8)],
'password_confirmation' => ['required'],
]);
$user = User::where('email', $validated['email'])->firstOrFail();
$user->update([
'password' => Hash::make($validated['password']),
]);
return response()->json(['success' => true]);
} catch (\Illuminate\Validation\ValidationException $e) {
return response()->json([
'success' => false,
'message' => collect($e->errors())->flatten()->first(),
], 422);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Terjadi kesalahan server: ' . $e->getMessage(),
], 500);
}
}
}

View File

@ -6,6 +6,7 @@
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Facades\Log;
use Illuminate\View\View;
class PasswordResetLinkController extends Controller
@ -29,6 +30,10 @@ public function store(Request $request): RedirectResponse
'email' => ['required', 'email'],
]);
Log::info('TES RESET PASSWORD MASUK', [
'email' => $request->email,
]);
// We will send the password reset link to this user. Once we have attempted
// to send the link, we will examine the response then see the message we
// need to show to the user. Finally, we'll send out a proper response.
@ -41,4 +46,4 @@ public function store(Request $request): RedirectResponse
: back()->withInput($request->only('email'))
->withErrors(['email' => __($status)]);
}
}
}

View File

@ -10,24 +10,38 @@ class LandingPageController extends Controller
{
public function index(Request $request)
{
$nik = trim((string) $request->query('nik',''));
$nik = trim((string) $request->query('nik', ''));
$statusData = null;
$penerimaFinal = null;
if($nik !== ''){
if ($nik !== '') {
$statusData = CalonPenerima::with(['rt.dusun'])
->where('nik',$nik)
->where('nik', $nik)
->first();
// LOGIKA PENETAPAN: pakai kolom status_verifikasi yang benar-benar ada di tabel
if ($statusData && isset($statusData->status_verifikasi)) {
$currentStatus = strtolower($statusData->status_verifikasi);
if (in_array($currentStatus, ['ditolak', 'disetujui'])) {
// Buat object penampung agar dibaca sebagai '$penerimaFinal' oleh file Blade Anda
$penerimaFinal = new \stdClass();
$penerimaFinal->status = $statusData->status_verifikasi;
$penerimaFinal->created_at = $statusData->updated_at ?? now();
}
}
}
$laporanPubliks = LaporanPublik::where('is_active',true)
$laporanPubliks = LaporanPublik::where('is_active', true)
->latest()
->get();
return view('landing',compact(
return view('landing', compact(
'nik',
'statusData',
'laporanPubliks'
'laporanPubliks',
'penerimaFinal'
));
}
}

View File

@ -7,9 +7,12 @@
use App\Models\PrediksiKelayakan;
use App\Models\RT;
use App\Services\MLPredictionService;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
class CalonPenerimaController extends Controller
{
@ -34,15 +37,12 @@ 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();
// fallback kalau data RT user belum ada (harusnya jarang terjadi)
if ($rts->isEmpty()) {
$rts = RT::with('dusun')->get();
}
@ -60,75 +60,142 @@ public function store(Request $request)
}
$validated = $request->validate([
// 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' => 'nullable|string|max:255', // akan kita set otomatis
'pekerjaan' => 'required|string',
'penghasilan' => 'required|numeric|min:0',
'jumlah_tanggungan' => 'required|integer|min:0',
'aset_kepemilikan' => 'required|string',
'bantuan_lain' => 'required|in:ya,tidak',
'usia' => 'required|integer|min:17|max:100',
'status_perkawinan' => 'required|string',
'rt_id' => 'nullable|exists:rts,id',
'no_kk' => 'required|digits:16|unique:calon_penerimas,no_kk',
'nik' => 'required|digits:16|unique:calon_penerimas,nik',
'nama_lengkap' => 'required|string|max:255',
'jenis_kelamin' => 'required|in:Laki-laki,Perempuan',
'tempat_lahir' => 'required|string|max:255',
'tanggal_lahir' => 'required|date',
'alamat' => 'required|string',
'desa' => 'nullable|string|max:255',
'pekerjaan' => 'required|string',
'penghasilan' => 'required|numeric|min:0',
'jumlah_tanggungan' => 'required|integer|min:0',
'aset_pilihan' => 'nullable|array',
'aset_pilihan.*' => 'nullable|string',
'aset_manual' => 'nullable|string|max:255',
'bantuan_lain' => 'required|in:ya,tidak',
'usia' => 'required|integer|min:17|max:100',
'status_perkawinan' => 'required|string',
'lantai_rumah' => 'required|string',
'dinding_rumah' => 'required|string',
'atap_rumah' => 'required|string',
'luas_rumah_m2' => 'required|integer|min:1',
'status_kepemilikan_rumah'=> 'required|string',
'meteran_listrik' => 'required|in:450VA,900VA,1300VA+',
'sumber_air' => 'required|in:PDAM,Sumur,Sungai',
'foto_rumah_depan' => 'nullable|image',
'foto_rumah_belakang' => 'nullable|image',
'foto_rumah_kanan' => 'nullable|image',
'foto_rumah_kiri' => 'nullable|image',
'foto_kk' => 'nullable|image',
'foto_ktp' => 'nullable|image',
'foto_rekening_listrik' => 'nullable|mimes:jpg,jpeg,png,pdf',
'foto_meteran_air' => 'nullable|image',
'dokumen_pendukung' => 'nullable|mimes:jpg,jpeg,png,pdf,doc,docx',
], [
'nik.unique' => 'NIK sudah pernah diinput.',
'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
$asetGabung = $this->buildAsetString(
$validated['aset_pilihan'] ?? [],
$validated['aset_manual'] ?? ''
);
$validated['aset_kepemilikan'] = $asetGabung;
$validated['kondisi_rumah'] = $this->deriveKondisiRumah(
$validated['lantai_rumah'],
$validated['dinding_rumah']
);
$rt = RT::with('dusun')->find($myRtId);
if ($rt && $rt->dusun) {
$validated['desa'] = $rt->dusun->nama_dusun;
} else {
// fallback aman kalau relasi dusun kosong
$validated['desa'] = $validated['desa'] ?? '-';
}
DB::beginTransaction();
try {
$fotoImages = [
'foto_rumah_depan', 'foto_rumah_belakang', 'foto_rumah_kanan', 'foto_rumah_kiri',
'foto_kk', 'foto_ktp', 'foto_meteran_air',
];
$fotoFiles = [
'foto_rekening_listrik', 'dokumen_pendukung',
];
foreach ($fotoImages as $field) {
if ($request->hasFile($field)) {
$validated[$field] = $this->simpanFotoKompres($request->file($field));
}
}
foreach ($fotoFiles as $field) {
if ($request->hasFile($field)) {
$validated[$field] = $request->file($field)->store('dokumen-warga', 'public');
}
}
$calonPenerima = CalonPenerima::create([
'user_id' => $user->id,
'rt_id' => $validated['rt_id'],
'no_kk' => $validated['no_kk'],
'nik' => $validated['nik'],
'nama_lengkap' => $validated['nama_lengkap'],
'jenis_kelamin' => $validated['jenis_kelamin'],
'tempat_lahir' => $validated['tempat_lahir'],
'tanggal_lahir' => $validated['tanggal_lahir'],
'alamat' => $validated['alamat'],
'desa' => $validated['desa'],
'pekerjaan' => $validated['pekerjaan'],
'penghasilan' => $validated['penghasilan'],
'jumlah_tanggungan' => $validated['jumlah_tanggungan'],
'aset_kepemilikan' => $validated['aset_kepemilikan'],
'bantuan_lain' => $validated['bantuan_lain'],
'usia' => $validated['usia'],
'status_perkawinan' => $validated['status_perkawinan'],
'status_verifikasi' => 'pending',
'tracking_status' => 'draft',
'user_id' => $user->id,
'rt_id' => $validated['rt_id'],
'no_kk' => $validated['no_kk'],
'nik' => $validated['nik'],
'nama_lengkap' => $validated['nama_lengkap'],
'jenis_kelamin' => $validated['jenis_kelamin'],
'tempat_lahir' => $validated['tempat_lahir'],
'tanggal_lahir' => $validated['tanggal_lahir'],
'alamat' => $validated['alamat'],
'desa' => $validated['desa'],
'pekerjaan' => $validated['pekerjaan'],
'penghasilan' => $validated['penghasilan'],
'jumlah_tanggungan' => $validated['jumlah_tanggungan'],
'aset_kepemilikan' => $validated['aset_kepemilikan'],
'kondisi_rumah' => $validated['kondisi_rumah'],
'lantai_rumah' => $validated['lantai_rumah'],
'dinding_rumah' => $validated['dinding_rumah'],
'atap_rumah' => $validated['atap_rumah'],
'luas_rumah_m2' => $validated['luas_rumah_m2'],
'status_kepemilikan_rumah' => $validated['status_kepemilikan_rumah'],
'meteran_listrik' => $validated['meteran_listrik'],
'sumber_air' => $validated['sumber_air'],
'bantuan_lain' => $validated['bantuan_lain'],
'usia' => $validated['usia'],
'status_perkawinan' => $validated['status_perkawinan'],
'status_verifikasi' => 'pending',
'tracking_status' => 'draft',
'periode_bulan' => Carbon::now('Asia/Jakarta')->startOfMonth()->toDateString(),
'foto_rumah_depan' => $validated['foto_rumah_depan'] ?? null,
'foto_rumah_belakang' => $validated['foto_rumah_belakang'] ?? null,
'foto_rumah_kanan' => $validated['foto_rumah_kanan'] ?? null,
'foto_rumah_kiri' => $validated['foto_rumah_kiri'] ?? null,
'foto_kk' => $validated['foto_kk'] ?? null,
'foto_ktp' => $validated['foto_ktp'] ?? null,
'foto_rekening_listrik' => $validated['foto_rekening_listrik'] ?? null,
'foto_meteran_air' => $validated['foto_meteran_air'] ?? null,
'dokumen_pendukung' => $validated['dokumen_pendukung'] ?? null,
]);
// 🔮 ML Prediction
$predictionData = [
'pekerjaan' => $validated['pekerjaan'],
'penghasilan' => $validated['penghasilan'],
'jumlah_tanggungan' => $validated['jumlah_tanggungan'],
'aset_kepemilikan' => $validated['aset_kepemilikan'],
'bantuan_lain' => $validated['bantuan_lain'],
'usia' => $validated['usia'],
'pekerjaan' => $validated['pekerjaan'],
'penghasilan' => $validated['penghasilan'],
'jumlah_tanggungan' => $validated['jumlah_tanggungan'],
'aset_pilihan' => $validated['aset_pilihan'] ?? [],
'aset_manual' => $validated['aset_manual'] ?? '',
'bantuan_lain' => $validated['bantuan_lain'],
'usia' => $validated['usia'],
'status_perkawinan' => $validated['status_perkawinan'],
'lantai_rumah' => $validated['lantai_rumah'],
'dinding_rumah' => $validated['dinding_rumah'],
'atap_rumah' => $validated['atap_rumah'],
'luas_rumah_m2' => $validated['luas_rumah_m2'],
'status_kepemilikan_rumah' => $validated['status_kepemilikan_rumah'],
'meteran_listrik' => $validated['meteran_listrik'],
'sumber_air' => $validated['sumber_air'],
];
$prediction = $this->mlService->getPrediction($predictionData);
@ -136,8 +203,11 @@ public function store(Request $request)
if ($prediction) {
PrediksiKelayakan::create([
'calon_penerima_id' => $calonPenerima->id,
'probability' => $prediction['probability'],
'recommendation' => $prediction['recommendation'],
'probability' => $prediction['probability'],
'recommendation' => $prediction['recommendation'],
'kondisi_rumah' => $validated['kondisi_rumah'],
'meteran_listrik' => $validated['meteran_listrik'],
'sumber_air' => $validated['sumber_air'],
]);
}
@ -147,10 +217,7 @@ public function store(Request $request)
->with('success', 'Data calon penerima berhasil ditambahkan!');
} catch (\Exception $e) {
DB::rollBack();
return back()
->withInput()
->with('error', 'Terjadi kesalahan: ' . $e->getMessage());
return back()->withInput()->with('error', 'Terjadi kesalahan: ' . $e->getMessage());
}
}
@ -158,8 +225,7 @@ public function show(CalonPenerima $calonPenerima)
{
$user = Auth::user();
// 🔒 RT hanya boleh lihat data miliknya
if ($calonPenerima->user_id !== $user->id) {
if ((int) $calonPenerima->user_id !== (int) $user->id) {
abort(403, 'Unauthorized action.');
}
@ -174,8 +240,7 @@ public function edit(CalonPenerima $calonPenerima)
{
$user = Auth::user();
// 🔒 RT hanya bisa edit kalau miliknya + masih draft
if ($calonPenerima->user_id !== $user->id || ($calonPenerima->tracking_status ?? 'draft') !== 'draft') {
if ((int) $calonPenerima->user_id !== (int) $user->id || ($calonPenerima->tracking_status ?? 'draft') !== 'draft') {
abort(403, 'Data yang sudah diajukan tidak dapat diubah.');
}
@ -186,8 +251,7 @@ public function update(Request $request, CalonPenerima $calonPenerima)
{
$user = Auth::user();
// 🔒 RT hanya bisa update kalau miliknya + masih draft
if ($calonPenerima->user_id !== $user->id || ($calonPenerima->tracking_status ?? 'draft') !== 'draft') {
if ((int) $calonPenerima->user_id !== (int) $user->id || ($calonPenerima->tracking_status ?? 'draft') !== 'draft') {
abort(403, 'Data yang sudah diajukan tidak dapat diubah.');
}
@ -197,34 +261,57 @@ public function update(Request $request, CalonPenerima $calonPenerima)
}
$validated = $request->validate([
'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' => 'nullable|string|max:255', // akan di-override
'pekerjaan' => 'required|string',
'penghasilan' => 'required|numeric|min:0',
'jumlah_tanggungan' => 'required|integer|min:0',
'aset_kepemilikan' => 'required|string',
'bantuan_lain' => 'required|in:ya,tidak',
'usia' => 'required|integer|min:17|max:100',
'status_perkawinan' => 'required|string',
'rt_id' => 'nullable|exists:rts,id',
'no_kk' => 'required|digits:16|unique:calon_penerimas,no_kk,' . $calonPenerima->id,
'nik' => 'required|digits:16|unique:calon_penerimas,nik,' . $calonPenerima->id,
'nama_lengkap' => 'required|string|max:255',
'jenis_kelamin' => 'required|in:Laki-laki,Perempuan',
'tempat_lahir' => 'required|string|max:255',
'tanggal_lahir' => 'required|date',
'alamat' => 'required|string',
'desa' => 'nullable|string|max:255',
'pekerjaan' => 'required|string',
'penghasilan' => 'required|numeric|min:0',
'jumlah_tanggungan' => 'required|integer|min:0',
'aset_pilihan' => 'nullable|array',
'aset_pilihan.*' => 'nullable|string',
'aset_manual' => 'nullable|string|max:255',
'bantuan_lain' => 'required|in:ya,tidak',
'usia' => 'required|integer|min:17|max:100',
'status_perkawinan' => 'required|string',
'lantai_rumah' => 'required|string',
'dinding_rumah' => 'required|string',
'atap_rumah' => 'required|string',
'luas_rumah_m2' => 'required|integer|min:1',
'status_kepemilikan_rumah'=> 'required|string',
'meteran_listrik' => 'required|in:450VA,900VA,1300VA+',
'sumber_air' => 'required|in:PDAM,Sumur,Sungai',
'foto_rumah_depan' => 'nullable|image',
'foto_rumah_belakang' => 'nullable|image',
'foto_rumah_kanan' => 'nullable|image',
'foto_rumah_kiri' => 'nullable|image',
'foto_kk' => 'nullable|image',
'foto_ktp' => 'nullable|image',
'foto_rekening_listrik' => 'nullable|mimes:jpg,jpeg,png,pdf',
'foto_meteran_air' => 'nullable|image',
'dokumen_pendukung' => 'nullable|mimes:jpg,jpeg,png,pdf,doc,docx',
], [
'nik.unique' => 'NIK sudah pernah diinput.',
'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
$validated['aset_kepemilikan'] = $this->buildAsetString(
$validated['aset_pilihan'] ?? [],
$validated['aset_manual'] ?? ''
);
$validated['kondisi_rumah'] = $this->deriveKondisiRumah(
$validated['lantai_rumah'],
$validated['dinding_rumah']
);
$rt = RT::with('dusun')->find($myRtId);
if ($rt && $rt->dusun) {
$validated['desa'] = $rt->dusun->nama_dusun;
@ -234,33 +321,32 @@ public function update(Request $request, CalonPenerima $calonPenerima)
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'],
'jumlah_tanggungan' => $validated['jumlah_tanggungan'],
'aset_kepemilikan' => $validated['aset_kepemilikan'],
'bantuan_lain' => $validated['bantuan_lain'],
'usia' => $validated['usia'],
$fotoImages = [
'foto_rumah_depan', 'foto_rumah_belakang', 'foto_rumah_kanan', 'foto_rumah_kiri',
'foto_kk', 'foto_ktp', 'foto_meteran_air',
];
$fotoFiles = [
'foto_rekening_listrik', 'dokumen_pendukung',
];
$prediction = $this->mlService->getPrediction($predictionData);
if ($prediction) {
PrediksiKelayakan::updateOrCreate(
['calon_penerima_id' => $calonPenerima->id],
[
'probability' => $prediction['probability'],
'recommendation' => $prediction['recommendation'],
]
);
foreach ($fotoImages as $field) {
if ($request->hasFile($field)) {
if ($calonPenerima->$field) {
Storage::disk('public')->delete($calonPenerima->$field);
}
$validated[$field] = $this->simpanFotoKompres($request->file($field));
}
}
*/
foreach ($fotoFiles as $field) {
if ($request->hasFile($field)) {
if ($calonPenerima->$field) {
Storage::disk('public')->delete($calonPenerima->$field);
}
$validated[$field] = $request->file($field)->store('dokumen-warga', 'public');
}
}
$calonPenerima->update($validated);
DB::commit();
@ -268,10 +354,7 @@ public function update(Request $request, CalonPenerima $calonPenerima)
->with('success', 'Data calon penerima berhasil diupdate!');
} catch (\Exception $e) {
DB::rollBack();
return back()
->withInput()
->with('error', 'Terjadi kesalahan: ' . $e->getMessage());
return back()->withInput()->with('error', 'Terjadi kesalahan: ' . $e->getMessage());
}
}
@ -279,7 +362,7 @@ public function ajukan(CalonPenerima $calonPenerima)
{
$user = Auth::user();
if ($calonPenerima->user_id !== $user->id) {
if ((int) $calonPenerima->user_id !== (int) $user->id) {
abort(403, 'Unauthorized action.');
}
@ -288,9 +371,7 @@ public function ajukan(CalonPenerima $calonPenerima)
->with('error', 'Data ini sudah diajukan dan tidak dapat diajukan lagi.');
}
$calonPenerima->update([
'tracking_status' => 'terkirim',
]);
$calonPenerima->update(['tracking_status' => 'terkirim']);
return redirect()->route('rt.calon-penerima.index')
->with('success', 'Data berhasil diajukan ke admin kelurahan.');
@ -300,32 +381,89 @@ public function destroy(CalonPenerima $calonPenerima)
{
$user = Auth::user();
// 🔒 RT hanya bisa hapus kalau miliknya + masih draft
if ($calonPenerima->user_id !== $user->id || ($calonPenerima->tracking_status ?? 'draft') !== 'draft') {
if ((int) $calonPenerima->user_id !== (int) $user->id || ($calonPenerima->tracking_status ?? 'draft') !== 'draft') {
abort(403, 'Data yang sudah diajukan tidak dapat dihapus.');
}
$fotoFields = [
'foto_rumah_depan', 'foto_rumah_belakang', 'foto_rumah_kanan', 'foto_rumah_kiri',
'foto_kk', 'foto_ktp', 'foto_rekening_listrik', 'foto_meteran_air', 'dokumen_pendukung',
];
foreach ($fotoFields as $field) {
if ($calonPenerima->$field) {
Storage::disk('public')->delete($calonPenerima->$field);
}
}
$calonPenerima->delete();
return redirect()->route('rt.calon-penerima.index')
->with('success', 'Data calon penerima berhasil dihapus!');
}
// ── PRIVATE HELPERS ───────────────────────────────────────────────────────
private function simpanFotoKompres(\Illuminate\Http\UploadedFile $file): string
{
return $file->store('dokumen-warga', 'public');
}
private function buildAsetString(array $pilihan, string $manual): string
{
$items = array_filter(array_map('trim', $pilihan));
if ($manual !== '') {
foreach (explode(',', $manual) as $item) {
$item = trim($item);
if ($item !== '') $items[] = $item;
}
}
$items = array_unique($items);
return count($items) > 0 ? implode(', ', $items) : 'Tidak Punya';
}
private function deriveKondisiRumah(string $lantai, string $dinding): string
{
$skorLantai = match($lantai) {
'Tanah' => 3,
'Papan/Kayu' => 2,
'Semen Kasar' => 2,
'Keramik Murah' => 1,
default => 0,
};
$skorDinding = match($dinding) {
'Bambu' => 3,
'Anyaman Bambu/Gedek' => 3,
'Kayu/Papan' => 2,
'Tembok Tidak Plester' => 1,
default => 0,
};
$total = $skorLantai + $skorDinding;
if ($total >= 4) return 'Tidak Layak';
if ($total >= 1) return 'Sedang';
return 'Layak';
}
private function getPredictionExplanation($calonPenerima): array
{
$positive = [];
$negative = [];
// PEKERJAAN
if (strtolower($calonPenerima->pekerjaan) === 'tidak bekerja') {
$positive[] = 'Tidak bekerja';
} elseif (in_array(strtolower($calonPenerima->pekerjaan), ['buruh harian', 'buruh', 'petani', 'nelayan'])) {
} elseif (in_array(strtolower($calonPenerima->pekerjaan), [
'buruh harian lepas', 'buruh harian', 'buruh',
'petani kecil', 'petani', 'mengurus rumah tangga',
])) {
$positive[] = 'Pekerjaan tergolong rentan secara ekonomi';
} else {
$negative[] = 'Memiliki pekerjaan yang relatif lebih stabil';
}
// PENGHASILAN
if ($calonPenerima->penghasilan <= 1000000) {
$positive[] = 'Penghasilan rendah';
} elseif ($calonPenerima->penghasilan <= 2000000) {
@ -334,7 +472,6 @@ private function getPredictionExplanation($calonPenerima): array
$negative[] = 'Penghasilan relatif lebih tinggi';
}
// JUMLAH TANGGUNGAN
if ($calonPenerima->jumlah_tanggungan >= 4) {
$positive[] = 'Jumlah tanggungan banyak';
} elseif ($calonPenerima->jumlah_tanggungan >= 2) {
@ -343,45 +480,82 @@ private function getPredictionExplanation($calonPenerima): array
$negative[] = 'Jumlah tanggungan sedikit';
}
// ASET KEPEMILIKAN
$aset = strtolower($calonPenerima->aset_kepemilikan);
$aset = strtolower($calonPenerima->aset_kepemilikan ?? '');
if (str_contains($aset, 'aset lengkap')) $negative[] = 'Memiliki aset bernilai tinggi';
elseif (str_contains($aset, 'motor + tanah')) $negative[] = 'Memiliki motor dan tanah';
elseif (str_contains($aset, 'motor + emas')) $negative[] = 'Memiliki motor dan emas';
elseif (str_contains($aset, 'motor')) $negative[] = 'Memiliki kendaraan bermotor';
elseif (str_contains($aset, 'tidak punya')) $positive[] = 'Tidak memiliki aset berarti';
if (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
$lantai = $calonPenerima->lantai_rumah ?? '';
match($lantai) {
'Tanah' => $positive[] = 'Lantai rumah masih tanah',
'Papan/Kayu' => $positive[] = 'Lantai rumah papan/kayu',
'Semen Kasar' => $positive[] = 'Lantai rumah semen kasar',
'Keramik Murah' => $positive[] = 'Lantai rumah keramik murah',
'Keramik' => $negative[] = 'Lantai rumah sudah keramik',
default => null,
};
$dinding = $calonPenerima->dinding_rumah ?? '';
match($dinding) {
'Bambu' => $positive[] = 'Dinding rumah bambu',
'Anyaman Bambu/Gedek' => $positive[] = 'Dinding rumah anyaman bambu/gedek',
'Kayu/Papan' => $positive[] = 'Dinding rumah kayu/papan',
'Tembok Tidak Plester' => $positive[] = 'Dinding rumah tembok belum diplester',
'Tembok Plester' => $negative[] = 'Dinding rumah tembok plester',
default => null,
};
$atap = $calonPenerima->atap_rumah ?? '';
match($atap) {
'Bambu/Jerami' => $positive[] = 'Atap rumah bambu/jerami',
'Seng' => $positive[] = 'Atap rumah seng',
'Campuran Seng+Genteng' => $positive[] = 'Atap rumah campuran seng dan genteng',
'Genteng' => $negative[] = 'Atap rumah sudah genteng',
'Genteng Beton' => $negative[] = 'Atap rumah genteng beton',
default => null,
};
$statusRumah = $calonPenerima->status_kepemilikan_rumah ?? '';
match($statusRumah) {
'Menumpang' => $positive[] = 'Status rumah menumpang',
'Sewa/Kontrak' => $positive[] = 'Status rumah sewa/kontrak',
'Milik Sendiri'=> $negative[] = 'Memiliki rumah sendiri',
default => null,
};
match($calonPenerima->meteran_listrik ?? '') {
'450VA' => $positive[] = 'Meteran listrik 450 VA',
'900VA' => $positive[] = 'Meteran listrik 900 VA',
'1300VA+' => $negative[] = 'Meteran listrik 1300 VA ke atas',
default => null,
};
match(strtolower($calonPenerima->sumber_air ?? '')) {
'sungai' => $positive[] = 'Sumber air dari sungai',
'sumur' => $positive[] = 'Sumber air dari sumur',
'pdam' => $negative[] = 'Sumber air PDAM',
default => null,
};
$probability = $calonPenerima->prediksiKelayakan->probability ?? 0;
$probability = $probability <= 1 ? $probability * 100 : $probability;
$probability = number_format($probability, 1);
$summary = "Nilai kelayakan {$probability}% diperoleh berdasarkan data pekerjaan, penghasilan, jumlah tanggungan, aset kepemilikan, bantuan lain, dan usia.";
$summary = "Nilai kelayakan {$probability}% diperoleh berdasarkan data pekerjaan, penghasilan, jumlah tanggungan, aset kepemilikan, bantuan lain, usia, kondisi rumah, meteran listrik, dan sumber air.";
if (count($positive) > 0 && count($negative) > 0) {
$summary .= " Sistem menilai terdapat beberapa faktor yang mendukung kelayakan, namun ada juga faktor yang mengurangi nilai kelayakan.";
@ -394,7 +568,7 @@ private function getPredictionExplanation($calonPenerima): array
return [
'positive' => $positive,
'negative' => $negative,
'summary' => $summary,
'summary' => $summary,
];
}
}

View File

@ -4,7 +4,7 @@
use App\Http\Controllers\Controller;
use App\Models\PenerimaFinal;
use App\Models\Rt;
use App\Models\RT;
use Illuminate\Http\Request;
class LaporanController extends Controller
@ -17,33 +17,29 @@ public function index(Request $request)
abort(403, 'Akses ditolak: hanya RT.');
}
$rt = Rt::with('dusun')->find($user->rt_id);
$rt = RT::with('dusun')->find($user->rt_id);
if (!$rt) {
$laporans = collect();
$stats = [
'total' => 0,
'total' => 0,
'diterima' => 0,
'ditolak' => 0,
'ditolak' => 0,
];
return view('rt.laporan', compact('laporans', 'stats'));
}
$nomorRt = $rt->nomor_rt;
$namaDusun = $rt->dusun->nama_dusun ?? null;
$laporans = PenerimaFinal::query()
->where('nomor_rt', $nomorRt)
->where('nama_dusun', $namaDusun)
->where('rt_id', $rt->id)
->whereIn('status_verifikasi', ['disetujui', 'ditolak'])
->orderByDesc('tanggal_penetapan')
->get();
$stats = [
'total' => $laporans->count(),
'total' => $laporans->count(),
'diterima' => $laporans->where('status_verifikasi', 'disetujui')->count(),
'ditolak' => $laporans->where('status_verifikasi', 'ditolak')->count(),
'ditolak' => $laporans->where('status_verifikasi', 'ditolak')->count(),
];
return view('rt.laporan', compact('laporans', 'stats'));

View File

@ -0,0 +1,60 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;
class AutoArsipMiddleware
{
public function handle(Request $request, Closure $next)
{
$this->jalankanAutoArsip();
return $next($request);
}
private function jalankanAutoArsip(): void
{
$sekarang = Carbon::now('Asia/Jakarta');
// ── 1. AUTO ARSIP ──────────────────────────────────────────────
// Tiap tanggal 1, arsipkan semua data yang periode_bulan-nya
// sudah bulan lalu (atau lebih lama) dan belum diarsip
if ($sekarang->day === 1) {
$batasBulan = $sekarang->copy()->startOfMonth(); // awal bulan ini
DB::table('calon_penerimas')
->whereIn('tracking_status', ['terkirim', 'sedang_validasi', 'selesai'])
->whereNull('arsip_tahun')
->whereNotNull('periode_bulan')
->where('periode_bulan', '<', $batasBulan->toDateString())
->update([
'arsip_tahun' => $batasBulan->copy()->subMonth()->year,
'updated_at' => now(),
]);
}
// ── 2. AUTO HAPUS ARSIP ────────────────────────────────────────
// Tiap tanggal 1 Januari, hapus permanen arsip tahun lalu
if ($sekarang->day === 1 && $sekarang->month === 1) {
$tahunHapus = $sekarang->year - 1;
// Hapus prediksi terkait dulu (foreign key)
$idArsip = DB::table('calon_penerimas')
->where('arsip_tahun', $tahunHapus)
->pluck('id');
if ($idArsip->isNotEmpty()) {
DB::table('prediksi_kelayakans')
->whereIn('calon_penerima_id', $idArsip)
->delete();
DB::table('calon_penerimas')
->where('arsip_tahun', $tahunHapus)
->delete();
}
}
}
}

View File

@ -24,16 +24,35 @@ class CalonPenerima extends Model
'penghasilan',
'jumlah_tanggungan',
'aset_kepemilikan',
'kondisi_rumah',
'lantai_rumah',
'dinding_rumah',
'atap_rumah',
'luas_rumah_m2',
'status_kepemilikan_rumah',
'meteran_listrik',
'sumber_air',
'bantuan_lain',
'usia',
'status_perkawinan',
'status_verifikasi',
'tracking_status', // status tracking proses
'tracking_status',
'catatan_admin',
'arsip_tahun',
'periode_bulan',
'foto_rumah_depan',
'foto_rumah_belakang',
'foto_rumah_kanan',
'foto_rumah_kiri',
'foto_kk',
'foto_ktp',
'foto_rekening_listrik',
'foto_meteran_air',
'dokumen_pendukung',
];
protected $casts = [
'penghasilan' => 'decimal:2',
'penghasilan' => 'decimal:2',
'tanggal_lahir' => 'date',
];

View File

@ -20,6 +20,9 @@ class PenerimaFinal extends Model
'penghasilan',
'jumlah_tanggungan',
'aset_kepemilikan',
'kondisi_rumah',
'meteran_listrik',
'sumber_air',
'bantuan_lain',
'usia',
'probability',
@ -31,10 +34,10 @@ class PenerimaFinal extends Model
protected $casts = [
'tanggal_penetapan' => 'date',
'jumlah_bantuan' => 'decimal:2',
'probability' => 'decimal:4',
'penghasilan' => 'integer',
'jumlah_bantuan' => 'decimal:2',
'probability' => 'decimal:4',
'penghasilan' => 'integer',
'jumlah_tanggungan' => 'integer',
'usia' => 'integer',
'usia' => 'integer',
];
}

View File

@ -0,0 +1,12 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class PenerimaFinalArsip extends Model
{
protected $table = 'penerima_final_arsip';
protected $guarded = [];
protected $casts = ['tanggal_penetapan' => 'date'];
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class PeriodeDanaBlt extends Model
{
protected $table = 'periode_dana_blt';
protected $fillable = [
'periode',
'dana_awal',
'jumlah_penerima',
'dana_terpakai',
'dana_sisa',
'keterangan',
];
protected $casts = [
'dana_awal' => 'integer',
'jumlah_penerima' => 'integer',
'dana_terpakai' => 'integer',
'dana_sisa' => 'integer',
];
}

View File

@ -0,0 +1,39 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class RiwayatPenerima extends Model
{
protected $table = 'riwayat_penerima';
protected $fillable = [
'periode',
'nama_lengkap',
'nik',
'no_kk',
'rt_id',
'nomor_rt',
'nama_dusun',
'pekerjaan',
'penghasilan',
'jumlah_tanggungan',
'aset_kepemilikan',
'kondisi_rumah',
'meteran_listrik',
'sumber_air',
'bantuan_lain',
'usia',
'probability',
'status_verifikasi',
'tanggal_penetapan',
'jumlah_bantuan',
'di_arsipkan_pada',
];
protected $casts = [
'tanggal_penetapan' => 'date',
'di_arsipkan_pada' => 'datetime',
];
}

View File

@ -3,80 +3,49 @@
namespace App\Services;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Http;
class MLPredictionService
{
/**
* Dapatkan prediksi dari script Python langsung
*/
public function getPrediction(array $data)
{
try {
$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),
'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),
'kondisi_rumah' => $data['kondisi_rumah'] ?? 'Tidak Diketahui',
'meteran_listrik' => $data['meteran_listrik'] ?? 'Tidak Diketahui',
'sumber_air' => $data['sumber_air'] ?? 'Tidak Diketahui',
];
$jsonPayload = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$apiUrl = env('ML_API_URL', 'https://ianvv.pythonanywhere.com') . '/predict';
$pythonPath = 'python';
$scriptPath = base_path('ml/predict.py');
$response = Http::timeout(30)->post($apiUrl, $payload);
$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;
}
fwrite($pipes[0], $jsonPayload);
fclose($pipes[0]);
$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,
if (!$response->successful()) {
Log::error('ML API gagal.', [
'status' => $response->status(),
'body' => $response->body(),
]);
return null;
}
$result = json_decode($output, true);
$result = $response->json();
if (!$result || isset($result['error'])) {
Log::error('Hasil prediksi ML tidak valid.', [
'output' => $output,
'stderr' => $errorOutput,
'output' => $response->body(),
'decoded' => $result,
]);
return null;
}
return [
'probability' => $result['probability'] ?? 0,
'probability' => $result['probability'] ?? 0,
'recommendation' => $result['recommendation'] ?? 'Tidak Layak',
];
} catch (\Throwable $e) {
@ -85,60 +54,30 @@ public function getPrediction(array $data)
}
}
/**
* Cek apakah script ML bisa dijalankan
*/
public function healthCheck()
{
try {
$pythonPath = 'python';
$scriptPath = base_path('ml/predict.py');
$apiUrl = env('ML_API_URL', 'https://ianvv.pythonanywhere.com') . '/predict';
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'],
$testPayload = [
'pekerjaan' => 'Buruh Harian',
'penghasilan' => 500000,
'jumlah_tanggungan' => 4,
'aset_kepemilikan' => 'Rumah Sederhana',
'bantuan_lain' => 'tidak',
'usia' => 50,
'kondisi_rumah' => 'Tidak Layak',
'meteran_listrik' => '450VA',
'sumber_air' => 'Sumur',
];
$process = proc_open(
$pythonPath . ' ' . escapeshellarg($scriptPath),
$descriptorspec,
$pipes,
base_path()
);
$response = Http::timeout(10)->post($apiUrl, $testPayload);
if (!is_resource($process)) {
if (!$response->successful()) {
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);
$result = $response->json();
return is_array($result) && isset($result['probability']) && isset($result['recommendation']);
} catch (\Throwable $e) {
@ -146,17 +85,12 @@ public function healthCheck()
}
}
/**
* Prediksi untuk banyak data sekaligus
*/
public function getBatchPredictions(array $dataArray)
{
$results = [];
foreach ($dataArray as $item) {
$results[] = $this->getPrediction($item);
}
return $results;
}
}

View File

@ -16,6 +16,10 @@
'is_active' => \App\Http\Middleware\EnsureUserIsActive::class,
]);
$middleware->web(append: [
\App\Http\Middleware\AutoArsipMiddleware::class,
]);
})
->withExceptions(function (Exceptions $exceptions): void {
//

View File

@ -8,6 +8,7 @@
"require": {
"php": "^8.2",
"barryvdh/laravel-dompdf": "^3.1",
"intervention/image": "^3.11",
"laravel/framework": "^12.0",
"laravel/tinker": "^2.10.1",
"maatwebsite/excel": "*"
@ -77,6 +78,9 @@
}
},
"config": {
"platform": {
"php": "8.2.0"
},
"optimize-autoloader": true,
"preferred-install": "dist",
"sort-packages": true,

807
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -45,7 +45,7 @@
'mysql' => [
'driver' => 'mysql',
'url' => env('DATABASE_URL'),
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'laravel'),

View File

@ -0,0 +1,32 @@
<?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->string('lantai_rumah')->nullable()->after('kondisi_rumah');
$table->string('dinding_rumah')->nullable()->after('lantai_rumah');
$table->string('atap_rumah')->nullable()->after('dinding_rumah');
$table->integer('luas_rumah_m2')->nullable()->after('atap_rumah');
$table->string('status_kepemilikan_rumah')->nullable()->after('luas_rumah_m2');
});
}
public function down(): void
{
Schema::table('calon_penerimas', function (Blueprint $table) {
$table->dropColumn([
'lantai_rumah',
'dinding_rumah',
'atap_rumah',
'luas_rumah_m2',
'status_kepemilikan_rumah',
]);
});
}
};

View File

@ -6,37 +6,44 @@
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
public function up(): void
{
Schema::create('calon_penerimas', function (Blueprint $table) {
$table->id(); // NO. URUT
$table->id();
$table->foreignId('user_id')->constrained('users')->onDelete('cascade');
$table->foreignId('rt_id')->constrained('rts')->onDelete('cascade');
// Data Sesuai Kebutuhan
$table->string('no_kk', 16); // NO. KK
$table->string('nik', 16)->unique(); // NO. NIK KTP
$table->string('nama_lengkap'); // NAMA LENGKAP
$table->enum('jenis_kelamin', ['Laki-laki', 'Perempuan']); // JENIS KELAMIN
$table->string('tempat_lahir'); // TEMPAT LAHIR
$table->date('tanggal_lahir'); // TANGGAL LAHIR
$table->text('alamat'); // ALAMAT
$table->string('desa'); // DESA
$table->string('pekerjaan'); // PEKERJAAN
$table->decimal('penghasilan', 15, 2); // PENGHASILAN
$table->integer('jumlah_tanggungan'); // JUMLAH TANGGUNGAN
$table->string('aset_kepemilikan'); // ASET KEPEMILIKAN
$table->enum('bantuan_lain', ['ya', 'tidak']); // BANTUAN LAIN
$table->integer('usia'); // USIA
$table->string('status_perkawinan'); // STATUS PERKAWINAN
// Untuk sistem (verifikasi admin)
$table->string('no_kk', 16);
$table->string('nik', 16)->unique();
$table->string('nama_lengkap');
$table->enum('jenis_kelamin', ['Laki-laki', 'Perempuan']);
$table->string('tempat_lahir');
$table->date('tanggal_lahir');
$table->text('alamat');
$table->string('desa');
$table->string('pekerjaan');
$table->decimal('penghasilan', 15, 2);
$table->integer('jumlah_tanggungan');
$table->string('aset_kepemilikan');
$table->string('kondisi_rumah')->default('Tidak Diketahui');
$table->string('meteran_listrik')->default('Tidak Diketahui');
$table->string('sumber_air')->default('Tidak Diketahui');
$table->enum('bantuan_lain', ['ya', 'tidak']);
$table->integer('usia');
$table->string('status_perkawinan');
$table->enum('status_verifikasi', ['pending', 'disetujui', 'ditolak'])->default('pending');
$table->text('catatan_admin')->nullable();
$table->string('foto_rumah_depan')->nullable();
$table->string('foto_rumah_belakang')->nullable();
$table->string('foto_rumah_kanan')->nullable();
$table->string('foto_rumah_kiri')->nullable();
$table->string('foto_kk')->nullable();
$table->string('foto_ktp')->nullable();
$table->string('foto_rekening_listrik')->nullable();
$table->string('foto_meteran_air')->nullable();
$table->string('dokumen_pendukung')->nullable();
$table->timestamps();
});
}
@ -45,4 +52,4 @@ public function down(): void
{
Schema::dropIfExists('calon_penerimas');
}
};
};

View File

@ -6,16 +6,16 @@
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
public function up(): void
{
Schema::create('prediksi_kelayakans', function (Blueprint $table) {
$table->id();
$table->foreignId('calon_penerima_id')->constrained('calon_penerimas')->onDelete('cascade');
$table->decimal('probability', 5, 2); // Probabilitas kelayakan (0-100)
$table->string('recommendation'); // Sangat Layak, Layak, Kurang Layak
$table->decimal('probability', 5, 2);
$table->string('recommendation');
$table->string('kondisi_rumah')->default('Tidak Diketahui');
$table->string('meteran_listrik')->default('Tidak Diketahui');
$table->string('sumber_air')->default('Tidak Diketahui');
$table->timestamps();
});
}
@ -24,4 +24,4 @@ public function down(): void
{
Schema::dropIfExists('prediksi_kelayakans');
}
};
};

View File

@ -0,0 +1,22 @@
<?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::create('password_reset_tokens', function (Blueprint $table) {
$table->string('email')->primary();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
}
public function down(): void
{
Schema::dropIfExists('password_reset_tokens');
}
};

View File

@ -0,0 +1,40 @@
<?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::create('penerima_final_arsip', function (Blueprint $table) {
$table->id();
$table->string('periode_arsip');
$table->unsignedBigInteger('penerima_final_id')->nullable();
$table->string('nama_lengkap')->nullable();
$table->string('nik')->nullable();
$table->string('no_kk')->nullable();
$table->string('nomor_rt')->nullable();
$table->string('nama_dusun')->nullable();
$table->string('pekerjaan')->nullable();
$table->bigInteger('penghasilan')->default(0);
$table->integer('jumlah_tanggungan')->default(0);
$table->string('aset_kepemilikan')->nullable();
$table->string('bantuan_lain')->nullable();
$table->integer('usia')->nullable();
$table->float('probability')->nullable();
$table->string('status_verifikasi')->nullable();
$table->date('tanggal_penetapan')->nullable();
$table->string('periode_bantuan')->nullable();
$table->bigInteger('jumlah_bantuan')->default(300000);
$table->timestamp('diarsipkan_pada')->useCurrent();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('penerima_final_arsip');
}
};

View File

@ -0,0 +1,42 @@
<?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::create('riwayat_penerima', function (Blueprint $table) {
$table->id();
$table->string('periode'); // contoh: "2026-04"
$table->string('nama_lengkap');
$table->string('nik', 16);
$table->string('no_kk', 16)->nullable();
$table->unsignedBigInteger('rt_id')->nullable();
$table->string('nomor_rt')->nullable();
$table->string('nama_dusun')->nullable();
$table->string('pekerjaan')->nullable();
$table->decimal('penghasilan', 15, 2)->default(0);
$table->integer('jumlah_tanggungan')->default(0);
$table->string('aset_kepemilikan')->nullable();
$table->string('kondisi_rumah')->nullable();
$table->string('meteran_listrik')->nullable();
$table->string('sumber_air')->nullable();
$table->string('bantuan_lain')->default('tidak');
$table->integer('usia')->default(0);
$table->decimal('probability', 8, 4)->default(0);
$table->string('status_verifikasi')->default('disetujui');
$table->date('tanggal_penetapan')->nullable();
$table->decimal('jumlah_bantuan', 15, 2)->default(0);
$table->timestamp('di_arsipkan_pada')->nullable(); // kapan di-reset/arsip
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('riwayat_penerima');
}
};

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('wargas', function (Blueprint $table) {
//
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('wargas', function (Blueprint $table) {
$table->year('arsip_tahun')->nullable()->after('tracking_status');
});
}
};

21
error_log Normal file
View File

@ -0,0 +1,21 @@
[08-Apr-2026 16:30:23 UTC] PHP Fatal error: Composer detected issues in your platform: Your Composer dependencies require a PHP version ">= 8.3.0". You are running 8.2.30. in /home/tamyhost/public_html/E31232406/vendor/composer/platform_check.php on line 26
[08-Apr-2026 17:03:37 UTC] PHP Fatal error: Composer detected issues in your platform: Your Composer dependencies require a PHP version ">= 8.3.0". You are running 8.2.30. in /home/tamyhost/public_html/E31232406/vendor/composer/platform_check.php on line 26
[08-Apr-2026 17:06:51 UTC] PHP Fatal error: Composer detected issues in your platform: Your Composer dependencies require a PHP version ">= 8.3.0". You are running 8.2.30. in /home/tamyhost/public_html/E31232406/vendor/composer/platform_check.php on line 26
[08-Apr-2026 17:42:39 UTC] PHP Warning: require(/home/tamyhost/public_html/E31232406/../vendor/autoload.php): Failed to open stream: No such file or directory in /home/tamyhost/public_html/E31232406/index.php on line 14
[08-Apr-2026 17:42:39 UTC] PHP Warning: require(/home/tamyhost/public_html/E31232406/../vendor/autoload.php): Failed to open stream: No such file or directory in /home/tamyhost/public_html/E31232406/index.php on line 14
[08-Apr-2026 17:42:39 UTC] PHP Fatal error: Uncaught Error: Failed opening required '/home/tamyhost/public_html/E31232406/../vendor/autoload.php' (include_path='.:/opt/cpanel/ea-php82/root/usr/share/pear') in /home/tamyhost/public_html/E31232406/index.php:14
Stack trace:
#0 {main}
thrown in /home/tamyhost/public_html/E31232406/index.php on line 14
[08-Apr-2026 17:42:42 UTC] PHP Warning: require(/home/tamyhost/public_html/E31232406/../vendor/autoload.php): Failed to open stream: No such file or directory in /home/tamyhost/public_html/E31232406/index.php on line 14
[08-Apr-2026 17:42:42 UTC] PHP Warning: require(/home/tamyhost/public_html/E31232406/../vendor/autoload.php): Failed to open stream: No such file or directory in /home/tamyhost/public_html/E31232406/index.php on line 14
[08-Apr-2026 17:42:42 UTC] PHP Fatal error: Uncaught Error: Failed opening required '/home/tamyhost/public_html/E31232406/../vendor/autoload.php' (include_path='.:/opt/cpanel/ea-php82/root/usr/share/pear') in /home/tamyhost/public_html/E31232406/index.php:14
Stack trace:
#0 {main}
thrown in /home/tamyhost/public_html/E31232406/index.php on line 14
[08-Apr-2026 17:42:43 UTC] PHP Warning: require(/home/tamyhost/public_html/E31232406/../vendor/autoload.php): Failed to open stream: No such file or directory in /home/tamyhost/public_html/E31232406/index.php on line 14
[08-Apr-2026 17:42:43 UTC] PHP Warning: require(/home/tamyhost/public_html/E31232406/../vendor/autoload.php): Failed to open stream: No such file or directory in /home/tamyhost/public_html/E31232406/index.php on line 14
[08-Apr-2026 17:42:43 UTC] PHP Fatal error: Uncaught Error: Failed opening required '/home/tamyhost/public_html/E31232406/../vendor/autoload.php' (include_path='.:/opt/cpanel/ea-php82/root/usr/share/pear') in /home/tamyhost/public_html/E31232406/index.php:14
Stack trace:
#0 {main}
thrown in /home/tamyhost/public_html/E31232406/index.php on line 14

View File

@ -2,15 +2,19 @@ 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))
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))
kondisi_rumah = str(data.get("kondisi_rumah", "")).lower()
meteran_listrik= str(data.get("meteran_listrik", "")).lower()
sumber_air = str(data.get("sumber_air", "")).lower()
score = 0
# Parameter lama (tidak diubah)
if pekerjaan in ["buruh", "tidak bekerja", "petani", "nelayan"]:
score += 0.2
@ -31,22 +35,32 @@ def predict(data):
if usia >= 60:
score += 0.1
probability = max(0, min(score,1))
# Parameter baru
if kondisi_rumah == "tidak layak":
score += 0.15
elif kondisi_rumah == "sedang":
score += 0.05
if meteran_listrik == "450va":
score += 0.1
elif meteran_listrik == "900va":
score += 0.05
if sumber_air == "sungai":
score += 0.1
elif sumber_air == "sumur":
score += 0.05
probability = max(0, min(score, 1))
recommendation = "Layak" if probability >= 0.5 else "Tidak Layak"
return {
"probability": round(probability,4),
"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:
@ -54,7 +68,6 @@ def read_input():
except:
pass
# Fallback ke argv
if len(sys.argv) > 1:
raw = sys.argv[1]
return json.loads(raw)
@ -63,7 +76,7 @@ def read_input():
if __name__ == "__main__":
try:
data = read_input()
data = read_input()
result = predict(data)
print(json.dumps(result))
except Exception as e:

View File

@ -18,4 +18,4 @@
"tailwindcss": "^3.1.0",
"vite": "^7.0.7"
}
}
}

View File

@ -23,3 +23,6 @@
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [L]
</IfModule>
php_value upload_max_filesize 20M
php_value post_max_size 25M

BIN
public/asset/galeri1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

BIN
public/asset/galeri2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

BIN
public/asset/galeri3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

BIN
public/asset/galeri4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

BIN
public/login-gedung.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

View File

@ -2,38 +2,36 @@
<x-slot name="header">
<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;">
<div>
<h2 style="font-size:17px;font-weight:900;color:#0f172a;line-height:1.2;letter-spacing:-.02em;">Dashboard Kelurahan</h2>
<p style="font-size:11px;color:#94a3b8;margin-top:2px;font-weight:500;">Ringkasan data kelayakan penerima BLT-DD seluruh dusun</p>
<h2 style="font-size:20px;font-weight:900;color:#0f172a;letter-spacing:-.02em;">Dashboard Admin Perangkat Desa</h2>
<p style="font-size:14px;color:#64748b;margin-top:4px;font-weight:500;">Ringkasan data kelayakan penerima BLT-DD</p>
</div>
<div style="display:flex;align-items:center;gap:8px;">
{{-- ── BELL NOTIFIKASI ── --}}
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;">
{{-- BELL --}}
<div style="position:relative;" id="notif-wrap">
<button onclick="toggleNotif()"
style="position:relative;width:36px;height:36px;background:#fff;border:1.5px solid #e2e8f0;border-radius:11px;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:background .15s;box-shadow:0 1px 4px rgba(0,0,0,.05);">
<svg width="16" height="16" fill="none" stroke="#64748b" viewBox="0 0 24 24">
style="position:relative;width:44px;height:44px;background:#fff;border:1.5px solid #e2e8f0;border-radius:12px;display:flex;align-items:center;justify-content:center;cursor:pointer;box-shadow:0 1px 4px rgba(0,0,0,.05);">
<svg width="20" height="20" fill="none" stroke="#64748b" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6 6 0 00-9.33-4.976A6 6 0 006 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/>
</svg>
<span id="notif-badge"
style="display:none;position:absolute;top:-5px;right:-5px;min-width:17px;height:17px;border-radius:99px;background:#ef4444;color:#fff;font-size:9px;font-weight:800;align-items:center;justify-content:center;padding:0 3px;border:2px solid #fff;">0</span>
style="display:none;position:absolute;top:-6px;right:-6px;min-width:20px;height:20px;border-radius:99px;background:#ef4444;color:#fff;font-size:11px;font-weight:800;align-items:center;justify-content:center;padding:0 4px;border:2px solid #fff;">0</span>
</button>
<div id="notif-dropdown"
style="display:none;position:absolute;top:calc(100% + 8px);right:0;width:310px;background:#fff;border-radius:18px;border:1.5px solid #f1f5f9;box-shadow:0 20px 56px rgba(0,0,0,.13);z-index:999;overflow:hidden;">
<div style="padding:12px 15px;border-bottom:1px solid #f1f5f9;background:#fafbfc;display:flex;align-items:center;justify-content:space-between;">
<span style="font-size:12px;font-weight:800;color:#0f172a;">Notifikasi</span>
<button onclick="clearNotifs()" style="font-size:10.5px;color:#2563eb;cursor:pointer;font-weight:700;background:none;border:none;">Hapus semua</button>
style="display:none;position:absolute;top:calc(100% + 10px);right:0;width:min(340px,92vw);background:#fff;border-radius:18px;border:1.5px solid #f1f5f9;box-shadow:0 20px 56px rgba(0,0,0,.13);z-index:999;overflow:hidden;">
<div style="padding:14px 18px;border-bottom:1px solid #f1f5f9;background:#fafbfc;display:flex;align-items:center;justify-content:space-between;">
<span style="font-size:15px;font-weight:800;color:#0f172a;">Notifikasi</span>
<button onclick="clearNotifs()" style="font-size:14px;color:#2563eb;cursor:pointer;font-weight:700;background:none;border:none;">Hapus semua</button>
</div>
<div id="notif-list" style="max-height:280px;overflow-y:auto;">
<div style="padding:24px 16px;text-align:center;font-size:12px;color:#94a3b8;font-weight:500;">Tidak ada notifikasi</div>
<div id="notif-list" style="max-height:300px;overflow-y:auto;">
<div style="padding:28px 18px;text-align:center;font-size:14px;color:#94a3b8;">Tidak ada notifikasi</div>
</div>
</div>
</div>
{{-- Clock --}}
<div style="display:inline-flex;align-items:center;gap:6px;background:#fff;border:1.5px solid #e2e8f0;border-radius:11px;padding:7px 12px;font-size:11px;color:#64748b;font-weight:500;box-shadow:0 1px 4px rgba(0,0,0,.04);">
<svg width="13" height="13" fill="none" stroke="#3b82f6" viewBox="0 0 24 24">
{{-- CLOCK --}}
<div id="admin-clock-wrap" style="display:inline-flex;align-items:center;gap:8px;background:#fff;border:1.5px solid #e2e8f0;border-radius:12px;padding:10px 16px;font-size:13px;color:#64748b;font-weight:600;">
<svg width="16" height="16" fill="none" stroke="#3b82f6" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<span id="admin-clock"></span>
@ -43,335 +41,426 @@
</x-slot>
<style>
/* ── Inline style approach — safe from Tailwind purge ── */
.db-card{background:#fff;border-radius:20px;border:1.5px solid #f1f5f9;box-shadow:0 2px 8px rgba(0,0,0,.04),0 0 0 0 transparent;overflow:hidden;}
/* ── SHARED ── */
.db-card{background:#fff;border-radius:18px;border:1.5px solid #f1f5f9;box-shadow:0 2px 8px rgba(0,0,0,.04);overflow:hidden;}
.sc-blob1{position:absolute;top:-16px;right:-16px;width:80px;height:80px;background:rgba(255,255,255,.12);border-radius:50%;pointer-events:none;}
.sc-blob2{position:absolute;bottom:-20px;right:8px;width:56px;height:56px;background:rgba(255,255,255,.09);border-radius:50%;pointer-events:none;}
/* Stat cards */
.sc-blob1{position:absolute;top:-16px;right:-16px;width:80px;height:80px;background:rgba(255,255,255,.12);border-radius:50%;}
.sc-blob2{position:absolute;bottom:-20px;right:8px;width:56px;height:56px;background:rgba(255,255,255,.09);border-radius:50%;}
/* Top table */
/* ── TABLE ── */
.tt table{width:100%;border-collapse:collapse;}
.tt thead tr{background:#f8fafc;border-bottom:1.5px solid #f1f5f9;}
.tt thead th{padding:10px 14px;text-align:left;font-size:9.5px;font-weight:800;color:#94a3b8;text-transform:uppercase;letter-spacing:.08em;white-space:nowrap;}
.tt thead th{padding:13px 14px;text-align:left;font-size:12px;font-weight:800;color:#64748b;white-space:nowrap;}
.tt tbody tr{border-bottom:1px solid #f8fafc;transition:background .1s;}
.tt tbody tr:hover{background:#f0f7ff;}
.tt tbody tr:last-child{border-bottom:none;}
.tt tbody td{padding:10px 14px;vertical-align:middle;}
.tt tbody tr:hover{background:#f8fbff;}
.tt tbody td{padding:13px 14px;font-size:14px;color:#374151;}
/* Rank badges */
.rk{width:28px;height:28px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:800;}
.rk-1{background:#fef9c3;color:#b45309;}
.rk-2{background:#f1f5f9;color:#475569;}
.rk-3{background:#fff7ed;color:#c2410c;}
.rk-n{background:#eff6ff;color:#2563eb;font-size:10px;}
/* notif items */
.ni{display:flex;align-items:flex-start;gap:12px;padding:13px 16px;border-bottom:1px solid #f8fafc;}
.ni.unread{background:#f0f7ff;}
.ni-icon{width:36px;height:36px;border-radius:10px;display:flex;align-items:center;justify-content:center;flex-shrink:0;}
.ni-icon svg{width:17px;height:17px;}
.ni-txt{font-size:14px;font-weight:600;color:#1e293b;line-height:1.5;}
.ni-time{font-size:12px;color:#94a3b8;margin-top:3px;}
/* Notif item */
.ni{display:flex;align-items:flex-start;gap:10px;padding:11px 15px;border-bottom:1px solid #f8fafc;transition:background .1s;}
.ni.unread{background:#eff6ff;}
.ni-icon{width:28px;height:28px;border-radius:8px;display:flex;align-items:center;justify-content:center;flex-shrink:0;}
.ni-icon svg{width:13px;height:13px;}
.ni-txt{font-size:11.5px;color:#374151;font-weight:500;line-height:1.5;}
.ni-time{font-size:10px;color:#94a3b8;margin-top:2px;}
/* ── RESPONSIVE DESKTOP ── */
@media (max-width: 1024px) {
.adm-stat-grid { grid-template-columns: repeat(2,1fr) !important; }
.adm-chart-grid { grid-template-columns: 1fr !important; }
}
/* ── RESPONSIVE MOBILE ── */
@media (max-width: 768px) {
/* Header */
.adm-header-title { font-size:17px !important; }
.adm-header-sub { font-size:13px !important; }
/* Jam disembunyikan di mobile */
#admin-clock-wrap { display:none !important; }
/* Notif button lebih mudah dipencet */
#notif-wrap button { width:48px !important; height:48px !important; }
/* Stat cards: 2 kolom proporsional */
.adm-stat-grid {
grid-template-columns: repeat(2,1fr) !important;
gap:10px !important;
}
.adm-stat-grid > div {
padding:16px 14px !important;
border-radius:14px !important;
}
.adm-stat-card-num { font-size:30px !important; line-height:1 !important; }
.adm-stat-card-lbl { font-size:13px !important; }
.adm-stat-card-sub { font-size:11px !important; }
.adm-stat-card-icon { width:44px !important; height:44px !important; }
.adm-stat-card-icon svg { width:22px !important; height:22px !important; }
/* Greeting: susun vertikal, full width */
.adm-greeting { flex-direction:column !important; gap:10px !important; }
.adm-greeting > div {
min-width:unset !important;
flex:none !important;
width:100% !important;
}
.adm-greeting-name { font-size:15px !important; }
.adm-greeting-sub { font-size:13px !important; }
.adm-pending-txt { font-size:14px !important; }
.adm-pending-link { font-size:13px !important; }
/* Chart: stack vertikal */
.adm-chart-grid {
grid-template-columns:1fr !important;
gap:12px !important;
}
.adm-section-title { font-size:15px !important; }
.adm-section-badge { font-size:13px !important; }
/* Dusun bar */
.adm-dusun-name { font-size:14px !important; }
.adm-dusun-num { font-size:18px !important; }
.adm-dusun-bar { height:8px !important; }
/* Top 10 table */
.tt thead th {
font-size:13px !important;
padding:12px 14px !important;
white-space:nowrap !important;
}
.tt tbody td {
font-size:13px !important;
padding:12px 14px !important;
}
.adm-tbl-name { font-size:14px !important; }
.adm-tbl-job { font-size:12px !important; }
.adm-tbl-nik { font-size:12px !important; }
.adm-tbl-dusun { font-size:12px !important; }
.adm-tbl-rt { font-size:12px !important; }
.adm-tbl-income { font-size:13px !important; }
.adm-tbl-pct { font-size:13px !important; }
.adm-tbl-badge { font-size:12px !important; padding:6px 12px !important; }
.adm-rank-badge { width:32px !important; height:32px !important; font-size:13px !important; }
.adm-avatar-sm { width:34px !important; height:34px !important; font-size:14px !important; }
/* Tombol Lihat Semua */
.adm-lihat-semua {
font-size:13px !important;
padding:9px 14px !important;
min-height:42px !important;
}
}
@media (max-width: 420px) {
/* HP sangat kecil: stat cards tetap 2 kolom, lebih compact */
.adm-stat-grid { grid-template-columns: repeat(2,1fr) !important; gap:8px !important; }
.adm-stat-card-num { font-size:26px !important; }
.adm-stat-grid > div { padding:14px 12px !important; }
}
</style>
<script>
/* ── Live clock (asli tidak diubah) ── */
function updateAdminClock() {
const now = new Date();
const dateStr = now.toLocaleDateString('id-ID', { timeZone:'Asia/Jakarta', weekday:'long', day:'2-digit', month:'long', year:'numeric' });
const timeStr = now.toLocaleTimeString('id-ID', { timeZone:'Asia/Jakarta', hour:'2-digit', minute:'2-digit', second:'2-digit', hour12:false });
const el = document.getElementById('admin-clock');
if (el) el.textContent = dateStr + ' — ' + timeStr + ' WIB';
/* Clock */
function updateAdminClock(){
const now=new Date();
const d=now.toLocaleDateString('id-ID',{timeZone:'Asia/Jakarta',weekday:'short',day:'2-digit',month:'short',year:'numeric'});
const t=now.toLocaleTimeString('id-ID',{timeZone:'Asia/Jakarta',hour:'2-digit',minute:'2-digit',second:'2-digit',hour12:false});
const el=document.getElementById('admin-clock');
if(el) el.textContent=d+' '+t+' WIB';
}
updateAdminClock();
setInterval(updateAdminClock, 1000);
setInterval(updateAdminClock,1000);
/* ── Notifikasi realtime ── */
const _nk = 'sbd_notifs_v1';
const NS = {
items: (() => { try { return JSON.parse(localStorage.getItem(_nk)||'[]'); } catch(e){ return []; } })(),
save() { try { localStorage.setItem(_nk, JSON.stringify(this.items)); } catch(e){} },
add(icon, color, text) {
this.items.unshift({ id: Date.now(), icon, color, text, time: new Date().toLocaleTimeString('id-ID',{hour:'2-digit',minute:'2-digit'}), read: false });
if (this.items.length > 30) this.items = this.items.slice(0, 30);
this.save(); renderNotifs();
/* Notifikasi */
const _nk='sbd_notifs_v1';
const NS={
items:(()=>{try{return JSON.parse(localStorage.getItem(_nk)||'[]');}catch(e){return [];}}()),
save(){try{localStorage.setItem(_nk,JSON.stringify(this.items));}catch(e){}},
add(icon,color,text){
this.items.unshift({id:Date.now(),icon,color,text,time:new Date().toLocaleTimeString('id-ID',{hour:'2-digit',minute:'2-digit'}),read:false});
if(this.items.length>30)this.items=this.items.slice(0,30);
this.save();renderNotifs();
},
markAllRead() { this.items.forEach(i => i.read = true); this.save(); },
clearAll() { this.items = []; this.save(); },
unread() { return this.items.filter(i => !i.read).length; }
markAllRead(){this.items.forEach(i=>i.read=true);this.save();},
clearAll(){this.items=[];this.save();},
unread(){return this.items.filter(i=>!i.read).length;}
};
function renderNotifs() {
const list = document.getElementById('notif-list');
const badge = document.getElementById('notif-badge');
if (!list) return;
const c = NS.unread();
if (badge) { badge.textContent = c > 9 ? '9+' : c; badge.style.display = c > 0 ? 'flex' : 'none'; }
if (!NS.items.length) {
list.innerHTML = '<div style="padding:24px 16px;text-align:center;font-size:12px;color:#94a3b8;font-weight:500;">Tidak ada notifikasi</div>';
return;
}
list.innerHTML = NS.items.map(n => `
<div class="ni ${n.read?'':'unread'}">
<div class="ni-icon" style="background:${n.color}18;">
<svg fill="none" stroke="${n.color}" viewBox="0 0 24 24">${n.icon}</svg>
</div>
<div><div class="ni-txt">${n.text}</div><div class="ni-time">${n.time}</div></div>
</div>`).join('');
function renderNotifs(){
const list=document.getElementById('notif-list');
const badge=document.getElementById('notif-badge');
if(!list)return;
const c=NS.unread();
if(badge){badge.textContent=c>9?'9+':c;badge.style.display=c>0?'flex':'none';}
if(!NS.items.length){list.innerHTML='<div style="padding:28px 18px;text-align:center;font-size:14px;color:#94a3b8;">Tidak ada notifikasi</div>';return;}
list.innerHTML=NS.items.map(n=>`<div class="ni ${n.read?'':'unread'}"><div class="ni-icon" style="background:${n.color}18;"><svg fill="none" stroke="${n.color}" viewBox="0 0 24 24">${n.icon}</svg></div><div><div class="ni-txt">${n.text}</div><div class="ni-time">${n.time}</div></div></div>`).join('');
}
function toggleNotif() {
const dd = document.getElementById('notif-dropdown');
if (!dd) return;
const opening = dd.style.display === 'none';
dd.style.display = opening ? 'block' : 'none';
if (opening) { NS.markAllRead(); renderNotifs(); }
function toggleNotif(){
const dd=document.getElementById('notif-dropdown');
if(!dd)return;
const opening=dd.style.display==='none';
dd.style.display=opening?'block':'none';
if(opening){NS.markAllRead();renderNotifs();}
}
function clearNotifs() { NS.clearAll(); renderNotifs(); }
function clearNotifs(){NS.clearAll();renderNotifs();}
/* Poll 30s */
let _lw = {{ $totalWarga ?? 0 }}, _lp = {{ $totalPending ?? 0 }}, _ln = {{ $totalPenerima ?? 0 }};
async function pollStats() {
try {
const res = await fetch(window.location.href, { headers:{'X-Requested-With':'XMLHttpRequest','Accept':'application/json'} });
if (!res.ok || !res.headers.get('content-type')?.includes('json')) return;
const d = await res.json();
if ((d.totalWarga??0) > _lw) { NS.add('<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"/>','#2563eb',`${d.totalWarga-_lw} data warga baru masuk ke sistem`); _lw=d.totalWarga; }
if ((d.totalPending??0) > _lp) { NS.add('<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>','#f59e0b',`${d.totalPending-_lp} data baru menunggu verifikasi`); _lp=d.totalPending; }
if ((d.totalPenerima??0) > _ln) { NS.add('<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"/>','#10b981',`${d.totalPenerima-_ln} warga baru ditetapkan sebagai penerima`); _ln=d.totalPenerima; }
} catch(e) {}
let _lw={{$totalWarga??0}},_lp={{$totalPending??0}},_ln={{$totalPenerima??0}};
async function pollStats(){
try{
const res=await fetch(window.location.href,{headers:{'X-Requested-With':'XMLHttpRequest','Accept':'application/json'}});
if(!res.ok||!res.headers.get('content-type')?.includes('json'))return;
const d=await res.json();
if((d.totalWarga??0)>_lw){NS.add('<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"/>','#2563eb',`${d.totalWarga-_lw} data warga baru`);_lw=d.totalWarga;}
if((d.totalPending??0)>_lp){NS.add('<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>','#f59e0b',`${d.totalPending-_lp} data baru menunggu verifikasi`);_lp=d.totalPending;}
if((d.totalPenerima??0)>_ln){NS.add('<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"/>','#10b981',`${d.totalPenerima-_ln} warga baru ditetapkan`);_ln=d.totalPenerima;}
}catch(e){}
}
document.addEventListener('DOMContentLoaded', () => {
document.addEventListener('DOMContentLoaded',()=>{
renderNotifs();
setInterval(pollStats, 30000);
document.addEventListener('click', e => {
const w = document.getElementById('notif-wrap');
if (w && !w.contains(e.target)) { const dd=document.getElementById('notif-dropdown'); if(dd) dd.style.display='none'; }
setInterval(pollStats,30000);
document.addEventListener('click',e=>{
const w=document.getElementById('notif-wrap');
if(w&&!w.contains(e.target)){const dd=document.getElementById('notif-dropdown');if(dd)dd.style.display='none';}
});
});
</script>
<div style="display:flex;flex-direction:column;gap:16px;">
{{-- GREETING + NOTIFIKASI (asli tidak diubah, hanya ganti class ke inline style) --}}
<div style="display:flex;flex-wrap:wrap;gap:10px;">
<div style="display:flex;align-items:center;gap:12px;background:#fff;border:1.5px solid #f1f5f9;border-radius:18px;padding:14px 18px;box-shadow:0 1px 4px rgba(0,0,0,.04);flex:1;min-width:240px;">
<div style="width:38px;height:38px;border-radius:50%;background:linear-gradient(135deg,#3b82f6,#2563eb);display:flex;align-items:center;justify-content:center;color:#fff;font-size:14px;font-weight:800;flex-shrink:0;box-shadow:0 4px 12px rgba(37,99,235,.28);">
{{-- GREETING --}}
<div class="adm-greeting" style="display:flex;flex-wrap:wrap;gap:12px;">
<div style="display:flex;align-items:center;gap:14px;background:#fff;border:1.5px solid #f1f5f9;border-radius:16px;padding:16px 20px;box-shadow:0 1px 4px rgba(0,0,0,.04);flex:1;min-width:240px;">
<div style="width:48px;height:48px;border-radius:50%;background:linear-gradient(135deg,#3b82f6,#2563eb);display:flex;align-items:center;justify-content:center;color:#fff;font-size:18px;font-weight:800;flex-shrink:0;box-shadow:0 4px 12px rgba(37,99,235,.28);">
{{ strtoupper(substr(Auth::user()->name ?? 'A', 0, 1)) }}
</div>
<div>
<p style="font-size:13px;font-weight:700;color:#0f172a;">Selamat datang, <span style="color:#2563eb;">{{ Auth::user()->name ?? 'Admin' }}</span> 👋</p>
<p style="font-size:11px;color:#94a3b8;margin-top:2px;font-weight:500;">Panel admin kelola dan pantau seluruh data warga.</p>
<p class="adm-greeting-name" style="font-size:16px;font-weight:700;color:#0f172a;">Selamat datang, <span style="color:#2563eb;">{{ Auth::user()->name ?? 'Admin' }}</span> 👋</p>
<p class="adm-greeting-sub" style="font-size:13px;color:#64748b;margin-top:4px;font-weight:500;">Panel admin kelola dan pantau seluruh data warga.</p>
</div>
</div>
<div style="display:flex;align-items:center;gap:12px;background:#fffbeb;border:1.5px solid #fde68a;border-radius:18px;padding:14px 18px;box-shadow:0 1px 4px rgba(0,0,0,.04);">
<div style="width:36px;height:36px;border-radius:50%;background:#f59e0b;display:flex;align-items:center;justify-content:center;flex-shrink:0;box-shadow:0 4px 12px rgba(245,158,11,.28);">
<svg width="16" height="16" fill="none" stroke="#fff" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6 6 0 00-9.33-4.976A6 6 0 006 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/>
</svg>
<div style="display:flex;align-items:center;gap:14px;background:#fffbeb;border:1.5px solid #fde68a;border-radius:16px;padding:16px 20px;">
<div style="width:44px;height:44px;border-radius:50%;background:#f59e0b;display:flex;align-items:center;justify-content:center;flex-shrink:0;">
<svg width="20" height="20" fill="none" stroke="#fff" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6 6 0 00-9.33-4.976A6 6 0 006 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/></svg>
</div>
<div>
<p style="font-size:12px;font-weight:700;color:#92400e;">{{ $totalPending ?? 0 }} data menunggu verifikasi</p>
<a href="{{ route('admin.data-warga') }}" style="font-size:11px;color:#b45309;text-decoration:none;font-weight:600;">Proses sekarang </a>
<p class="adm-pending-txt" style="font-size:15px;font-weight:700;color:#92400e;">{{ $totalPending ?? 0 }} data menunggu verifikasi</p>
<a href="{{ route('admin.data-warga') }}" class="adm-pending-link" style="font-size:13px;color:#b45309;text-decoration:none;font-weight:600;">Proses sekarang </a>
</div>
</div>
</div>
{{-- STAT CARDS (asli tidak diubah, ganti ke inline style) --}}
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:12px;">
{{-- STAT CARDS --}}
<div class="adm-stat-grid" style="display:grid;grid-template-columns:repeat(4,1fr);gap:14px;">
<div style="position:relative;background:linear-gradient(135deg,#2563eb,#1d4ed8);color:#fff;border-radius:18px;padding:18px;display:flex;align-items:center;justify-content:space-between;overflow:hidden;box-shadow:0 6px 20px rgba(37,99,235,.3);">
@php
$cards = [
['label'=>'Total Warga','sub'=>'Dari semua dusun','val'=>$totalWarga??0,'grad'=>'#2563eb,#1d4ed8','shadow'=>'rgba(37,99,235,.3)','icon'=>'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','tc'=>'rgba(191,219,254,.9)'],
['label'=>'Pending','sub'=>'Belum diproses','val'=>$totalPending??0,'grad'=>'#f59e0b,#d97706','shadow'=>'rgba(245,158,11,.3)','icon'=>'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z','tc'=>'rgba(254,243,199,.9)'],
['label'=>'Diterima','sub'=>'Sudah ditetapkan','val'=>$totalPenerima??0,'grad'=>'#10b981,#059669','shadow'=>'rgba(16,185,129,.3)','icon'=>'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z','tc'=>'rgba(167,243,208,.9)'],
['label'=>'Ditolak','sub'=>'Tidak memenuhi syarat','val'=>$totalDitolak??0,'grad'=>'#f43f5e,#e11d48','shadow'=>'rgba(244,63,94,.3)','icon'=>'M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z','tc'=>'rgba(254,205,211,.9)'],
];
@endphp
@foreach($cards as $c)
<div style="position:relative;background:linear-gradient(135deg,{{ $c['grad'] }});color:#fff;border-radius:18px;padding:20px 18px;display:flex;align-items:center;justify-content:space-between;overflow:hidden;box-shadow:0 6px 20px {{ $c['shadow'] }};">
<div class="sc-blob1"></div><div class="sc-blob2"></div>
<div style="position:relative;z-index:1;">
<p style="font-size:10.5px;font-weight:600;color:rgba(191,219,254,.9);">Total Warga Masuk</p>
<p style="font-size:32px;font-weight:900;margin-top:2px;line-height:1;letter-spacing:-.04em;">{{ $totalWarga ?? 0 }}</p>
<p style="font-size:10px;color:rgba(191,219,254,.75);margin-top:3px;">Dari semua dusun</p>
<p class="adm-stat-card-lbl" style="font-size:13px;font-weight:700;color:{{ $c['tc'] }};">{{ $c['label'] }}</p>
<p class="adm-stat-card-num" style="font-size:34px;font-weight:900;margin-top:4px;line-height:1;letter-spacing:-.04em;">{{ $c['val'] }}</p>
<p class="adm-stat-card-sub" style="font-size:12px;color:rgba(255,255,255,.7);margin-top:4px;">{{ $c['sub'] }}</p>
</div>
<div style="position:relative;z-index:1;width:44px;height:44px;background:rgba(255,255,255,.18);border-radius:13px;display:flex;align-items:center;justify-content:center;flex-shrink:0;border:1px solid rgba(255,255,255,.2);">
<svg width="22" height="22" fill="none" stroke="#fff" 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>
</div>
<div style="position:relative;background:linear-gradient(135deg,#f59e0b,#d97706);color:#fff;border-radius:18px;padding:18px;display:flex;align-items:center;justify-content:space-between;overflow:hidden;box-shadow:0 6px 20px rgba(245,158,11,.3);">
<div class="sc-blob1"></div><div class="sc-blob2"></div>
<div style="position:relative;z-index:1;">
<p style="font-size:10.5px;font-weight:600;color:rgba(254,243,199,.9);">Pending Verifikasi</p>
<p style="font-size:32px;font-weight:900;margin-top:2px;line-height:1;letter-spacing:-.04em;">{{ $totalPending ?? 0 }}</p>
<p style="font-size:10px;color:rgba(254,243,199,.75);margin-top:3px;">Belum diproses</p>
</div>
<div style="position:relative;z-index:1;width:44px;height:44px;background:rgba(255,255,255,.18);border-radius:13px;display:flex;align-items:center;justify-content:center;flex-shrink:0;border:1px solid rgba(255,255,255,.2);">
<svg width="22" height="22" fill="none" stroke="#fff" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
</div>
</div>
<div style="position:relative;background:linear-gradient(135deg,#10b981,#059669);color:#fff;border-radius:18px;padding:18px;display:flex;align-items:center;justify-content:space-between;overflow:hidden;box-shadow:0 6px 20px rgba(16,185,129,.3);">
<div class="sc-blob1"></div><div class="sc-blob2"></div>
<div style="position:relative;z-index:1;">
<p style="font-size:10.5px;font-weight:600;color:rgba(167,243,208,.9);">Diterima / Ditetapkan</p>
<p style="font-size:32px;font-weight:900;margin-top:2px;line-height:1;letter-spacing:-.04em;">{{ $totalPenerima ?? 0 }}</p>
<p style="font-size:10px;color:rgba(167,243,208,.75);margin-top:3px;">Sudah ditetapkan</p>
</div>
<div style="position:relative;z-index:1;width:44px;height:44px;background:rgba(255,255,255,.18);border-radius:13px;display:flex;align-items:center;justify-content:center;flex-shrink:0;border:1px solid rgba(255,255,255,.2);">
<svg width="22" height="22" fill="none" stroke="#fff" 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>
</div>
</div>
<div style="position:relative;background:linear-gradient(135deg,#f43f5e,#e11d48);color:#fff;border-radius:18px;padding:18px;display:flex;align-items:center;justify-content:space-between;overflow:hidden;box-shadow:0 6px 20px rgba(244,63,94,.3);">
<div class="sc-blob1"></div><div class="sc-blob2"></div>
<div style="position:relative;z-index:1;">
<p style="font-size:10.5px;font-weight:600;color:rgba(254,205,211,.9);">Ditolak</p>
<p style="font-size:32px;font-weight:900;margin-top:2px;line-height:1;letter-spacing:-.04em;">{{ $totalDitolak ?? 0 }}</p>
<p style="font-size:10px;color:rgba(254,205,211,.75);margin-top:3px;">Tidak memenuhi syarat</p>
</div>
<div style="position:relative;z-index:1;width:44px;height:44px;background:rgba(255,255,255,.18);border-radius:13px;display:flex;align-items:center;justify-content:center;flex-shrink:0;border:1px solid rgba(255,255,255,.2);">
<svg width="22" height="22" fill="none" stroke="#fff" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
<div class="adm-stat-card-icon" style="position:relative;z-index:1;width:50px;height:50px;background:rgba(255,255,255,.18);border-radius:14px;display:flex;align-items:center;justify-content:center;flex-shrink:0;border:1px solid rgba(255,255,255,.2);">
<svg width="26" height="26" fill="none" stroke="#fff" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="{{ $c['icon'] }}"/></svg>
</div>
</div>
@endforeach
</div>
{{-- GRAFIK + DATA PER DUSUN (logika PHP asli 100% tidak diubah) --}}
<div style="display:grid;grid-template-columns:2fr 1fr;gap:14px;">
{{-- GRAFIK + DATA PER DUSUN --}}
<div class="adm-chart-grid" style="display:grid;grid-template-columns:2fr 1fr;gap:16px;">
<div class="db-card">
<div style="padding:12px 18px;border-bottom:1px solid #f1f5f9;background:#fafbfc;display:flex;align-items:center;justify-content:space-between;">
<div style="display:flex;align-items:center;gap:7px;">
<div style="width:3px;height:16px;background:linear-gradient(180deg,#3b82f6,#2563eb);border-radius:4px;"></div>
<h3 style="font-size:13px;font-weight:700;color:#1e293b;">Jumlah Warga per Dusun</h3>
{{-- Grafik Modern --}}
<div class="db-card" style="position:relative;">
{{-- Header --}}
<div style="padding:16px 20px 14px;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;">
<div>
<div style="display:flex;align-items:center;gap:8px;">
<div style="width:10px;height:10px;border-radius:50%;background:linear-gradient(135deg,#60a5fa,#2563eb);box-shadow:0 0 0 3px rgba(37,99,235,.15);flex-shrink:0;"></div>
<h3 class="adm-section-title" style="font-size:15px;font-weight:800;color:#0f172a;">Warga per Dusun</h3>
</div>
<p style="font-size:12px;color:#94a3b8;margin-top:3px;margin-left:18px;">Distribusi warga di setiap dusun</p>
</div>
<div style="text-align:right;background:#eff6ff;border:1px solid #bfdbfe;border-radius:12px;padding:8px 14px;">
<p style="font-size:20px;font-weight:900;color:#1d4ed8;line-height:1;">{{ $wargaPerDusun->sum('total') }}</p>
<p style="font-size:11px;color:#60a5fa;font-weight:600;">total warga</p>
</div>
<span style="font-size:10.5px;color:#94a3b8;background:#fff;border:1px solid #f1f5f9;padding:2px 9px;border-radius:20px;font-weight:600;">
{{ $wargaPerDusun->sum('total') }} total warga
</span>
</div>
<div style="padding:18px 20px;">
{{-- Divider --}}
<div style="height:1px;background:linear-gradient(90deg,transparent,#e2e8f0 20%,#e2e8f0 80%,transparent);margin:0 16px;"></div>
{{-- SVG Chart --}}
<div style="padding:16px 12px 4px;position:relative;">
@php
$dusuns = $wargaPerDusun->values();
$n = max($dusuns->count(), 1);
$maxV = max((int)$dusuns->max('total'), 1);
$W = 560; $H = 210;
$pL = 40; $pR = 20;
$pT = 30; $pB = 45;
$cW = $W - $pL - $pR;
$cH = $H - $pT - $pB;
$W=360; $H=190; $pL=36; $pR=12; $pT=30; $pB=46;
$cW=$W-$pL-$pR; $cH=$H-$pT-$pB;
$botY=$pT+$cH;
$step = max(1, (int)ceil($maxV / 4));
$yTicks = range(0, $maxV + $step, $step);
$pts = [];
foreach ($dusuns as $i => $d) {
$x = $pL + ($n === 1 ? $cW / 2 : ($i / ($n - 1)) * $cW);
$y = $pT + $cH - ($d->total / $maxV) * $cH;
$pts[] = ['x' => round($x, 1), 'y' => round($y, 1), 'val' => $d->total, 'name' => $d->dusun];
$pts=[];
foreach($dusuns as $i=>$d){
$x = round($pL + ($n===1 ? $cW/2 : ($i/($n-1))*$cW), 1);
$y = round($pT + $cH - ($d->total / $maxV) * $cH, 1);
$pts[] = ['x'=>$x,'y'=>$y,'val'=>$d->total,'name'=>$d->dusun];
}
$poly = implode(' ', array_map(fn($p) => "{$p['x']},{$p['y']}", $pts));
$firstPt = $pts[0];
$lastPt = $pts[count($pts)-1];
$botY = $pT + $cH;
$areaPath = "M {$firstPt['x']},{$firstPt['y']} "
. implode(' ', array_map(fn($p) => "L {$p['x']},{$p['y']}", array_slice($pts, 1)))
. " L {$lastPt['x']},{$botY} L {$firstPt['x']},{$botY} Z";
/* Smooth cubic bezier */
$linePath = "M {$pts[0]['x']},{$pts[0]['y']}";
for($i=0;$i<count($pts)-1;$i++){
$cp1x = round($pts[$i]['x'] + ($pts[$i+1]['x']-$pts[$i]['x'])*0.45, 1);
$cp2x = round($pts[$i+1]['x'] - ($pts[$i+1]['x']-$pts[$i]['x'])*0.45, 1);
$linePath .= " C {$cp1x},{$pts[$i]['y']} {$cp2x},{$pts[$i+1]['y']} {$pts[$i+1]['x']},{$pts[$i+1]['y']}";
}
$areaPath = $linePath." L {$pts[count($pts)-1]['x']},{$botY} L {$pts[0]['x']},{$botY} Z";
$step = max(1,(int)ceil($maxV/4));
$yTicks = range(0, $maxV, $step);
@endphp
<svg viewBox="0 0 {{ $W }} {{ $H }}" width="100%" xmlns="http://www.w3.org/2000/svg">
<svg viewBox="0 0 {{ $W }} {{ $H }}" width="100%" style="display:block;overflow:visible;" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="fillGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#3b82f6" stop-opacity="0.18"/>
<linearGradient id="areaGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#3b82f6" stop-opacity=".25"/>
<stop offset="75%" stop-color="#3b82f6" stop-opacity=".05"/>
<stop offset="100%" stop-color="#3b82f6" stop-opacity="0"/>
</linearGradient>
<linearGradient id="lineGrad" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#60a5fa"/>
<stop offset="100%" stop-color="#1d4ed8"/>
</linearGradient>
<filter id="glow">
<feGaussianBlur stdDeviation="2" result="b"/>
<feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<clipPath id="cClip">
<rect x="{{ $pL }}" y="{{ $pT-2 }}" width="{{ $cW }}" height="{{ $cH+4 }}"/>
</clipPath>
</defs>
{{-- Grid garis datar dashed --}}
@foreach($yTicks as $t)
@if($t <= $maxV + $step)
@php $gy = $pT + $cH - min($t, $maxV) / $maxV * $cH; @endphp
<line x1="{{ $pL }}" y1="{{ $gy }}" x2="{{ $W - $pR }}" y2="{{ $gy }}" stroke="#f1f5f9" stroke-width="1"/>
<text x="{{ $pL - 5 }}" y="{{ $gy + 4 }}" text-anchor="end" font-size="10" fill="#94a3b8">{{ $t }}</text>
@endif
@php $gy = round($pT + $cH - ($t/$maxV)*$cH, 1); @endphp
<line x1="{{ $pL }}" y1="{{ $gy }}" x2="{{ $W-$pR }}" y2="{{ $gy }}"
stroke="#f1f5f9" stroke-width="1" stroke-dasharray="4,5"/>
<text x="{{ $pL-5 }}" y="{{ $gy+4 }}" text-anchor="end"
font-size="10" fill="#cbd5e1" font-weight="700"
font-family="Figtree,sans-serif">{{ $t }}</text>
@endforeach
<text x="10" y="{{ $pT + $cH / 2 }}" text-anchor="middle" font-size="9" fill="#94a3b8" transform="rotate(-90,10,{{ $pT + $cH / 2 }})">Jumlah Warga</text>
<line x1="{{ $pL }}" y1="{{ $botY }}" x2="{{ $W - $pR }}" y2="{{ $botY }}" stroke="#cbd5e1" stroke-width="1.5"/>
<polygon points="{{ $W - $pR }},{{ $botY - 4 }} {{ $W - $pR + 8 }},{{ $botY }} {{ $W - $pR }},{{ $botY + 4 }}" fill="#94a3b8"/>
<line x1="{{ $pL }}" y1="{{ $pT }}" x2="{{ $pL }}" y2="{{ $botY }}" stroke="#cbd5e1" stroke-width="1.5"/>
<polygon points="{{ $pL - 4 }},{{ $pT }} {{ $pL }},{{ $pT - 8 }} {{ $pL + 4 }},{{ $pT }}" fill="#94a3b8"/>
<path d="{{ $areaPath }}" fill="url(#fillGrad)"/>
<polyline points="{{ $poly }}" fill="none" stroke="#2563eb" stroke-width="2.5" stroke-linejoin="round" stroke-linecap="round"/>
@foreach($pts as $p)
<circle cx="{{ $p['x'] }}" cy="{{ $p['y'] }}" r="5" fill="white" stroke="#2563eb" stroke-width="2.5"/>
<text x="{{ $p['x'] }}" y="{{ $p['y'] - 10 }}" text-anchor="middle" font-size="11" font-weight="700" fill="#2563eb">{{ $p['val'] }}</text>
<text x="{{ $p['x'] }}" y="{{ $botY + 16 }}" text-anchor="middle" font-size="10" fill="#64748b">{{ Str::limit($p['name'], 9) }}</text>
{{-- Baseline --}}
<line x1="{{ $pL }}" y1="{{ $botY }}" x2="{{ $W-$pR }}" y2="{{ $botY }}"
stroke="#e2e8f0" stroke-width="1.2"/>
{{-- Area fill --}}
<path d="{{ $areaPath }}" fill="url(#areaGrad)" clip-path="url(#cClip)"/>
{{-- Line smooth + glow --}}
{{-- Shadow line tipis di bawah --}}
<path d="{{ $linePath }}" fill="none" stroke="#bfdbfe"
stroke-width="5" stroke-linecap="round" opacity="0.4" clip-path="url(#cClip)"/>
{{-- Garis utama --}}
<path d="{{ $linePath }}" fill="none" stroke="url(#lineGrad)"
stroke-width="3" stroke-linecap="round" clip-path="url(#cClip)"/>
{{-- Titik data --}}
@foreach($pts as $idx => $p)
{{-- Ring luar --}}
<circle cx="{{ $p['x'] }}" cy="{{ $p['y'] }}" r="10"
fill="rgba(37,99,235,.08)"/>
{{-- Dot putih dengan border biru --}}
<circle cx="{{ $p['x'] }}" cy="{{ $p['y'] }}" r="5.5"
fill="#fff" stroke="url(#lineGrad)" stroke-width="2.5"
filter="url(#glow)"/>
{{-- Dot inner biru kecil --}}
<circle cx="{{ $p['x'] }}" cy="{{ $p['y'] }}" r="2.5"
fill="#2563eb"/>
{{-- Badge nilai di atas titik --}}
<rect x="{{ $p['x']-12 }}" y="{{ $p['y']-28 }}"
width="24" height="16" rx="5"
fill="#1d4ed8"/>
<text x="{{ $p['x'] }}" y="{{ $p['y']-17 }}" text-anchor="middle"
font-size="10" font-weight="800" fill="#fff"
font-family="Figtree,sans-serif">{{ $p['val'] }}</text>
{{-- Label dusun di bawah --}}
<text x="{{ $p['x'] }}" y="{{ $botY+14 }}" text-anchor="middle"
font-size="10" fill="#64748b" font-weight="700"
font-family="Figtree,sans-serif">{{ Str::limit($p['name'],7) }}</text>
<line x1="{{ $p['x'] }}" y1="{{ $botY+2 }}" x2="{{ $p['x'] }}" y2="{{ $botY+5 }}"
stroke="#e2e8f0" stroke-width="1"/>
@endforeach
<text x="{{ $W - $pR + 10 }}" y="{{ $botY + 4 }}" text-anchor="start" font-size="9" fill="#94a3b8">Dusun</text>
</svg>
</div>
{{-- Legend --}}
<div style="padding:4px 20px 16px;display:flex;align-items:center;gap:8px;">
<div style="width:20px;height:3px;background:linear-gradient(90deg,#60a5fa,#1d4ed8);border-radius:99px;"></div>
<span style="font-size:12px;color:#94a3b8;font-weight:600;">Jumlah warga per dusun</span>
</div>
</div>
{{-- Data per dusun --}}
<div class="db-card">
<div style="padding:12px 18px;border-bottom:1px solid #f1f5f9;background:#fafbfc;display:flex;align-items:center;gap:7px;">
<div style="width:3px;height:16px;background:linear-gradient(180deg,#10b981,#059669);border-radius:4px;"></div>
<h3 style="font-size:13px;font-weight:700;color:#1e293b;">Data Per Dusun</h3>
<div style="padding:14px 18px;border-bottom:1px solid #f1f5f9;background:#fafbfc;display:flex;align-items:center;gap:9px;">
<div style="width:4px;height:18px;background:linear-gradient(180deg,#10b981,#059669);border-radius:4px;"></div>
<h3 class="adm-section-title" style="font-size:15px;font-weight:700;color:#1e293b;">Per Dusun</h3>
</div>
<div style="padding:14px;display:flex;flex-direction:column;gap:8px;">
@php $maxDusun = $wargaPerDusun->max('total') ?: 1; @endphp
<div style="padding:14px;display:flex;flex-direction:column;gap:10px;">
@php $maxDusun=$wargaPerDusun->max('total')?:1; @endphp
@foreach($wargaPerDusun as $data)
<div style="padding:10px 12px;background:#f8fafc;border:1.5px solid #f1f5f9;border-radius:13px;transition:border-color .15s;">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;">
<div style="display:flex;align-items:center;gap:8px;">
<div style="width:26px;height:26px;background:#eff6ff;border-radius:8px;display:flex;align-items:center;justify-content:center;flex-shrink:0;">
<svg width="13" height="13" fill="#2563eb" 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>
</div>
<span style="font-size:12.5px;font-weight:700;color:#1e293b;">{{ $data->dusun }}</span>
</div>
<span style="font-size:14px;font-weight:900;color:#2563eb;">{{ $data->total }}</span>
</div>
<div style="width:100%;height:5px;background:#e2e8f0;border-radius:99px;overflow:hidden;">
<div style="height:100%;background:linear-gradient(90deg,#3b82f6,#2563eb);border-radius:99px;width:{{ ($data->total / $maxDusun) * 100 }}%;"></div>
</div>
<div style="padding:12px 14px;background:#f8fafc;border:1.5px solid #f1f5f9;border-radius:12px;">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;">
<span class="adm-dusun-name" style="font-size:14px;font-weight:700;color:#1e293b;">{{ $data->dusun }}</span>
<span class="adm-dusun-num" style="font-size:17px;font-weight:900;color:#2563eb;">{{ $data->total }}</span>
</div>
<div class="adm-dusun-bar" style="width:100%;height:7px;background:#e2e8f0;border-radius:99px;overflow:hidden;">
<div style="height:100%;background:linear-gradient(90deg,#3b82f6,#2563eb);border-radius:99px;width:{{ ($data->total/$maxDusun)*100 }}%;"></div>
</div>
</div>
@endforeach
</div>
</div>
</div>
{{-- TOP 10 full width, tambah kolom Dusun & RT, logika asli 100% tidak diubah --}}
{{-- TOP 10 --}}
<div class="db-card tt">
<div style="padding:14px 18px;border-bottom:1.5px solid #f1f5f9;background:#fafbfc;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px;">
<div style="display:flex;align-items:center;gap:8px;">
<div style="width:3px;height:18px;background:linear-gradient(180deg,#3b82f6,#2563eb);border-radius:4px;"></div>
<div style="display:flex;align-items:center;gap:10px;">
<div style="width:4px;height:18px;background:linear-gradient(180deg,#3b82f6,#2563eb);border-radius:4px;"></div>
<div>
<h3 style="font-size:13px;font-weight:800;color:#0f172a;">Top 10 Probabilitas Tertinggi</h3>
<p style="font-size:10.5px;color:#94a3b8;margin-top:1px;font-weight:500;">Seluruh dusun diurutkan dari probabilitas terbesar</p>
<h3 class="adm-section-title" style="font-size:15px;font-weight:800;color:#0f172a;">Top 10 Probabilitas Tertinggi</h3>
<p style="font-size:12px;color:#94a3b8;margin-top:3px;">Diurutkan dari probabilitas terbesar</p>
</div>
</div>
<div style="display:flex;align-items:center;gap:12px;">
<div style="display:flex;align-items:center;gap:10px;font-size:10.5px;color:#94a3b8;font-weight:600;">
<span style="display:flex;align-items:center;gap:4px;"><span style="width:8px;height:8px;border-radius:50%;background:#10b981;display:inline-block;"></span>≥70%</span>
<span style="display:flex;align-items:center;gap:4px;"><span style="width:8px;height:8px;border-radius:50%;background:#f59e0b;display:inline-block;"></span>4070%</span>
<span style="display:flex;align-items:center;gap:4px;"><span style="width:8px;height:8px;border-radius:50%;background:#f43f5e;display:inline-block;"></span>&lt;40%</span>
</div>
<a href="{{ route('admin.filterisasi') }}"
style="display:inline-flex;align-items:center;gap:5px;font-size:11.5px;font-weight:700;color:#2563eb;background:#eff6ff;border:1px solid #bfdbfe;padding:5px 12px;border-radius:9px;text-decoration:none;">
Lihat Semua
<svg width="11" height="11" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
</a>
</div>
<a href="{{ route('admin.filterisasi') }}" class="adm-lihat-semua"
style="display:inline-flex;align-items:center;gap:6px;font-size:14px;font-weight:700;color:#2563eb;background:#eff6ff;border:1px solid #bfdbfe;padding:10px 18px;border-radius:10px;text-decoration:none;min-height:44px;">
Lihat Semua
<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="M9 5l7 7-7 7"/></svg>
</a>
</div>
<div style="overflow-x:auto;">
<div style="overflow-x:auto;-webkit-overflow-scrolling:touch;">
<table>
<thead>
<tr>
<th style="width:48px;">Rank</th>
<th>#</th>
<th>Nama</th>
<th>NIK</th>
<th>Dusun</th>
<th>RT</th>
<th>Dusun / RT</th>
<th>Penghasilan</th>
<th style="min-width:160px;">Probabilitas</th>
<th>Status</th>
@ -379,103 +468,55 @@ function clearNotifs() { NS.clearAll(); renderNotifs(); }
</thead>
<tbody>
@forelse($topWarga as $index => $warga)
<tr>
{{-- Rank (asli) --}}
<td>
@if($index == 0)
<div class="rk rk-1">1</div>
@elseif($index == 1)
<div class="rk rk-2">2</div>
@elseif($index == 2)
<div class="rk rk-3">3</div>
@else
<div class="rk rk-n">{{ $index + 1 }}</div>
@endif
</td>
{{-- Nama (asli) --}}
<td>
<div style="display:flex;align-items:center;gap:9px;">
<div style="width:30px;height:30px;border-radius:9px;background:linear-gradient(135deg,#3b82f6,#2563eb);display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:11px;font-weight:800;color:#fff;">
{{ strtoupper(substr($warga->nama, 0, 1)) }}
</div>
<div>
<div style="font-size:12.5px;font-weight:700;color:#0f172a;">{{ $warga->nama }}</div>
<div style="font-size:10.5px;color:#94a3b8;margin-top:1px;">{{ $warga->pekerjaan }}</div>
</div>
<tr>
<td>
@php
$rk = ['linear-gradient(135deg,#f59e0b,#d97706)','linear-gradient(135deg,#94a3b8,#64748b)','linear-gradient(135deg,#f97316,#ea580c)'];
$bg = $index < 3 ? $rk[$index] : '#f1f5f9';
$tc = $index < 3 ? '#fff' : '#64748b';
@endphp
<div class="adm-rank-badge" style="width:30px;height:30px;border-radius:8px;background:{{ $bg }};display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:800;color:{{ $tc }};">{{ $index+1 }}</div>
</td>
<td>
<div style="display:flex;align-items:center;gap:10px;">
<div class="adm-avatar-sm" style="width:34px;height:34px;border-radius:10px;background:linear-gradient(135deg,#3b82f6,#2563eb);display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:14px;font-weight:800;color:#fff;">
{{ strtoupper(substr($warga->nama,0,1)) }}
</div>
</td>
{{-- NIK (asli) --}}
<td>
<span style="font-family:monospace;font-size:10.5px;color:#6b7280;background:#f3f4f6;padding:2px 7px;border-radius:6px;">{{ $warga->nik }}</span>
</td>
{{-- Dusun (ditambahkan pakai $warga->dusun yang sudah ada di data asli) --}}
<td>
<span style="display:inline-flex;padding:3px 9px;border-radius:7px;background:#eff6ff;color:#1d4ed8;font-size:10.5px;font-weight:700;border:1px solid #bfdbfe;white-space:nowrap;">
{{ $warga->dusun }}
</span>
</td>
{{-- RT (ditambahkan pakai $warga->rt jika ada) --}}
<td style="font-size:11px;color:#64748b;font-weight:600;">
RT {{ str_pad($warga->rt ?? '0', 3, '0', STR_PAD_LEFT) }}
</td>
{{-- Penghasilan (asli) --}}
<td style="font-size:12px;font-weight:600;color:#374151;white-space:nowrap;">
Rp {{ number_format($warga->penghasilan, 0, ',', '.') }}
</td>
{{-- Probabilitas (asli) --}}
<td>
@php
$sc = $warga->probabilitas >= 70 ? '#10b981' : ($warga->probabilitas >= 40 ? '#f59e0b' : '#f43f5e');
$scB = $warga->probabilitas >= 70 ? '#f0fdf4' : ($warga->probabilitas >= 40 ? '#fffbeb' : '#fff1f2');
@endphp
<div style="display:flex;align-items:center;gap:8px;">
<div style="flex:1;background:#f1f5f9;border-radius:99px;height:6px;overflow:hidden;min-width:80px;">
<div style="height:100%;border-radius:99px;background:{{ $sc }};width:{{ min($warga->probabilitas, 100) }}%;"></div>
</div>
<span style="font-size:12px;font-weight:800;color:{{ $sc }};white-space:nowrap;">
{{ number_format($warga->probabilitas, 1) }}%
</span>
<div>
<div class="adm-tbl-name" style="font-size:14px;font-weight:700;color:#0f172a;white-space:nowrap;">{{ $warga->nama }}</div>
<div class="adm-tbl-job" style="font-size:12px;color:#94a3b8;margin-top:2px;">{{ $warga->pekerjaan }}</div>
</div>
</td>
{{-- Status (asli) --}}
<td>
@if($warga->status == 'pending')
<span style="display:inline-flex;align-items:center;gap:4px;padding:4px 10px;font-size:10.5px;font-weight:700;border-radius:20px;background:#fffbeb;color:#b45309;border:1px solid #fde68a;white-space:nowrap;">
<span style="width:5px;height:5px;border-radius:50%;background:#f59e0b;flex-shrink:0;"></span>Pending
</span>
@elseif($warga->status == 'diterima')
<span style="display:inline-flex;align-items:center;gap:4px;padding:4px 10px;font-size:10.5px;font-weight:700;border-radius:20px;background:#f0fdf4;color:#166534;border:1px solid #bbf7d0;white-space:nowrap;">
<span style="width:5px;height:5px;border-radius:50%;background:#22c55e;flex-shrink:0;"></span>Diterima
</span>
@elseif($warga->status == 'ditolak')
<span style="display:inline-flex;align-items:center;gap:4px;padding:4px 10px;font-size:10.5px;font-weight:700;border-radius:20px;background:#fff1f2;color:#be123c;border:1px solid #fecdd3;white-space:nowrap;">
<span style="width:5px;height:5px;border-radius:50%;background:#f43f5e;flex-shrink:0;"></span>Ditolak
</span>
@else
<span style="display:inline-flex;align-items:center;gap:4px;padding:4px 10px;font-size:10.5px;font-weight:700;border-radius:20px;background:#eff6ff;color:#1d4ed8;border:1px solid #bfdbfe;white-space:nowrap;">
<span style="width:5px;height:5px;border-radius:50%;background:#3b82f6;flex-shrink:0;"></span>{{ ucfirst($warga->status) }}
</span>
@endif
</td>
</tr>
</div>
</td>
<td><span class="adm-tbl-nik" style="font-family:monospace;font-size:12px;color:#6b7280;background:#f3f4f6;padding:3px 9px;border-radius:6px;white-space:nowrap;">{{ $warga->nik }}</span></td>
<td>
<div style="white-space:nowrap;">
<span class="adm-tbl-dusun" style="display:inline-block;padding:3px 10px;border-radius:6px;background:#eff6ff;color:#1d4ed8;font-size:12px;font-weight:700;border:1px solid #bfdbfe;">{{ $warga->dusun }}</span>
<span class="adm-tbl-rt" style="font-size:12px;color:#64748b;font-weight:600;margin-left:5px;">RT {{ str_pad($warga->rt??'0',3,'0',STR_PAD_LEFT) }}</span>
</div>
</td>
<td class="adm-tbl-income" style="white-space:nowrap;font-weight:600;font-size:14px;">Rp {{ number_format($warga->penghasilan,0,',','.') }}</td>
<td>
@php $sc=$warga->probabilitas>=70?'#10b981':($warga->probabilitas>=40?'#f59e0b':'#f43f5e'); @endphp
<div style="display:flex;align-items:center;gap:8px;">
<div style="flex:1;background:#f1f5f9;border-radius:99px;height:7px;overflow:hidden;min-width:80px;">
<div style="height:100%;border-radius:99px;background:{{ $sc }};width:{{ min($warga->probabilitas,100) }}%;"></div>
</div>
<span class="adm-tbl-pct" style="font-size:13px;font-weight:800;color:{{ $sc }};white-space:nowrap;">{{ number_format($warga->probabilitas,1) }}%</span>
</div>
</td>
<td>
@php
$st=['pending'=>['#fffbeb','#b45309','#fde68a','Pending'],'diterima'=>['#f0fdf4','#166534','#bbf7d0','Diterima'],'ditolak'=>['#fff1f2','#be123c','#fecdd3','Ditolak']];
$s=$st[$warga->status]??['#eff6ff','#1d4ed8','#bfdbfe',ucfirst($warga->status)];
@endphp
<span class="adm-tbl-badge" style="display:inline-flex;align-items:center;gap:5px;padding:5px 12px;font-size:12px;font-weight:700;border-radius:20px;background:{{ $s[0] }};color:{{ $s[1] }};border:1px solid {{ $s[2] }};white-space:nowrap;">
<span style="width:6px;height:6px;border-radius:50%;background:{{ $s[1] }};flex-shrink:0;"></span>{{ $s[3] }}
</span>
</td>
</tr>
@empty
<tr>
<td colspan="8">
<div style="padding:48px 16px;text-align:center;">
<div style="width:44px;height:44px;background:#f3f4f6;border-radius:50%;display:flex;align-items:center;justify-content:center;margin:0 auto 10px;">
<svg width="20" height="20" fill="none" stroke="#d1d5db" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" 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>
<p style="font-size:13px;font-weight:600;color:#94a3b8;">Belum ada data warga</p>
</div>
</td>
</tr>
<tr><td colspan="7" style="text-align:center;padding:48px;color:#94a3b8;font-size:15px;">Belum ada data warga</td></tr>
@endforelse
</tbody>
</table>

View File

@ -1,299 +1,383 @@
<x-app-layout>
<x-slot name="header">
<div class="flex items-center justify-between">
<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;">
<div>
<h2 class="font-semibold text-xl text-gray-800 leading-tight">Data Akun</h2>
<p class="text-sm text-gray-500 mt-0.5">Kelola akun Admin & RT yang menggunakan sistem.</p>
<h2 style="font-size:16px;font-weight:800;color:#0f172a;letter-spacing:-.02em;">Data Akun</h2>
<p style="font-size:11px;color:#94a3b8;margin-top:2px;">Kelola akun Admin & RT yang menggunakan sistem</p>
</div>
<div class="inline-flex items-center gap-2 bg-gray-50 border border-gray-200 rounded-xl px-3 py-1.5 text-xs text-gray-500">
<svg class="w-3.5 h-3.5 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"/>
</svg>
@if(isset($users) && method_exists($users, 'total'))
{{ $users->total() }} akun terdaftar
@endif
@if(isset($users) && method_exists($users,'total'))
<div style="display:inline-flex;align-items:center;gap:6px;background:#f8fafc;border:1.5px solid #e2e8f0;border-radius:10px;padding:6px 12px;font-size:11px;color:#64748b;">
<svg width="13" height="13" fill="none" stroke="#3b82f6" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"/></svg>
{{ $users->total() }} akun terdaftar
</div>
@endif
</div>
</x-slot>
<div class="space-y-4">
<style>
.da-card{background:#fff;border-radius:16px;border:1.5px solid #f1f5f9;box-shadow:0 1px 4px rgba(0,0,0,.04);overflow:hidden;}
.fl-box{display:flex;align-items:center;gap:9px;border-radius:12px;padding:10px 14px;font-size:12px;font-weight:500;margin-bottom:10px;}
.fl-box svg{width:14px;height:14px;flex-shrink:0;}
{{-- NOTIFIKASI --}}
@if(session('success'))
<div class="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>
{{ session('success') }}
</div>
@endif
@if(session('error'))
<div class="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>
{{ session('error') }}
</div>
@endif
/* search */
.sw{position:relative;}
.sw svg{position:absolute;left:11px;top:50%;transform:translateY(-50%);width:14px;height:14px;stroke:#9ca3af;pointer-events:none;}
.sw input{width:100%;padding:9px 12px 9px 34px;font-size:13px;color:#111827;background:#f9fafb;border:1.5px solid #e5e7eb;border-radius:11px;outline:none;transition:border-color .15s,box-shadow .15s;font-family:inherit;}
.sw input:focus{border-color:#3b82f6;background:#fff;box-shadow:0 0 0 3px rgba(59,130,246,.10);}
.sw input::placeholder{color:#9ca3af;}
{{-- SEARCH --}}
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 p-5">
<div class="flex items-center gap-2 mb-3">
<div class="w-1 h-4 bg-blue-600 rounded-full"></div>
<h3 class="text-sm font-semibold text-gray-700">Pencarian</h3>
</div>
<form method="GET" action="{{ route('admin.data-akun') }}" class="flex flex-col md:flex-row gap-3">
<div class="relative flex-1">
<svg class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
<input type="text" name="q" value="{{ request('q') }}"
placeholder="Cari nama / email..."
class="w-full pl-9 pr-3 py-2 text-sm border border-gray-200 rounded-xl bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"/>
</div>
<div class="flex gap-2">
<button type="submit"
class="inline-flex items-center justify-center gap-2 px-5 py-2 text-sm font-semibold bg-blue-600 text-white rounded-xl hover:bg-blue-700 transition shadow-sm 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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
Cari
</button>
@if(request()->filled('q'))
<a href="{{ route('admin.data-akun') }}"
class="inline-flex items-center justify-center w-10 text-sm bg-gray-100 text-gray-500 rounded-xl hover:bg-gray-200 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="M6 18L18 6M6 6l12 12"/>
</svg>
</a>
@endif
</div>
</form>
/* table */
.tbl-wrap{overflow-x:auto;-webkit-overflow-scrolling:touch;}
table{width:100%;border-collapse:collapse;min-width:580px;}
thead tr{background:#f8fafc;border-bottom:1.5px solid #f1f5f9;}
thead th{padding:9px 14px;text-align:left;font-size:9.5px;font-weight:700;color:#94a3b8;text-transform:uppercase;letter-spacing:.07em;white-space:nowrap;}
tbody tr{border-bottom:1px solid #f8fafc;transition:background .1s;}
tbody tr:hover{background:#f0f7ff;}
tbody tr:last-child{border-bottom:none;}
tbody td{padding:10px 14px;vertical-align:middle;}
/* 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;}
/* action buttons */
.btn-a{display:inline-flex;align-items:center;gap:4px;padding:4px 10px;border-radius:8px;font-size:10.5px;font-weight:600;border:none;cursor:pointer;white-space:nowrap;font-family:inherit;text-decoration:none;line-height:1.4;transition:filter .1s;}
.btn-a:hover{filter:brightness(.92);}
.btn-a svg{width:11px;height:11px;flex-shrink:0;}
.btn-role{background:#f1f5f9;color:#475569;}
.btn-nonaktif{background:#fffbeb;color:#b45309;}
.btn-aktif{background:#f0fdf4;color:#166534;}
.btn-hapus{background:#fff1f2;color:#be123c;}
.btn-locked{background:#f8fafc;color:#94a3b8;border:1.5px solid #e2e8f0;cursor:default;}
/* avatar */
.av-wrap{position:relative;flex-shrink:0;}
.av{width:34px;height:34px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:800;color:#fff;}
.av-dot{position:absolute;bottom:-1px;right:-1px;width:10px;height:10px;border-radius:50%;background:#10b981;border:2px solid #fff;}
/* mobile card view */
.mob-card{display:none;flex-direction:column;gap:10px;padding:14px 16px;}
.mob-row{background:#f8fafc;border-radius:13px;padding:12px 14px;border:1.5px solid #f1f5f9;}
.mob-name{font-size:13px;font-weight:700;color:#0f172a;}
.mob-email{font-size:11px;color:#64748b;margin-top:2px;}
.mob-meta{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-top:8px;}
.mob-actions{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-top:10px;padding-top:10px;border-top:1px solid #f1f5f9;}
@media(max-width:640px){
.tbl-wrap table{display:none;}
.mob-card{display:flex;}
}
</style>
{{-- FLASH --}}
@if(session('success'))
<div class="fl-box" 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="fl-box" 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
{{-- TABLE CARD --}}
<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 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">
Daftar Akun
@if(isset($users) && method_exists($users, 'total'))
<span class="ml-1 text-gray-400 font-normal">({{ $users->total() }} akun)</span>
@endif
</span>
</div>
@if(isset($users) && method_exists($users, 'currentPage') && $users->lastPage() > 1)
<span class="text-xs text-gray-400">Hal. {{ $users->currentPage() }} / {{ $users->lastPage() }}</span>
@endif
{{-- SEARCH --}}
<div class="da-card" style="padding:14px 16px;margin-bottom:12px;">
<div style="display:flex;align-items:center;gap:7px;margin-bottom:10px;">
<div style="width:3px;height:14px;background:#2563eb;border-radius:4px;"></div>
<h3 style="font-size:12px;font-weight:700;color:#374151;">Pencarian</h3>
</div>
<form method="GET" action="{{ route('admin.data-akun') }}"
style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
<div class="sw" style="flex:1;min-width:200px;">
<svg fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
<input type="text" name="q" value="{{ request('q') }}" placeholder="Cari nama atau email...">
</div>
<div class="overflow-x-auto w-full">
<table class="w-full text-sm table-fixed">
<colgroup>
<col style="width:28%">
<col style="width:23%">
<col style="width:11%">
<col style="width:11%">
<col style="width:27%">
</colgroup>
<thead>
<tr class="bg-gray-50/80 border-b border-gray-100">
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wide">Nama</th>
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wide">Email</th>
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wide">Role</th>
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wide">Status</th>
<th class="px-4 py-3 text-right text-xs font-semibold text-gray-400 uppercase tracking-wide">Aksi</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
@forelse($users ?? [] as $u)
@php
$isMe = ($u->id === auth()->id());
$isActive = isset($u->is_active) ? (bool)$u->is_active : true;
$isAdmin = $u->role === 'admin';
$newRole = $isAdmin ? 'rt' : 'admin';
$newRoleLabel = $isAdmin ? 'RT' : 'Admin';
$initial = strtoupper(substr($u->name, 0, 1));
$avatarGrad = $isAdmin
? 'from-emerald-500 to-emerald-600 shadow-emerald-200'
: 'from-blue-500 to-blue-600 shadow-blue-200';
@endphp
<tr class="hover:bg-blue-50/20 transition-colors duration-150 {{ !$isActive ? 'opacity-50' : '' }}">
{{-- NAMA --}}
<td class="px-4 py-6">
<div class="flex items-center gap-2.5">
<div class="relative shrink-0">
<div class="w-10 h-10 rounded-full bg-gradient-to-br {{ $avatarGrad }} flex items-center justify-center shadow-sm">
<span class="text-sm font-bold text-white">{{ $initial }}</span>
</div>
@if($isActive)
<span class="absolute -bottom-0.5 -right-0.5 w-3 h-3 bg-emerald-500 border-2 border-white rounded-full"></span>
@endif
</div>
<div class="min-w-0">
<div class="text-sm font-semibold text-gray-800 truncate flex items-center gap-1.5 flex-wrap">
{{ $u->name }}
@if($isMe)
<span class="text-[10px] px-1.5 py-0.5 rounded-full bg-indigo-100 text-indigo-600 border border-indigo-200 font-semibold leading-none">Kamu</span>
@endif
</div>
<div class="text-xs text-gray-400 mt-0.5">ID #{{ $u->id }}</div>
</div>
</div>
</td>
{{-- EMAIL --}}
<td class="px-4 py-6">
<span class="text-sm text-gray-500 truncate block">{{ $u->email }}</span>
</td>
{{-- ROLE --}}
<td class="px-4 py-6">
@if($isAdmin)
<span class="inline-flex items-center gap-1 px-2 py-1 text-xs font-semibold rounded-lg bg-emerald-100 text-emerald-700 border border-emerald-200">
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-6-3a2 2 0 11-4 0 2 2 0 014 0zm-2 4a5 5 0 00-4.546 2.916A5.986 5.986 0 0010 16a5.986 5.986 0 004.546-2.084A5 5 0 0010 11z" clip-rule="evenodd"/>
</svg>
Admin
</span>
@else
<span class="inline-flex items-center gap-1 px-2 py-1 text-xs font-semibold rounded-lg bg-blue-100 text-blue-700 border border-blue-200">
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
<path d="M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z"/>
</svg>
RT
</span>
@endif
</td>
{{-- STATUS --}}
<td class="px-4 py-6">
@if($isActive)
<span class="inline-flex items-center gap-1 px-2 py-1 text-xs font-semibold rounded-lg bg-emerald-50 text-emerald-700 border border-emerald-100">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse"></span>
Aktif
</span>
@else
<span class="inline-flex items-center gap-1 px-2 py-1 text-xs font-semibold rounded-lg bg-gray-100 text-gray-400 border border-gray-200">
<span class="w-1.5 h-1.5 rounded-full bg-gray-400"></span>
Nonaktif
</span>
@endif
</td>
{{-- AKSI --}}
<td class="px-4 py-6">
<div class="flex items-center justify-end gap-1.5">
@if($isMe)
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 text-xs font-medium bg-gray-100 text-gray-400 rounded-lg border border-gray-200">
<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="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>
Terkunci
</span>
@else
{{-- UBAH ROLE --}}
<form action="{{ route('admin.data-akun.role', $u->id) }}" method="POST">
@csrf
<input type="hidden" name="role" value="{{ $newRole }}">
<button type="submit"
onclick="return confirm('Ubah role {{ addslashes($u->name) }} menjadi {{ $newRoleLabel }}?')"
class="inline-flex items-center gap-1 px-2.5 py-1 text-xs font-medium bg-slate-100 text-slate-600 rounded-lg hover:bg-slate-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="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4"/>
</svg>
{{ $newRoleLabel }}
</button>
</form>
{{-- AKTIF / NONAKTIF --}}
<form action="{{ route('admin.data-akun.toggle-aktif', $u->id) }}" method="POST">
@csrf
<button type="submit"
onclick="return confirm('{{ $isActive ? 'Nonaktifkan' : 'Aktifkan' }} akun {{ addslashes($u->name) }}?')"
class="inline-flex items-center gap-1 px-2.5 py-1 text-xs font-medium rounded-lg transition
{{ $isActive ? 'bg-amber-100 text-amber-700 hover:bg-amber-200' : 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200' }}">
@if($isActive)
<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="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"/>
</svg>
Nonaktifkan
@else
<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="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
Aktifkan
@endif
</button>
</form>
{{-- HAPUS --}}
<form action="{{ route('admin.data-akun.hapus', $u->id) }}" method="POST"
onsubmit="return confirm('Yakin hapus akun {{ addslashes($u->name) }}? Tindakan ini tidak bisa dibatalkan.')">
@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>
Hapus
</button>
</form>
@endif
</div>
</td>
</tr>
@empty
<tr>
<td colspan="5" class="py-16 text-center">
<div class="flex flex-col items-center gap-3">
<div class="w-14 h-14 bg-gray-100 rounded-2xl flex items-center justify-center">
<svg class="w-7 h-7 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"/>
</svg>
</div>
<div>
<p class="text-sm font-semibold text-gray-500">Belum ada akun</p>
@if(request()->filled('q'))
<p class="text-xs text-gray-400 mt-0.5">Tidak ada hasil untuk "<span class="font-medium">{{ request('q') }}</span>"</p>
<a href="{{ route('admin.data-akun') }}" class="inline-block mt-2 text-xs text-blue-600 hover:underline"> Hapus pencarian</a>
@endif
</div>
</div>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
{{-- PAGINATION --}}
@if(isset($users) && method_exists($users, 'links') && $users->hasPages())
<div class="px-5 py-3 border-t border-gray-100 bg-gray-50">
{{ $users->appends(request()->query())->links() }}
</div>
<button type="submit"
style="display:inline-flex;align-items:center;gap:6px;padding:9px 16px;background:#2563eb;color:#fff;font-size:12px;font-weight:700;border:none;border-radius:11px;cursor:pointer;white-space:nowrap;font-family:inherit;">
<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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
Cari
</button>
@if(request()->filled('q'))
<a href="{{ route('admin.data-akun') }}"
style="display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;background:#f3f4f6;border-radius:10px;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="M6 18L18 6M6 6l12 12"/></svg>
</a>
@endif
{{-- CREDIT WATERMARK --}}
<div class="px-5 py-5 border-t border-gray-100 flex items-center justify-center gap-2 select-none mt-1">
<svg class="w-3.5 h-3.5 text-gray-300" fill="currentColor" 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 class="text-xs text-gray-300 font-medium tracking-wide">Aplikasi SiBantuDes</span>
<span class="text-gray-200">·</span>
<span class="text-xs text-gray-300 tracking-wide">Desa Ngerong</span>
</div>
</div>
</form>
</div>
{{-- TABLE CARD --}}
<div class="da-card">
<div style="padding:10px 16px;border-bottom:1px solid #f1f5f9;background:#fafafa;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:6px;">
<div style="display:flex;align-items:center;gap:7px;">
<div style="width:3px;height:15px;background:#2563eb;border-radius:4px;"></div>
<span style="font-size:12.5px;font-weight:700;color:#374151;">
Daftar Akun
@if(isset($users) && method_exists($users,'total'))
<span style="color:#9ca3af;font-weight:400;font-size:11px;"> ({{ $users->total() }} akun)</span>
@endif
</span>
</div>
@if(isset($users) && method_exists($users,'currentPage') && $users->lastPage() > 1)
<span style="font-size:11px;color:#9ca3af;">Hal. {{ $users->currentPage() }} / {{ $users->lastPage() }}</span>
@endif
</div>
{{-- ── DESKTOP TABLE ── --}}
<div class="tbl-wrap">
<table>
<thead>
<tr>
<th>Nama</th>
<th>Email</th>
<th>Role</th>
<th>Status</th>
<th style="text-align:right;">Aksi</th>
</tr>
</thead>
<tbody>
@forelse($users ?? [] as $u)
@php
$isMe = ($u->id === auth()->id());
$isActive = isset($u->is_active) ? (bool)$u->is_active : true;
$isAdmin = $u->role === 'admin';
$newRole = $isAdmin ? 'rt' : 'admin';
$newRoleLabel = $isAdmin ? 'RT' : 'Admin';
$initial = strtoupper(substr($u->name,0,1));
$avBg = $isAdmin
? 'background:linear-gradient(135deg,#10b981,#059669);'
: 'background:linear-gradient(135deg,#3b82f6,#2563eb);';
@endphp
<tr style="{{ !$isActive ? 'opacity:.5;' : '' }}">
{{-- NAMA --}}
<td>
<div style="display:flex;align-items:center;gap:10px;">
<div class="av-wrap">
<div class="av" style="{{ $avBg }}">{{ $initial }}</div>
@if($isActive)<div class="av-dot"></div>@endif
</div>
<div style="min-width:0;">
<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap;">
<span style="font-size:12.5px;font-weight:700;color:#0f172a;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:130px;">{{ $u->name }}</span>
@if($isMe)
<span style="font-size:9.5px;padding:1px 7px;border-radius:20px;background:#ede9fe;color:#6d28d9;border:1px solid #ddd6fe;font-weight:700;">Kamu</span>
@endif
</div>
<div style="font-size:10.5px;color:#94a3b8;margin-top:1px;">ID #{{ $u->id }}</div>
</div>
</div>
</td>
{{-- EMAIL --}}
<td style="font-size:12px;color:#64748b;max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">
{{ $u->email }}
</td>
{{-- ROLE --}}
<td>
@if($isAdmin)
<span class="badge" style="background:#f0fdf4;color:#166534;border:1px solid #bbf7d0;">
<span class="dot" style="background:#10b981;"></span>Admin
</span>
@else
<span class="badge" style="background:#eff6ff;color:#1d4ed8;border:1px solid #bfdbfe;">
<span class="dot" style="background:#3b82f6;"></span>RT
</span>
@endif
</td>
{{-- STATUS --}}
<td>
@if($isActive)
<span class="badge" style="background:#f0fdf4;color:#166534;border:1px solid #bbf7d0;">
<span class="dot" style="background:#10b981;animation:pulse 2s infinite;"></span>Aktif
</span>
@else
<span class="badge" style="background:#f3f4f6;color:#6b7280;border:1px solid #e5e7eb;">
<span class="dot" style="background:#9ca3af;"></span>Nonaktif
</span>
@endif
</td>
{{-- AKSI --}}
<td>
<div style="display:flex;align-items:center;justify-content:flex-end;gap:5px;flex-wrap:wrap;">
@if($isMe)
<span class="btn-a btn-locked">
<svg fill="none" stroke="currentColor" 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>
Terkunci
</span>
@else
{{-- Ubah role --}}
<form method="POST" action="{{ route('admin.data-akun.role',$u->id) }}">
@csrf
<input type="hidden" name="role" value="{{ $newRole }}">
<button type="submit" class="btn-a btn-role"
onclick="return confirm('Ubah role {{ addslashes($u->name) }} menjadi {{ $newRoleLabel }}?')">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4"/></svg>
{{ $newRoleLabel }}
</button>
</form>
{{-- Toggle aktif --}}
<form method="POST" action="{{ route('admin.data-akun.toggle-aktif',$u->id) }}">
@csrf
<button type="submit"
class="btn-a {{ $isActive ? 'btn-nonaktif' : 'btn-aktif' }}"
onclick="return confirm('{{ $isActive ? 'Nonaktifkan' : 'Aktifkan' }} akun {{ addslashes($u->name) }}?')">
@if($isActive)
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"/></svg>
Nonaktifkan
@else
<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>
Aktifkan
@endif
</button>
</form>
{{-- Hapus --}}
<form method="POST" action="{{ route('admin.data-akun.hapus',$u->id) }}"
onsubmit="return confirm('Yakin hapus akun {{ addslashes($u->name) }}? Tindakan ini tidak bisa dibatalkan.')">
@csrf @method('DELETE')
<button type="submit" class="btn-a 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="5">
<div style="padding:48px 16px;text-align:center;">
<div style="width:44px;height:44px;background:#f3f4f6;border-radius:50%;display:flex;align-items:center;justify-content:center;margin:0 auto 10px;">
<svg width="22" height="22" fill="none" stroke="#d1d5db" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"/></svg>
</div>
<p style="font-size:13px;font-weight:600;color:#6b7280;">Belum ada akun</p>
@if(request()->filled('q'))
<p style="font-size:11.5px;color:#9ca3af;margin-top:4px;">Tidak ada hasil untuk "<strong>{{ request('q') }}</strong>"</p>
<a href="{{ route('admin.data-akun') }}" style="display:inline-block;margin-top:8px;font-size:12px;color:#2563eb;font-weight:600;text-decoration:none;"> Hapus pencarian</a>
@endif
</div>
</td></tr>
@endforelse
</tbody>
</table>
</div>
{{-- ── MOBILE CARD VIEW ── --}}
<div class="mob-card">
@forelse($users ?? [] as $u)
@php
$isMe = ($u->id === auth()->id());
$isActive = isset($u->is_active) ? (bool)$u->is_active : true;
$isAdmin = $u->role === 'admin';
$newRole = $isAdmin ? 'rt' : 'admin';
$newRoleLabel = $isAdmin ? 'RT' : 'Admin';
$initial = strtoupper(substr($u->name,0,1));
$avBg = $isAdmin
? 'background:linear-gradient(135deg,#10b981,#059669);'
: 'background:linear-gradient(135deg,#3b82f6,#2563eb);';
@endphp
<div class="mob-row" style="{{ !$isActive ? 'opacity:.5;' : '' }}">
<div style="display:flex;align-items:center;gap:10px;">
<div class="av-wrap">
<div class="av" style="{{ $avBg }}">{{ $initial }}</div>
@if($isActive)<div class="av-dot"></div>@endif
</div>
<div style="flex:1;min-width:0;">
<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap;">
<span class="mob-name">{{ $u->name }}</span>
@if($isMe)
<span style="font-size:9.5px;padding:1px 7px;border-radius:20px;background:#ede9fe;color:#6d28d9;border:1px solid #ddd6fe;font-weight:700;">Kamu</span>
@endif
</div>
<div class="mob-email">{{ $u->email }}</div>
</div>
</div>
<div class="mob-meta">
@if($isAdmin)
<span class="badge" style="background:#f0fdf4;color:#166534;border:1px solid #bbf7d0;"><span class="dot" style="background:#10b981;"></span>Admin</span>
@else
<span class="badge" style="background:#eff6ff;color:#1d4ed8;border:1px solid #bfdbfe;"><span class="dot" style="background:#3b82f6;"></span>RT</span>
@endif
@if($isActive)
<span class="badge" style="background:#f0fdf4;color:#166534;border:1px solid #bbf7d0;"><span class="dot" style="background:#10b981;"></span>Aktif</span>
@else
<span class="badge" style="background:#f3f4f6;color:#6b7280;border:1px solid #e5e7eb;"><span class="dot" style="background:#9ca3af;"></span>Nonaktif</span>
@endif
<span style="font-size:10.5px;color:#94a3b8;">ID #{{ $u->id }}</span>
</div>
<div class="mob-actions">
@if($isMe)
<span class="btn-a btn-locked">
<svg fill="none" stroke="currentColor" 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>
Terkunci
</span>
@else
<form method="POST" action="{{ route('admin.data-akun.role',$u->id) }}">
@csrf
<input type="hidden" name="role" value="{{ $newRole }}">
<button type="submit" class="btn-a btn-role"
onclick="return confirm('Ubah role {{ addslashes($u->name) }} menjadi {{ $newRoleLabel }}?')">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4"/></svg>
{{ $newRoleLabel }}
</button>
</form>
<form method="POST" action="{{ route('admin.data-akun.toggle-aktif',$u->id) }}">
@csrf
<button type="submit"
class="btn-a {{ $isActive ? 'btn-nonaktif' : 'btn-aktif' }}"
onclick="return confirm('{{ $isActive ? 'Nonaktifkan' : 'Aktifkan' }} akun {{ addslashes($u->name) }}?')">
{{ $isActive ? 'Nonaktifkan' : 'Aktifkan' }}
</button>
</form>
<form method="POST" action="{{ route('admin.data-akun.hapus',$u->id) }}"
onsubmit="return confirm('Yakin hapus akun {{ addslashes($u->name) }}?')">
@csrf @method('DELETE')
<button type="submit" class="btn-a 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>
</div>
@empty
<div style="padding:40px 16px;text-align:center;">
<p style="font-size:13px;font-weight:600;color:#6b7280;">Belum ada akun</p>
</div>
@endforelse
</div>
{{-- PAGINATION --}}
@if(isset($users) && method_exists($users,'links') && $users->hasPages())
<div style="padding:10px 16px;border-top:1px solid #f1f5f9;background:#fafafa;">
{{ $users->appends(request()->query())->links() }}
</div>
@endif
{{-- WATERMARK --}}
<div style="padding:12px 16px;border-top:1px solid #f1f5f9;display:flex;align-items:center;justify-content:center;gap:8px;">
<svg width="13" height="13" fill="#d1d5db" 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 style="font-size:11px;color:#d1d5db;font-weight:500;">SiBantuDes · Desa Ngerong</span>
</div>
</div>
<style>
@keyframes pulse{0%,100%{opacity:1;}50%{opacity:.5;}}
</style>
</x-app-layout>

View File

@ -0,0 +1,215 @@
<x-app-layout>
<x-slot name="header">
<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;">
<div>
<h2 style="font-size:16px;font-weight:800;color:#0f172a;letter-spacing:-.02em;">Arsip Data Warga</h2>
<p style="font-size:11px;color:#94a3b8;margin-top:2px;">Data warga yang telah diarsipkan berdasarkan tahun periode</p>
</div>
<a href="{{ route('admin.data-warga') }}" style="display:inline-flex;align-items:center;gap:6px;background:#fff;border:1.5px solid #e2e8f0;border-radius:10px;padding:7px 14px;font-size:12px;font-weight:600;color:#374151;text-decoration:none;white-space:nowrap;">
<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="M15 19l-7-7 7-7"/></svg>
Kembali ke Data Warga
</a>
</div>
</x-slot>
<style>
.dw-card{background:#fff;border-radius:16px;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;}
.fl-box{display:flex;align-items:center;gap:9px;border-radius:12px;padding:10px 14px;font-size:12px;font-weight:500;}
.sw{position:relative;}
.sw svg{position:absolute;left:11px;top:50%;transform:translateY(-50%);width:14px;height:14px;stroke:#9ca3af;pointer-events:none;}
.sw input{width:100%;padding:9px 12px 9px 34px;font-size:13px;color:#111827;background:#f9fafb;border:1.5px solid #e5e7eb;border-radius:11px;outline:none;transition:border-color .15s,box-shadow .15s;font-family:inherit;}
.sw input:focus{border-color:#3b82f6;background:#fff;box-shadow:0 0 0 3px rgba(59,130,246,.10);}
.sw input::placeholder{color:#9ca3af;}
.tbl-wrap{overflow-x:auto;-webkit-overflow-scrolling:touch;}
table{width:100%;border-collapse:collapse;min-width:700px;}
thead tr{background:#f8fafc;border-bottom:1.5px solid #f1f5f9;}
thead th{padding:9px 12px;text-align:left;font-size:9.5px;font-weight:700;color:#94a3b8;text-transform:uppercase;letter-spacing:.07em;white-space:nowrap;}
tbody tr{border-bottom:1px solid #f8fafc;transition:background .1s;}
tbody tr:hover{background:#f0f7ff;}
tbody tr:last-child{border-bottom:none;}
tbody td{padding:10px 12px;vertical-align:middle;}
.av{width:30px;height:30px;border-radius:9px;background:linear-gradient(135deg,#64748b,#475569);display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:800;color:#fff;flex-shrink:0;}
.nik-pill{font-family:monospace;font-size:10px;color:#6b7280;background:#f3f4f6;padding:2px 6px;border-radius:6px;white-space:nowrap;}
.rt-badge{display:inline-flex;align-items:center;padding:2px 7px;border-radius:6px;background:#eff6ff;color:#1d4ed8;font-size:10px;font-weight:700;border:1px solid #bfdbfe;margin-top:2px;}
.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;}
.stat-yes{background:#f0fdf4;color:#166534;border:1px solid #bbf7d0;}
.stat-no{background:#fff1f2;color:#9f1239;border:1px solid #fecdd3;}
.stat-pending{background:#fffbeb;color:#b45309;border:1px solid #fde68a;}
.prob-wrap{display:flex;align-items:center;gap:6px;}
.prob-bg{flex:1;height:5px;background:#f1f5f9;border-radius:99px;overflow:hidden;min-width:50px;}
.prob-bar{height:100%;border-radius:99px;}
.tab-tahun{display:inline-flex;align-items:center;padding:6px 14px;border-radius:20px;font-size:12px;font-weight:600;text-decoration:none;border:1.5px solid #e2e8f0;color:#64748b;background:#fff;transition:all .15s;white-space:nowrap;}
.tab-tahun:hover{background:#f1f5f9;}
.tab-tahun.active{background:#1e40af;color:#fff;border-color:#1e40af;}
.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;transition:filter .1s;font-family:inherit;}
.btn-act:hover{filter:brightness(.92);}
.btn-act svg{width:11px;height:11px;flex-shrink:0;}
</style>
@if(session('success'))
<div class="fl-box" style="background:#f0fdf4;border:1.5px solid #bbf7d0;color:#166534;margin-bottom:10px;">
<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="fl-box" style="background:#fff1f2;border:1.5px solid #fecdd3;color:#9f1239;margin-bottom:10px;">
<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
{{-- FILTER TAHUN --}}
<div class="dw-card" style="padding:14px 16px;margin-bottom:12px;">
<div style="display:flex;align-items:center;gap:7px;margin-bottom:12px;">
<div style="width:3px;height:14px;background:#475569;border-radius:4px;"></div>
<h3 style="font-size:12px;font-weight:700;color:#374151;">Filter Tahun</h3>
</div>
@if($daftarTahun->isEmpty())
<p style="font-size:13px;color:#9ca3af;">Belum ada data yang diarsipkan.</p>
@else
<div style="display:flex;gap:8px;flex-wrap:wrap;">
@foreach($daftarTahun as $th)
<a href="{{ route('admin.data-warga.arsip', ['tahun' => $th, 'q' => request('q')]) }}"
class="tab-tahun {{ $tahun == $th ? 'active' : '' }}">
{{ $th }}
</a>
@endforeach
</div>
@endif
{{-- Search --}}
<form method="GET" action="{{ route('admin.data-warga.arsip') }}" style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:12px;padding-top:12px;border-top:1px solid #f1f5f9;">
<input type="hidden" name="tahun" value="{{ $tahun }}">
<div class="sw" style="flex:1;min-width:200px;">
<svg fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
<input type="text" name="q" value="{{ request('q') }}" placeholder="Cari NIK atau Nama...">
</div>
<button type="submit" style="display:inline-flex;align-items:center;gap:6px;padding:9px 16px;background:#475569;color:#fff;font-size:12px;font-weight:700;border:none;border-radius:11px;cursor:pointer;white-space:nowrap;font-family:inherit;">
<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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
Cari
</button>
@if(request()->filled('q'))
<a href="{{ route('admin.data-warga.arsip', ['tahun' => $tahun]) }}" style="display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;background:#f3f4f6;border-radius:10px;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="M6 18L18 6M6 6l12 12"/></svg>
</a>
@endif
</form>
</div>
{{-- TABLE ARSIP --}}
<div class="dw-card">
<div style="padding:10px 16px;border-bottom:1px solid #f1f5f9;background:#fafafa;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:6px;">
<div style="display:flex;align-items:center;gap:7px;">
<div style="width:3px;height:15px;background:#475569;border-radius:4px;"></div>
<span style="font-size:12.5px;font-weight:700;color:#374151;">
Arsip Tahun <span style="color:#1e40af;">{{ $tahun }}</span>
@if(isset($arsips) && method_exists($arsips,'total'))
<span style="color:#475569;">{{ $arsips->total() }}</span><span style="color:#9ca3af;font-weight:400;"> data</span>
@endif
</span>
</div>
@if(isset($arsips) && method_exists($arsips,'currentPage') && $arsips->lastPage() > 1)
<span style="font-size:11px;color:#9ca3af;">Hal. {{ $arsips->currentPage() }} / {{ $arsips->lastPage() }}</span>
@endif
</div>
<div class="tbl-wrap">
<table>
<thead>
<tr>
<th>NIK</th>
<th>Nama / Pekerjaan</th>
<th>Dusun · RT</th>
<th>Penghasilan</th>
<th>Probabilitas</th>
<th>Hasil</th>
<th style="text-align:right;">Aksi</th>
</tr>
</thead>
<tbody>
@forelse($arsips ?? [] as $warga)
@php
$prob = optional($warga->prediksiKelayakan)->probability ?? null;
$probPct = $prob !== null ? ($prob <= 1 ? $prob * 100 : $prob) : null;
$sc = $probPct !== null ? ($probPct >= 70 ? '#10b981' : ($probPct >= 40 ? '#f59e0b' : '#f43f5e')) : '#e5e7eb';
@endphp
<tr>
<td><span class="nik-pill">{{ $warga->nik }}</span></td>
<td>
<div style="display:flex;align-items:center;gap:8px;">
<div class="av">{{ strtoupper(substr($warga->nama_lengkap,0,1)) }}</div>
<div style="min-width:0;">
<div style="font-size:12px;font-weight:600;color:#111827;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:140px;">{{ $warga->nama_lengkap }}</div>
<div style="font-size:10.5px;color:#9ca3af;margin-top:1px;">{{ $warga->pekerjaan ?? '-' }}</div>
</div>
</div>
</td>
<td>
<div style="font-size:11.5px;font-weight:600;color:#374151;white-space:nowrap;">{{ $warga->rt->dusun->nama_dusun ?? '-' }}</div>
<span class="rt-badge">RT {{ str_pad($warga->rt->nomor_rt ?? '-', 3, '0', STR_PAD_LEFT) }}</span>
</td>
<td style="font-size:12px;font-weight:600;color:#374151;white-space:nowrap;">
Rp {{ number_format($warga->penghasilan ?? 0, 0, ',', '.') }}
</td>
<td>
@if($probPct !== null)
<div class="prob-wrap">
<div class="prob-bg"><div class="prob-bar" style="width:{{ min($probPct,100) }}%;background:{{ $sc }};"></div></div>
<span style="font-size:11px;font-weight:700;color:{{ $sc }};white-space:nowrap;">{{ number_format($probPct,1) }}%</span>
</div>
@else
<span style="font-size:12px;color:#d1d5db;"></span>
@endif
</td>
<td>
@if(($warga->status_verifikasi ?? '') === 'disetujui')
<span class="badge stat-yes"><span class="dot" style="background:#22c55e;"></span>Diterima</span>
@elseif(($warga->status_verifikasi ?? '') === 'ditolak')
<span class="badge stat-no"><span class="dot" style="background:#f43f5e;"></span>Ditolak</span>
@else
<span class="badge stat-pending"><span class="dot" style="background:#f59e0b;"></span>Pending</span>
@endif
</td>
<td>
<div style="display:flex;align-items:center;justify-content:flex-end;gap:4px;">
{{-- Aktifkan Kembali dari Arsip --}}
<form method="POST" action="{{ route('admin.data-warga.aktifkan-kembali', $warga->id) }}" onsubmit="return confirm('Keluarkan {{ addslashes($warga->nama_lengkap) }} dari arsip dan aktifkan kembali?')">
@csrf
<button type="submit" class="btn-act" style="background:#fffbeb;color:#b45309;">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
Aktifkan Kembali
</button>
</form>
</div>
</td>
</tr>
@empty
<tr><td colspan="7">
<div style="padding:48px 16px;text-align:center;">
<div style="width:44px;height:44px;background:#f3f4f6;border-radius:50%;display:flex;align-items:center;justify-content:center;margin:0 auto 10px;">
<svg width="20" height="20" fill="none" stroke="#d1d5db" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4"/></svg>
</div>
<p style="font-size:13px;font-weight:600;color:#6b7280;margin-bottom:4px;">Tidak ada data arsip untuk tahun {{ $tahun }}</p>
@if(request()->filled('q'))
<p style="font-size:11.5px;color:#9ca3af;">Tidak ada hasil untuk "<strong>{{ request('q') }}</strong>"</p>
<a href="{{ route('admin.data-warga.arsip', ['tahun' => $tahun]) }}" style="display:inline-block;margin-top:8px;font-size:12px;color:#2563eb;font-weight:600;text-decoration:none;"> Hapus pencarian</a>
@endif
</div>
</td></tr>
@endforelse
</tbody>
</table>
</div>
@if(isset($arsips) && method_exists($arsips,'links') && $arsips->hasPages())
<div style="padding:10px 16px;border-top:1px solid #f1f5f9;background:#fafafa;">
{{ $arsips->links() }}
</div>
@endif
</div>
</x-app-layout>

File diff suppressed because it is too large Load Diff

View File

@ -1,41 +1,308 @@
<x-app-layout>
<x-slot name="header">
<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 tertinggi per dusun.
</p>
<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;">
<div>
<h2 style="font-size:16px;font-weight:800;color:#0f172a;letter-spacing:-.02em;">Filterisasi</h2>
<p style="font-size:11px;color:#94a3b8;margin-top:2px;">Penetapan penerima BLT-DD berdasarkan probabilitas tertinggi per dusun</p>
</div>
</div>
</x-slot>
<div class="space-y-4">
<style>
.fi-card{background:#fff;border-radius:16px;border:1.5px solid #f1f5f9;box-shadow:0 1px 4px rgba(0,0,0,.04);overflow:hidden;}
.fl-box{display:flex;align-items:center;gap:9px;border-radius:12px;padding:10px 14px;font-size:12px;font-weight:500;margin-bottom:10px;}
.fl-box svg{width:14px;height:14px;flex-shrink:0;}
.fi-label{display:block;font-size:10.5px;font-weight:600;color:#64748b;margin-bottom:5px;}
.fi-select,.fi-input{width:100%;padding:9px 12px;font-size:13px;color:#111827;background:#f9fafb;border:1.5px solid #e5e7eb;border-radius:11px;outline:none;font-family:inherit;transition:border-color .15s;}
.fi-select:focus,.fi-input:focus{border-color:#3b82f6;background:#fff;}
.btn-fi{display:inline-flex;align-items:center;gap:6px;padding:9px 18px;font-size:12.5px;font-weight:700;border:none;border-radius:11px;cursor:pointer;white-space:nowrap;font-family:inherit;transition:filter .1s;}
.btn-fi:hover{filter:brightness(.93);}
.btn-tampil{background:#2563eb;color:#fff;box-shadow:0 2px 8px rgba(37,99,235,.2);}
.btn-reset-fi{background:#f1f5f9;color:#475569;}
.btn-tetapkan{background:#059669;color:#fff;box-shadow:0 2px 8px rgba(5,150,105,.2);}
.tbl-wrap{overflow-x:auto;-webkit-overflow-scrolling:touch;}
table{width:100%;border-collapse:collapse;min-width:580px;}
thead tr{background:#f8fafc;border-bottom:1.5px solid #f1f5f9;}
thead th{padding:9px 12px;text-align:left;font-size:9.5px;font-weight:700;color:#94a3b8;text-transform:uppercase;letter-spacing:.07em;white-space:nowrap;}
tbody tr{border-bottom:1px solid #f8fafc;transition:background .1s;}
tbody tr:hover{background:#f0f7ff;}
tbody tr:last-child{border-bottom:none;}
tbody td{padding:10px 12px;vertical-align:middle;font-size:12.5px;}
.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;}
.prob-wrap{display:flex;align-items:center;gap:6px;}
.prob-bg{flex:1;height:5px;background:#f1f5f9;border-radius:99px;overflow:hidden;min-width:50px;}
.prob-bar{height:100%;border-radius:99px;}
.rank-badge{width:26px;height:26px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:800;}
.note-box{background:#fffbeb;border:1.5px solid #fde68a;border-radius:14px;padding:14px 16px;font-size:12px;color:#92400e;line-height:1.8;}
.note-box b{font-weight:800;}
.fi-grid{display:grid;grid-template-columns:1fr 1fr auto;gap:10px;align-items:end;}
.fi-actions{display:flex;gap:8px;flex-wrap:wrap;align-items:center;}
.mob-fi{display:none;flex-direction:column;gap:8px;padding:12px 14px;}
.mob-fi-row{background:#f8fafc;border:1.5px solid #f1f5f9;border-radius:12px;padding:11px 13px;}
@media(max-width:768px){
.fi-grid{grid-template-columns:1fr 1fr;gap:8px;}
.fi-grid > *:last-child{grid-column:span 2;}
}
@media(max-width:560px){
.fi-grid{grid-template-columns:1fr;}
.fi-grid > *:last-child{grid-column:span 1;}
.tbl-wrap table{display:none;}
.mob-fi{display:flex;}
}
@if(session('success'))
<div class="rounded-xl border border-green-200 bg-green-50 px-4 py-3 text-sm text-green-700">
{{ session('success') }}
@keyframes fi-spin { to { transform: rotate(360deg); } }
.fi-spinner {
display:none;
width:13px;height:13px;
border:2px solid rgba(255,255,255,.35);
border-top-color:#fff;
border-radius:50%;
animation:fi-spin .6s linear infinite;
flex-shrink:0;
}
.fi-spinner-dark {
border-color:rgba(71,85,105,.3);
border-top-color:#475569;
}
/* Overlay loading saat proses tetapkan */
#fi-overlay {
display:none;
position:fixed;inset:0;z-index:9999;
background:rgba(15,23,42,.45);
backdrop-filter:blur(2px);
align-items:center;justify-content:center;
}
#fi-overlay.show { display:flex; }
.fi-overlay-box {
background:#fff;border-radius:20px;
padding:28px 32px;
display:flex;flex-direction:column;align-items:center;gap:16px;
box-shadow:0 20px 60px rgba(0,0,0,.2);
min-width:240px;text-align:center;
}
.fi-overlay-ring {
width:48px;height:48px;
border:4px solid #e5e7eb;
border-top-color:#059669;
border-radius:50%;
animation:fi-spin .7s linear infinite;
}
.fi-overlay-ring.blue { border-top-color:#2563eb; }
.fi-overlay-title { font-size:14px;font-weight:700;color:#0f172a; }
.fi-overlay-sub { font-size:11.5px;color:#94a3b8;margin-top:-8px; }
/* Dana Cair Box */
.dana-box{background:linear-gradient(135deg,#eff6ff,#dbeafe);border:1.5px solid #bfdbfe;border-radius:14px;padding:14px 16px;margin-bottom:12px;}
.dana-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px;align-items:end;}
.dana-stat-row{display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin-top:12px;}
.dana-stat{background:#fff;border-radius:10px;padding:10px 13px;border:1.5px solid #e0eaff;}
.dana-stat.green{border-color:#a7f3d0;background:#f0fdf4;}
.dana-stat.red{border-color:#fecdd3;background:#fff1f2;}
.dana-stat.amber{border-color:#fde68a;background:#fffbeb;}
.dana-stat-label{font-size:9.5px;font-weight:700;color:#64748b;text-transform:uppercase;letter-spacing:.06em;margin-bottom:3px;}
.dana-stat-val{font-size:15px;font-weight:800;color:#1e40af;}
.dana-stat-val.green{color:#059669;}
.dana-stat-val.red{color:#e11d48;}
.dana-stat-val.amber{color:#b45309;}
.dana-stat-sub{font-size:9.5px;color:#94a3b8;margin-top:1px;}
.kuota-rec{background:#fff;border:1.5px solid #a7f3d0;border-radius:10px;padding:8px 12px;font-size:11.5px;color:#065f46;font-weight:600;display:flex;align-items:center;gap:6px;}
.periode-next{background:#fffbeb;border:1.5px solid #fde68a;border-radius:10px;padding:10px 13px;margin-top:10px;font-size:11.5px;color:#92400e;display:none;line-height:1.7;}
.periode-next b{font-weight:800;}
@media(max-width:640px){.dana-grid{grid-template-columns:1fr;}.dana-stat-row{grid-template-columns:1fr 1fr;}.dana-stat-row>*:last-child{grid-column:span 2;}}
@media(max-width:420px){.dana-stat-row{grid-template-columns:1fr;}}
/* Row warga sudah terpilih (dikunci) */
tr.row-locked { opacity:.55; }
tr.row-locked td { pointer-events:none; }
</style>
{{-- OVERLAY LOADING --}}
<div id="fi-overlay">
<div class="fi-overlay-box">
<div class="fi-overlay-ring" id="fi-overlay-ring"></div>
<div class="fi-overlay-title" id="fi-overlay-title">Memuat data...</div>
<div class="fi-overlay-sub" id="fi-overlay-sub">Mohon tunggu sebentar</div>
</div>
</div>
{{-- FLASH --}}
@if(session('success'))
<div class="fl-box" 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="fl-box" 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
{{-- ══════════════════════════════════════════════════════
PANEL DANA & PERIODE BLT-DD
══════════════════════════════════════════════════════ --}}
<div class="dana-box" style="margin-bottom:12px;">
<div style="display:flex;align-items:center;gap:7px;margin-bottom:11px;">
<div style="width:3px;height:14px;background:#2563eb;border-radius:4px;"></div>
<h3 style="font-size:12px;font-weight:700;color:#1e40af;">Kalkulator Dana BLT-DD</h3>
<span style="margin-left:auto;font-size:10px;font-weight:600;color:#3b82f6;background:#dbeafe;padding:2px 8px;border-radius:20px;">Rp 300.000 / orang / bulan</span>
</div>
<div class="dana-grid">
<div>
<label class="fi-label" style="color:#1e40af;">Total Dana Cair (Rp)</label>
<input type="text" id="inputDana" inputmode="numeric"
placeholder="Contoh: 300.000.000"
value="{{ $danaSisaTerakhir ? number_format($danaSisaTerakhir,0,',','.') : '' }}"
class="fi-input"
style="border-color:#bfdbfe;background:#fff;"
oninput="formatDana(this)">
@if($danaSisaTerakhir)
<div style="font-size:9.5px;color:#059669;margin-top:3px;font-weight:600;">
Otomatis diisi dari sisa periode {{ $periodeTerakhir->periode }}
</div>
@else
<div style="font-size:9.5px;color:#64748b;margin-top:3px;">Masukkan total dana yang dicairkan pemerintah</div>
@endif
</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') }}
<div>
<label class="fi-label" style="color:#1e40af;">Penerima Bulan Ini (orang)</label>
<div id="penerimaOtomatis" class="fi-input" style="border-color:#bfdbfe;background:#f0f9ff;display:flex;align-items:center;font-weight:800;color:#1e40af;font-size:16px;"></div>
<div style="font-size:9.5px;color:#64748b;margin-top:3px;" id="penerimaOtomatisSub">Otomatis: Dana ÷ {{ max(1, 13 - $nextPeriodeNum) }} bulan ÷ Rp 300.000</div>
</div>
@endif
</div>
<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</h3>
{{-- Stat row --}}
<div class="dana-stat-row" id="danaStatRow" style="display:none;">
<div class="dana-stat red">
<div class="dana-stat-label">Dana Terpakai</div>
<div class="dana-stat-val red" id="danaTermakai"></div>
<div class="dana-stat-sub" id="danaTermakaiSub"></div>
</div>
<div class="dana-stat green">
<div class="dana-stat-label">Sisa Dana</div>
<div class="dana-stat-val green" id="danaSisa"></div>
<div class="dana-stat-sub" id="danaSisaSub"></div>
</div>
<div class="dana-stat amber">
<div class="dana-stat-label">Sisa untuk Periode Berikutnya</div>
<div class="dana-stat-val amber" id="estimasiPeriode"></div>
<div class="dana-stat-sub" id="estimasiPeriodeSub"></div>
</div>
</div>
<form method="GET" action="{{ route('admin.filterisasi') }}" class="grid grid-cols-1 md:grid-cols-4 gap-3">
{{-- Form simpan periode --}}
<div id="formSimpanWrap" style="display:none;margin-top:12px;background:#fff;border:1.5px solid #bfdbfe;border-radius:12px;padding:12px 14px;">
<div style="font-size:11px;font-weight:700;color:#1e40af;margin-bottom:9px;">Simpan ke Riwayat Periode</div>
<form method="POST" action="{{ route('admin.filterisasi.simpanPeriode') }}"
style="display:grid;grid-template-columns:1fr 1fr auto;gap:9px;align-items:end;">
@csrf
<input type="hidden" name="dana_awal" id="hiddenDanaAwal">
<input type="hidden" name="jumlah_penerima" id="hiddenPenerima">
<div>
<label class="text-xs text-gray-500">Dusun</label>
<select name="dusun_id" class="mt-1 w-full border border-gray-200 rounded-xl bg-gray-50 px-3 py-2 text-sm">
<label class="fi-label">Nama Periode</label>
<input type="text" name="periode" value="Bulan {{ $nextPeriodeNum }}"
class="fi-input" style="font-size:12px;" placeholder="Misal: Bulan 1">
</div>
<div>
<label class="fi-label">Keterangan (opsional)</label>
<input type="text" name="keterangan" class="fi-input" style="font-size:12px;"
placeholder="Misal: Triwulan I 2026">
</div>
<button type="submit" class="btn-fi btn-tetapkan" style="padding:9px 14px;"
onclick="return syncHidden()">
<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="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4"/></svg>
Simpan
</button>
</form>
</div>
{{-- Rekomendasi per dusun --}}
<div id="rekomendasiBox" style="display:none;margin-top:12px;">
<div style="font-size:10.5px;font-weight:700;color:#1e40af;margin-bottom:7px;">
Rekomendasi Kuota per Dusun (proporsional berdasarkan jumlah kandidat)
</div>
<div id="rekomendasiList" style="display:flex;flex-wrap:wrap;gap:7px;"></div>
<div style="font-size:9.5px;color:#64748b;margin-top:7px;">* Klik rekomendasi dusun untuk langsung isi kuota di form filter bawah</div>
</div>
</div>
{{-- RIWAYAT PERIODE DANA --}}
@if($periodeDana->isNotEmpty())
<div class="fi-card" style="margin-bottom:12px;">
<div style="padding:10px 16px;border-bottom:1px solid #f1f5f9;background:#fafafa;display:flex;align-items:center;gap:7px;">
<div style="width:3px;height:15px;background:#059669;border-radius:4px;"></div>
<span style="font-size:12.5px;font-weight:700;color:#374151;">Riwayat Periode Dana BLT-DD</span>
<span style="margin-left:auto;font-size:11px;color:#059669;background:#f0fdf4;padding:2px 9px;border-radius:20px;font-weight:600;border:1px solid #bbf7d0;">
{{ $periodeDana->count() }} periode
</span>
</div>
<div class="tbl-wrap">
<table>
<thead>
<tr>
<th>Periode</th>
<th>Dana Awal</th>
<th>Penerima</th>
<th>Dana Terpakai</th>
<th>Sisa Dana</th>
<th>Keterangan</th>
<th>Aksi</th>
</tr>
</thead>
<tbody>
@foreach($periodeDana as $p)
<tr>
<td><span style="font-size:12px;font-weight:700;color:#0f172a;">{{ $p->periode }}</span></td>
<td style="font-size:12px;color:#374151;">Rp {{ number_format($p->dana_awal,0,',','.') }}</td>
<td>
<span class="badge" style="background:#dbeafe;color:#1e40af;border:1px solid #bfdbfe;">
{{ $p->jumlah_penerima }} orang
</span>
</td>
<td style="font-size:12px;color:#e11d48;font-weight:600;">Rp {{ number_format($p->dana_terpakai,0,',','.') }}</td>
<td style="font-size:12px;color:#059669;font-weight:700;">Rp {{ number_format($p->dana_sisa,0,',','.') }}</td>
<td style="font-size:11.5px;color:#64748b;">{{ $p->keterangan ?? '-' }}</td>
<td>
<form method="POST" action="{{ route('admin.filterisasi.hapusPeriode', $p->id) }}" style="display:inline;">
@csrf
@method('DELETE')
<button type="submit" class="btn-fi btn-reset-fi" style="padding:5px 10px;font-size:11px;"
onclick="return confirm('Hapus data periode ini?')">
<svg width="11" height="11" 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>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
@endif
{{-- FILTER FORM --}}
<div class="fi-card" style="padding:14px 16px;margin-bottom:12px;">
<div style="display:flex;align-items:center;gap:7px;margin-bottom:12px;">
<div style="width:3px;height:14px;background:#2563eb;border-radius:4px;"></div>
<h3 style="font-size:12px;font-weight:700;color:#374151;">Pilih Dusun & Kuota</h3>
</div>
<form method="GET" action="{{ route('admin.filterisasi') }}" id="formTampil">
<div class="fi-grid">
<div>
<label class="fi-label">Dusun</label>
<select name="dusun_id" class="fi-select" id="selectDusun">
<option value="0">-- pilih dusun --</option>
@foreach($dusuns as $d)
<option value="{{ $d->id }}" {{ (int)($dusunId ?? 0) === $d->id ? 'selected' : '' }}>
<option value="{{ $d->id }}"
data-nama="{{ $d->nama_dusun }}"
{{ (int)($dusunId??0)===$d->id?'selected':'' }}>
{{ $d->nama_dusun }}
</option>
@endforeach
@ -43,137 +310,510 @@
</div>
<div>
<label class="text-xs text-gray-500">Kuota per dusun</label>
<input type="number" name="kuota" min="1" max="100" value="{{ (int)($kuota ?? 7) }}"
class="mt-1 w-full border border-gray-200 rounded-xl bg-gray-50 px-3 py-2 text-sm" />
<label class="fi-label">Kuota per Dusun</label>
<input type="number" name="kuota" id="inputKuota" min="1" max="100"
value="{{ (int)($kuota??7) }}" class="fi-input">
</div>
<div class="flex items-end gap-2 md:col-span-2">
<button type="submit"
class="px-5 py-2 text-sm font-semibold bg-blue-600 text-white rounded-xl hover:bg-blue-700 transition">
Tampilkan
<div class="fi-actions">
{{-- Tombol Tampilkan --}}
<button type="submit" class="btn-fi btn-tampil" id="btnTampil">
<span class="fi-spinner" id="spinnerTampil"></span>
<svg id="iconTampil" width="13" height="13" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2a1 1 0 01-.293.707L13 13.414V19a1 1 0 01-.553.894l-4 2A1 1 0 017 21v-7.586L3.293 6.707A1 1 0 013 6V4z"/></svg>
<span id="textTampil">Tampilkan</span>
</button>
@if(!empty($dusunId))
<form method="POST" action="{{ route('admin.filterisasi.reset') }}">
<form method="POST" action="{{ route('admin.filterisasi.reset') }}" style="display:contents;">
@csrf
<input type="hidden" name="dusun_id" value="{{ $dusunId }}">
<button type="submit"
onclick="return confirm('Reset hasil filterisasi dusun ini?')"
class="px-4 py-2 text-sm font-semibold bg-gray-100 text-gray-700 rounded-xl hover:bg-gray-200 transition">
<button type="submit" class="btn-fi btn-reset-fi"
onclick="return confirm('Reset hasil filterisasi dusun ini?')">
<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="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
Reset
</button>
</form>
<form method="POST" action="{{ route('admin.filterisasi.tetapkan') }}" class="ml-auto">
<form method="POST" action="{{ route('admin.filterisasi.tetapkan') }}" style="display:contents;" id="formTetapkan">
@csrf
<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 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
<input type="hidden" name="kuota" value="{{ (int)($kuota??7) }}">
<button type="submit" class="btn-fi btn-tetapkan" id="btnTetapkan">
<span class="fi-spinner" id="spinnerTetapkan"></span>
<svg id="iconTetapkan" width="13" height="13" 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>
<span id="textTetapkan">Tetapkan {{ (int)($kuota??7) }} Teratas</span>
</button>
</form>
@endif
</div>
</form>
</div>
<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 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 Sedang Divalidasi / Sudah Diproses</span>
</div>
</div>
</form>
</div>
<div class="overflow-x-auto">
<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">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 $index => $w)
@php
$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 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>
@else
<span class="px-2 py-1 rounded-full text-xs bg-gray-100 text-gray-600">Belum</span>
@endif
</td>
</tr>
@empty
<tr>
<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
</tbody>
</table>
</div>
@if(isset($candidates) && method_exists($candidates, 'links') && $candidates->hasPages())
<div class="px-5 py-3 border-t border-gray-100 bg-gray-50">
{{ $candidates->appends(request()->query())->links() }}
</div>
{{-- TABLE CARD --}}
<div class="fi-card" style="margin-bottom:12px;">
<div style="padding:10px 16px;border-bottom:1px solid #f1f5f9;background:#fafafa;display:flex;align-items:center;gap:7px;">
<div style="width:3px;height:15px;background:#2563eb;border-radius:4px;"></div>
<span style="font-size:12.5px;font-weight:700;color:#374151;">Kandidat Sedang Divalidasi / Sudah Diproses</span>
@if(isset($candidates) && method_exists($candidates, 'total') && $candidates->total() > 0)
@php
$totalKandidatAktif = $candidates->total() - (isset($pickedNiks) ? $pickedNiks->count() : 0);
@endphp
<span style="margin-left:auto;font-size:11px;color:#64748b;background:#f1f5f9;padding:2px 9px;border-radius:20px;font-weight:600;">
{{ $candidates->total() }} kandidat
@if(isset($pickedNiks) && $pickedNiks->count() > 0)
<span style="color:#059669;">({{ $pickedNiks->count() }} sudah terpilih)</span>
@endif
</span>
@endif
</div>
<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>
{{-- INFO warga sudah terpilih dikunci --}}
@if(isset($pickedNiks) && $pickedNiks->count() > 0)
<div style="padding:8px 16px;background:#f0fdf4;border-bottom:1px solid #bbf7d0;font-size:11.5px;color:#166534;display:flex;align-items:center;gap:6px;">
<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="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>
<span><b>{{ $pickedNiks->count() }} warga</b> sudah ditetapkan sebagai penerima tidak ikut seleksi ulang (ditampilkan redup)</span>
</div>
@endif
{{-- DESKTOP TABLE --}}
<div class="tbl-wrap">
<table>
<thead>
<tr>
<th style="width:52px;">Rank</th>
<th>Nama</th>
<th>NIK</th>
<th>Dusun</th>
<th style="min-width:130px;">Probabilitas</th>
<th>Status</th>
<th>Final</th>
</tr>
</thead>
<tbody>
@if(!isset($dusunId) || (int)$dusunId === 0)
<tr><td colspan="7">
<div style="padding:48px 16px;text-align:center;">
<div style="width:44px;height:44px;background:#f3f4f6;border-radius:50%;display:flex;align-items:center;justify-content:center;margin:0 auto 10px;">
<svg width="22" height="22" fill="none" stroke="#d1d5db" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2a1 1 0 01-.293.707L13 13.414V19a1 1 0 01-.553.894l-4 2A1 1 0 017 21v-7.586L3.293 6.707A1 1 0 013 6V4z"/></svg>
</div>
<p style="font-size:13px;font-weight:600;color:#6b7280;">Pilih dusun untuk menampilkan kandidat</p>
</div>
</td></tr>
@elseif($candidates->isEmpty())
<tr><td colspan="7">
<div style="padding:48px 16px;text-align:center;">
<div style="width:44px;height:44px;background:#fff7ed;border-radius:50%;display:flex;align-items:center;justify-content:middle;margin:0 auto 10px;border:1.5px solid #fed7aa;">
<svg width="22" height="22" fill="none" stroke="#f97316" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 9v2m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
</div>
<p style="font-size:13px;font-weight:600;color:#6b7280;">Tidak ada kandidat dengan status <strong>sedang_validasi</strong> atau <strong>selesai</strong> di dusun ini</p>
</div>
</td></tr>
@else
@foreach($candidates as $index => $w)
@php
$prob = $w->prob ?? optional($w->prediksiKelayakan)->probability;
$probPct = $prob !== null ? ($prob<=1 ? $prob*100 : $prob) : null;
$sc = $probPct !== null ? ($probPct>=70?'#10b981':($probPct>=40?'#f59e0b':'#f43f5e')) : '#e5e7eb';
$picked = isset($pickedNiks) ? $pickedNiks->contains($w->nik) : false;
$rank = (($candidates->currentPage()-1)*$candidates->perPage())+$index+1;
$rk = ['linear-gradient(135deg,#f59e0b,#d97706)','linear-gradient(135deg,#94a3b8,#64748b)','linear-gradient(135deg,#f97316,#ea580c)'];
$rbg = $rank<=3 ? $rk[$rank-1] : '#f1f5f9';
$rtc = $rank<=3 ? '#fff' : '#64748b';
$svMap = ['pending'=>['#fffbeb','#b45309','#fde68a','Pending'],'disetujui'=>['#f0fdf4','#166534','#bbf7d0','Disetujui'],'ditolak'=>['#fff1f2','#9f1239','#fecdd3','Ditolak']];
$sv = $svMap[$w->status_verifikasi??''] ?? ['#f3f4f6','#6b7280','#e5e7eb',ucfirst($w->status_verifikasi??'-')];
@endphp
<tr class="{{ $picked ? 'row-locked' : '' }}" style="{{ $picked ? 'background:#f0fdf4;' : '' }}">
<td>
<div style="display:flex;align-items:center;gap:5px;">
<div class="rank-badge" style="background:{{ $rbg }};color:{{ $rtc }};">{{ $rank }}</div>
@if($picked)
<svg width="11" height="11" fill="none" stroke="#059669" viewBox="0 0 24 24" title="Sudah terpilih"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" 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>
@endif
</div>
</td>
<td>
<div style="display:flex;align-items:center;gap:8px;">
<div style="width:28px;height:28px;border-radius:8px;background:{{ $picked ? 'linear-gradient(135deg,#059669,#047857)' : 'linear-gradient(135deg,#3b82f6,#2563eb)' }};display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:800;color:#fff;flex-shrink:0;">
{{ strtoupper(substr($w->nama_lengkap,0,1)) }}
</div>
<div>
<span style="font-size:12px;font-weight:600;color:#0f172a;">{{ $w->nama_lengkap }}</span>
@if($picked)
<div style="font-size:9.5px;color:#059669;font-weight:600;">Sudah ditetapkan</div>
@endif
</div>
</div>
</td>
<td>
<span style="font-family:monospace;font-size:10.5px;color:#6b7280;background:#f3f4f6;padding:2px 7px;border-radius:6px;">{{ $w->nik }}</span>
</td>
<td style="font-size:12px;color:#374151;">
{{ optional(optional($w->rt)->dusun)->nama_dusun ?? '-' }}
</td>
<td>
@if($probPct !== null)
<div class="prob-wrap">
<div class="prob-bg">
<div class="prob-bar" style="width:{{ min($probPct,100) }}%;background:{{ $sc }};"></div>
</div>
<span style="font-size:11.5px;font-weight:700;color:{{ $sc }};white-space:nowrap;">{{ number_format((float)$probPct,1) }}%</span>
</div>
@else
<span style="color:#d1d5db;"></span>
@endif
</td>
<td>
<span class="badge" style="background:{{ $sv[0] }};color:{{ $sv[1] }};border:1px solid {{ $sv[2] }};">
<span class="dot" style="background:{{ $sv[1] }};"></span>{{ $sv[3] }}
</span>
</td>
<td>
@if($picked)
<span class="badge" style="background:#f0fdf4;color:#166534;border:1px solid #bbf7d0;">
<span class="dot" style="background:#10b981;"></span>Terpilih
</span>
@else
<span class="badge" style="background:#f3f4f6;color:#6b7280;border:1px solid #e5e7eb;">
<span class="dot" style="background:#9ca3af;"></span>Belum
</span>
@endif
</td>
</tr>
@endforeach
@endif
</tbody>
</table>
</div>
{{-- MOBILE CARD VIEW --}}
<div class="mob-fi">
@if(!isset($dusunId) || (int)$dusunId === 0)
<div style="padding:32px 16px;text-align:center;">
<p style="font-size:13px;font-weight:600;color:#6b7280;">Pilih dusun untuk menampilkan kandidat</p>
</div>
@elseif($candidates->isEmpty())
<div style="padding:32px 16px;text-align:center;">
<p style="font-size:13px;font-weight:600;color:#6b7280;">Tidak ada kandidat dengan status <strong>sedang_validasi</strong> atau <strong>selesai</strong> di dusun ini</p>
</div>
@else
@foreach($candidates as $index => $w)
@php
$prob = $w->prob ?? optional($w->prediksiKelayakan)->probability;
$probPct = $prob !== null ? ($prob<=1 ? $prob*100 : $prob) : null;
$sc = $probPct!==null ? ($probPct>=70?'#10b981':($probPct>=40?'#f59e0b':'#f43f5e')) : '#e5e7eb';
$picked = isset($pickedNiks) ? $pickedNiks->contains($w->nik) : false;
$rank = (($candidates->currentPage()-1)*$candidates->perPage())+$index+1;
$rk = ['linear-gradient(135deg,#f59e0b,#d97706)','linear-gradient(135deg,#94a3b8,#64748b)','linear-gradient(135deg,#f97316,#ea580c)'];
$rbg = $rank<=3 ? $rk[$rank-1] : '#f1f5f9';
$rtc = $rank<=3 ? '#fff' : '#64748b';
$svMap = ['pending'=>['#fffbeb','#b45309','Pending'],'disetujui'=>['#f0fdf4','#166534','Disetujui'],'ditolak'=>['#fff1f2','#9f1239','Ditolak']];
$sv = $svMap[$w->status_verifikasi??''] ?? ['#f3f4f6','#6b7280',ucfirst($w->status_verifikasi??'-')];
@endphp
<div class="mob-fi-row {{ $picked ? 'row-locked' : '' }}" style="{{ $picked ? 'background:#f0fdf4;border-color:#bbf7d0;opacity:.6;' : '' }}">
<div style="display:flex;align-items:center;gap:10px;">
<div class="rank-badge" style="background:{{ $rbg }};color:{{ $rtc }};flex-shrink:0;">{{ $rank }}</div>
<div style="flex:1;min-width:0;">
<div style="font-size:13px;font-weight:700;color:#0f172a;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">{{ $w->nama_lengkap }}</div>
<div style="font-size:10.5px;color:#64748b;margin-top:1px;font-family:monospace;">{{ $w->nik }}</div>
@if($picked)
<div style="font-size:9.5px;color:#059669;font-weight:600;">🔒 Sudah ditetapkan</div>
@endif
</div>
@if($picked)
<span class="badge" style="background:#f0fdf4;color:#166534;border:1px solid #bbf7d0;flex-shrink:0;"> Terpilih</span>
@endif
</div>
<div style="display:flex;align-items:center;gap:8px;margin-top:10px;flex-wrap:wrap;">
<span style="font-size:11px;color:#64748b;background:#f1f5f9;padding:2px 8px;border-radius:6px;">
{{ optional(optional($w->rt)->dusun)->nama_dusun ?? '-' }}
</span>
<span class="badge" style="background:{{ $sv[0] }};color:{{ $sv[1] }};border:1px solid {{ $sv[0] }};">
<span class="dot" style="background:{{ $sv[1] }};"></span>{{ $sv[2] }}
</span>
@if($probPct !== null)
<div style="display:flex;align-items:center;gap:6px;flex:1;min-width:100px;">
<div style="flex:1;height:5px;background:#e2e8f0;border-radius:99px;overflow:hidden;">
<div style="height:100%;border-radius:99px;background:{{ $sc }};width:{{ min($probPct,100) }}%;"></div>
</div>
<span style="font-size:11px;font-weight:700;color:{{ $sc }};white-space:nowrap;">{{ number_format((float)$probPct,1) }}%</span>
</div>
@endif
</div>
</div>
@endforeach
@endif
</div>
{{-- PAGINATION --}}
@if(isset($candidates) && method_exists($candidates,'links') && $candidates->hasPages())
<div style="padding:10px 16px;border-top:1px solid #f1f5f9;background:#fafafa;">
{{ $candidates->appends(request()->query())->links() }}
</div>
@endif
</div>
{{-- NOTE --}}
<div class="note-box">
<b>Catatan:</b> Kandidat yang tampil adalah data warga pada tahap
<b>sedang_validasi</b> atau <b>selesai</b>.
Warga yang sudah ditetapkan sebelumnya <b>tidak ikut seleksi ulang</b> (ditampilkan redup/terkunci).<br>
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>
<script>
// ── DATA DUSUN DARI SERVER ──────────────────────────────────────
// Jumlah kandidat aktif per dusun (exclude yang sudah terpilih)
// Dikirim dari blade via PHP ke JS sebagai object
const dusunData = {
@foreach($dusuns as $d)
"{{ $d->id }}": {
nama: "{{ $d->nama_dusun }}",
// Hitung kandidat aktif: total kandidat dusun ini minus yang sudah picked
// Kita pakai nilai dari server yang sudah di-query
kandidat: @php
// Hitung kandidat aktif per dusun (tidak sudah terpilih)
$jumlahKandidatDusun = \App\Models\CalonPenerima::query()
->whereIn('tracking_status', ['sedang_validasi', 'selesai'])
->whereHas('rt', fn($q) => $q->where('dusun_id', $d->id))
->whereNotIn('nik', \App\Models\PenerimaFinal::where('nama_dusun', $d->nama_dusun)->where('status_verifikasi','disetujui')->pluck('nik')->toArray())
->count();
echo $jumlahKandidatDusun;
@endphp
},
@endforeach
};
// ── FORMAT INPUT DANA DENGAN TITIK ──────────────────────────────
function formatDana(el) {
let raw = el.value.replace(/\D/g, '');
if (raw.length > 0) {
el.value = parseInt(raw, 10).toLocaleString('id-ID');
} else {
el.value = '';
}
hitungKuota();
}
function getDanaValue() {
const raw = document.getElementById('inputDana').value.replace(/\./g, '').replace(/,/g, '');
return parseFloat(raw) || 0;
}
function rupiah(n) {
return 'Rp ' + Math.round(n).toLocaleString('id-ID');
}
// Sisa bulan rencana penyaluran (12 - jumlah periode yang sudah tercatat)
const sisaBulan = {{ max(1, 13 - $nextPeriodeNum) }};
// ── HITUNG PENERIMA BULAN INI & SISA DANA ────────────────────────
function hitungKuota() {
const dana = getDanaValue();
const nilaiPerOrang = 300000;
// Sembunyikan semua stat kalau input belum lengkap
if (dana <= 0) {
document.getElementById('penerimaOtomatis').textContent = '—';
document.getElementById('danaStatRow').style.display = 'none';
document.getElementById('rekomendasiBox').style.display = 'none';
document.getElementById('formSimpanWrap').style.display = 'none';
return;
}
// Jatah dana untuk periode/bulan ini = total dana dibagi sisa bulan rencana
const jatahBulanIni = dana / sisaBulan;
const penerima = Math.floor(jatahBulanIni / nilaiPerOrang);
if (penerima <= 0) {
document.getElementById('penerimaOtomatis').textContent = '0 orang';
document.getElementById('danaStatRow').style.display = 'none';
document.getElementById('rekomendasiBox').style.display = 'none';
document.getElementById('formSimpanWrap').style.display = 'none';
return;
}
document.getElementById('penerimaOtomatis').textContent = penerima + ' orang';
const terpakai = penerima * nilaiPerOrang;
const sisa = dana - terpakai;
const periodeBerikutnya = sisa > 0 ? Math.floor((sisa / Math.max(1, sisaBulan - 1)) / nilaiPerOrang) : 0;
// Update stat row
document.getElementById('danaStatRow').style.display = 'grid';
document.getElementById('danaTermakai').textContent = rupiah(terpakai);
document.getElementById('danaTermakaiSub').textContent = penerima + ' orang x Rp 300.000';
if (sisa >= 0) {
document.getElementById('danaSisa').textContent = rupiah(sisa);
document.getElementById('danaSisaSub').textContent = sisa > 0 ? 'Tersisa untuk ' + Math.max(1, sisaBulan - 1) + ' bulan berikutnya' : 'Dana habis tepat';
document.getElementById('danaSisa').className = 'dana-stat-val ' + (sisa > 0 ? 'green' : 'amber');
} else {
document.getElementById('danaSisa').textContent = rupiah(sisa);
document.getElementById('danaSisaSub').textContent = 'Dana tidak mencukupi!';
document.getElementById('danaSisa').className = 'dana-stat-val red';
}
if (periodeBerikutnya > 0) {
document.getElementById('estimasiPeriode').textContent = rupiah(sisa);
document.getElementById('estimasiPeriodeSub').textContent = 'kira-kira ' + periodeBerikutnya + ' orang/bulan untuk ' + Math.max(1, sisaBulan - 1) + ' bulan ke depan';
} else {
document.getElementById('estimasiPeriode').textContent = sisa > 0 ? rupiah(sisa) : '—';
document.getElementById('estimasiPeriodeSub').textContent = sisa > 0 ? 'Sisa tidak cukup 1 orang/bulan' : 'Dana habis';
}
// Tampilkan form simpan periode
document.getElementById('formSimpanWrap').style.display = 'block';
// Hitung rekomendasi per dusun proporsional
hitungProporsional(penerima);
}
function syncHidden() {
const dana = getDanaValue();
const jatahBulanIni = dana / sisaBulan;
const penerima = Math.floor(jatahBulanIni / 300000);
if (dana <= 0 || penerima <= 0) {
alert('Dana belum cukup untuk membiayai minimal 1 penerima bulan ini.');
return false;
}
document.getElementById('hiddenDanaAwal').value = dana;
document.getElementById('hiddenPenerima').value = penerima;
return true;
}
function hitungProporsional(totalKuota) {
// Total kandidat aktif semua dusun
let totalKandidat = 0;
for (const id in dusunData) {
totalKandidat += dusunData[id].kandidat;
}
const listEl = document.getElementById('rekomendasiList');
listEl.innerHTML = '';
if (totalKandidat === 0) {
document.getElementById('rekomendasiBox').style.display = 'none';
return;
}
let sisaKuota = totalKuota;
const entries = Object.entries(dusunData).filter(([,v]) => v.kandidat > 0);
// Hitung kuota proporsional dengan metode Hamilton (sisa terbesar)
const kuotaFloat = entries.map(([id, v]) => ({
id,
nama: v.nama,
kandidat: v.kandidat,
kuotaFloat: (v.kandidat / totalKandidat) * totalKuota
}));
// Assign floor dulu
kuotaFloat.forEach(e => { e.kuota = Math.floor(e.kuotaFloat); });
let sisaBagi = totalKuota - kuotaFloat.reduce((s, e) => s + e.kuota, 0);
// Distribusi sisa ke yang punya desimal terbesar
kuotaFloat
.sort((a, b) => (b.kuotaFloat - b.kuota) - (a.kuotaFloat - a.kuota))
.forEach(e => {
if (sisaBagi > 0) { e.kuota++; sisaBagi--; }
});
// Pastikan tidak melebihi jumlah kandidat
kuotaFloat.forEach(e => {
e.kuota = Math.min(e.kuota, e.kandidat);
});
// Sort by nama dusun
kuotaFloat.sort((a, b) => a.nama.localeCompare(b.nama));
kuotaFloat.forEach(e => {
const chip = document.createElement('div');
chip.className = 'kuota-rec';
chip.style.cursor = 'pointer';
chip.title = 'Klik untuk pilih dusun ini & isi kuota ' + e.kuota;
chip.innerHTML = `
<svg width="11" height="11" 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>
<b>${e.nama}</b>: <span style="color:#059669;">${e.kuota} orang</span>
<span style="color:#94a3b8;font-size:9.5px;">(${e.kandidat} kandidat aktif)</span>
`;
chip.addEventListener('click', () => {
// Set dusun di select
const sel = document.getElementById('selectDusun');
for (let i = 0; i < sel.options.length; i++) {
if (sel.options[i].getAttribute('data-nama') === e.nama) {
sel.selectedIndex = i;
break;
}
}
// Set kuota
document.getElementById('inputKuota').value = e.kuota;
});
listEl.appendChild(chip);
});
document.getElementById('rekomendasiBox').style.display = 'block';
}
// ── TOMBOL TAMPILKAN ────────────────────────────────────────────
document.getElementById('formTampil')?.addEventListener('submit', function () {
const btn = document.getElementById('btnTampil');
const spinner = document.getElementById('spinnerTampil');
const icon = document.getElementById('iconTampil');
const text = document.getElementById('textTampil');
btn.disabled = true;
btn.style.opacity = '0.8';
btn.style.cursor = 'not-allowed';
spinner.style.display = 'block';
icon.style.display = 'none';
text.textContent = 'Memuat...';
// Tampilkan overlay
const overlay = document.getElementById('fi-overlay');
document.getElementById('fi-overlay-ring').className = 'fi-overlay-ring blue';
document.getElementById('fi-overlay-title').textContent = 'Memuat Kandidat';
document.getElementById('fi-overlay-sub').textContent = 'Mengambil data dari server...';
overlay.classList.add('show');
});
// ── TOMBOL TETAPKAN ─────────────────────────────────────────────
document.getElementById('formTetapkan')?.addEventListener('submit', function (e) {
const kuota = {{ (int)($kuota??7) }};
if (!confirm('Tetapkan ' + kuota + ' orang dengan probabilitas tertinggi untuk dusun ini?\n\nWarga yang sudah terpilih sebelumnya tidak akan terpengaruh.')) {
e.preventDefault();
return;
}
const btn = document.getElementById('btnTetapkan');
const spinner = document.getElementById('spinnerTetapkan');
const icon = document.getElementById('iconTetapkan');
const text = document.getElementById('textTetapkan');
btn.disabled = true;
btn.style.opacity = '0.8';
btn.style.cursor = 'not-allowed';
spinner.style.display = 'block';
icon.style.display = 'none';
text.textContent = 'Memproses...';
// Tampilkan overlay
const overlay = document.getElementById('fi-overlay');
document.getElementById('fi-overlay-ring').className = 'fi-overlay-ring';
document.getElementById('fi-overlay-title').textContent = 'Menetapkan Penerima';
document.getElementById('fi-overlay-sub').textContent = 'Sistem sedang memproses & menetapkan kandidat...';
overlay.classList.add('show');
});
// Init jika ada nilai dana dari query string
(function(){
const dana = document.getElementById('inputDana').value;
if (dana) hitungKuota();
})();
</script>
</x-app-layout>

View File

@ -1,93 +1,144 @@
<x-app-layout>
<style>
.lp-wrap{max-width:1400px;margin:0 auto;padding:24px 20px 40px;}
.lp-card{background:#fff;border-radius:20px;border:1.5px solid #f1f5f9;box-shadow:0 2px 8px rgba(0,0,0,.05);overflow:hidden;margin-bottom:14px;}
.sum-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:14px;}
@media(max-width:860px){.sum-grid{grid-template-columns:1fr 1fr;}}
@media(max-width:560px){.sum-grid{grid-template-columns:1fr;}}
.sum-card{background:#fff;border-radius:18px;border:1.5px solid #f1f5f9;box-shadow:0 1px 6px rgba(0,0,0,.04);padding:16px 18px;}
.sum-icon{width:36px;height:36px;border-radius:11px;display:flex;align-items:center;justify-content:center;margin-bottom:10px;}
.sum-icon svg{width:18px;height:18px;}
.sum-val{font-size:24px;font-weight:900;color:#0f172a;line-height:1;font-family:'Georgia',serif;letter-spacing:-1px;}
.sum-val.sm{font-size:16px;letter-spacing:-.5px;line-height:1.3;}
.sum-lbl{font-size:10.5px;color:#94a3b8;font-weight:600;margin-top:3px;text-transform:uppercase;letter-spacing:.04em;}
.sec-hd{padding:12px 18px;border-bottom:1.5px solid #f1f5f9;background:#fafbfc;display:flex;align-items:center;justify-content:space-between;gap:10px;}
.sec-hd-l{display:flex;align-items:center;gap:8px;}
.sec-bar{width:3px;height:16px;background:linear-gradient(180deg,#3b82f6,#2563eb);border-radius:4px;}
.sec-title{font-size:13px;font-weight:800;color:#1e293b;}
.sec-count{font-size:11px;color:#94a3b8;background:#f1f5f9;padding:2px 8px;border-radius:20px;font-weight:600;}
.exp-row{display:flex;flex-wrap:wrap;gap:10px;align-items:center;padding:16px 18px;}
.exp-btn{display:inline-flex;align-items:center;gap:8px;padding:11px 20px;border-radius:13px;font-size:13px;font-weight:700;text-decoration:none;border:none;cursor:pointer;transition:filter .15s,transform .12s,box-shadow .15s;line-height:1;white-space:nowrap;letter-spacing:-.01em;}
.exp-btn svg{width:15px;height:15px;flex-shrink:0;}
.exp-btn:hover{filter:brightness(.93);transform:translateY(-1px);}
.btn-pdf{background:linear-gradient(135deg,#ef4444,#dc2626);color:#fff;box-shadow:0 4px 14px rgba(220,38,38,.35);}
.btn-excel{background:linear-gradient(135deg,#22c55e,#16a34a);color:#fff;box-shadow:0 4px 14px rgba(22,163,74,.35);}
.btn-send{background:linear-gradient(135deg,#3b82f6,#2563eb);color:#fff;box-shadow:0 4px 14px rgba(37,99,235,.35);}
.exp-meta{font-size:11px;color:#94a3b8;padding-left:4px;}
.pub-wrap{padding:16px 18px;}
.pub-form{display:grid;grid-template-columns:2fr 2fr auto;gap:10px;align-items:center;}
@media(max-width:860px){.pub-form{grid-template-columns:1fr;}}
.pub-input{padding:11px 14px;border:1.5px solid #e2e8f0;border-radius:12px;font-size:13px;outline:none;background:#fff;color:#0f172a;}
.pub-input:focus{border-color:#93c5fd;box-shadow:0 0 0 3px rgba(37,99,235,.08);}
.pub-list{margin-top:16px;display:flex;flex-direction:column;gap:10px;}
.pub-item{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:12px 14px;border:1.5px solid #f1f5f9;border-radius:14px;background:#fafbfc;}
@media(max-width:760px){.pub-item{flex-direction:column;align-items:flex-start;}}
.pub-title{font-size:13px;font-weight:700;color:#0f172a;}
.pub-meta{font-size:11px;color:#94a3b8;margin-top:2px;}
.pub-actions{display:flex;gap:8px;align-items:center;flex-wrap:wrap;}
.btn-gray{padding:9px 14px;border:none;border-radius:12px;background:#e5e7eb;color:#374151;font-size:12px;font-weight:700;cursor:pointer;}
.lp-tbl-wrap{overflow-x:auto;}
table.lp-tbl{width:100%;border-collapse:collapse;}
table.lp-tbl thead tr{background:#f8fafc;border-bottom:1.5px solid #f1f5f9;}
table.lp-tbl thead th{padding:10px 12px;text-align:left;font-size:9.5px;font-weight:800;color:#94a3b8;text-transform:uppercase;letter-spacing:.08em;white-space:nowrap;}
table.lp-tbl tbody tr{border-bottom:1px solid #f8fafc;transition:background .1s;}
table.lp-tbl tbody tr:hover{background:#f0f7ff;}
table.lp-tbl tbody tr:last-child{border-bottom:none;}
table.lp-tbl tbody td{padding:10px 12px;vertical-align:middle;font-size:12.5px;color:#374151;}
.av{width:32px;height:32px;border-radius:10px;background:linear-gradient(135deg,#3b82f6,#2563eb);display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:800;color:#fff;flex-shrink:0;}
.name-cell{display:flex;align-items:center;gap:9px;}
.name-txt{font-size:12.5px;font-weight:700;color:#0f172a;white-space:nowrap;}
.nik-pill{font-family:'Courier New',monospace;font-size:10.5px;color:#64748b;background:#f1f5f9;padding:3px 7px;border-radius:6px;letter-spacing:.03em;}
.rt-badge{display:inline-flex;padding:3px 8px;border-radius:7px;background:#eff6ff;color:#1d4ed8;font-size:10.5px;font-weight:800;border:1px solid #bfdbfe;}
.bantuan-ya{display:inline-flex;padding:2px 9px;border-radius:20px;background:#fffbeb;color:#b45309;font-size:10.5px;font-weight:700;border:1px solid #fde68a;}
.bantuan-tdk{display:inline-flex;padding:2px 9px;border-radius:20px;background:#f8fafc;color:#94a3b8;font-size:10.5px;font-weight:600;}
.prob-cell{display:flex;align-items:center;gap:7px;}
.prob-bg{width:42px;height:5px;background:#f1f5f9;border-radius:99px;overflow:hidden;flex-shrink:0;}
.prob-fill{height:100%;border-radius:99px;}
.prob-txt{font-size:11.5px;font-weight:800;white-space:nowrap;}
.st-pill{display:inline-flex;align-items:center;gap:5px;padding:4px 10px;border-radius:20px;font-size:10.5px;font-weight:700;}
.st-dot{width:5px;height:5px;border-radius:50%;}
.empty-state{padding:56px 16px;text-align:center;}
.empty-icon{width:48px;height:48px;background:#f1f5f9;border-radius:50%;display:flex;align-items:center;justify-content:center;margin:0 auto 12px;}
.empty-icon svg{width:22px;height:22px;}
.empty-txt{font-size:13px;font-weight:600;color:#94a3b8;}
.tbl-foot{padding:10px 18px;border-top:1.5px solid #f1f5f9;background:#fafbfc;text-align:center;}
.credit{font-size:11px;color:#cbd5e1;font-weight:500;letter-spacing:.03em;}
.flash-ok{display:flex;align-items:center;gap:8px;background:#f0fdf4;border:1.5px solid #bbf7d0;color:#166534;padding:12px 16px;border-radius:13px;font-size:12.5px;font-weight:600;margin-bottom:14px;}
.flash-ok svg{width:16px;height:16px;flex-shrink:0;}
</style>
<x-slot name="header">
<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;">
<div>
<h2 style="font-size:16px;font-weight:800;color:#0f172a;letter-spacing:-.02em;">Laporan Penerima BLT-DD</h2>
<p style="font-size:11px;color:#94a3b8;margin-top:2px;">DesaNgerong warga yang telah ditetapkan sebagai penerima final</p>
</div>
</div>
</x-slot>
<div class="lp-wrap">
<style>
.lp-card{background:#fff;border-radius:16px;border:1.5px solid #f1f5f9;box-shadow:0 2px 8px rgba(0,0,0,.05);overflow:hidden;margin-bottom:14px;}
.sec-hd{padding:11px 16px;border-bottom:1.5px solid #f1f5f9;background:#fafbfc;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;}
.sec-hd-l{display:flex;align-items:center;gap:8px;}
.sec-bar{width:3px;height:15px;background:linear-gradient(180deg,#3b82f6,#2563eb);border-radius:4px;flex-shrink:0;}
.sec-title{font-size:13px;font-weight:800;color:#1e293b;}
.sec-count{font-size:11px;color:#94a3b8;background:#f1f5f9;padding:2px 8px;border-radius:20px;font-weight:600;white-space:nowrap;}
<div style="margin-bottom:18px;">
<h1 style="font-size:20px;font-weight:900;color:#0f172a;line-height:1.2;letter-spacing:-.03em;">
Laporan Penerima BLT-DD
</h1>
<p style="font-size:11.5px;color:#94a3b8;margin-top:4px;">
Kelurahan Ngerong warga yang telah ditetapkan sebagai penerima final BLT Dana Desa
</p>
</div>
/* stat */
.sum-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:14px;}
@media(max-width:860px){.sum-grid{grid-template-columns:1fr 1fr;}}
@media(max-width:480px){.sum-grid{grid-template-columns:1fr 1fr;gap:8px;}}
.sum-card{background:#fff;border-radius:16px;border:1.5px solid #f1f5f9;box-shadow:0 1px 6px rgba(0,0,0,.04);padding:14px 16px;}
@media(max-width:480px){.sum-card{padding:12px 13px;border-radius:13px;}}
.sum-icon{width:34px;height:34px;border-radius:10px;display:flex;align-items:center;justify-content:center;margin-bottom:8px;}
.sum-icon svg{width:17px;height:17px;}
.sum-val{font-size:22px;font-weight:900;color:#0f172a;line-height:1;letter-spacing:-.03em;}
.sum-val-sm{font-size:14px;font-weight:900;color:#0f172a;line-height:1.3;letter-spacing:-.01em;}
@media(max-width:480px){.sum-val{font-size:18px;}.sum-val-sm{font-size:12px;}}
.sum-lbl{font-size:10px;color:#94a3b8;font-weight:700;margin-top:3px;text-transform:uppercase;letter-spacing:.04em;}
@media(max-width:480px){.sum-lbl{font-size:9px;}}
/* export buttons */
.exp-row{display:flex;flex-wrap:wrap;gap:8px;padding:14px 16px;align-items:center;}
.exp-btn{display:inline-flex;align-items:center;gap:7px;padding:10px 18px;border-radius:12px;font-size:12.5px;font-weight:700;text-decoration:none;border:none;cursor:pointer;transition:filter .15s,transform .1s;line-height:1;white-space:nowrap;font-family:inherit;}
.exp-btn svg{width:14px;height:14px;flex-shrink:0;}
.exp-btn:hover{filter:brightness(.93);transform:translateY(-1px);}
@media(max-width:480px){
.exp-btn{padding:10px 14px;font-size:12px;border-radius:10px;flex:1;justify-content:center;}
}
.btn-pdf{background:linear-gradient(135deg,#ef4444,#dc2626);color:#fff;box-shadow:0 3px 12px rgba(220,38,38,.3);}
.btn-excel{background:linear-gradient(135deg,#22c55e,#16a34a);color:#fff;box-shadow:0 3px 12px rgba(22,163,74,.3);}
.btn-send{background:linear-gradient(135deg,#3b82f6,#2563eb);color:#fff;box-shadow:0 3px 12px rgba(37,99,235,.3);}
.btn-reset{background:linear-gradient(135deg,#f59e0b,#d97706);color:#fff;box-shadow:0 3px 12px rgba(217,119,6,.3);}
.btn-gray{display:inline-flex;align-items:center;padding:8px 14px;border:none;border-radius:10px;background:#e5e7eb;color:#374151;font-size:12px;font-weight:700;cursor:pointer;font-family:inherit;}
/* upload form */
.pub-wrap{padding:14px 16px;}
.pub-form{display:grid;grid-template-columns:1fr 1fr auto;gap:10px;align-items:end;}
@media(max-width:768px){.pub-form{grid-template-columns:1fr 1fr;}}
@media(max-width:500px){.pub-form{grid-template-columns:1fr;}}
@media(max-width:500px){.pub-form .exp-btn{justify-content:center;}}
.pub-input{padding:10px 13px;border:1.5px solid #e2e8f0;border-radius:11px;font-size:16px;outline:none;background:#fff;color:#0f172a;width:100%;font-family:inherit;}
.pub-input:focus{border-color:#93c5fd;box-shadow:0 0 0 3px rgba(37,99,235,.08);}
.pub-list{margin-top:14px;display:flex;flex-direction:column;gap:10px;}
.pub-item{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:12px 14px;border:1.5px solid #f1f5f9;border-radius:13px;background:#fafbfc;flex-wrap:wrap;}
.pub-title{font-size:13px;font-weight:700;color:#0f172a;}
.pub-meta{font-size:11px;color:#94a3b8;margin-top:2px;}
.pub-actions{display:flex;gap:8px;align-items:center;flex-wrap:wrap;}
@media(max-width:480px){.pub-actions{width:100%;}}
@media(max-width:480px){.pub-actions .exp-btn,.pub-actions .btn-gray{flex:1;justify-content:center;}}
/* desktop table */
.lp-tbl-wrap{overflow-x:auto;-webkit-overflow-scrolling:touch;}
table.lp-tbl{width:100%;border-collapse:collapse;min-width:1000px;}
table.lp-tbl thead tr{background:#f8fafc;border-bottom:1.5px solid #f1f5f9;}
table.lp-tbl thead th{padding:9px 11px;text-align:left;font-size:9.5px;font-weight:800;color:#94a3b8;text-transform:uppercase;letter-spacing:.07em;white-space:nowrap;}
table.lp-tbl tbody tr{border-bottom:1px solid #f8fafc;transition:background .1s;}
table.lp-tbl tbody tr:hover{background:#f0f7ff;}
table.lp-tbl tbody tr:last-child{border-bottom:none;}
table.lp-tbl tbody td{padding:9px 11px;vertical-align:middle;font-size:12px;color:#374151;}
/* mobile card */
.mob-list{display:none;flex-direction:column;gap:10px;padding:14px;}
@media(max-width:640px){
.lp-tbl-wrap{display:none;}
.mob-list{display:flex;}
}
.mob-card{background:#fafbfc;border:1.5px solid #f1f5f9;border-radius:13px;padding:13px;display:flex;flex-direction:column;gap:10px;}
.mob-top{display:flex;align-items:center;gap:10px;}
.mob-nomor{width:22px;height:22px;border-radius:6px;background:#f1f5f9;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:800;color:#94a3b8;flex-shrink:0;}
.mob-name{font-size:13px;font-weight:800;color:#0f172a;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.mob-rows{display:grid;grid-template-columns:1fr 1fr;gap:6px 10px;}
.mob-kv{display:flex;flex-direction:column;gap:2px;}
.mob-k{font-size:9px;font-weight:700;color:#94a3b8;text-transform:uppercase;letter-spacing:.05em;}
.mob-v{font-size:11.5px;font-weight:600;color:#374151;}
.mob-nik{font-family:'Courier New',monospace;font-size:10px;color:#64748b;background:#f1f5f9;padding:2px 5px;border-radius:4px;display:inline-block;}
.mob-bottom{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:6px;padding-top:8px;border-top:1px solid #f1f5f9;}
/* shared */
.av{width:30px;height:30px;border-radius:9px;background:linear-gradient(135deg,#3b82f6,#2563eb);display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:800;color:#fff;flex-shrink:0;}
.nik-pill{font-family:'Courier New',monospace;font-size:10px;color:#64748b;background:#f1f5f9;padding:2px 6px;border-radius:5px;}
.rt-badge{display:inline-flex;padding:2px 7px;border-radius:6px;background:#eff6ff;color:#1d4ed8;font-size:10px;font-weight:800;border:1px solid #bfdbfe;}
.prob-cell{display:flex;align-items:center;gap:6px;}
.prob-bg{width:38px;height:4px;background:#f1f5f9;border-radius:99px;overflow:hidden;flex-shrink:0;}
.prob-fill{height:100%;border-radius:99px;}
.prob-txt{font-size:11px;font-weight:800;white-space:nowrap;}
.st-pill{display:inline-flex;align-items:center;gap:4px;padding:4px 10px;border-radius:20px;font-size:10.5px;font-weight:700;}
.st-dot{width:5px;height:5px;border-radius:50%;flex-shrink:0;}
.bantuan-ya{display:inline-flex;padding:2px 8px;border-radius:20px;background:#fffbeb;color:#b45309;font-size:10px;font-weight:700;border:1px solid #fde68a;}
.bantuan-tdk{display:inline-flex;padding:2px 8px;border-radius:20px;background:#f8fafc;color:#94a3b8;font-size:10px;font-weight:600;}
.flash-ok{display:flex;align-items:center;gap:8px;background:#f0fdf4;border:1.5px solid #bbf7d0;color:#166534;padding:11px 16px;border-radius:13px;font-size:12.5px;font-weight:600;margin-bottom:14px;}
.flash-ok svg{width:15px;height:15px;flex-shrink:0;}
.flash-err{display:flex;align-items:center;gap:8px;background:#fff1f2;border:1.5px solid #fecdd3;color:#9f1239;padding:11px 16px;border-radius:13px;font-size:12.5px;font-weight:600;margin-bottom:14px;}
.flash-err svg{width:15px;height:15px;flex-shrink:0;}
.empty-state{padding:52px 16px;text-align:center;}
.empty-icon{width:46px;height:46px;background:#f1f5f9;border-radius:50%;display:flex;align-items:center;justify-content:center;margin:0 auto 10px;}
.tbl-foot{padding:9px 16px;border-top:1.5px solid #f1f5f9;background:#fafbfc;text-align:center;}
.credit{font-size:11px;color:#cbd5e1;font-weight:500;}
/* riwayat accordion */
.rw-periode{border:1.5px solid #f1f5f9;border-radius:13px;overflow:hidden;margin-bottom:10px;}
.rw-periode-hd{display:flex;align-items:center;justify-content:space-between;padding:11px 14px;background:#fafbfc;cursor:pointer;user-select:none;gap:8px;}
.rw-periode-hd:hover{background:#f1f5f9;}
.rw-periode-title{font-size:12.5px;font-weight:800;color:#1e293b;}
.rw-periode-meta{font-size:11px;color:#94a3b8;}
.rw-periode-body{display:none;overflow-x:auto;}
.rw-periode-body.open{display:block;}
table.rw-tbl{width:100%;border-collapse:collapse;min-width:700px;}
table.rw-tbl thead tr{background:#f8fafc;}
table.rw-tbl thead th{padding:8px 10px;font-size:9px;font-weight:800;color:#94a3b8;text-transform:uppercase;letter-spacing:.07em;text-align:left;white-space:nowrap;}
table.rw-tbl tbody tr{border-top:1px solid #f8fafc;}
table.rw-tbl tbody td{padding:8px 10px;font-size:11.5px;color:#374151;vertical-align:middle;}
.chev{width:14px;height:14px;transition:transform .2s;flex-shrink:0;color:#94a3b8;}
.chev.open{transform:rotate(180deg);}
</style>
{{-- FLASH --}}
@if(session('success'))
<div class="flash-ok">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" 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="flash-err">
<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
@php
$totalPenerima = $laporans->count();
@ -99,52 +150,39 @@
$periodes = $laporans->pluck('periode_bantuan')->filter()->unique()->count();
@endphp
{{-- STAT CARDS --}}
<div class="sum-grid">
<div class="sum-card">
<div class="sum-icon" style="background:#eff6ff;">
<svg fill="none" stroke="#2563eb" 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>
<svg fill="none" stroke="#2563eb" 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>
<div class="sum-val">{{ $totalPenerima }}</div>
<div class="sum-lbl">Total Penerima</div>
</div>
<div class="sum-card">
<div class="sum-icon" style="background:#f0fdf4;">
<svg fill="none" stroke="#16a34a" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<svg fill="none" stroke="#16a34a" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
</div>
<div class="sum-val sm">Rp {{ number_format($totalBantuan, 0, ',', '.') }}</div>
<div class="sum-lbl">Total Dana Tersalurkan</div>
<div class="sum-val-sm">Rp {{ number_format($totalBantuan,0,',','.') }}</div>
<div class="sum-lbl">Total Dana</div>
</div>
<div class="sum-card">
<div class="sum-icon" style="background:#fffbeb;">
<svg fill="none" stroke="#d97706" 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>
<svg fill="none" stroke="#d97706" 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>
<div class="sum-val">{{ number_format($avgProb, 1) }}<span style="font-size:14px;font-weight:700;">%</span></div>
<div class="sum-lbl">Rata-rata Probabilitas</div>
<div class="sum-val">{{ number_format($avgProb,1) }}<span style="font-size:13px;font-weight:700;">%</span></div>
<div class="sum-lbl">Rata-rata Prob</div>
</div>
<div class="sum-card">
<div class="sum-icon" style="background:#fff1f2;">
<svg fill="none" stroke="#e11d48" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
<svg fill="none" stroke="#e11d48" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
</div>
<div class="sum-val">{{ $periodes }}</div>
<div class="sum-lbl">Periode Aktif</div>
</div>
</div>
{{-- EKSPOR --}}
<div class="lp-card">
<div class="sec-hd">
<div class="sec-hd-l">
@ -152,24 +190,42 @@
<span class="sec-title">Ekspor & Distribusi Data</span>
</div>
</div>
<div class="exp-row">
<a href="{{ route('admin.laporan.pdf') }}" class="exp-btn btn-pdf">Unduh PDF</a>
<a href="{{ route('admin.laporan.excel') }}" class="exp-btn btn-excel">Unduh Excel</a>
<form action="{{ route('admin.laporan.kirim-ke-rt') }}" method="POST" style="margin:0;">
<a href="{{ route('admin.laporan.pdf') }}" class="exp-btn btn-pdf">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
Unduh PDF
</a>
<a href="{{ route('admin.laporan.excel') }}" class="exp-btn btn-excel">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
Unduh Excel
</a>
<form action="{{ route('admin.laporan.kirim-ke-rt') }}" method="POST" style="display:contents;">
@csrf
<button type="submit"
onclick="return confirm('Kirim semua data penerima final ke RT dusun?')"
class="exp-btn btn-send">
Kirim ke RT Dusun
<button type="submit" class="exp-btn btn-send"
onclick="return confirm('Kirim semua data penerima final ke RT dusun?')">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"/></svg>
Kirim ke RT
</button>
</form>
<span class="exp-meta">{{ $totalPenerima }} penerima terdaftar</span>
{{-- ── TOMBOL RESET MANUAL (BARU) ── --}}
@if($laporans->count() > 0)
<form action="{{ route('admin.laporan.reset-manual') }}" method="POST" style="display:contents;">
@csrf
<button type="submit" class="exp-btn btn-reset"
onclick="return confirm('Arsipkan semua {{ $laporans->count() }} data penerima final ke riwayat lalu kosongkan tabel? Tindakan ini tidak dapat dibatalkan.')">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
Reset & Arsipkan
</button>
</form>
@endif
{{-- ────────────────────────────── --}}
<span style="font-size:11px;color:#94a3b8;width:100%;padding-top:2px;">{{ $totalPenerima }} penerima terdaftar</span>
</div>
</div>
{{-- UPLOAD PDF PUBLIK --}}
<div class="lp-card">
<div class="sec-hd">
<div class="sec-hd-l">
@ -177,14 +233,15 @@ class="exp-btn btn-send">
<span class="sec-title">Upload PDF Laporan Publik</span>
</div>
</div>
<div class="pub-wrap">
<form action="{{ route('admin.laporan.upload-publik-pdf') }}" method="POST" enctype="multipart/form-data" class="pub-form">
<form action="{{ route('admin.laporan.upload-publik-pdf') }}" method="POST"
enctype="multipart/form-data" class="pub-form">
@csrf
<input type="text" name="judul" placeholder="Judul laporan publik" required class="pub-input">
<input type="file" name="file_pdf" accept="application/pdf" required class="pub-input">
<button type="submit" class="exp-btn btn-send" style="justify-content:center;">
Upload PDF Publik
<input type="file" name="file_pdf" accept="application/pdf" required class="pub-input" style="font-size:13px;">
<button type="submit" class="exp-btn btn-send">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"/></svg>
Upload
</button>
</form>
@ -192,24 +249,17 @@ class="exp-btn btn-send">
<div class="pub-list">
@foreach($laporanPubliks as $pdf)
<div class="pub-item">
<div>
<div style="flex:1;min-width:0;">
<div class="pub-title">{{ $pdf->judul }}</div>
<div class="pub-meta">
Diupload: {{ $pdf->created_at->format('d-m-Y H:i') }}
</div>
<div class="pub-meta">Diupload: {{ $pdf->created_at->format('d-m-Y H:i') }}</div>
</div>
<div class="pub-actions">
<a href="{{ asset('storage/' . $pdf->file_path) }}"
target="_blank"
class="exp-btn btn-pdf"
style="padding:9px 14px;">
<a href="{{ asset('storage/'.$pdf->file_path) }}" target="_blank" class="exp-btn btn-pdf" style="padding:8px 14px;">
Lihat PDF
</a>
<form action="{{ route('admin.laporan.hapus-publik-pdf', $pdf->id) }}" method="POST" onsubmit="return confirm('Hapus PDF publik ini?')">
@csrf
@method('DELETE')
<form action="{{ route('admin.laporan.hapus-publik-pdf',$pdf->id) }}" method="POST"
onsubmit="return confirm('Hapus PDF publik ini?')" style="display:contents;">
@csrf @method('DELETE')
<button type="submit" class="btn-gray">Hapus</button>
</form>
</div>
@ -220,6 +270,7 @@ class="exp-btn btn-pdf"
</div>
</div>
{{-- DAFTAR PENERIMA FINAL --}}
<div class="lp-card">
<div class="sec-hd">
<div class="sec-hd-l">
@ -229,6 +280,7 @@ class="exp-btn btn-pdf"
<span class="sec-count">{{ $totalPenerima }} data</span>
</div>
{{-- DESKTOP TABLE --}}
<div class="lp-tbl-wrap">
<table class="lp-tbl">
<thead>
@ -243,109 +295,265 @@ class="exp-btn btn-pdf"
<th>Penghasilan</th>
<th>Tanggungan</th>
<th>Aset</th>
<th>Bantuan Lain</th>
<th>Bantuan</th>
<th>Usia</th>
<th>Probabilitas</th>
<th>Prob</th>
<th>Status</th>
<th>Tgl Penetapan</th>
<th>Penetapan</th>
<th>Periode</th>
<th>Jumlah Bantuan</th>
<th>Jumlah</th>
</tr>
</thead>
<tbody>
@forelse($laporans as $index => $item)
@php
$nama = $item->nama_lengkap ?? '-';
$init = strtoupper(substr($nama, 0, 1));
$init = strtoupper(substr($nama,0,1));
$prob = $item->probability ?? 0;
if ($prob <= 1) $prob = $prob * 100;
$sc = $prob >= 70 ? '#10b981' : ($prob >= 40 ? '#f59e0b' : '#f43f5e');
$scBg = $prob >= 70 ? '#f0fdf4' : ($prob >= 40 ? '#fffbeb' : '#fff1f2');
$scBd = $prob >= 70 ? '#bbf7d0' : ($prob >= 40 ? '#fde68a' : '#fecdd3');
$bantuanLain = strtolower($item->bantuan_lain ?? 'tidak');
if($prob<=1) $prob=$prob*100;
$sc = $prob>=70?'#10b981':($prob>=40?'#f59e0b':'#f43f5e');
$scBg = $prob>=70?'#f0fdf4':($prob>=40?'#fffbeb':'#fff1f2');
$scBd = $prob>=70?'#bbf7d0':($prob>=40?'#fde68a':'#fecdd3');
@endphp
<tr>
<td>{{ $index + 1 }}</td>
<td style="text-align:center;font-size:10.5px;color:#94a3b8;font-weight:700;">{{ $index+1 }}</td>
<td>
<div class="name-cell">
<div style="display:flex;align-items:center;gap:8px;">
<div class="av">{{ $init }}</div>
<span class="name-txt">{{ $nama }}</span>
<span style="font-size:12px;font-weight:700;color:#0f172a;white-space:nowrap;">{{ $nama }}</span>
</div>
</td>
<td><span class="nik-pill">{{ $item->nik ?? '-' }}</span></td>
<td><span class="nik-pill">{{ $item->no_kk ?? '-' }}</span></td>
<td><span class="nik-pill">{{ $item->nik??'-' }}</span></td>
<td><span class="nik-pill">{{ $item->no_kk??'-' }}</span></td>
<td><span class="rt-badge">RT {{ str_pad($item->nomor_rt??'0',3,'0',STR_PAD_LEFT) }}</span></td>
<td style="font-size:11.5px;white-space:nowrap;">{{ $item->nama_dusun??'-' }}</td>
<td style="font-size:11.5px;">{{ $item->pekerjaan??'-' }}</td>
<td style="font-size:12px;font-weight:600;white-space:nowrap;">Rp {{ number_format($item->penghasilan??0,0,',','.') }}</td>
<td style="text-align:center;font-size:12px;">{{ $item->jumlah_tanggungan??0 }}</td>
<td style="font-size:11.5px;">{{ $item->aset_kepemilikan??'-' }}</td>
<td>
<span class="rt-badge">
RT {{ str_pad($item->nomor_rt ?? '0', 3, '0', STR_PAD_LEFT) }}
</span>
</td>
<td>{{ $item->nama_dusun ?? '-' }}</td>
<td>{{ $item->pekerjaan ?? '-' }}</td>
<td>Rp {{ number_format($item->penghasilan ?? 0, 0, ',', '.') }}</td>
<td>{{ $item->jumlah_tanggungan ?? 0 }}</td>
<td>{{ $item->aset_kepemilikan ?? '-' }}</td>
<td>
@if($bantuanLain === 'ya')
@if(strtolower($item->bantuan_lain??'')=='ya')
<span class="bantuan-ya">Ya</span>
@else
<span class="bantuan-tdk">Tidak</span>
@endif
</td>
<td>{{ $item->usia ?? 0 }}</td>
<td style="text-align:center;font-size:12px;">{{ $item->usia??0 }}</td>
<td>
<div class="prob-cell">
<div class="prob-bg">
<div class="prob-fill" style="width:{{ min($prob, 100) }}%;background:{{ $sc }};"></div>
</div>
<span class="prob-txt" style="color:{{ $sc }};">{{ number_format($prob, 1) }}%</span>
<div class="prob-bg"><div class="prob-fill" style="width:{{ min($prob,100) }}%;background:{{ $sc }};"></div></div>
<span class="prob-txt" style="color:{{ $sc }};">{{ number_format($prob,1) }}%</span>
</div>
</td>
<td>
<span class="st-pill" style="background:{{ $scBg }};color:{{ $sc }};border:1px solid {{ $scBd }};">
<span class="st-dot" style="background:{{ $sc }};"></span>
{{ ucfirst($item->status_verifikasi ?? '-') }}
{{ ucfirst($item->status_verifikasi??'-') }}
</span>
</td>
<td>{{ $item->tanggal_penetapan?->format('d-m-Y') ?? '-' }}</td>
<td>{{ $item->periode_bantuan ?? '-' }}</td>
<td>Rp {{ number_format($item->jumlah_bantuan ?? 300000, 0, ',', '.') }}</td>
<td style="font-size:11px;white-space:nowrap;">{{ $item->tanggal_penetapan?->format('d-m-Y')??'-' }}</td>
<td style="font-size:11.5px;white-space:nowrap;">{{ $item->periode_bantuan??'-' }}</td>
<td style="font-size:12px;font-weight:700;color:#166534;white-space:nowrap;">Rp {{ number_format($item->jumlah_bantuan??300000,0,',','.') }}</td>
</tr>
@empty
<tr>
<td colspan="17">
<div class="empty-state">
<div class="empty-icon">
<svg fill="none" stroke="#d1d5db" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
</div>
<p class="empty-txt">Belum ada data penerima final</p>
</div>
</td>
</tr>
<tr><td colspan="17">
<div class="empty-state">
<div class="empty-icon"><svg fill="none" stroke="#d1d5db" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg></div>
<p style="font-size:13px;font-weight:600;color:#94a3b8;">Belum ada data penerima final</p>
</div>
</td></tr>
@endforelse
</tbody>
</table>
</div>
<div class="tbl-foot">
<span class="credit">SiBantuDes · Kelurahan Ngerong</span>
{{-- MOBILE CARD LIST --}}
<div class="mob-list">
@forelse($laporans as $index => $item)
@php
$nama = $item->nama_lengkap ?? '-';
$init = strtoupper(substr($nama,0,1));
$prob = $item->probability ?? 0;
if($prob<=1) $prob=$prob*100;
$sc = $prob>=70?'#10b981':($prob>=40?'#f59e0b':'#f43f5e');
$scBg = $prob>=70?'#f0fdf4':($prob>=40?'#fffbeb':'#fff1f2');
$scBd = $prob>=70?'#bbf7d0':($prob>=40?'#fde68a':'#fecdd3');
@endphp
<div class="mob-card">
<div class="mob-top">
<div class="mob-nomor">{{ $index+1 }}</div>
<div class="av">{{ $init }}</div>
<div class="mob-name">{{ $nama }}</div>
</div>
<div style="display:flex;gap:10px;flex-wrap:wrap;">
<div class="mob-kv">
<span class="mob-k">NIK</span>
<span class="mob-nik">{{ $item->nik??'-' }}</span>
</div>
<div class="mob-kv">
<span class="mob-k">No KK</span>
<span class="mob-nik">{{ $item->no_kk??'-' }}</span>
</div>
</div>
<div class="mob-rows">
<div class="mob-kv">
<span class="mob-k">RT</span>
<span class="rt-badge" style="width:fit-content;">RT {{ str_pad($item->nomor_rt??'0',3,'0',STR_PAD_LEFT) }}</span>
</div>
<div class="mob-kv">
<span class="mob-k">Dusun</span>
<span class="mob-v">{{ $item->nama_dusun??'-' }}</span>
</div>
<div class="mob-kv">
<span class="mob-k">Pekerjaan</span>
<span class="mob-v">{{ $item->pekerjaan??'-' }}</span>
</div>
<div class="mob-kv">
<span class="mob-k">Usia</span>
<span class="mob-v">{{ $item->usia??0 }} thn</span>
</div>
<div class="mob-kv">
<span class="mob-k">Penghasilan</span>
<span class="mob-v">Rp {{ number_format($item->penghasilan??0,0,',','.') }}</span>
</div>
<div class="mob-kv">
<span class="mob-k">Tanggungan</span>
<span class="mob-v">{{ $item->jumlah_tanggungan??0 }} orang</span>
</div>
<div class="mob-kv">
<span class="mob-k">Aset</span>
<span class="mob-v">{{ $item->aset_kepemilikan??'-' }}</span>
</div>
<div class="mob-kv">
<span class="mob-k">Bantuan Lain</span>
@if(strtolower($item->bantuan_lain??'')=='ya')
<span class="bantuan-ya" style="width:fit-content;">Ya</span>
@else
<span class="bantuan-tdk" style="width:fit-content;">Tidak</span>
@endif
</div>
</div>
<div class="mob-bottom">
<div class="prob-cell">
<div class="prob-bg"><div class="prob-fill" style="width:{{ min($prob,100) }}%;background:{{ $sc }};"></div></div>
<span class="prob-txt" style="color:{{ $sc }};">{{ number_format($prob,1) }}%</span>
</div>
<span class="st-pill" style="background:{{ $scBg }};color:{{ $sc }};border:1px solid {{ $scBd }};">
<span class="st-dot" style="background:{{ $sc }};"></span>{{ ucfirst($item->status_verifikasi??'-') }}
</span>
<span style="font-size:12px;font-weight:800;color:#166534;background:#f0fdf4;border:1px solid #bbf7d0;padding:3px 9px;border-radius:8px;white-space:nowrap;">
Rp {{ number_format($item->jumlah_bantuan??300000,0,',','.') }}
</span>
</div>
<div style="font-size:10.5px;color:#94a3b8;">
Periode: <strong style="color:#475569;">{{ $item->periode_bantuan??'-' }}</strong>
&bull; Penetapan: <strong style="color:#475569;">{{ $item->tanggal_penetapan?->format('d-m-Y')??'-' }}</strong>
</div>
</div>
@empty
<div class="empty-state">
<div class="empty-icon"><svg fill="none" stroke="#d1d5db" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg></div>
<p style="font-size:13px;font-weight:600;color:#94a3b8;">Belum ada data penerima final</p>
</div>
@endforelse
</div>
<div class="tbl-foot"><span class="credit">SiBantuDes · Desa Ngerong</span></div>
</div>
</div>
{{-- ── RIWAYAT PENERIMA (BARU) ─────────────────────────────────────── --}}
<div class="lp-card">
<div class="sec-hd">
<div class="sec-hd-l">
<div class="sec-bar" style="background:linear-gradient(180deg,#f59e0b,#d97706);"></div>
<span class="sec-title">Riwayat Penerima per Periode</span>
</div>
<span class="sec-count">{{ isset($riwayats) ? $riwayats->count() : 0 }} periode</span>
</div>
</x-app-layout>
<div style="padding:14px 16px;">
@if(isset($riwayats) && $riwayats->count() > 0)
@foreach($riwayats as $periode => $rows)
<div class="rw-periode">
<div class="rw-periode-hd" onclick="toggleRiwayat('rw-{{ Str::slug($periode) }}')">
<div>
<div class="rw-periode-title">Periode {{ $periode }}</div>
<div class="rw-periode-meta">
{{ $rows->count() }} penerima &bull;
Diarsipkan: {{ $rows->first()->di_arsipkan_pada?->format('d-m-Y H:i') ?? '-' }}
</div>
</div>
<svg class="chev" id="chev-rw-{{ Str::slug($periode) }}" 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>
</div>
<div class="rw-periode-body" id="rw-{{ Str::slug($periode) }}">
<table class="rw-tbl">
<thead>
<tr>
<th>#</th>
<th>Nama</th>
<th>NIK</th>
<th>Dusun</th>
<th>Pekerjaan</th>
<th>Penghasilan</th>
<th>Prob</th>
<th>Status</th>
<th>Bantuan</th>
</tr>
</thead>
<tbody>
@foreach($rows as $ri => $r)
@php
$rp = $r->probability ?? 0;
if($rp<=1) $rp=$rp*100;
$rc = $rp>=70?'#10b981':($rp>=40?'#f59e0b':'#f43f5e');
@endphp
<tr>
<td style="color:#94a3b8;font-size:10.5px;font-weight:700;">{{ $ri+1 }}</td>
<td style="font-weight:600;color:#0f172a;">{{ $r->nama_lengkap }}</td>
<td><span class="nik-pill">{{ $r->nik }}</span></td>
<td>{{ $r->nama_dusun ?? '-' }}</td>
<td>{{ $r->pekerjaan ?? '-' }}</td>
<td style="white-space:nowrap;">Rp {{ number_format($r->penghasilan??0,0,',','.') }}</td>
<td>
<span style="font-size:11px;font-weight:800;color:{{ $rc }};">{{ number_format($rp,1) }}%</span>
</td>
<td>
<span class="st-pill" style="background:{{ $rp>=70?'#f0fdf4':($rp>=40?'#fffbeb':'#fff1f2') }};color:{{ $rc }};border:1px solid {{ $rp>=70?'#bbf7d0':($rp>=40?'#fde68a':'#fecdd3') }};">
<span class="st-dot" style="background:{{ $rc }};"></span>
{{ ucfirst($r->status_verifikasi ?? '-') }}
</span>
</td>
<td style="font-weight:700;color:#166534;white-space:nowrap;">Rp {{ number_format($r->jumlah_bantuan??0,0,',','.') }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
@endforeach
@else
<div class="empty-state">
<div class="empty-icon">
<svg fill="none" stroke="#d1d5db" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
</div>
<p style="font-size:13px;font-weight:600;color:#94a3b8;">Belum ada riwayat arsip penerima</p>
<p style="font-size:11px;color:#cbd5e1;margin-top:4px;">Riwayat akan muncul setelah klik "Reset & Arsipkan"</p>
</div>
@endif
</div>
</div>
{{-- ────────────────────────────────────────────────────────────────── --}}
<script>
function toggleRiwayat(id) {
const body = document.getElementById(id);
const chev = document.getElementById('chev-' + id);
if (!body) return;
body.classList.toggle('open');
chev?.classList.toggle('open');
}
</script>
</x-app-layout>

View File

@ -1,25 +1,478 @@
<x-guest-layout>
<div class="mb-4 text-sm text-gray-600">
{{ __('Forgot your password? No problem. Just let us know your email address and we will email you a password reset link that will allow you to choose a new one.') }}
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Reset Password SiBantuDes</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Plus Jakarta Sans', sans-serif;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
background: linear-gradient(145deg, #eff6ff 0%, #f8fafc 45%, #f0f9ff 100%);
-webkit-font-smoothing: antialiased;
}
.shell {
width: 100%;
max-width: 960px;
min-height: 560px;
display: flex;
border-radius: 24px;
overflow: hidden;
box-shadow: 0 32px 80px rgba(0,0,0,.18), 0 0 0 1px rgba(255,255,255,.06);
}
/* ── LEFT PANEL ── */
.left {
width: 50%;
position: relative;
overflow: hidden;
flex-shrink: 0;
}
.left-img {
position: absolute; inset: 0;
background: url('/E31232406/public/login-gedung.jpg') center/cover no-repeat;
}
.left-overlay {
position: absolute; inset: 0;
background: linear-gradient(170deg, rgba(15,23,42,.45) 0%, rgba(15,23,42,.25) 40%, rgba(15,23,42,.78) 100%);
}
.left-content {
position: relative; z-index: 2; height: 100%;
display: flex; flex-direction: column; justify-content: space-between;
padding: 32px 36px;
}
.left-brand { display: flex; align-items: center; gap: 10px; }
.left-brand-name { font-size: 16px; font-weight: 800; color: #fff; letter-spacing: -.02em; }
.left-brand-sub { font-size: 11px; color: rgba(255,255,255,.75); font-weight: 500; }
.left-caption { font-size: 22px; font-weight: 900; color: #fff; line-height: 1.25; letter-spacing: -.03em; margin-bottom: 16px; }
.left-caption span { color: #93c5fd; }
.left-features { display: flex; flex-direction: column; gap: 10px; }
.feat { display: flex; align-items: center; gap: 9px; }
.feat-dot {
width: 20px; height: 20px; background: rgba(255,255,255,.15);
border: 1px solid rgba(255,255,255,.25); border-radius: 6px;
display: flex; align-items: center; justify-content: center;
flex-shrink: 0; backdrop-filter: blur(4px);
}
.feat-dot svg { width: 11px; height: 11px; color: #93c5fd; }
.feat span { font-size: 12px; color: rgba(255,255,255,.88); font-weight: 500; }
.loc-tag {
display: inline-flex; align-items: center; gap: 5px;
background: rgba(255,255,255,.1); border: 1px solid rgba(255,255,255,.2);
backdrop-filter: blur(6px); padding: 4px 10px; border-radius: 20px;
font-size: 10.5px; color: rgba(255,255,255,.8); font-weight: 600; margin-bottom: 14px;
}
.loc-tag svg { width: 11px; height: 11px; }
/* ── RIGHT PANEL ── */
.right {
flex: 1; background: #fff;
display: flex; flex-direction: column; justify-content: center;
padding: 36px 44px; overflow-y: auto;
}
/* ── BACK BUTTON ── */
.btn-back {
display: inline-flex; align-items: center; gap: 6px;
font-size: 12px; font-weight: 700; color: #64748b; text-decoration: none;
background: #f8fafc; border: 1.5px solid #e2e8f0; padding: 6px 12px;
border-radius: 10px; transition: background .15s, color .15s, border-color .15s;
margin-bottom: 20px; width: fit-content;
}
.btn-back:hover { background: #fff; color: #2563eb; border-color: #bfdbfe; }
.btn-back svg { width: 13px; height: 13px; flex-shrink: 0; }
/* ── LOGO + HEADER ── */
.fp-logo { display: flex; justify-content: center; margin-bottom: 12px; }
.fp-logo img { width: 46px; height: 46px; border-radius: 13px; object-fit: cover; box-shadow: 0 8px 20px rgba(37,99,235,.15); }
.fp-eyebrow { text-align: center; font-size: 11px; font-weight: 800; color: #2563eb; text-transform: uppercase; letter-spacing: .08em; margin-bottom: 5px; }
.fp-title { text-align: center; font-size: 22px; font-weight: 900; color: #0f172a; letter-spacing: -.03em; line-height: 1.1; margin-bottom: 6px; }
.fp-sub { font-size: 12.5px; line-height: 1.7; color: #64748b; text-align: center; margin-bottom: 18px; }
/* ── STEP INDICATOR ── */
.fp-steps { display: flex; align-items: center; justify-content: center; margin-bottom: 18px; }
.fp-step { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.fp-step-dot {
width: 26px; height: 26px; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 11px; font-weight: 800;
border: 2px solid #e2e8f0; color: #94a3b8; background: #f8fafc; transition: all .2s;
}
.fp-step-dot.active { background: #2563eb; border-color: #2563eb; color: #fff; box-shadow: 0 4px 12px rgba(37,99,235,.3); }
.fp-step-dot.done { background: #10b981; border-color: #10b981; color: #fff; }
.fp-step-label { font-size: 10px; font-weight: 600; color: #94a3b8; white-space: nowrap; }
.fp-step-label.active { color: #2563eb; }
.fp-step-label.done { color: #10b981; }
.fp-step-line { width: 44px; height: 2px; background: #e2e8f0; margin: 0 4px; margin-bottom: 18px; border-radius: 99px; transition: background .2s; }
.fp-step-line.done { background: #10b981; }
/* ── ALERTS ── */
.fp-alert {
margin-bottom: 13px; padding: 10px 13px; border-radius: 11px;
font-size: 12px; font-weight: 600; display: flex; align-items: center; gap: 8px;
}
.fp-alert svg { width: 14px; height: 14px; flex-shrink: 0; }
.fp-alert.success { background: #f0fdf4; color: #166534; border: 1px solid #bbf7d0; }
.fp-alert.error { background: #fff1f2; color: #9f1239; border: 1px solid #fecdd3; }
/* ── FIELDS ── */
.fp-field { margin-bottom: 13px; }
.fp-label { display: block; margin-bottom: 6px; font-size: 12px; font-weight: 700; color: #334155; letter-spacing: .01em; }
.fp-input-wrap { position: relative; }
.fp-input-icon {
position: absolute; left: 12px; top: 50%; transform: translateY(-50%);
width: 28px; height: 28px; border-radius: 8px; background: #eff6ff;
display: flex; align-items: center; justify-content: center; pointer-events: none;
}
.fp-input-icon svg { width: 13px; height: 13px; stroke: #2563eb; }
.fp-input {
width: 100%; height: 46px; padding: 0 14px 0 50px;
border: 1.5px solid #e2e8f0; border-radius: 12px;
font-size: 16px; color: #0f172a; background: #f8fafc;
outline: none; font-family: inherit;
transition: border-color .15s, box-shadow .15s, background .15s;
}
@media(min-width:640px){ .fp-input { font-size: 13.5px; } }
.fp-input:focus { border-color: #93c5fd; background: #fff; box-shadow: 0 0 0 3px rgba(37,99,235,.10); }
.fp-input::placeholder { color: #cbd5e1; }
.fp-input-toggle {
position: absolute; right: 12px; top: 50%; transform: translateY(-50%);
width: 26px; height: 26px; display: flex; align-items: center; justify-content: center;
cursor: pointer; color: #94a3b8; background: none; border: none;
border-radius: 7px; transition: color .15s, background .15s;
}
.fp-input-toggle:hover { color: #475569; background: #f1f5f9; }
.fp-input-toggle svg { width: 14px; height: 14px; }
.fp-hint { margin-top: 4px; font-size: 11px; color: #94a3b8; font-weight: 500; }
.fp-hint.error { color: #dc2626; }
.fp-strength { margin-top: 5px; display: flex; gap: 4px; }
.fp-strength-seg { flex: 1; height: 3px; border-radius: 99px; background: #e2e8f0; transition: background .2s; }
/* ── BUTTON ── */
.fp-btn {
width: 100%; height: 46px; border: none; border-radius: 12px;
background: linear-gradient(135deg, #3b82f6, #2563eb);
color: #fff; font-size: 13.5px; font-weight: 800; letter-spacing: -.01em;
cursor: pointer; font-family: inherit;
box-shadow: 0 6px 16px rgba(37,99,235,.28);
transition: transform .12s, box-shadow .12s;
display: flex; align-items: center; justify-content: center; gap: 8px;
}
.fp-btn:hover { transform: translateY(-1px); box-shadow: 0 10px 24px rgba(37,99,235,.34); }
.fp-btn:active { transform: scale(.98); }
.fp-btn:disabled { opacity: .55; cursor: not-allowed; transform: none; }
.fp-footer { margin-top: 14px; text-align: center; font-size: 12px; color: #64748b; }
.fp-footer a { color: #2563eb; text-decoration: none; font-weight: 700; }
.fp-footer a:hover { text-decoration: underline; }
.fp-section { display: none; }
.fp-section.visible { display: block; }
/* ── MOBILE ── */
@media(max-width:768px){
body { padding: 0; align-items: stretch; min-height: 100dvh; }
.shell { flex-direction: column; border-radius: 0; min-height: 100dvh; box-shadow: none; max-width: 100%; }
.left { display: none; }
.right { flex: 1; padding: 40px 24px 32px; justify-content: center; min-height: 100dvh; }
.fp-title { font-size: 20px; }
}
@media(max-width:390px){
.right { padding: 32px 18px 28px; }
}
</style>
</head>
<body>
<div class="shell">
{{-- ── LEFT PANEL ── --}}
<div class="left">
<div class="left-img"></div>
<div class="left-overlay"></div>
<div class="left-content">
<div class="left-brand">
<img src="{{ asset('favicon.ico') }}" alt="Logo SiBantuDes"
style="width:38px;height:38px;border-radius:11px;box-shadow:0 3px 10px rgba(0,0,0,.3);object-fit:cover;">
<div>
<div class="left-brand-name">SiBantuDes</div>
<div class="left-brand-sub">Sistem Bantuan Tunai Desa</div>
</div>
</div>
<div>
<div class="loc-tag">
<svg fill="none" stroke="currentColor" 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>
Desa Ngerong
</div>
<div class="left-caption">Penyaluran Bantuan<br>yang <span>Transparan</span><br>&amp; Akuntabel</div>
<div class="left-features">
<div class="feat"><div class="feat-dot"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7"/></svg></div><span>Pendataan warga calon penerima bantuan</span></div>
<div class="feat"><div class="feat-dot"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7"/></svg></div><span>Prediksi kelayakan berbasis Machine Learning</span></div>
<div class="feat"><div class="feat-dot"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7"/></svg></div><span>Laporan &amp; monitoring penyaluran bantuan</span></div>
</div>
</div>
</div>
</div>
<!-- Session Status -->
<x-auth-session-status class="mb-4" :status="session('status')" />
{{-- ── RIGHT PANEL ── --}}
<div class="right">
<form method="POST" action="{{ route('password.email') }}">
@csrf
{{-- <a href="{{ route('login') }}" class="btn-back">
<svg 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 ke Login
</a> --}}
<!-- Email Address -->
<div>
<x-input-label for="email" :value="__('Email')" />
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autofocus />
<x-input-error :messages="$errors->get('email')" class="mt-2" />
<div class="fp-logo">
<img src="{{ asset('favicon.ico') }}" alt="Logo SiBantuDes">
</div>
<div class="flex items-center justify-end mt-4">
<x-primary-button>
{{ __('Email Password Reset Link') }}
</x-primary-button>
<div class="fp-eyebrow">Reset Password</div>
<div class="fp-title" id="fp-title">Lupa Password?</div>
<div class="fp-sub" id="fp-sub">Masukkan email yang terdaftar untuk memulai reset password.</div>
<div class="fp-steps">
<div class="fp-step">
<div class="fp-step-dot active" id="dot-1">1</div>
<div class="fp-step-label active" id="lbl-1">Verifikasi</div>
</div>
<div class="fp-step-line" id="line-1"></div>
<div class="fp-step">
<div class="fp-step-dot" id="dot-2">2</div>
<div class="fp-step-label" id="lbl-2">Password Baru</div>
</div>
<div class="fp-step-line" id="line-2"></div>
<div class="fp-step">
<div class="fp-step-dot" id="dot-3">3</div>
<div class="fp-step-label" id="lbl-3">Selesai</div>
</div>
</div>
</form>
</x-guest-layout>
<div id="fp-alert-area"></div>
@if($errors->any())
<div class="fp-alert error">
<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>
{{ $errors->first() }}
</div>
@endif
{{-- STEP 1 --}}
<div class="fp-section visible" id="step-1">
<div class="fp-field">
<label class="fp-label" for="s1-email">Alamat Email</label>
<div class="fp-input-wrap">
<div class="fp-input-icon">
<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 id="s1-email" type="email" class="fp-input" placeholder="nama@email.com" autocomplete="email" autofocus>
</div>
<div class="fp-hint" id="s1-email-hint"></div>
</div>
<button class="fp-btn" id="btn-verify" onclick="verifyEmail()">
<svg width="15" height="15" 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>
Verifikasi Email
</button>
</div>
{{-- STEP 2 --}}
<div class="fp-section" id="step-2">
<div class="fp-field">
<label class="fp-label">Password Baru</label>
<div class="fp-input-wrap">
<div class="fp-input-icon">
<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 id="s2-password" type="password" class="fp-input" placeholder="Minimal 8 karakter" oninput="checkStrength()">
<button type="button" class="fp-input-toggle" onclick="togglePw('s2-password', this)">
<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>
</button>
</div>
<div class="fp-strength"><div class="fp-strength-seg" id="seg1"></div><div class="fp-strength-seg" id="seg2"></div><div class="fp-strength-seg" id="seg3"></div><div class="fp-strength-seg" id="seg4"></div></div>
<div class="fp-hint" id="s2-pw-hint"></div>
</div>
<div class="fp-field">
<label class="fp-label">Konfirmasi Password Baru</label>
<div class="fp-input-wrap">
<div class="fp-input-icon">
<svg fill="none" 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>
</div>
<input id="s2-confirm" type="password" class="fp-input" placeholder="Ulangi password baru" oninput="checkMatch()">
<button type="button" class="fp-input-toggle" onclick="togglePw('s2-confirm', this)">
<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>
</button>
</div>
<div class="fp-hint" id="s2-confirm-hint"></div>
</div>
<button class="fp-btn" id="btn-reset" onclick="submitReset()">
<svg width="15" height="15" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
Simpan Password Baru
</button>
</div>
{{-- STEP 3 --}}
<div class="fp-section" id="step-3" style="text-align:center;padding:8px 0;">
<div style="width:54px;height:54px;border-radius:50%;background:#f0fdf4;border:2px solid #bbf7d0;display:flex;align-items:center;justify-content:center;margin:0 auto 12px;">
<svg width="24" height="24" fill="none" stroke="#10b981" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7"/></svg>
</div>
<p style="font-size:14px;font-weight:800;color:#0f172a;margin-bottom:5px;">Password Berhasil Diubah!</p>
<p style="font-size:12.5px;color:#64748b;line-height:1.6;margin-bottom:16px;">Password akun Anda sudah diperbarui. Silakan login menggunakan password baru.</p>
<a href="{{ route('login') }}" style="display:inline-flex;align-items:center;justify-content:center;gap:8px;width:100%;height:46px;border-radius:12px;background:linear-gradient(135deg,#3b82f6,#2563eb);color:#fff;font-size:13.5px;font-weight:800;text-decoration:none;box-shadow:0 6px 16px 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" d="M11 16l-4-4m0 0l4-4m-4 4h14"/></svg>
Kembali ke Login
</a>
</div>
<div class="fp-footer" id="fp-footer-link">
Sudah ingat password? <a href="{{ route('login') }}">Kembali ke login</a>
</div>
</div>
</div>
<script>
var verifiedEmail = '';
function togglePw(inputId, btn) {
var inp = document.getElementById(inputId);
var showing = inp.type === 'text';
inp.type = showing ? 'password' : 'text';
btn.querySelector('svg').innerHTML = showing
? '<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"/>';
}
function checkStrength() {
var v = document.getElementById('s2-password').value;
var score = 0;
if (v.length >= 8) score++;
if (/[A-Z]/.test(v)) score++;
if (/[0-9]/.test(v)) score++;
if (/[^A-Za-z0-9]/.test(v)) score++;
var colors = ['#f43f5e','#f59e0b','#3b82f6','#10b981'];
var labels = ['Sangat lemah','Cukup','Kuat','Sangat kuat'];
for (var i = 1; i <= 4; i++) {
document.getElementById('seg'+i).style.background = i <= score ? colors[score-1] : '#e2e8f0';
}
var hint = document.getElementById('s2-pw-hint');
hint.textContent = v.length ? (labels[score-1] || '') : '';
hint.className = 'fp-hint';
checkMatch();
}
function checkMatch() {
var pw = document.getElementById('s2-password').value;
var cfm = document.getElementById('s2-confirm').value;
var hint = document.getElementById('s2-confirm-hint');
if (!cfm) { hint.textContent = ''; return; }
if (pw === cfm) { hint.textContent = '✓ Password cocok'; hint.className = 'fp-hint'; hint.style.color = '#10b981'; }
else { hint.textContent = 'Password tidak cocok'; hint.className = 'fp-hint error'; }
}
function showAlert(type, msg) {
var icon = type === 'success'
? '<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"/>'
: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01M12 3a9 9 0 100 18A9 9 0 0012 3z"/>';
document.getElementById('fp-alert-area').innerHTML =
'<div class="fp-alert '+type+'"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24">'+icon+'</svg>'+msg+'</div>';
}
function clearAlert() { document.getElementById('fp-alert-area').innerHTML = ''; }
function goStep(n) {
[1,2,3].forEach(function(i){
document.getElementById('step-'+i).classList.toggle('visible', i === n);
var dot = document.getElementById('dot-'+i);
var lbl = document.getElementById('lbl-'+i);
dot.className = 'fp-step-dot ' + (i < n ? 'done' : i === n ? 'active' : '');
lbl.className = 'fp-step-label ' + (i < n ? 'done' : i === n ? 'active' : '');
dot.innerHTML = i < n ? '&#10003;' : String(i);
});
document.getElementById('line-1').className = 'fp-step-line' + (n > 1 ? ' done' : '');
document.getElementById('line-2').className = 'fp-step-line' + (n > 2 ? ' done' : '');
var titles = ['','Lupa Password?','Password Baru','Selesai!'];
var subs = ['','Masukkan email yang terdaftar untuk memulai reset password.',
'Buat password baru untuk akun <strong>'+verifiedEmail+'</strong>.',''];
document.getElementById('fp-title').textContent = titles[n];
document.getElementById('fp-sub').innerHTML = subs[n];
document.getElementById('fp-footer-link').style.display = n === 3 ? 'none' : '';
}
function verifyEmail() {
var email = document.getElementById('s1-email').value.trim();
var hint = document.getElementById('s1-email-hint');
if (!email) { hint.textContent = 'Email wajib diisi.'; hint.className = 'fp-hint error'; return; }
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { hint.textContent = 'Format email tidak valid.'; hint.className = 'fp-hint error'; return; }
hint.textContent = '';
var btn = document.getElementById('btn-verify');
var spinSvg = '<svg width="15" height="15" fill="none" stroke="currentColor" viewBox="0 0 24 24" style="animation:spin .8s linear infinite"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>';
var checkSvg = '<svg width="15" height="15" 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>';
btn.disabled = true;
btn.innerHTML = spinSvg + ' Memverifikasi...';
fetch('{{ route("password.manual.check") }}', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}', 'Accept': 'application/json' },
body: JSON.stringify({ email: email })
})
.then(function(r){ return r.json().then(function(d){ return { ok: r.ok, data: d }; }); })
.then(function(res) {
btn.disabled = false;
btn.innerHTML = checkSvg + ' Verifikasi Email';
if (!res.ok) { showAlert('error', res.data.message || 'Terjadi kesalahan server.'); return; }
if (res.data.found) { verifiedEmail = email; clearAlert(); goStep(2); }
else showAlert('error', 'Email <strong>'+email+'</strong> tidak terdaftar dalam sistem.');
})
.catch(function(){ btn.disabled = false; btn.innerHTML = checkSvg + ' Verifikasi Email'; showAlert('error', 'Tidak dapat terhubung ke server.'); });
}
function submitReset() {
var pw = document.getElementById('s2-password').value;
var cfm = document.getElementById('s2-confirm').value;
if (pw.length < 8) { document.getElementById('s2-pw-hint').textContent = 'Password minimal 8 karakter.'; document.getElementById('s2-pw-hint').className = 'fp-hint error'; return; }
if (pw !== cfm) { document.getElementById('s2-confirm-hint').textContent = 'Password tidak cocok.'; document.getElementById('s2-confirm-hint').className = 'fp-hint error'; return; }
var btn = document.getElementById('btn-reset');
var spinSvg = '<svg width="15" height="15" fill="none" stroke="currentColor" viewBox="0 0 24 24" style="animation:spin .8s linear infinite"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>';
var checkSvg = '<svg width="15" height="15" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>';
btn.disabled = true;
btn.innerHTML = spinSvg + ' Menyimpan...';
fetch('{{ route("password.manual.update") }}', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}', 'Accept': 'application/json' },
body: JSON.stringify({ email: verifiedEmail, password: pw, password_confirmation: cfm })
})
.then(function(r){ return r.json().then(function(d){ return { ok: r.ok, data: d }; }); })
.then(function(res) {
btn.disabled = false;
btn.innerHTML = checkSvg + ' Simpan Password Baru';
if (res.data.success) { clearAlert(); goStep(3); }
else showAlert('error', res.data.message || 'Gagal menyimpan password.');
})
.catch(function(){ btn.disabled = false; btn.innerHTML = checkSvg + ' Simpan Password Baru'; showAlert('error', 'Tidak dapat terhubung ke server.'); });
}
document.addEventListener('keydown', function(e){
if (e.key !== 'Enter') return;
if (document.getElementById('step-1').classList.contains('visible')) verifyEmail();
else if (document.getElementById('step-2').classList.contains('visible')) submitReset();
});
var st = document.createElement('style');
st.textContent = '@keyframes spin{to{transform:rotate(360deg)}}';
document.head.appendChild(st);
</script>
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@ -244,7 +244,7 @@
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>
Kelurahan Ngerong
Desa Ngerong
</div>
<div class="left-caption">
Daftar &amp; Kelola<br>Data Warga <span>BLT-DD</span><br>Secara Digital
@ -264,7 +264,7 @@
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7"/>
</svg>
</div>
<span>Validasi &amp; pengajuan ke kelurahan</span>
<span>Validasi &amp; pengajuan ke Admin Prangkat Desa</span>
</div>
<div class="feat">
<div class="feat-dot">
@ -282,7 +282,7 @@
<!-- ── RIGHT PANEL ── -->
<div class="right">
<div class="form-eyebrow">SiBantuDes &bull; Kelurahan Ngerong</div>
<div class="form-eyebrow">SiBantuDes &bull; Desa Ngerong</div>
<div class="form-title">Daftar Akun RT</div>
<div class="form-sub">Lengkapi data untuk mendaftar ke sistem</div>

File diff suppressed because one or more lines are too long

View File

@ -1,128 +1,344 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ config('app.name', 'Laravel') }}</title>
<link rel="icon" type="image/png" href="{{ asset('Lambang_Kabupaten_Pasuruan.png') }}">
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet"/>
@vite(['resources/css/app.css', 'resources/js/app.js'])
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}
body{font-family:'Figtree',sans-serif;background:#f1f5f9;min-height:100dvh;-webkit-font-smoothing:antialiased;}
<title>{{ config('app.name', 'Laravel') }}</title>
/* ══ NAVBAR ══ */
#nb-wrap{
position:sticky;top:0;z-index:50;
padding:10px 16px 0;
transition:padding .25s;
}
#nb-wrap.scrolled{ padding-top:0; }
<link rel="icon" type="image/png" href="{{ asset('Lambang_Kabupaten_Pasuruan.png') }}">
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
#nb-shell{
max-width:80rem;margin:0 auto;
background:rgba(255,255,255,.96);
backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);
border:1.5px solid #e2e8f0;
border-radius:16px;
box-shadow:0 4px 24px rgba(15,23,42,.07);
padding:14px 22px;
transition:border-radius .25s,box-shadow .25s;
will-change:transform;
transform:translateZ(0);
}
#nb-wrap.scrolled #nb-shell{
border-radius:0 0 16px 16px;
border-top:none;
box-shadow:0 8px 32px rgba(15,23,42,.10);
}
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="font-sans antialiased">
<div class="min-h-screen bg-gray-100">
.nb-row{ display:flex;align-items:center;gap:12px; }
<div class="bg-white border-b border-gray-200 sticky top-0 z-50 shadow-sm">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-3">
<div class="flex items-center justify-between gap-4">
/* ── LINKS ── */
.nb-links{ display:flex;align-items:center;gap:8px;flex:1; }
@php
$role = Auth::user()->role ?? null;
@endphp
.nb-links a{
display:inline-flex;align-items:center;
padding:10px 18px;border-radius:12px;
font-size:15px;font-weight:700;
color:#475569;background:#f8fafc;
border:1.5px solid transparent;
text-decoration:none;white-space:nowrap;
letter-spacing:.01em;
transition:background .15s,color .15s,border-color .15s,box-shadow .15s,
max-width .2s ease, opacity .2s ease, padding .2s ease, margin .2s ease;
max-width:200px;
overflow:hidden;
opacity:1;
will-change:max-width, opacity;
}
.nb-links a:hover{ background:#fff;border-color:#e2e8f0;color:#1e293b; }
.nb-links a.nb-active{
background:#2563eb;color:#fff;
border-color:#2563eb;
box-shadow:0 3px 10px rgba(37,99,235,.25);
}
<div class="flex items-center gap-2 flex-1">
.nb-links.hide-inactive a:not(.nb-active){
max-width:0;opacity:0;
padding-left:0;padding-right:0;
margin:0;border-width:0;
pointer-events:none;
}
@if($role === 'admin')
<a href="{{ route('admin.dashboard') }}"
class="flex-1 text-center px-4 py-2 rounded-xl text-sm font-medium border
{{ request()->routeIs('admin.dashboard') ? 'bg-blue-600 text-white border-blue-600' : 'bg-white text-gray-700 border-gray-200 hover:bg-gray-50' }}">
Dashboard
</a>
/* ── USER ── */
.nb-user{ display:flex;align-items:center;gap:10px;flex-shrink:0; }
.nb-avatar{
width:40px;height:40px;border-radius:50%;
background:linear-gradient(135deg,#3b82f6,#2563eb);
display:flex;align-items:center;justify-content:center;
color:#fff;font-size:15px;font-weight:800;
box-shadow:0 2px 8px rgba(37,99,235,.22);flex-shrink:0;
}
.nb-name{ font-size:14px;font-weight:700;color:#374151;white-space:nowrap; }
.nb-logout{
font-size:14px;font-weight:700;color:#ef4444;
background:#fff1f2;border:1.5px solid #fecdd3;
padding:8px 16px;border-radius:10px;cursor:pointer;
transition:background .15s;white-space:nowrap;
}
.nb-logout:hover{ background:#ffe4e6; }
<a href="{{ route('admin.data-warga') }}"
class="flex-1 text-center px-4 py-2 rounded-xl text-sm font-medium border
{{ request()->routeIs('admin.data-warga') ? 'bg-blue-600 text-white border-blue-600' : 'bg-white text-gray-700 border-gray-200 hover:bg-gray-50' }}">
Data Warga
</a>
/* ══ MOBILE ══ */
@media(max-width:768px){
#nb-wrap{ padding:8px 8px 0; }
#nb-shell{ padding:10px 12px 12px; border-radius:14px; }
.nb-row{ flex-direction:column; align-items:stretch; gap:8px; }
<a href="{{ route('admin.data-akun') }}"
class="flex-1 text-center px-4 py-2 rounded-xl text-sm font-medium border
{{ request()->routeIs('admin.data-akun') ? 'bg-blue-600 text-white border-blue-600' : 'bg-white text-gray-700 border-gray-200 hover:bg-gray-50' }}">
Data Akun
</a>
/* Baris user: avatar + nama + tombol keluar */
.nb-user{
justify-content:space-between;
width:100%;
background:#f8fafc;
border:1.5px solid #e2e8f0;
border-radius:12px;
padding:10px 14px;
}
.nb-user > div:first-child{ display:flex; align-items:center; gap:10px; flex:1; }
.nb-avatar{ width:38px; height:38px; font-size:16px; flex-shrink:0; }
.nb-name{
display:block !important;
font-size:15px;
font-weight:700;
color:#1e293b;
}
.nb-logout{
font-size:14px;
font-weight:700;
padding:9px 18px;
border-radius:10px;
min-height:44px;
display:flex;
align-items:center;
white-space:nowrap;
flex-shrink:0;
}
<a href="{{ route('admin.filterisasi') }}"
class="flex-1 text-center px-4 py-2 rounded-xl text-sm font-medium border
{{ request()->routeIs('admin.filterisasi') ? 'bg-blue-600 text-white border-blue-600' : 'bg-white text-gray-700 border-gray-200 hover:bg-gray-50' }}">
Filterisasi
</a>
/* Nav links: scroll horizontal, semua label tampil penuh */
.nb-links{
overflow-x:auto;
-webkit-overflow-scrolling:touch;
scrollbar-width:none;
gap:6px;
padding-bottom:2px;
display:flex !important;
flex-wrap:nowrap;
}
.nb-links::-webkit-scrollbar{ display:none; }
.nb-links a{
font-size:14px !important;
font-weight:700 !important;
padding:12px 20px !important;
border-radius:10px !important;
min-height:48px !important;
display:inline-flex !important;
align-items:center !important;
white-space:nowrap !important;
/* Paksa tampil semua — override hide-inactive */
max-width:none !important;
opacity:1 !important;
border-width:1.5px !important;
pointer-events:auto !important;
overflow:visible !important;
}
/* Paksa semua link tetap terlihat di mobile */
.nb-links.hide-inactive a:not(.nb-active){
max-width:none !important;
opacity:1 !important;
padding:12px 20px !important;
margin:0 !important;
border-width:1.5px !important;
pointer-events:auto !important;
}
}
<a href="{{ route('admin.laporan') }}"
class="flex-1 text-center px-4 py-2 rounded-xl text-sm font-medium border
{{ request()->routeIs('admin.laporan') ? 'bg-blue-600 text-white border-blue-600' : 'bg-white text-gray-700 border-gray-200 hover:bg-gray-50' }}">
Laporan
</a>
@media(max-width:400px){
.nb-links a{ font-size:13px !important; padding:11px 16px !important; min-height:44px !important; }
.nb-logout{ font-size:13px; padding:9px 14px; }
.nb-name{ font-size:14px; }
}
@else
<a href="{{ route('dashboard') }}"
class="flex-1 text-center px-4 py-2 rounded-xl text-sm font-medium border
{{ request()->routeIs('dashboard') ? 'bg-blue-600 text-white border-blue-600' : 'bg-white text-gray-700 border-gray-200 hover:bg-gray-50' }}">
Dashboard
</a>
/* ══ LAYOUT ══ */
.page-header{ max-width:80rem;margin:14px auto 0;padding:0 16px; }
.page-header-inner{
background:#fff;border:1.5px solid #e2e8f0;border-radius:16px;
box-shadow:0 2px 12px rgba(15,23,42,.05);padding:14px 18px;
}
.page-main{
max-width:80rem;margin:14px auto 24px;padding:0 16px;
display:flex;flex-direction:column;gap:14px;
}
<a href="{{ route('rt.calon-penerima.create') }}"
class="flex-1 text-center px-4 py-2 rounded-xl text-sm font-medium border
{{ request()->routeIs('rt.calon-penerima.create') ? 'bg-blue-600 text-white border-blue-600' : 'bg-white text-gray-700 border-gray-200 hover:bg-gray-50' }}">
Pendataan
</a>
@media(max-width:768px){
.page-header{ padding:0 10px;margin-top:12px; }
.page-header-inner{ padding:14px 16px;border-radius:14px; }
.page-main{ padding:0 10px;margin-top:12px;margin-bottom:24px;gap:12px; }
}
</style>
</head>
<body>
<a href="{{ route('rt.calon-penerima.index') }}"
class="flex-1 text-center px-4 py-2 rounded-xl text-sm font-medium border
{{ request()->routeIs('rt.calon-penerima.index') ? 'bg-blue-600 text-white border-blue-600' : 'bg-white text-gray-700 border-gray-200 hover:bg-gray-50' }}">
Riwayat
</a>
<!-- ══ NAVBAR ══ -->
<div id="nb-wrap">
<div id="nb-shell">
<div class="nb-row">
<a href="{{ route('rt.laporan.index') }}"
class="flex-1 text-center px-4 py-2 rounded-xl text-sm font-medium border
{{ request()->routeIs('rt.laporan.*') ? 'bg-blue-600 text-white border-blue-600' : 'bg-white text-gray-700 border-gray-200 hover:bg-gray-50' }}">
Laporan
</a>
@endif
</div>
<div class="flex items-center gap-3">
<div class="w-9 h-9 rounded-full bg-gray-200 flex items-center justify-center overflow-hidden">
<span class="text-sm font-semibold text-gray-700">
{{ strtoupper(substr(Auth::user()->name ?? 'U', 0, 1)) }}
</span>
</div>
<div class="text-sm text-gray-700 whitespace-nowrap">
{{ Auth::user()->name ?? '' }}
</div>
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit" class="text-sm text-red-500 hover:text-red-600">
Keluar
</button>
</form>
</div>
@php $role = Auth::user()->role ?? null; @endphp
<!-- USER -->
<div class="nb-user">
<div style="display:flex;align-items:center;gap:10px;">
<div class="nb-avatar">{{ strtoupper(substr(Auth::user()->name ?? 'U', 0, 1)) }}</div>
<span class="nb-name">{{ Auth::user()->name ?? '' }}</span>
</div>
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit" class="nb-logout">Keluar</button>
</form>
</div>
<!-- LINKS -->
<nav class="nb-links" id="nb-links">
@if($role === 'admin')
<a href="{{ route('admin.dashboard') }}"
class="{{ request()->routeIs('admin.dashboard') ? 'nb-active' : '' }}">Dashboard</a>
<a href="{{ route('admin.data-warga') }}"
class="{{ request()->routeIs('admin.data-warga') ? 'nb-active' : '' }}">Data Warga</a>
<a href="{{ route('admin.data-akun') }}"
class="{{ request()->routeIs('admin.data-akun') ? 'nb-active' : '' }}">Data Akun</a>
<a href="{{ route('admin.filterisasi') }}"
class="{{ request()->routeIs('admin.filterisasi') ? 'nb-active' : '' }}">Filterisasi</a>
<a href="{{ route('admin.laporan') }}"
class="{{ request()->routeIs('admin.laporan') ? 'nb-active' : '' }}">Laporan</a>
@else
<a href="{{ route('dashboard') }}"
class="{{ request()->routeIs('dashboard') ? 'nb-active' : '' }}">Dashboard</a>
<a href="{{ route('rt.calon-penerima.create') }}"
class="{{ request()->routeIs('rt.calon-penerima.create') ? 'nb-active' : '' }}">Inputkan Warga</a>
<a href="{{ route('rt.calon-penerima.index') }}"
class="{{ request()->routeIs('rt.calon-penerima.index') ? 'nb-active' : '' }}">Riwayat</a>
<a href="{{ route('rt.laporan.index') }}"
class="{{ request()->routeIs('rt.laporan.*') ? 'nb-active' : '' }}">Laporan</a>
@endif
</nav>
</div>
@isset($header)
<header class="bg-white shadow">
<div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
{{ $header }}
</div>
</header>
@endisset
<main>
<div class="py-8">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{{ $slot }}
</div>
</div>
</main>
</div>
</body>
</div>
<!-- HEADER SLOT -->
@isset($header)
<div class="page-header">
<div class="page-header-inner">{{ $header }}</div>
</div>
@endisset
<!-- MAIN -->
<main class="page-main">{{ $slot }}</main>
<script>
(function(){
var wrap = document.getElementById('nb-wrap');
var links = document.getElementById('nb-links');
/* ── scrolled class: IntersectionObserver, zero reflow ── */
var sentinel = document.createElement('div');
sentinel.style.cssText = 'position:absolute;top:1px;left:0;width:1px;height:1px;pointer-events:none;';
document.body.prepend(sentinel);
new IntersectionObserver(function(e){
e[0].isIntersecting
? wrap.classList.remove('scrolled')
: wrap.classList.add('scrolled');
}, {threshold:0}).observe(sentinel);
var MOBILE_BP = 768;
var lastY = window.scrollY;
var peakY = window.scrollY;
var hidden = false;
var rafPending = null;
var DEAD_ZONE = 8;
var UP_THRESHOLD = 32;
var BOTTOM_GUARD = 12;
function isMobile(){ return window.innerWidth <= MOBILE_BP; }
function maxScroll(){
return Math.max(0, document.documentElement.scrollHeight - window.innerHeight);
}
function applyHide(val){
if(val === hidden) return;
hidden = val;
if(val) links.classList.add('hide-inactive');
else links.classList.remove('hide-inactive');
}
function tick(){
rafPending = null;
/* Mobile: selalu tampilkan semua link, jangan collapse */
if(isMobile()){
applyHide(false);
lastY = window.scrollY;
peakY = window.scrollY;
return;
}
var y = window.scrollY;
var diff = y - lastY;
if(y > peakY) peakY = y;
/* Bottom guard */
if(y >= maxScroll() - BOTTOM_GUARD){
if(y > 50) applyHide(true);
lastY = y;
return;
}
/* Dead zone */
if(Math.abs(diff) < DEAD_ZONE){
lastY = y;
return;
}
if(diff > 0 && y > 50){
applyHide(true);
} else if(diff < 0 && hidden){
if(peakY - y >= UP_THRESHOLD){
applyHide(false);
peakY = y;
}
}
lastY = y;
}
window.addEventListener('scroll', function(){
if(rafPending) return;
rafPending = requestAnimationFrame(tick);
}, {passive:true});
/* Resize: kalau user putar layar dari landscape ke portrait, reset state */
window.addEventListener('resize', function(){
if(isMobile()) applyHide(false);
}, {passive:true});
})();
</script>
</body>
</html>

View File

@ -1,30 +1,40 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ config('app.name', 'Laravel') }}</title>
<!-- GANTI TITLE -->
<title>SiBantuDes</title>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
<!-- Scripts -->
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="font-sans text-gray-900 antialiased">
<div class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100">
<div>
<a href="/">
<x-application-logo class="w-20 h-20 fill-current text-gray-500" />
</a>
</div>
<!-- Scripts -->
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white shadow-md overflow-hidden sm:rounded-lg">
{{ $slot }}
</div>
<body class="font-sans text-gray-900 antialiased">
<!-- HAPUS LOGO LARAVEL -->
<div class="min-h-screen flex flex-col justify-center items-center bg-gray-100">
<!-- INI DIHAPUS
<div>
<a href="/">
<x-application-logo class="w-20 h-20 fill-current text-gray-500" />
</a>
</div>
</body>
</html>
-->
<!-- ISI HALAMAN (LOGIN / FORGOT PASSWORD) -->
<div class="w-full sm:max-w-md px-6 py-4 bg-white shadow-md overflow-hidden sm:rounded-lg">
{{ $slot }}
</div>
</div>
</body>
</html>

View File

@ -6,7 +6,28 @@
</div>
</x-slot>
{{-- TAMPILKAN ERROR VALIDASI --}}
<style>
.input-field { font-size: 16px !important; }
@media (min-width: 640px) { .input-field { font-size: 14px !important; } }
@media (max-width: 480px) {
.action-row { flex-direction: column-reverse !important; }
.action-row a, .action-row button { width: 100%; justify-content: center; text-align: center; }
}
@media (max-width: 640px) {
.section-body { padding: 14px 14px !important; }
.section-head { padding: 10px 14px !important; }
}
@keyframes spin { to { transform: rotate(360deg); } }
/* Checkbox aset */
.aset-checkbox-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 8px; }
.aset-checkbox-item { display: flex; align-items: center; gap: 8px; padding: 8px 12px;
border: 1px solid #e5e7eb; border-radius: 10px; cursor: pointer; transition: background .15s, border-color .15s; }
.aset-checkbox-item:has(input:checked) { background: #eff6ff; border-color: #93c5fd; }
.aset-checkbox-item input[type=checkbox] { accent-color: #2563eb; width: 15px; height: 15px; flex-shrink: 0; }
.aset-checkbox-item span { font-size: 13px; color: #374151; }
</style>
@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>
@ -18,185 +39,106 @@
</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
<form action="{{ route('rt.calon-penerima.store') }}" method="POST" id="formPendataan" novalidate>
<form action="{{ route('rt.calon-penerima.store') }}" method="POST" id="formPendataan" novalidate enctype="multipart/form-data">
@csrf
{{-- 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">
<div class="section-head 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>
<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="section-body 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>
{{-- 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
>
<label class="block text-xs font-semibold text-gray-600 mb-1">RT <span class="text-red-500">*</span></label>
<select id="rt_id_display" class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm bg-gray-100" disabled>
@foreach(($rts ?? []) as $rt)
<option
value="{{ $rt->id }}"
{{ (string)$rt->id === (string)$myRtId ? 'selected' : '' }}
>
<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 inputmode="numeric">
<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 inputmode="numeric">
<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" id="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="">-- Pilih --</option>
<option value="Laki-laki">Laki-laki</option>
<option value="Perempuan">Perempuan</option>
</select>
<p class="error-msg text-xs text-red-500 mt-1 hidden">Jenis kelamin wajib dipilih.</p>
</div>
{{-- 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 inputmode="numeric">
<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>
@ -210,158 +152,283 @@ class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm fo
{{-- SECTION 2: Data Tempat Tinggal --}}
<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">
<div class="section-head 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-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"
<div class="section-body p-5 grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="field-group md:col-span-2 lg:col-span-1">
<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>
@php
$myDusunName = auth()->user()->rt->dusun->nama_dusun ?? '';
@endphp
@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>
{{-- 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 --}}
<label class="block text-xs font-semibold text-gray-600 mb-1">Dusun <span class="text-red-500">*</span></label>
<input type="text" id="desa_display"
class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm bg-gray-100"
value="{{ $myDusunName }}" readonly>
<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"
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
>
<p class="error-msg text-xs text-red-500 mt-1 hidden">Aset kepemilikan wajib diisi.</p>
</div>
</div>
</div>
{{-- SECTION 3: Data Ekonomi --}}
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 mb-5 overflow-hidden">
<div class="px-5 py-3 border-b border-gray-100 flex items-center gap-2 bg-gray-50">
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 mb-4 overflow-hidden">
<div class="section-head 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-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{{-- PEKERJAAN --}}
<div class="section-body p-5 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<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"
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
>
<p class="error-msg text-xs text-red-500 mt-1 hidden">Pekerjaan wajib diisi.</p>
<label class="block text-xs font-semibold text-gray-600 mb-1">Pekerjaan <span class="text-red-500">*</span></label>
<select 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">
<option value="">-- Pilih --</option>
<option value="Tidak Bekerja">Tidak Bekerja</option>
<option value="Buruh Harian Lepas">Buruh Harian Lepas</option>
<option value="Petani Kecil">Petani Kecil</option>
<option value="Mengurus Rumah Tangga">Mengurus Rumah Tangga</option>
<option value="Pedagang">Pedagang</option>
<option value="Wiraswasta">Wiraswasta</option>
<option value="Karyawan Swasta">Karyawan Swasta</option>
<option value="Guru Honorer">Guru Honorer</option>
<option value="PNS">PNS</option>
<option value="TNI/Polri">TNI/Polri</option>
</select>
<p class="error-msg text-xs text-red-500 mt-1 hidden">Pekerjaan wajib dipilih.</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="text" id="penghasilan_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-50"
placeholder="0"
required
>
placeholder="0" inputmode="numeric" autocomplete="off">
<input type="hidden" name="penghasilan" id="penghasilan">
<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 inputmode="numeric">
<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" id="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>
{{-- Aset Kepemilikan checkbox + manual --}}
<div class="section-body px-5 pb-5">
<label class="block text-xs font-semibold text-gray-600 mb-2">
Aset Kepemilikan <span class="text-red-500">*</span>
<span class="text-gray-400 font-normal ml-1">(centang semua yang dimiliki)</span>
</label>
<div class="aset-checkbox-grid mb-3" id="aset-checkbox-grid">
@php
$asetOptions = [
'motor' => 'Motor',
'motor tua' => 'Motor Tua',
'tanah' => 'Tanah',
'emas' => 'Emas',
'mobil' => 'Mobil',
'rumah' => 'Rumah',
'sepeda' => 'Sepeda',
'ternak' => 'Ternak',
];
@endphp
@foreach($asetOptions as $val => $label)
<label class="aset-checkbox-item">
<input type="checkbox" name="aset_pilihan[]" value="{{ $val }}">
<span>{{ $label }}</span>
</label>
@endforeach
</div>
<div class="flex flex-col gap-1">
<label class="text-xs text-gray-500">Aset lain (tulis manual, pisahkan dengan koma):</label>
<input type="text" name="aset_manual" id="aset_manual"
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="Contoh: sawah, kebun, mesin jahit">
</div>
<p class="error-msg-aset text-xs text-red-500 mt-1 hidden">Aset kepemilikan wajib diisi (centang minimal satu atau isi kolom manual).</p>
</div>
</div>
{{-- SECTION 4: Kondisi Tempat Tinggal --}}
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 mb-4 overflow-hidden">
<div class="section-head px-5 py-3 border-b border-gray-100 flex items-center gap-2 bg-gray-50">
<div class="w-1 h-4 bg-purple-500 rounded-full"></div>
<h3 class="text-sm font-semibold text-gray-700">Kondisi Tempat Tinggal</h3>
</div>
<div class="section-body p-5 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{{-- Lantai Rumah --}}
<div class="field-group">
<label class="block text-xs font-semibold text-gray-600 mb-1">Lantai Rumah <span class="text-red-500">*</span></label>
<select name="lantai_rumah" id="lantai_rumah"
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="Tanah">Tanah</option>
<option value="Papan/Kayu">Papan / Kayu</option>
<option value="Semen Kasar">Semen Kasar</option>
<option value="Keramik Murah">Keramik Murah</option>
<option value="Keramik">Keramik</option>
</select>
<p class="error-msg text-xs text-red-500 mt-1 hidden">Lantai rumah wajib dipilih.</p>
</div>
{{-- Dinding Rumah --}}
<div class="field-group">
<label class="block text-xs font-semibold text-gray-600 mb-1">Dinding Rumah <span class="text-red-500">*</span></label>
<select name="dinding_rumah" id="dinding_rumah"
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="Bambu">Bambu</option>
<option value="Anyaman Bambu/Gedek">Anyaman Bambu / Gedek</option>
<option value="Kayu/Papan">Kayu / Papan</option>
<option value="Tembok Tidak Plester">Tembok Tidak Plester</option>
<option value="Tembok Plester">Tembok Plester</option>
</select>
<p class="error-msg text-xs text-red-500 mt-1 hidden">Dinding rumah wajib dipilih.</p>
</div>
{{-- Atap Rumah --}}
<div class="field-group">
<label class="block text-xs font-semibold text-gray-600 mb-1">Atap Rumah <span class="text-red-500">*</span></label>
<select name="atap_rumah" id="atap_rumah"
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="Bambu/Jerami">Bambu / Jerami</option>
<option value="Seng">Seng</option>
<option value="Campuran Seng+Genteng">Campuran Seng + Genteng</option>
<option value="Genteng">Genteng</option>
<option value="Genteng Beton">Genteng Beton</option>
</select>
<p class="error-msg text-xs text-red-500 mt-1 hidden">Atap rumah wajib dipilih.</p>
</div>
<div class="field-group">
<label class="block text-xs font-semibold text-gray-600 mb-1">Luas Rumah () <span class="text-red-500">*</span></label>
<input type="number" name="luas_rumah_m2" id="luas_rumah_m2" min="1"
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="Contoh: 36" required inputmode="numeric">
<p class="error-msg text-xs text-red-500 mt-1 hidden">Luas rumah wajib diisi.</p>
</div>
<div class="field-group">
<label class="block text-xs font-semibold text-gray-600 mb-1">Status Kepemilikan Rumah <span class="text-red-500">*</span></label>
<select name="status_kepemilikan_rumah" id="status_kepemilikan_rumah"
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="Menumpang">Menumpang</option>
<option value="Sewa/Kontrak">Sewa / Kontrak</option>
<option value="Milik Sendiri">Milik Sendiri</option>
</select>
<p class="error-msg text-xs text-red-500 mt-1 hidden">Status kepemilikan rumah wajib dipilih.</p>
</div>
<div class="field-group">
<label class="block text-xs font-semibold text-gray-600 mb-1">Meteran Listrik <span class="text-red-500">*</span></label>
<select name="meteran_listrik" id="meteran_listrik"
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="450VA">450 VA</option>
<option value="900VA">900 VA</option>
<option value="1300VA+">1300 VA ke atas</option>
</select>
<p class="error-msg text-xs text-red-500 mt-1 hidden">Meteran listrik wajib dipilih.</p>
</div>
<div class="field-group">
<label class="block text-xs font-semibold text-gray-600 mb-1">Sumber Air <span class="text-red-500">*</span></label>
<select name="sumber_air" id="sumber_air"
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="Sungai">Sungai</option>
<option value="Sumur">Sumur</option>
<option value="PDAM">PDAM</option>
</select>
<p class="error-msg text-xs text-red-500 mt-1 hidden">Sumber air wajib dipilih.</p>
</div>
</div>
</div>
{{-- SECTION 5: Upload Dokumen & Foto --}}
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 mb-5 overflow-hidden">
<div class="section-head px-5 py-3 border-b border-gray-100 flex items-center gap-2 bg-gray-50">
<div class="w-1 h-4 bg-rose-500 rounded-full"></div>
<h3 class="text-sm font-semibold text-gray-700">Upload Dokumen & Foto</h3>
</div>
<div class="section-body p-5 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div class="field-group">
<label class="block text-xs font-semibold text-gray-600 mb-1">Foto Rumah Depan</label>
<input type="file" name="foto_rumah_depan" accept="image/*" class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm bg-gray-50">
</div>
<div class="field-group">
<label class="block text-xs font-semibold text-gray-600 mb-1">Foto Rumah Belakang</label>
<input type="file" name="foto_rumah_belakang" accept="image/*" class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm bg-gray-50">
</div>
<div class="field-group">
<label class="block text-xs font-semibold text-gray-600 mb-1">Foto Rumah Kanan</label>
<input type="file" name="foto_rumah_kanan" accept="image/*" class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm bg-gray-50">
</div>
<div class="field-group">
<label class="block text-xs font-semibold text-gray-600 mb-1">Foto Rumah Kiri</label>
<input type="file" name="foto_rumah_kiri" accept="image/*" class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm bg-gray-50">
</div>
<div class="field-group">
<label class="block text-xs font-semibold text-gray-600 mb-1">Foto KK</label>
<input type="file" name="foto_kk" accept="image/*" class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm bg-gray-50">
</div>
<div class="field-group">
<label class="block text-xs font-semibold text-gray-600 mb-1">Foto KTP</label>
<input type="file" name="foto_ktp" accept="image/*" class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm bg-gray-50">
</div>
<div class="field-group">
<label class="block text-xs font-semibold text-gray-600 mb-1">Foto Rekening Listrik</label>
<input type="file" name="foto_rekening_listrik" accept="image/*,.pdf" class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm bg-gray-50">
</div>
<div class="field-group">
<label class="block text-xs font-semibold text-gray-600 mb-1">Foto Meteran Air</label>
<input type="file" name="foto_meteran_air" accept="image/*" class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm bg-gray-50">
</div>
<div class="field-group lg:col-span-2">
<label class="block text-xs font-semibold text-gray-600 mb-1">Dokumen Pendukung Lainnya</label>
<input type="file" name="dokumen_pendukung" accept="image/*,.pdf,.doc,.docx" class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm bg-gray-50">
</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"
>
<div class="action-row 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">
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"
>
Simpan Data
<button type="submit" id="btnSimpan"
style="display:inline-flex;align-items:center;gap:8px;"
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">
<svg id="btnSpinner" style="display:none;width:16px;height:16px;animation:spin .7s linear infinite;flex-shrink:0;" fill="none" viewBox="0 0 24 24">
<circle style="opacity:.25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
<path style="opacity:.75" fill="currentColor" d="M4 12a8 8 0 018-8v8z"/>
</svg>
<span id="btnText">Simpan Data</span>
</button>
</div>
</form>
@ -396,57 +463,89 @@ function showToast(msg) {
setTimeout(() => t.classList.add('hidden'), 3500);
}
// Validasi aset: minimal satu checkbox atau ada teks manual
function validateAset() {
const checked = document.querySelectorAll('input[name="aset_pilihan[]"]:checked').length > 0;
const manual = (document.getElementById('aset_manual')?.value ?? '').trim() !== '';
const errEl = document.querySelector('.error-msg-aset');
if (!checked && !manual) {
if (errEl) errEl.classList.remove('hidden');
document.getElementById('aset-checkbox-grid')?.classList.add('ring-1', 'ring-red-400', 'rounded-xl');
return false;
}
if (errEl) errEl.classList.add('hidden');
document.getElementById('aset-checkbox-grid')?.classList.remove('ring-1', 'ring-red-400', 'rounded-xl');
return true;
}
document.querySelectorAll('input[name="aset_pilihan[]"]').forEach(cb => {
cb.addEventListener('change', validateAset);
});
document.getElementById('aset_manual')?.addEventListener('input', validateAset);
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.',
usia: v => (parseInt(v) >= 0 && parseInt(v) <= 150) ? null : 'Usia harus antara 0150.',
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.',
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.',
jenis_kelamin: v => v !== '' ? null : 'Jenis kelamin wajib dipilih.',
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) >= 0 && parseInt(v) <= 150) ? null : 'Usia harus antara 0150.',
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.',
pekerjaan: v => v !== '' ? null : 'Pekerjaan wajib dipilih.',
penghasilan: v => (v !== '' && parseFloat(v) >= 0) ? null : 'Penghasilan tidak boleh negatif.',
jumlah_tanggungan: v => parseInt(v) >= 0 ? null : 'Jumlah tanggungan tidak boleh negatif.',
lantai_rumah: v => v !== '' ? null : 'Lantai rumah wajib dipilih.',
dinding_rumah: v => v !== '' ? null : 'Dinding rumah wajib dipilih.',
atap_rumah: v => v !== '' ? null : 'Atap rumah wajib dipilih.',
luas_rumah_m2: v => (parseInt(v) >= 1) ? null : 'Luas rumah wajib diisi.',
status_kepemilikan_rumah: v => v !== '' ? null : 'Status kepemilikan rumah wajib dipilih.',
meteran_listrik: v => v !== '' ? null : 'Meteran listrik wajib dipilih.',
sumber_air: v => v !== '' ? null : 'Sumber air wajib dipilih.',
};
Object.keys(rules).forEach(id => {
const el = document.getElementById(id);
if (!el) return;
el.addEventListener('blur', () => {
const err = rules[id](el.value);
err ? showError(id, err) : clearError(id);
});
el.addEventListener('input', () => {
if (!el.classList.contains('border-red-400')) return;
const err = rules[id](el.value);
err ? showError(id, err) : clearError(id);
});
el.addEventListener('change', () => {
const err = rules[id](el.value);
err ? showError(id, err) : clearError(id);
});
el.addEventListener('blur', () => { const e = rules[id](el.value); e ? showError(id, e) : clearError(id); });
el.addEventListener('input', () => { if (!el.classList.contains('border-red-400')) return; const e = rules[id](el.value); e ? showError(id, e) : clearError(id); });
el.addEventListener('change', () => { const e = rules[id](el.value); e ? showError(id, e) : clearError(id); });
});
// Auto-isi usia dari tanggal lahir
document.getElementById('tanggal_lahir')?.addEventListener('change', function () {
const dob = new Date(this.value);
const today = new Date();
let age = today.getFullYear() - dob.getFullYear();
const m = today.getMonth() - dob.getMonth();
if (m < 0 || (m === 0 && today.getDate() < dob.getDate())) age--;
if (age >= 0 && age <= 150) {
document.getElementById('usia').value = age;
clearError('usia');
}
if (age >= 0 && age <= 150) { document.getElementById('usia').value = age; clearError('usia'); }
});
// Submit validation
const displayInput = document.getElementById('penghasilan_display');
const hiddenInput = document.getElementById('penghasilan');
if (displayInput && hiddenInput) {
displayInput.addEventListener('input', function () {
let raw = this.value.replace(/\D/g, '');
this.value = raw ? parseInt(raw).toLocaleString('id-ID') : '';
hiddenInput.value = raw;
const e = rules['penghasilan'](hiddenInput.value);
e ? showError('penghasilan', e) : clearError('penghasilan');
});
displayInput.addEventListener('blur', function () {
const e = rules['penghasilan'](hiddenInput.value);
e ? showError('penghasilan', e) : clearError('penghasilan');
});
}
document.getElementById('formPendataan').addEventListener('submit', function (e) {
if (displayInput && hiddenInput && hiddenInput.value === '') {
hiddenInput.value = displayInput.value.replace(/\D/g, '');
}
let valid = true;
Object.keys(rules).forEach(id => {
const el = document.getElementById(id);
if (!el) return;
@ -454,12 +553,23 @@ function showToast(msg) {
if (err) { showError(id, err); valid = false; }
else clearError(id);
});
// Validasi aset
if (!validateAset()) valid = false;
if (!valid) {
e.preventDefault();
showToast('Mohon perbaiki field yang tidak valid sebelum menyimpan.');
const firstErr = document.querySelector('.border-red-400');
firstErr?.scrollIntoView({ behavior: 'smooth', block: 'center' });
document.querySelector('.border-red-400, .ring-red-400')?.scrollIntoView({ behavior: 'smooth', block: 'center' });
return;
}
const btn = document.getElementById('btnSimpan');
btn.disabled = true;
btn.style.opacity = '0.75';
btn.style.cursor = 'not-allowed';
document.getElementById('btnSpinner').style.display = 'block';
document.getElementById('btnText').textContent = 'Memproses & Memprediksi...';
});
</script>
</x-app-layout>

View File

@ -1,6 +1,6 @@
<x-app-layout>
<x-slot name="header">
<div class="flex items-center justify-between">
<div class="flex items-center justify-between flex-wrap gap-3">
<div>
<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>
@ -15,7 +15,51 @@ class="inline-flex items-center gap-1.5 px-4 py-2 bg-white border border-gray-20
</div>
</x-slot>
{{-- ERROR VALIDASI --}}
<style>
.input-field { font-size: 16px !important; }
@media (min-width: 640px) { .input-field { font-size: 14px !important; } }
.section-grid {
display: grid;
grid-template-columns: 1fr;
gap: 14px;
padding: 16px;
}
@media (min-width: 640px) {
.section-grid { grid-template-columns: 1fr 1fr; padding: 18px; gap: 16px; }
}
@media (min-width: 1024px) {
.section-grid { grid-template-columns: repeat(4, 1fr); padding: 20px; }
}
.col-span-2-up { grid-column: span 1; }
@media (min-width: 640px) { .col-span-2-up { grid-column: span 2; } }
.section-head { padding: 10px 16px; }
@media (min-width: 640px) { .section-head { padding: 10px 20px; } }
.action-row {
display: flex;
justify-content: flex-end;
gap: 10px;
flex-wrap: wrap;
}
@media (max-width: 480px) {
.action-row { flex-direction: column-reverse; }
.action-row a,
.action-row button { width: 100%; justify-content: center; text-align: center; }
}
@keyframes spin { to { transform: rotate(360deg); } }
.aset-checkbox-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 8px; }
.aset-checkbox-item { display: flex; align-items: center; gap: 8px; padding: 8px 12px;
border: 1px solid #e5e7eb; border-radius: 10px; cursor: pointer; transition: background .15s, border-color .15s; }
.aset-checkbox-item:has(input:checked) { background: #eff6ff; border-color: #93c5fd; }
.aset-checkbox-item input[type=checkbox] { accent-color: #2563eb; width: 15px; height: 15px; flex-shrink: 0; }
.aset-checkbox-item span { font-size: 13px; color: #374151; }
</style>
@if ($errors->any())
<div class="mb-4 flex items-start 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 mt-0.5 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@ -32,106 +76,95 @@ class="inline-flex items-center gap-1.5 px-4 py-2 bg-white border border-gray-20
</div>
@endif
<form action="{{ route('rt.calon-penerima.update', $calonPenerima->id) }}" method="POST" id="formEditPendataan" novalidate>
<form action="{{ route('rt.calon-penerima.update', $calonPenerima->id) }}" method="POST" id="formEditPendataan" novalidate enctype="multipart/form-data">
@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">
<div class="section-head 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>
<div class="p-5 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div class="section-grid">
{{-- RT (readonly) --}}
<div class="field-group">
<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>
class="w-full border border-gray-200 rounded-xl px-3 py-2 bg-gray-100 text-gray-700 cursor-not-allowed"
style="font-size:16px;" readonly>
<p class="text-[11px] text-gray-400 mt-1">RT mengikuti akun RT yang sedang login.</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"
value="{{ old('no_kk', $calonPenerima->no_kk) }}"
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>
class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-gray-50"
placeholder="16 digit No. KK" required inputmode="numeric">
<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"
value="{{ old('nik', $calonPenerima->nik) }}"
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>
class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-gray-50"
placeholder="16 digit NIK" required inputmode="numeric">
<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"
value="{{ old('nama_lengkap', $calonPenerima->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"
class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-gray-50"
placeholder="Nama sesuai KTP" required>
<p class="error-msg text-xs text-red-500 mt-1 hidden">Nama minimal 3 karakter.</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">
class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-gray-50">
<option value="Laki-laki" {{ old('jenis_kelamin', $calonPenerima->jenis_kelamin) === 'Laki-laki' ? 'selected' : '' }}>Laki-laki</option>
<option value="Perempuan" {{ old('jenis_kelamin', $calonPenerima->jenis_kelamin) === 'Perempuan' ? 'selected' : '' }}>Perempuan</option>
</select>
</div>
{{-- 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"
value="{{ old('tempat_lahir', $calonPenerima->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"
class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-gray-50"
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"
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"
class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 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>
</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="17" max="100"
value="{{ old('usia', $calonPenerima->usia) }}"
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="Tahun" required>
class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-gray-50"
placeholder="Tahun" required inputmode="numeric">
<p class="error-msg text-xs text-red-500 mt-1 hidden">Usia harus antara 17100 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">
class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-gray-50">
<option value="">-- Pilih --</option>
@foreach(['Belum Kawin','Kawin','Cerai Hidup','Cerai Mati'] as $sp)
<option value="{{ $sp }}" {{ old('status_perkawinan', $calonPenerima->status_perkawinan) === $sp ? 'selected' : '' }}>{{ $sp }}</option>
@ -145,66 +178,61 @@ class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm fo
{{-- SECTION 2: Data Tempat Tinggal --}}
<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">
<div class="section-head 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-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div class="section-grid">
<div class="field-group lg:col-span-2">
<div class="field-group col-span-2-up">
<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"
value="{{ old('alamat', $calonPenerima->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"
class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-gray-50"
placeholder="Jalan, nomor rumah, RT/RW" required>
<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</label>
<input type="text" name="desa" id="desa"
value="{{ old('desa', $calonPenerima->desa) }}"
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>
class="w-full border border-gray-200 rounded-xl px-3 py-2 bg-gray-100 text-gray-700 cursor-not-allowed"
style="font-size:16px;" readonly>
<p class="text-[11px] text-gray-400 mt-1">Dusun mengikuti RT dan tidak dapat diubah.</p>
</div>
<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"
value="{{ old('aset_kepemilikan', $calonPenerima->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>
<p class="error-msg text-xs text-red-500 mt-1 hidden">Aset kepemilikan wajib diisi.</p>
</div>
</div>
</div>
{{-- SECTION 3: Data Ekonomi --}}
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 mb-5 overflow-hidden">
<div class="px-5 py-3 border-b border-gray-100 flex items-center gap-2 bg-gray-50">
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 mb-4 overflow-hidden">
<div class="section-head 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-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div class="section-grid">
<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"
value="{{ old('pekerjaan', $calonPenerima->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>
<p class="error-msg text-xs text-red-500 mt-1 hidden">Pekerjaan wajib diisi.</p>
<select name="pekerjaan" id="pekerjaan"
class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-gray-50">
<option value="">-- Pilih --</option>
@foreach(['Tidak Bekerja','Buruh Harian Lepas','Petani Kecil','Mengurus Rumah Tangga','Pedagang','Wiraswasta','Karyawan Swasta','Guru Honorer','PNS','TNI/Polri'] as $pkj)
<option value="{{ $pkj }}" {{ old('pekerjaan', $calonPenerima->pekerjaan) === $pkj ? 'selected' : '' }}>{{ $pkj }}</option>
@endforeach
</select>
<p class="error-msg text-xs text-red-500 mt-1 hidden">Pekerjaan wajib dipilih.</p>
</div>
<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"
value="{{ old('penghasilan', $calonPenerima->penghasilan) }}"
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>
<input type="text" id="penghasilan_display"
value="{{ old('penghasilan', $calonPenerima->penghasilan) ? number_format((float) old('penghasilan', $calonPenerima->penghasilan), 0, ',', '.') : '' }}"
class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-gray-50"
placeholder="0" inputmode="numeric" autocomplete="off">
<input type="hidden" name="penghasilan" id="penghasilan"
value="{{ old('penghasilan', $calonPenerima->penghasilan) }}">
<p class="error-msg text-xs text-red-500 mt-1 hidden">Penghasilan tidak boleh negatif.</p>
</div>
@ -212,42 +240,237 @@ class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm fo
<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"
value="{{ old('jumlah_tanggungan', $calonPenerima->jumlah_tanggungan) }}"
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>
class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-gray-50"
placeholder="Jumlah orang" required inputmode="numeric">
<p class="error-msg text-xs text-red-500 mt-1 hidden">Jumlah tanggungan tidak boleh negatif.</p>
</div>
<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">
class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-gray-50">
<option value="tidak" {{ old('bantuan_lain', $calonPenerima->bantuan_lain) === 'tidak' ? 'selected' : '' }}>Tidak</option>
<option value="ya" {{ old('bantuan_lain', $calonPenerima->bantuan_lain) === 'ya' ? 'selected' : '' }}>Ya</option>
</select>
</div>
</div>
{{-- Aset Kepemilikan --}}
<div style="padding: 0 16px 16px;">
@php
$asetOptions = ['motor'=>'Motor','motor tua'=>'Motor Tua','tanah'=>'Tanah','emas'=>'Emas','mobil'=>'Mobil','rumah'=>'Rumah','sepeda'=>'Sepeda','ternak'=>'Ternak'];
$asetLama = strtolower(old('aset_kepemilikan', $calonPenerima->aset_kepemilikan ?? ''));
$asetLamaArr = array_map('trim', explode(',', $asetLama));
$asetManualVal = '';
$asetChecked = [];
foreach ($asetLamaArr as $item) {
if (array_key_exists($item, $asetOptions)) {
$asetChecked[] = $item;
} else if ($item !== '' && $item !== 'tidak punya') {
$asetManualVal .= ($asetManualVal ? ', ' : '') . $item;
}
}
@endphp
<label class="block text-xs font-semibold text-gray-600 mb-2">
Aset Kepemilikan <span class="text-red-500">*</span>
<span class="text-gray-400 font-normal ml-1">(centang semua yang dimiliki)</span>
</label>
<div class="aset-checkbox-grid mb-3" id="aset-checkbox-grid">
@foreach($asetOptions as $val => $label)
<label class="aset-checkbox-item">
<input type="checkbox" name="aset_pilihan[]" value="{{ $val }}"
{{ in_array($val, $asetChecked) ? 'checked' : '' }}>
<span>{{ $label }}</span>
</label>
@endforeach
</div>
<div class="flex flex-col gap-1">
<label class="text-xs text-gray-500">Aset lain (tulis manual, pisahkan dengan koma):</label>
<input type="text" name="aset_manual" id="aset_manual"
value="{{ $asetManualVal }}"
class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-gray-50"
placeholder="Contoh: sawah, kebun, mesin jahit">
</div>
<p class="error-msg-aset text-xs text-red-500 mt-1 hidden">Aset kepemilikan wajib diisi.</p>
</div>
</div>
{{-- SECTION 4: Kondisi Tempat Tinggal --}}
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 mb-4 overflow-hidden">
<div class="section-head border-b border-gray-100 flex items-center gap-2 bg-gray-50">
<div class="w-1 h-4 bg-purple-500 rounded-full"></div>
<h3 class="text-sm font-semibold text-gray-700">Kondisi Tempat Tinggal</h3>
</div>
<div class="section-grid" style="grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));">
<div class="field-group">
<label class="block text-xs font-semibold text-gray-600 mb-1">Lantai Rumah <span class="text-red-500">*</span></label>
<select name="lantai_rumah" id="lantai_rumah"
class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-gray-50">
<option value="">-- Pilih --</option>
@foreach(['Tanah','Papan/Kayu','Semen Kasar','Keramik Murah','Keramik'] as $opt)
<option value="{{ $opt }}" {{ old('lantai_rumah', $calonPenerima->lantai_rumah) === $opt ? 'selected' : '' }}>{{ $opt }}</option>
@endforeach
</select>
<p class="error-msg text-xs text-red-500 mt-1 hidden">Lantai rumah wajib dipilih.</p>
</div>
<div class="field-group">
<label class="block text-xs font-semibold text-gray-600 mb-1">Dinding Rumah <span class="text-red-500">*</span></label>
<select name="dinding_rumah" id="dinding_rumah"
class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-gray-50">
<option value="">-- Pilih --</option>
@foreach(['Bambu','Anyaman Bambu/Gedek','Kayu/Papan','Tembok Tidak Plester','Tembok Plester'] as $opt)
<option value="{{ $opt }}" {{ old('dinding_rumah', $calonPenerima->dinding_rumah) === $opt ? 'selected' : '' }}>{{ $opt }}</option>
@endforeach
</select>
<p class="error-msg text-xs text-red-500 mt-1 hidden">Dinding rumah wajib dipilih.</p>
</div>
<div class="field-group">
<label class="block text-xs font-semibold text-gray-600 mb-1">Atap Rumah <span class="text-red-500">*</span></label>
<select name="atap_rumah" id="atap_rumah"
class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-gray-50">
<option value="">-- Pilih --</option>
@foreach(['Bambu/Jerami','Seng','Campuran Seng+Genteng','Genteng','Genteng Beton'] as $opt)
<option value="{{ $opt }}" {{ old('atap_rumah', $calonPenerima->atap_rumah) === $opt ? 'selected' : '' }}>{{ $opt }}</option>
@endforeach
</select>
<p class="error-msg text-xs text-red-500 mt-1 hidden">Atap rumah wajib dipilih.</p>
</div>
<div class="field-group">
<label class="block text-xs font-semibold text-gray-600 mb-1">Luas Rumah () <span class="text-red-500">*</span></label>
<input type="number" name="luas_rumah_m2" id="luas_rumah_m2" min="1"
value="{{ old('luas_rumah_m2', $calonPenerima->luas_rumah_m2) }}"
class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-gray-50"
placeholder="Contoh: 36" required inputmode="numeric">
<p class="error-msg text-xs text-red-500 mt-1 hidden">Luas rumah wajib diisi.</p>
</div>
<div class="field-group">
<label class="block text-xs font-semibold text-gray-600 mb-1">Status Kepemilikan Rumah <span class="text-red-500">*</span></label>
<select name="status_kepemilikan_rumah" id="status_kepemilikan_rumah"
class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-gray-50">
<option value="">-- Pilih --</option>
@foreach(['Menumpang','Sewa/Kontrak','Milik Sendiri'] as $opt)
<option value="{{ $opt }}" {{ old('status_kepemilikan_rumah', $calonPenerima->status_kepemilikan_rumah) === $opt ? 'selected' : '' }}>{{ $opt }}</option>
@endforeach
</select>
<p class="error-msg text-xs text-red-500 mt-1 hidden">Status kepemilikan rumah wajib dipilih.</p>
</div>
<div class="field-group">
<label class="block text-xs font-semibold text-gray-600 mb-1">Meteran Listrik <span class="text-red-500">*</span></label>
<select name="meteran_listrik" id="meteran_listrik"
class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-gray-50">
<option value="">-- Pilih --</option>
@foreach(['450VA'=>'450 VA','900VA'=>'900 VA','1300VA+'=>'1300 VA ke atas'] as $val => $lbl)
<option value="{{ $val }}" {{ old('meteran_listrik', $calonPenerima->meteran_listrik) === $val ? 'selected' : '' }}>{{ $lbl }}</option>
@endforeach
</select>
<p class="error-msg text-xs text-red-500 mt-1 hidden">Meteran listrik wajib dipilih.</p>
</div>
<div class="field-group">
<label class="block text-xs font-semibold text-gray-600 mb-1">Sumber Air <span class="text-red-500">*</span></label>
<select name="sumber_air" id="sumber_air"
class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-gray-50">
<option value="">-- Pilih --</option>
@foreach(['Sungai','Sumur','PDAM'] as $opt)
<option value="{{ $opt }}" {{ old('sumber_air', $calonPenerima->sumber_air) === $opt ? 'selected' : '' }}>{{ $opt }}</option>
@endforeach
</select>
<p class="error-msg text-xs text-red-500 mt-1 hidden">Sumber air wajib dipilih.</p>
</div>
</div>
</div>
{{-- SECTION 5: Upload Foto --}}
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 mb-5 overflow-hidden">
<div class="section-head border-b border-gray-100 flex items-center gap-2 bg-gray-50">
<div class="w-1 h-4 bg-rose-500 rounded-full"></div>
<h3 class="text-sm font-semibold text-gray-700">Upload Dokumen & Foto</h3>
<span class="text-xs text-gray-400 font-normal">(kosongkan jika tidak ingin mengganti)</span>
</div>
<div class="section-grid" style="grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));">
@php
$fotoFields = [
'foto_rumah_depan' => 'Foto Rumah Depan',
'foto_rumah_belakang' => 'Foto Rumah Belakang',
'foto_rumah_kanan' => 'Foto Rumah Kanan',
'foto_rumah_kiri' => 'Foto Rumah Kiri',
'foto_kk' => 'Foto KK',
'foto_ktp' => 'Foto KTP',
'foto_meteran_air' => 'Foto Meteran Air',
];
@endphp
@foreach($fotoFields as $field => $label)
<div class="field-group">
<label class="block text-xs font-semibold text-gray-600 mb-1">{{ $label }}</label>
@if($calonPenerima->$field)
<div class="mb-1">
<img src="{{ asset('storage/' . $calonPenerima->$field) }}"
class="w-full h-24 object-cover rounded-lg border border-gray-200">
</div>
@endif
<input type="file" name="{{ $field }}" accept="image/*"
class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm bg-gray-50">
</div>
@endforeach
<div class="field-group">
<label class="block text-xs font-semibold text-gray-600 mb-1">Foto Rekening Listrik</label>
@if($calonPenerima->foto_rekening_listrik)
<div class="mb-1 text-xs text-blue-600">
<a href="{{ asset('storage/' . $calonPenerima->foto_rekening_listrik) }}" target="_blank">Lihat file saat ini</a>
</div>
@endif
<input type="file" name="foto_rekening_listrik" accept="image/*,.pdf"
class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm bg-gray-50">
</div>
<div class="field-group">
<label class="block text-xs font-semibold text-gray-600 mb-1">Dokumen Pendukung Lainnya</label>
@if($calonPenerima->dokumen_pendukung)
<div class="mb-1 text-xs text-blue-600">
<a href="{{ asset('storage/' . $calonPenerima->dokumen_pendukung) }}" target="_blank">Lihat file saat ini</a>
</div>
@endif
<input type="file" name="dokumen_pendukung" accept="image/*,.pdf,.doc,.docx"
class="input-field w-full border border-gray-200 rounded-xl px-3 py-2 text-sm bg-gray-50">
</div>
</div>
</div>
{{-- ACTIONS --}}
<div class="flex justify-end gap-3">
<div class="action-row">
<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">
class="inline-flex items-center gap-1.5 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="inline-flex items-center gap-2 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">
<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="M5 13l4 4L19 7"/>
<button type="submit" id="btnSimpan"
style="display:inline-flex;align-items:center;gap:8px;"
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">
<svg id="btnSpinner"
style="display:none;width:16px;height:16px;animation:spin .7s linear infinite;flex-shrink:0;"
fill="none" viewBox="0 0 24 24">
<circle style="opacity:.25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
<path style="opacity:.75" fill="currentColor" d="M4 12a8 8 0 018-8v8z"/>
</svg>
Simpan Perubahan
<span id="btnText">Simpan Perubahan</span>
</button>
</div>
</form>
{{-- TOAST --}}
<div id="toast" class="fixed bottom-5 right-5 z-50 hidden">
<div id="toast" class="fixed bottom-5 right-5 z-50 hidden" style="max-width:calc(100vw - 40px);">
<div class="flex items-center gap-3 bg-red-500 text-white text-sm font-medium px-4 py-3 rounded-xl shadow-lg">
<svg class="w-4 h-4 shrink-0" 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"/>
@ -263,14 +486,12 @@ 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 +499,43 @@ function showToast(msg) {
setTimeout(() => t.classList.add('hidden'), 3500);
}
function validateAset() {
const checked = document.querySelectorAll('input[name="aset_pilihan[]"]:checked').length > 0;
const manual = (document.getElementById('aset_manual')?.value ?? '').trim() !== '';
const errEl = document.querySelector('.error-msg-aset');
if (!checked && !manual) {
if (errEl) errEl.classList.remove('hidden');
document.getElementById('aset-checkbox-grid')?.classList.add('ring-1', 'ring-red-400', 'rounded-xl');
return false;
}
if (errEl) errEl.classList.add('hidden');
document.getElementById('aset-checkbox-grid')?.classList.remove('ring-1', 'ring-red-400', 'rounded-xl');
return true;
}
document.querySelectorAll('input[name="aset_pilihan[]"]').forEach(cb => cb.addEventListener('change', validateAset));
document.getElementById('aset_manual')?.addEventListener('input', validateAset);
const rules = {
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.',
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.',
pekerjaan: v => v !== '' ? null : 'Pekerjaan wajib dipilih.',
penghasilan: v => (v !== '' && parseFloat(v) >= 0) ? null : 'Penghasilan tidak boleh negatif.',
jumlah_tanggungan: v => parseInt(v) >= 0 ? null : 'Jumlah tanggungan tidak boleh negatif.',
lantai_rumah: v => v !== '' ? null : 'Lantai rumah wajib dipilih.',
dinding_rumah: v => v !== '' ? null : 'Dinding rumah wajib dipilih.',
atap_rumah: v => v !== '' ? null : 'Atap rumah wajib dipilih.',
luas_rumah_m2: v => parseInt(v) >= 1 ? null : 'Luas rumah wajib diisi.',
status_kepemilikan_rumah: v => v !== '' ? null : 'Status kepemilikan rumah wajib dipilih.',
meteran_listrik: v => v !== '' ? null : 'Meteran listrik wajib dipilih.',
sumber_air: v => v !== '' ? null : 'Sumber air wajib dipilih.',
};
Object.keys(rules).forEach(id => {
@ -311,13 +555,29 @@ function showToast(msg) {
let age = today.getFullYear() - dob.getFullYear();
const m = today.getMonth() - dob.getMonth();
if (m < 0 || (m === 0 && today.getDate() < dob.getDate())) age--;
if (age >= 0 && age <= 150) {
document.getElementById('usia').value = age;
clearError('usia');
}
if (age >= 0 && age <= 150) { document.getElementById('usia').value = age; clearError('usia'); }
});
const displayInput = document.getElementById('penghasilan_display');
const hiddenInput = document.getElementById('penghasilan');
if (displayInput && hiddenInput) {
displayInput.addEventListener('input', function () {
let raw = this.value.replace(/\D/g, '');
this.value = raw ? parseInt(raw).toLocaleString('id-ID') : '';
hiddenInput.value = raw;
const err = rules['penghasilan'](hiddenInput.value);
err ? showError('penghasilan', err) : clearError('penghasilan');
});
displayInput.addEventListener('blur', function () {
const err = rules['penghasilan'](hiddenInput.value);
err ? showError('penghasilan', err) : clearError('penghasilan');
});
}
document.getElementById('formEditPendataan').addEventListener('submit', function (e) {
if (displayInput && hiddenInput && hiddenInput.value === '') {
hiddenInput.value = displayInput.value.replace(/\D/g, '');
}
let valid = true;
Object.keys(rules).forEach(id => {
const el = document.getElementById(id);
@ -326,11 +586,19 @@ function showToast(msg) {
if (err) { showError(id, err); valid = false; }
else clearError(id);
});
if (!validateAset()) valid = false;
if (!valid) {
e.preventDefault();
showToast('Mohon perbaiki field yang tidak valid sebelum menyimpan.');
document.querySelector('.border-red-400')?.scrollIntoView({ behavior: 'smooth', block: 'center' });
document.querySelector('.border-red-400, .ring-red-400')?.scrollIntoView({ behavior: 'smooth', block: 'center' });
return;
}
const btn = document.getElementById('btnSimpan');
btn.disabled = true;
btn.style.opacity = '0.75';
btn.style.cursor = 'not-allowed';
document.getElementById('btnSpinner').style.display = 'block';
document.getElementById('btnText').textContent = 'Menyimpan...';
});
</script>
</x-app-layout>

View File

@ -1,71 +1,70 @@
<x-app-layout>
<x-slot name="header">
<div style="display:flex;align-items:center;justify-content:space-between;gap:16px;">
<div style="display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap;">
<div>
<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') }}"
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">
style="display:inline-flex;align-items:center;gap:7px;padding:10px 20px;background:#2563eb;color:#fff;font-size:13px;font-weight:700;border-radius:12px;text-decoration:none;box-shadow:0 3px 10px rgba(37,99,235,.28);">
<svg width="15" height="15" 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
Tambah Data Warga
</a>
</div>
</x-slot>
<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 */
.tbl-wrap{overflow-x:auto;-webkit-overflow-scrolling:touch;}
table.idx-tbl{width:100%;border-collapse:collapse;min-width:680px;}
table.idx-tbl thead tr{background:#f8fafc;border-bottom:1.5px solid #f1f5f9;}
table.idx-tbl 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;}
table.idx-tbl tbody tr{border-bottom:1px solid #f8fafc;transition:background .12s;}
table.idx-tbl tbody tr:hover{background:#f0f7ff;}
table.idx-tbl tbody tr:last-child{border-bottom:none;}
table.idx-tbl tbody td{padding:11px 14px;vertical-align:middle;}
.mob-list{display:none;flex-direction:column;gap:12px;padding:14px;}
@media(max-width:640px){.tbl-wrap{display:none;}.mob-list{display:flex;}}
.mob-card{background:#fafbfc;border:1.5px solid #f1f5f9;border-radius:16px;padding:14px;}
.mob-top{display:flex;align-items:center;gap:10px;margin-bottom:10px;}
.mob-name{font-size:14px;font-weight:800;color:#111827;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.mob-sub{font-size:10.5px;color:#94a3b8;margin-top:1px;font-family:monospace;}
.mob-row{display:flex;justify-content:space-between;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:10px;}
.mob-actions{display:flex;gap:8px;flex-wrap:wrap;padding-top:10px;border-top:1px solid #f1f5f9;}
.mob-actions .btn-act{flex:1;justify-content:center;padding:9px 10px;font-size:12px;}
.av{width:36px;height:36px;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:800;color:#fff;flex-shrink:0;background:linear-gradient(135deg,#3b82f6,#2563eb);}
.badge{display:inline-flex;align-items:center;gap:4px;padding:4px 10px;border-radius:20px;font-size:11px;font-weight:700;white-space:nowrap;}
.dot{width:6px;height:6px;border-radius:50%;flex-shrink:0;}
.badge-rt{display:inline-flex;align-items:center;padding:3px 9px;border-radius:7px;background:#eff6ff;color:#1d4ed8;font-size:11px;font-weight:700;border:1px solid #bfdbfe;}
.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;}
.trk-ditinjau{background:#f5f3ff;color:#6d28d9;border:1px solid #ddd6fe;}.trk-ditinjau .dot{background:#7c3aed;}
.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;}
.btn-act{display:inline-flex;align-items:center;gap:5px;padding:7px 12px;border-radius:9px;font-size:12px;font-weight:700;text-decoration:none;border:none;cursor:pointer;white-space:nowrap;line-height:1.4;}
.btn-act svg{width:13px;height:13px;flex-shrink:0;}
.btn-detail{background:#2563eb;color:#fff;box-shadow:0 2px 8px rgba(37,99,235,.25);}
.btn-detail:hover{background:#1d4ed8;}
.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{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;}
.hasil-hint{display:flex;align-items:center;gap:8px;background:#fef9c3;border:1.5px solid #fde047;border-radius:10px;padding:8px 12px;margin-top:6px;}
.hasil-hint svg{width:14px;height:14px;stroke:#ca8a04;flex-shrink:0;}
.hasil-hint span{font-size:11px;font-weight:700;color:#92400e;}
.btn-detail-selesai{background:linear-gradient(135deg,#10b981,#059669);color:#fff;box-shadow:0 3px 10px rgba(16,185,129,.3);animation:glow 2s ease-in-out infinite;}
.btn-detail-selesai:hover{filter:brightness(1.1);}
@keyframes glow{0%,100%{box-shadow:0 3px 10px rgba(16,185,129,.3);}50%{box-shadow:0 3px 18px rgba(16,185,129,.6);}}
</style>
{{-- FLASH --}}
@ -85,11 +84,11 @@
<div class="idx-card">
{{-- 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="padding:12px 16px;border-bottom:1px solid #f1f5f9;background:#fafafa;display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;">
<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 style="font-size:13px;font-weight:700;color:#374151;">
Total <span style="color:#2563eb;">{{ $calonPenerimas->total() }}</span> data terdaftar
</span>
</div>
<span style="font-size:11px;color:#9ca3af;">
@ -97,9 +96,9 @@
</span>
</div>
{{-- TABLE --}}
<div style="overflow-x:auto;">
<table>
{{-- ── DESKTOP TABLE ── --}}
<div class="tbl-wrap">
<table class="idx-tbl">
<thead>
<tr>
<th style="width:40px;">#</th>
@ -114,94 +113,89 @@
</thead>
<tbody>
@forelse($calonPenerimas as $index => $item)
@php $trk = $item->tracking_status ?? 'draft'; @endphp
<tr>
{{-- No --}}
<td style="font-size:11px;color:#9ca3af;font-weight:600;text-align:center;">
{{ $calonPenerimas->firstItem() + $index }}
</td>
{{-- 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 style="font-family:monospace;font-size:11.5px;color:#6b7280;letter-spacing:.02em;">
{{ $item->nik }}
</td>
{{-- RT --}}
<td style="font-family:monospace;font-size:11.5px;color:#6b7280;">{{ $item->nik }}</td>
<td><span class="badge-rt">RT {{ str_pad($item->rt->nomor_rt ?? '-', 3, '0', STR_PAD_LEFT) }}</span></td>
<td style="font-size:12px;color:#6b7280;">{{ $item->rt->dusun->nama_dusun ?? '-' }}</td>
<td>
<span class="badge-rt">RT {{ str_pad($item->rt->nomor_rt ?? '-', 3, '0', STR_PAD_LEFT) }}</span>
</td>
{{-- Dusun --}}
<td style="font-size:12px;color:#6b7280;">
{{ $item->rt->dusun->nama_dusun ?? '-' }}
</td>
{{-- 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="badge trk-draft"><span class="dot"></span></span>
@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>
@elseif($trk==='ditinjau_ulang') <span class="badge trk-ditinjau"><span class="dot"></span>Ditinjau Ulang</span>
@else <span class="badge trk-draft"><span class="dot"></span></span>
@endif
</td>
{{-- 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>
@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>
<div style="display:flex;flex-direction:column;gap:5px;align-items:flex-start;">
<div style="display:flex;align-items:center;gap:5px;flex-wrap:wrap;">
@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 href="{{ route('rt.calon-penerima.show', $item->id) }}"
class="btn-act {{ in_array($trk,['selesai','sedang_validasi']) ? 'btn-detail-selesai' : '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>
Lihat Detail
</a>
<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>
@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 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>
<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>
{{-- Hint teks di bawah tombol --}}
@if($trk==='selesai')
<div class="hasil-hint">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M12 3a9 9 0 100 18A9 9 0 0012 3z"/></svg>
<span>Hasil sudah ada klik Lihat Detail</span>
</div>
@elseif($trk==='sedang_validasi')
<div class="hasil-hint" style="background:#eff6ff;border-color:#bfdbfe;">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" style="stroke:#2563eb;"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
<span style="color:#1e40af;">Sedang diproses admin</span>
</div>
@elseif($trk==='ditinjau_ulang')
<div class="hasil-hint" style="background:#f5f3ff;border-color:#ddd6fe;">
<svg fill="none" stroke="#7c3aed" 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>
<span style="color:#6d28d9;">Data sedang ditinjau ulang oleh admin</span>
</div>
@elseif($trk==='draft')
<div style="font-size:10.5px;color:#94a3b8;margin-top:2px;"> Cek detail dulu, lalu ajukan</div>
@endif
</div>
</td>
</tr>
@ -222,11 +216,115 @@
</table>
</div>
{{-- PAGINATION --}}
<div class="pagi">
{{ $calonPenerimas->links() }}
{{-- ── MOBILE CARD LIST ── --}}
<div class="mob-list">
@forelse($calonPenerimas as $index => $item)
@php $trk = $item->tracking_status ?? 'draft'; @endphp
<div class="mob-card">
<div class="mob-top">
<div class="av">{{ strtoupper(substr($item->nama_lengkap, 0, 1)) }}</div>
<div style="flex:1;min-width:0;">
<div class="mob-name">{{ $item->nama_lengkap }}</div>
<div class="mob-sub">{{ $item->nik }}</div>
</div>
<span style="font-size:10px;color:#94a3b8;font-weight:700;">#{{ $calonPenerimas->firstItem() + $index }}</span>
</div>
<div class="mob-row">
<span class="badge-rt">RT {{ str_pad($item->rt->nomor_rt ?? '-', 3, '0', STR_PAD_LEFT) }}</span>
<span style="font-size:11px;color:#6b7280;">{{ $item->rt->dusun->nama_dusun ?? '-' }}</span>
@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>Sedang Diproses</span>
@elseif($trk==='selesai') <span class="badge trk-selesai"><span class="dot"></span>Selesai</span>
@elseif($trk==='ditinjau_ulang') <span class="badge trk-ditinjau"><span class="dot"></span>Ditinjau Ulang</span>
@endif
@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
</div>
{{-- Kotak info kontekstual --}}
@if($trk==='selesai')
<div style="background:#fef9c3;border:1.5px solid #fde047;border-radius:10px;padding:10px 12px;margin-bottom:10px;display:flex;align-items:center;gap:8px;">
<span style="font-size:20px;">📋</span>
<div>
<div style="font-size:12px;font-weight:800;color:#92400e;">Hasil sudah keluar!</div>
<div style="font-size:11px;color:#a16207;margin-top:1px;">Tekan tombol <b>Lihat Detail</b> di bawah untuk melihat hasilnya</div>
</div>
</div>
@elseif($trk==='sedang_validasi')
<div style="background:#eff6ff;border:1.5px solid #bfdbfe;border-radius:10px;padding:10px 12px;margin-bottom:10px;display:flex;align-items:center;gap:8px;">
<span style="font-size:20px;"></span>
<div>
<div style="font-size:12px;font-weight:800;color:#1e40af;">Sedang diproses admin</div>
<div style="font-size:11px;color:#3b82f6;margin-top:1px;">Mohon tunggu, hasil akan muncul setelah admin selesai</div>
</div>
</div>
@elseif($trk==='ditinjau_ulang')
<div style="background:#f5f3ff;border:1.5px solid #ddd6fe;border-radius:10px;padding:10px 12px;margin-bottom:10px;display:flex;align-items:center;gap:8px;">
<span style="font-size:20px;">🔍</span>
<div>
<div style="font-size:12px;font-weight:800;color:#6d28d9;">Data sedang ditinjau ulang</div>
<div style="font-size:11px;color:#7c3aed;margin-top:1px;">Admin perangkat desa sedang meninjau kembali data ini</div>
</div>
</div>
@elseif($trk==='draft')
<div style="background:#f0fdf4;border:1.5px solid #bbf7d0;border-radius:10px;padding:10px 12px;margin-bottom:10px;display:flex;align-items:center;gap:8px;">
<span style="font-size:20px;">👆</span>
<div>
<div style="font-size:12px;font-weight:800;color:#166534;">Cek dulu, lalu ajukan</div>
<div style="font-size:11px;color:#16a34a;margin-top:1px;">Tekan <b>Lihat Detail</b> untuk cek data, lalu tekan <b>Ajukan</b></div>
</div>
</div>
@endif
<div class="mob-actions">
<a href="{{ route('rt.calon-penerima.show', $item->id) }}"
class="btn-act {{ in_array($trk,['selesai','sedang_validasi']) ? 'btn-detail-selesai' : '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>
Lihat Detail
</a>
@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 method="POST" action="{{ route('rt.calon-penerima.ajukan', $item->id) }}"
onsubmit="return confirm('Yakin ajukan data {{ addslashes($item->nama_lengkap) }}?')"
style="display:contents;">
@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) }}?')"
style="display:contents;">
@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>
</div>
@empty
<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 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>
@endforelse
</div>
<div class="pagi">{{ $calonPenerimas->links() }}</div>
</div>
</x-app-layout>

View File

@ -1,421 +1,506 @@
<x-app-layout>
<x-slot name="header">
<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') }}"
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>
</a>
<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 class="flex items-center justify-between">
<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">Informasi lengkap & hasil prediksi kelayakan.</p>
</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
<a href="{{ route('rt.calon-penerima.index') }}"
class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium bg-white border border-gray-200 text-gray-600 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="M15 19l-7-7 7-7"/>
</svg>
Kembali
</a>
</div>
</x-slot>
<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;}}
.hero-banner { background: linear-gradient(135deg,#1e40af 0%,#2563eb 60%,#3b82f6 100%); border-radius:1rem; padding:1.5rem; position:relative; overflow:hidden; margin-bottom:1.25rem; }
.hero-bubble { position:absolute; border-radius:50%; pointer-events:none; }
.detail-card { background:#fff; border-radius:1rem; border:1px solid #f1f5f9; margin-bottom:1rem; overflow:hidden; }
.detail-card-head { padding:.65rem 1.1rem; border-bottom:1px solid #f1f5f9; display:flex; align-items:center; gap:8px; background:#f9fafb; }
.detail-card-accent{ width:3px; height:16px; border-radius:2px; flex-shrink:0; }
.detail-card-title { font-size:11px; font-weight:600; text-transform:uppercase; letter-spacing:.06em; color:#6b7280; }
.detail-card-body { padding:1rem 1.1rem; }
.data-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(160px,1fr)); gap:1rem; }
.data-lbl { font-size:10px; font-weight:600; text-transform:uppercase; letter-spacing:.05em; color:#9ca3af; margin-bottom:3px; }
.data-val { font-size:14px; color:#111827; font-weight:500; }
.badge-rt { display:inline-flex; align-items:center; padding:3px 10px; border-radius:20px; font-size:12px; background:#dbeafe; color:#1e40af; font-weight:600; }
.donut-ring { transform:rotate(-90deg); transform-origin:36px 36px; }
.progress-track{ height:6px; border-radius:3px; background:#e5e7eb; overflow:hidden; }
.faktor-item { display:flex; align-items:flex-start; gap:8px; padding:7px 10px; border-radius:8px; font-size:13px; margin-bottom:5px; }
.faktor-pos { background:#f0fdf4; color:#166534; }
.faktor-neg { background:#fff1f2; color:#9f1239; }
.penjelasan-row { display:flex; justify-content:space-between; align-items:center; padding:7px 0; font-size:13px; border-bottom:1px solid #f1f5f9; }
.penjelasan-row:last-child { border-bottom:none; }
.ringkasan-box { background:#f0f9ff; border:1px solid #bae6fd; border-radius:10px; padding:12px 14px; font-size:13px; color:#0c4a6e; line-height:1.6; }
.track-step { display:flex; gap:.875rem; padding:.65rem 0; }
.track-num { width:28px; height:28px; border-radius:50%; display:flex; align-items:center; justify-content:center; font-size:12px; font-weight:600; flex-shrink:0; margin-top:2px; }
.step-done { background:#dcfce7; color:#15803d; }
.step-active { background:#dbeafe; color:#1d4ed8; }
.step-pending{ background:#f3f4f6; color:#9ca3af; }
.track-line { width:2px; height:16px; background:#e5e7eb; margin:0 auto; }
.status-bar { background:#f9fafb; border:1px solid #f1f5f9; border-radius:10px; padding:14px 16px; display:flex; align-items:center; gap:12px; flex-wrap:wrap; }
.status-pill{ display:inline-flex; align-items:center; gap:5px; padding:4px 12px; border-radius:20px; font-size:12px; font-weight:600; }
.foto-grid { display:grid; grid-template-columns:repeat(4,1fr); gap:8px; }
@media(max-width:480px){ .foto-grid{ grid-template-columns:repeat(3,1fr); } }
.foto-thumb { aspect-ratio:1; border-radius:8px; overflow:hidden; border:1px solid #e5e7eb; position:relative; cursor:pointer; }
.foto-thumb img { width:100%; height:100%; object-fit:cover; display:block; }
.foto-placeholder{ width:100%; height:100%; background:#f9fafb; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:4px; }
.foto-label{ position:absolute; bottom:0; left:0; right:0; background:rgba(0,0,0,.5); color:#fff; font-size:10px; text-align:center; padding:3px 4px; }
.zona-bahaya { border:1px solid #fecaca; border-radius:1rem; overflow:hidden; margin-bottom:1rem; }
.zona-bahaya-head { padding:.65rem 1.1rem; border-bottom:1px solid #fecaca; display:flex; align-items:center; gap:8px; background:#fff5f5; }
.detail-layout { display:grid; grid-template-columns:1fr 320px; gap:1rem; align-items:start; }
@media(max-width:900px){ .detail-layout{ grid-template-columns:1fr; } }
.badge-layak { background:#dcfce7; color:#15803d; padding:5px 14px; border-radius:20px; font-size:13px; font-weight:600; display:inline-flex; align-items:center; gap:5px; }
.badge-tl { background:#fee2e2; color:#b91c1c; padding:5px 14px; border-radius:20px; font-size:13px; font-weight:600; display:inline-flex; align-items:center; gap:5px; }
.badge-draft { display:inline-flex; align-items:center; gap:5px; padding:4px 10px; border-radius:20px; font-size:12px; font-weight:500; background:rgba(255,255,255,.15); color:#fff; border:1px solid rgba(255,255,255,.25); }
</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'
};
$prediksi = $calonPenerima->prediksiKelayakan;
$prob = 0;
$label = null;
if ($prediksi) {
$prob = $prediksi->probability <= 1 ? round($prediksi->probability * 100) : round($prediksi->probability);
$label = $prediksi->recommendation; // 'Layak' atau 'Tidak Layak'
}
$isLayak = $label === 'Layak';
$ringColor = $isLayak ? '#22c55e' : ($label ? '#ef4444' : '#d1d5db');
$r = 28;
$circ = round(2 * M_PI * $r, 2);
$fill = round($circ * $prob / 100, 2);
$gap = $circ - $fill;
$currentStatus = $calonPenerima->tracking_status ?? 'draft';
@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 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
{{-- 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 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 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>
{{-- HERO --}}
<div class="hero-banner">
<div class="hero-bubble" style="width:180px;height:180px;background:rgba(255,255,255,.06);top:-40px;right:-30px;"></div>
<div class="hero-bubble" style="width:90px;height:90px;background:rgba(255,255,255,.04);top:50px;right:100px;"></div>
<div class="flex items-start justify-between gap-3 flex-wrap">
<div class="flex items-center gap-3">
<div class="w-12 h-12 rounded-full bg-white/20 flex items-center justify-center text-white font-semibold text-lg flex-shrink-0">
{{ strtoupper(substr($calonPenerima->nama_lengkap, 0, 1)) }}
</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>
<div>
<div class="text-white font-semibold text-xl leading-tight">{{ $calonPenerima->nama_lengkap }}</div>
<div class="text-white/70 text-sm">NIK: {{ $calonPenerima->nik }}</div>
<div class="mt-1.5">
<span class="badge-draft">
<span class="w-1.5 h-1.5 rounded-full bg-yellow-400 inline-block"></span>
{{ ucfirst($currentStatus) }}
</span>
</div>
</div>
</div>
<div class="text-right">
@if($prediksi)
<div class="text-white font-semibold leading-none" style="font-size:42px;">
{{ $prob }}<span style="font-size:20px;">%</span>
</div>
<div class="text-white/65 text-xs mt-1">probabilitas</div>
<div class="mt-1.5">
@if($isLayak)
<span class="badge-layak"> Layak</span>
@else
<span class="badge-tl"> Tidak Layak</span>
@endif
</div>
@else
<span class="pill" style="background:#fffbeb;color:#b45309;border:1px solid #fde68a;"><span class="dot" style="background:#f59e0b;"></span>Menunggu Hasil</span>
<div class="text-white/50 text-sm mt-2">Belum diprediksi</div>
@endif
@else
<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>
{{-- MAIN GRID --}}
<div class="mg" style="display:grid;grid-template-columns:1fr 310px;gap:12px;align-items:start;">
{{-- ACTION BUTTONS --}}
<div class="flex items-center gap-2 flex-wrap mb-4">
@if($currentStatus === 'draft')
<a href="{{ route('rt.calon-penerima.edit', $calonPenerima->id) }}"
class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-xl border border-blue-200 bg-blue-50 text-blue-700 hover:bg-blue-100 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="M15.232 5.232l3.536 3.536M9 13l6.586-6.586a2 2 0 012.828 2.828L11.828 16H9v-2.828z"/></svg>
Edit Data
</a>
<form action="{{ route('rt.calon-penerima.ajukan', $calonPenerima->id) }}" method="POST" class="inline">
@csrf
<button type="submit"
class="inline-flex items-center gap-2 px-5 py-2 text-sm font-semibold rounded-xl bg-blue-600 text-white 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 19l9 2-9-18-9 18 9-2zm0 0v-8"/></svg>
Ajukan ke perangkat desa
</button>
</form>
@endif
</div>
{{-- KIRI --}}
<div style="display:flex;flex-direction:column;gap:12px;">
{{-- MAIN LAYOUT --}}
<div class="detail-layout">
<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>
{{-- ═══ KOLOM KIRI ═══ --}}
<div>
{{-- IDENTITAS --}}
<div class="detail-card">
<div class="detail-card-head">
<div class="detail-card-accent" style="background:#2563eb;"></div>
<span class="detail-card-title">Identitas</span>
</div>
{{-- 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 class="detail-card-body">
<div class="data-grid">
<div><div class="data-lbl">No. KK</div><div class="data-val">{{ $calonPenerima->no_kk }}</div></div>
<div><div class="data-lbl">Jenis Kelamin</div><div class="data-val">{{ $calonPenerima->jenis_kelamin }}</div></div>
<div><div class="data-lbl">Tempat Lahir</div><div class="data-val">{{ $calonPenerima->tempat_lahir }}</div></div>
<div><div class="data-lbl">Tanggal Lahir</div><div class="data-val">{{ \Carbon\Carbon::parse($calonPenerima->tanggal_lahir)->translatedFormat('d F Y') }}</div></div>
<div><div class="data-lbl">Usia</div><div class="data-val">{{ $calonPenerima->usia }} tahun</div></div>
<div><div class="data-lbl">Status Kawin</div><div class="data-val">{{ $calonPenerima->status_perkawinan }}</div></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>
{{-- TEMPAT TINGGAL --}}
<div class="detail-card">
<div class="detail-card-head">
<div class="detail-card-accent" style="background:#10b981;"></div>
<span class="detail-card-title">Tempat Tinggal</span>
</div>
<div class="detail-card-body">
<div class="data-grid">
<div style="grid-column:span 2;">
<div class="data-lbl">Alamat</div>
<div class="data-val">{{ $calonPenerima->alamat }}</div>
</div>
<div><div class="data-lbl">Dusun</div><div class="data-val">{{ $calonPenerima->desa }}</div></div>
<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 class="data-lbl">RT</div>
<div class="data-val"><span class="badge-rt">RT {{ str_pad($calonPenerima->rt->nomor_rt ?? '?', 3, '0', STR_PAD_LEFT) }}</span></div>
</div>
<div><div class="data-lbl">Aset Kepemilikan</div><div class="data-val">{{ $calonPenerima->aset_kepemilikan ?? '-' }}</div></div>
<div><div class="data-lbl">Lantai</div><div class="data-val">{{ $calonPenerima->lantai_rumah }}</div></div>
<div><div class="data-lbl">Dinding</div><div class="data-val">{{ $calonPenerima->dinding_rumah }}</div></div>
<div><div class="data-lbl">Atap</div><div class="data-val">{{ $calonPenerima->atap_rumah }}</div></div>
<div><div class="data-lbl">Luas Rumah</div><div class="data-val">{{ $calonPenerima->luas_rumah_m2 }} </div></div>
<div><div class="data-lbl">Status Kepemilikan</div><div class="data-val">{{ $calonPenerima->status_kepemilikan_rumah }}</div></div>
<div><div class="data-lbl">Meteran Listrik</div><div class="data-val">{{ $calonPenerima->meteran_listrik }}</div></div>
<div><div class="data-lbl">Sumber Air</div><div class="data-val">{{ $calonPenerima->sumber_air }}</div></div>
</div>
</div>
</div>
{{-- 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>
{{-- DATA EKONOMI --}}
<div class="detail-card">
<div class="detail-card-head">
<div class="detail-card-accent" style="background:#f59e0b;"></div>
<span class="detail-card-title">Data Ekonomi</span>
</div>
<div class="detail-card-body">
<div class="data-grid">
<div><div class="data-lbl">Pekerjaan</div><div class="data-val">{{ $calonPenerima->pekerjaan }}</div></div>
<div><div class="data-lbl">Penghasilan</div><div class="data-val">Rp {{ number_format($calonPenerima->penghasilan, 0, ',', '.') }}</div></div>
<div><div class="data-lbl">Tanggungan</div><div class="data-val">{{ $calonPenerima->jumlah_tanggungan }} orang</div></div>
<div>
<div class="data-lbl">Bantuan Lain</div>
<div class="data-val">
@if($calonPenerima->bantuan_lain === 'ya')
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-700">Ya</span>
@else
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-700">Tidak</span>
@endif
</div>
</div>
</div>
</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="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>
{{-- FOTO & DOKUMEN --}}
<div class="detail-card">
<div class="detail-card-head">
<div class="detail-card-accent" style="background:#e11d48;"></div>
<span class="detail-card-title">Foto & Dokumen</span>
</div>
<div class="detail-card-body">
@php
$fotoFields = [
'foto_rumah_depan' => 'Rumah Depan',
'foto_rumah_belakang' => 'Rumah Belakang',
'foto_rumah_kanan' => 'Rumah Kanan',
'foto_rumah_kiri' => 'Rumah Kiri',
'foto_kk' => 'Foto KK',
'foto_ktp' => 'Foto KTP',
'foto_rekening_listrik' => 'Rekening Listrik',
'foto_meteran_air' => 'Meteran Air',
];
@endphp
<div class="foto-grid">
@foreach($fotoFields as $field => $label)
<div class="foto-thumb" @if($calonPenerima->$field) onclick="openFoto('{{ asset('storage/' . $calonPenerima->$field) }}')" @endif>
@if($calonPenerima->$field)
<img src="{{ asset('storage/' . $calonPenerima->$field) }}" alt="{{ $label }}" loading="lazy">
@else
<div class="foto-placeholder">
<svg class="w-6 h-6 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
<span class="text-xs text-gray-400">Belum ada</span>
</div>
@endif
<div class="foto-label">{{ $label }}</div>
</div>
@endforeach
</div>
@if($calonPenerima->dokumen_pendukung)
<div class="mt-3 pt-3 border-t border-gray-100">
<a href="{{ asset('storage/' . $calonPenerima->dokumen_pendukung) }}" target="_blank"
class="inline-flex items-center gap-2 text-sm text-blue-600 hover:underline">
<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="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13"/></svg>
Lihat Dokumen Pendukung
</a>
</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>
{{-- KANAN --}}
<div style="display:flex;flex-direction:column;gap:12px;">
{{-- 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>
{{-- TRACKING PENGAJUAN --}}
<div class="detail-card">
<div class="detail-card-head">
<div class="detail-card-accent" style="background:#8b5cf6;"></div>
<span class="detail-card-title">Tracking Pengajuan</span>
</div>
<div class="detail-card-body" style="padding-top:.5rem;padding-bottom:.5rem;">
@php
$steps = [
'draft' => ['label' => 'Data dibuat RT', 'desc' => 'Data masih bisa diubah dan dihapus sebelum diajukan.'],
'terkirim' => ['label' => 'Data diajukan ke kelurahan', 'desc' => 'Menunggu tindakan RT untuk mengajukan data.'],
'validasi' => ['label' => 'Proses validasi & filterisasi','desc' => 'Menunggu admin memulai proses validasi.'],
'final' => ['label' => 'Hasil akhir ditetapkan', 'desc' => 'Menunggu hasil akhir dari admin perangkat desa.'],
];
$statusOrder = array_keys($steps);
$currentIndex = array_search($currentStatus, $statusOrder) ?? 0;
@endphp
@foreach($steps as $key => $step)
@php
$stepIndex = array_search($key, $statusOrder);
$isDone = $stepIndex < $currentIndex;
$isActive = $stepIndex === $currentIndex;
@endphp
<div class="track-step">
<div class="flex flex-col items-center">
<div class="track-num {{ $isDone ? 'step-done' : ($isActive ? 'step-active' : 'step-pending') }}">
@if($isDone)
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7"/></svg>
@else
{{ $stepIndex + 1 }}
@endif
</div>
@if(!$loop->last)<div class="track-line"></div>@endif
</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 class="pb-1">
<div class="text-sm font-semibold {{ $isActive ? 'text-gray-800' : ($isDone ? 'text-gray-700' : 'text-gray-400') }} leading-snug">
{{ $step['label'] }}
</div>
<div class="text-xs text-gray-400 mt-0.5">{{ $step['desc'] }}</div>
</div>
</div>
@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>
@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>
<p style="font-size:11px;color:#9ca3af;">Belum ada data prediksi</p>
</div>
@endif
@endforeach
</div>
</div>
{{-- 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>
{{-- STATUS PENGAJUAN --}}
<div class="detail-card">
<div class="detail-card-head">
<div class="detail-card-accent" style="background:#64748b;"></div>
<span class="detail-card-title">Status Pengajuan</span>
</div>
<div class="detail-card-body">
<div class="status-bar">
@php
$statusMap = [
'draft' => ['bg' => 'bg-yellow-100', 'text' => 'text-yellow-700', 'dot' => 'bg-yellow-400', 'label' => 'Draft'],
'terkirim' => ['bg' => 'bg-blue-100', 'text' => 'text-blue-700', 'dot' => 'bg-blue-400', 'label' => 'Diajukan'],
'validasi' => ['bg' => 'bg-purple-100', 'text' => 'text-purple-700', 'dot' => 'bg-purple-400', 'label' => 'Validasi'],
'final' => ['bg' => 'bg-green-100', 'text' => 'text-green-700', 'dot' => 'bg-green-400', 'label' => 'Final'],
];
$s = $statusMap[$currentStatus] ?? $statusMap['draft'];
@endphp
<span class="status-pill {{ $s['bg'] }} {{ $s['text'] }}">
<span class="w-1.5 h-1.5 rounded-full {{ $s['dot'] }} inline-block"></span>
{{ $s['label'] }}
</span>
<div class="flex gap-6 text-sm text-gray-500 flex-wrap">
<div>
<span class="text-xs text-gray-400 block">Dibuat</span>
<span class="font-medium text-gray-700">{{ $calonPenerima->created_at->translatedFormat('d M Y, H:i') }}</span>
</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>
<span class="text-xs text-gray-400 block">Diperbarui</span>
<span class="font-medium text-gray-700">{{ $calonPenerima->updated_at->translatedFormat('d M Y, H:i') }}</span>
</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>
</div>
@endif
</div>
{{-- 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 }}?')">
</div>{{-- end kolom kiri --}}
{{-- ═══ KOLOM KANAN: PREDIKSI ═══ --}}
<div>
<div style="position:sticky;top:1rem;">
<div class="detail-card">
<div class="detail-card-head">
<div class="detail-card-accent" style="background:#22c55e;"></div>
<span class="detail-card-title">Prediksi Kelayakan</span>
</div>
<div class="detail-card-body">
{{-- Donut --}}
<div class="flex items-center gap-4 mb-4">
<svg width="80" height="80" viewBox="0 0 72 72">
<circle cx="36" cy="36" r="{{ $r }}" fill="none" stroke="#e5e7eb" stroke-width="8"/>
<circle cx="36" cy="36" r="{{ $r }}" fill="none"
stroke="{{ $ringColor }}" stroke-width="8"
stroke-dasharray="{{ $fill }} {{ $gap }}"
stroke-linecap="round"
class="donut-ring"/>
<text x="36" y="40" text-anchor="middle" font-size="13" font-weight="600" fill="#111827">{{ $prob }}%</text>
</svg>
<div>
<div class="text-xs text-gray-400 mb-1.5">Rekomendasi</div>
@if($prediksi)
@if($isLayak)
<span class="badge-layak">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7"/></svg>
Layak
</span>
@else
<span class="badge-tl">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12"/></svg>
Tidak Layak
</span>
@endif
@else
<span class="text-sm text-gray-400">Belum diprediksi</span>
@endif
<div class="progress-track mt-2" style="width:140px;">
<div style="height:100%;border-radius:3px;background:{{ $ringColor }};width:{{ $prob }}%;"></div>
</div>
</div>
</div>
{{-- Faktor Pendukung --}}
@if(!empty($explanation['positive']))
<div class="mb-3">
<div class="text-xs font-semibold uppercase tracking-wider text-green-700 mb-2 flex items-center gap-1.5">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
Faktor Pendukung
</div>
@foreach($explanation['positive'] as $f)
<div class="faktor-item faktor-pos">
<svg class="w-3.5 h-3.5 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7"/></svg>
{{ $f }}
</div>
@endforeach
</div>
@endif
{{-- Faktor Pengurang --}}
@if(!empty($explanation['negative']))
<div class="mb-3">
<div class="text-xs font-semibold uppercase tracking-wider text-red-700 mb-2 flex items-center gap-1.5">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
Faktor Pengurang
</div>
@foreach($explanation['negative'] as $f)
<div class="faktor-item faktor-neg">
<svg class="w-3.5 h-3.5 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12"/></svg>
{{ $f }}
</div>
@endforeach
</div>
@endif
{{-- Penjelasan Lengkap selalu terbuka --}}
<div class="mb-3">
<div class="text-xs font-semibold uppercase tracking-wider text-gray-500 mb-2">Penjelasan Lengkap</div>
<div class="border border-gray-100 rounded-xl overflow-hidden">
@php
$penjelasanRows = [
'Pekerjaan' => $calonPenerima->pekerjaan,
'Penghasilan' => 'Rp ' . number_format($calonPenerima->penghasilan, 0, ',', '.'),
'Tanggungan' => $calonPenerima->jumlah_tanggungan . ' orang',
'Aset' => $calonPenerima->aset_kepemilikan ?? '-',
'Bantuan Lain' => ucfirst($calonPenerima->bantuan_lain),
'Usia' => $calonPenerima->usia . ' tahun',
'Kondisi Rumah' => ($calonPenerima->kondisi_rumah ?? '-'),
'Meteran Listrik' => $calonPenerima->meteran_listrik,
'Sumber Air' => $calonPenerima->sumber_air,
];
@endphp
@foreach($penjelasanRows as $k => $v)
<div class="penjelasan-row px-3">
<span class="text-gray-400">{{ $k }}</span>
<span class="text-gray-800 font-medium text-right ml-2">{{ $v }}</span>
</div>
@endforeach
</div>
</div>
{{-- Ringkasan --}}
@if(!empty($explanation['summary']))
<div class="ringkasan-box">
<div class="text-xs font-semibold uppercase tracking-wider text-sky-700 mb-1.5">Ringkasan</div>
<p class="text-sm text-sky-900 leading-relaxed">{{ $explanation['summary'] }}</p>
</div>
@endif
</div>
</div>
{{-- ZONA BAHAYA --}}
@if($currentStatus === 'draft')
<div class="zona-bahaya">
<div class="zona-bahaya-head">
<div class="detail-card-accent" style="background:#ef4444;"></div>
<span class="detail-card-title" style="color:#b91c1c;">Zona Bahaya</span>
</div>
<div class="detail-card-body">
<p class="text-sm text-gray-500 mb-3">
Hapus data ini secara permanen. Tindakan ini <strong class="text-gray-700">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>
<button type="submit"
class="w-full inline-flex items-center justify-center gap-2 px-4 py-2.5 text-sm font-semibold rounded-xl bg-red-600 text-white hover:bg-red-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>
Hapus Data
</button>
</form>
</div>
</div>
@endif
@endif
</div>
</div>
</div>
{{-- LIGHTBOX --}}
<div id="lightbox" class="fixed inset-0 z-50 hidden" style="background:rgba(0,0,0,.8);" onclick="closeLightbox()">
<div class="flex items-center justify-center h-full p-4">
<img id="lightboxImg" src="" alt="Foto" class="max-w-full max-h-full rounded-xl object-contain shadow-2xl">
</div>
</div>
</x-app-layout>
<script>
function openFoto(src) {
document.getElementById('lightboxImg').src = src;
document.getElementById('lightbox').classList.remove('hidden');
}
function closeLightbox() {
document.getElementById('lightbox').classList.add('hidden');
}
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeLightbox(); });
</script>
</x-app-layout>

View File

@ -1,242 +1,284 @@
<x-app-layout>
<x-slot name="header">
<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;">
<div style="display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;">
<div>
<h2 style="font-size:17px;font-weight:900;color:#0f172a;line-height:1.2;letter-spacing:-.02em;">Dashboard</h2>
<h2 style="font-size:16px;font-weight:900;color:#0f172a;letter-spacing:-.02em;">Dashboard</h2>
<p style="font-size:11px;color:#94a3b8;margin-top:2px;font-weight:500;">
Ringkasan data pendataan warga {{ Auth::user()->rt->dusun->nama_dusun ?? '' }}
Pendataan warga {{ Auth::user()->rt->dusun->nama_dusun ?? '' }}
</p>
</div>
<div style="display:inline-flex;align-items:center;gap:6px;background:#fff;border:1.5px solid #e2e8f0;border-radius:11px;padding:7px 12px;font-size:11px;color:#64748b;font-weight:500;box-shadow:0 1px 4px rgba(0,0,0,.04);">
<svg width="13" height="13" fill="none" stroke="#3b82f6" viewBox="0 0 24 24">
<div style="display:inline-flex;align-items:center;gap:6px;background:#fff;border:1.5px solid #e2e8f0;border-radius:11px;padding:7px 12px;font-size:11px;color:#64748b;font-weight:500;flex-shrink:0;min-width:0;overflow:hidden;">
<svg width="13" height="13" fill="none" stroke="#3b82f6" viewBox="0 0 24 24" style="flex-shrink:0;">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
<span id="live-clock">
{{ \Carbon\Carbon::now('Asia/Jakarta')->translatedFormat('l, d F Y') }}
{{ \Carbon\Carbon::now('Asia/Jakarta')->format('H:i') }} WIB
</span>
<span id="live-clock" style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">{{ \Carbon\Carbon::now('Asia/Jakarta')->translatedFormat('l, d F Y') }} {{ \Carbon\Carbon::now('Asia/Jakarta')->format('H:i') }} WIB</span>
</div>
</div>
<script>
function updateClock() {
const now = new Date();
const options = { timeZone: 'Asia/Jakarta', weekday: 'long', day: '2-digit', month: 'long', year: 'numeric' };
const dateStr = now.toLocaleDateString('id-ID', options);
const timeStr = now.toLocaleTimeString('id-ID', { timeZone: 'Asia/Jakarta', hour: '2-digit', minute: '2-digit', hour12: false });
const el = document.getElementById('live-clock');
if (el) el.textContent = dateStr + ' — ' + timeStr + ' WIB';
function updateClock(){
var now=new Date();
var d=now.toLocaleDateString('id-ID',{timeZone:'Asia/Jakarta',weekday:'long',day:'2-digit',month:'long',year:'numeric'});
var t=now.toLocaleTimeString('id-ID',{timeZone:'Asia/Jakarta',hour:'2-digit',minute:'2-digit',hour12:false});
var el=document.getElementById('live-clock');
if(el) el.textContent=d+' — '+t+' WIB';
}
updateClock();
setInterval(updateClock, 1000);
setInterval(updateClock,1000);
</script>
</x-slot>
@php
$avgProb = \App\Models\PrediksiKelayakan::whereHas('calonPenerima', function ($q) {
$q->where('user_id', Auth::id());
})->avg('probability');
$avgProb = $avgProb ?? 0;
if ($avgProb <= 1) {
$avgProb = $avgProb * 100;
}
})->avg('probability') ?? 0;
if ($avgProb <= 1) $avgProb = $avgProb * 100;
@endphp
<style>
.db-card{background:#fff;border-radius:20px;border:1.5px solid #f1f5f9;box-shadow:0 2px 8px rgba(0,0,0,.04);overflow:hidden;}
.sc-blob1{position:absolute;top:-16px;right:-16px;width:80px;height:80px;background:rgba(255,255,255,.12);border-radius:50%;}
.sc-blob2{position:absolute;bottom:-20px;right:8px;width:56px;height:56px;background:rgba(255,255,255,.09);border-radius:50%;}
.db-card{background:#fff;border-radius:18px;border:1.5px solid #f1f5f9;box-shadow:0 2px 8px rgba(0,0,0,.04);overflow:hidden;}
.sc-blob1{position:absolute;top:-16px;right:-16px;width:80px;height:80px;background:rgba(255,255,255,.12);border-radius:50%;pointer-events:none;}
.sc-blob2{position:absolute;bottom:-20px;right:8px;width:56px;height:56px;background:rgba(255,255,255,.09);border-radius:50%;pointer-events:none;}
.aksi-btn{display:flex;align-items:center;gap:12px;padding:12px 14px;border-radius:14px;border:1.5px solid;text-decoration:none;transition:filter .15s,transform .1s;}
.aksi-btn{display:flex;align-items:center;gap:12px;padding:12px 14px;border-radius:13px;border:1.5px solid;text-decoration:none;transition:filter .15s,transform .1s;}
.aksi-btn:hover{filter:brightness(.96);transform:translateY(-1px);}
.aksi-icon{width:38px;height:38px;border-radius:11px;display:flex;align-items:center;justify-content:center;flex-shrink:0;}
.aksi-icon{width:36px;height:36px;border-radius:10px;display:flex;align-items:center;justify-content:center;flex-shrink:0;}
.aksi-icon svg{width:16px;height:16px;stroke:#fff;}
.recent-item{display:flex;align-items:center;justify-content:space-between;padding:11px 16px;border-bottom:1px solid #f8fafc;transition:background .1s;}
.recent-item{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:11px 14px;border-bottom:1px solid #f8fafc;transition:background .1s;}
.recent-item:hover{background:#f0f7ff;}
.recent-item:last-child{border-bottom:none;}
.rt-stat-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;}
@media(max-width:860px){.rt-stat-grid{grid-template-columns:repeat(2,1fr);}}
.rt-main-grid{display:grid;grid-template-columns:2fr 1fr;gap:14px;align-items:start;}
@media(max-width:1024px){.rt-main-grid{grid-template-columns:1fr;}}
@media(max-width:480px){
.recent-item{flex-wrap:wrap;gap:6px;}
.recent-item-right{width:100%;display:flex;justify-content:flex-end;}
}
@media(max-width:500px){#live-clock{font-size:10px;}}
@keyframes pulse{0%,100%{opacity:1;transform:scale(1);}50%{opacity:.5;transform:scale(.85);}}
/* ── ALUR BANNER ── */
.alur-banner{background:linear-gradient(135deg,#1e40af,#2563eb);border-radius:18px;padding:16px;box-shadow:0 6px 24px rgba(37,99,235,.3);position:relative;overflow:hidden;}
.alur-banner::before{content:'';position:absolute;top:-30px;right:-30px;width:120px;height:120px;background:rgba(255,255,255,.06);border-radius:50%;}
.alur-banner::after{content:'';position:absolute;bottom:-20px;right:60px;width:80px;height:80px;background:rgba(255,255,255,.04);border-radius:50%;}
.alur-title{font-size:13px;font-weight:900;color:#fff;margin-bottom:12px;display:flex;align-items:center;gap:7px;}
.alur-title svg{width:16px;height:16px;stroke:#93c5fd;flex-shrink:0;}
/* DESKTOP: flex 1 baris */
.alur-steps-wrap{display:flex;align-items:stretch;position:relative;z-index:1;}
.alur-arrow-wrap{display:flex;align-items:center;padding:0 5px;flex-shrink:0;}
.alur-arrow-wrap svg{width:16px;height:16px;stroke:rgba(255,255,255,.4);}
.alur-step-box{flex:1;background:rgba(255,255,255,.1);border:1px solid rgba(255,255,255,.18);border-radius:12px;padding:12px 8px;display:flex;flex-direction:column;align-items:center;text-align:center;gap:5px;}
.alur-step-num{width:22px;height:22px;border-radius:50%;background:#fff;color:#1e40af;font-size:11px;font-weight:900;display:flex;align-items:center;justify-content:center;flex-shrink:0;}
.alur-step-icon{width:34px;height:34px;background:rgba(255,255,255,.15);border-radius:9px;display:flex;align-items:center;justify-content:center;}
.alur-step-icon svg{width:17px;height:17px;stroke:#fff;}
.alur-step-label{font-size:11.5px;font-weight:800;color:#fff;line-height:1.3;}
.alur-step-sub{font-size:10px;color:rgba(191,219,254,.85);line-height:1.4;}
/* MOBILE: grid 2x2, panah disembunyikan */
@media(max-width:640px){
.alur-banner{padding:13px 12px;}
.alur-title{font-size:11px;margin-bottom:10px;}
.alur-steps-wrap{display:grid;grid-template-columns:1fr 1fr;gap:7px;}
.alur-arrow-wrap{display:none;}
.alur-step-box{flex:none;padding:10px 7px;gap:4px;border-radius:10px;}
.alur-step-num{width:19px;height:19px;font-size:10px;}
.alur-step-icon{width:28px;height:28px;border-radius:7px;}
.alur-step-icon svg{width:14px;height:14px;}
.alur-step-label{font-size:10.5px;}
.alur-step-sub{font-size:9px;color:rgba(191,219,254,.8);}
}
.alur-step-box.active{background:rgba(255,255,255,.22);border-color:rgba(255,255,255,.5);box-shadow:0 0 0 2px rgba(255,255,255,.25);}
.alur-step-box.done{background:rgba(16,185,129,.25);border-color:rgba(167,243,208,.4);}
</style>
<div style="display:flex;flex-direction:column;gap:14px;">
{{-- GREETING (asli tidak diubah) --}}
<div style="display:flex;align-items:center;gap:12px;background:#fff;border:1.5px solid #f1f5f9;border-radius:18px;padding:14px 18px;box-shadow:0 1px 4px rgba(0,0,0,.04);">
{{-- GREETING --}}
<div style="display:flex;align-items:center;gap:12px;background:#fff;border:1.5px solid #f1f5f9;border-radius:16px;padding:14px 16px;box-shadow:0 1px 4px rgba(0,0,0,.04);">
<div style="width:40px;height:40px;border-radius:50%;background:linear-gradient(135deg,#3b82f6,#2563eb);display:flex;align-items:center;justify-content:center;color:#fff;font-size:15px;font-weight:800;flex-shrink:0;box-shadow:0 4px 12px rgba(37,99,235,.28);">
{{ strtoupper(substr(Auth::user()->name ?? 'U', 0, 1)) }}
</div>
<div>
<p style="font-size:13.5px;font-weight:700;color:#0f172a;">
Selamat datang, <span style="color:#2563eb;">{{ Auth::user()->name ?? 'Pengguna' }}</span> 👋
</p>
<div style="min-width:0;">
<p style="font-size:13px;font-weight:700;color:#0f172a;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">Selamat datang, <span style="color:#2563eb;">{{ Auth::user()->name ?? 'Pengguna' }}</span> 👋</p>
<p style="font-size:11px;color:#94a3b8;margin-top:2px;font-weight:500;">Berikut ringkasan data terkini yang perlu Anda pantau.</p>
</div>
</div>
{{-- STAT CARDS (asli tidak diubah) --}}
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:12px;">
<div style="position:relative;background:linear-gradient(135deg,#2563eb,#1d4ed8);color:#fff;border-radius:18px;padding:18px;display:flex;align-items:center;justify-content:space-between;overflow:hidden;box-shadow:0 6px 20px rgba(37,99,235,.3);">
<div class="sc-blob1"></div><div class="sc-blob2"></div>
<div style="position:relative;z-index:1;">
<p style="font-size:10.5px;font-weight:600;color:rgba(191,219,254,.9);">Total Warga</p>
<p style="font-size:32px;font-weight:900;margin-top:2px;line-height:1;letter-spacing:-.04em;">{{ $stats['total_input'] }}</p>
</div>
<div style="position:relative;z-index:1;width:44px;height:44px;background:rgba(255,255,255,.18);border-radius:13px;display:flex;align-items:center;justify-content:center;flex-shrink:0;border:1px solid rgba(255,255,255,.2);">
<svg width="22" height="22" fill="none" stroke="#fff" 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>
{{-- ── BANNER ALUR PENDATAAN ── --}}
<div class="alur-banner">
<div class="alur-title">
<svg fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M12 3a9 9 0 100 18A9 9 0 0012 3z"/></svg>
Panduan: Cara Mendaftarkan &amp; Melihat Hasil Warga
</div>
<div class="alur-steps-wrap">
<div style="position:relative;background:linear-gradient(135deg,#f59e0b,#d97706);color:#fff;border-radius:18px;padding:18px;display:flex;align-items:center;justify-content:space-between;overflow:hidden;box-shadow:0 6px 20px rgba(245,158,11,.3);">
<div class="sc-blob1"></div><div class="sc-blob2"></div>
<div style="position:relative;z-index:1;">
<p style="font-size:10.5px;font-weight:600;color:rgba(254,243,199,.9);">Pending</p>
<p style="font-size:32px;font-weight:900;margin-top:2px;line-height:1;letter-spacing:-.04em;">{{ $stats['pending'] }}</p>
<div class="alur-step-box">
<div class="alur-step-num">1</div>
<div class="alur-step-icon">
<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>
</div>
<div class="alur-step-label">Input Data Warga</div>
<div class="alur-step-sub">Klik "Pendataan" lalu "Tambah Data" untuk mendaftarkan warga</div>
</div>
<div style="position:relative;z-index:1;width:44px;height:44px;background:rgba(255,255,255,.18);border-radius:13px;display:flex;align-items:center;justify-content:center;flex-shrink:0;border:1px solid rgba(255,255,255,.2);">
<svg width="22" height="22" fill="none" stroke="#fff" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
<div class="alur-arrow-wrap">
<svg fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
</div>
<div class="alur-step-box">
<div class="alur-step-num">2</div>
<div class="alur-step-icon">
<svg fill="none" 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>
</div>
<div class="alur-step-label">Cek Detail Warga</div>
<div class="alur-step-sub">Klik tombol "Detail" pada data warga untuk melihat informasi lengkap</div>
</div>
<div class="alur-arrow-wrap">
<svg fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
</div>
<div class="alur-step-box">
<div class="alur-step-num">3</div>
<div class="alur-step-icon">
<svg fill="none" 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>
</div>
<div class="alur-step-label">Ajukan ke perangkat desa</div>
<div class="alur-step-sub">Jika data sudah benar, klik "Ajukan" untuk dikirim ke admin perangkat desa</div>
</div>
<div class="alur-arrow-wrap">
<svg fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
</div>
<div class="alur-step-box">
<div class="alur-step-num">4</div>
<div class="alur-step-icon">
<svg fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/></svg>
</div>
<div class="alur-step-label">Lihat Hasil di Laporan</div>
<div class="alur-step-sub">Hasil akhir Diterima / Ditolak bisa dilihat di menu "Laporan"</div>
</div>
</div>
<div style="position:relative;background:linear-gradient(135deg,#10b981,#059669);color:#fff;border-radius:18px;padding:18px;display:flex;align-items:center;justify-content:space-between;overflow:hidden;box-shadow:0 6px 20px rgba(16,185,129,.3);">
<div class="sc-blob1"></div><div class="sc-blob2"></div>
<div style="position:relative;z-index:1;">
<p style="font-size:10.5px;font-weight:600;color:rgba(167,243,208,.9);">Diterima</p>
<p style="font-size:32px;font-weight:900;margin-top:2px;line-height:1;letter-spacing:-.04em;">{{ $stats['disetujui'] }}</p>
</div>
<div style="position:relative;z-index:1;width:44px;height:44px;background:rgba(255,255,255,.18);border-radius:13px;display:flex;align-items:center;justify-content:center;flex-shrink:0;border:1px solid rgba(255,255,255,.2);">
<svg width="22" height="22" fill="none" stroke="#fff" 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>
</div>
</div>
<div style="position:relative;background:linear-gradient(135deg,#f43f5e,#e11d48);color:#fff;border-radius:18px;padding:18px;display:flex;align-items:center;justify-content:space-between;overflow:hidden;box-shadow:0 6px 20px rgba(244,63,94,.3);">
<div class="sc-blob1"></div><div class="sc-blob2"></div>
<div style="position:relative;z-index:1;">
<p style="font-size:10.5px;font-weight:600;color:rgba(254,205,211,.9);">Ditolak</p>
<p style="font-size:32px;font-weight:900;margin-top:2px;line-height:1;letter-spacing:-.04em;">{{ $stats['ditolak'] }}</p>
</div>
<div style="position:relative;z-index:1;width:44px;height:44px;background:rgba(255,255,255,.18);border-radius:13px;display:flex;align-items:center;justify-content:center;flex-shrink:0;border:1px solid rgba(255,255,255,.2);">
<svg width="22" height="22" fill="none" stroke="#fff" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
</div>
</div>
</div>
{{-- GRID: Pendataan Terbaru + Aksi Cepat (logika asli 100% tidak diubah) --}}
<div style="display:grid;grid-template-columns:2fr 1fr;gap:14px;">
{{-- STAT CARDS --}}
<div class="rt-stat-grid">
@php
$rtCards = [
['label'=>'Total Warga','val'=>$stats['total_input'],'grad'=>'#2563eb,#1d4ed8','shadow'=>'rgba(37,99,235,.3)','tc'=>'rgba(191,219,254,.9)','icon'=>'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'],
['label'=>'Pending','val'=>$stats['pending'],'grad'=>'#f59e0b,#d97706','shadow'=>'rgba(245,158,11,.3)','tc'=>'rgba(254,243,199,.9)','icon'=>'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z'],
['label'=>'Diterima','val'=>$stats['disetujui'],'grad'=>'#10b981,#059669','shadow'=>'rgba(16,185,129,.3)','tc'=>'rgba(167,243,208,.9)','icon'=>'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z'],
['label'=>'Ditolak','val'=>$stats['ditolak'],'grad'=>'#f43f5e,#e11d48','shadow'=>'rgba(244,63,94,.3)','tc'=>'rgba(254,205,211,.9)','icon'=>'M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z'],
];
@endphp
@foreach($rtCards as $c)
<div style="position:relative;background:linear-gradient(135deg,{{ $c['grad'] }});color:#fff;border-radius:16px;padding:16px;display:flex;align-items:center;justify-content:space-between;overflow:hidden;box-shadow:0 6px 20px {{ $c['shadow'] }};">
<div class="sc-blob1"></div><div class="sc-blob2"></div>
<div style="position:relative;z-index:1;">
<p style="font-size:10px;font-weight:600;color:{{ $c['tc'] }};">{{ $c['label'] }}</p>
<p style="font-size:28px;font-weight:900;margin-top:2px;line-height:1;letter-spacing:-.04em;">{{ $c['val'] }}</p>
</div>
<div style="position:relative;z-index:1;width:38px;height:38px;background:rgba(255,255,255,.18);border-radius:11px;display:flex;align-items:center;justify-content:center;flex-shrink:0;border:1px solid rgba(255,255,255,.2);">
<svg width="18" height="18" fill="none" stroke="#fff" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="{{ $c['icon'] }}"/></svg>
</div>
</div>
@endforeach
</div>
{{-- GRID: Terbaru + Aksi --}}
<div class="rt-main-grid">
{{-- Pendataan Terbaru --}}
<div class="db-card">
<div style="padding:12px 18px;border-bottom:1px solid #f1f5f9;background:#fafbfc;display:flex;align-items:center;justify-content:space-between;">
<div style="padding:12px 16px;border-bottom:1px solid #f1f5f9;background:#fafbfc;display:flex;align-items:center;justify-content:space-between;">
<div style="display:flex;align-items:center;gap:7px;">
<div style="width:3px;height:16px;background:linear-gradient(180deg,#3b82f6,#2563eb);border-radius:4px;"></div>
<div style="width:3px;height:15px;background:linear-gradient(180deg,#3b82f6,#2563eb);border-radius:4px;"></div>
<h3 style="font-size:13px;font-weight:700;color:#1e293b;">Pendataan Terbaru</h3>
</div>
<span style="display:inline-flex;align-items:center;gap:4px;font-size:10px;color:#10b981;background:#f0fdf4;border:1px solid #bbf7d0;padding:2px 9px;border-radius:20px;font-weight:700;">
<span style="width:5px;height:5px;border-radius:50%;background:#10b981;animation:pulse 2s infinite;"></span>
Live
<span style="display:inline-flex;align-items:center;gap:4px;font-size:10px;color:#10b981;background:#f0fdf4;border:1px solid #bbf7d0;padding:2px 9px;border-radius:20px;font-weight:700;flex-shrink:0;">
<span style="width:5px;height:5px;border-radius:50%;background:#10b981;animation:pulse 2s infinite;display:inline-block;"></span>Live
</span>
</div>
<div>
@forelse($recentCalonPenerima as $calon)
<div class="recent-item">
<div style="display:flex;align-items:center;gap:10px;">
<div style="width:34px;height:34px;border-radius:10px;background:linear-gradient(135deg,#3b82f6,#2563eb);display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:12px;font-weight:800;color:#fff;box-shadow:0 3px 8px rgba(37,99,235,.22);">
{{ strtoupper(substr($calon->nama_lengkap, 0, 1)) }}
</div>
<div>
<p style="font-size:12.5px;font-weight:700;color:#0f172a;">{{ $calon->nama_lengkap }}</p>
<p style="font-size:10.5px;color:#94a3b8;margin-top:1px;font-family:monospace;">NIK: {{ $calon->nik }}</p>
</div>
<div class="recent-item">
<div style="display:flex;align-items:center;gap:10px;min-width:0;flex:1;">
<div style="width:32px;height:32px;border-radius:9px;background:linear-gradient(135deg,#3b82f6,#2563eb);display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:12px;font-weight:800;color:#fff;">
{{ strtoupper(substr($calon->nama_lengkap,0,1)) }}
</div>
<div style="text-align:right;">
@if($calon->status_verifikasi == 'pending')
<span style="display:inline-flex;align-items:center;gap:3px;padding:3px 9px;font-size:10.5px;font-weight:700;border-radius:20px;background:#fffbeb;color:#b45309;border:1px solid #fde68a;">
<span style="width:5px;height:5px;border-radius:50%;background:#f59e0b;flex-shrink:0;"></span>
Pending
</span>
@elseif($calon->status_verifikasi == 'disetujui')
<span style="display:inline-flex;align-items:center;gap:3px;padding:3px 9px;font-size:10.5px;font-weight:700;border-radius:20px;background:#f0fdf4;color:#166534;border:1px solid #bbf7d0;">
<span style="width:5px;height:5px;border-radius:50%;background:#22c55e;flex-shrink:0;"></span>
Disetujui
</span>
@else
<span style="display:inline-flex;align-items:center;gap:3px;padding:3px 9px;font-size:10.5px;font-weight:700;border-radius:20px;background:#fff1f2;color:#be123c;border:1px solid #fecdd3;">
<span style="width:5px;height:5px;border-radius:50%;background:#f43f5e;flex-shrink:0;"></span>
{{ ucfirst($calon->status_verifikasi) }}
</span>
@endif
<p style="font-size:10px;color:#94a3b8;margin-top:3px;">{{ $calon->created_at->diffForHumans() }}</p>
<div style="min-width:0;">
<p style="font-size:12px;font-weight:700;color:#0f172a;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:180px;">{{ $calon->nama_lengkap }}</p>
<p style="font-size:10px;color:#94a3b8;font-family:monospace;">{{ $calon->nik }}</p>
</div>
</div>
<div class="recent-item-right" style="text-align:right;flex-shrink:0;">
@php
$sv=['pending'=>['#fffbeb','#b45309','#fde68a','Pending'],'disetujui'=>['#f0fdf4','#166534','#bbf7d0','Disetujui']];
$sv=$sv[$calon->status_verifikasi]??['#fff1f2','#be123c','#fecdd3',ucfirst($calon->status_verifikasi)];
@endphp
<span style="display:inline-flex;align-items:center;gap:3px;padding:3px 9px;font-size:10px;font-weight:700;border-radius:20px;background:{{ $sv[0] }};color:{{ $sv[1] }};border:1px solid {{ $sv[2] }};white-space:nowrap;">
<span style="width:5px;height:5px;border-radius:50%;background:{{ $sv[1] }};flex-shrink:0;display:inline-block;"></span>{{ $sv[3] }}
</span>
<p style="font-size:10px;color:#94a3b8;margin-top:3px;">{{ $calon->created_at->diffForHumans() }}</p>
</div>
</div>
@empty
<div style="padding:44px 16px;text-align:center;">
<div style="width:44px;height:44px;background:#f1f5f9;border-radius:50%;display:flex;align-items:center;justify-content:center;margin:0 auto 10px;">
<svg width="20" height="20" fill="none" stroke="#d1d5db" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/></svg>
</div>
<p style="font-size:12.5px;font-weight:600;color:#94a3b8;">Belum ada data pendataan</p>
</div>
<div style="padding:44px 16px;text-align:center;">
<p style="font-size:12.5px;font-weight:600;color:#94a3b8;">Belum ada data pendataan</p>
</div>
@endforelse
</div>
</div>
{{-- Aksi Cepat (asli tidak diubah) --}}
{{-- Aksi Cepat --}}
<div class="db-card" style="padding:0;">
<div style="padding:12px 18px;border-bottom:1px solid #f1f5f9;background:#fafbfc;display:flex;align-items:center;gap:7px;">
<div style="width:3px;height:16px;background:linear-gradient(180deg,#10b981,#059669);border-radius:4px;"></div>
<div style="padding:12px 16px;border-bottom:1px solid #f1f5f9;background:#fafbfc;display:flex;align-items:center;gap:7px;">
<div style="width:3px;height:15px;background:linear-gradient(180deg,#10b981,#059669);border-radius:4px;"></div>
<h3 style="font-size:13px;font-weight:700;color:#1e293b;">Aksi Cepat</h3>
</div>
<div style="padding:14px;display:flex;flex-direction:column;gap:10px;">
{{-- Daftarkan Warga Baru (asli) --}}
<a href="{{ route('rt.calon-penerima.create') }}" class="aksi-btn"
style="background:#eff6ff;border-color:#bfdbfe;">
<div style="padding:12px;display:flex;flex-direction:column;gap:10px;">
<a href="{{ route('rt.calon-penerima.create') }}" class="aksi-btn" style="background:#eff6ff;border-color:#bfdbfe;">
<div class="aksi-icon" style="background:linear-gradient(135deg,#3b82f6,#2563eb);box-shadow:0 4px 12px rgba(37,99,235,.28);">
<svg 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>
</div>
<div>
<p style="font-size:12.5px;font-weight:700;color:#1e293b;">Daftarkan Warga Baru</p>
<div style="min-width:0;">
<p style="font-size:12px;font-weight:700;color:#1e293b;">Daftarkan Warga Baru</p>
<p style="font-size:10.5px;color:#64748b;margin-top:1px;">Tambah data calon penerima</p>
</div>
</a>
{{-- Lihat Laporan (asli) --}}
<a href="{{ route('rt.laporan.index') }}" class="aksi-btn"
style="background:#f0fdf4;border-color:#bbf7d0;">
<a href="{{ route('rt.laporan.index') }}" class="aksi-btn" style="background:#f0fdf4;border-color:#bbf7d0;">
<div class="aksi-icon" style="background:linear-gradient(135deg,#10b981,#059669);box-shadow:0 4px 12px rgba(16,185,129,.28);">
<svg viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/></svg>
</div>
<div>
<p style="font-size:12.5px;font-weight:700;color:#1e293b;">Lihat Laporan</p>
<p style="font-size:10.5px;color:#64748b;margin-top:1px;">Lihat hasil akhir yang dikirim admin</p>
<div style="min-width:0;">
<p style="font-size:12px;font-weight:700;color:#1e293b;">Lihat Laporan</p>
<p style="font-size:10.5px;color:#64748b;margin-top:1px;">Hasil akhir dari admin</p>
</div>
</a>
{{-- Rata-rata skor (asli, $avgProb tidak diubah) --}}
<div style="padding:14px;background:linear-gradient(135deg,#1e40af,#2563eb,#3b82f6);border-radius:14px;box-shadow:0 6px 20px rgba(37,99,235,.3);position:relative;overflow:hidden;">
<div style="position:absolute;top:-14px;right:-14px;width:70px;height:70px;background:rgba(255,255,255,.08);border-radius:50%;"></div>
<div style="position:absolute;bottom:-18px;left:30%;width:54px;height:54px;background:rgba(255,255,255,.06);border-radius:50%;"></div>
<div style="padding:14px;background:linear-gradient(135deg,#1e40af,#2563eb,#3b82f6);border-radius:13px;box-shadow:0 6px 20px rgba(37,99,235,.3);position:relative;overflow:hidden;">
<div style="position:absolute;top:-14px;right:-14px;width:70px;height:70px;background:rgba(255,255,255,.08);border-radius:50%;pointer-events:none;"></div>
<div style="position:relative;z-index:1;">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px;">
<p style="font-size:10.5px;font-weight:700;color:rgba(255,255,255,.85);">Rata-rata Skor Kelayakan</p>
<svg width="14" height="14" fill="none" stroke="rgba(191,219,254,.8)" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/></svg>
</div>
<p style="font-size:30px;font-weight:900;color:#fff;line-height:1;letter-spacing:-.04em;margin-bottom:4px;">
{{ number_format($avgProb, 1) }}<span style="font-size:14px;font-weight:600;color:rgba(191,219,254,.8);">%</span>
<p style="font-size:10px;font-weight:700;color:rgba(255,255,255,.85);margin-bottom:4px;">Rata-rata Skor Kelayakan</p>
<p style="font-size:28px;font-weight:900;color:#fff;line-height:1;letter-spacing:-.04em;margin-bottom:4px;">
{{ number_format($avgProb,1) }}<span style="font-size:13px;font-weight:600;color:rgba(191,219,254,.8);">%</span>
</p>
<div style="width:100%;height:4px;background:rgba(255,255,255,.2);border-radius:99px;overflow:hidden;margin-bottom:6px;">
<div style="height:100%;border-radius:99px;background:rgba(255,255,255,.7);width:{{ min($avgProb, 100) }}%;"></div>
<div style="height:100%;border-radius:99px;background:rgba(255,255,255,.7);width:{{ min($avgProb,100) }}%;"></div>
</div>
<p style="font-size:10px;color:rgba(191,219,254,.75);font-weight:500;line-height:1.5;">
Nilai rata-rata prediksi kelayakan, bukan keputusan akhir bantuan
</p>
<p style="font-size:10px;color:rgba(191,219,254,.75);font-weight:500;line-height:1.5;">Rata-rata prediksi, bukan keputusan akhir</p>
</div>
</div>
</div>
</div>
@ -244,11 +286,4 @@ function updateClock() {
</div>
<style>
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: .5; transform: scale(.85); }
}
</style>
</x-app-layout>

View File

@ -1,111 +1,103 @@
<x-app-layout>
<style>
.lk-wrap{max-width:1200px;margin:0 auto;padding:24px 20px 40px;}
<x-slot name="header">
<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;">
<div>
<h2 style="font-size:16px;font-weight:800;color:#0f172a;letter-spacing:-.02em;">Laporan Hasil Kelayakan</h2>
<p style="font-size:11px;color:#94a3b8;margin-top:2px;">Data hasil akhir verifikasi warga yang telah dikirim admin perangkat desa</p>
</div>
</div>
</x-slot>
/* Card */
.lk-card{background:#fff;border-radius:20px;border:1.5px solid #f1f5f9;box-shadow:0 2px 8px rgba(0,0,0,.05);overflow:hidden;margin-bottom:14px;}
<style>
.lk-card{background:#fff;border-radius:16px;border:1.5px solid #f1f5f9;box-shadow:0 2px 8px rgba(0,0,0,.05);overflow:hidden;margin-bottom:14px;}
.sec-hd{padding:11px 16px;border-bottom:1.5px solid #f1f5f9;background:#fafbfc;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;}
.sec-hd-l{display:flex;align-items:center;gap:8px;}
.sec-bar{width:3px;height:15px;background:linear-gradient(180deg,#3b82f6,#2563eb);border-radius:4px;}
.sec-title{font-size:13px;font-weight:800;color:#1e293b;}
.sec-count{font-size:11px;color:#94a3b8;background:#f1f5f9;padding:2px 8px;border-radius:20px;font-weight:600;}
/* Section header */
.sec-hd{padding:12px 18px;border-bottom:1.5px solid #f1f5f9;background:#fafbfc;display:flex;align-items:center;justify-content:space-between;}
.sec-hd-l{display:flex;align-items:center;gap:8px;}
.sec-bar{width:3px;height:16px;background:linear-gradient(180deg,#3b82f6,#2563eb);border-radius:4px;}
.sec-title{font-size:13px;font-weight:800;color:#1e293b;}
.sec-count{font-size:11px;color:#94a3b8;background:#f1f5f9;padding:2px 8px;border-radius:20px;font-weight:600;}
/* stat cards */
.sum-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-bottom:14px;}
@media(max-width:560px){.sum-grid{grid-template-columns:1fr 1fr;}}
.sum-card{background:#fff;border-radius:16px;border:1.5px solid #f1f5f9;box-shadow:0 1px 6px rgba(0,0,0,.04);padding:14px 16px;}
.sum-icon{width:34px;height:34px;border-radius:10px;display:flex;align-items:center;justify-content:center;margin-bottom:8px;}
.sum-icon svg{width:17px;height:17px;}
.sum-val{font-size:24px;font-weight:900;color:#0f172a;line-height:1;letter-spacing:-.03em;}
.sum-lbl{font-size:10px;color:#94a3b8;font-weight:700;margin-top:3px;text-transform:uppercase;letter-spacing:.04em;}
/* Summary */
.sum-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-bottom:14px;}
@media(max-width:700px){.sum-grid{grid-template-columns:1fr 1fr;}}
.sum-card{background:#fff;border-radius:18px;border:1.5px solid #f1f5f9;box-shadow:0 1px 6px rgba(0,0,0,.04);padding:14px 16px;}
.sum-icon{width:34px;height:34px;border-radius:10px;display:flex;align-items:center;justify-content:center;margin-bottom:9px;}
.sum-icon svg{width:17px;height:17px;}
.sum-val{font-size:22px;font-weight:900;color:#0f172a;line-height:1;letter-spacing:-.03em;}
.sum-lbl{font-size:10px;color:#94a3b8;font-weight:700;margin-top:3px;text-transform:uppercase;letter-spacing:.04em;}
/* desktop table */
.lk-tbl-wrap{overflow-x:auto;-webkit-overflow-scrolling:touch;}
table.lk-tbl{width:100%;border-collapse:collapse;min-width:860px;}
table.lk-tbl thead tr{background:#f8fafc;border-bottom:1.5px solid #f1f5f9;}
table.lk-tbl thead th{padding:9px 11px;text-align:left;font-size:9.5px;font-weight:800;color:#94a3b8;text-transform:uppercase;letter-spacing:.07em;white-space:nowrap;}
table.lk-tbl tbody tr{border-bottom:1px solid #f8fafc;transition:background .1s;}
table.lk-tbl tbody tr:hover{background:#f0f7ff;}
table.lk-tbl tbody tr:last-child{border-bottom:none;}
table.lk-tbl tbody td{padding:9px 11px;vertical-align:middle;font-size:12px;color:#374151;}
/* Table */
.lk-tbl-wrap{overflow-x:auto;}
table.lk-tbl{width:100%;border-collapse:collapse;}
table.lk-tbl thead tr{background:#f8fafc;border-bottom:1.5px solid #f1f5f9;}
table.lk-tbl thead th{padding:10px 11px;text-align:left;font-size:9.5px;font-weight:800;color:#94a3b8;text-transform:uppercase;letter-spacing:.07em;white-space:nowrap;}
table.lk-tbl tbody tr{border-bottom:1px solid #f8fafc;transition:background .1s;}
table.lk-tbl tbody tr:hover{background:#f0f7ff;}
table.lk-tbl tbody tr:last-child{border-bottom:none;}
table.lk-tbl tbody td{padding:9px 11px;vertical-align:middle;font-size:12px;color:#374151;}
/* mobile card */
.mob-list{display:none;flex-direction:column;gap:10px;padding:14px;}
@media(max-width:640px){
.lk-tbl-wrap{display:none;}
.mob-list{display:flex;}
}
.mob-card{background:#fafbfc;border:1.5px solid #f1f5f9;border-radius:13px;padding:13px;display:flex;flex-direction:column;gap:10px;}
.mob-top{display:flex;align-items:center;gap:10px;}
.mob-name{font-size:13px;font-weight:800;color:#0f172a;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.mob-rows{display:grid;grid-template-columns:1fr 1fr;gap:6px 10px;}
.mob-kv{display:flex;flex-direction:column;gap:2px;}
.mob-k{font-size:9px;font-weight:700;color:#94a3b8;text-transform:uppercase;letter-spacing:.05em;}
.mob-v{font-size:11.5px;font-weight:600;color:#374151;}
.mob-bottom{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:6px;padding-top:8px;border-top:1px solid #f1f5f9;}
/* Cells */
.av{width:30px;height:30px;border-radius:9px;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:800;color:#fff;flex-shrink:0;}
.name-cell{display:flex;align-items:center;gap:8px;}
.nik-pill{font-family:'Courier New',monospace;font-size:10px;color:#64748b;background:#f1f5f9;padding:2px 6px;border-radius:5px;}
.rt-badge{display:inline-flex;padding:2px 7px;border-radius:6px;background:#eff6ff;color:#1d4ed8;font-size:10px;font-weight:800;border:1px solid #bfdbfe;}
.prob-cell{display:flex;align-items:center;gap:6px;}
.prob-bg{width:38px;height:4px;background:#f1f5f9;border-radius:99px;overflow:hidden;flex-shrink:0;}
.prob-fill{height:100%;border-radius:99px;}
.st-pill{display:inline-flex;align-items:center;gap:4px;padding:4px 10px;border-radius:20px;font-size:10.5px;font-weight:700;}
.st-dot{width:5px;height:5px;border-radius:50%;flex-shrink:0;}
/* shared */
.av{width:30px;height:30px;border-radius:9px;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:800;color:#fff;flex-shrink:0;}
.nik-pill{font-family:'Courier New',monospace;font-size:10px;color:#64748b;background:#f1f5f9;padding:2px 6px;border-radius:5px;}
.rt-badge{display:inline-flex;padding:2px 7px;border-radius:6px;background:#eff6ff;color:#1d4ed8;font-size:10px;font-weight:800;border:1px solid #bfdbfe;}
.prob-cell{display:flex;align-items:center;gap:6px;}
.prob-bg{width:38px;height:4px;background:#f1f5f9;border-radius:99px;overflow:hidden;flex-shrink:0;}
.prob-fill{height:100%;border-radius:99px;}
.st-pill{display:inline-flex;align-items:center;gap:4px;padding:4px 10px;border-radius:20px;font-size:10.5px;font-weight:700;}
.st-dot{width:5px;height:5px;border-radius:50%;flex-shrink:0;}
.empty-state{padding:52px 16px;text-align:center;}
.empty-icon{width:46px;height:46px;background:#f1f5f9;border-radius:50%;display:flex;align-items:center;justify-content:center;margin:0 auto 10px;}
.tbl-foot{padding:9px 16px;border-top:1.5px solid #f1f5f9;background:#fafbfc;text-align:center;}
.credit{font-size:11px;color:#cbd5e1;font-weight:500;}
</style>
/* Empty */
.empty-state{padding:52px 16px;text-align:center;}
.empty-icon{width:46px;height:46px;background:#f1f5f9;border-radius:50%;display:flex;align-items:center;justify-content:center;margin:0 auto 10px;}
.empty-icon svg{width:20px;height:20px;}
/* Footer */
.tbl-foot{padding:9px 18px;border-top:1.5px solid #f1f5f9;background:#fafbfc;text-align:center;}
.credit{font-size:11px;color:#cbd5e1;font-weight:500;}
</style>
<div class="lk-wrap">
{{-- ── HEADER ── --}}
<div style="margin-bottom:18px;">
<h1 style="font-size:20px;font-weight:900;color:#0f172a;letter-spacing:-.03em;line-height:1.2;">
Laporan Hasil Kelayakan
</h1>
<p style="font-size:11.5px;color:#94a3b8;margin-top:4px;">
Data hasil akhir verifikasi warga yang telah dikirim oleh admin kelurahan
</p>
</div>
{{-- ── SUMMARY ── --}}
@php
$total = $stats['total'] ?? $laporans->count();
$diterima = $stats['diterima'] ?? $laporans->where('status_verifikasi', 'disetujui')->count();
$ditolak = $stats['ditolak'] ?? $laporans->where('status_verifikasi', 'ditolak')->count();
$total = $stats['total'] ?? $laporans->count();
$diterima = $stats['diterima'] ?? $laporans->where('status_verifikasi','disetujui')->count();
$ditolak = $stats['ditolak'] ?? $laporans->where('status_verifikasi','ditolak')->count();
@endphp
{{-- STAT CARDS --}}
<div class="sum-grid">
<div class="sum-card">
<div class="sum-icon" style="background:#eff6ff;">
<svg fill="none" stroke="#2563eb" 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>
<svg fill="none" stroke="#2563eb" 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>
<div class="sum-val">{{ $total }}</div>
<div class="sum-lbl">Total Warga</div>
</div>
<div class="sum-card">
<div class="sum-icon" style="background:#f0fdf4;">
<svg fill="none" stroke="#16a34a" 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>
<svg fill="none" stroke="#16a34a" 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>
</div>
<div class="sum-val" style="color:#16a34a;">{{ $diterima }}</div>
<div class="sum-lbl">Diterima</div>
</div>
<div class="sum-card">
<div class="sum-icon" style="background:#fff1f2;">
<svg fill="none" stroke="#e11d48" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<svg fill="none" stroke="#e11d48" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
</div>
<div class="sum-val" style="color:#e11d48;">{{ $ditolak }}</div>
<div class="sum-lbl">Ditolak</div>
</div>
</div>
{{-- ── TABLE ── --}}
{{-- TABLE CARD --}}
<div class="lk-card">
<div class="sec-hd">
<div class="sec-hd-l">
@ -115,11 +107,12 @@
<span class="sec-count">{{ $total }} data</span>
</div>
{{-- DESKTOP TABLE --}}
<div class="lk-tbl-wrap">
<table class="lk-tbl">
<thead>
<tr>
<th style="width:32px;">#</th>
<th>#</th>
<th>Nama</th>
<th>NIK</th>
<th>RT</th>
@ -137,113 +130,149 @@
<tbody>
@forelse($laporans as $i => $item)
@php
$prob = $item->probability ?? 0;
if ($prob <= 1) $prob = $prob * 100;
$prob = $item->probability ?? 0;
if($prob <= 1) $prob = $prob * 100;
$nama = $item->nama_lengkap ?? '-';
$init = strtoupper(substr($nama, 0, 1));
$sc = $prob >= 70 ? '#10b981' : ($prob >= 40 ? '#f59e0b' : '#f43f5e');
$init = strtoupper(substr($nama,0,1));
$sc = $prob>=70?'#10b981':($prob>=40?'#f59e0b':'#f43f5e');
$colors = ['#3b82f6','#8b5cf6','#ec4899','#f59e0b','#10b981','#ef4444','#06b6d4'];
$avColor = $colors[ord($init) % count($colors)];
$diterima = $item->status_verifikasi === 'disetujui';
$acc = ($item->status_verifikasi === 'disetujui');
@endphp
<tr>
<td style="text-align:center;font-size:10.5px;color:#94a3b8;font-weight:700;">{{ $i + 1 }}</td>
<td style="text-align:center;font-size:10.5px;color:#94a3b8;font-weight:700;">{{ $i+1 }}</td>
<td>
<div class="name-cell">
<div style="display:flex;align-items:center;gap:8px;">
<div class="av" style="background:{{ $avColor }};">{{ $init }}</div>
<div>
<div style="font-size:12.5px;font-weight:700;color:#0f172a;">{{ $nama }}</div>
</div>
<span style="font-size:12.5px;font-weight:700;color:#0f172a;white-space:nowrap;">{{ $nama }}</span>
</div>
</td>
<td><span class="nik-pill">{{ $item->nik ?? '-' }}</span></td>
<td><span class="nik-pill">{{ $item->nik??'-' }}</span></td>
<td><span class="rt-badge">RT {{ str_pad($item->nomor_rt??'-',3,'0',STR_PAD_LEFT) }}</span></td>
<td style="font-size:11.5px;white-space:nowrap;">{{ $item->nama_dusun??'-' }}</td>
<td style="font-size:11.5px;">{{ $item->pekerjaan??'-' }}</td>
<td style="font-size:12px;font-weight:600;white-space:nowrap;">Rp {{ number_format($item->penghasilan??0,0,',','.') }}</td>
<td style="text-align:center;font-size:12px;font-weight:600;">{{ $item->jumlah_tanggungan??'-' }}</td>
<td style="font-size:11.5px;">{{ $item->aset_kepemilikan??'-' }}</td>
<td>
<span class="rt-badge">
RT {{ str_pad($item->nomor_rt ?? '-', 3, '0', STR_PAD_LEFT) }}
</span>
</td>
<td style="font-size:11.5px;white-space:nowrap;">
{{ $item->nama_dusun ?? '-' }}
</td>
<td style="font-size:11.5px;max-width:110px;">{{ $item->pekerjaan ?? '-' }}</td>
<td style="font-size:12px;font-weight:600;color:#0f172a;white-space:nowrap;">
Rp {{ number_format($item->penghasilan ?? 0, 0, ',', '.') }}
</td>
<td style="text-align:center;font-size:12px;font-weight:600;">
{{ $item->jumlah_tanggungan ?? '-' }}
</td>
<td style="font-size:11.5px;max-width:110px;">{{ $item->aset_kepemilikan ?? '-' }}</td>
<td>
@if(strtolower($item->bantuan_lain ?? '') === 'ya')
@if(strtolower($item->bantuan_lain??'')=='ya')
<span style="display:inline-flex;padding:2px 8px;border-radius:20px;background:#fffbeb;color:#b45309;font-size:10px;font-weight:700;border:1px solid #fde68a;">Ya</span>
@else
<span style="display:inline-flex;padding:2px 8px;border-radius:20px;background:#f8fafc;color:#94a3b8;font-size:10px;font-weight:600;">Tidak</span>
@endif
</td>
<td style="text-align:center;font-size:12px;color:#374151;">{{ $item->usia ?? '-' }}</td>
<td style="text-align:center;font-size:12px;">{{ $item->usia??'-' }}</td>
<td>
<div class="prob-cell">
<div class="prob-bg">
<div class="prob-fill" style="width:{{ min($prob,100) }}%;background:{{ $sc }};"></div>
</div>
<span style="font-size:11px;font-weight:800;color:{{ $sc }};white-space:nowrap;">
{{ number_format($prob, 1) }}%
</span>
<div class="prob-bg"><div class="prob-fill" style="width:{{ min($prob,100) }}%;background:{{ $sc }};"></div></div>
<span style="font-size:11px;font-weight:800;color:{{ $sc }};white-space:nowrap;">{{ number_format($prob,1) }}%</span>
</div>
</td>
<td>
@if($diterima)
<span class="st-pill" style="background:#f0fdf4;color:#166534;border:1px solid #bbf7d0;">
<span class="st-dot" style="background:#22c55e;"></span>
Diterima
</span>
@if($acc)
<span class="st-pill" style="background:#f0fdf4;color:#166534;border:1px solid #bbf7d0;"><span class="st-dot" style="background:#22c55e;"></span>Diterima</span>
@else
<span class="st-pill" style="background:#fff1f2;color:#be123c;border:1px solid #fecdd3;">
<span class="st-dot" style="background:#f43f5e;"></span>
Ditolak
</span>
<span class="st-pill" style="background:#fff1f2;color:#be123c;border:1px solid #fecdd3;"><span class="st-dot" style="background:#f43f5e;"></span>Ditolak</span>
@endif
</td>
</tr>
@empty
<tr>
<td colspan="13">
<div class="empty-state">
<div class="empty-icon">
<svg fill="none" stroke="#d1d5db" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
</div>
<p style="font-size:13px;font-weight:600;color:#94a3b8;">Belum ada laporan yang dikirim admin</p>
</div>
</td>
</tr>
<tr><td colspan="13">
<div class="empty-state">
<div class="empty-icon"><svg fill="none" stroke="#d1d5db" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg></div>
<p style="font-size:13px;font-weight:600;color:#94a3b8;">Belum ada laporan yang dikirim admin</p>
</div>
</td></tr>
@endforelse
</tbody>
</table>
</div>
<div class="tbl-foot">
<span class="credit">SiBantuDes &middot; Kelurahan Ngerong</span>
{{-- MOBILE CARD LIST --}}
<div class="mob-list">
@forelse($laporans as $i => $item)
@php
$prob = $item->probability ?? 0;
if($prob <= 1) $prob = $prob * 100;
$nama = $item->nama_lengkap ?? '-';
$init = strtoupper(substr($nama,0,1));
$sc = $prob>=70?'#10b981':($prob>=40?'#f59e0b':'#f43f5e');
$scBg = $prob>=70?'#f0fdf4':($prob>=40?'#fffbeb':'#fff1f2');
$scBd = $prob>=70?'#bbf7d0':($prob>=40?'#fde68a':'#fecdd3');
$colors = ['#3b82f6','#8b5cf6','#ec4899','#f59e0b','#10b981','#ef4444','#06b6d4'];
$avColor = $colors[ord($init) % count($colors)];
$acc = ($item->status_verifikasi === 'disetujui');
@endphp
<div class="mob-card">
<div class="mob-top">
<div class="av" style="background:{{ $avColor }};">{{ $init }}</div>
<div style="flex:1;min-width:0;">
<div class="mob-name">{{ $nama }}</div>
<div style="font-size:10px;font-family:monospace;color:#94a3b8;">{{ $item->nik??'-' }}</div>
</div>
<span style="font-size:10px;color:#94a3b8;font-weight:700;">#{{ $i+1 }}</span>
</div>
<div class="mob-rows">
<div class="mob-kv">
<span class="mob-k">RT</span>
<span class="rt-badge" style="width:fit-content;">RT {{ str_pad($item->nomor_rt??'-',3,'0',STR_PAD_LEFT) }}</span>
</div>
<div class="mob-kv">
<span class="mob-k">Dusun</span>
<span class="mob-v">{{ $item->nama_dusun??'-' }}</span>
</div>
<div class="mob-kv">
<span class="mob-k">Pekerjaan</span>
<span class="mob-v">{{ $item->pekerjaan??'-' }}</span>
</div>
<div class="mob-kv">
<span class="mob-k">Usia</span>
<span class="mob-v">{{ $item->usia??'-' }} thn</span>
</div>
<div class="mob-kv">
<span class="mob-k">Penghasilan</span>
<span class="mob-v">Rp {{ number_format($item->penghasilan??0,0,',','.') }}</span>
</div>
<div class="mob-kv">
<span class="mob-k">Tanggungan</span>
<span class="mob-v">{{ $item->jumlah_tanggungan??'-' }} orang</span>
</div>
<div class="mob-kv">
<span class="mob-k">Aset</span>
<span class="mob-v">{{ $item->aset_kepemilikan??'-' }}</span>
</div>
<div class="mob-kv">
<span class="mob-k">Bantuan Lain</span>
@if(strtolower($item->bantuan_lain??'')=='ya')
<span style="display:inline-flex;padding:2px 7px;border-radius:20px;background:#fffbeb;color:#b45309;font-size:10px;font-weight:700;border:1px solid #fde68a;width:fit-content;">Ya</span>
@else
<span style="display:inline-flex;padding:2px 7px;border-radius:20px;background:#f8fafc;color:#94a3b8;font-size:10px;font-weight:600;width:fit-content;">Tidak</span>
@endif
</div>
</div>
<div class="mob-bottom">
<div class="prob-cell">
<div class="prob-bg"><div class="prob-fill" style="width:{{ min($prob,100) }}%;background:{{ $sc }};"></div></div>
<span style="font-size:11px;font-weight:800;color:{{ $sc }};">{{ number_format($prob,1) }}%</span>
</div>
@if($acc)
<span class="st-pill" style="background:#f0fdf4;color:#166534;border:1px solid #bbf7d0;"><span class="st-dot" style="background:#22c55e;"></span>Diterima</span>
@else
<span class="st-pill" style="background:#fff1f2;color:#be123c;border:1px solid #fecdd3;"><span class="st-dot" style="background:#f43f5e;"></span>Ditolak</span>
@endif
</div>
</div>
@empty
<div class="empty-state">
<div class="empty-icon"><svg fill="none" stroke="#d1d5db" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg></div>
<p style="font-size:13px;font-weight:600;color:#94a3b8;">Belum ada laporan yang dikirim admin</p>
</div>
@endforelse
</div>
<div class="tbl-foot"><span class="credit">SiBantuDes · Desa Ngerong</span></div>
</div>
</div>
</x-app-layout>

File diff suppressed because one or more lines are too long

View File

@ -6,7 +6,6 @@
use App\Http\Controllers\Auth\EmailVerificationPromptController;
use App\Http\Controllers\Auth\NewPasswordController;
use App\Http\Controllers\Auth\PasswordController;
use App\Http\Controllers\Auth\PasswordResetLinkController;
use App\Http\Controllers\Auth\RegisteredUserController;
use App\Http\Controllers\Auth\VerifyEmailController;
use Illuminate\Support\Facades\Route;
@ -22,17 +21,8 @@
Route::post('login', [AuthenticatedSessionController::class, 'store']);
Route::get('forgot-password', [PasswordResetLinkController::class, 'create'])
->name('password.request');
Route::post('forgot-password', [PasswordResetLinkController::class, 'store'])
->name('password.email');
Route::get('reset-password/{token}', [NewPasswordController::class, 'create'])
->name('password.reset');
Route::post('reset-password', [NewPasswordController::class, 'store'])
->name('password.store');
/* forgot-password & reset-password dihapus dari sini,
sudah diganti dengan ManualPasswordResetController di web.php */
});
Route::middleware('auth')->group(function () {
@ -56,4 +46,4 @@
Route::post('logout', [AuthenticatedSessionController::class, 'destroy'])
->name('logout');
});
});

View File

@ -2,7 +2,10 @@
use Illuminate\Foundation\Inspiring;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Schedule;
Artisan::command('inspire', function () {
$this->comment(Inspiring::quote());
})->purpose('Display an inspiring quote');
Schedule::command('blt:reset-bulanan')->monthlyOn(1, '01:00');

View File

@ -6,18 +6,21 @@
use App\Models\CalonPenerima;
use App\Models\Dusun;
use App\Models\PenerimaFinal;
use App\Http\Controllers\LandingPageController;
use App\Http\Controllers\Admin\AdminController;
use App\Http\Controllers\Admin\FilterisasiController;
use App\Http\Controllers\Admin\LaporanController;
// RT Controllers
use App\Http\Controllers\Rt\DashboardController as RtDashboardController;
use App\Http\Controllers\Rt\CalonPenerimaController as RtCalonPenerimaController;
use App\Http\Controllers\Rt\LaporanController as RtLaporanController;
// Manual Password Reset
use App\Http\Controllers\Auth\ManualPasswordResetController;
/*
|--------------------------------------------------------------------------
| ROOT / LANDING PAGE PUBLIK
@ -25,6 +28,23 @@
*/
Route::get('/', [LandingPageController::class, 'index'])->name('landing');
/*
|--------------------------------------------------------------------------
| MANUAL PASSWORD RESET (guest only)
|--------------------------------------------------------------------------
*/
Route::get('/forgot-password', [ManualPasswordResetController::class, 'showForm'])
->middleware('guest')
->name('password.request');
Route::post('/password/manual/check', [ManualPasswordResetController::class, 'checkEmail'])
->middleware('guest')
->name('password.manual.check');
Route::post('/password/manual/update', [ManualPasswordResetController::class, 'updatePassword'])
->middleware('guest')
->name('password.manual.update');
/*
|--------------------------------------------------------------------------
| DASHBOARD UTAMA (redirect sesuai role)
@ -137,7 +157,8 @@
$q = trim((string) $request->query('q', ''));
$wargasQuery = CalonPenerima::with(['rt.dusun', 'prediksiKelayakan', 'user'])
->whereIn('tracking_status', ['terkirim', 'sedang_validasi', 'selesai'])
->whereIn('tracking_status', ['terkirim', 'sedang_validasi', 'selesai', 'ditinjau_ulang'])
->whereNull('arsip_tahun')
->latest();
if ($q !== '') {
@ -157,25 +178,36 @@
return view('admin.data-warga', compact('dusuns', 'wargas'));
})->name('data-warga');
// TAMBAHAN: BERSIHKAN DATA WARGA AKTIF
Route::delete('/data-warga/bersihkan', function (Request $request) {
// LIHAT ARSIP — harus di atas semua route {id} agar tidak bentrok
Route::get('/data-warga/arsip', function (Request $request) {
$user = $request->user();
if (!$user || $user->role !== 'admin') {
abort(403, 'Akses ditolak: hanya Admin.');
}
DB::transaction(function () {
// hapus hasil prediksi yang terkait data aktif
DB::table('prediksi_kelayakans')->delete();
$tahun = $request->query('tahun', now()->year);
$q = trim((string) $request->query('q', ''));
$daftarTahun = CalonPenerima::whereNotNull('arsip_tahun')
->distinct()
->orderByDesc('arsip_tahun')
->pluck('arsip_tahun');
// hapus data pengajuan aktif
DB::table('calon_penerimas')->delete();
});
$arsipQuery = CalonPenerima::with(['rt.dusun', 'prediksiKelayakan', 'user'])
->where('arsip_tahun', $tahun)
->latest();
return redirect()->route('admin.data-warga')
->with('success', 'Semua data warga aktif berhasil dibersihkan. Arsip laporan tetap aman.');
})->name('data-warga.bersihkan');
if ($q !== '') {
$arsipQuery->where(function ($sub) use ($q) {
$sub->where('nik', 'like', "%{$q}%")
->orWhere('nama_lengkap', 'like', "%{$q}%");
});
}
$arsips = $arsipQuery->paginate(10)->withQueryString();
return view('admin.data-warga-arsip', compact('arsips', 'tahun', 'daftarTahun'));
})->name('data-warga.arsip');
// MULAI VALIDASI
Route::post('/data-warga/{id}/mulai-validasi', function (Request $request, $id) {
@ -187,7 +219,7 @@
$warga = CalonPenerima::findOrFail($id);
if ($warga->tracking_status !== 'terkirim') {
if (!in_array($warga->tracking_status, ['terkirim', 'ditinjau_ulang'])) {
return back()->with('error', 'Data ini belum bisa masuk tahap validasi.');
}
@ -224,6 +256,132 @@
return back()->with('success', 'Hasil validasi berhasil dikirim ke RT.');
})->name('data-warga.selesai-validasi');
// TERIMA LANGSUNG
Route::post('/data-warga/{id}/terima', function (Request $request, $id) {
$user = $request->user();
if (!$user || $user->role !== 'admin') {
abort(403, 'Akses ditolak: hanya Admin.');
}
$warga = CalonPenerima::findOrFail($id);
if (!in_array($warga->tracking_status, ['terkirim', 'sedang_validasi', 'ditinjau_ulang'])) {
return back()->with('error', 'Status tidak memungkinkan untuk diterima langsung.');
}
$warga->update([
'tracking_status' => 'selesai',
'status_verifikasi' => 'disetujui',
]);
return back()->with('success', 'Warga berhasil diterima sebagai penerima BLT-DD.');
})->name('data-warga.terima');
// TOLAK LANGSUNG
Route::post('/data-warga/{id}/tolak', function (Request $request, $id) {
$user = $request->user();
if (!$user || $user->role !== 'admin') {
abort(403, 'Akses ditolak: hanya Admin.');
}
$warga = CalonPenerima::findOrFail($id);
if (!in_array($warga->tracking_status, ['terkirim', 'sedang_validasi', 'ditinjau_ulang'])) {
return back()->with('error', 'Status tidak memungkinkan untuk ditolak langsung.');
}
$warga->update([
'tracking_status' => 'selesai',
'status_verifikasi' => 'ditolak',
]);
// Otomatis catat sebagai penerima final dengan status ditolak,
// agar langsung muncul di Laporan dan tidak ikut Filterisasi.
PenerimaFinal::updateOrCreate(
[
'nik' => $warga->nik,
'periode_bantuan' => '2026 Triwulan 1',
],
[
'nama_lengkap' => $warga->nama_lengkap ?? '-',
'no_kk' => $warga->no_kk ?? '-',
'rt_id' => $warga->rt_id,
'nomor_rt' => $warga->rt?->nomor_rt ?? null,
'nama_dusun' => $warga->rt?->dusun?->nama_dusun ?? '-',
'pekerjaan' => $warga->pekerjaan ?? '-',
'penghasilan' => $warga->penghasilan ?? 0,
'jumlah_tanggungan' => $warga->jumlah_tanggungan ?? 0,
'aset_kepemilikan' => $warga->aset_kepemilikan ?? '-',
'kondisi_rumah' => $warga->kondisi_rumah ?? 'Tidak Diketahui',
'meteran_listrik' => $warga->meteran_listrik ?? 'Tidak Diketahui',
'sumber_air' => $warga->sumber_air ?? 'Tidak Diketahui',
'bantuan_lain' => $warga->bantuan_lain ?? 'tidak',
'usia' => $warga->usia ?? 0,
'probability' => $warga->prediksiKelayakan->probability ?? 0,
'status_verifikasi' => 'ditolak',
'tanggal_penetapan' => now()->toDateString(),
'periode_bantuan' => '2026 Triwulan 1',
'jumlah_bantuan' => 300000,
]
);
return back()->with('success', 'Pengajuan warga berhasil ditolak.');
})->name('data-warga.tolak');
// AKTIFKAN KEMBALI
Route::post('/data-warga/{id}/aktifkan-kembali', function (Request $request, $id) {
$user = $request->user();
if (!$user || $user->role !== 'admin') {
abort(403, 'Akses ditolak: hanya Admin.');
}
$warga = CalonPenerima::findOrFail($id);
if ($warga->tracking_status !== 'selesai') {
return back()->with('error', 'Hanya data yang sudah selesai yang bisa diaktifkan kembali.');
}
// Jika warga sebelumnya tercatat sebagai PenerimaFinal dengan status ditolak
// (hasil tolak langsung dari Data Warga), hapus agar tidak nyangkut di Laporan
// saat diaktifkan kembali untuk ditinjau ulang.
PenerimaFinal::where('nik', $warga->nik)
->where('periode_bantuan', '2026 Triwulan 1')
->where('status_verifikasi', 'ditolak')
->delete();
$warga->update([
'tracking_status' => 'ditinjau_ulang',
'status_verifikasi' => 'pending',
'arsip_tahun' => null,
]);
return back()->with('success', 'Pengajuan warga berhasil diaktifkan kembali untuk ditinjau ulang.');
})->name('data-warga.aktifkan-kembali');
// ARSIPKAN
Route::post('/data-warga/{id}/arsipkan', function (Request $request, $id) {
$user = $request->user();
if (!$user || $user->role !== 'admin') {
abort(403, 'Akses ditolak: hanya Admin.');
}
$warga = CalonPenerima::findOrFail($id);
if ($warga->tracking_status !== 'selesai') {
return back()->with('error', 'Hanya data selesai yang bisa diarsipkan.');
}
$warga->update([
'arsip_tahun' => now()->year,
]);
return back()->with('success', 'Data berhasil diarsipkan ke tahun ' . now()->year . '.');
})->name('data-warga.arsipkan');
// DATA AKUN
Route::get('/data-akun', [AdminController::class, 'dataAkun'])->name('data-akun');
Route::post('/data-akun/{id}/role', [AdminController::class, 'ubahRole'])->name('data-akun.role');
@ -234,12 +392,15 @@
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');
Route::post('/filterisasi/simpan-periode', [FilterisasiController::class, 'simpanPeriode'])->name('filterisasi.simpanPeriode');
Route::delete('/filterisasi/hapus-periode/{id}', [FilterisasiController::class, 'hapusPeriode'])->name('filterisasi.hapusPeriode');
// LAPORAN ADMIN
Route::get('/laporan', [LaporanController::class, 'index'])->name('laporan');
Route::get('/laporan/pdf', [LaporanController::class, 'exportPdf'])->name('laporan.pdf');
Route::get('/laporan/excel', [LaporanController::class, 'exportExcel'])->name('laporan.excel');
Route::post('/laporan/kirim-ke-rt', [LaporanController::class, 'kirimKeRt'])->name('laporan.kirim-ke-rt');
Route::post('/laporan/reset-manual', [LaporanController::class, 'resetManual'])->name('laporan.reset-manual');
Route::post('/laporan/upload-publik-pdf', [LaporanController::class, 'uploadPublikPdf'])->name('laporan.upload-publik-pdf');
Route::delete('/laporan/publik-pdf/{id}', [LaporanController::class, 'hapusPublikPdf'])->name('laporan.hapus-publik-pdf');
Route::delete('/laporan/bersihkan-riwayat-selesai', [LaporanController::class, 'bersihkanRiwayatSelesai'])