make landing page, fix blade admin & RT

This commit is contained in:
Ryanpratama01 2026-03-16 01:47:06 +07:00
parent b0140be0a8
commit e55dc1104b
32 changed files with 4881 additions and 810 deletions

35
Dockerfile Normal file
View File

@ -0,0 +1,35 @@
FROM php:8.2-apache
RUN apt-get update && apt-get install -y \
git \
unzip \
libpq-dev \
libzip-dev \
zip \
curl \
python3 \
python3-pip \
&& docker-php-ext-install pdo pdo_pgsql pgsql zip
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /var/www/html
COPY . /var/www/html
RUN composer install --no-dev --optimize-autoloader
RUN if [ -f requirements.txt ]; then pip3 install -r requirements.txt; fi
RUN a2enmod rewrite
RUN chown -R www-data:www-data /var/www/html \
&& chmod -R 775 /var/www/html/storage /var/www/html/bootstrap/cache
COPY render-apache.conf /etc/apache2/sites-available/000-default.conf
EXPOSE 80
CMD php artisan config:clear && \
php artisan cache:clear && \
apache2-foreground

View File

@ -0,0 +1,319 @@
<?php
namespace App\Exports;
use App\Models\PenerimaFinal;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithStyles;
use Maatwebsite\Excel\Concerns\WithColumnWidths;
use Maatwebsite\Excel\Concerns\WithTitle;
use Maatwebsite\Excel\Concerns\WithCustomStartCell;
use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Events\AfterSheet;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup;
use PhpOffice\PhpSpreadsheet\Style\Fill;
use PhpOffice\PhpSpreadsheet\Style\Alignment;
use PhpOffice\PhpSpreadsheet\Style\Border;
class PenerimaFinalExport implements
FromCollection,
WithHeadings,
WithStyles,
WithColumnWidths,
WithTitle,
WithCustomStartCell,
WithEvents
{
protected int $rowCount = 0;
// ── Collection ────────────────────────────────────────────────────────────
public function collection()
{
$items = PenerimaFinal::query()
->where('status_verifikasi', 'disetujui')
->orderByDesc('tanggal_penetapan')
->get();
$this->rowCount = $items->count();
return $items->map(function ($item, $index) {
$prob = $item->probability ?? 0;
if ($prob <= 1) {
$prob = $prob * 100;
}
$nama = $item->nama_lengkap ?? $item->nama ?? '-';
// Penghasilan: coba dari relasi calonPenerima, fallback ke kolom langsung.
// Jika null atau 0, tampilkan teks supaya cell tidak kosong.
$penghasilanRaw = $item->calonPenerima?->penghasilan
?? $item->penghasilan
?? null;
$penghasilan = ($penghasilanRaw !== null && (int) $penghasilanRaw > 0)
? (int) $penghasilanRaw
: 'Tidak Berpenghasilan';
return [
'No' => $index + 1,
'Nama Lengkap' => $nama,
'NIK' => "\t" . ($item->nik ?? '-'),
'No KK' => "\t" . ($item->no_kk ?? '-'),
'Nomor RT' => $item->nomor_rt ?? '-',
'Dusun' => $item->nama_dusun ?? '-',
'Pekerjaan' => $item->pekerjaan ?? '-',
'Penghasilan' => $penghasilan,
'Tanggungan' => $item->jumlah_tanggungan ?? 0,
'Aset' => $item->aset_kepemilikan ?? '-',
'Bantuan Lain' => ucfirst($item->bantuan_lain ?? 'tidak'),
'Usia' => $item->usia ?? 0,
'Probabilitas' => round($prob, 1),
'Status' => ucfirst($item->status_verifikasi ?? '-'),
'Tgl Penetapan' => optional($item->tanggal_penetapan)->format('d-m-Y') ?? '-',
'Periode Bantuan' => $item->periode_bantuan ?? '-',
'Jumlah Bantuan' => $item->jumlah_bantuan ?? 300000,
];
});
}
// ── Headings ──────────────────────────────────────────────────────────────
public function headings(): array
{
return [
'No', 'Nama Lengkap', 'NIK', 'No KK', 'Nomor RT', 'Dusun',
'Pekerjaan', 'Penghasilan (Rp)', 'Tanggungan',
'Aset Kepemilikan', 'Bantuan Lain', 'Usia',
'Probabilitas (%)', 'Status',
'Tgl Penetapan', 'Periode Bantuan', 'Jumlah Bantuan (Rp)',
];
}
public function startCell(): string { return 'A5'; }
public function title(): string { return 'Penerima BLT-DD'; }
// ── Lebar kolom ──────────────────────────────────────────────────────────
public function columnWidths(): array
{
return [
'A' => 5, 'B' => 28, 'C' => 20, 'D' => 20,
'E' => 8, 'F' => 18, 'G' => 20, 'H' => 18,
'I' => 9, 'J' => 22, 'K' => 13, 'L' => 7,
'M' => 14, 'N' => 16, 'O' => 15, 'P' => 18, 'Q' => 20,
];
}
// ── Style header tabel (baris 5) ─────────────────────────────────────────
public function styles(Worksheet $sheet)
{
$sheet->getStyle('A5:Q5')->applyFromArray([
'font' => [
'bold' => true,
'color' => ['argb' => 'FFFFFFFF'],
'size' => 9,
'name' => 'Arial',
],
'fill' => [
'fillType' => Fill::FILL_SOLID,
'startColor' => ['argb' => 'FF1E3A8A'],
],
'alignment' => [
'horizontal' => Alignment::HORIZONTAL_CENTER,
'vertical' => Alignment::VERTICAL_CENTER,
'wrapText' => true,
],
]);
return [];
}
// ── Events ────────────────────────────────────────────────────────────────
public function registerEvents(): array
{
return [
AfterSheet::class => function (AfterSheet $event) {
$sheet = $event->sheet->getDelegate();
$lastRow = 5 + $this->rowCount;
$this->buildTitleBlock($sheet);
$this->formatDataRows($sheet, $lastRow);
$this->applyTableBorders($sheet, $lastRow);
$this->buildSummaryRow($sheet, $lastRow);
$this->buildFootnote($sheet, $lastRow);
$this->applyPrintSettings($sheet);
$sheet->freezePane('A6');
$sheet->setAutoFilter("A5:Q5");
},
];
}
// ── Helpers ───────────────────────────────────────────────────────────────
private function buildTitleBlock(Worksheet $sheet): void
{
foreach (['A1:Q1', 'A2:Q2', 'A3:Q3', 'A4:Q4'] as $range) {
$sheet->mergeCells($range);
}
$sheet->setCellValue('A1', 'LAPORAN PENERIMA BANTUAN LANGSUNG TUNAI DANA DESA (BLT-DD)');
$sheet->setCellValue('A2', 'Desa Ngerong, Kecamatan Gempol, Kabupaten Pasuruan — Jawa Timur');
$sheet->setCellValue('A3', 'Status: Disetujui | Dicetak: ' . now()->translatedFormat('d F Y, H:i') . ' WIB');
$sheet->setCellValue('A4', '');
$sheet->getStyle('A1')->applyFromArray([
'font' => ['bold' => true, 'size' => 14, 'name' => 'Arial', 'color' => ['argb' => 'FF1E3A8A']],
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER],
]);
$sheet->getStyle('A2')->applyFromArray([
'font' => ['size' => 10, 'name' => 'Arial', 'color' => ['argb' => 'FF334155']],
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER],
]);
$sheet->getStyle('A3')->applyFromArray([
'font' => ['size' => 9, 'italic' => true, 'name' => 'Arial', 'color' => ['argb' => 'FF64748B']],
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER],
]);
$sheet->getStyle('A3:Q3')->applyFromArray([
'borders' => [
'bottom' => ['borderStyle' => Border::BORDER_MEDIUM, 'color' => ['argb' => 'FF1E3A8A']],
],
]);
$sheet->getRowDimension(1)->setRowHeight(26);
$sheet->getRowDimension(2)->setRowHeight(16);
$sheet->getRowDimension(3)->setRowHeight(14);
$sheet->getRowDimension(4)->setRowHeight(6);
$sheet->getRowDimension(5)->setRowHeight(30);
}
private function formatDataRows(Worksheet $sheet, int $lastRow): void
{
for ($row = 6; $row <= $lastRow; $row++) {
$isEven = ($row % 2 === 0);
$sheet->getStyle("A{$row}:Q{$row}")->applyFromArray([
'fill' => [
'fillType' => Fill::FILL_SOLID,
'startColor' => ['argb' => $isEven ? 'FFF8FAFC' : 'FFFFFFFF'],
],
'font' => ['name' => 'Arial', 'size' => 9],
'alignment' => ['vertical' => Alignment::VERTICAL_CENTER],
]);
$sheet->getRowDimension($row)->setRowHeight(18);
foreach (['A', 'E', 'I', 'L', 'M', 'N', 'O'] as $col) {
$sheet->getStyle("{$col}{$row}")
->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
}
foreach (['H', 'Q'] as $col) {
$sheet->getStyle("{$col}{$row}")
->getAlignment()->setHorizontal(Alignment::HORIZONTAL_RIGHT);
}
// Format Penghasilan — angka pakai Rp, teks (Tidak Berpenghasilan) pakai italic gray
$hVal = $sheet->getCell("H{$row}")->getValue();
if (is_numeric($hVal)) {
$sheet->getStyle("H{$row}")->applyFromArray([
'numberFormat' => ['formatCode' => '"Rp "#,##0'],
'alignment' => ['horizontal' => Alignment::HORIZONTAL_RIGHT],
]);
} else {
$sheet->getStyle("H{$row}")->applyFromArray([
'font' => ['italic' => true, 'color' => ['argb' => 'FF94A3B8'], 'size' => 8, 'name' => 'Arial'],
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER],
]);
}
$sheet->getStyle("Q{$row}")->applyFromArray([
'font' => ['bold' => true, 'color' => ['argb' => 'FF166534'], 'name' => 'Arial', 'size' => 9],
'numberFormat' => ['formatCode' => '"Rp "#,##0'],
]);
$prob = (float) ($sheet->getCell("M{$row}")->getValue() ?? 0);
$probColor = $prob >= 70 ? 'FF10B981' : ($prob >= 40 ? 'FFF59E0B' : 'FFF43F5E');
$sheet->getStyle("M{$row}")->applyFromArray([
'font' => ['bold' => true, 'color' => ['argb' => $probColor], 'name' => 'Arial', 'size' => 9],
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER],
'numberFormat' => ['formatCode' => '0.0"%"'],
]);
$sheet->getStyle("N{$row}")->applyFromArray([
'font' => ['bold' => true, 'color' => ['argb' => 'FF166534'], 'name' => 'Arial', 'size' => 9],
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER],
]);
}
}
private function applyTableBorders(Worksheet $sheet, int $lastRow): void
{
$sheet->getStyle("A5:Q{$lastRow}")->applyFromArray([
'borders' => [
'allBorders' => [
'borderStyle' => Border::BORDER_THIN,
'color' => ['argb' => 'FFE2E8F0'],
],
'outline' => [
'borderStyle' => Border::BORDER_MEDIUM,
'color' => ['argb' => 'FF1E3A8A'],
],
],
]);
}
private function buildSummaryRow(Worksheet $sheet, int $lastRow): void
{
$sumRow = $lastRow + 2;
$sheet->mergeCells("A{$sumRow}:B{$sumRow}");
$sheet->setCellValue("A{$sumRow}", 'RINGKASAN');
$sheet->setCellValue("C{$sumRow}", 'Total Penerima:');
$sheet->setCellValue("D{$sumRow}", $this->rowCount . ' orang');
// ← Total Penghasilan dihapus, diganti Total Dana BLT saja
$sheet->setCellValue("P{$sumRow}", 'Total Dana BLT:');
$sheet->setCellValue("Q{$sumRow}", "=SUM(Q6:Q{$lastRow})");
$sheet->getStyle("A{$sumRow}:Q{$sumRow}")->applyFromArray([
'font' => ['bold' => true, 'size' => 9, 'name' => 'Arial', 'color' => ['argb' => 'FF1E3A8A']],
'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['argb' => 'FFEFF6FF']],
'borders' => [
'outline' => ['borderStyle' => Border::BORDER_MEDIUM, 'color' => ['argb' => 'FF1E3A8A']],
],
]);
$sheet->getStyle("Q{$sumRow}")->getNumberFormat()->setFormatCode('"Rp "#,##0');
$sheet->getRowDimension($sumRow)->setRowHeight(18);
}
private function buildFootnote(Worksheet $sheet, int $lastRow): void
{
$noteRow = $lastRow + 4;
$sheet->mergeCells("A{$noteRow}:Q{$noteRow}");
$sheet->setCellValue(
"A{$noteRow}",
'* Dokumen ini digenerate otomatis oleh SiBantuDes — Sistem Informasi BLT Dana Desa, Desa Ngerong, Kab. Pasuruan.'
);
$sheet->getStyle("A{$noteRow}")->applyFromArray([
'font' => ['italic' => true, 'size' => 8, 'name' => 'Arial', 'color' => ['argb' => 'FF94A3B8']],
'alignment' => ['horizontal' => Alignment::HORIZONTAL_LEFT],
]);
}
private function applyPrintSettings(Worksheet $sheet): void
{
$sheet->getPageSetup()
->setOrientation(PageSetup::ORIENTATION_LANDSCAPE)
->setPaperSize(PageSetup::PAPERSIZE_A4)
->setFitToWidth(1)
->setFitToHeight(0);
$sheet->getPageMargins()
->setTop(0.75)->setBottom(0.75)
->setLeft(0.5)->setRight(0.5);
$sheet->getHeaderFooter()
->setOddHeader('&C&B Laporan Penerima BLT-DD — Desa Ngerong')
->setOddFooter('&L&8SiBantuDes&C&8Halaman &P dari &N&R&8' . now()->format('d/m/Y'));
}
}

View File

@ -14,6 +14,7 @@ class FilterisasiController extends Controller
public function index(Request $request)
{
$user = $request->user();
if (!$user || $user->role !== 'admin') {
abort(403, 'Akses ditolak: hanya Admin.');
}
@ -28,9 +29,11 @@ public function index(Request $request)
}
$candidates = collect();
$pickedIds = collect();
$pickedNiks = collect();
if ($dusunId > 0) {
$dusun = Dusun::find($dusunId);
$query = CalonPenerima::query()
->with(['rt.dusun', 'prediksiKelayakan'])
->whereIn('tracking_status', ['sedang_validasi', 'selesai'])
@ -45,16 +48,19 @@ public function index(Request $request)
$candidates = $query->paginate(15)->withQueryString();
$pickedIds = PenerimaFinal::whereIn('calon_penerima_id', $candidates->pluck('id')->all())
->pluck('calon_penerima_id');
$pickedNiks = PenerimaFinal::query()
->where('status_verifikasi', 'disetujui')
->where('nama_dusun', $dusun?->nama_dusun)
->pluck('nik');
}
return view('admin.filterisasi', compact('dusuns', 'dusunId', 'kuota', 'candidates', 'pickedIds'));
return view('admin.filterisasi', compact('dusuns', 'dusunId', 'kuota', 'candidates', 'pickedNiks'));
}
public function tetapkan(Request $request)
{
$user = $request->user();
if (!$user || $user->role !== 'admin') {
abort(403, 'Akses ditolak: hanya Admin.');
}
@ -68,14 +74,16 @@ public function tetapkan(Request $request)
$kuota = (int) ($data['kuota'] ?? 7);
DB::transaction(function () use ($dusunId, $kuota) {
// Ambil semua kandidat dalam dusun yang sedang divalidasi / sudah selesai
$dusun = Dusun::findOrFail($dusunId);
$allCandidates = CalonPenerima::query()
->with(['rt.dusun', 'prediksiKelayakan'])
->whereIn('tracking_status', ['sedang_validasi', 'selesai'])
->whereHas('rt', function ($r) use ($dusunId) {
$r->where('dusun_id', $dusunId);
})
->leftJoin('prediksi_kelayakans', 'prediksi_kelayakans.calon_penerima_id', '=', 'calon_penerimas.id')
->select('calon_penerimas.id', DB::raw('COALESCE(prediksi_kelayakans.probability, 0) as prob'))
->select('calon_penerimas.*', DB::raw('COALESCE(prediksi_kelayakans.probability, 0) as prob'))
->orderByDesc('prob')
->orderByDesc('calon_penerimas.created_at')
->get();
@ -83,39 +91,56 @@ public function tetapkan(Request $request)
$topIds = $allCandidates->take($kuota)->pluck('id')->all();
$allIds = $allCandidates->pluck('id')->all();
// Hapus hasil final lama untuk kandidat pada dusun ini
if (!empty($allIds)) {
PenerimaFinal::whereIn('calon_penerima_id', $allIds)->delete();
}
// Simpan hasil final penerima yang masuk kuota
foreach ($topIds as $id) {
PenerimaFinal::updateOrCreate(
['calon_penerima_id' => $id],
[
'tanggal_penetapan' => now()->toDateString(),
'periode_bantuan' => date('Y') . ' Triwulan 1',
'jumlah_bantuan' => 0,
'status_pencairan' => 'belum_cair',
'tanggal_pencairan' => null,
]
);
}
// Yang masuk kuota = disetujui
// Top kuota = disetujui
if (!empty($topIds)) {
CalonPenerima::whereIn('id', $topIds)->update([
'status_verifikasi' => 'disetujui',
]);
}
// Yang tidak masuk kuota = ditolak
// Sisanya = ditolak
$notPickedIds = array_diff($allIds, $topIds);
if (!empty($notPickedIds)) {
CalonPenerima::whereIn('id', $notPickedIds)->update([
'status_verifikasi' => 'ditolak',
]);
}
// Ambil ulang data final setelah status diperbarui
$finalCandidates = CalonPenerima::query()
->with(['rt.dusun', 'prediksiKelayakan'])
->whereIn('id', $allIds)
->get();
foreach ($finalCandidates as $candidate) {
$probability = $candidate->prediksiKelayakan->probability ?? 0;
PenerimaFinal::updateOrCreate(
[
'nik' => $candidate->nik,
'periode_bantuan' => '2026 Triwulan 1',
],
[
'nama_lengkap' => $candidate->nama_lengkap ?? $candidate->nama ?? '-',
'no_kk' => $candidate->no_kk ?? '-',
'rt_id' => $candidate->rt_id,
'nomor_rt' => $candidate->rt?->nomor_rt ?? null,
'nama_dusun' => $candidate->rt?->dusun?->nama_dusun ?? $dusun->nama_dusun,
'pekerjaan' => $candidate->pekerjaan ?? '-',
'penghasilan' => $candidate->penghasilan ?? 0,
'jumlah_tanggungan' => $candidate->jumlah_tanggungan ?? 0,
'aset_kepemilikan' => $candidate->aset_kepemilikan ?? '-',
'bantuan_lain' => $candidate->bantuan_lain ?? 'tidak',
'usia' => $candidate->usia ?? 0,
'probability' => $probability,
'status_verifikasi' => $candidate->status_verifikasi ?? 'pending',
'tanggal_penetapan' => now()->toDateString(),
'periode_bantuan' => '2026 Triwulan 1',
'jumlah_bantuan' => 300000,
]
);
}
});
return back()->with('success', "Berhasil menetapkan kuota {$kuota} penerima untuk dusun terpilih.");
@ -124,6 +149,7 @@ public function tetapkan(Request $request)
public function resetDusun(Request $request)
{
$user = $request->user();
if (!$user || $user->role !== 'admin') {
abort(403, 'Akses ditolak: hanya Admin.');
}
@ -132,24 +158,24 @@ public function resetDusun(Request $request)
'dusun_id' => 'required|exists:dusuns,id',
]);
$dusunId = (int) $data['dusun_id'];
$dusun = Dusun::findOrFail($data['dusun_id']);
DB::transaction(function () use ($dusunId) {
DB::transaction(function () use ($dusun) {
$candidateIds = CalonPenerima::query()
->whereHas('rt', function ($r) use ($dusunId) {
$r->where('dusun_id', $dusunId);
->whereHas('rt', function ($r) use ($dusun) {
$r->where('dusun_id', $dusun->id);
})
->whereIn('tracking_status', ['sedang_validasi', 'selesai'])
->pluck('id')
->all();
if (!empty($candidateIds)) {
PenerimaFinal::whereIn('calon_penerima_id', $candidateIds)->delete();
CalonPenerima::whereIn('id', $candidateIds)->update([
'status_verifikasi' => 'pending',
]);
}
PenerimaFinal::where('nama_dusun', $dusun->nama_dusun)->delete();
});
return back()->with('success', 'Hasil filterisasi dusun berhasil di-reset.');

View File

@ -0,0 +1,146 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\CalonPenerima;
use App\Models\PenerimaFinal;
use App\Models\LaporanPublik;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Barryvdh\DomPDF\Facade\Pdf;
use App\Exports\PenerimaFinalExport;
use Maatwebsite\Excel\Facades\Excel;
class LaporanController extends Controller
{
public function index(Request $request)
{
$user = $request->user();
if (!$user || $user->role !== 'admin') {
abort(403, 'Akses ditolak: hanya Admin.');
}
$laporans = PenerimaFinal::query()
->where('status_verifikasi', 'disetujui')
->orderByDesc('tanggal_penetapan')
->get();
$laporanPubliks = LaporanPublik::latest()->get();
return view('admin.laporan', compact('laporans', 'laporanPubliks'));
}
public function exportPdf(Request $request)
{
$user = $request->user();
if (!$user || $user->role !== 'admin') {
abort(403, 'Akses ditolak: hanya Admin.');
}
$laporans = PenerimaFinal::query()
->where('status_verifikasi', 'disetujui')
->orderByDesc('tanggal_penetapan')
->get();
$pdf = Pdf::loadView('admin.laporan-pdf', compact('laporans'))
->setPaper('a4', 'landscape');
return $pdf->download('laporan-penerima-blt-dd-kelurahan-ngerong.pdf');
}
public function exportExcel(Request $request)
{
$user = $request->user();
if (!$user || $user->role !== 'admin') {
abort(403, 'Akses ditolak: hanya Admin.');
}
return Excel::download(new PenerimaFinalExport, 'laporan-penerima-blt-dd-kelurahan-ngerong.xlsx');
}
public function kirimKeRt(Request $request)
{
$user = $request->user();
if (!$user || $user->role !== 'admin') {
abort(403, 'Akses ditolak: hanya Admin.');
}
$candidateIds = CalonPenerima::query()
->whereIn('tracking_status', ['sedang_validasi'])
->whereIn('status_verifikasi', ['disetujui', 'ditolak'])
->pluck('id')
->all();
if (!empty($candidateIds)) {
CalonPenerima::whereIn('id', $candidateIds)->update([
'tracking_status' => 'selesai',
]);
}
return back()->with('success', 'Hasil final berhasil dikirim ke RT dusun. RT sekarang hanya dapat melihat status diterima atau ditolak.');
}
public function uploadPublikPdf(Request $request)
{
$user = $request->user();
if (!$user || $user->role !== 'admin') {
abort(403, 'Akses ditolak: hanya Admin.');
}
$data = $request->validate([
'judul' => 'required|string|max:255',
'file_pdf' => 'required|mimes:pdf|max:5120',
]);
$path = $request->file('file_pdf')->store('laporan-publik', 'public');
LaporanPublik::create([
'judul' => $data['judul'],
'file_path' => $path,
'uploaded_by' => $user->id,
'is_active' => true,
]);
return back()->with('success', 'PDF laporan publik berhasil diupload.');
}
public function hapusPublikPdf(Request $request, $id)
{
$user = $request->user();
if (!$user || $user->role !== 'admin') {
abort(403, 'Akses ditolak: hanya Admin.');
}
$laporan = LaporanPublik::findOrFail($id);
if ($laporan->file_path && Storage::disk('public')->exists($laporan->file_path)) {
Storage::disk('public')->delete($laporan->file_path);
}
$laporan->delete();
return back()->with('success', 'PDF laporan publik berhasil dihapus.');
}
public function bersihkanRiwayatSelesai(Request $request)
{
$user = $request->user();
if (!$user || $user->role !== 'admin') {
abort(403, 'Akses ditolak: hanya Admin.');
}
$deleted = \App\Models\CalonPenerima::query()
->where('tracking_status', 'selesai')
->delete();
return back()->with('success', "Berhasil membersihkan {$deleted} data riwayat selesai. Arsip laporan tetap tersimpan.");
}
}

View File

@ -28,6 +28,27 @@ public function store(LoginRequest $request): RedirectResponse
$request->session()->regenerate();
$user = Auth::user();
if (!$user->is_active) {
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return back()->withErrors([
'email' => 'Akun Anda belum diaktifkan oleh admin kelurahan.',
])->onlyInput('email');
}
if ($user->role === 'admin') {
return redirect()->route('admin.dashboard');
}
if ($user->role === 'rt') {
return redirect()->route('rt.dashboard');
}
return redirect()->intended(route('dashboard', absolute: false));
}
@ -39,7 +60,6 @@ public function destroy(Request $request): RedirectResponse
Auth::guard('web')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');

View File

@ -8,7 +8,6 @@
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules;
use Illuminate\View\View;
@ -20,7 +19,6 @@ class RegisteredUserController extends Controller
*/
public function create(): View
{
// Ambil semua RT + dusun untuk dropdown di halaman register
$rts = RT::with('dusun')
->orderBy('dusun_id')
->orderBy('nomor_rt')
@ -40,8 +38,6 @@ public function store(Request $request): RedirectResponse
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:' . User::class],
'password' => ['required', 'confirmed', Rules\Password::defaults()],
// WAJIB pilih RT biar user punya rt_id
'rt_id' => ['required', 'exists:rts,id'],
]);
@ -49,19 +45,13 @@ public function store(Request $request): RedirectResponse
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
// Set default akun hasil register sebagai RT
'role' => 'rt',
'rt_id' => $request->rt_id,
'is_active' => false,
]);
event(new Registered($user));
Auth::login($user);
// Kamu bisa arahkan ke dashboard RT kalau punya route khusus RT
// return redirect()->route('rt.dashboard');
return redirect(route('dashboard', absolute: false));
return redirect()->route('login')->with('success', 'Pendaftaran berhasil. Akun Anda sedang menunggu aktivasi dari admin kelurahan.');
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace App\Http\Controllers;
use App\Models\CalonPenerima;
use App\Models\LaporanPublik;
use Illuminate\Http\Request;
class LandingPageController extends Controller
{
public function index(Request $request)
{
$nik = trim((string) $request->query('nik',''));
$statusData = null;
if($nik !== ''){
$statusData = CalonPenerima::with(['rt.dusun'])
->where('nik',$nik)
->first();
}
$laporanPubliks = LaporanPublik::where('is_active',true)
->latest()
->get();
return view('landing',compact(
'nik',
'statusData',
'laporanPubliks'
));
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace App\Http\Controllers\Rt;
use App\Http\Controllers\Controller;
use App\Models\PenerimaFinal;
use App\Models\Rt;
use Illuminate\Http\Request;
class LaporanController extends Controller
{
public function index(Request $request)
{
$user = $request->user();
if (!$user || $user->role !== 'rt') {
abort(403, 'Akses ditolak: hanya RT.');
}
$rt = Rt::with('dusun')->find($user->rt_id);
if (!$rt) {
$laporans = collect();
$stats = [
'total' => 0,
'diterima' => 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)
->whereIn('status_verifikasi', ['disetujui', 'ditolak'])
->orderByDesc('tanggal_penetapan')
->get();
$stats = [
'total' => $laporans->count(),
'diterima' => $laporans->where('status_verifikasi', 'disetujui')->count(),
'ditolak' => $laporans->where('status_verifikasi', 'ditolak')->count(),
];
return view('rt.laporan', compact('laporans', 'stats'));
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class LaporanPublik extends Model
{
use HasFactory;
protected $fillable = [
'judul',
'file_path',
'uploaded_by',
'is_active',
];
public function uploader()
{
return $this->belongsTo(User::class, 'uploaded_by');
}
}

View File

@ -10,18 +10,31 @@ class PenerimaFinal extends Model
use HasFactory;
protected $fillable = [
'calon_penerima_id', 'tanggal_penetapan', 'periode_bantuan',
'jumlah_bantuan', 'status_pencairan', 'tanggal_pencairan',
'nama_lengkap',
'nik',
'no_kk',
'rt_id',
'nomor_rt',
'nama_dusun',
'pekerjaan',
'penghasilan',
'jumlah_tanggungan',
'aset_kepemilikan',
'bantuan_lain',
'usia',
'probability',
'status_verifikasi',
'tanggal_penetapan',
'periode_bantuan',
'jumlah_bantuan',
];
protected $casts = [
'tanggal_penetapan' => 'date',
'tanggal_pencairan' => 'date',
'jumlah_bantuan' => 'decimal:2',
'probability' => 'decimal:4',
'penghasilan' => 'integer',
'jumlah_tanggungan' => 'integer',
'usia' => 'integer',
];
public function calonPenerima()
{
return $this->belongsTo(CalonPenerima::class);
}
}

View File

@ -2,24 +2,34 @@
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class User extends Authenticatable
{
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory, Notifiable;
protected $fillable = ['name', 'email', 'password', 'role', 'rt_id'];
protected $hidden = ['password', 'remember_token'];
protected $fillable = [
'name',
'email',
'password',
'role',
'rt_id',
'is_active',
];
protected $hidden = [
'password',
'remember_token',
];
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
'is_active' => 'boolean',
];
}

View File

@ -7,8 +7,10 @@
"license": "MIT",
"require": {
"php": "^8.2",
"barryvdh/laravel-dompdf": "^3.1",
"laravel/framework": "^12.0",
"laravel/tinker": "^2.10.1"
"laravel/tinker": "^2.10.1",
"maatwebsite/excel": "*"
},
"require-dev": {
"barryvdh/laravel-ide-helper": "^3.6",

1194
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -60,12 +60,11 @@
|--------------------------------------------------------------------------
|
| Here you may specify the default timezone for your application, which
| will be used by the PHP date and date-time functions. The timezone
| is set to "UTC" by default as it is suitable for most use cases.
| will be used by the PHP date and date-time functions.
|
*/
'timezone' => 'UTC',
'timezone' => 'Asia/Jakarta',
/*
|--------------------------------------------------------------------------

View File

@ -14,22 +14,32 @@ public function up(): void
Schema::create('penerima_finals', function (Blueprint $table) {
$table->id();
$table->foreignId('calon_penerima_id')
->constrained('calon_penerimas')
->onDelete('cascade');
// Snapshot data warga
$table->string('nama_lengkap');
$table->string('nik', 20);
$table->string('no_kk', 20);
// Snapshot wilayah
$table->unsignedBigInteger('rt_id')->nullable();
$table->string('nomor_rt')->nullable();
$table->string('nama_dusun')->nullable();
// Snapshot data penilaian
$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')->default(0);
// Snapshot hasil prediksi/final
$table->decimal('probability', 8, 4)->default(0);
$table->enum('status_verifikasi', ['pending', 'disetujui', 'ditolak'])->default('pending');
// Data arsip bantuan
$table->date('tanggal_penetapan');
$table->string('periode_bantuan'); // contoh: 2026 Triwulan 1
$table->decimal('jumlah_bantuan', 15, 2);
$table->enum('status_pencairan', [
'belum_cair',
'sudah_cair'
])->default('belum_cair');
$table->date('tanggal_pencairan')->nullable();
$table->string('periode_bantuan');
$table->decimal('jumlah_bantuan', 15, 2)->default(300000);
$table->timestamps();
});

View File

@ -1,27 +1,22 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class EnsureUserIsActive
return new class extends Migration
{
public function handle(Request $request, Closure $next)
public function up(): void
{
$user = $request->user();
// kalau user login tapi nonaktif -> logout + balik ke login
if ($user && (int)($user->is_active ?? 1) === 0) {
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect()->route('login')
->with('error', 'Akun kamu sudah dinonaktifkan oleh Admin.');
Schema::table('users', function (Blueprint $table) {
$table->boolean('is_active')->default(false)->after('role');
});
}
return $next($request);
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('is_active');
});
}
};

View File

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('laporan_publiks', function (Blueprint $table) {
$table->id();
$table->string('judul');
$table->string('file_path');
$table->foreignId('uploaded_by')->nullable()->constrained('users')->nullOnDelete();
$table->boolean('is_active')->default(true);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('laporan_publiks');
}
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 0 B

After

Width:  |  Height:  |  Size: 549 KiB

12
render-apache.conf Normal file
View File

@ -0,0 +1,12 @@
<VirtualHost *:80>
ServerAdmin webmaster@localhost
DocumentRoot /var/www/html/public
<Directory /var/www/html/public>
AllowOverride All
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>

View File

@ -1,20 +1,82 @@
<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">Dashboard Kelurahan</h2>
<p class="text-sm text-gray-500 mt-0.5">Ringkasan data kelayakan penerima BLT-DD seluruh dusun</p>
<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>
</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">
<div style="display:flex;align-items:center;gap:8px;">
{{-- ── BELL NOTIFIKASI ── --}}
<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">
<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>
</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>
</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>
</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">
<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>
</div>
</div>
</div>
</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;}
/* 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 */
.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 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;}
/* 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 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;}
</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' });
@ -24,131 +86,180 @@ function updateAdminClock() {
}
updateAdminClock();
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();
},
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 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(); }
/* 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) {}
}
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'; }
});
});
</script>
<div class="space-y-4">
<div style="display:flex;flex-direction:column;gap:16px;">
{{-- GREETING + NOTIFIKASI --}}
<div class="flex flex-col sm:flex-row gap-3">
<div class="flex items-center gap-3 bg-white border border-gray-100 rounded-xl px-4 py-3 shadow-sm flex-1">
<div class="w-8 h-8 rounded-full bg-blue-600 flex items-center justify-center text-white text-sm font-bold shrink-0">
{{-- 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);">
{{ strtoupper(substr(Auth::user()->name ?? 'A', 0, 1)) }}
</div>
<div>
<p class="text-sm font-medium text-gray-800">Selamat datang, <span class="text-blue-600">{{ Auth::user()->name ?? 'Admin' }}</span> 👋</p>
<p class="text-xs text-gray-400">Panel admin kelola dan pantau seluruh data warga.</p>
<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>
</div>
</div>
<div class="flex items-center gap-3 bg-amber-50 border border-amber-100 rounded-xl px-4 py-3 shadow-sm">
<div class="w-8 h-8 rounded-full bg-amber-500 flex items-center justify-center shrink-0">
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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>
<div>
<p class="text-xs font-semibold text-amber-800">{{ $totalPending ?? 0 }} data menunggu verifikasi</p>
<a href="{{ route('admin.data-warga') }}" class="text-xs text-amber-600 hover:underline">Proses sekarang </a>
<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>
</div>
</div>
</div>
{{-- STAT CARDS --}}
<div class="grid grid-cols-2 lg:grid-cols-4 gap-3">
{{-- STAT CARDS (asli tidak diubah, ganti ke inline style) --}}
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:12px;">
<div class="relative bg-blue-600 text-white rounded-xl p-4 flex items-center justify-between overflow-hidden shadow-md shadow-blue-200">
<div class="absolute -right-3 -top-3 w-20 h-20 bg-white/10 rounded-full"></div>
<div class="absolute -right-1 -bottom-4 w-14 h-14 bg-white/10 rounded-full"></div>
<div class="relative z-10">
<p class="text-xs font-medium text-blue-100">Total Warga Masuk</p>
<p class="text-3xl font-bold mt-0.5">{{ $totalWarga ?? 0 }}</p>
<p class="text-xs text-blue-200 mt-0.5">Dari semua dusun</p>
<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 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>
</div>
<div class="relative z-10 w-11 h-11 bg-white/20 rounded-lg flex items-center justify-center shrink-0">
<svg class="w-6 h-6" 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 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 class="relative bg-amber-500 text-white rounded-xl p-4 flex items-center justify-between overflow-hidden shadow-md shadow-amber-200">
<div class="absolute -right-3 -top-3 w-20 h-20 bg-white/10 rounded-full"></div>
<div class="absolute -right-1 -bottom-4 w-14 h-14 bg-white/10 rounded-full"></div>
<div class="relative z-10">
<p class="text-xs font-medium text-amber-100">Pending Verifikasi</p>
<p class="text-3xl font-bold mt-0.5">{{ $totalPending ?? 0 }}</p>
<p class="text-xs text-amber-200 mt-0.5">Belum diproses</p>
<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 class="relative z-10 w-11 h-11 bg-white/20 rounded-lg flex items-center justify-center shrink-0">
<svg class="w-6 h-6" fill="none" stroke="currentColor" 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 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 class="relative bg-emerald-500 text-white rounded-xl p-4 flex items-center justify-between overflow-hidden shadow-md shadow-emerald-200">
<div class="absolute -right-3 -top-3 w-20 h-20 bg-white/10 rounded-full"></div>
<div class="absolute -right-1 -bottom-4 w-14 h-14 bg-white/10 rounded-full"></div>
<div class="relative z-10">
<p class="text-xs font-medium text-emerald-100">Diterima / Ditetapkan</p>
<p class="text-3xl font-bold mt-0.5">{{ $totalPenerima ?? 0 }}</p>
<p class="text-xs text-emerald-200 mt-0.5">Sudah ditetapkan</p>
<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 class="relative z-10 w-11 h-11 bg-white/20 rounded-lg flex items-center justify-center shrink-0">
<svg class="w-6 h-6" 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>
<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 class="relative bg-rose-500 text-white rounded-xl p-4 flex items-center justify-between overflow-hidden shadow-md shadow-rose-200">
<div class="absolute -right-3 -top-3 w-20 h-20 bg-white/10 rounded-full"></div>
<div class="absolute -right-1 -bottom-4 w-14 h-14 bg-white/10 rounded-full"></div>
<div class="relative z-10">
<p class="text-xs font-medium text-rose-100">Ditolak</p>
<p class="text-3xl font-bold mt-0.5">{{ $totalDitolak ?? 0 }}</p>
<p class="text-xs text-rose-200 mt-0.5">Tidak memenuhi syarat</p>
<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 class="relative z-10 w-11 h-11 bg-white/20 rounded-lg flex items-center justify-center shrink-0">
<svg class="w-6 h-6" fill="none" stroke="currentColor" 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 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>
{{-- GRAFIK + DATA PER DUSUN --}}
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
{{-- GRAFIK + DATA PER DUSUN (logika PHP asli 100% tidak diubah) --}}
<div style="display:grid;grid-template-columns:2fr 1fr;gap:14px;">
{{-- GRAFIK POLIGON LINE PER DUSUN (SVG murni, seperti referensi) --}}
<div class="lg:col-span-2 bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden">
<div class="px-5 py-3 border-b border-gray-100 flex items-center justify-between bg-gray-50">
<div class="flex items-center gap-2">
<div class="w-1 h-4 bg-blue-600 rounded-full"></div>
<h3 class="text-sm font-semibold text-gray-700">Jumlah Warga per Dusun</h3>
<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>
</div>
<span class="text-xs text-gray-400 bg-white border border-gray-100 px-2.5 py-0.5 rounded-full">
<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 class="p-5">
<div style="padding:18px 20px;">
@php
$dusuns = $wargaPerDusun->values();
$n = max($dusuns->count(), 1);
$maxV = max((int)$dusuns->max('total'), 1);
// Canvas
$W = 560; $H = 210;
$pL = 40; $pR = 20;
$pT = 30; $pB = 45;
$cW = $W - $pL - $pR;
$cH = $H - $pT - $pB;
// Y ticks (0 .. maxV, max 5 steps)
$step = max(1, (int)ceil($maxV / 4));
$yTicks = range(0, $maxV + $step, $step);
// Compute point coords
$pts = [];
foreach ($dusuns as $i => $d) {
$x = $pL + ($n === 1 ? $cW / 2 : ($i / ($n - 1)) * $cW);
@ -173,87 +284,53 @@ function updateAdminClock() {
</linearGradient>
</defs>
{{-- Y grid + label --}}
@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>
<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
@endforeach
{{-- Label sumbu Y --}}
<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>
{{-- Sumbu X --}}
<line x1="{{ $pL }}" y1="{{ $botY }}" x2="{{ $W - $pR }}" y2="{{ $botY }}"
stroke="#cbd5e1" stroke-width="1.5"/>
{{-- Panah X --}}
<polygon points="{{ $W - $pR }},{{ $botY - 4 }} {{ $W - $pR + 8 }},{{ $botY }} {{ $W - $pR }},{{ $botY + 4 }}"
fill="#94a3b8"/>
{{-- Sumbu Y --}}
<line x1="{{ $pL }}" y1="{{ $pT }}" x2="{{ $pL }}" y2="{{ $botY }}"
stroke="#cbd5e1" stroke-width="1.5"/>
{{-- Panah Y --}}
<polygon points="{{ $pL - 4 }},{{ $pT }} {{ $pL }},{{ $pT - 8 }} {{ $pL + 4 }},{{ $pT }}"
fill="#94a3b8"/>
{{-- Area fill --}}
<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)"/>
{{-- Garis poligon biru --}}
<polyline points="{{ $poly }}"
fill="none" stroke="#2563eb" stroke-width="2.5"
stroke-linejoin="round" stroke-linecap="round"/>
{{-- Titik & label per dusun --}}
<polyline points="{{ $poly }}" fill="none" stroke="#2563eb" stroke-width="2.5" stroke-linejoin="round" stroke-linecap="round"/>
@foreach($pts as $p)
{{-- Dot --}}
<circle cx="{{ $p['x'] }}" cy="{{ $p['y'] }}" r="5"
fill="white" stroke="#2563eb" stroke-width="2.5"/>
{{-- Nilai di atas titik --}}
<text x="{{ $p['x'] }}" y="{{ $p['y'] - 10 }}" text-anchor="middle"
font-size="11" font-weight="700" fill="#2563eb">{{ $p['val'] }}</text>
{{-- Nama dusun di bawah sumbu X --}}
<text x="{{ $p['x'] }}" y="{{ $botY + 16 }}" text-anchor="middle"
font-size="10" fill="#64748b">{{ Str::limit($p['name'], 9) }}</text>
<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>
@endforeach
{{-- Label "Nama Dusun" sumbu X --}}
<text x="{{ $W - $pR + 10 }}" y="{{ $botY + 4 }}" text-anchor="start"
font-size="9" fill="#94a3b8">Dusun</text>
<text x="{{ $W - $pR + 10 }}" y="{{ $botY + 4 }}" text-anchor="start" font-size="9" fill="#94a3b8">Dusun</text>
</svg>
</div>
</div>
{{-- DATA PER DUSUN (tidak diubah) --}}
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden">
<div class="px-5 py-3 border-b border-gray-100 flex items-center gap-2 bg-gray-50">
<div class="w-1 h-4 bg-emerald-500 rounded-full"></div>
<h3 class="text-sm font-semibold text-gray-700">Data Per Dusun</h3>
<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>
<div class="p-4 space-y-2.5">
<div style="padding:14px;display:flex;flex-direction:column;gap:8px;">
@php $maxDusun = $wargaPerDusun->max('total') ?: 1; @endphp
@foreach($wargaPerDusun as $data)
<div class="p-3 bg-gray-50 hover:bg-blue-50 border border-gray-100 hover:border-blue-100 rounded-xl transition-all duration-150">
<div class="flex items-center justify-between mb-1.5">
<div class="flex items-center gap-2">
<div class="w-7 h-7 bg-blue-100 rounded-lg flex items-center justify-center shrink-0">
<svg class="w-3.5 h-3.5 text-blue-600" fill="currentColor" viewBox="0 0 20 20">
<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 class="text-sm font-semibold text-gray-800">{{ $data->dusun }}</span>
<span style="font-size:12.5px;font-weight:700;color:#1e293b;">{{ $data->dusun }}</span>
</div>
<span class="text-base font-bold text-blue-600">{{ $data->total }}</span>
<span style="font-size:14px;font-weight:900;color:#2563eb;">{{ $data->total }}</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-1.5">
<div class="h-1.5 bg-blue-500 rounded-full" style="width: {{ ($data->total / $maxDusun) * 100 }}%"></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>
@endforeach
@ -262,88 +339,141 @@ function updateAdminClock() {
</div>
{{-- TOP 10 (tidak diubah) --}}
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden">
<div class="px-5 py-3 border-b border-gray-100 flex items-center justify-between bg-gray-50">
<div class="flex items-center gap-2">
<div class="w-1 h-4 bg-blue-600 rounded-full"></div>
<h3 class="text-sm font-semibold text-gray-700">Top 10 Probabilitas Tertinggi</h3>
{{-- TOP 10 full width, tambah kolom Dusun & RT, logika asli 100% tidak diubah --}}
<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>
<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>
</div>
<a href="{{ route('admin.filterisasi') }}" class="text-xs text-blue-600 hover:underline font-medium">Lihat Semua </a>
</div>
<div class="overflow-x-auto">
<table class="min-w-full text-sm">
<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>
</div>
<div style="overflow-x:auto;">
<table>
<thead>
<tr class="bg-gray-50 border-b border-gray-100">
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wide w-12">Rank</th>
<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">NIK</th>
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wide">Dusun</th>
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wide">Penghasilan</th>
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wide">Probabilitas</th>
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wide">Status</th>
<tr>
<th style="width:48px;">Rank</th>
<th>Nama</th>
<th>NIK</th>
<th>Dusun</th>
<th>RT</th>
<th>Penghasilan</th>
<th style="min-width:160px;">Probabilitas</th>
<th>Status</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-50">
<tbody>
@forelse($topWarga as $index => $warga)
<tr class="hover:bg-blue-50/30 transition-colors duration-150">
<td class="px-4 py-3">
<tr>
{{-- Rank (asli) --}}
<td>
@if($index == 0)
<div class="w-7 h-7 rounded-full bg-yellow-100 text-yellow-600 flex items-center justify-center text-xs font-bold">1</div>
<div class="rk rk-1">1</div>
@elseif($index == 1)
<div class="w-7 h-7 rounded-full bg-gray-100 text-gray-500 flex items-center justify-center text-xs font-bold">2</div>
<div class="rk rk-2">2</div>
@elseif($index == 2)
<div class="w-7 h-7 rounded-full bg-orange-100 text-orange-500 flex items-center justify-center text-xs font-bold">3</div>
<div class="rk rk-3">3</div>
@else
<div class="w-7 h-7 rounded-full bg-blue-50 text-blue-400 flex items-center justify-center text-xs">{{ $index + 1 }}</div>
<div class="rk rk-n">{{ $index + 1 }}</div>
@endif
</td>
<td class="px-4 py-3">
<div class="flex items-center gap-2">
<div class="w-7 h-7 rounded-full bg-gradient-to-br from-blue-500 to-blue-600 flex items-center justify-center shrink-0">
<span class="text-xs font-bold text-white">{{ strtoupper(substr($warga->nama, 0, 1)) }}</span>
{{-- 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 class="text-sm font-medium text-gray-800">{{ $warga->nama }}</div>
<div class="text-xs text-gray-400">{{ $warga->pekerjaan }}</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>
</div>
</td>
<td class="px-4 py-3 text-xs font-mono text-gray-500">{{ $warga->nik }}</td>
<td class="px-4 py-3 text-xs text-gray-600">{{ $warga->dusun }}</td>
<td class="px-4 py-3 text-xs text-gray-600">Rp {{ number_format($warga->penghasilan, 0, ',', '.') }}</td>
<td class="px-4 py-3">
<div class="flex items-center gap-2">
<div class="flex-1 bg-gray-100 rounded-full h-1.5" style="min-width:60px">
<div class="h-1.5 rounded-full {{ $warga->probabilitas >= 70 ? 'bg-emerald-500' : ($warga->probabilitas >= 40 ? 'bg-amber-400' : 'bg-rose-400') }}"
style="width: {{ min($warga->probabilitas, 100) }}%"></div>
{{-- 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 class="text-xs font-bold {{ $warga->probabilitas >= 70 ? 'text-emerald-600' : ($warga->probabilitas >= 40 ? 'text-amber-600' : 'text-rose-500') }}">
<span style="font-size:12px;font-weight:800;color:{{ $sc }};white-space:nowrap;">
{{ number_format($warga->probabilitas, 1) }}%
</span>
</div>
</td>
<td class="px-4 py-3">
{{-- Status (asli) --}}
<td>
@if($warga->status == 'pending')
<span class="inline-flex items-center gap-1 px-2.5 py-0.5 text-xs font-semibold rounded-full bg-amber-100 text-amber-700"><span class="w-1.5 h-1.5 rounded-full bg-amber-500"></span>Pending</span>
<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 class="inline-flex items-center gap-1 px-2.5 py-0.5 text-xs font-semibold rounded-full bg-emerald-100 text-emerald-700"><span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>Diterima</span>
<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 class="inline-flex items-center gap-1 px-2.5 py-0.5 text-xs font-semibold rounded-full bg-rose-100 text-rose-700"><span class="w-1.5 h-1.5 rounded-full bg-rose-500"></span>Ditolak</span>
<span 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 class="inline-flex items-center gap-1 px-2.5 py-0.5 text-xs font-semibold rounded-full bg-blue-100 text-blue-700"><span class="w-1.5 h-1.5 rounded-full bg-blue-500"></span>{{ ucfirst($warga->status) }}</span>
<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>
@empty
<tr>
<td colspan="7" class="px-4 py-10 text-center">
<div class="w-10 h-10 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-2">
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
</svg>
<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>
<p class="text-xs text-gray-400">Belum ada data warga</p>
</td>
</tr>
@endforelse

View File

@ -49,6 +49,8 @@
.btn-detail{background:#f3f4f6;color:#374151;}
.btn-validasi{background:#eff6ff;color:#1d4ed8;}
.btn-kirim{background:#fffbeb;color:#b45309;}
.btn-reset{display:inline-flex;align-items:center;gap:6px;padding:9px 14px;background:#ef4444;color:#fff;font-size:12px;font-weight:700;border:none;border-radius:11px;cursor:pointer;box-shadow:0 2px 8px rgba(239,68,68,.22);white-space:nowrap;}
.btn-reset:hover{filter:brightness(.95);}
.info-chip{display:inline-flex;align-items:center;gap:5px;padding:4px 10px;border-radius:999px;font-size:10.5px;font-weight:700;background:#f8fafc;color:#475569;border:1px solid #e2e8f0;}
@ -56,9 +58,10 @@
.prob-bg{flex:1;height:5px;background:#f1f5f9;border-radius:99px;overflow:hidden;}
.prob-bar{height:100%;border-radius:99px;}
/* ── MODAL FIX — scroll handled by .m-wrap, not .m-body ── */
.m-backdrop{position:fixed;inset:0;z-index:40;background:rgba(0,0,0,.45);backdrop-filter:blur(3px);}
.m-wrap{position:fixed;inset:0;z-index:50;overflow-y:auto;pointer-events:none;display:flex;align-items:center;justify-content:center;padding:16px;}
.m-box{width:100%;max-width:620px;background:#fff;border-radius:22px;box-shadow:0 24px 64px rgba(0,0,0,.18);overflow:hidden;display:flex;flex-direction:column;max-height:90vh;pointer-events:auto;}
.m-wrap{position:fixed;inset:0;z-index:50;overflow-y:auto;pointer-events:none;display:flex;align-items:flex-start;justify-content:center;padding:24px 16px;}
.m-box{width:100%;max-width:620px;background:#fff;border-radius:22px;box-shadow:0 24px 64px rgba(0,0,0,.18);overflow:hidden;display:flex;flex-direction:column;pointer-events:auto;margin:auto;}
.mhero{position:relative;overflow:hidden;padding:18px 22px;background:linear-gradient(135deg,#1e40af 0%,#2563eb 55%,#3b82f6 100%);}
.mhero-b1{position:absolute;top:-20px;right:-20px;width:90px;height:90px;background:rgba(255,255,255,.08);border-radius:50%;}
.mhero-b2{position:absolute;bottom:-22px;left:35%;width:70px;height:70px;background:rgba(255,255,255,.06);border-radius:50%;}
@ -70,7 +73,7 @@
.m-close:hover{background:rgba(255,255,255,.28);}
.m-close svg{width:14px;height:14px;stroke:currentColor;}
.m-st-pill{margin-top:10px;position:relative;display:inline-flex;align-items:center;gap:5px;padding:4px 11px;border-radius:20px;background:rgba(255,255,255,.18);color:#fff;border:1px solid rgba(255,255,255,.25);font-size:11px;font-weight:700;}
.m-body{overflow-y:auto;flex:1;padding:14px;background:#f8fafc;display:flex;flex-direction:column;gap:10px;}
.m-body{overflow-y:visible;flex:1;padding:14px;background:#f8fafc;display:flex;flex-direction:column;gap:10px;}
.msec{background:#fff;border-radius:14px;border:1.5px solid #f1f5f9;overflow:hidden;}
.msec-h{padding:8px 14px;border-bottom:1px solid #f1f5f9;background:#fafafa;display:flex;align-items:center;gap:7px;}
.msec-h .bar{width:3px;height:13px;border-radius:3px;flex-shrink:0;}
@ -105,21 +108,39 @@
<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="{{ url()->current() }}" style="display:flex;gap:8px;align-items:center;">
<div class="sw" style="flex:1;">
<form method="GET" action="{{ url()->current() }}" style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
<div class="sw" style="flex:1;min-width:260px;">
<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, Nama, atau Email...">
</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;box-shadow:0 2px 8px rgba(37,99,235,.25);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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
Cari
</button>
@if(request()->filled('q'))
<a href="{{ url()->current() }}" style="display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;background:#f3f4f6;border-radius:10px;color:#6b7280;text-decoration:none;" title="Hapus">
<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 style="margin-top:12px;display:flex;justify-content:flex-end;">
<form method="POST"
action="{{ route('admin.data-warga.bersihkan') }}"
onsubmit="return confirm('Yakin ingin menghapus semua data warga aktif? Data pada laporan arsip tidak akan terhapus.');">
@csrf
@method('DELETE')
<button type="submit" class="btn-reset">
<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>
Bersihkan Data Warga
</button>
</form>
</div>
</div>
{{-- TABLE --}}
@ -448,7 +469,8 @@ function openDetailModal(id){
set('d_nik',d.nik||'-');set('d_no_kk',d.no_kk||'-');set('d_nama',d.nama_lengkap||'-');
set('d_jk',d.jenis_kelamin||'-');set('d_lahir',(d.tempat_lahir||'-')+', '+(d.tanggal_lahir||'-'));
set('d_usia',d.usia?d.usia+' tahun':'-');set('d_kawin',d.status_perkawinan||'-');
set('d_alamat',d.alamat||'-');set('d_dusun',d.desa||'-');
set('d_alamat', d.alamat || '-');
set('d_dusun', d.dusun || '-');
set('d_rt','RT '+String(d.rt||'-').padStart(3,'0'));set('d_aset',d.aset_kepemilikan||'-');
set('d_pekerjaan',d.pekerjaan||'-');
set('d_penghasilan','Rp '+Number(d.penghasilan||0).toLocaleString('id-ID'));

View File

@ -0,0 +1,192 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<title>Laporan Penerima BLT-DD Desa Ngerong</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: DejaVu Sans, sans-serif;
font-size: 10px;
color: #1e293b;
padding: 26px 30px;
}
/* ── Header ── */
.hd { display: table; width: 100%; border-bottom: 2px solid #2563eb; padding-bottom: 12px; margin-bottom: 14px; }
.hd-logo-cell { display: table-cell; vertical-align: middle; width: 48px; }
.hd-logo { width: 40px; height: 40px; background: #1e3a8a; border-radius: 9px; text-align: center; line-height: 40px; font-size: 18px; font-weight: 900; color: #fff; }
.hd-text-cell { display: table-cell; vertical-align: middle; padding-left: 11px; }
.hd-title { font-size: 13px; font-weight: 900; color: #0f172a; text-transform: uppercase; letter-spacing: .02em; }
.hd-sub { font-size: 8.5px; color: #64748b; margin-top: 2px; }
.hd-meta-cell { display: table-cell; vertical-align: middle; text-align: right; width: 150px; }
.hd-badge { display: inline-block; background: #eff6ff; border: 1px solid #bfdbfe; color: #1d4ed8; font-size: 7.5px; font-weight: 700; padding: 2px 7px; border-radius: 4px; text-transform: uppercase; letter-spacing: .05em; }
.hd-date { font-size: 7.5px; color: #94a3b8; margin-top: 3px; }
/* ── Summary ── */
.sum-wrap { display: table; width: 100%; margin-bottom: 14px; border: 1px solid #e2e8f0; border-radius: 7px; overflow: hidden; }
.sum-cell { display: table-cell; width: 25%; padding: 9px 12px; background: #f8fafc; border-right: 1px solid #e2e8f0; vertical-align: middle; }
.sum-cell:last-child { border-right: none; }
.sum-val { font-size: 15px; font-weight: 900; color: #0f172a; line-height: 1; }
.sum-val.blue { color: #2563eb; }
.sum-val.green { color: #16a34a; }
.sum-val.amber { color: #d97706; }
.sum-val.red { color: #e11d48; }
.sum-val.sm { font-size: 11px; }
.sum-lbl { font-size: 7px; color: #94a3b8; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; margin-top: 3px; }
/* ── Section label ── */
.sec-lbl { font-size: 8px; font-weight: 700; color: #2563eb; text-transform: uppercase; letter-spacing: .07em; margin-bottom: 6px; padding-left: 6px; border-left: 2.5px solid #2563eb; }
/* ── Table ── */
table.main { width: 100%; border-collapse: collapse; }
table.main thead tr { background: #1e40af; }
table.main thead th { padding: 7px 6px; text-align: center; font-size: 7.5px; font-weight: 700; color: #fff; text-transform: uppercase; letter-spacing: .04em; border: none; }
table.main thead th:first-child { border-radius: 4px 0 0 0; }
table.main thead th:last-child { border-radius: 0 4px 0 0; }
table.main tbody tr:nth-child(even) { background: #f8fafc; }
table.main tbody tr:nth-child(odd) { background: #fff; }
table.main tbody td { padding: 5.5px 6px; font-size: 9px; color: #374151; border: none; border-bottom: 1px solid #f1f5f9; vertical-align: middle; }
table.main tbody td.mid { text-align: center; }
table.main tbody td.right { text-align: right; }
/* ── Cell elements ── */
.av { display: inline-block; width: 18px; height: 18px; background: #2563eb; border-radius: 4px; text-align: center; line-height: 18px; font-size: 8px; font-weight: 700; color: #fff; vertical-align: middle; margin-right: 4px; }
.nik { font-family: 'Courier New', monospace; font-size: 7.5px; background: #f1f5f9; padding: 1px 4px; border-radius: 3px; color: #475569; }
.rt-b { display: inline-block; background: #eff6ff; border: 1px solid #bfdbfe; color: #1d4ed8; font-size: 7.5px; font-weight: 700; padding: 1px 5px; border-radius: 3px; }
.rp { font-weight: 700; color: #166534; background: #f0fdf4; border: 1px solid #bbf7d0; padding: 2px 5px; border-radius: 3px; display: inline-block; white-space: nowrap; font-size: 8.5px; }
/* Prob bar */
.prob-bg { display: inline-block; width: 28px; height: 3px; background: #e2e8f0; border-radius: 99px; vertical-align: middle; margin-right: 3px; overflow: hidden; }
/* ── Footer ── */
.foot { display: table; width: 100%; margin-top: 18px; }
.foot-l { display: table-cell; vertical-align: bottom; font-size: 7px; color: #94a3b8; }
.foot-l strong { color: #475569; }
.foot-r { display: table-cell; vertical-align: bottom; text-align: right; width: 160px; }
.ttd-box { border: 1px solid #e2e8f0; border-radius: 5px; padding: 7px 10px; text-align: center; background: #f8fafc; }
.ttd-line { width: 90px; border-bottom: 1px solid #334155; margin: 22px auto 3px; }
.ttd-name { font-size: 8px; font-weight: 700; color: #1e293b; }
.ttd-pos { font-size: 7px; color: #64748b; margin-top: 1px; }
.ttd-lbl { font-size: 7px; color: #94a3b8; }
</style>
</head>
<body>
@php
$totalPenerima = $laporans->count();
$totalDana = $laporans->sum('jumlah_bantuan') ?: ($totalPenerima * 300000);
$avgProb = $laporans->avg(function ($item) {
$p = $item->probability ?? 0;
return $p <= 1 ? $p * 100 : $p;
});
$periodeAktif = $laporans->pluck('periode_bantuan')->filter()->unique()->count();
@endphp
{{-- HEADER --}}
<div class="hd">
<div class="hd-logo-cell"><div class="hd-logo">S</div></div>
<div class="hd-text-cell">
<div class="hd-title">Laporan Penerima BLT-DD</div>
<div class="hd-sub">Desa Ngerong, Kec. Gempol, Kab. Pasuruan &mdash; Penerima final BLT Dana Desa</div>
</div>
<div class="hd-meta-cell">
<div class="hd-badge">Laporan Resmi</div>
<div class="hd-date">Dicetak: {{ now()->translatedFormat('d F Y') }}</div>
</div>
</div>
{{-- SUMMARY --}}
<div class="sum-wrap">
<div class="sum-cell">
<div class="sum-val blue">{{ $totalPenerima }}</div>
<div class="sum-lbl">Total Penerima</div>
</div>
<div class="sum-cell">
<div class="sum-val green sm">Rp {{ number_format($totalDana, 0, ',', '.') }}</div>
<div class="sum-lbl">Total Dana</div>
</div>
<div class="sum-cell">
<div class="sum-val amber">{{ number_format($avgProb, 1) }}%</div>
<div class="sum-lbl">Rata-rata Probabilitas</div>
</div>
<div class="sum-cell">
<div class="sum-val red">{{ $periodeAktif }}</div>
<div class="sum-lbl">Periode Aktif</div>
</div>
</div>
{{-- LABEL --}}
<div class="sec-lbl">Daftar Penerima Final</div>
{{-- TABLE --}}
<table class="main">
<thead>
<tr>
<th style="width:20px;">#</th>
<th style="text-align:left;">Nama</th>
<th>NIK</th>
<th>No KK</th>
<th>RT</th>
<th>Dusun</th>
<th>Probabilitas</th>
<th>Tgl Penetapan</th>
<th>Periode</th>
<th>Jumlah Bantuan</th>
</tr>
</thead>
<tbody>
@forelse($laporans as $index => $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');
$jml = $item->jumlah_bantuan ?? 300000;
@endphp
<tr>
<td class="mid" style="font-size:8px;color:#94a3b8;font-weight:700;">{{ $index + 1 }}</td>
<td><span class="av">{{ $init }}</span>{{ $nama }}</td>
<td class="mid"><span class="nik">{{ $item->nik ?? '-' }}</span></td>
<td class="mid"><span class="nik">{{ $item->no_kk ?? '-' }}</span></td>
<td class="mid"><span class="rt-b">RT {{ str_pad($item->nomor_rt ?? '0', 3, '0', STR_PAD_LEFT) }}</span></td>
<td style="white-space:nowrap;">{{ $item->nama_dusun ?? '-' }}</td>
<td class="mid">
<span class="prob-bg">
<span style="display:inline-block;width:{{ min($prob,100) }}%;height:3px;background:{{ $sc }};border-radius:99px;vertical-align:top;"></span>
</span>
<span style="font-size:8.5px;font-weight:700;color:{{ $sc }};">{{ number_format($prob, 1) }}%</span>
</td>
<td class="mid" style="white-space:nowrap;color:#64748b;">{{ $item->tanggal_penetapan?->format('d-m-Y') ?? '-' }}</td>
<td class="mid" style="white-space:nowrap;">{{ $item->periode_bantuan ?? '-' }}</td>
<td class="right"><span class="rp">Rp {{ number_format($jml, 0, ',', '.') }}</span></td>
</tr>
@empty
<tr>
<td colspan="10" class="mid" style="padding:28px;color:#94a3b8;font-weight:600;">Belum ada data penerima final.</td>
</tr>
@endforelse
</tbody>
</table>
{{-- FOOTER --}}
<div class="foot">
<div class="foot-l">
<div style="margin-bottom:2px;"><strong>SiBantuDes</strong> &mdash; Sistem Informasi Bantuan Langsung Tunai Dana Desa</div>
<div>Digenerate otomatis pada {{ now()->translatedFormat('d F Y, H:i') }} WIB</div>
<div style="margin-top:5px;color:#cbd5e1;">{{ $totalPenerima }} penerima &bull; Total Rp {{ number_format($totalDana, 0, ',', '.') }}</div>
</div>
<div class="foot-r">
<div class="ttd-box">
<div class="ttd-lbl">Mengetahui,</div>
<div class="ttd-line"></div>
<div class="ttd-name">Kepala Desa Ngerong</div>
<div class="ttd-pos">Kec. Gempol, Kab. Pasuruan</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -1,24 +0,0 @@
<x-app-layout>
@section('content')
<div class="space-y-6">
<div>
<h1 class="text-2xl font-bold text-gray-800">Laporan</h1>
<p class="text-sm text-gray-500">Rekap data penerima & status, siap untuk cetak/ekspor.</p>
</div>
<div class="bg-white rounded-xl shadow-sm p-6">
<p class="text-gray-700">
Placeholder Laporan. Biasanya isi:
</p>
<ul class="list-disc ml-5 mt-3 text-sm text-gray-600 space-y-1">
<li>Rekap per dusun</li>
<li>Rekap diterima/ditolak</li>
<li>Export PDF/Excel</li>
</ul>
</div>
</div>
</x-app-layout>

View File

@ -0,0 +1,351 @@
<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>
<div class="lp-wrap">
<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>
@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>
{{ session('success') }}
</div>
@endif
@php
$totalPenerima = $laporans->count();
$totalBantuan = $laporans->sum('jumlah_bantuan');
$avgProb = $laporans->avg(function($i){
$p = $i->probability ?? 0;
return $p <= 1 ? $p * 100 : $p;
});
$periodes = $laporans->pluck('periode_bantuan')->filter()->unique()->count();
@endphp
<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>
</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>
</div>
<div class="sum-val sm">Rp {{ number_format($totalBantuan, 0, ',', '.') }}</div>
<div class="sum-lbl">Total Dana Tersalurkan</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>
</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>
<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>
</div>
<div class="sum-val">{{ $periodes }}</div>
<div class="sum-lbl">Periode Aktif</div>
</div>
</div>
<div class="lp-card">
<div class="sec-hd">
<div class="sec-hd-l">
<div class="sec-bar"></div>
<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;">
@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>
</form>
<span class="exp-meta">{{ $totalPenerima }} penerima terdaftar</span>
</div>
</div>
<div class="lp-card">
<div class="sec-hd">
<div class="sec-hd-l">
<div class="sec-bar"></div>
<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">
@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
</button>
</form>
@if(isset($laporanPubliks) && $laporanPubliks->count() > 0)
<div class="pub-list">
@foreach($laporanPubliks as $pdf)
<div class="pub-item">
<div>
<div class="pub-title">{{ $pdf->judul }}</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;">
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')
<button type="submit" class="btn-gray">Hapus</button>
</form>
</div>
</div>
@endforeach
</div>
@endif
</div>
</div>
<div class="lp-card">
<div class="sec-hd">
<div class="sec-hd-l">
<div class="sec-bar"></div>
<span class="sec-title">Daftar Penerima Final</span>
</div>
<span class="sec-count">{{ $totalPenerima }} data</span>
</div>
<div class="lp-tbl-wrap">
<table class="lp-tbl">
<thead>
<tr>
<th>#</th>
<th>Nama</th>
<th>NIK</th>
<th>No KK</th>
<th>RT</th>
<th>Dusun</th>
<th>Pekerjaan</th>
<th>Penghasilan</th>
<th>Tanggungan</th>
<th>Aset</th>
<th>Bantuan Lain</th>
<th>Usia</th>
<th>Probabilitas</th>
<th>Status</th>
<th>Tgl Penetapan</th>
<th>Periode</th>
<th>Jumlah Bantuan</th>
</tr>
</thead>
<tbody>
@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');
$bantuanLain = strtolower($item->bantuan_lain ?? 'tidak');
@endphp
<tr>
<td>{{ $index + 1 }}</td>
<td>
<div class="name-cell">
<div class="av">{{ $init }}</div>
<span class="name-txt">{{ $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="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')
<span class="bantuan-ya">Ya</span>
@else
<span class="bantuan-tdk">Tidak</span>
@endif
</td>
<td>{{ $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>
</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 ?? '-') }}
</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>
</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>
@endforelse
</tbody>
</table>
</div>
<div class="tbl-foot">
<span class="credit">SiBantuDes · Kelurahan Ngerong</span>
</div>
</div>
</div>
</x-app-layout>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -7,17 +7,15 @@
<title>{{ config('app.name', 'Laravel') }}</title>
<!-- Fonts -->
<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" />
<!-- Scripts -->
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="font-sans antialiased">
<div class="min-h-screen bg-gray-100">
{{-- BAR TAB + PROFIL (sticky) --}}
<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">
@ -26,11 +24,9 @@
$role = Auth::user()->role ?? null;
@endphp
{{-- TAB MENU (fullwidth) --}}
<div class="flex items-center gap-2 flex-1">
@if($role === 'admin')
{{-- ADMIN MENU --}}
<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' }}">
@ -62,7 +58,6 @@ class="flex-1 text-center px-4 py-2 rounded-xl text-sm font-medium border
</a>
@else
{{-- RT MENU (default) --}}
<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' }}">
@ -71,26 +66,25 @@ class="flex-1 text-center px-4 py-2 rounded-xl text-sm font-medium border
<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('calon-penerima.create') ? 'bg-blue-600 text-white border-blue-600' : 'bg-white text-gray-700 border-gray-200 hover:bg-gray-50' }}">
{{ 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>
<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('calon-penerima.index') ? 'bg-blue-600 text-white border-blue-600' : 'bg-white text-gray-700 border-gray-200 hover:bg-gray-50' }}">
{{ 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>
<a href="{{ route('rt.calon-penerima.index') }}"
<a href="{{ route('rt.laporan.index') }}"
class="flex-1 text-center px-4 py-2 rounded-xl text-sm font-medium border
{{ false ? 'bg-blue-600 text-white border-blue-600' : 'bg-white text-gray-700 border-gray-200 hover:bg-gray-50' }}">
Status Pengajuan
{{ 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>
{{-- PROFIL (kanan) --}}
<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">
@ -114,7 +108,6 @@ class="flex-1 text-center px-4 py-2 rounded-xl text-sm font-medium border
</div>
</div>
<!-- Page Heading -->
@isset($header)
<header class="bg-white shadow">
<div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
@ -123,7 +116,6 @@ class="flex-1 text-center px-4 py-2 rounded-xl text-sm font-medium border
</header>
@endisset
<!-- Page Content -->
<main>
<div class="py-8">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">

View File

@ -1,8 +1,12 @@
<nav x-data="{ open: false }" class="bg-white border-b border-gray-100">
<!-- Primary Navigation Menu -->
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<!-- LEFT MENU -->
<div class="flex">
<!-- Logo -->
<div class="shrink-0 flex items-center">
<a href="{{ route('dashboard') }}">
@ -10,91 +14,109 @@
</a>
</div>
{{-- <!-- Navigation Links -->
<!-- MENU -->
<div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
<x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
{{ __('Dashboard') }}
</x-nav-link>
</div>
</div> --}}
<!-- Settings Dropdown -->
<x-nav-link :href="route('rt.dashboard')" :active="request()->routeIs('rt.dashboard')">
Dashboard
</x-nav-link>
<x-nav-link :href="route('rt.calon-penerima.index')" :active="request()->routeIs('rt.calon-penerima.*')">
Pendataan
</x-nav-link>
<x-nav-link :href="route('rt.riwayat.index')" :active="request()->routeIs('rt.riwayat.*')">
Riwayat
</x-nav-link>
<x-nav-link :href="route('rt.laporan.index')" :active="request()->routeIs('rt.laporan.*')">
Laporan
</x-nav-link>
</div>
</div>
<!-- USER DROPDOWN -->
<div class="hidden sm:flex sm:items-center sm:ms-6">
<x-dropdown align="right" width="48">
<x-slot name="trigger">
<button class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none transition ease-in-out duration-150">
<button class="inline-flex items-center px-3 py-2 text-sm font-medium rounded-md text-gray-600 hover:text-gray-800 focus:outline-none">
<div>{{ Auth::user()->name }}</div>
<div class="ms-1">
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
<svg class="fill-current h-4 w-4" viewBox="0 0 20 20">
<path fill-rule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4
4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clip-rule="evenodd"/>
</svg>
</div>
</button>
</x-slot>
<x-slot name="content">
<x-dropdown-link :href="route('profile.edit')">
{{ __('Profile') }}
Profile
</x-dropdown-link>
<!-- Authentication -->
<form method="POST" action="{{ route('logout') }}">
@csrf
<x-dropdown-link :href="route('logout')"
onclick="event.preventDefault();
this.closest('form').submit();">
{{ __('Log Out') }}
Log Out
</x-dropdown-link>
</form>
</x-slot>
</x-dropdown>
</div>
<!-- Hamburger -->
<!-- HAMBURGER -->
<div class="-me-2 flex items-center sm:hidden">
<button @click="open = ! open" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 focus:text-gray-500 transition duration-150 ease-in-out">
<button @click="open = ! open"
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100">
<svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
<path :class="{'hidden': open, 'inline-flex': ! open }" class="inline-flex" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
<path :class="{'hidden': ! open, 'inline-flex': open }" class="hidden" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
<path :class="{'hidden': open, 'inline-flex': ! open }"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"/>
<path :class="{'hidden': ! open, 'inline-flex': open }"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
</div>
</div>
<!-- Responsive Navigation Menu -->
<div :class="{'block': open, 'hidden': ! open}" class="hidden sm:hidden">
<div class="pt-2 pb-3 space-y-1">
<x-responsive-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
{{ __('Dashboard') }}
</x-responsive-nav-link>
</div>
<!-- Responsive Settings Options -->
<div class="pt-4 pb-1 border-t border-gray-200">
<div class="px-4">
<div class="font-medium text-base text-gray-800">{{ Auth::user()->name }}</div>
<div class="font-medium text-sm text-gray-500">{{ Auth::user()->email }}</div>
</div>
<div class="mt-3 space-y-1">
<x-responsive-nav-link :href="route('profile.edit')">
{{ __('Profile') }}
</x-responsive-nav-link>
<!-- Authentication -->
<form method="POST" action="{{ route('logout') }}">
@csrf
<x-responsive-nav-link :href="route('logout')"
onclick="event.preventDefault();
this.closest('form').submit();">
{{ __('Log Out') }}
</x-responsive-nav-link>
</form>
</div>
</div>
</div>
</nav>

View File

@ -1,16 +1,14 @@
<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">Dashboard</h2>
<p class="text-sm text-gray-500 mt-0.5">
Ringkasan data pendataran warga {{ Auth::user()->rt->dusun->nama_dusun ?? "" }}
<h2 style="font-size:17px;font-weight:900;color:#0f172a;line-height:1.2;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 ?? '' }}
</p>
</div>
{{-- Tanggal di header --}}
<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">
<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">
<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">
@ -35,190 +33,222 @@ function updateClock() {
</script>
</x-slot>
<div class="space-y-4">
@php
$avgProb = \App\Models\PrediksiKelayakan::whereHas('calonPenerima', function ($q) {
$q->where('user_id', Auth::id());
})->avg('probability');
{{-- Greeting kecil --}}
<div class="flex items-center gap-3 bg-white border border-gray-100 rounded-xl px-4 py-3 shadow-sm">
<div class="w-8 h-8 rounded-full bg-blue-600 flex items-center justify-center text-white text-sm font-bold shrink-0">
$avgProb = $avgProb ?? 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%;}
.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: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 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:hover{background:#f0f7ff;}
.recent-item:last-child{border-bottom:none;}
</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);">
<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 class="text-sm font-medium text-gray-800">
Selamat datang, <span class="text-blue-600">{{ Auth::user()->name ?? 'Pengguna' }}</span> 👋
<p style="font-size:13.5px;font-weight:700;color:#0f172a;">
Selamat datang, <span style="color:#2563eb;">{{ Auth::user()->name ?? 'Pengguna' }}</span> 👋
</p>
<p class="text-xs text-gray-400">Berikut ringkasan data terkini yang perlu Anda pantau.</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>
{{-- Statistik Cards --}}
<div class="grid grid-cols-2 lg:grid-cols-4 gap-3">
{{-- STAT CARDS (asli tidak diubah) --}}
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:12px;">
<div class="relative bg-blue-600 text-white rounded-xl p-4 flex items-center justify-between overflow-hidden shadow-md shadow-blue-200">
<div class="absolute -right-3 -top-3 w-20 h-20 bg-white/10 rounded-full"></div>
<div class="absolute -right-1 -bottom-4 w-14 h-14 bg-white/10 rounded-full"></div>
<div class="relative z-10">
<p class="text-xs font-medium text-blue-100">Total Warga</p>
<p class="text-3xl font-bold mt-0.5">{{ $stats['total_input'] }}</p>
<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 class="relative z-10 w-11 h-11 bg-white/20 rounded-lg flex items-center justify-center shrink-0">
<svg class="w-6 h-6" 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 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 class="relative bg-amber-500 text-white rounded-xl p-4 flex items-center justify-between overflow-hidden shadow-md shadow-amber-200">
<div class="absolute -right-3 -top-3 w-20 h-20 bg-white/10 rounded-full"></div>
<div class="absolute -right-1 -bottom-4 w-14 h-14 bg-white/10 rounded-full"></div>
<div class="relative z-10">
<p class="text-xs font-medium text-amber-100">Pending</p>
<p class="text-3xl font-bold mt-0.5">{{ $stats['pending'] }}</p>
<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>
<div class="relative z-10 w-11 h-11 bg-white/20 rounded-lg flex items-center justify-center shrink-0">
<svg class="w-6 h-6" fill="none" stroke="currentColor" 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 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 class="relative bg-emerald-500 text-white rounded-xl p-4 flex items-center justify-between overflow-hidden shadow-md shadow-emerald-200">
<div class="absolute -right-3 -top-3 w-20 h-20 bg-white/10 rounded-full"></div>
<div class="absolute -right-1 -bottom-4 w-14 h-14 bg-white/10 rounded-full"></div>
<div class="relative z-10">
<p class="text-xs font-medium text-emerald-100">Diterima</p>
<p class="text-3xl font-bold mt-0.5">{{ $stats['disetujui'] }}</p>
<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 class="relative z-10 w-11 h-11 bg-white/20 rounded-lg flex items-center justify-center shrink-0">
<svg class="w-6 h-6" 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>
<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 class="relative bg-rose-500 text-white rounded-xl p-4 flex items-center justify-between overflow-hidden shadow-md shadow-rose-200">
<div class="absolute -right-3 -top-3 w-20 h-20 bg-white/10 rounded-full"></div>
<div class="absolute -right-1 -bottom-4 w-14 h-14 bg-white/10 rounded-full"></div>
<div class="relative z-10">
<p class="text-xs font-medium text-rose-100">Ditolak</p>
<p class="text-3xl font-bold mt-0.5">{{ $stats['ditolak'] }}</p>
<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 class="relative z-10 w-11 h-11 bg-white/20 rounded-lg flex items-center justify-center shrink-0">
<svg class="w-6 h-6" fill="none" stroke="currentColor" 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 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>
{{-- Bottom Section --}}
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
{{-- GRID: Pendataan Terbaru + Aksi Cepat (logika asli 100% tidak diubah) --}}
<div style="display:grid;grid-template-columns:2fr 1fr;gap:14px;">
{{-- Pendataran Terbaru --}}
<div class="lg:col-span-2 bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<div class="px-4 py-3 border-b border-gray-100 flex items-center justify-between">
<div class="flex items-center gap-2">
<div class="w-1 h-4 bg-blue-600 rounded-full"></div>
<h3 class="text-sm font-semibold text-gray-800">Pendataran Terbaru</h3>
{{-- 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="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;">Pendataan Terbaru</h3>
</div>
<span class="text-xs text-gray-400 bg-gray-50 border border-gray-100 px-2.5 py-0.5 rounded-full">Live</span>
</div>
<div class="divide-y divide-gray-50">
@forelse($recentCalonPenerima as $calon)
<div class="flex items-center justify-between px-4 py-3 hover:bg-blue-50/40 transition-colors duration-150">
<div class="flex items-center space-x-3">
<div class="w-8 h-8 bg-gradient-to-br from-blue-500 to-blue-600 rounded-full flex items-center justify-center shadow-sm shrink-0">
<span class="text-xs font-bold text-white">
{{ strtoupper(substr($calon->nama_lengkap, 0, 1)) }}
<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>
</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 class="text-sm font-medium text-gray-800">{{ $calon->nama_lengkap }}</p>
<p class="text-xs text-gray-400">NIK: {{ $calon->nik }}</p>
<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>
<div class="text-right">
<span class="px-2.5 py-0.5 text-xs font-semibold rounded-full
@if($calon->status_verifikasi == 'pending') bg-amber-100 text-amber-700
@elseif($calon->status_verifikasi == 'disetujui') bg-emerald-100 text-emerald-700
@else bg-rose-100 text-rose-700 @endif">
<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>
<p class="text-xs text-gray-400 mt-1">{{ $calon->created_at->diffForHumans() }}</p>
@endif
<p style="font-size:10px;color:#94a3b8;margin-top:3px;">{{ $calon->created_at->diffForHumans() }}</p>
</div>
</div>
@empty
<div class="px-4 py-8 text-center">
<div class="w-10 h-10 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-2">
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 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 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 class="text-xs text-gray-400">Belum ada data pendataran</p>
<p style="font-size:12.5px;font-weight:600;color:#94a3b8;">Belum ada data pendataan</p>
</div>
@endforelse
</div>
</div>
{{-- Aksi Cepat --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-4">
<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-800">Aksi Cepat</h3>
{{-- Aksi Cepat (asli tidak diubah) --}}
<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>
<h3 style="font-size:13px;font-weight:700;color:#1e293b;">Aksi Cepat</h3>
</div>
<div class="space-y-2.5">
<a href="{{ route('rt.calon-penerima.create') }}"
class="group flex items-center space-x-3 p-3 bg-blue-50 hover:bg-blue-600 border border-blue-100 hover:border-blue-600 rounded-xl transition-all duration-200">
<div class="w-9 h-9 bg-blue-600 group-hover:bg-white/20 rounded-lg flex items-center justify-center shrink-0 transition-colors duration-200">
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" 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 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 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 class="text-sm font-semibold text-gray-800 group-hover:text-white transition-colors">Daftarkan Warga Baru</p>
<p class="text-xs text-gray-500 group-hover:text-blue-100 transition-colors">Tambah data calon penerima</p>
<p style="font-size:12.5px;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>
<a href="{{ route('rt.calon-penerima.index') }}"
class="group flex items-center space-x-3 p-3 bg-emerald-50 hover:bg-emerald-600 border border-emerald-100 hover:border-emerald-600 rounded-xl transition-all duration-200">
<div class="w-9 h-9 bg-emerald-600 group-hover:bg-white/20 rounded-lg flex items-center justify-center shrink-0 transition-colors duration-200">
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" 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>
{{-- Lihat Laporan (asli) --}}
<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 class="text-sm font-semibold text-gray-800 group-hover:text-white transition-colors">Cek Status</p>
<p class="text-xs text-gray-500 group-hover:text-emerald-100 transition-colors">Lihat status pengajuan</p>
<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>
</a>
{{-- Rata-rata Probabilitas --}}
<div class="p-3 bg-gradient-to-br from-blue-600 to-blue-500 rounded-xl shadow-sm shadow-blue-200">
<div class="flex items-center justify-between mb-0.5">
<p class="text-xs font-semibold text-white">Rata-rata Probabilitas</p>
<svg class="w-3.5 h-3.5 text-blue-200" fill="none" stroke="currentColor" 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>
{{-- 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="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>
@php
$avgProb = \App\Models\PrediksiKelayakan::whereHas('calonPenerima', function($q) {
$q->where('user_id', Auth::id());
})->avg('probability');
@endphp
<p class="text-2xl font-bold text-white mt-0.5">
{{ number_format($avgProb ?? 0, 2) }}<span class="text-sm text-blue-200">%</span>
<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>
<p class="text-xs text-blue-200 mt-0.5">Berdasarkan prediksi kelayakan</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>
<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>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: .5; transform: scale(.85); }
}
</style>
</x-app-layout>

View File

@ -0,0 +1,249 @@
<x-app-layout>
<style>
.lk-wrap{max-width:1200px;margin:0 auto;padding:24px 20px 40px;}
/* 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;}
/* 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;}
/* 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;}
/* 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;}
/* 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;}
/* 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();
@endphp
<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>
</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>
</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>
</div>
<div class="sum-val" style="color:#e11d48;">{{ $ditolak }}</div>
<div class="sum-lbl">Ditolak</div>
</div>
</div>
{{-- ── TABLE ── --}}
<div class="lk-card">
<div class="sec-hd">
<div class="sec-hd-l">
<div class="sec-bar"></div>
<span class="sec-title">Daftar Hasil Verifikasi Warga</span>
</div>
<span class="sec-count">{{ $total }} data</span>
</div>
<div class="lk-tbl-wrap">
<table class="lk-tbl">
<thead>
<tr>
<th style="width:32px;">#</th>
<th>Nama</th>
<th>NIK</th>
<th>RT</th>
<th>Dusun</th>
<th>Pekerjaan</th>
<th>Penghasilan</th>
<th>Tanggungan</th>
<th>Aset</th>
<th>Bantuan Lain</th>
<th>Usia</th>
<th>Probabilitas</th>
<th>Status</th>
</tr>
</thead>
<tbody>
@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');
$colors = ['#3b82f6','#8b5cf6','#ec4899','#f59e0b','#10b981','#ef4444','#06b6d4'];
$avColor = $colors[ord($init) % count($colors)];
$diterima = $item->status_verifikasi === 'disetujui';
@endphp
<tr>
<td style="text-align:center;font-size:10.5px;color:#94a3b8;font-weight:700;">{{ $i + 1 }}</td>
<td>
<div class="name-cell">
<div class="av" style="background:{{ $avColor }};">{{ $init }}</div>
<div>
<div style="font-size:12.5px;font-weight:700;color:#0f172a;">{{ $nama }}</div>
</div>
</div>
</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;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')
<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>
<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>
</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>
@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
</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>
@endforelse
</tbody>
</table>
</div>
<div class="tbl-foot">
<span class="credit">SiBantuDes &middot; Kelurahan Ngerong</span>
</div>
</div>
</div>
</x-app-layout>

View File

@ -7,21 +7,23 @@
use App\Models\CalonPenerima;
use App\Models\Dusun;
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;
/*
|--------------------------------------------------------------------------
| ROOT
| ROOT / LANDING PAGE PUBLIK
|--------------------------------------------------------------------------
*/
Route::get('/', function () {
return redirect()->route('login');
});
Route::get('/', [LandingPageController::class, 'index'])->name('landing');
/*
|--------------------------------------------------------------------------
@ -59,6 +61,7 @@
// DASHBOARD ADMIN
Route::get('/dashboard', function (Request $request) {
$user = $request->user();
if (!$user || $user->role !== 'admin') {
abort(403, 'Akses ditolak: hanya Admin.');
}
@ -66,10 +69,13 @@
$visibleStatuses = ['terkirim', 'sedang_validasi', 'selesai'];
$totalWarga = CalonPenerima::whereIn('tracking_status', $visibleStatuses)->count();
$totalDusun = Dusun::count();
$totalPenerima = CalonPenerima::whereIn('tracking_status', $visibleStatuses)
->where('status_verifikasi', 'disetujui')
->count();
$totalPending = CalonPenerima::whereIn('tracking_status', $visibleStatuses)
->where('status_verifikasi', 'pending')
->count();
@ -122,6 +128,7 @@
// DATA WARGA + SEARCH
Route::get('/data-warga', function (Request $request) {
$user = $request->user();
if (!$user || $user->role !== 'admin') {
abort(403, 'Akses ditolak: hanya Admin.');
}
@ -150,9 +157,30 @@
return view('admin.data-warga', compact('dusuns', 'wargas'));
})->name('data-warga');
// TAMBAHAN: BERSIHKAN DATA WARGA AKTIF
Route::delete('/data-warga/bersihkan', 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();
// hapus data pengajuan aktif
DB::table('calon_penerimas')->delete();
});
return redirect()->route('admin.data-warga')
->with('success', 'Semua data warga aktif berhasil dibersihkan. Arsip laporan tetap aman.');
})->name('data-warga.bersihkan');
// MULAI VALIDASI
Route::post('/data-warga/{id}/mulai-validasi', function (Request $request, $id) {
$user = $request->user();
if (!$user || $user->role !== 'admin') {
abort(403, 'Akses ditolak: hanya Admin.');
}
@ -174,6 +202,7 @@
// KIRIM HASIL VALIDASI KE RT
Route::post('/data-warga/{id}/selesai-validasi', function (Request $request, $id) {
$user = $request->user();
if (!$user || $user->role !== 'admin') {
abort(403, 'Akses ditolak: hanya Admin.');
}
@ -195,7 +224,7 @@
return back()->with('success', 'Hasil validasi berhasil dikirim ke RT.');
})->name('data-warga.selesai-validasi');
// DATA AKUN (AdminController)
// 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');
Route::post('/data-akun/{id}/toggle-aktif', [AdminController::class, 'toggleAktif'])->name('data-akun.toggle-aktif');
@ -206,8 +235,15 @@
Route::post('/filterisasi/tetapkan', [FilterisasiController::class, 'tetapkan'])->name('filterisasi.tetapkan');
Route::post('/filterisasi/reset', [FilterisasiController::class, 'resetDusun'])->name('filterisasi.reset');
// LAPORAN
Route::get('/laporan', fn () => view('admin.laporan'))->name('laporan');
// 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/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'])
->name('laporan.bersihkan-riwayat-selesai');
});
/*
@ -215,7 +251,7 @@
| RT ROUTES (RT ONLY)
|--------------------------------------------------------------------------
*/
Route::middleware(['auth', 'verified'])
Route::middleware(['auth', 'verified', 'is_active'])
->prefix('rt')
->name('rt.')
->group(function () {
@ -223,6 +259,9 @@
// DASHBOARD RT
Route::get('/dashboard', [RtDashboardController::class, 'index'])->name('dashboard');
// LAPORAN RT
Route::get('/laporan', [RtLaporanController::class, 'index'])->name('laporan.index');
// AJUKAN DATA KE ADMIN
Route::patch('/calon-penerima/{calonPenerima}/ajukan', [RtCalonPenerimaController::class, 'ajukan'])
->name('calon-penerima.ajukan');